Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions agents/bunqueue-assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ Validated on the client with the broker's validator; an invalid option throws.
| Option | Meaning | Default |
|---|---|---|
| `priority` | Integer, higher runs first. | `0` |
| `delay` | Ms until ready (max 365 days; negative = now). | `0` |
| `delay` | Run-time offset in ms, bounded to ±4,320,000,000,000,000; negative keeps a past run time (ready immediately; among equal-priority FIFO jobs, earlier run times go first). Priority and LIFO ordering still apply. | `0` |
| `attempts` | Max attempts including the first. | `3` |
| `backoff` | Ms (exponential with jitter, cap 1 h) or `{type:'fixed'\|'exponential', delay, maxDelay?}`. | `1000`, exponential |
| `timeout` | Processing timeout in ms (the processor `signal` aborts). | none |
| `jobId` | Custom id; while that job is live, `add` returns it instead of creating one. Not with `repeat`. | UUIDv7 |
| `jobId` | Custom id; returns the live generation while ownership is retained. Memory/SQLite use a bounded 50,000-ID default cache; PostgreSQL checks the database. See [custom-ID limits](https://bunqueue.dev/guide/queue/deduplication/#custom-id-limits). Not with `repeat`. | UUIDv7 |
| `removeOnComplete` / `removeOnFail` | Drop the job on completion / final failure. Only booleans are honoured. | `false` |
| `durable` | Write straight to SQLite (no write buffer); in TCP it skips the auto-batcher. | `false` |
| `stallTimeout` | Stall interval for this job only. | queue `stallInterval` (30 s) |
Expand All @@ -82,7 +82,7 @@ Validated on the client with the broker's validator; an invalid option throws.
| `stackTraceLimit` | Stack lines kept on failure. | `10` |
| `failParentOnFailure` / `removeDependencyOnFailure` / `continueParentOnFailure` / `ignoreDependencyOnFailure` | What a failed child does to its parent. | `false` |
| `timestamp` | Overrides creation time. | `Date.now()` |
| `deduplication` `{id, ttl?, extend?, replace?}` | A duplicate add with the same id returns the existing job; `ttl` bounds the key, `extend` restarts it, `replace` swaps a pending owner. | none |
| `deduplication` `{id, ttl?, extend?, replace?}` | While the unique key is owned, a duplicate returns the existing job; `ttl` bounds the key, `extend` restarts it, `replace` swaps a pending owner. Separate from custom-ID ownership. | none |
| `group` `{id, maxSize?, priority?}` | Puts the job in a group; `maxSize` caps pending group jobs. | none |
| `keepLogs`, `sizeLimit`, `debounce` | Stored and shown on the job; not enforced by `Queue.add` (`debounce` is applied by `Bunqueue`). | none |

Expand Down Expand Up @@ -179,7 +179,7 @@ DLQ config defaults: `autoRetry` false, `autoRetryInterval` 1 h, `maxAutoRetries

### Queue: schedulers (repeatable jobs)

`RepeatOpts`: `pattern | every`, `limit`, `immediately`, `count`, `prevMillis`, `offset`, `jobId`, `timezone` (embedded default UTC, not sent over TCP), `skipMissedOnRestart` (true), `skipIfNoWorker` (false), `preventOverlap` (true).
`RepeatOpts`: `pattern | every`, `limit`, `immediately`, `count`, `prevMillis`, `offset`, `jobId`, `timezone` (forwarded in both modes; default UTC), `skipMissedOnRestart` (true), `skipIfNoWorker` (false), `preventOverlap` (true).

| API | What it does | Embedded | TCP |
|---|---|---|---|
Expand Down
5 changes: 3 additions & 2 deletions docs/agents/client-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const flow = new Workflow('order')
.step('notify-warehouse', async () => ({ notified: true }))
.step('notify-email', async () => ({ sent: true }))
)
.step('classify', async () => ({ tier: 'basic' }))
.branch((ctx) => (ctx.steps['classify'] as { tier: string }).tier)
.path('vip', (w) => w.step('vip-handler', async () => ({ discount: 20 })))
.path('basic', (w) => w.step('basic-handler', async () => ({ discount: 0 })))
Expand Down Expand Up @@ -192,10 +193,10 @@ const queue3 = new Queue('jobs', { autoBatch: { enabled: false } });
interface JobOptions {
priority?: number; // Higher = sooner
delay?: number; // ms before processing
attempts?: number; // Max retries (default: 3)
attempts?: number; // Maximum attempts, including the first (default: 3)
backoff?: number; // Retry backoff (default: 1000ms)
timeout?: number; // Processing timeout
jobId?: string; // Custom ID (idempotent)
jobId?: string; // Custom ID; retained-ownership dedup (memory/SQLite default cache: 50,000 IDs; PostgreSQL checks DB)
removeOnComplete?: boolean;
removeOnFail?: boolean;
durable?: boolean; // Bypass write buffer
Expand Down
17 changes: 11 additions & 6 deletions docs/agents/project-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ High-performance job queue server for Bun. SQLite persistence, cron jobs, priori

1. **PUSH**: Client → TcpPool → TcpServer → QueueManager → Shard[hash(queue)] → PriorityQueue → WriteBuffer → SQLite
2. **PULL**: Client → TcpServer → QueueManager → Shard → PriorityQueue.pop() → Job (state: active)
3. **ACK**: Client → TcpServer → AckBatcher → Shard.complete() → jobResults (LRU) + completedJobs (Set)
4. **FAIL**: Client → TcpServer → Shard.fail() → retry (backoff) OR → DLQ (max attempts)
3. **ACK**: Worker → optional client-side AckBatcher → TCP handler (`handlers/core.ts`) → QueueManager.ack → `operations/ack/completion.ts` → shard/index/storage, jobResults (LRU) + completedJobs (Set)
4. **FAIL**: Client → TCP handler (`handlers/core.ts`) → QueueManager.fail → `operations/ack/failure.ts` → shard/index/storage → retry (backoff) OR → DLQ (max attempts)

## Directory Structure

Expand Down Expand Up @@ -148,7 +148,7 @@ Cleanup runs every 10s (it also purges expired `removedResults`). Evicts 10% whe
TCP_PORT=6789 HTTP_PORT=6790
HOST=0.0.0.0 BUNQUEUE_DATA_PATH=./data/bunq.db
AUTH_TOKENS=token1,token2 CORS_ALLOW_ORIGIN=*
METRICS_AUTH=false # Require auth for /metrics endpoint
METRICS_AUTH=false # Require auth for /prometheus scrape endpoint
TCP_SOCKET_PATH= # RESERVED, not wired: TCP always binds HOST:TCP_PORT
# (client ConnectionOptions.socketPath is not read either)
HTTP_SOCKET_PATH= # Unix socket for HTTP (works)
Expand Down Expand Up @@ -257,9 +257,14 @@ bun run bench # Native macOS benchmarks only

| Mode | Throughput | Data Loss Risk |
| ------------------ | --------------- | -------------- |
| Buffered (default) | ~100k jobs/sec | Up to 10ms |
| Durable | ~10k jobs/sec | None |
| Auto-batch (TCP) | ~145k ops/s (concurrent), ~10k ops/s (sequential) | None (same as PUSH/PUSHB) |
| Buffered (default) | ~100k jobs/sec | Pending application buffer; typical flush interval 10 ms, longer during stalls/retries |
| Durable | ~10k jobs/sec | Bypasses the application admission buffer; host/storage power-loss limitations remain |
| Auto-batch (TCP) | ~145k ops/s (concurrent), ~10k ops/s (sequential) | Inherits the selected PUSH/PUSHB admission durability |

These throughput figures are indicative historical measurements, not results of
this documentation review. SQLite uses WAL with `synchronous=NORMAL`; durable
admission does not promise unconditional power-loss safety. See
[Persistence](../features/persistence.md).


## Background Tasks
Expand Down
7 changes: 5 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,11 @@ remote server via `embeddedQueue.forward({ to, queue })`
([`src/client/forwarder.ts`](../src/client/forwarder.ts)). Remote failures fall
back to local retry/DLQ. With persistent local storage, queued work survives
uplink outages while its process or volume survives; a deterministic remote jobId
`fwd:<queue>:<localId>` dedupes re-forwards within the server's custom-id
retention window. Trade-off: at-least-once delivery with bounded dedup; ideal for
`fwd:<queue>:<localId>` suppresses re-forwards while remote live ownership is
retained. Memory/SQLite use a bounded cache (50,000 IDs by default); PostgreSQL
checks database identity. Cache eviction can end local-engine suppression before
completion, so retain application idempotency for external effects. Trade-off:
at-least-once delivery with bounded dedup; ideal for
IoT/edge that must tolerate intermittent connectivity. See
[Store-and-Forward](./features/store-and-forward.md).

Expand Down
32 changes: 22 additions & 10 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ The core record. Defined in `src/domain/types/jobs/model.ts:41-90`.
export interface Job {
readonly id: JobId; // UUIDv7, branded string (types/jobs/model.ts:1)
readonly queue: string;
readonly name: string; // processor routing name, separate from user data
readonly data: unknown; // user payload (msgpack BLOB on disk)
readonly priority: number; // ungrouped: higher first; grouped: 0 first, then ascending
readonly createdAt: number; // epoch ms
readonly lifo: boolean; // tie-break among equal priority

// Scheduling
runAt: number; // createdAt + delay (a negative delay counts as 0); <=now means ready
runAt: number; // createdAt + delay; negative delay keeps a past run time; <=now means ready
startedAt: number | null; // set when pulled (active)
completedAt: number | null; // set when completed

Expand Down Expand Up @@ -143,13 +144,19 @@ export interface Job {
```

`customId` is a broker-wide identity key, not a per-queue key. When supplied it
also becomes `jobs.id`, whose SQLite primary key is global. Re-adding a live
custom ID from any queue returns the existing generation; after a terminal
also becomes `jobs.id`, whose SQLite primary key is global. Ordinary
memory/SQLite adds return a live generation only while its ownership is retained
in the bounded custom-ID LRU (50,000 IDs by default). SQLite admission uses
upserts, so the primary key does not preserve the original live generation
after eviction; durable admission does not remove that limit. PostgreSQL checks
locked database identity independently. See
[Deduplication & Unique Jobs](./features/deduplication-and-unique.md). After a terminal
generation, reuse retires the completed/DLQ record and its result in the same
SQLite transaction that inserts the new generation (also for a non-durable
add, which then bypasses the `WriteBuffer`), and only afterwards retires the
in-memory record and result/log ownership. At no point may two rows or two live
jobs share the same custom ID, and a crash never leaves zero generations.
in-memory record and result/log ownership. This transactional terminal-reuse
path is distinct from the unresolved live-ownership eviction limitation;
ordinary adds must not be used as an unconditional uniqueness guarantee.
Persisted job IDs must be well-formed Unicode. Standalone custom IDs and every
planned atomic-flow ID are rejected if they contain an isolated UTF-16
surrogate, because such values do not round-trip through Bun SQLite TEXT and
Expand All @@ -172,7 +179,8 @@ Notable supporting types:
- `JobId = string & { readonly __brand: 'JobId' }` — UUIDv7, generated by
`Bun.randomUUIDv7()` (`src/domain/job/ids.ts:7-8`).
- `RepeatConfig` (`src/domain/types/jobs/model.ts:13-25`) — repeatable-job config (`every`, `pattern`,
`limit`, `count`, `tz`, …). In-memory only on a `Job`; not a SQLite column.
`limit`, `count`, `tz`, …). No dedicated legacy SQLite column; persisted in
`jobs.extended_options`. PostgreSQL stores it in the complete job payload.
- `BackoffConfig` (`src/domain/types/jobs/model.ts:27-31`) — `{ type: 'fixed' | 'exponential'; delay; maxDelay? }`.
It is also the object form of `JobInput.backoff`, `PushCommand.backoff` and
`CronJobOptions.backoff`. `DEFAULT_MAX_BACKOFF = 3_600_000` (1h) is the cap when
Expand Down Expand Up @@ -267,7 +275,8 @@ Allowed transitions (enforced across `pull`/`ack`/`fail` operations and
> required and must be a finite number in both modes (TCP/HTTP handlers,
> `QueueManager.changeDelay`/`changeWaitingDelay`, and the client job methods before
> sending); a negative delay keeps its past run time (`runAt = now + delay`: the job is
> ready at once, ahead of later ready jobs; the PostgreSQL engine applies `now +
> ready at once, ahead of equal-priority FIFO jobs with later run times; priority
> and LIFO ordering still apply. The PostgreSQL engine applies `now +
> max(0, delay)`, as on 2.9.10) and one beyond ±`MAX_JOB_DURATION_MS` is clamped, as
> 2.9.10 applied them (`delayArgument`); a
> non-finite `timestamp` is rejected. In-queue
Expand Down Expand Up @@ -300,7 +309,8 @@ by `createJob` (`src/domain/job/create.ts`) using `JOB_DEFAULTS` (`src/domain/jo
`timestamp`, `delay`, `backoff` (or object `backoff.delay`), `priority` or
`maxAttempts` falls back to now, no delay or the default. A negative `delay` is kept as
in 2.9.10 (`run_at = created_at + delay`, bounded to ±`MAX_JOB_DURATION_MS`): the job is
never `delayed` and sorts ahead of ready jobs with a later `run_at`.
never `delayed` and, among equal-priority FIFO jobs, sorts ahead of ready jobs
with a later `run_at`. Priority and LIFO ordering still apply.

`lifo` is stored as a boolean whatever the input (`1` is `true`), as the heap
comparator requires; `ChangePriority` normalizes its `lifo` the same way.
Expand All @@ -319,7 +329,7 @@ its number; "clamped" values above the maximum are stored as the maximum
| Option | Bound |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `priority` | finite (grouped: integer 0..2,097,151) |
| `delay` | finite; negative = a past run time (ready, sorts first); clamped to ±MAX |
| `delay` | finite; negative = a past run time (ready; earlier run times go first among equal-priority FIFO jobs); clamped to ±MAX |
| `ttl`, `timeout` | finite, >= 0; clamped |
| `stallTimeout` | finite |
| `maxAttempts` | any number, ±Infinity allowed; stored 1 or less → 1, fraction rounded up, at most 2,147,483,647 |
Expand All @@ -333,6 +343,7 @@ its number; "clamped" values above the maximum are stored as the maximum
| Option | Type | Default |
| --------------------------- | ------------------------------------------------------------------- | ------------ |
| `data` | `unknown` (required) | — |
| `name` | `string` (optional) | `'default'` |
| `priority` | `number` | `0` |
| `delay` | `number` (ms) | `0` |
| `maxAttempts` | `number` | `3` |
Expand Down Expand Up @@ -839,6 +850,7 @@ export interface PushCommand extends BaseCommand {
// src/domain/types/commands/core.ts:5-52
readonly cmd: 'PUSH';
readonly queue: string;
readonly name?: string; // processor routing name, default 'default'
readonly data: unknown;
readonly priority?: number;
readonly delay?: number;
Expand All @@ -860,7 +872,7 @@ export interface PullCommand extends BaseCommand {
// src/domain/types/commands/core.ts:65-72
readonly cmd: 'PULL';
readonly queue: string;
readonly timeout?: number; // long-poll ms, finite 0..60000 (QueueManager.pull* clamps)
readonly timeout?: number; // wire long-poll ms, finite 0..60000; direct QueueManager waits differ
readonly owner?: string; // lock owner id
readonly lockTtl?: number; // default 30000; any finite number (2.9.10), not NaN/Infinity
readonly detach?: boolean; // don't auto-release on disconnect
Expand Down
12 changes: 8 additions & 4 deletions docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,13 @@ are separate, so a healthy IPv6 listener is not misreported as unreachable.

### 7. Backup (`executeBackupCommand`, `src/cli/commands/backup.ts:21`)

Requires a data path from
`BUNQUEUE_DATA_PATH`→`BQ_DATA_PATH`→`DATA_PATH`→`SQLITE_PATH`; otherwise
fails. It accepts temporary S3 credentials (`S3_SESSION_TOKEN` /
Reads the server's config file (`--config`/`-c <file>`, else discovered
`bunqueue.config.{ts,js,mjs}` in the working directory). The data path resolves as
`storage.dataPath` > `BUNQUEUE_DATA_PATH` > `BQ_DATA_PATH` > `DATA_PATH` >
`SQLITE_PATH`; without a path it fails. The selected storage driver must be
SQLite; PostgreSQL and memory are refused. Backup settings use the file's
`backup` section > `S3_*`/`AWS_*` environment > defaults, through
`resolveBackupCommandConfig`. It accepts temporary S3 credentials (`S3_SESSION_TOKEN` /
`AWS_SESSION_TOKEN`) and virtual-host addressing
(`S3_VIRTUAL_HOSTED_STYLE`). It validates S3 config via
`S3BackupManager.validate()`. `restore` refuses without `--force` (`-f`) and
Expand Down Expand Up @@ -308,7 +312,7 @@ Client-affecting env vars / flags (defaults in parentheses):
- `--json` (off), `--help`/`-h`, `--version`/`-v`.
- `NO_COLOR` (any non-empty value) disables ANSI output; `FORCE_COLOR` forces it (`0`/`false` force it off); `TERM=dumb` disables it.
- Connection timeout 5000 ms (`src/cli/client.ts:171-176`); command timeout has a 30000 ms base (`src/cli/client.ts:44-48`).
- `backup` reads the server's config file (`--config`/`-c <file>`, else `bunqueue.config.{ts,js,mjs}` in the working directory) with the server's precedence: data path ← `storage.dataPath` > `BUNQUEUE_DATA_PATH` > `BQ_DATA_PATH` > `DATA_PATH` > `SQLITE_PATH`, backup settings ← the `backup` section > `S3_*`/`AWS_*` > defaults (`resolveBackupCommandConfig`). A configuration that selects PostgreSQL or memory is refused.
- `backup` uses the server's config-file discovery, storage selection and backup precedence described in the Backup section above.

Server-start flags (`bunqueue start`, merged over `bunqueue.config.ts`, CLI
wins): `--tcp-port` (6789; 0-65535, 0 lets the OS pick), `--http-port` (6790;
Expand Down
Loading
Loading