diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index 6a4479e75a2..6ce92871c98 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -1988,7 +1988,7 @@ export interface DbBlobResponse extends DbResponse { // @internal (undocumented) export interface DbQueryConfig { - autoShutdowWhenIdealForSeconds?: number; + autoShutdowWhenIdlelForSeconds?: number; // (undocumented) doNotUsePrimaryConnToPrepare?: boolean; // (undocumented) diff --git a/core/backend/src/test/ecdb/ConcurrentQueryLoad.test.ts b/core/backend/src/test/ecdb/ConcurrentQueryLoad.test.ts index b37ddd81cf0..b57d93272bf 100644 --- a/core/backend/src/test/ecdb/ConcurrentQueryLoad.test.ts +++ b/core/backend/src/test/ecdb/ConcurrentQueryLoad.test.ts @@ -120,23 +120,24 @@ describe.skip("ConcurrentQueryLoad", () => { it("should run", async () => { Logger.initializeToConsole(); Logger.setLevel("ECDb.ConcurrentQuery", LogLevel.Trace); - // const defaultConfig: { + // { // workerThreads: 4, // requestQueueSize: 2000, // ignorePriority: false, // ignoreDelay: true, // doNotUsePrimaryConnToPrepare: false, - // autoShutdowWhenIdealForSeconds: 120, + // autoShutdowWhenIdlelForSeconds: 300, // statementCacheSizePerWorker: 40, // monitorPollInterval: 1000, // memoryMapFileSize: 0, + // allowTestingArgs: false, // globalQuota: { time: 60, memory: 8388608 } - // }; + // } const senario: ISenario = { name: "ConcurrentQueryLoad", config: { - autoShutdowWhenIdealForSeconds: 10, + }, totalBatches: 1, taskPerBatch: 1, diff --git a/core/backend/src/test/ecdb/ECSqlQuery.test.ts b/core/backend/src/test/ecdb/ECSqlQuery.test.ts index 41bcbe9ec0b..e2eeb30360c 100644 --- a/core/backend/src/test/ecdb/ECSqlQuery.test.ts +++ b/core/backend/src/test/ecdb/ECSqlQuery.test.ts @@ -393,7 +393,60 @@ describe("ECSql Query", () => { assert.isTrue(hasRow, "imodel1.query() must return latest one row"); } }); + // new new addon build + it("ecsql interrupt check", async () => { + let cancelled = 0; + let successful = 0; + let rowCount = 0; + try { + ConcurrentQuery.shutdown(imodel1[_nativeDb]); + ConcurrentQuery.resetConfig(imodel1[_nativeDb], { allowTestingArgs: true }); + const scheduleQuery = async () => { + return new Promise(async (resolve, reject) => { + try { + const options = new QueryOptionsBuilder(); + options.setTestingArgs({ interrupt: true }); + options.setDelay(1000); + const reader = imodel1.createQueryReader(` + WITH sequence(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM sequence WHERE n < 10000 + ) + SELECT COUNT(*) + FROM bis.SpatialIndex i, sequence s`, undefined, options.getOptions()); + while (await reader.step()) { + rowCount++; + } + successful++; + resolve(); + } catch (err: any) { + // we expect query to be cancelled + if (err.errorNumber === DbResult.BE_SQLITE_INTERRUPT) { + cancelled++; + resolve(); + } else { + reject(new Error("rejected")); + } + } + }); + }; + const queries = []; + for (let i = 0; i < 100; i++) { + queries.push(scheduleQuery()); + } + + await Promise.all(queries); + // We expect at least one query to be cancelled + assert.equal(successful, 100, "success should be 100"); + assert.equal(rowCount, 100, "expect 100 rows"); + assert.isAtLeast(cancelled, 0, "should not have any cancelled query"); + } finally { + ConcurrentQuery.shutdown(imodel1[_nativeDb]); + ConcurrentQuery.resetConfig(imodel1[_nativeDb]); + } + }); // new new addon build it("ecsql with blob", async () => { let rows = await executeQuery(imodel1, "SELECT ECInstanceId,GeometryStream FROM bis.GeometricElement3d WHERE GeometryStream IS NOT NULL LIMIT 1"); diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index ae0a89f1135..741f9053639 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -115,6 +115,10 @@ export interface BaseReaderOptions { * concurrent query is configure to honour it. */ delay?: number; + /** + * @internal + */ + testingArgs?: TestingArgs; } /** @@ -252,6 +256,16 @@ export class QueryOptionsBuilder { this._options.delay = val; return this; } + /** + * @internal + * Use for testing internal logic. This parameter is ignored by default unless concurrent query is configure to not ignore it. + * @param val Testing arguments. + * @returns @type QueryOptionsBuilder for fluent interface. + */ + public setTestingArgs(val: TestingArgs) { + this._options.testingArgs = val; + return this; + } } /** @beta */ export class BlobOptionsBuilder { @@ -674,6 +688,11 @@ export enum DbResponseStatus { Error_BlobIO_OutOfRange = Error + 6, /* range specified is invalid based on size of blob.*/ } +/** @internal */ +export interface TestingArgs { + interrupt?: boolean +} + /** @internal */ export enum DbValueFormat { ECSqlNames = 0, @@ -683,6 +702,7 @@ export enum DbValueFormat { /** @internal */ export interface DbRequest extends BaseReaderOptions { kind?: DbRequestKind; + testingArgs?: TestingArgs } /** @internal */ @@ -753,11 +773,13 @@ export interface DbQueryConfig { workerThreads?: number; doNotUsePrimaryConnToPrepare?: boolean; /** After no activity for given time concurrenty query will automatically shutdown */ - autoShutdowWhenIdealForSeconds?: number; + autoShutdowWhenIdlelForSeconds?: number; /** Maximum number of statement cache per worker. Default to 40 */ statementCacheSizePerWorker?: number; /* Monitor poll interval in milliseconds. Its responsable for cancelling queries that pass quota. It can be set between 1000 and Max time quota for query */ monitorPollInterval?: number; /** Set memory map io for each worker connection size in bytes. Default to zero mean do not use mmap io */ memoryMapFileSize?: number; + /** Used by test to simulate certain test cases. Its is false by default. */ + allowTestingArgs?: boolean; }