velocious 1.0.714 → 1.0.715
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/background-jobs/local-store.js +3 -0
- package/build/background-jobs/store.js +77 -1
- package/build/background-jobs/types.js +2 -0
- package/build/database/drivers/base.js +60 -0
- package/build/database/drivers/mssql/index.js +59 -0
- package/build/database/drivers/mysql/index.js +51 -0
- package/build/database/drivers/pgsql/index.js +61 -0
- package/build/database/initializer-from-require-context.js +31 -7
- package/build/database/schema-metadata-prefetcher.js +108 -0
- package/build/src/background-jobs/local-store.d.ts.map +1 -1
- package/build/src/background-jobs/local-store.js +4 -1
- package/build/src/background-jobs/store.d.ts +24 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +66 -2
- package/build/src/background-jobs/types.d.ts +10 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +3 -1
- package/build/src/database/drivers/base.d.ts +29 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +55 -1
- package/build/src/database/drivers/mssql/index.d.ts +9 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +54 -1
- package/build/src/database/drivers/mysql/index.d.ts +13 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +46 -1
- package/build/src/database/drivers/pgsql/index.d.ts +12 -2
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +56 -1
- package/build/src/database/initializer-from-require-context.d.ts.map +1 -1
- package/build/src/database/initializer-from-require-context.js +26 -7
- package/build/src/database/schema-metadata-prefetcher.d.ts +53 -0
- package/build/src/database/schema-metadata-prefetcher.d.ts.map +1 -0
- package/build/src/database/schema-metadata-prefetcher.js +95 -0
- package/build/src/utils/bounded-task-runner.d.ts +26 -0
- package/build/src/utils/bounded-task-runner.d.ts.map +1 -0
- package/build/src/utils/bounded-task-runner.js +44 -0
- package/build/utils/bounded-task-runner.js +51 -0
- package/package.json +1 -1
- package/src/background-jobs/local-store.js +3 -0
- package/src/background-jobs/store.js +77 -1
- package/src/background-jobs/types.js +2 -0
- package/src/database/drivers/base.js +60 -0
- package/src/database/drivers/mssql/index.js +59 -0
- package/src/database/drivers/mysql/index.js +51 -0
- package/src/database/drivers/pgsql/index.js +61 -0
- package/src/database/initializer-from-require-context.js +31 -7
- package/src/database/schema-metadata-prefetcher.js +108 -0
- package/src/utils/bounded-task-runner.js +51 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Runs one callback per item with a bounded number of callbacks in flight.
|
|
4
|
+
*
|
|
5
|
+
* Used where a startup pass has to touch many independent records (model
|
|
6
|
+
* metadata, per-table schema reads) and running them sequentially multiplies the
|
|
7
|
+
* round-trip latency by the item count, while running them all at once would
|
|
8
|
+
* open an unbounded number of database connections.
|
|
9
|
+
*/
|
|
10
|
+
export default class BoundedTaskRunner {
|
|
11
|
+
/**
|
|
12
|
+
* Runs the worker for every item, keeping at most `concurrency` callbacks in
|
|
13
|
+
* flight and returning the results in input order.
|
|
14
|
+
* @template T, R
|
|
15
|
+
* @param {object} args - Run args.
|
|
16
|
+
* @param {number} [args.concurrency] - Maximum callbacks in flight.
|
|
17
|
+
* @param {T[]} args.items - Items to process.
|
|
18
|
+
* @param {(item: T, index: number) => Promise<R>} args.worker - Callback per item.
|
|
19
|
+
* @returns {Promise<R[]>} - Results in input order.
|
|
20
|
+
*/
|
|
21
|
+
static async run({ concurrency = 8, items, worker }) {
|
|
22
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
23
|
+
throw new Error(`Concurrency must be a positive integer but got ${String(concurrency)}`);
|
|
24
|
+
}
|
|
25
|
+
/** @type {R[]} */
|
|
26
|
+
const results = new Array(items.length);
|
|
27
|
+
let nextIndex = 0;
|
|
28
|
+
const runNext = async () => {
|
|
29
|
+
while (nextIndex < items.length) {
|
|
30
|
+
const index = nextIndex;
|
|
31
|
+
nextIndex += 1;
|
|
32
|
+
results[index] = await worker(items[index], index);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** @type {Promise<void>[]} */
|
|
36
|
+
const runners = [];
|
|
37
|
+
for (let index = 0; index < Math.min(concurrency, items.length); index++) {
|
|
38
|
+
runners.push(runNext());
|
|
39
|
+
}
|
|
40
|
+
await Promise.all(runners);
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm91bmRlZC10YXNrLXJ1bm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy91dGlscy9ib3VuZGVkLXRhc2stcnVubmVyLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWjs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxDQUFDLE9BQU8sT0FBTyxpQkFBaUI7SUFDcEM7Ozs7Ozs7OztPQVNHO0lBQ0gsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsRUFBQyxXQUFXLEdBQUcsQ0FBQyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUM7UUFDL0MsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLElBQUksV0FBVyxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3RELE1BQU0sSUFBSSxLQUFLLENBQUMsa0RBQWtELE1BQU0sQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUE7UUFDMUYsQ0FBQztRQUVELGtCQUFrQjtRQUNsQixNQUFNLE9BQU8sR0FBRyxJQUFJLEtBQUssQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUE7UUFDdkMsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFBO1FBRWpCLE1BQU0sT0FBTyxHQUFHLEtBQUssSUFBSSxFQUFFO1lBQ3pCLE9BQU8sU0FBUyxHQUFHLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDaEMsTUFBTSxLQUFLLEdBQUcsU0FBUyxDQUFBO2dCQUV2QixTQUFTLElBQUksQ0FBQyxDQUFBO2dCQUNkLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEVBQUUsS0FBSyxDQUFDLENBQUE7WUFDcEQsQ0FBQztRQUNILENBQUMsQ0FBQTtRQUVELDhCQUE4QjtRQUM5QixNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUE7UUFFbEIsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsV0FBVyxFQUFFLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDO1lBQ3pFLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQTtRQUN6QixDQUFDO1FBRUQsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFBO1FBRTFCLE9BQU8sT0FBTyxDQUFBO0lBQ2hCLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG4vKipcbiAqIFJ1bnMgb25lIGNhbGxiYWNrIHBlciBpdGVtIHdpdGggYSBib3VuZGVkIG51bWJlciBvZiBjYWxsYmFja3MgaW4gZmxpZ2h0LlxuICpcbiAqIFVzZWQgd2hlcmUgYSBzdGFydHVwIHBhc3MgaGFzIHRvIHRvdWNoIG1hbnkgaW5kZXBlbmRlbnQgcmVjb3JkcyAobW9kZWxcbiAqIG1ldGFkYXRhLCBwZXItdGFibGUgc2NoZW1hIHJlYWRzKSBhbmQgcnVubmluZyB0aGVtIHNlcXVlbnRpYWxseSBtdWx0aXBsaWVzIHRoZVxuICogcm91bmQtdHJpcCBsYXRlbmN5IGJ5IHRoZSBpdGVtIGNvdW50LCB3aGlsZSBydW5uaW5nIHRoZW0gYWxsIGF0IG9uY2Ugd291bGRcbiAqIG9wZW4gYW4gdW5ib3VuZGVkIG51bWJlciBvZiBkYXRhYmFzZSBjb25uZWN0aW9ucy5cbiAqL1xuZXhwb3J0IGRlZmF1bHQgY2xhc3MgQm91bmRlZFRhc2tSdW5uZXIge1xuICAvKipcbiAgICogUnVucyB0aGUgd29ya2VyIGZvciBldmVyeSBpdGVtLCBrZWVwaW5nIGF0IG1vc3QgYGNvbmN1cnJlbmN5YCBjYWxsYmFja3MgaW5cbiAgICogZmxpZ2h0IGFuZCByZXR1cm5pbmcgdGhlIHJlc3VsdHMgaW4gaW5wdXQgb3JkZXIuXG4gICAqIEB0ZW1wbGF0ZSBULCBSXG4gICAqIEBwYXJhbSB7b2JqZWN0fSBhcmdzIC0gUnVuIGFyZ3MuXG4gICAqIEBwYXJhbSB7bnVtYmVyfSBbYXJncy5jb25jdXJyZW5jeV0gLSBNYXhpbXVtIGNhbGxiYWNrcyBpbiBmbGlnaHQuXG4gICAqIEBwYXJhbSB7VFtdfSBhcmdzLml0ZW1zIC0gSXRlbXMgdG8gcHJvY2Vzcy5cbiAgICogQHBhcmFtIHsoaXRlbTogVCwgaW5kZXg6IG51bWJlcikgPT4gUHJvbWlzZTxSPn0gYXJncy53b3JrZXIgLSBDYWxsYmFjayBwZXIgaXRlbS5cbiAgICogQHJldHVybnMge1Byb21pc2U8UltdPn0gLSBSZXN1bHRzIGluIGlucHV0IG9yZGVyLlxuICAgKi9cbiAgc3RhdGljIGFzeW5jIHJ1bih7Y29uY3VycmVuY3kgPSA4LCBpdGVtcywgd29ya2VyfSkge1xuICAgIGlmICghTnVtYmVyLmlzSW50ZWdlcihjb25jdXJyZW5jeSkgfHwgY29uY3VycmVuY3kgPCAxKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYENvbmN1cnJlbmN5IG11c3QgYmUgYSBwb3NpdGl2ZSBpbnRlZ2VyIGJ1dCBnb3QgJHtTdHJpbmcoY29uY3VycmVuY3kpfWApXG4gICAgfVxuXG4gICAgLyoqIEB0eXBlIHtSW119ICovXG4gICAgY29uc3QgcmVzdWx0cyA9IG5ldyBBcnJheShpdGVtcy5sZW5ndGgpXG4gICAgbGV0IG5leHRJbmRleCA9IDBcblxuICAgIGNvbnN0IHJ1bk5leHQgPSBhc3luYyAoKSA9PiB7XG4gICAgICB3aGlsZSAobmV4dEluZGV4IDwgaXRlbXMubGVuZ3RoKSB7XG4gICAgICAgIGNvbnN0IGluZGV4ID0gbmV4dEluZGV4XG5cbiAgICAgICAgbmV4dEluZGV4ICs9IDFcbiAgICAgICAgcmVzdWx0c1tpbmRleF0gPSBhd2FpdCB3b3JrZXIoaXRlbXNbaW5kZXhdLCBpbmRleClcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKiogQHR5cGUge1Byb21pc2U8dm9pZD5bXX0gKi9cbiAgICBjb25zdCBydW5uZXJzID0gW11cblxuICAgIGZvciAobGV0IGluZGV4ID0gMDsgaW5kZXggPCBNYXRoLm1pbihjb25jdXJyZW5jeSwgaXRlbXMubGVuZ3RoKTsgaW5kZXgrKykge1xuICAgICAgcnVubmVycy5wdXNoKHJ1bk5leHQoKSlcbiAgICB9XG5cbiAgICBhd2FpdCBQcm9taXNlLmFsbChydW5uZXJzKVxuXG4gICAgcmV0dXJuIHJlc3VsdHNcbiAgfVxufVxuIl19
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs one callback per item with a bounded number of callbacks in flight.
|
|
5
|
+
*
|
|
6
|
+
* Used where a startup pass has to touch many independent records (model
|
|
7
|
+
* metadata, per-table schema reads) and running them sequentially multiplies the
|
|
8
|
+
* round-trip latency by the item count, while running them all at once would
|
|
9
|
+
* open an unbounded number of database connections.
|
|
10
|
+
*/
|
|
11
|
+
export default class BoundedTaskRunner {
|
|
12
|
+
/**
|
|
13
|
+
* Runs the worker for every item, keeping at most `concurrency` callbacks in
|
|
14
|
+
* flight and returning the results in input order.
|
|
15
|
+
* @template T, R
|
|
16
|
+
* @param {object} args - Run args.
|
|
17
|
+
* @param {number} [args.concurrency] - Maximum callbacks in flight.
|
|
18
|
+
* @param {T[]} args.items - Items to process.
|
|
19
|
+
* @param {(item: T, index: number) => Promise<R>} args.worker - Callback per item.
|
|
20
|
+
* @returns {Promise<R[]>} - Results in input order.
|
|
21
|
+
*/
|
|
22
|
+
static async run({concurrency = 8, items, worker}) {
|
|
23
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
24
|
+
throw new Error(`Concurrency must be a positive integer but got ${String(concurrency)}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @type {R[]} */
|
|
28
|
+
const results = new Array(items.length)
|
|
29
|
+
let nextIndex = 0
|
|
30
|
+
|
|
31
|
+
const runNext = async () => {
|
|
32
|
+
while (nextIndex < items.length) {
|
|
33
|
+
const index = nextIndex
|
|
34
|
+
|
|
35
|
+
nextIndex += 1
|
|
36
|
+
results[index] = await worker(items[index], index)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {Promise<void>[]} */
|
|
41
|
+
const runners = []
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < Math.min(concurrency, items.length); index++) {
|
|
44
|
+
runners.push(runNext())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await Promise.all(runners)
|
|
48
|
+
|
|
49
|
+
return results
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1598,6 +1598,9 @@ export default class LocalBackgroundJobsStore {
|
|
|
1598
1598
|
failedAtMs: this._numberOrNull(row.failed_at_ms),
|
|
1599
1599
|
handedOffAtMs: this._numberOrNull(row.handed_off_at_ms),
|
|
1600
1600
|
handoffId: row.handoff_id === null || row.handoff_id === undefined ? null : String(row.handoff_id),
|
|
1601
|
+
// Local (browser/Expo) dispatch is strictly FIFO and has no priority column,
|
|
1602
|
+
// so every row reports the default priority.
|
|
1603
|
+
priority: 0,
|
|
1601
1604
|
id: String(row.id),
|
|
1602
1605
|
jobName: String(row.job_name),
|
|
1603
1606
|
lastError: row.last_error === null || row.last_error === undefined ? null : String(row.last_error),
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
* @property {string} jobId - New job id.
|
|
42
42
|
* @property {string} jobName - Job name.
|
|
43
43
|
* @property {number} maxRetries - Retry cap.
|
|
44
|
+
* @property {number} priority - Dispatch priority within the queue.
|
|
44
45
|
* @property {string} queue - Queue name.
|
|
45
46
|
* @property {number} scheduledAtMs - Eligibility timestamp.
|
|
46
47
|
* @property {number | null} timeoutMs - Per-job timeout override, or null when omitted.
|
|
@@ -137,8 +138,22 @@ export const BACKGROUND_JOB_COUNT_BUCKETS = ["all", "queued", "handed_off", "com
|
|
|
137
138
|
const COUNTED_JOB_STATUSES = BACKGROUND_JOB_COUNT_BUCKETS.slice(1)
|
|
138
139
|
const MAX_JOB_TIMEOUT_MS = 2_147_483_647
|
|
139
140
|
const JOB_TIMEOUT_VALIDATION_MESSAGE = `background job timeoutMs must be a finite non-positive number or an integer between 1 and ${MAX_JOB_TIMEOUT_MS}`
|
|
141
|
+
const BACKGROUND_JOB_PRIORITY_VALIDATION_MESSAGE = "background job priority must be a non-negative integer"
|
|
140
142
|
const ORPHANED_AFTER_MS = 2 * 60 * 60 * 1000
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Priority a job is demoted to when it is re-queued after a failed attempt.
|
|
146
|
+
* A retry has already consumed capacity and time, so it must never overtake
|
|
147
|
+
* work that has not had its first chance yet. Enqueue priorities are validated
|
|
148
|
+
* to be non-negative integers, which makes `-1` the lowest reachable priority.
|
|
149
|
+
*/
|
|
150
|
+
export const RETRY_BACKGROUND_JOB_PRIORITY = -1
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Priority a job gets when the producer does not ask for one.
|
|
154
|
+
*/
|
|
155
|
+
export const DEFAULT_BACKGROUND_JOB_PRIORITY = 0
|
|
156
|
+
|
|
142
157
|
/**
|
|
143
158
|
* Columns the dashboard is allowed to sort job listings by, mapped to their
|
|
144
159
|
* database column names. Restricting to this set keeps the sort parameter
|
|
@@ -1159,6 +1174,12 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
1159
1174
|
const priorityOrder = this._queuePriorityOrderSql(db)
|
|
1160
1175
|
|
|
1161
1176
|
if (priorityOrder) query = query.order(`${priorityOrder} DESC`)
|
|
1177
|
+
|
|
1178
|
+
// Job-level priority applies inside the queue ranking above. A retry is
|
|
1179
|
+
// stored at RETRY_BACKGROUND_JOB_PRIORITY (-1), so it sorts behind every
|
|
1180
|
+
// first attempt without delaying itself further than the backoff already
|
|
1181
|
+
// does. COALESCE covers rows written before the column existed.
|
|
1182
|
+
query = query.order(`COALESCE(${db.quoteColumn("priority")}, ${DEFAULT_BACKGROUND_JOB_PRIORITY}) DESC`)
|
|
1162
1183
|
}
|
|
1163
1184
|
|
|
1164
1185
|
query = query
|
|
@@ -1965,12 +1986,33 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
1965
1986
|
jobId: randomUUID(),
|
|
1966
1987
|
jobName,
|
|
1967
1988
|
maxRetries: this._normalizeMaxRetries(options?.maxRetries),
|
|
1989
|
+
priority: this._normalizeJobPriority(options),
|
|
1968
1990
|
queue,
|
|
1969
1991
|
scheduledAtMs: this._normalizeScheduledAtMs(options?.scheduledAtMs, createdAtMs),
|
|
1970
1992
|
timeoutMs: this._normalizeJobTimeoutMs(options)
|
|
1971
1993
|
}
|
|
1972
1994
|
}
|
|
1973
1995
|
|
|
1996
|
+
/**
|
|
1997
|
+
* Normalizes a requested dispatch priority. Priorities order jobs within their
|
|
1998
|
+
* queue on the dispatch path only: higher goes first, and the default keeps
|
|
1999
|
+
* FIFO order. Negative priorities are rejected because the store reserves them
|
|
2000
|
+
* for {@link RETRY_BACKGROUND_JOB_PRIORITY}, which must stay the lowest.
|
|
2001
|
+
* @param {import("./types.js").BackgroundJobOptions | undefined} options - Job options.
|
|
2002
|
+
* @returns {number} - Priority.
|
|
2003
|
+
*/
|
|
2004
|
+
_normalizeJobPriority(options) {
|
|
2005
|
+
const priority = options?.priority
|
|
2006
|
+
|
|
2007
|
+
if (priority === undefined) return DEFAULT_BACKGROUND_JOB_PRIORITY
|
|
2008
|
+
|
|
2009
|
+
if (typeof priority !== "number" || !Number.isInteger(priority) || priority < 0) {
|
|
2010
|
+
throw VelociousError.safe(BACKGROUND_JOB_PRIORITY_VALIDATION_MESSAGE)
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
return priority
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1974
2016
|
/**
|
|
1975
2017
|
* Normalizes a per-job timeout while preserving omitted (worker fallback)
|
|
1976
2018
|
* separately from explicitly disabled.
|
|
@@ -2024,6 +2066,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2024
2066
|
execution_mode: preparedJob.executionMode,
|
|
2025
2067
|
queue: preparedJob.queue,
|
|
2026
2068
|
max_retries: preparedJob.maxRetries,
|
|
2069
|
+
priority: preparedJob.priority,
|
|
2027
2070
|
attempts: 0,
|
|
2028
2071
|
status: "queued",
|
|
2029
2072
|
scheduled_at_ms: preparedJob.scheduledAtMs,
|
|
@@ -2334,6 +2377,30 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2334
2377
|
}
|
|
2335
2378
|
}
|
|
2336
2379
|
|
|
2380
|
+
const priorityLockName = `${MIGRATION_SCOPE}:priority_column`
|
|
2381
|
+
const priorityLockAcquired = await db.acquireAdvisoryLock(priorityLockName)
|
|
2382
|
+
|
|
2383
|
+
if (!priorityLockAcquired) throw new Error("Failed to acquire background jobs priority schema lock")
|
|
2384
|
+
|
|
2385
|
+
try {
|
|
2386
|
+
db.clearSchemaCache()
|
|
2387
|
+
const priorityTable = await db.getTableByNameOrFail(JOBS_TABLE)
|
|
2388
|
+
|
|
2389
|
+
if (!(await priorityTable.getColumnByName("priority"))) {
|
|
2390
|
+
const tableData = new TableData(JOBS_TABLE)
|
|
2391
|
+
tableData.bigint("priority", {null: true})
|
|
2392
|
+
const sqls = await db.alterTableSQLs(tableData)
|
|
2393
|
+
|
|
2394
|
+
for (const sql of sqls) {
|
|
2395
|
+
await db.query(sql)
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
db.clearSchemaCache()
|
|
2399
|
+
}
|
|
2400
|
+
} finally {
|
|
2401
|
+
await db.releaseAdvisoryLock(priorityLockName)
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2337
2404
|
await this._backfillExecutionModesOnce(db)
|
|
2338
2405
|
await this._dropForkedColumnOnce(db)
|
|
2339
2406
|
|
|
@@ -3109,6 +3176,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
3109
3176
|
|
|
3110
3177
|
if (markOrphaned) transitionedJob.orphanedAtMs = now
|
|
3111
3178
|
if (shouldRetry) {
|
|
3179
|
+
transitionedJob.priority = RETRY_BACKGROUND_JOB_PRIORITY
|
|
3112
3180
|
transitionedJob.scheduledAtMs = scheduledAt
|
|
3113
3181
|
} else if (!markOrphaned) {
|
|
3114
3182
|
transitionedJob.failedAtMs = now
|
|
@@ -3142,7 +3210,14 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
3142
3210
|
// A retry starts a fresh handoff with a possibly different runner, so the
|
|
3143
3211
|
// previous child's acceptance evidence must not leak into the next attempt.
|
|
3144
3212
|
// Terminal failures keep it as historical evidence for the lost attempt.
|
|
3145
|
-
if (shouldRetry)
|
|
3213
|
+
if (shouldRetry) {
|
|
3214
|
+
Object.assign(update, this._clearedChildAcceptanceData())
|
|
3215
|
+
|
|
3216
|
+
// Retried work goes to the back of the dispatch order. It has already had
|
|
3217
|
+
// a turn, and letting it overtake first attempts is how a failing producer
|
|
3218
|
+
// starves healthy jobs of the same queue.
|
|
3219
|
+
update.priority = RETRY_BACKGROUND_JOB_PRIORITY
|
|
3220
|
+
}
|
|
3146
3221
|
|
|
3147
3222
|
this._applyOrphanedFailureUpdate({markOrphaned, now, update})
|
|
3148
3223
|
this._applyFailureStatusUpdate({markOrphaned, now, scheduledAt, shouldRetry, update})
|
|
@@ -3211,6 +3286,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
3211
3286
|
status: normalizeBackgroundJobStatus(row.status ? String(row.status) : "queued"),
|
|
3212
3287
|
attempts: this._normalizeNumber(row.attempts),
|
|
3213
3288
|
maxRetries: this._normalizeNumber(row.max_retries),
|
|
3289
|
+
priority: this._normalizeNumber(row.priority) ?? DEFAULT_BACKGROUND_JOB_PRIORITY,
|
|
3214
3290
|
scheduledAtMs: this._normalizeNumber(row.scheduled_at_ms),
|
|
3215
3291
|
createdAtMs: this._normalizeNumber(row.created_at_ms),
|
|
3216
3292
|
handedOffAtMs: this._normalizeNumber(row.handed_off_at_ms),
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
* @typedef {object} BackgroundJobOptions
|
|
170
170
|
* @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Node defaults to `"pooled"` (a warm, reused local runner process). Browser/Expo local dispatch defaults to and only accepts `"inline"`. `"forked"` runs a Node job in a fresh `child_process.fork()` child, and `"spawned"` in a detached CLI runner.
|
|
171
171
|
* @property {number} [maxRetries] - Max retries for a failed job before it is marked failed.
|
|
172
|
+
* @property {number} [priority] - Dispatch priority within the queue: higher runs first, default `0`, retries always run last.
|
|
172
173
|
* @property {string} [queue] - Queue name. Defaults to `"default"`. When the queue has a configured cap in `backgroundJobs.queues`, that cap is enforced cluster-wide.
|
|
173
174
|
* @property {string} [concurrencyKey] - Opaque non-empty key used to share a concurrency cap. Overrides any queue-derived cap.
|
|
174
175
|
* @property {number} [maxConcurrency] - Positive integer cap; must be paired with `concurrencyKey`.
|
|
@@ -207,6 +208,7 @@
|
|
|
207
208
|
* @property {BackgroundJobStatus} status - Current job status.
|
|
208
209
|
* @property {number | null} attempts - Failure attempts count.
|
|
209
210
|
* @property {number | null} maxRetries - Max retry attempts.
|
|
211
|
+
* @property {number} priority - Dispatch priority; `-1` means retried and therefore queued last.
|
|
210
212
|
* @property {number | null} scheduledAtMs - Next scheduled time in ms.
|
|
211
213
|
* @property {number | null} createdAtMs - Creation time in ms.
|
|
212
214
|
* @property {number | null} handedOffAtMs - Time handed to worker in ms.
|
|
@@ -157,6 +157,7 @@
|
|
|
157
157
|
*/
|
|
158
158
|
|
|
159
159
|
import BacktraceCleaner from "../../utils/backtrace-cleaner.js"
|
|
160
|
+
import BoundedTaskRunner from "../../utils/bounded-task-runner.js"
|
|
160
161
|
import { getDatabaseAnnotations } from "../annotations.js"
|
|
161
162
|
import { formatDateForDatabase } from "../datetime-storage.js"
|
|
162
163
|
import isDate from "../../utils/is-date.js"
|
|
@@ -911,6 +912,65 @@ export default class VelociousDatabaseDriversBase {
|
|
|
911
912
|
return await this._cachedSchemaMetadata(`table:${tableName}:${metadataName}`, callback)
|
|
912
913
|
}
|
|
913
914
|
|
|
915
|
+
/**
|
|
916
|
+
* Seeds schema metadata that was loaded in one batch for many tables, so a later
|
|
917
|
+
* per-table read resolves from the cache instead of issuing its own query.
|
|
918
|
+
* @template T
|
|
919
|
+
* @param {string} tableName - Table name.
|
|
920
|
+
* @param {string} metadataName - Metadata name.
|
|
921
|
+
* @param {T} value - Already-resolved metadata.
|
|
922
|
+
* @returns {void}
|
|
923
|
+
*/
|
|
924
|
+
seedTableSchemaMetadata(tableName, metadataName, value) {
|
|
925
|
+
if (!this._schemaCacheEnabled()) return
|
|
926
|
+
|
|
927
|
+
this._schemaCache.set(`table:${tableName}:${metadataName}`, Promise.resolve(value))
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Loads column metadata for several tables, combining the reads into one query
|
|
932
|
+
* when the driver can answer for all of them at once and seeding the per-table
|
|
933
|
+
* cache so the models that follow initialize without querying.
|
|
934
|
+
* @param {Array<import("./base-table.js").default>} tables - Tables to load columns for.
|
|
935
|
+
* @param {{concurrency?: number}} [args] - Concurrency for drivers that can only read per table.
|
|
936
|
+
* @returns {Promise<void>}
|
|
937
|
+
*/
|
|
938
|
+
async prefetchTableColumns(tables, {concurrency = 8} = {}) {
|
|
939
|
+
if (!this._schemaCacheEnabled()) return
|
|
940
|
+
if (tables.length === 0) return
|
|
941
|
+
|
|
942
|
+
const batchedColumns = await this.batchTableColumns(tables)
|
|
943
|
+
|
|
944
|
+
if (batchedColumns) {
|
|
945
|
+
for (const [tableName, columns] of batchedColumns) {
|
|
946
|
+
this.seedTableSchemaMetadata(tableName, "columns", columns)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
await BoundedTaskRunner.run({
|
|
953
|
+
concurrency,
|
|
954
|
+
items: tables,
|
|
955
|
+
worker: async (table) => {
|
|
956
|
+
await table.getColumns()
|
|
957
|
+
}
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Batched column metadata for many tables at once. Drivers whose engine cannot
|
|
963
|
+
* answer for several tables in one statement return null, leaving the caller to
|
|
964
|
+
* read them per table.
|
|
965
|
+
* @param {Array<import("./base-table.js").default>} tables - Tables to load columns for.
|
|
966
|
+
* @returns {Promise<Map<string, Array<import("./base-column.js").default>> | null>} - Columns keyed by table name, or null when unsupported.
|
|
967
|
+
*/
|
|
968
|
+
async batchTableColumns(tables) {
|
|
969
|
+
void tables
|
|
970
|
+
|
|
971
|
+
return null
|
|
972
|
+
}
|
|
973
|
+
|
|
914
974
|
/**
|
|
915
975
|
* Runs schema cache return value.
|
|
916
976
|
* @param {ReturnType<typeof JSON.parse>} value - Cached value.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import AlterTable from "./sql/alter-table.js"
|
|
4
4
|
import Base from "../base.js"
|
|
5
|
+
import Column from "./column.js"
|
|
5
6
|
import CreateDatabase from "./sql/create-database.js"
|
|
6
7
|
import CreateIndex from "./sql/create-index.js"
|
|
7
8
|
import CreateTable from "./sql/create-table.js"
|
|
@@ -516,6 +517,64 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
516
517
|
})
|
|
517
518
|
}
|
|
518
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Reads column metadata for many tables in one query. The per-table read of the
|
|
522
|
+
* same three-way `INFORMATION_SCHEMA` join is one of the slowest things a boot
|
|
523
|
+
* does against the legacy SQL Server, so it is worth combining regardless of the
|
|
524
|
+
* number of tables.
|
|
525
|
+
* @param {Array<import("../base-table.js").default>} tables - Tables to load columns for.
|
|
526
|
+
* @returns {Promise<Map<string, Array<import("../base-column.js").default>>>} - Columns keyed by table name.
|
|
527
|
+
*/
|
|
528
|
+
async batchTableColumns(tables) {
|
|
529
|
+
/** @type {Map<string, Array<import("../base-column.js").default>>} */
|
|
530
|
+
const columnsByTable = new Map()
|
|
531
|
+
|
|
532
|
+
if (tables.length === 0) return columnsByTable
|
|
533
|
+
|
|
534
|
+
const schema = this.getArgs()?.schema || this.getArgs()?.sqlConfig?.options?.schema
|
|
535
|
+
const schemaClause = schema
|
|
536
|
+
? ` AND columns.[TABLE_SCHEMA] = ${this.quote(schema)}`
|
|
537
|
+
: " AND columns.[TABLE_SCHEMA] = SCHEMA_NAME()"
|
|
538
|
+
const tablesByName = new Map(tables.map((table) => [table.getName(), table]))
|
|
539
|
+
const tableNamesSql = [...tablesByName.keys()].map((tableName) => this.quote(tableName)).join(", ")
|
|
540
|
+
const rows = await this.query(`
|
|
541
|
+
SELECT
|
|
542
|
+
columns.*,
|
|
543
|
+
COLUMNPROPERTY(object_id(columns.TABLE_SCHEMA + '.' + columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') AS isIdentity,
|
|
544
|
+
CASE WHEN keyColumnUsage.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS isPrimaryKey
|
|
545
|
+
FROM [INFORMATION_SCHEMA].[COLUMNS] AS columns
|
|
546
|
+
LEFT JOIN [INFORMATION_SCHEMA].[TABLE_CONSTRAINTS] AS tableConstraints ON
|
|
547
|
+
tableConstraints.CONSTRAINT_TYPE = 'PRIMARY KEY' AND
|
|
548
|
+
tableConstraints.TABLE_CATALOG = columns.TABLE_CATALOG AND
|
|
549
|
+
tableConstraints.TABLE_SCHEMA = columns.TABLE_SCHEMA AND
|
|
550
|
+
tableConstraints.TABLE_NAME = columns.TABLE_NAME
|
|
551
|
+
LEFT JOIN [INFORMATION_SCHEMA].[KEY_COLUMN_USAGE] AS keyColumnUsage ON
|
|
552
|
+
keyColumnUsage.CONSTRAINT_CATALOG = tableConstraints.CONSTRAINT_CATALOG AND
|
|
553
|
+
keyColumnUsage.CONSTRAINT_SCHEMA = tableConstraints.CONSTRAINT_SCHEMA AND
|
|
554
|
+
keyColumnUsage.CONSTRAINT_NAME = tableConstraints.CONSTRAINT_NAME AND
|
|
555
|
+
keyColumnUsage.TABLE_CATALOG = columns.TABLE_CATALOG AND
|
|
556
|
+
keyColumnUsage.TABLE_SCHEMA = columns.TABLE_SCHEMA AND
|
|
557
|
+
keyColumnUsage.TABLE_NAME = columns.TABLE_NAME AND
|
|
558
|
+
keyColumnUsage.COLUMN_NAME = columns.COLUMN_NAME
|
|
559
|
+
WHERE columns.TABLE_NAME IN (${tableNamesSql})${schemaClause}
|
|
560
|
+
ORDER BY columns.TABLE_NAME, columns.ORDINAL_POSITION
|
|
561
|
+
`)
|
|
562
|
+
|
|
563
|
+
for (const row of rows) {
|
|
564
|
+
const tableName = /** @type {string} */ (row.TABLE_NAME)
|
|
565
|
+
const table = tablesByName.get(tableName)
|
|
566
|
+
|
|
567
|
+
if (!table) continue
|
|
568
|
+
|
|
569
|
+
const columns = columnsByTable.get(tableName) || []
|
|
570
|
+
|
|
571
|
+
columns.push(new Column(table, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row)))
|
|
572
|
+
columnsByTable.set(tableName, columns)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return columnsByTable
|
|
576
|
+
}
|
|
577
|
+
|
|
519
578
|
/**
|
|
520
579
|
* Keeps constraint toggles and cleanup inside each physical SQL Server
|
|
521
580
|
* request so a pool cannot split lifecycle ownership across sessions.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import AlterTable from "./sql/alter-table.js"
|
|
4
4
|
import Base from "../base.js"
|
|
5
|
+
import Column from "./column.js"
|
|
5
6
|
import CreateDatabase from "./sql/create-database.js"
|
|
6
7
|
import CreateIndex from "./sql/create-index.js"
|
|
7
8
|
import CreateTable from "./sql/create-table.js"
|
|
@@ -765,6 +766,56 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
765
766
|
})
|
|
766
767
|
}
|
|
767
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Reads column metadata for many tables in one query. `SHOW FULL COLUMNS` only
|
|
771
|
+
* describes a single table, but `information_schema.COLUMNS` carries the same
|
|
772
|
+
* fields, so the result is aliased to the shape `Column` already reads.
|
|
773
|
+
*
|
|
774
|
+
* `NULLIF(COLUMN_DEFAULT, 'NULL')` compensates for MariaDB reporting a SQL NULL
|
|
775
|
+
* default as the literal string 'NULL', where `SHOW FULL COLUMNS` reports NULL.
|
|
776
|
+
* Verified column-for-column against `SHOW FULL COLUMNS` on a 44-table MariaDB
|
|
777
|
+
* schema; the only divergence was that sentinel.
|
|
778
|
+
* @param {Array<import("../base-table.js").default>} tables - Tables to load columns for.
|
|
779
|
+
* @returns {Promise<Map<string, Array<import("../base-column.js").default>>>} - Columns keyed by table name.
|
|
780
|
+
*/
|
|
781
|
+
async batchTableColumns(tables) {
|
|
782
|
+
/** @type {Map<string, Array<import("../base-column.js").default>>} */
|
|
783
|
+
const columnsByTable = new Map()
|
|
784
|
+
|
|
785
|
+
if (tables.length === 0) return columnsByTable
|
|
786
|
+
|
|
787
|
+
const tablesByName = new Map(tables.map((table) => [table.getName(), table]))
|
|
788
|
+
const tableNamesSql = [...tablesByName.keys()].map((tableName) => this.quote(tableName)).join(", ")
|
|
789
|
+
const rows = await this.query(`
|
|
790
|
+
SELECT
|
|
791
|
+
TABLE_NAME AS tableName,
|
|
792
|
+
COLUMN_NAME AS Field,
|
|
793
|
+
COLUMN_TYPE AS Type,
|
|
794
|
+
IS_NULLABLE AS \`Null\`,
|
|
795
|
+
COLUMN_KEY AS \`Key\`,
|
|
796
|
+
NULLIF(COLUMN_DEFAULT, 'NULL') AS \`Default\`,
|
|
797
|
+
EXTRA AS Extra,
|
|
798
|
+
COLUMN_COMMENT AS Comment
|
|
799
|
+
FROM information_schema.COLUMNS
|
|
800
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${tableNamesSql})
|
|
801
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
802
|
+
`)
|
|
803
|
+
|
|
804
|
+
for (const row of rows) {
|
|
805
|
+
const tableName = /** @type {string} */ (row.tableName)
|
|
806
|
+
const table = tablesByName.get(tableName)
|
|
807
|
+
|
|
808
|
+
if (!table) continue
|
|
809
|
+
|
|
810
|
+
const columns = columnsByTable.get(tableName) || []
|
|
811
|
+
|
|
812
|
+
columns.push(new Column(table, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row)))
|
|
813
|
+
columnsByTable.set(tableName, columns)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return columnsByTable
|
|
817
|
+
}
|
|
818
|
+
|
|
768
819
|
/**
|
|
769
820
|
* Runs structure sql.
|
|
770
821
|
* @returns {Promise<string | null>} - Resolves with SQL string.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import AlterTable from "./sql/alter-table.js"
|
|
4
4
|
import wait from "awaitery/build/wait.js"
|
|
5
5
|
import Base from "../base.js"
|
|
6
|
+
import Column from "./column.js"
|
|
6
7
|
import { Client, types as pgTypes } from "pg"
|
|
7
8
|
import CreateDatabase from "./sql/create-database.js"
|
|
8
9
|
import CreateIndex from "./sql/create-index.js"
|
|
@@ -329,6 +330,10 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
329
330
|
return insert.toSql()
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Runs get tables.
|
|
335
|
+
* @returns {Promise<Array<import("../base-table.js").default>>} - Resolves with the tables.
|
|
336
|
+
*/
|
|
332
337
|
async getTables() {
|
|
333
338
|
return await this._cachedSchemaMetadata("tables", async () => {
|
|
334
339
|
const result = await this.query("SELECT * FROM information_schema.tables WHERE table_catalog = CURRENT_DATABASE() AND table_schema = 'public'")
|
|
@@ -344,6 +349,62 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
344
349
|
})
|
|
345
350
|
}
|
|
346
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Reads column metadata for many tables in one query instead of one catalog
|
|
354
|
+
* join per table.
|
|
355
|
+
* @param {Array<import("../base-table.js").default>} tables - Tables to load columns for.
|
|
356
|
+
* @returns {Promise<Map<string, Array<import("../base-column.js").default>>>} - Columns keyed by table name.
|
|
357
|
+
*/
|
|
358
|
+
async batchTableColumns(tables) {
|
|
359
|
+
/** @type {Map<string, Array<import("../base-column.js").default>>} */
|
|
360
|
+
const columnsByTable = new Map()
|
|
361
|
+
|
|
362
|
+
if (tables.length === 0) return columnsByTable
|
|
363
|
+
|
|
364
|
+
const tablesByName = new Map(tables.map((table) => [table.getName(), table]))
|
|
365
|
+
const tableNamesSql = [...tablesByName.keys()].map((tableName) => this.quote(tableName)).join(", ")
|
|
366
|
+
const rows = await this.query(`
|
|
367
|
+
SELECT
|
|
368
|
+
columns.*,
|
|
369
|
+
CASE WHEN key_column_usage.column_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary_key,
|
|
370
|
+
col_description((columns.table_schema || '.' || columns.table_name)::regclass, columns.ordinal_position) AS column_comment
|
|
371
|
+
|
|
372
|
+
FROM
|
|
373
|
+
information_schema.columns AS columns
|
|
374
|
+
|
|
375
|
+
LEFT JOIN information_schema.table_constraints AS table_constraints ON
|
|
376
|
+
table_constraints.table_name = columns.table_name AND
|
|
377
|
+
table_constraints.table_schema = columns.table_schema AND
|
|
378
|
+
table_constraints.constraint_type = 'PRIMARY KEY'
|
|
379
|
+
|
|
380
|
+
LEFT JOIN information_schema.key_column_usage AS key_column_usage ON
|
|
381
|
+
key_column_usage.constraint_name = table_constraints.constraint_name AND
|
|
382
|
+
key_column_usage.table_schema = table_constraints.table_schema AND
|
|
383
|
+
key_column_usage.table_name = columns.table_name AND
|
|
384
|
+
key_column_usage.column_name = columns.column_name
|
|
385
|
+
|
|
386
|
+
WHERE
|
|
387
|
+
columns.table_catalog = CURRENT_DATABASE() AND
|
|
388
|
+
columns.table_schema = 'public' AND
|
|
389
|
+
columns.table_name IN (${tableNamesSql})
|
|
390
|
+
ORDER BY columns.table_name, columns.ordinal_position
|
|
391
|
+
`)
|
|
392
|
+
|
|
393
|
+
for (const row of rows) {
|
|
394
|
+
const tableName = /** @type {string} */ (row.table_name)
|
|
395
|
+
const table = tablesByName.get(tableName)
|
|
396
|
+
|
|
397
|
+
if (!table) continue
|
|
398
|
+
|
|
399
|
+
const columns = columnsByTable.get(tableName) || []
|
|
400
|
+
|
|
401
|
+
columns.push(new Column(table, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row)))
|
|
402
|
+
columnsByTable.set(tableName, columns)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return columnsByTable
|
|
406
|
+
}
|
|
407
|
+
|
|
347
408
|
/**
|
|
348
409
|
* Truncates all eligible tables in one PostgreSQL request.
|
|
349
410
|
* @param {Array<import("../base-table.js").default>} tables - Eligible tables.
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import Logger from "../logger.js"
|
|
13
|
+
import BoundedTaskRunner from "../utils/bounded-task-runner.js"
|
|
13
14
|
import restArgsError from "../utils/rest-args-error.js"
|
|
15
|
+
import SchemaMetadataPrefetcher from "./schema-metadata-prefetcher.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How many model classes initialize at once. Metadata comes from the prefetched
|
|
19
|
+
* schema cache, so this is CPU-bound attribute mapping rather than database work.
|
|
20
|
+
*/
|
|
21
|
+
const MODEL_INITIALIZATION_CONCURRENCY = 8
|
|
14
22
|
|
|
15
23
|
export default class VelociousDatabaseInitializerFromRequireContext {
|
|
16
24
|
/**
|
|
@@ -34,6 +42,9 @@ export default class VelociousDatabaseInitializerFromRequireContext {
|
|
|
34
42
|
async initialize({configuration, ...restArgs}) {
|
|
35
43
|
restArgsError(restArgs)
|
|
36
44
|
|
|
45
|
+
/** @type {Array<typeof import("./record/index.js").default>} */
|
|
46
|
+
const modelClasses = []
|
|
47
|
+
|
|
37
48
|
for (const fileName of this.requireContext.keys()) {
|
|
38
49
|
const modelClassImport = this.requireContext(fileName)
|
|
39
50
|
|
|
@@ -50,14 +61,27 @@ export default class VelociousDatabaseInitializerFromRequireContext {
|
|
|
50
61
|
continue
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await this._bestEffortInitializeDeferredModel({configuration, modelClass})
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await this._initializeModelRecord({configuration, modelClass})
|
|
64
|
+
modelClass.registerRecordClass({configuration})
|
|
65
|
+
modelClasses.push(modelClass)
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
// One catalog read per database instead of one per model, shared by every
|
|
69
|
+
// model initialized below through the driver's schema cache.
|
|
70
|
+
await new SchemaMetadataPrefetcher({configuration, logger: this.logger}).prefetch({modelClasses})
|
|
71
|
+
|
|
72
|
+
await BoundedTaskRunner.run({
|
|
73
|
+
concurrency: MODEL_INITIALIZATION_CONCURRENCY,
|
|
74
|
+
items: modelClasses,
|
|
75
|
+
worker: async (modelClass) => {
|
|
76
|
+
if (!modelClass.getEagerLoadRecordMetadata()) {
|
|
77
|
+
await this._bestEffortInitializeDeferredModel({configuration, modelClass})
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await this._initializeModelRecord({configuration, modelClass})
|
|
83
|
+
}
|
|
84
|
+
})
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
/**
|