From fc4e216389ab34cae60cc62f11055f64ff34e52a Mon Sep 17 00:00:00 2001 From: merencia Date: Fri, 19 Jun 2026 17:19:38 -0300 Subject: [PATCH 1/2] docs: document execution modes and cooperative timeout/cancellation Adds a new "Execution Modes" page covering the fork and runner options, the four fork/runner combinations and when to use each (serverless, tests, SQLite, framework integrations), abortGracePeriodMs, and the cooperative timeout/cancellation model via this.abortSignal. Includes disclaimers for the sharp edges: no-fork removes crash isolation; inline jobs block the event loop and cannot be force-stopped (so timeouts and cancellation only work if the job honors this.abortSignal); grace=0 gives no cooperative window in thread mode; canceling a running inline job is best-effort. Also documents this.abortSignal in the job control page, the new options in the configuration reference, and cross-links from how-it-works and the lifecycle page. --- packages/docs/.vitepress/config.mts | 1 + .../docs/getting-started/configuration.md | 58 ++--- packages/docs/guide/jobs/lifecycle.md | 6 +- packages/docs/guide/jobs/running.md | 63 ++++-- packages/docs/introduction/how-it-works.md | 4 + packages/docs/production/execution-modes.md | 200 ++++++++++++++++++ 6 files changed, 288 insertions(+), 44 deletions(-) create mode 100644 packages/docs/production/execution-modes.md diff --git a/packages/docs/.vitepress/config.mts b/packages/docs/.vitepress/config.mts index fdac912f..f341bd3b 100644 --- a/packages/docs/.vitepress/config.mts +++ b/packages/docs/.vitepress/config.mts @@ -99,6 +99,7 @@ export default defineConfig({ collapsed: false, items: [ { text: "Backends", link: "/backends" }, + { text: "Execution Modes", link: "/execution-modes" }, { text: "Graceful Shutdown", link: "/graceful-shutdown" }, { text: "Cleanup", link: "/cleanup" }, { text: "Manual Job Resolution", link: "/manual-resolution" }, diff --git a/packages/docs/getting-started/configuration.md b/packages/docs/getting-started/configuration.md index 8be70380..442b4f95 100644 --- a/packages/docs/getting-started/configuration.md +++ b/packages/docs/getting-started/configuration.md @@ -122,6 +122,9 @@ await Sidequest.start({ minThreads: 4, maxThreads: 8, idleWorkerTimeout: 10000, // 10 seconds + fork: true, // run the engine in a child process + runner: "thread", // "thread" (worker pool) or "inline" + abortGracePeriodMs: 0, // grace before force-killing a timed-out/canceled thread job // 4. Migration and startup skipMigration: false, @@ -179,32 +182,35 @@ await Sidequest.start({ ### Configuration Options -| Option | Description | Default | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -| `backend.driver` | Backend driver package name (SQLite, Postgres, MySQL, MongoDB) | `@sidequest/sqlite-backend` | -| `backend.config` | Backend-specific connection string or [Knex configuration object](https://knexjs.org/guide/#configuration-options) | `./sidequest.sqlite` | -| `dashboard.enabled` | Whether to enable the dashboard web interface | `true` | -| `dashboard.port` | Port for the dashboard web interface | `8678` | -| `dashboard.auth` | Basic auth configuration with `user` and `password`. If omitted, no auth is required. | `undefined` | -| `queues` | Array of queue configurations with name, concurrency, priority, and state | `[]` | -| `maxConcurrentJobs` | Maximum number of jobs processed simultaneously across all queues | `10` | -| `minThreads` | Minimum number of worker threads to use | Number of CPU cores | -| `maxThreads` | Maximum number of worker threads to use | `minThreads * 2` | -| `idleWorkerTimeout` | Timeout (milliseconds) for idle workers before they are terminated | `10000` (10 seconds) | -| `skipMigration` | Whether to skip database migration on startup | `false` | -| `releaseStaleJobsIntervalMin` | Frequency (minutes) for releasing stale jobs. Set to `false` to disable | `60` | -| `releaseStaleJobsMaxStaleMs` | Maximum age (milliseconds) for a running job to be considered stale | `600000` (10 minutes) | -| `releaseStaleJobsMaxClaimedMs` | Maximum age (milliseconds) for a claimed job to be considered stale | `60000` (1 minute) | -| `cleanupFinishedJobsIntervalMin` | Frequency (minutes) for cleaning up finished jobs. Set to `false` to disable | `60` | -| `cleanupFinishedJobsOlderThan` | Age (milliseconds) after which finished jobs are deleted | `2592000000` (30 days) | -| `logger.level` | Minimum log level (`debug`, `info`, `warn`, `error`) | `info` | -| `logger.json` | Whether to output logs in JSON format | `false` | -| `gracefulShutdown` | Whether to enable graceful shutdown handling | `true` | -| `jobDefaults` | Default values for new jobs. Used while enqueueing | `undefined` | -| `queueDefaults` | Default values for auto-created queues | `undefined` | -| `manualJobResolution` | Whether to manually resolve job classes. See [Manual Job Resolution](/production/manual-resolution) | `false` | -| `jobsFilePath` | Optional path to the file where job classes are exported. Ignored if `manualJobResolution` is `false`. | `undefined` | -| `jobPollingInterval` | Interval (milliseconds) for polling new jobs to process. Increase this number to reduce DB load at the cost of job start latency. | `100` (100 milliseconds) | +| Option | Description | Default | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| `backend.driver` | Backend driver package name (SQLite, Postgres, MySQL, MongoDB) | `@sidequest/sqlite-backend` | +| `backend.config` | Backend-specific connection string or [Knex configuration object](https://knexjs.org/guide/#configuration-options) | `./sidequest.sqlite` | +| `dashboard.enabled` | Whether to enable the dashboard web interface | `true` | +| `dashboard.port` | Port for the dashboard web interface | `8678` | +| `dashboard.auth` | Basic auth configuration with `user` and `password`. If omitted, no auth is required. | `undefined` | +| `queues` | Array of queue configurations with name, concurrency, priority, and state | `[]` | +| `maxConcurrentJobs` | Maximum number of jobs processed simultaneously across all queues | `10` | +| `fork` | Run the engine in a child process (crash isolation). Set `false` to run in-process. See [Execution Modes](/production/execution-modes#fork-process-isolation) | `true` | +| `runner` | How jobs run: `"thread"` (worker pool) or `"inline"` (current thread). See [Execution Modes](/production/execution-modes#runner-thread-pool-vs-inline) | `"thread"` | +| `abortGracePeriodMs` | Grace period (ms) before a timed-out/canceled job's worker **thread** is force-killed. `0` kills immediately. No effect with `runner: "inline"`. See [Execution Modes](/production/execution-modes#cooperative-timeout-and-cancellation) | `0` | +| `minThreads` | Minimum number of worker threads to use (`runner: "thread"` only) | Number of CPU cores | +| `maxThreads` | Maximum number of worker threads to use (`runner: "thread"` only) | `minThreads * 2` | +| `idleWorkerTimeout` | Timeout (milliseconds) for idle workers before they are terminated (`runner: "thread"` only) | `10000` (10 seconds) | +| `skipMigration` | Whether to skip database migration on startup | `false` | +| `releaseStaleJobsIntervalMin` | Frequency (minutes) for releasing stale jobs. Set to `false` to disable | `60` | +| `releaseStaleJobsMaxStaleMs` | Maximum age (milliseconds) for a running job to be considered stale | `600000` (10 minutes) | +| `releaseStaleJobsMaxClaimedMs` | Maximum age (milliseconds) for a claimed job to be considered stale | `60000` (1 minute) | +| `cleanupFinishedJobsIntervalMin` | Frequency (minutes) for cleaning up finished jobs. Set to `false` to disable | `60` | +| `cleanupFinishedJobsOlderThan` | Age (milliseconds) after which finished jobs are deleted | `2592000000` (30 days) | +| `logger.level` | Minimum log level (`debug`, `info`, `warn`, `error`) | `info` | +| `logger.json` | Whether to output logs in JSON format | `false` | +| `gracefulShutdown` | Whether to enable graceful shutdown handling | `true` | +| `jobDefaults` | Default values for new jobs. Used while enqueueing | `undefined` | +| `queueDefaults` | Default values for auto-created queues | `undefined` | +| `manualJobResolution` | Whether to manually resolve job classes. See [Manual Job Resolution](/production/manual-resolution) | `false` | +| `jobsFilePath` | Optional path to the file where job classes are exported. Ignored if `manualJobResolution` is `false`. | `undefined` | +| `jobPollingInterval` | Interval (milliseconds) for polling new jobs to process. Increase this number to reduce DB load at the cost of job start latency. | `100` (100 milliseconds) | ::: danger If `auth` is not configured and `dashboard: true` is enabled in production, the dashboard will be publicly accessible. This is a security risk and **not recommended**. diff --git a/packages/docs/guide/jobs/lifecycle.md b/packages/docs/guide/jobs/lifecycle.md index 304d8544..53ab0d5e 100644 --- a/packages/docs/guide/jobs/lifecycle.md +++ b/packages/docs/guide/jobs/lifecycle.md @@ -67,7 +67,11 @@ Jobs can be manually canceled at any point before completion: - Waiting jobs are immediately marked as `canceled` - Claimed jobs are marked as `canceled` before execution an are prevented from running -- Running jobs receive a cancellation signal and transition to `canceled` +- Running jobs receive a cancellation signal via `this.abortSignal` and transition to `canceled` + +::: warning +Stopping a _running_ job depends on the [execution mode](/production/execution-modes#cooperative-timeout-and-cancellation). With the default thread pool the worker is terminated. In `runner: "inline"` mode the job cannot be force-stopped, so it must honor `this.abortSignal`; a running inline job that ignores it finishes with its own result instead of `canceled`. The same applies to job timeouts. +::: ## Best Practices diff --git a/packages/docs/guide/jobs/running.md b/packages/docs/guide/jobs/running.md index cf2e6c27..09a9fbe7 100644 --- a/packages/docs/guide/jobs/running.md +++ b/packages/docs/guide/jobs/running.md @@ -35,15 +35,16 @@ When you need finer control — fail without retrying, retry with a custom delay Before `run()` executes, Sidequest injects read-only properties onto `this`: -| Property | Type | Description | -| ------------------- | ----------- | -------------------------------- | -| `this.id` | `string` | Job ID | -| `this.attempt` | `number` | Current attempt number (1-based) | -| `this.max_attempts` | `number` | Maximum allowed attempts | -| `this.queue` | `string` | Queue the job is running in | -| `this.state` | `string` | Current state (`"running"`) | -| `this.inserted_at` | `Date` | When the job was first enqueued | -| `this.args` | `unknown[]` | The run arguments | +| Property | Type | Description | +| ------------------- | ------------- | --------------------------------------------------------------------------------------------------- | +| `this.id` | `string` | Job ID | +| `this.attempt` | `number` | Current attempt number (1-based) | +| `this.max_attempts` | `number` | Maximum allowed attempts | +| `this.queue` | `string` | Queue the job is running in | +| `this.state` | `string` | Current state (`"running"`) | +| `this.inserted_at` | `Date` | When the job was first enqueued | +| `this.args` | `unknown[]` | The run arguments | +| `this.abortSignal` | `AbortSignal` | Aborts when the job times out or is canceled. See [below](#responding-to-timeout-and-cancellation). | ::: warning These properties are only available inside `run()`. They are `undefined` in the constructor. @@ -57,9 +58,10 @@ These methods let you explicitly transition the job to a specific lifecycle stat You must **`return`** the result of every flow control method. Calling one without returning it is a no-op — the transition won't happen. ```typescript -this.fail("reason"); // ❌ does nothing +this.fail("reason"); // ❌ does nothing return this.fail("reason"); // ✅ transitions to failed ``` + ::: ### `return this.complete(result)` @@ -129,15 +131,42 @@ async run(payload: unknown) { Use `snooze` for time-based deferrals: rate limit windows, maintenance modes, business hours. +## Responding to timeout and cancellation + +When a job exceeds its `timeout`, or is canceled (via the dashboard or `Sidequest.job.cancel(id)`), Sidequest aborts `this.abortSignal`. Use it to stop your work promptly: + +```typescript +async run(url: string) { + // Pass it to any abort-aware API; it cancels automatically. + const res = await fetch(url, { signal: this.abortSignal }); + + // Or check it cooperatively in loops / between steps. + for (const item of await res.json()) { + this.abortSignal.throwIfAborted(); // throws if timed out / canceled + await process(item); + } +} +``` + +`this.abortSignal.reason` is a `JobTimeout` or `JobCanceled` (both exported from `sidequest`) so you can react differently to each. + +::: danger Whether the signal can actually stop the job depends on the execution mode + +- In the default thread pool with `abortGracePeriodMs: 0`, the worker is terminated, so honoring the signal is optional (it just lets you clean up; set a grace period to get a cooperative window). +- In **`runner: "inline"` mode there is no way to forcibly stop a job.** If your job ignores `this.abortSignal`, timeouts and cancellation **will not stop it**: it runs to completion. Honoring the signal is mandatory for long-running inline jobs. + +See [Execution Modes](/production/execution-modes#cooperative-timeout-and-cancellation) for the full behavior across modes. +::: + ## Choosing the right method -| Situation | Use | -|---|---| -| Normal completion | `return result` or `return this.complete(result)` | -| Permanent, unrecoverable error | `return this.fail(reason)` | -| Transient error, controlled retry delay | `return this.retry(reason, delay)` | -| Not the right time — try again later | `return this.snooze(delay)` | -| Unexpected error — let Sidequest decide | `throw error` | +| Situation | Use | +| --------------------------------------- | ------------------------------------------------- | +| Normal completion | `return result` or `return this.complete(result)` | +| Permanent, unrecoverable error | `return this.fail(reason)` | +| Transient error, controlled retry delay | `return this.retry(reason, delay)` | +| Not the right time — try again later | `return this.snooze(delay)` | +| Unexpected error — let Sidequest decide | `throw error` | ## Best practices diff --git a/packages/docs/introduction/how-it-works.md b/packages/docs/introduction/how-it-works.md index bccf7c42..771b16c7 100644 --- a/packages/docs/introduction/how-it-works.md +++ b/packages/docs/introduction/how-it-works.md @@ -30,6 +30,10 @@ Your app process Because the engine is a separate process, a job that calls `process.exit()` or throws an unhandled exception will kill the engine process but **not your app**. The engine restarts automatically. +::: tip +This forked, worker-thread model is the default and the right choice for most deployments. For serverless runtimes, test suites, or framework integrations that need jobs to share live in-process state, you can run the engine in-process and/or run jobs inline. See [Execution Modes](/production/execution-modes). +::: + ## How jobs are claimed The Dispatcher polls the database at a configurable interval (default: **100 ms**). When it finds waiting jobs that fit within queue concurrency limits, it claims them atomically: diff --git a/packages/docs/production/execution-modes.md b/packages/docs/production/execution-modes.md new file mode 100644 index 00000000..f99028d8 --- /dev/null +++ b/packages/docs/production/execution-modes.md @@ -0,0 +1,200 @@ +--- +outline: deep +title: Execution Modes +description: Choose how and where Sidequest runs your jobs (forked vs in-process, thread pool vs inline) and how cooperative timeout/cancellation works. +--- + +# Execution Modes + +By default Sidequest runs your jobs with **two layers of isolation**: the engine runs in a forked child process, and each job runs in its own worker thread inside that process (see [How It Works](/introduction/how-it-works)). This is the most robust setup and what you want in most deployments. + +Some environments and integrations need a different trade-off. Two independent options let you change where and how jobs run: + +- [`fork`](#fork-process-isolation): run the engine in a child process (default) or in your application's process. +- [`runner`](#runner-thread-pool-vs-inline): run each job in a worker thread pool (default) or inline in the current thread. + +They are orthogonal: `fork` controls the **process**, `runner` controls the **thread**. A related option, [`abortGracePeriodMs`](#cooperative-timeout-and-cancellation), controls how timeouts and cancellations stop a running job. + +::: tip TL;DR +Keep the defaults (`fork: true`, `runner: "thread"`) unless you have a concrete reason not to. Reach for `inline` + `fork: false` for serverless, test suites, or framework integrations that need jobs to share live in-process state. +::: + +## `fork`: process isolation + +```typescript +await Sidequest.start({ fork: false }); // default: true +``` + +| Value | Where the engine runs | Crash isolation | +| ---------------- | -------------------------- | -------------------------------------------------------------------------------------------------- | +| `true` (default) | A `child_process.fork` | A job crash (or `process.exit()`) kills the fork, not your app. The engine restarts automatically. | +| `false` | Your application's process | No isolation. An uncaught error in job code can take down your app. | + +Use `fork: false` when: + +- You can't spawn child processes (many **serverless / edge** runtimes). +- You're running an **integration test** and want to avoid IPC and process teardown flakiness. +- Your jobs need access to **live, in-process state** that can't cross a process boundary, for example a dependency-injection container (this is what `@sidequest/nestjs` relies on). + +::: danger No crash isolation with `fork: false` +With the default `fork: true`, a job that throws an unhandled exception or calls `process.exit()` only takes down the engine fork, and Sidequest restarts it. With `fork: false`, the engine shares your application's process: **a misbehaving job can crash your whole app.** Only use it when you understand and accept that. +::: + +## `runner`: thread pool vs inline + +```typescript +await Sidequest.start({ runner: "inline" }); // default: "thread" +``` + +| Value | How a job runs | CPU isolation | Can be force-stopped? | +| -------------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------- | +| `"thread"` (default) | In a [piscina](https://github.com/piscinajs/piscina) worker thread | Yes | Yes (the worker thread is terminated) | +| `"inline"` | Directly in the current thread, no pool | No | **No** | + +With `runner: "thread"`, `minThreads` / `maxThreads` / `idleWorkerTimeout` size the pool, and a job can be forcibly stopped by terminating its worker thread. + +With `runner: "inline"`, there is no pool and no separate thread. This is required when jobs must reach state that lives in the current thread, and it's handy for single-process setups. But it comes with two important consequences: + +::: warning Inline jobs block the event loop +An inline job runs on the same thread as everything else in that process: the dispatcher, and your app too if `fork: false`. A **CPU-bound** inline job will starve all of it until it finishes. Keep inline jobs I/O-bound, or use the thread pool for heavy work. +::: + +::: danger Inline jobs cannot be forcibly stopped +There is no separate thread to terminate, so Sidequest **cannot** kill a running inline job. Timeouts and cancellation only work if the job **cooperates** with the abort signal (see [Cooperative timeout and cancellation](#cooperative-timeout-and-cancellation) below). A job that ignores the signal runs to completion no matter what. +::: + +## Choosing a combination + +`fork` and `runner` combine into four setups: + +| `fork` | `runner` | Crash isolation | CPU isolation | Typical use | +| ------- | -------- | ---------------- | ------------- | ----------------------------------------------------------------------- | +| `true` | `thread` | ✅ | ✅ | **Default.** Production. | +| `true` | `inline` | ✅ (engine fork) | ❌ | Lighter execution with crash isolation kept; e.g. SQLite single-writer. | +| `false` | `thread` | ❌ | ✅ | Run in-process but still isolate CPU per job. | +| `false` | `inline` | ❌ | ❌ | Serverless, tests, and DI/framework integrations (e.g. NestJS). | + +::: code-group + +```typescript [Serverless / single-process] +// No child process, no worker threads: everything in one place. +await Sidequest.start({ + fork: false, + runner: "inline", + backend: { driver: "@sidequest/postgres-backend", config: process.env.DATABASE_URL }, +}); +``` + +```typescript [SQLite] +// SQLite is single-writer; running jobs inline avoids cross-thread write contention. +await Sidequest.start({ + runner: "inline", + maxConcurrentJobs: 1, + backend: { driver: "@sidequest/sqlite-backend", config: "./jobs.sqlite" }, +}); +``` + +```typescript [Integration tests] +await Sidequest.start({ + fork: false, // no IPC to wait on + runner: "inline", // deterministic, in-process execution + backend: { driver: "@sidequest/sqlite-backend", config: ":memory:" }, +}); +``` + +::: + +::: warning SQLite and concurrency +SQLite allows a single writer. Concurrency above 1 against the same file leads to `SQLITE_BUSY`. Keep `maxConcurrentJobs: 1`, use a separate `.sqlite` file from your app, or use a server database (Postgres/MySQL) for real concurrency. This is independent of the execution mode. +::: + +## Cooperative timeout and cancellation + +A job is stopped early in two cases: it exceeds its `timeout`, or it is canceled (via the dashboard or `Sidequest.job.cancel(id)`). How that actually stops the job depends on the mode. + +Sidequest hands every job an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) at `this.abortSignal`. When a timeout or cancellation fires, that signal aborts. Your job can observe it and stop: + +```typescript +import { Job } from "sidequest"; + +export class SyncContactsJob extends Job { + async run(accountId: string) { + // 1. Hand the signal to anything that accepts one; it aborts automatically. + const res = await fetch(`https://api.example.com/${accountId}/contacts`, { + signal: this.abortSignal, + }); + const contacts = await res.json(); + + // 2. For long loops or CPU work, check it cooperatively. + for (const contact of contacts) { + this.abortSignal.throwIfAborted(); // bail out promptly on timeout/cancel + await upsert(contact); + } + + return this.complete({ synced: contacts.length }); + } +} +``` + +`this.abortSignal.reason` tells you _why_ it aborted. It is a `JobTimeout` or a `JobCanceled`: + +```typescript +import { JobTimeout, JobCanceled } from "sidequest"; + +this.abortSignal.addEventListener("abort", () => { + const reason = this.abortSignal.reason; + if (reason instanceof JobTimeout) { + // exceeded `timeout` + } else if (reason instanceof JobCanceled) { + // canceled by an operator + } +}); +``` + +### When does the job actually receive the signal? + +| Mode | Gets a live `abortSignal`? | If the job ignores it | +| ----------------------------------------------------- | --------------------------------- | --------------------------------------------- | +| `runner: "inline"` | **Always** | Runs to completion (cannot be force-stopped). | +| `runner: "thread"`, `abortGracePeriodMs: 0` (default) | No (worker is killed immediately) | Killed right away. | +| `runner: "thread"`, `abortGracePeriodMs > 0` | Yes, for the grace window | Killed after the grace period. | + +::: danger Inline timeout/cancel only work if your job honors the signal +In `runner: "inline"` there is no way to forcibly stop a job. If your job does not pass `this.abortSignal` to its async work or check `this.abortSignal.aborted` / `throwIfAborted()`, then **timeouts and cancellation have no effect**: the job keeps running until it returns on its own. Treat `this.abortSignal` as mandatory for any long-running inline job. +::: + +### `abortGracePeriodMs`: graceful kill for thread jobs + +```typescript +await Sidequest.start({ abortGracePeriodMs: 5000 }); // default: 0 +``` + +Applies only to `runner: "thread"`. It controls the window between _signaling_ an abort and _forcibly terminating_ the worker thread: + +- `0` (default): the worker is terminated immediately. The job is not given a chance to react, and `this.abortSignal` is not delivered to it. This is the historical behavior. +- `> 0`: the abort is delivered to the job via `this.abortSignal` first; if the job has not finished after this many milliseconds, the worker thread is terminated. Use this to let thread jobs clean up (close handles, flush buffers) before being killed. + +::: tip +A positive grace period allocates a small message channel per job to deliver the abort into the worker. The cost only applies while a grace period is configured, and only matters for the rare cancel/timeout. Leave it at `0` unless your thread jobs need graceful shutdown. +::: + +### What state does the job end in? + +The terminal state is decided when the run **actually ends**, never while it is still running (so a job is never re-queued while a copy of it is still in flight): + +| What happened | Terminal state | +| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| The job returned a value/transition (it finished) | Whatever the job returned (`completed`, `failed`, a retry, etc.). This holds **even if** a timeout/cancel was signaled but the job finished anyway. | +| The worker was hard-killed by a **timeout** (thread, no result) | Retried (or `failed` if no attempts remain). | +| The worker was hard-killed by a **cancellation** (thread, no result) | `canceled`. | +| The job threw an unexpected error | Retried (or `failed`). | + +::: warning Canceling a running inline job is best-effort +Because an inline job's result is respected once it returns, a running inline job that **ignores** a cancellation and finishes will be recorded with its own result (e.g. `completed`), not `canceled`. Cancellation of a _running_ inline job only takes effect if the job honors `this.abortSignal`. Canceling a **waiting** job always works (it is simply never claimed). +::: + +## Next steps + +- [Execution and Control](/guide/jobs/running): using `this.abortSignal` inside `run()` +- [Configuration reference](/getting-started/configuration): all engine options +- [Graceful Shutdown](/production/graceful-shutdown): draining jobs on shutdown From 1a56dcf475d219e77ca27cd21c8045d1cf24368e Mon Sep 17 00:00:00 2001 From: merencia Date: Fri, 19 Jun 2026 17:27:49 -0300 Subject: [PATCH 2/2] docs: drop references to the not-yet-released @sidequest/nestjs package --- packages/docs/production/execution-modes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/production/execution-modes.md b/packages/docs/production/execution-modes.md index f99028d8..98528d3e 100644 --- a/packages/docs/production/execution-modes.md +++ b/packages/docs/production/execution-modes.md @@ -34,7 +34,7 @@ Use `fork: false` when: - You can't spawn child processes (many **serverless / edge** runtimes). - You're running an **integration test** and want to avoid IPC and process teardown flakiness. -- Your jobs need access to **live, in-process state** that can't cross a process boundary, for example a dependency-injection container (this is what `@sidequest/nestjs` relies on). +- Your jobs need access to **live, in-process state** that can't cross a process boundary, for example a dependency-injection container. ::: danger No crash isolation with `fork: false` With the default `fork: true`, a job that throws an unhandled exception or calls `process.exit()` only takes down the engine fork, and Sidequest restarts it. With `fork: false`, the engine shares your application's process: **a misbehaving job can crash your whole app.** Only use it when you understand and accept that. @@ -72,7 +72,7 @@ There is no separate thread to terminate, so Sidequest **cannot** kill a running | `true` | `thread` | ✅ | ✅ | **Default.** Production. | | `true` | `inline` | ✅ (engine fork) | ❌ | Lighter execution with crash isolation kept; e.g. SQLite single-writer. | | `false` | `thread` | ❌ | ✅ | Run in-process but still isolate CPU per job. | -| `false` | `inline` | ❌ | ❌ | Serverless, tests, and DI/framework integrations (e.g. NestJS). | +| `false` | `inline` | ❌ | ❌ | Serverless, tests, and integrations that need live in-process state. | ::: code-group