velocious 1.0.514 → 1.0.516
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/README.md +4 -3
- package/build/background-jobs/cron-expression.js +9 -9
- package/build/background-jobs/forked-runner-child.js +1 -1
- package/build/background-jobs/job-runner.js +21 -15
- package/build/background-jobs/json-socket.js +4 -0
- package/build/background-jobs/main.js +180 -18
- package/build/background-jobs/status-reporter.js +12 -5
- package/build/background-jobs/store.js +132 -50
- package/build/background-jobs/types.js +11 -3
- package/build/background-jobs/worker.js +9 -5
- package/build/beacon/client.js +13 -6
- package/build/configuration.js +54 -29
- package/build/controller.js +9 -3
- package/build/database/drivers/sqlite/base.js +1 -1
- package/build/database/drivers/sqlite/index.js +15 -14
- package/build/database/migrations-ledger.js +16 -16
- package/build/database/pool/async-tracked-multi-connection.js +16 -0
- package/build/database/pool/base.js +26 -0
- package/build/database/query/where-model-class-hash.js +2 -0
- package/build/database/query/with-count.js +1 -1
- package/build/database/record/acts-as-list.js +2 -2
- package/build/database/record/auditing.js +23 -6
- package/build/database/record/index.js +7 -7
- package/build/database/record/validators/uniqueness.js +3 -3
- package/build/database/tenants/data-copier.js +23 -23
- package/build/database/tenants/schema-cloner.js +27 -27
- package/build/environment-handlers/browser.js +0 -1
- package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/build/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/build/environment-handlers/node.js +1 -1
- package/build/frontend-model-controller.js +26 -18
- package/build/frontend-model-resource/base-resource.js +4 -4
- package/build/frontend-models/base.js +5 -5
- package/build/frontend-models/query.js +12 -12
- package/build/http-server/client/index.js +61 -15
- package/build/http-server/client/request-runner.js +7 -1
- package/build/http-server/client/response.js +17 -1
- package/build/http-server/client/websocket-session.js +13 -13
- package/build/http-server/server-client.js +96 -0
- package/build/http-server/websocket-channel.js +12 -11
- package/build/http-server/websocket-connection.js +12 -11
- package/build/http-server/websocket-events-host.js +1 -1
- package/build/http-server/worker-handler/in-process.js +48 -9
- package/build/http-server/worker-handler/index.js +67 -7
- package/build/http-server/worker-handler/worker-thread.js +68 -0
- package/build/src/background-jobs/cron-expression.d.ts +2 -2
- package/build/src/background-jobs/cron-expression.js +10 -10
- package/build/src/background-jobs/forked-runner-child.js +2 -2
- package/build/src/background-jobs/job-runner.d.ts +5 -1
- package/build/src/background-jobs/job-runner.d.ts.map +1 -1
- package/build/src/background-jobs/job-runner.js +22 -16
- package/build/src/background-jobs/json-socket.d.ts +4 -0
- package/build/src/background-jobs/json-socket.d.ts.map +1 -1
- package/build/src/background-jobs/json-socket.js +5 -1
- package/build/src/background-jobs/main.d.ts +62 -10
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +171 -19
- package/build/src/background-jobs/status-reporter.d.ts +6 -2
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +13 -6
- package/build/src/background-jobs/store.d.ts +48 -25
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +123 -50
- package/build/src/background-jobs/types.d.ts +36 -3
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +12 -4
- package/build/src/background-jobs/worker.d.ts +7 -5
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +10 -6
- package/build/src/beacon/client.d.ts +6 -3
- package/build/src/beacon/client.d.ts.map +1 -1
- package/build/src/beacon/client.js +14 -7
- package/build/src/configuration-types.d.ts +1 -1
- package/build/src/configuration.d.ts +42 -29
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +52 -30
- package/build/src/controller.d.ts +3 -0
- package/build/src/controller.d.ts.map +1 -1
- package/build/src/controller.js +9 -4
- package/build/src/database/drivers/sqlite/base.js +2 -2
- package/build/src/database/drivers/sqlite/index.d.ts +7 -6
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +16 -15
- package/build/src/database/migrations-ledger.d.ts +16 -16
- package/build/src/database/migrations-ledger.js +17 -17
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +0 -11
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +16 -1
- package/build/src/database/pool/base.d.ts +21 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +24 -1
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +3 -1
- package/build/src/database/query/with-count.js +2 -2
- package/build/src/database/record/acts-as-list.js +3 -3
- package/build/src/database/record/auditing.d.ts +1 -1
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +24 -7
- package/build/src/database/record/index.d.ts +7 -7
- package/build/src/database/record/index.js +8 -8
- package/build/src/database/record/validators/uniqueness.d.ts +3 -3
- package/build/src/database/record/validators/uniqueness.js +4 -4
- package/build/src/database/tenants/data-copier.d.ts +18 -18
- package/build/src/database/tenants/data-copier.js +24 -24
- package/build/src/database/tenants/schema-cloner.d.ts +27 -27
- package/build/src/database/tenants/schema-cloner.js +28 -28
- package/build/src/environment-handlers/browser.d.ts +9 -0
- package/build/src/environment-handlers/browser.d.ts.map +1 -1
- package/build/src/environment-handlers/browser.js +1 -2
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +3 -2
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +3 -3
- package/build/src/environment-handlers/node.js +2 -2
- package/build/src/frontend-model-controller.d.ts +9 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +26 -19
- package/build/src/frontend-model-resource/base-resource.js +5 -5
- package/build/src/frontend-models/base.d.ts +4 -4
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +6 -6
- package/build/src/frontend-models/query.d.ts +7 -7
- package/build/src/frontend-models/query.js +13 -13
- package/build/src/http-server/client/index.d.ts +23 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +55 -17
- package/build/src/http-server/client/request-runner.d.ts.map +1 -1
- package/build/src/http-server/client/request-runner.js +8 -2
- package/build/src/http-server/client/response.d.ts +11 -1
- package/build/src/http-server/client/response.d.ts.map +1 -1
- package/build/src/http-server/client/response.js +16 -2
- package/build/src/http-server/client/websocket-session.d.ts +13 -13
- package/build/src/http-server/client/websocket-session.js +14 -14
- package/build/src/http-server/server-client.d.ts +13 -0
- package/build/src/http-server/server-client.d.ts.map +1 -1
- package/build/src/http-server/server-client.js +94 -1
- package/build/src/http-server/websocket-channel.d.ts +12 -11
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +13 -12
- package/build/src/http-server/websocket-connection.d.ts +12 -11
- package/build/src/http-server/websocket-connection.d.ts.map +1 -1
- package/build/src/http-server/websocket-connection.js +13 -12
- package/build/src/http-server/websocket-events-host.d.ts +1 -1
- package/build/src/http-server/websocket-events-host.js +2 -2
- package/build/src/http-server/worker-handler/in-process.d.ts +2 -0
- package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/in-process.js +42 -11
- package/build/src/http-server/worker-handler/index.d.ts +21 -0
- package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/index.js +61 -7
- package/build/src/http-server/worker-handler/worker-thread.d.ts +30 -0
- package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/worker-thread.js +61 -1
- package/build/src/sync/device-identity.d.ts +2 -2
- package/build/src/sync/device-identity.d.ts.map +1 -1
- package/build/src/sync/device-identity.js +4 -4
- package/build/src/sync/local-mutation-log.d.ts +0 -1
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +1 -2
- package/build/src/sync/query-scope.d.ts +5 -0
- package/build/src/sync/query-scope.d.ts.map +1 -1
- package/build/src/sync/query-scope.js +7 -2
- package/build/src/sync/sync-client-types.d.ts +2 -2
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +1 -1
- package/build/src/sync/sync-client.d.ts +17 -2
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +28 -14
- package/build/src/sync/sync-realtime-bridge.d.ts +6 -2
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +14 -3
- package/build/src/sync/sync-resource-base.d.ts +14 -3
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +20 -4
- package/build/src/sync/sync-scope-store.d.ts +3 -3
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +7 -5
- package/build/src/sync/sync-websocket-channel.d.ts +10 -0
- package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
- package/build/src/sync/sync-websocket-channel.js +20 -3
- package/build/src/tenants/tenant-iterator.d.ts +8 -8
- package/build/src/tenants/tenant-iterator.js +9 -9
- package/build/src/tenants/tenant.d.ts +6 -6
- package/build/src/tenants/tenant.js +7 -7
- package/build/src/testing/test-runner.d.ts +16 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +79 -19
- package/build/sync/device-identity.js +3 -3
- package/build/sync/local-mutation-log.js +0 -1
- package/build/sync/query-scope.js +6 -1
- package/build/sync/sync-client-types.js +1 -1
- package/build/sync/sync-client.js +28 -15
- package/build/sync/sync-realtime-bridge.js +13 -2
- package/build/sync/sync-resource-base.js +20 -3
- package/build/sync/sync-scope-store.js +6 -4
- package/build/sync/sync-websocket-channel.js +18 -2
- package/build/tenants/tenant-iterator.js +8 -8
- package/build/tenants/tenant.js +6 -6
- package/build/testing/test-runner.js +86 -20
- package/package.json +2 -2
- package/src/background-jobs/cron-expression.js +9 -9
- package/src/background-jobs/forked-runner-child.js +1 -1
- package/src/background-jobs/job-runner.js +21 -15
- package/src/background-jobs/json-socket.js +4 -0
- package/src/background-jobs/main.js +180 -18
- package/src/background-jobs/status-reporter.js +12 -5
- package/src/background-jobs/store.js +132 -50
- package/src/background-jobs/types.js +11 -3
- package/src/background-jobs/worker.js +9 -5
- package/src/beacon/client.js +13 -6
- package/src/configuration.js +54 -29
- package/src/controller.js +9 -3
- package/src/database/drivers/sqlite/base.js +1 -1
- package/src/database/drivers/sqlite/index.js +15 -14
- package/src/database/migrations-ledger.js +16 -16
- package/src/database/pool/async-tracked-multi-connection.js +16 -0
- package/src/database/pool/base.js +26 -0
- package/src/database/query/where-model-class-hash.js +2 -0
- package/src/database/query/with-count.js +1 -1
- package/src/database/record/acts-as-list.js +2 -2
- package/src/database/record/auditing.js +23 -6
- package/src/database/record/index.js +7 -7
- package/src/database/record/validators/uniqueness.js +3 -3
- package/src/database/tenants/data-copier.js +23 -23
- package/src/database/tenants/schema-cloner.js +27 -27
- package/src/environment-handlers/browser.js +0 -1
- package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/src/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/src/environment-handlers/node.js +1 -1
- package/src/frontend-model-controller.js +26 -18
- package/src/frontend-model-resource/base-resource.js +4 -4
- package/src/frontend-models/base.js +5 -5
- package/src/frontend-models/query.js +12 -12
- package/src/http-server/client/index.js +61 -15
- package/src/http-server/client/request-runner.js +7 -1
- package/src/http-server/client/response.js +17 -1
- package/src/http-server/client/websocket-session.js +13 -13
- package/src/http-server/server-client.js +96 -0
- package/src/http-server/websocket-channel.js +12 -11
- package/src/http-server/websocket-connection.js +12 -11
- package/src/http-server/websocket-events-host.js +1 -1
- package/src/http-server/worker-handler/in-process.js +48 -9
- package/src/http-server/worker-handler/index.js +67 -7
- package/src/http-server/worker-handler/worker-thread.js +68 -0
- package/src/sync/device-identity.js +3 -3
- package/src/sync/local-mutation-log.js +0 -1
- package/src/sync/query-scope.js +6 -1
- package/src/sync/sync-client-types.js +1 -1
- package/src/sync/sync-client.js +28 -15
- package/src/sync/sync-realtime-bridge.js +13 -2
- package/src/sync/sync-resource-base.js +20 -3
- package/src/sync/sync-scope-store.js +6 -4
- package/src/sync/sync-websocket-channel.js +18 -2
- package/src/tenants/tenant-iterator.js +8 -8
- package/src/tenants/tenant.js +6 -6
- package/src/testing/test-runner.js +86 -20
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
24
24
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
25
25
|
* Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
|
|
26
|
-
* Configurable HTTP server worker handlers
|
|
26
|
+
* Configurable HTTP server worker handlers plus backpressured, descriptor-only file responses with completion callbacks (see [docs/http-server.md](docs/http-server.md))
|
|
27
27
|
* Background jobs with failure events for production reporting (see [docs/background-jobs.md](docs/background-jobs.md))
|
|
28
28
|
* Rails-style request and database query logging (see [docs/logging.md](docs/logging.md))
|
|
29
29
|
* EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
|
|
@@ -590,6 +590,7 @@ Frontend-model HTTP requests always use `credentials: "include"` so shared custo
|
|
|
590
590
|
|
|
591
591
|
Unexpected frontend-model endpoint failures stay client-safe in production with `errorMessage: "Request failed."`.
|
|
592
592
|
Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `preload`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
|
|
593
|
+
Invalid frontend-model write attributes and attachment names, including attributes rejected by `permittedParams()`, return the specific safe error message with `velocious.code: "frontend-model-attribute-error"` and are not emitted as framework errors.
|
|
593
594
|
In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
|
|
594
595
|
Other non-production environments, such as `staging`, keep the same client-safe default unless you explicitly opt in with `exposeInternalErrorsToClients: true`:
|
|
595
596
|
|
|
@@ -1986,7 +1987,7 @@ VELOCIOUS_BACKGROUND_JOBS_WORKER_SHUTDOWN_TIMEOUT_MS=indefinite
|
|
|
1986
1987
|
|
|
1987
1988
|
`maxConcurrentInlineJobs` (default: `4`) caps how many `executionMode: "inline"` jobs a single `background-jobs-worker` process runs in parallel. Concurrency is at the JS event-loop level: every job in flight shares the worker's process and DB connection pool, so the cap should fit the pool, not the CPU count. Forking remains the right tool when you need memory isolation across long-running jobs or want to use more cores. The older `forked: false` option still maps to inline mode.
|
|
1988
1989
|
|
|
1989
|
-
`maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel and exit
|
|
1990
|
+
`maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel. After the main process acknowledges their durable status report, forked and spawned one-shot runners exit without waiting for graceful Beacon/database teardown; the OS closes their process-owned resources. A missing or rejected status acknowledgement makes the runner exit as failed instead of reporting clean success. Spawned jobs use the legacy `background-jobs-runner` CLI process via `child_process.spawn()` and are only for callers that intentionally want that spawned behavior.
|
|
1990
1991
|
|
|
1991
1992
|
### Dispatch strategy
|
|
1992
1993
|
|
|
@@ -2107,7 +2108,7 @@ Each job must define exactly one of `every` or `cron`. Cron times are evaluated
|
|
|
2107
2108
|
|
|
2108
2109
|
## Persistence and retries
|
|
2109
2110
|
|
|
2110
|
-
Jobs are persisted in the configured database (`backgroundJobs.databaseIdentifier`) in an internal `background_jobs` table. When a worker picks a job, the job is marked as handed off and the worker reports completion or failure back to the main process.
|
|
2111
|
+
Jobs are persisted in the configured database (`backgroundJobs.databaseIdentifier`) in an internal `background_jobs` table. When a worker picks a job, the job is marked as handed off with a unique lease id and the worker reports completion or failure back to the main process. If that worker socket disconnects unexpectedly, only the leases handed to that exact socket are immediately returned to the queue; late reports are fenced by lease id so they cannot mutate a newer attempt. This recovery is at-least-once and may repeat application side effects if the disconnected attempt had already started them. Gracefully draining workers keep their leases while they finish. For rolling upgrades, upgrade workers before the main process: a lease-aware main keeps legacy workers connected for old reports but dispatches new jobs only to workers advertising lease-reporting support. See [docs/background-jobs.md](docs/background-jobs.md#worker-disconnect-recovery).
|
|
2111
2112
|
|
|
2112
2113
|
Failed jobs are re-queued with backoff and retried up to 10 times by default (10s, 1m, 10m, 1h, then +1h per retry). You can override the retry limit per job:
|
|
2113
2114
|
|
|
@@ -52,7 +52,7 @@ const FIELDS = [
|
|
|
52
52
|
/**
|
|
53
53
|
* Runs the parseCronExpression helper.
|
|
54
54
|
* @param {string} expression - Cron expression or shortcut.
|
|
55
|
-
* @returns {ParsedCron}
|
|
55
|
+
* @returns {ParsedCron} - Parsed cron schedule fields.
|
|
56
56
|
*/
|
|
57
57
|
export function parseCronExpression(expression) {
|
|
58
58
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
@@ -87,8 +87,8 @@ export function parseCronExpression(expression) {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Runs normalize day of week.
|
|
90
|
-
* @param {Set<number>} dayOfWeek
|
|
91
|
-
* @returns {Set<number>}
|
|
90
|
+
* @param {Set<number>} dayOfWeek - Day-of-week values.
|
|
91
|
+
* @returns {Set<number>} - Normalized day-of-week values.
|
|
92
92
|
*/
|
|
93
93
|
function normalizeDayOfWeek(dayOfWeek) {
|
|
94
94
|
if (dayOfWeek.has(7)) {
|
|
@@ -104,7 +104,7 @@ function normalizeDayOfWeek(dayOfWeek) {
|
|
|
104
104
|
* @param {string} field - Field expression.
|
|
105
105
|
* @param {{name: string, min: number, max: number, names?: string[]}} fieldSpec - Field spec.
|
|
106
106
|
* @param {string} expression - Whole cron expression for error messages.
|
|
107
|
-
* @returns {Set<number>}
|
|
107
|
+
* @returns {Set<number>} - Parsed allowed field values.
|
|
108
108
|
*/
|
|
109
109
|
function parseField(field, fieldSpec, expression) {
|
|
110
110
|
const result = new Set()
|
|
@@ -147,7 +147,7 @@ function addPartValues(part, fieldSpec, expression, result) {
|
|
|
147
147
|
* @param {string} value - Step value.
|
|
148
148
|
* @param {{name: string, min: number, max: number}} fieldSpec - Field spec.
|
|
149
149
|
* @param {string} expression - Original expression for errors.
|
|
150
|
-
* @returns {number}
|
|
150
|
+
* @returns {number} - Parsed positive step size.
|
|
151
151
|
*/
|
|
152
152
|
function parseStep(value, fieldSpec, expression) {
|
|
153
153
|
const step = Number(value)
|
|
@@ -165,7 +165,7 @@ function parseStep(value, fieldSpec, expression) {
|
|
|
165
165
|
* @param {{name: string, min: number, max: number, names?: string[]}} fieldSpec - Field spec.
|
|
166
166
|
* @param {string} expression - Original expression for errors.
|
|
167
167
|
* @param {boolean} hasStep - Whether the part had a `/step` suffix.
|
|
168
|
-
* @returns {[number, number]}
|
|
168
|
+
* @returns {[number, number]} - Inclusive field range.
|
|
169
169
|
*/
|
|
170
170
|
function parseRange(rangePart, fieldSpec, expression, hasStep) {
|
|
171
171
|
if (rangePart === "*") {
|
|
@@ -196,7 +196,7 @@ function parseRange(rangePart, fieldSpec, expression, hasStep) {
|
|
|
196
196
|
* @param {string} rawValue - Raw value (may be a name).
|
|
197
197
|
* @param {{name: string, min: number, max: number, names?: string[]}} fieldSpec - Field spec.
|
|
198
198
|
* @param {string} expression - Original expression for errors.
|
|
199
|
-
* @returns {number}
|
|
199
|
+
* @returns {number} - Parsed numeric field value.
|
|
200
200
|
*/
|
|
201
201
|
function parseValue(rawValue, fieldSpec, expression) {
|
|
202
202
|
if (!rawValue) {
|
|
@@ -230,7 +230,7 @@ const MAX_NEXT_FIRE_ITERATIONS = 5 * 366 * 24 * 60
|
|
|
230
230
|
* real time (e.g., `0 0 31 2 *` — Feb 31st).
|
|
231
231
|
* @param {ParsedCron} parsed - Parsed cron expression.
|
|
232
232
|
* @param {Date} from - Reference Date — the next match is strictly after this.
|
|
233
|
-
* @returns {Date}
|
|
233
|
+
* @returns {Date} - Next date matching the expression.
|
|
234
234
|
*/
|
|
235
235
|
export function nextCronFireDate(parsed, from) {
|
|
236
236
|
const candidate = new Date(from.getTime())
|
|
@@ -251,7 +251,7 @@ export function nextCronFireDate(parsed, from) {
|
|
|
251
251
|
* Runs candidate matches.
|
|
252
252
|
* @param {Date} candidate - Candidate Date (in local time).
|
|
253
253
|
* @param {ParsedCron} parsed - Parsed expression.
|
|
254
|
-
* @returns {boolean}
|
|
254
|
+
* @returns {boolean} - Whether the candidate matches the parsed schedule.
|
|
255
255
|
*/
|
|
256
256
|
function candidateMatches(candidate, parsed) {
|
|
257
257
|
if (!parsed.minute.has(candidate.getMinutes())) return false
|
|
@@ -50,9 +50,11 @@ async function connectBeacon(configuration) {
|
|
|
50
50
|
/**
|
|
51
51
|
* Runs run job payload.
|
|
52
52
|
* @param {import("./types.js").BackgroundJobPayload} payload - Payload.
|
|
53
|
+
* @param {object} [options] - Runner options.
|
|
54
|
+
* @param {boolean} [options.closeConnections] - Whether to gracefully close framework connections after the job.
|
|
53
55
|
* @returns {Promise<void>} - Resolves when complete.
|
|
54
56
|
*/
|
|
55
|
-
export default async function runJobPayload(payload) {
|
|
57
|
+
export default async function runJobPayload(payload, {closeConnections = true} = {}) {
|
|
56
58
|
const configuration = await configurationResolver()
|
|
57
59
|
configuration.setCurrent()
|
|
58
60
|
await configuration.initialize({type: "background-jobs-runner"})
|
|
@@ -73,22 +75,13 @@ export default async function runJobPayload(payload) {
|
|
|
73
75
|
await configuration.withConnections({name: `Background job runner: ${payload.jobName}`}, async () => {
|
|
74
76
|
await perform.apply(jobInstance, payload.args || [])
|
|
75
77
|
})
|
|
76
|
-
|
|
77
|
-
if (payload.id) {
|
|
78
|
-
await reporter.reportWithRetry({
|
|
79
|
-
jobId: payload.id,
|
|
80
|
-
status: "completed",
|
|
81
|
-
workerId: payload.workerId,
|
|
82
|
-
handedOffAtMs: payload.handedOffAtMs,
|
|
83
|
-
maxDurationMs: 30000
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
78
|
} catch (error) {
|
|
87
79
|
if (payload.id) {
|
|
88
80
|
await reporter.reportWithRetry({
|
|
89
81
|
jobId: payload.id,
|
|
90
82
|
status: "failed",
|
|
91
83
|
error,
|
|
84
|
+
handoffId: payload.handoffId,
|
|
92
85
|
workerId: payload.workerId,
|
|
93
86
|
handedOffAtMs: payload.handedOffAtMs,
|
|
94
87
|
maxDurationMs: 30000
|
|
@@ -97,11 +90,24 @@ export default async function runJobPayload(payload) {
|
|
|
97
90
|
|
|
98
91
|
throw error
|
|
99
92
|
}
|
|
93
|
+
|
|
94
|
+
if (payload.id) {
|
|
95
|
+
await reporter.reportWithRetry({
|
|
96
|
+
jobId: payload.id,
|
|
97
|
+
status: "completed",
|
|
98
|
+
handoffId: payload.handoffId,
|
|
99
|
+
workerId: payload.workerId,
|
|
100
|
+
handedOffAtMs: payload.handedOffAtMs,
|
|
101
|
+
maxDurationMs: 30000
|
|
102
|
+
})
|
|
103
|
+
}
|
|
100
104
|
} finally {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
if (closeConnections) {
|
|
106
|
+
try {
|
|
107
|
+
await configuration.disconnectBeacon()
|
|
108
|
+
} finally {
|
|
109
|
+
await configuration.closeDatabaseConnections()
|
|
110
|
+
}
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
}
|
|
@@ -14,6 +14,10 @@ export default class JsonSocket extends EventEmitter {
|
|
|
14
14
|
* Narrows the runtime value to the documented type.
|
|
15
15
|
* @type {string | undefined} */
|
|
16
16
|
this.workerId = undefined
|
|
17
|
+
/**
|
|
18
|
+
* Narrows the runtime value to the documented type.
|
|
19
|
+
* @type {boolean} */
|
|
20
|
+
this.supportsHandoffIdReporting = false
|
|
17
21
|
/**
|
|
18
22
|
* Narrows the runtime value to the documented type.
|
|
19
23
|
* @type {boolean} */
|
|
@@ -64,25 +64,29 @@ export default class BackgroundJobsMain {
|
|
|
64
64
|
* Narrows the runtime value to the documented type.
|
|
65
65
|
* @type {Set<JsonSocket>} */
|
|
66
66
|
this.readyWorkers = new Set()
|
|
67
|
+
/**
|
|
68
|
+
* Active durable handoffs keyed by the exact worker socket that received them.
|
|
69
|
+
* @type {Map<JsonSocket, Map<string, string>>} */
|
|
70
|
+
this.workerHandoffs = new Map()
|
|
67
71
|
/**
|
|
68
72
|
* Narrows the runtime value to the documented type.
|
|
69
73
|
* @type {net.Server | undefined} */
|
|
70
74
|
this.server = undefined
|
|
71
75
|
/**
|
|
72
76
|
* Narrows the runtime value to the documented type.
|
|
73
|
-
* @type {
|
|
77
|
+
* @type {ReturnType<typeof setTimeout> | undefined} */
|
|
74
78
|
this._pollTimer = undefined
|
|
75
79
|
/**
|
|
76
80
|
* Narrows the runtime value to the documented type.
|
|
77
|
-
* @type {
|
|
81
|
+
* @type {ReturnType<typeof setTimeout> | undefined} */
|
|
78
82
|
this._scheduledTimer = undefined
|
|
79
83
|
/**
|
|
80
84
|
* Narrows the runtime value to the documented type.
|
|
81
|
-
* @type {
|
|
85
|
+
* @type {ReturnType<typeof setTimeout> | undefined} */
|
|
82
86
|
this._errorRetryTimer = undefined
|
|
83
87
|
/**
|
|
84
88
|
* Narrows the runtime value to the documented type.
|
|
85
|
-
* @type {
|
|
89
|
+
* @type {ReturnType<typeof setTimeout> | undefined} */
|
|
86
90
|
this._orphanTimer = undefined
|
|
87
91
|
/**
|
|
88
92
|
* Narrows the runtime value to the documented type.
|
|
@@ -270,7 +274,7 @@ export default class BackgroundJobsMain {
|
|
|
270
274
|
_setupDispatchTriggers() {
|
|
271
275
|
if (this.dispatchStrategy === "polling") {
|
|
272
276
|
this._pollTimer = setInterval(() => {
|
|
273
|
-
void this.
|
|
277
|
+
void this._retryAfterError()
|
|
274
278
|
}, this.pollIntervalMs)
|
|
275
279
|
return
|
|
276
280
|
}
|
|
@@ -330,11 +334,12 @@ export default class BackgroundJobsMain {
|
|
|
330
334
|
* @type {import("./types.js").BackgroundJobSocketRole | null} */
|
|
331
335
|
let role = null
|
|
332
336
|
|
|
337
|
+
let cleanedUp = false
|
|
333
338
|
const cleanup = () => {
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
if (cleanedUp) return
|
|
340
|
+
cleanedUp = true
|
|
341
|
+
|
|
342
|
+
if (role === "worker") void this._handleWorkerSocketClosed(jsonSocket)
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
jsonSocket.on("close", cleanup)
|
|
@@ -377,7 +382,9 @@ export default class BackgroundJobsMain {
|
|
|
377
382
|
|
|
378
383
|
if (message.role === "worker") {
|
|
379
384
|
jsonSocket.workerId = message.workerId
|
|
385
|
+
jsonSocket.supportsHandoffIdReporting = message.supportsHandoffIdReporting === true
|
|
380
386
|
this.workers.add(jsonSocket)
|
|
387
|
+
this.workerHandoffs.set(jsonSocket, new Map())
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
return message.role
|
|
@@ -446,7 +453,11 @@ export default class BackgroundJobsMain {
|
|
|
446
453
|
jsonSocket.acceptsSpawnedJobs = message.acceptsSpawned !== false && message.acceptsForked !== false
|
|
447
454
|
jsonSocket.acceptsForkedJobs = message.acceptsForked !== false
|
|
448
455
|
jsonSocket.acceptsInlineJobs = message.acceptsInline !== false
|
|
449
|
-
|
|
456
|
+
if (jsonSocket.supportsHandoffIdReporting) {
|
|
457
|
+
this.readyWorkers.add(jsonSocket)
|
|
458
|
+
} else {
|
|
459
|
+
this.readyWorkers.delete(jsonSocket)
|
|
460
|
+
}
|
|
450
461
|
void this._drain()
|
|
451
462
|
}
|
|
452
463
|
|
|
@@ -463,6 +474,98 @@ export default class BackgroundJobsMain {
|
|
|
463
474
|
this.readyWorkers.delete(jsonSocket)
|
|
464
475
|
}
|
|
465
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Removes a lost worker socket and releases only leases dispatched through it.
|
|
479
|
+
* @param {JsonSocket} worker - Disconnected worker socket.
|
|
480
|
+
* @returns {Promise<void>} - Resolves after its active leases are released.
|
|
481
|
+
*/
|
|
482
|
+
async _handleWorkerSocketClosed(worker) {
|
|
483
|
+
this.workers.delete(worker)
|
|
484
|
+
this.readyWorkers.delete(worker)
|
|
485
|
+
|
|
486
|
+
if (this._stopped) {
|
|
487
|
+
this.workerHandoffs.delete(worker)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
await this._releaseWorkerHandoffs(worker)
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this._reportHandoffReleaseError(error)
|
|
495
|
+
this._scheduleErrorRetry()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Releases all leases still owned by one exact worker socket.
|
|
501
|
+
* @param {JsonSocket} worker - Worker socket.
|
|
502
|
+
* @returns {Promise<void>} - Resolves after fenced releases and dispatch wake-up.
|
|
503
|
+
*/
|
|
504
|
+
async _releaseWorkerHandoffs(worker) {
|
|
505
|
+
const handoffs = this.workerHandoffs.get(worker)
|
|
506
|
+
|
|
507
|
+
if (!handoffs || handoffs.size === 0) {
|
|
508
|
+
this.workerHandoffs.delete(worker)
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const [jobId, handoffId] of handoffs) {
|
|
513
|
+
await this._releaseHandoff({handoffId, jobId, worker})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.workerHandoffs.delete(worker)
|
|
517
|
+
this._notifyEnqueued()
|
|
518
|
+
await this._drain()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Runs one idempotent conditional lease release.
|
|
523
|
+
* @param {object} args - Options.
|
|
524
|
+
* @param {string} args.handoffId - Handoff lease id.
|
|
525
|
+
* @param {string} args.jobId - Job id.
|
|
526
|
+
* @param {JsonSocket} args.worker - Socket that received the lease.
|
|
527
|
+
* @returns {Promise<void>} - Resolves after the fenced transition.
|
|
528
|
+
*/
|
|
529
|
+
async _releaseHandoff({handoffId, jobId, worker}) {
|
|
530
|
+
await this.store.markReturnedToQueue({handoffId, jobId})
|
|
531
|
+
|
|
532
|
+
const handoffs = this.workerHandoffs.get(worker)
|
|
533
|
+
|
|
534
|
+
if (handoffs?.get(jobId) === handoffId) handoffs.delete(jobId)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Forgets a successfully reported lease without relying on worker ids.
|
|
539
|
+
* @param {object} args - Options.
|
|
540
|
+
* @param {string} args.handoffId - Handoff lease id.
|
|
541
|
+
* @param {string} args.jobId - Job id.
|
|
542
|
+
* @returns {void}
|
|
543
|
+
*/
|
|
544
|
+
_forgetHandoff({handoffId, jobId}) {
|
|
545
|
+
for (const [worker, handoffs] of this.workerHandoffs) {
|
|
546
|
+
if (handoffs.get(jobId) !== handoffId) continue
|
|
547
|
+
|
|
548
|
+
handoffs.delete(jobId)
|
|
549
|
+
if (handoffs.size === 0 && !this.workers.has(worker)) this.workerHandoffs.delete(worker)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Reports an unexpected lease-release failure on framework error channels.
|
|
556
|
+
* @param {?} error - Release failure.
|
|
557
|
+
* @returns {void}
|
|
558
|
+
*/
|
|
559
|
+
_reportHandoffReleaseError(error) {
|
|
560
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
561
|
+
const payload = {context: {stage: "background-job-handoff-release"}, error: normalizedError}
|
|
562
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
563
|
+
|
|
564
|
+
this.logger.error(() => ["Failed to release disconnected worker handoffs:", normalizedError])
|
|
565
|
+
errorEvents.emit("framework-error", payload)
|
|
566
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
567
|
+
}
|
|
568
|
+
|
|
466
569
|
/**
|
|
467
570
|
* Runs handle enqueue.
|
|
468
571
|
* @param {object} args - Options.
|
|
@@ -496,11 +599,15 @@ export default class BackgroundJobsMain {
|
|
|
496
599
|
*/
|
|
497
600
|
async _handleJobComplete({jsonSocket, message}) {
|
|
498
601
|
try {
|
|
499
|
-
await this.store.markCompleted({
|
|
602
|
+
const accepted = await this.store.markCompleted({
|
|
500
603
|
jobId: message.jobId,
|
|
604
|
+
handoffId: message.handoffId,
|
|
501
605
|
workerId: message.workerId,
|
|
502
606
|
handedOffAtMs: message.handedOffAtMs
|
|
503
607
|
})
|
|
608
|
+
if (accepted && message.handoffId) {
|
|
609
|
+
this._forgetHandoff({handoffId: message.handoffId, jobId: message.jobId})
|
|
610
|
+
}
|
|
504
611
|
jsonSocket.send({type: "job-updated", jobId: message.jobId})
|
|
505
612
|
} catch (error) {
|
|
506
613
|
this.logger.error(() => ["Failed to update job completion:", error])
|
|
@@ -520,13 +627,18 @@ export default class BackgroundJobsMain {
|
|
|
520
627
|
const failedJob = await this.store.markFailed({
|
|
521
628
|
jobId: message.jobId,
|
|
522
629
|
error: message.error,
|
|
630
|
+
handoffId: message.handoffId,
|
|
523
631
|
workerId: message.workerId,
|
|
524
632
|
handedOffAtMs: message.handedOffAtMs
|
|
525
633
|
})
|
|
526
634
|
|
|
527
635
|
if (failedJob) {
|
|
636
|
+
if (message.handoffId) {
|
|
637
|
+
this._forgetHandoff({handoffId: message.handoffId, jobId: message.jobId})
|
|
638
|
+
}
|
|
528
639
|
this._emitBackgroundJobFailed({
|
|
529
640
|
error: message.error,
|
|
641
|
+
handoffId: message.handoffId,
|
|
530
642
|
handedOffAtMs: message.handedOffAtMs,
|
|
531
643
|
job: failedJob,
|
|
532
644
|
workerId: message.workerId
|
|
@@ -546,14 +658,15 @@ export default class BackgroundJobsMain {
|
|
|
546
658
|
|
|
547
659
|
/**
|
|
548
660
|
* Runs emit background job failed.
|
|
549
|
-
* @param {{error: ?, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, workerId?: string}} args - Failure event data.
|
|
661
|
+
* @param {{error: ?, handoffId?: string, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, workerId?: string}} args - Failure event data.
|
|
550
662
|
* @returns {void}
|
|
551
663
|
*/
|
|
552
|
-
_emitBackgroundJobFailed({error, handedOffAtMs, job, workerId}) {
|
|
664
|
+
_emitBackgroundJobFailed({error, handoffId, handedOffAtMs, job, workerId}) {
|
|
553
665
|
const normalizedError = this._normalizeFailureError(error)
|
|
554
666
|
const payload = {
|
|
555
667
|
context: {
|
|
556
668
|
attempts: job.attempts,
|
|
669
|
+
handoffId,
|
|
557
670
|
handedOffAtMs,
|
|
558
671
|
jobArgs: job.args,
|
|
559
672
|
jobId: job.id,
|
|
@@ -699,6 +812,10 @@ export default class BackgroundJobsMain {
|
|
|
699
812
|
* Runs clear error retry timer.
|
|
700
813
|
* @returns {void} */
|
|
701
814
|
_clearErrorRetryTimer() {
|
|
815
|
+
for (const worker of this.workerHandoffs.keys()) {
|
|
816
|
+
if (!this.workers.has(worker)) return
|
|
817
|
+
}
|
|
818
|
+
|
|
702
819
|
if (this._errorRetryTimer) {
|
|
703
820
|
clearTimeout(this._errorRetryTimer)
|
|
704
821
|
this._errorRetryTimer = undefined
|
|
@@ -771,10 +888,30 @@ export default class BackgroundJobsMain {
|
|
|
771
888
|
|
|
772
889
|
this._errorRetryTimer = setTimeout(() => {
|
|
773
890
|
this._errorRetryTimer = undefined
|
|
774
|
-
void this.
|
|
891
|
+
void this._retryAfterError()
|
|
775
892
|
}, this.pollIntervalMs)
|
|
776
893
|
}
|
|
777
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Retries failed disconnected-socket releases before draining queued work.
|
|
897
|
+
* @returns {Promise<void>} - Resolves after retry work.
|
|
898
|
+
*/
|
|
899
|
+
async _retryAfterError() {
|
|
900
|
+
if (this._stopped) return
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
for (const worker of this.workerHandoffs.keys()) {
|
|
904
|
+
if (!this.workers.has(worker)) await this._releaseWorkerHandoffs(worker)
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
this._reportHandoffReleaseError(error)
|
|
908
|
+
this._scheduleErrorRetry()
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
await this._drain()
|
|
913
|
+
}
|
|
914
|
+
|
|
778
915
|
/**
|
|
779
916
|
* Inner drain loop: pulls eligible queued jobs and hands them off to
|
|
780
917
|
* ready workers until one of them runs out.
|
|
@@ -790,7 +927,23 @@ export default class BackgroundJobsMain {
|
|
|
790
927
|
|
|
791
928
|
this.readyWorkers.delete(worker)
|
|
792
929
|
|
|
793
|
-
const
|
|
930
|
+
const handoff = await this.store.markHandedOff({jobId: job.id, workerId: worker.workerId})
|
|
931
|
+
|
|
932
|
+
if (!handoff) {
|
|
933
|
+
if (this.workers.has(worker)) this.readyWorkers.add(worker)
|
|
934
|
+
continue
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const handoffs = this.workerHandoffs.get(worker)
|
|
938
|
+
|
|
939
|
+
if (!handoffs || !this.workers.has(worker)) {
|
|
940
|
+
await this.store.markReturnedToQueue({handoffId: handoff.handoffId, jobId: job.id})
|
|
941
|
+
this._notifyEnqueued()
|
|
942
|
+
await this._drain()
|
|
943
|
+
continue
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
handoffs.set(job.id, handoff.handoffId)
|
|
794
947
|
|
|
795
948
|
try {
|
|
796
949
|
worker.send({
|
|
@@ -799,8 +952,9 @@ export default class BackgroundJobsMain {
|
|
|
799
952
|
id: job.id,
|
|
800
953
|
jobName: job.jobName,
|
|
801
954
|
args: job.args,
|
|
955
|
+
handoffId: handoff.handoffId,
|
|
802
956
|
workerId: worker.workerId,
|
|
803
|
-
handedOffAtMs,
|
|
957
|
+
handedOffAtMs: handoff.handedOffAtMs,
|
|
804
958
|
options: {
|
|
805
959
|
executionMode: job.executionMode,
|
|
806
960
|
forked: job.forked
|
|
@@ -809,8 +963,12 @@ export default class BackgroundJobsMain {
|
|
|
809
963
|
})
|
|
810
964
|
} catch (error) {
|
|
811
965
|
this.logger.warn(() => ["Failed to send job to worker, re-queueing:", error])
|
|
812
|
-
|
|
813
|
-
|
|
966
|
+
try {
|
|
967
|
+
worker.close()
|
|
968
|
+
} catch (closeError) {
|
|
969
|
+
this.logger.warn(() => ["Failed to close worker after job send failure:", closeError])
|
|
970
|
+
}
|
|
971
|
+
await this._handleWorkerSocketClosed(worker)
|
|
814
972
|
}
|
|
815
973
|
}
|
|
816
974
|
}
|
|
@@ -850,6 +1008,8 @@ export default class BackgroundJobsMain {
|
|
|
850
1008
|
* @returns {void}
|
|
851
1009
|
*/
|
|
852
1010
|
_addAcceptedExecutionModes({executionModes, worker}) {
|
|
1011
|
+
if (!worker.supportsHandoffIdReporting) return
|
|
1012
|
+
|
|
853
1013
|
for (const capability of WORKER_EXECUTION_MODE_CAPABILITIES) {
|
|
854
1014
|
if (capability.accepts(worker)) executionModes.add(capability.executionMode)
|
|
855
1015
|
}
|
|
@@ -874,6 +1034,8 @@ export default class BackgroundJobsMain {
|
|
|
874
1034
|
* @returns {boolean} - Whether the worker accepts the job mode.
|
|
875
1035
|
*/
|
|
876
1036
|
_workerAcceptsJob({job, worker}) {
|
|
1037
|
+
if (!worker.supportsHandoffIdReporting) return false
|
|
1038
|
+
|
|
877
1039
|
const capability = WORKER_EXECUTION_MODE_CAPABILITIES_BY_MODE.get(job.executionMode)
|
|
878
1040
|
|
|
879
1041
|
if (!capability) return false
|
|
@@ -6,6 +6,8 @@ import Logger from "../logger.js"
|
|
|
6
6
|
import normalizeBackgroundJobError from "./normalize-error.js"
|
|
7
7
|
import BackgroundJobsSocketRequest from "./socket-request.js"
|
|
8
8
|
|
|
9
|
+
class BackgroundJobUpdateError extends Error {}
|
|
10
|
+
|
|
9
11
|
export default class BackgroundJobsStatusReporter {
|
|
10
12
|
/**
|
|
11
13
|
* Runs constructor.
|
|
@@ -27,11 +29,12 @@ export default class BackgroundJobsStatusReporter {
|
|
|
27
29
|
* @param {string} args.jobId - Job id.
|
|
28
30
|
* @param {"completed" | "failed"} args.status - Status.
|
|
29
31
|
* @param {?} [args.error] - Error.
|
|
32
|
+
* @param {string} [args.handoffId] - Handoff lease id.
|
|
30
33
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
31
34
|
* @param {string} [args.workerId] - Worker id.
|
|
32
35
|
* @returns {Promise<void>} - Resolves when reported.
|
|
33
36
|
*/
|
|
34
|
-
async report({jobId, status, error, handedOffAtMs, workerId}) {
|
|
37
|
+
async report({jobId, status, error, handoffId, handedOffAtMs, workerId}) {
|
|
35
38
|
const config = this.configuration.getBackgroundJobsConfig()
|
|
36
39
|
const host = this.host || config.host
|
|
37
40
|
const port = typeof this.port === "number" ? this.port : config.port
|
|
@@ -44,6 +47,7 @@ export default class BackgroundJobsStatusReporter {
|
|
|
44
47
|
jsonSocket.send({
|
|
45
48
|
type: status === "completed" ? "job-complete" : "job-failed",
|
|
46
49
|
jobId,
|
|
50
|
+
handoffId,
|
|
47
51
|
workerId,
|
|
48
52
|
handedOffAtMs,
|
|
49
53
|
error: error ? normalizeBackgroundJobError(error) : undefined
|
|
@@ -56,7 +60,7 @@ export default class BackgroundJobsStatusReporter {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
if (message?.type === "job-update-error" && message.jobId === jobId) {
|
|
59
|
-
reject(new
|
|
63
|
+
reject(new BackgroundJobUpdateError(message.error || "Job update failed"))
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
})
|
|
@@ -69,20 +73,23 @@ export default class BackgroundJobsStatusReporter {
|
|
|
69
73
|
* @param {string} args.jobId - Job id.
|
|
70
74
|
* @param {"completed" | "failed"} args.status - Status.
|
|
71
75
|
* @param {?} [args.error] - Error.
|
|
76
|
+
* @param {string} [args.handoffId] - Handoff lease id.
|
|
72
77
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
73
78
|
* @param {string} [args.workerId] - Worker id.
|
|
74
79
|
* @param {number} [args.maxDurationMs] - Max duration for retries.
|
|
75
80
|
* @returns {Promise<void>} - Resolves when reported.
|
|
76
81
|
*/
|
|
77
|
-
async reportWithRetry({jobId, status, error, handedOffAtMs, workerId, maxDurationMs}) {
|
|
82
|
+
async reportWithRetry({jobId, status, error, handoffId, handedOffAtMs, workerId, maxDurationMs}) {
|
|
78
83
|
let attempt = 0
|
|
79
84
|
const startTime = Date.now()
|
|
80
85
|
|
|
81
86
|
while (true) {
|
|
82
87
|
try {
|
|
83
|
-
await this.report({jobId, status, error, handedOffAtMs, workerId})
|
|
88
|
+
await this.report({jobId, status, error, handoffId, handedOffAtMs, workerId})
|
|
84
89
|
return
|
|
85
90
|
} catch (error) {
|
|
91
|
+
if (error instanceof BackgroundJobUpdateError) throw error
|
|
92
|
+
|
|
86
93
|
attempt += 1
|
|
87
94
|
const delaySeconds = Math.min(30, 0.5 * attempt)
|
|
88
95
|
|
|
@@ -90,7 +97,7 @@ export default class BackgroundJobsStatusReporter {
|
|
|
90
97
|
|
|
91
98
|
if (maxDurationMs && Date.now() - startTime >= maxDurationMs) {
|
|
92
99
|
this.logger.warn(() => ["Background job status report timed out, giving up", error])
|
|
93
|
-
|
|
100
|
+
throw error
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
await wait(delaySeconds)
|