From 8089ae01463d3b5e93f83ffe5a013254a30a5e6f Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 14 Oct 2026 08:13:35 -0700 Subject: [PATCH] Describe non-Go clients without referring to River Go River's Rust, JavaScript, Ruby, and Java clients document much of their behavior by comparison with River Go: "like River for Go's `Config.FetchCooldown`", "a Go duration such as `1h`", "aligned like Go River's by-period uniqueness". Each port is its own piece of software, and a user of one shouldn't need to know River Go to understand it. Rewrite those references in user-facing text to describe the behavior directly: public API doc comments, READMEs and docs pages, CLI help, a gem and Maven package description, and error messages. Where the point is interop through a shared database, schema, or wire format, the text now refers to other River clients. For example, the duration grammar `@every` accepts and the epoch by-period windows align to are spelled out, and a list cursor's format is described as shared by every River client. Error messages that compared with Go now describe the problem instead: the derive macro's invalid duration, an empty unique-args path segment in Rust, a JavaScript unique path segment other clients read as an array index, a cron schedule that can't pass a skipped day, and an out-of-range Ruby `@every` duration. Accepted input formats don't change. Documents about running alongside Go keep their Go references, as do contributor docs for tooling that needs Go, the Java documents that compare its API with Go's, internal comments, tests, conformance code, and changelogs. The JavaScript API reports are regenerated. --- java/README.md | 36 +-- java/docs/development.md | 4 +- java/river/pom.xml | 2 +- .../main/java/com/riverqueue/Migrator.java | 2 +- .../main/java/com/riverqueue/Schedule.java | 2 +- .../src/main/java/com/riverqueue/Workers.java | 2 +- js/cli/README.md | 22 +- js/cli/etc/cli.api.md | 2 +- js/cli/src/bench.ts | 4 +- js/cli/src/run.ts | 2 +- js/docs/README.md | 109 +++++---- js/docs/databases.md | 54 +++-- js/docs/deployment.md | 21 +- js/docs/errors-and-retries.md | 95 ++++---- js/docs/migrating-from-0.1.md | 23 +- js/docs/observability.md | 12 +- js/docs/periodic-jobs.md | 48 ++-- js/docs/resumable-jobs.md | 4 +- js/docs/runtime.md | 121 +++++----- js/docs/testing.md | 18 +- js/driver/pg/README.md | 25 +- js/driver/pg/etc/driver-pg.api.md | 4 +- js/driver/pg/src/driver.ts | 4 +- js/driver/prisma/README.md | 10 +- js/driver/prisma/etc/driver-prisma.api.md | 3 +- js/driver/prisma/src/driver.ts | 3 +- js/driver/sqlite/README.md | 61 +++-- js/driver/sqlite/etc/driver-sqlite.api.md | 16 +- js/driver/sqlite/src/driver.ts | 13 +- js/driver/sqlite/src/types.ts | 3 +- js/etc/riverqueue.api.md | 217 +++++++++--------- js/etc/riverqueue.unstable-driver.api.md | 2 +- js/migrate/README.md | 21 +- js/src/client.ts | 29 ++- js/src/cron.ts | 44 ++-- js/src/driver.ts | 2 +- js/src/errors.ts | 1 - js/src/extensions.ts | 40 ++-- js/src/insert-options.ts | 11 +- js/src/job-definition.ts | 9 +- js/src/options.ts | 35 ++- js/src/periodic.ts | 13 +- js/src/query.ts | 11 +- js/src/worker.ts | 30 ++- js/test/README.md | 3 +- js/test/etc/test.api.md | 8 +- js/test/src/index.ts | 10 +- js/worker-threads/README.md | 11 +- ruby/docs/README.md | 39 ++-- ruby/docs/development.md | 2 +- ruby/docs/migrating_from_sidekiq.md | 12 +- ruby/docs/migrations.md | 22 +- ruby/docs/yugabyte.md | 9 +- ruby/lib/client.rb | 6 +- ruby/lib/insert_opts.rb | 2 +- ruby/lib/job.rb | 8 +- ruby/lib/job_persisted_logging/plugin.rb | 3 +- ruby/lib/periodic_cron.rb | 9 +- ruby/rails/riverqueue-rails/README.md | 6 +- ruby/riverqueue.gemspec | 2 +- rust/README.md | 2 +- rust/docs/development.md | 2 +- rust/riverqueue-cli/src/bench.rs | 2 +- rust/riverqueue-macros/src/lib.rs | 2 +- .../tests/ui/unique_period_invalid.stderr | 2 +- rust/riverqueue-migrate/README.md | 8 +- rust/riverqueue/README.md | 21 +- rust/riverqueue/docs/mixed-deployments.md | 6 +- rust/riverqueue/examples/mixed_go_rust.rs | 2 +- rust/riverqueue/examples/periodic.rs | 2 +- rust/riverqueue/src/client/builder.rs | 10 +- rust/riverqueue/src/database.rs | 3 +- rust/riverqueue/src/encoding.rs | 59 +++-- rust/riverqueue/src/job.rs | 8 +- rust/riverqueue/src/job/attempt_error.rs | 13 +- rust/riverqueue/src/periodic/cron.rs | 12 +- rust/riverqueue/src/periodic/mod.rs | 7 +- rust/riverqueue/src/unique.rs | 2 +- 78 files changed, 731 insertions(+), 774 deletions(-) diff --git a/java/README.md b/java/README.md index 4381192ed..b6b14557c 100644 --- a/java/README.md +++ b/java/README.md @@ -6,7 +6,7 @@ retry, or work them using the same database. ## Installation -Java 21 or newer is required. Add `com.riverqueue:river:0.48.0-alpha.1` and a JDBC driver to your application's dependencies. Jackson 3 is included transitively. Applications do not need Go. +Java 21 or newer is required. Add `com.riverqueue:river:0.48.0-alpha.1` and a JDBC driver to your application's dependencies. Jackson 3 is included transitively. ```xml @@ -79,7 +79,7 @@ for each operation. River never closes an application-owned data source. An ## Migration CLI The `river-cli` executable JAR includes Postgres and SQLite JDBC drivers and -runs with Java 21 or newer. It needs no Go installation or application classpath. +runs with Java 21 or newer. It needs no application classpath. For a local build, see the [development guide](docs/development.md#building-from-source). Set `RIVER_CLI` to the executable JAR, then run: ```sh @@ -125,8 +125,8 @@ for SQLite, or select the dialect through the URL. Export accepts comma-separate setup when exporting for another migration framework. Exit codes are `0` for success, `1` for an operation failure, and `2` for invalid arguments. -The [migration history](https://riverqueue.com/docs/migrations) is shared with Go -and the other ports. Use the CLI version matching your Java library. The Java +The [migration history](https://riverqueue.com/docs/migrations) is shared with +River clients in other languages. Use the CLI version matching your Java library. The Java API remains available for embedded use: ```java @@ -200,9 +200,9 @@ Typed retrieval checks the kind and decodes the arguments. Insertion results contain `Job` because uniqueness can return an existing job with another kind or argument schema. The returned arguments are the persisted values, including any additional fields written by another language. -See the [API comparison](API.md) for the Go mapping and prerelease API changes. +See the [API review](API.md) for the full application surface and prerelease API changes. -Lists default to ascending job ID order, matching Go. Keep the filters and order +Lists default to ascending job ID order. Keep the filters and order unchanged when continuing from a page's cursor: ```java @@ -279,7 +279,7 @@ workerConfig.add(email, context -> { Long-running handlers should check cancellation between operations and use interruptible I/O. `context.awaitCancellation()` waits for the signal. -As in Go, a normal return reports success even if cancellation or a timeout was +A normal return reports success even if cancellation or a timeout was requested. Call `context.checkCancelled()` when abandoning unfinished work. ### Getting the client within workers @@ -329,8 +329,8 @@ reporting hooks fast and avoid throwing from them. ### Job-persisted logging [Job logs](https://riverqueue.com/docs/job-logging) live in `river:log` metadata -as an array of `{attempt, log}` objects. Java has no bundled equivalent of Go's -`riverlog` middleware yet. You can explicitly write that shared format; this +as an array of `{attempt, log}` objects. Java has no bundled job-logging +middleware yet. You can explicitly write that shared format; this example appends a fixed message and retains the last ten attempt entries. ```java @@ -398,8 +398,8 @@ zone when run by workers, including daylight-saving changes. Use an explicit zone when leaders may run in different time zones. Direct `schedule.next(...)` calls use the reference time's zone or fixed offset unless the expression specifies one; pass a `ZonedDateTime` to retain a named zone's daylight-saving rules. -Time-zone rules come from the JVM; matching Go and Java schedules requires -matching time-zone data in both deployments. +Time-zone rules come from the JVM; schedules computed by River clients in other +languages match only when they use the same time-zone data. ```java var workerConfig = client.workers() @@ -452,7 +452,7 @@ workerConfig.add(email, context -> { For loops, `context.stepWithCursor(name, cursor -> ...)` receives a saved JSON cursor, and `context.cursor(value)` updates it. Java buffers step progress until -the attempt ends; it does not yet expose Go's transactional checkpoint helpers. +the attempt ends; it does not yet expose transactional checkpoint helpers. A process crash can lose buffered progress, so steps must remain idempotent. ### Scheduled jobs @@ -535,7 +535,7 @@ void completes(@org.junit.jupiter.api.io.TempDir java.nio.file.Path directory) t `new Client(database, clock)` accepts an injected `Clock` for deterministic insert timestamps. The worker runtime uses wall time. Java does not currently provide -a separate equivalent of Go's `rivertest.Worker` harness. +a dedicated worker-test harness. ### Transactional job completion @@ -559,8 +559,8 @@ workerConfig.add(email, context -> { [Uniqueness](https://riverqueue.com/docs/unique-jobs) can combine arguments, queue, time period, and job states. A duplicate insert returns the existing job -with `uniqueSkippedAsDuplicate()` set. The key format is shared with Go and the -other ports. +with `uniqueSkippedAsDuplicate()` set. The key format is shared with River +clients in other languages. ```java var options = InsertOptions.builder() @@ -663,7 +663,7 @@ reindexing configured Postgres indexes. Leadership is enabled by default. `rescueAfter` must not be shorter than `jobTimeout`, so another client does not rescue an attempt that is still allowed to run. Its default is one hour, plus an explicitly configured positive job timeout. Worker durations and snooze delays -must fit in signed 64-bit nanoseconds, the same range as Go's `time.Duration`. +must fit in signed 64-bit nanoseconds, up to about 292 years. ```java var workerConfig = client.workers() @@ -676,8 +676,8 @@ Retention arguments are cancelled, completed, and discarded durations, in that order. Negative durations retain that state indefinitely. If a runtime uses `leadership(false)`, another eligible client must run maintenance; periodic registrations require leadership. `workers.isLeader()` reports the local status. -Leadership leases last `serviceInterval` plus ten seconds, matching Go's renewal -margin even when the interval is increased. +Leadership leases last `serviceInterval` plus ten seconds, keeping the same +renewal margin even when the interval is increased. `serviceInterval` also controls maintenance and queue heartbeats. It must be positive and shorter than one day, the retention period for inactive queues. Short retries and snoozes, including rescued retries, use timed fetch wake-ups diff --git a/java/docs/development.md b/java/docs/development.md index fe24afa65..3c55228f2 100644 --- a/java/docs/development.md +++ b/java/docs/development.md @@ -4,7 +4,7 @@ Run commands from the repository root unless a section says otherwise. See the [ ## Building from source -Install the Go version specified by [`go.work`](../../go.work), Maven, and JDK 21 or 25. Set `JAVA_HOME` and `PATH` to that JDK; the pinned formatter does not run on JDK 27. Go is needed to generate test fixtures, but applications using River do not need Go. +Install the Go version specified by [`go.work`](../../go.work), Maven, and JDK 21 or 25. Set `JAVA_HOME` and `PATH` to that JDK; the pinned formatter does not run on JDK 27. Go is needed to generate test fixtures. Generate the fixtures and install the Java artifacts into your local Maven repository: @@ -58,7 +58,7 @@ payloads, and immediate leadership wakeup after a peer resigns. Uniqueness tests include the raw JSON fixtures for duplicate keys and integer-like map keys. Protocol tests exercise both retry jitter boundaries, decode stored states and uniqueness bits, and verify periodic IDs, insert nonces, resumable -checkpoints, output, and rescue counters using Go's metadata keys. +checkpoints, output, and rescue counters using River's reserved metadata keys. `make generate/java-migrations` syncs SQL and the migration catalog from the canonical Go drivers. `verify/java-migrations` detects changed, missing, or extra diff --git a/java/river/pom.xml b/java/river/pom.xml index 0e160e03e..5d7582502 100644 --- a/java/river/pom.xml +++ b/java/river/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.riverqueueriver-java0.48.0-alpha.1 river - Transactional jobs interoperable with River Go + Transactional jobs interoperable with River clients in other languages Mozilla Public License 2.0https://mozilla.org/MPL/2.0/repo tools.jackson.corejackson-databind diff --git a/java/river/src/main/java/com/riverqueue/Migrator.java b/java/river/src/main/java/com/riverqueue/Migrator.java index 8e9a8cb4e..07ac2caaf 100644 --- a/java/river/src/main/java/com/riverqueue/Migrator.java +++ b/java/river/src/main/java/com/riverqueue/Migrator.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Objects; -/** Applies the exact, versioned River migration line shared with Go. */ +/** Applies the exact, versioned River migration line shared by all River clients. */ public final class Migrator { private static final List NAMES = Sql.resource("migration/index.txt").lines().toList(); public static final int LATEST = NAMES.size(); diff --git a/java/river/src/main/java/com/riverqueue/Schedule.java b/java/river/src/main/java/com/riverqueue/Schedule.java index bd505e0b3..787c427ca 100644 --- a/java/river/src/main/java/com/riverqueue/Schedule.java +++ b/java/river/src/main/java/com/riverqueue/Schedule.java @@ -18,7 +18,7 @@ default Optional next(ZonedDateTime after) { return next(after.toOffsetDateTime()).map(next -> next.atZoneSameInstant(after.getZone())); } - /** Parses River Go's five-field cron syntax, including descriptors and time zones. */ + /** Parses River's five-field cron syntax, including descriptors and time zones. */ static Schedule cron(String expression) { return new CronSchedule(expression); } diff --git a/java/river/src/main/java/com/riverqueue/Workers.java b/java/river/src/main/java/com/riverqueue/Workers.java index afb46db51..4da311738 100644 --- a/java/river/src/main/java/com/riverqueue/Workers.java +++ b/java/river/src/main/java/com/riverqueue/Workers.java @@ -1658,7 +1658,7 @@ public Event(EventKind kind, Job job) { } } - /** Event names shared with Go and the conformance protocol. */ + /** Event names shared with other River clients and the conformance protocol. */ public enum EventKind { JOB_CANCELLED, JOB_COMPLETED, diff --git a/js/cli/README.md b/js/cli/README.md index 126aa4985..12a26b92d 100644 --- a/js/cli/README.md +++ b/js/cli/README.md @@ -1,8 +1,8 @@ # `@riverqueue/cli` The `riverqueue` command runs River's database migrations and a throughput -benchmark, and upgrades code written for `riverqueue@0.1`. It requires -Node.js 24 or newer and mirrors the commands of River's Go CLI. +benchmark, and upgrades code written for `riverqueue@0.1`. It requires Node.js +24 or newer. ```sh npm install --save-dev @riverqueue/cli @@ -41,7 +41,7 @@ Other common flags: - `--dry-run` and `--show-sql`: print what would run, with its SQL. - `--statement-timeout DURATION`: Postgres's `statement_timeout`, such as `30s` or `5m`. It defaults to a `statement_timeout` parameter in the URL, - and otherwise to 10 seconds, as in River's Go CLI. + and otherwise to 10 seconds. ```sh # Check in CI that a database is migrated. @@ -102,11 +102,11 @@ backlog, so it reflects queue depth as much as per-job latency. The command fails if running jobs or pending completions exceed their configured limits, or if the pool exceeds `--max-connections`. -The default workload matches River's Go and Rust benchmarks so results are -comparable: a 75,000-job backlog topped up in batches of 5,000 and 2,000 -concurrent workers. The pool defaults to 50 connections. `--backlog`, -`--batch-size`, `--max-workers`, `--max-connections`, and `--skip-vacuum` -change the workload for controlled comparisons. +The default workload matches the benchmarks of River's other implementations so +results are comparable: a 75,000-job backlog topped up in batches of 5,000 and +2,000 concurrent workers. The pool defaults to 50 connections. `--backlog`, +`--batch-size`, `--max-workers`, `--max-connections`, and `--skip-vacuum` change +the workload for controlled comparisons. ## Running from code @@ -122,9 +122,9 @@ process.exitCode = await run([ ]); ``` -Extension packages can embed this CLI and add migration lines, as with -River's Go CLI. The migration commands select an added line with -`--line NAME`, while River's `main` line stays the default: +Extension packages can embed this CLI and add migration lines. The migration +commands select an added line with `--line NAME`, while River's `main` line +stays the default: ```ts import type { Migration, MigrationBackend } from "@riverqueue/migrate"; diff --git a/js/cli/etc/cli.api.md b/js/cli/etc/cli.api.md index 5bee98224..09b01b3a7 100644 --- a/js/cli/etc/cli.api.md +++ b/js/cli/etc/cli.api.md @@ -34,7 +34,7 @@ export declare function run( * Options for {@link run}. Each stream defaults to the process's own. * * A package that ships its own migration line can embed this command line - * with its lines added, as River's Go CLI allows: + * with its lines added: * * ```ts * import { run } from "@riverqueue/cli"; diff --git a/js/cli/src/bench.ts b/js/cli/src/bench.ts index 0ff3fbc98..f4cc483cd 100644 --- a/js/cli/src/bench.ts +++ b/js/cli/src/bench.ts @@ -57,8 +57,8 @@ fixed number of jobs first and stops once all of them are worked. Every two seconds it prints the jobs worked and inserted and jobs per second. The summary adds the overall rate, the 95th percentile time from insert to -completion, and peak resource use. The workload matches River's Go and Rust -benchmarks so results are comparable.`, +completion, and peak resource use. The workload matches the benchmarks of +River's other implementations so results are comparable.`, name: "bench", options: { backlog: { diff --git a/js/cli/src/run.ts b/js/cli/src/run.ts index 0bebb7265..09aa7325e 100644 --- a/js/cli/src/run.ts +++ b/js/cli/src/run.ts @@ -22,7 +22,7 @@ import { formatHelp, parseOptions, UsageError } from "./options.js"; * Options for {@link run}. Each stream defaults to the process's own. * * A package that ships its own migration line can embed this command line - * with its lines added, as River's Go CLI allows: + * with its lines added: * * ```ts * import { run } from "@riverqueue/cli"; diff --git a/js/docs/README.md b/js/docs/README.md index a18ab0553..91c650d97 100644 --- a/js/docs/README.md +++ b/js/docs/README.md @@ -28,10 +28,10 @@ export const sendEmail = defineJob({ ``` River runs the schema when a job is inserted and again before it is worked, -because another producer (an older deploy, a Go or Rust service, or a SQL -script) may have inserted it. Producers pass the schema's input type; workers -receive its output type, so schema defaults and transforms apply to what the -worker sees. River persists the producer's input exactly as given, so +because another producer (an older deploy, a service in another language, or a +SQL script) may have inserted it. Producers pass the schema's input type; +workers receive its output type, so schema defaults and transforms apply to what +the worker sees. River persists the producer's input exactly as given, so uniqueness hashes and other languages see the same JSON. The schema's input must be JSON. A schema with a `Date` or `bigint` input is @@ -91,11 +91,11 @@ Kinds are persisted and shared with other languages, so treat them as part of your data model. A kind starts with a letter, digit, or underscore, and kinds starting with `river_internal_` are reserved. -To rename a kind without orphaning jobs already stored under the old name, -make the new name the `kind` and list the old one in `kindAliases`, like River -for Go's `JobArgsWithKindAliases`. New jobs are inserted under the new kind, -and the definition's worker also works jobs stored under the alias. Remove the -alias once those jobs have finished, including their retries: +To rename a kind without orphaning jobs already stored under the old name, make +the new name the `kind` and list the old one in `kindAliases`. New jobs are +inserted under the new kind, and the definition's worker also works jobs stored +under the alias. Remove the alias once those jobs have finished, including their +retries: ```ts import { defineJob } from "riverqueue"; @@ -173,33 +173,32 @@ if (result.status === "duplicate") { } ``` -Uniqueness is always by kind (unless `excludeKind`), and optionally by all -args or selected `byArgs` paths (such as `["account.id"]`), by `byQueue`, and -within fixed `byPeriod` windows. Like River for Go, `excludeKind` requires -`byArgs`, `byQueue`, or `byPeriod`. `byState` chooses which job states count as -duplicates; the default is every state except `cancelled` and `discarded`. -Unique keys are computed exactly as River for Go computes them, so uniqueness -holds across languages. +Uniqueness is always by kind (unless `excludeKind`), and optionally by all args +or selected `byArgs` paths (such as `["account.id"]`), by `byQueue`, and within +fixed `byPeriod` windows. `excludeKind` requires `byArgs`, `byQueue`, or +`byPeriod`. `byState` chooses which job states count as duplicates; the default +is every state except `cancelled` and `discarded`. Unique keys are computed +exactly as other River clients compute them, so uniqueness holds across +languages. By-args uniqueness hashes the JSON River writes. Top-level keys are sorted, but nested objects are hashed in their own key order, so jobs from different -languages deduplicate only when they encode nested objects identically. Go -writes struct fields in declaration order and map keys sorted; JavaScript writes -object keys in insertion order, except that keys that look like array indices -(`"2"`, `"10"`) always come first in ascending numeric order. Keep nested keys -in the same order in every producer, and avoid integer-like keys in nested -objects that must deduplicate against another language. +languages deduplicate only when they encode nested objects identically. Other +languages may write struct fields in declaration order or map keys sorted; +JavaScript writes object keys in insertion order, except that keys that look +like array indices (`"2"`, `"10"`) always come first in ascending numeric order. +Keep nested keys in the same order in every producer, and avoid integer-like +keys in nested objects that must deduplicate against another language. With `byArgs: true`, every top-level key is hashed literally, including empty -keys and keys containing JSON path punctuation. A selected `byArgs` path uses -an unescaped dot to reach a nested field: `"account.id"` selects `id` within -`account`. Escape a dot to select a literal key containing one: -`"account\\.id"` selects the top-level key `account.id`. Use `"\\\\"` for -a literal backslash in a key. Empty path segments and a trailing backslash -are invalid. So is a segment that is an unsigned integer or `-1`, escaped or -not: River for Go reads it as an array index and builds a JSON array instead -of an object when assembling the selected fields, so the keys wouldn't -match. +keys and keys containing JSON path punctuation. A selected `byArgs` path uses an +unescaped dot to reach a nested field: `"account.id"` selects `id` within +`account`. Escape a dot to select a literal key containing one: `"account\\.id"` +selects the top-level key `account.id`. Use `"\\\\"` for a literal backslash in +a key. Empty path segments and a trailing backslash are invalid. So is a segment +that is an unsigned integer or `-1`, escaped or not: other River clients read it +as an array index and build a JSON array instead of an object when assembling +the selected fields, so the keys wouldn't match. ### Batches @@ -255,15 +254,14 @@ try { } ``` -Like River for Go, River runs its statements directly in your transaction and -opens no savepoint or nested transaction in it, which on Postgres would cost -a subtransaction for every call. When a call fails after River wrote to the -database, for example because insert middleware or an `afterInsert` hook threw -after the job was inserted, the write stays in your transaction, so roll it -back as above. On Postgres a database error also aborts the transaction. To -recover from a failed call and continue the transaction, wrap the call in a -savepoint of your own. Without `{ tx }`, River's own transaction rolls the -whole call back. +River runs its statements directly in your transaction and opens no savepoint or +nested transaction in it, which on Postgres would cost a subtransaction for +every call. When a call fails after River wrote to the database, for example +because insert middleware or an `afterInsert` hook threw after the job was +inserted, the write stays in your transaction, so roll it back as above. On +Postgres a database error also aborts the transaction. To recover from a failed +call and continue the transaction, wrap the call in a savepoint of your own. +Without `{ tx }`, River's own transaction rolls the whole call back. The job's insert notification is delivered when the transaction commits, so workers never see a job whose transaction rolled back. Prisma and SQLite use @@ -432,18 +430,17 @@ await client.queues.pause("email"); // or "*" for every queue await client.queues.resume("email"); ``` -A job list cursor is the same text River for Go's `JobListCursor` and River -for Rust's `JobListCursor` produce, so a service in one language can hand a -page token to a service in another to continue the listing. River accepts -cursors in either base64 alphabet, with or without padding. Queue list -cursors are specific to JavaScript. +A job list cursor is the same text other River clients produce, so a service in +one language can hand a page token to a service in another to continue the +listing. River accepts cursors in either base64 alphabet, with or without +padding. Queue list cursors are specific to JavaScript. -Like River for Go, `orderBy: "time"` sorts every listed job by the time -field of the first listed state (`scheduledAt` for `available`, `pending`, -`retryable`, and `scheduled`, `attemptedAt` for `running`, `finalizedAt` -for finalized states), and by `scheduledAt` when `states` is empty. Jobs -where that field is null, like `finalizedAt` for an unfinished job, sort -after all others ascending and before all others descending. +`orderBy: "time"` sorts every listed job by the time field of the first listed +state (`scheduledAt` for `available`, `pending`, `retryable`, and `scheduled`, +`attemptedAt` for `running`, `finalizedAt` for finalized states), and by +`scheduledAt` when `states` is empty. Jobs where that field is null, like +`finalizedAt` for an unfinished job, sort after all others ascending and before +all others descending. ## Exact values and JSON @@ -458,10 +455,10 @@ beyond `Number.MAX_SAFE_INTEGER`, cycles, sparse arrays, accessors, class instances, and invalid Unicode instead of silently coercing them. Like `JSON.stringify`, it omits object properties whose value is `undefined`. -A number another producer stored that JavaScript cannot represent exactly -(such as a Go `int64` above 2^53) arrives as an `ExactJsonNumber`, so reading -and re-inserting it never changes it. Validated args only contain one if your -schema accepts it. For untyped `JsonObject` args, use `isJsonNumber` and +A number another producer stored that JavaScript cannot represent exactly (such +as a 64-bit integer above 2^53) arrives as an `ExactJsonNumber`, so reading and +re-inserting it never changes it. Validated args only contain one if your schema +accepts it. For untyped `JsonObject` args, use `isJsonNumber` and `jsonNumberToBigInt`: ```ts @@ -499,4 +496,4 @@ Runnable examples: - [Graceful shutdown](../examples/graceful-shutdown/README.md) - [Hooks and metrics](../examples/hooks-metrics/README.md) - [CPU work on worker threads](../examples/worker-thread-cpu/README.md) -- [A payload shared with Go](../examples/mixed-language/README.md) +- [A payload shared across languages](../examples/mixed-language/README.md) diff --git a/js/docs/databases.md b/js/docs/databases.md index fd0f3c092..c368d516d 100644 --- a/js/docs/databases.md +++ b/js/docs/databases.md @@ -44,24 +44,23 @@ migrator, and CLI. ### YugabyteDB -River's Postgres drivers detect YugabyteDB from the server's `version()` -the first time they need to, and cache what they find for the driver's -lifetime, like River for Go. YugabyteDB has no `xmax`, so a unique insertion -marks each row with a `river:unique_nonce` metadata value instead, as on -SQLite. Without `LISTEN`/`NOTIFY`, which needs YugabyteDB 2025.2.3 or later -with `ysql_yb_enable_listen_notify=true` on both masters and tservers, River -sends no notifications and a client polls even without `pollOnly: true`: it -claims new jobs every queue's `pollInterval`, rereads queue pauses, resumes, -and metadata, and checks its running jobs for cancellations every -`queueControlPollInterval` (two seconds by default), including while a stop -drains them. After enabling notifications, construct a new driver, for -example by restarting, so River detects them. On Postgres 18 and later, -unique insertions read the conflicting row through `RETURNING OLD`. +River's Postgres drivers detect YugabyteDB from the server's `version()` the +first time they need to, and cache what they find for the driver's lifetime. +YugabyteDB has no `xmax`, so a unique insertion marks each row with a +`river:unique_nonce` metadata value instead, as on SQLite. Without +`LISTEN`/`NOTIFY`, which needs YugabyteDB 2025.2.3 or later with +`ysql_yb_enable_listen_notify=true` on both masters and tservers, River sends no +notifications and a client polls even without `pollOnly: true`: it claims new +jobs every queue's `pollInterval`, rereads queue pauses, resumes, and metadata, +and checks its running jobs for cancellations every `queueControlPollInterval` +(two seconds by default), including while a stop drains them. After enabling +notifications, construct a new driver, for example by restarting, so River +detects them. On Postgres 18 and later, unique insertions read the conflicting +row through `RETURNING OLD`. On Postgres, `@riverqueue/migrate` serializes concurrent migrators with a transaction-scoped advisory lock. YugabyteDB has advisory locks only behind a -preview flag, so there, like River for Go's migrator everywhere, it takes no -lock; run one migrator at a time. +preview flag, so there it takes no lock; run one migrator at a time. `@riverqueue/driver-prisma` is deliberately producer-only. It lets application writes and job insertion share a Prisma transaction, but it does not claim or @@ -73,25 +72,24 @@ work jobs. Use the Postgres runtime in worker services. statement briefly blocks the event loop, so run heavily loaded SQLite workers in a process separate from latency-sensitive HTTP handling. -Like River for Go, River runs on a private connection to the application's -database file, in WAL mode, so application statements never join River's -transactions. Pass an application handle with an open transaction as `{ tx }` -to insert jobs with application rows; the driver's `transaction` helper begins -one with `BEGIN IMMEDIATE`. When another connection or process (for example a -Go River client, or an application transaction) holds SQLite's write lock, -River waits with an asynchronous backoff instead of blocking the event loop, -and fails with a retryable error after `busyTimeout`. For in-memory databases, -use `SqliteDriver.memory()`. +River runs on a private connection to the application's database file, in WAL +mode, so application statements never join River's transactions. Pass an +application handle with an open transaction as `{ tx }` to insert jobs with +application rows; the driver's `transaction` helper begins one with `BEGIN +IMMEDIATE`. When another connection or process (for example another River +client, or an application transaction) holds SQLite's write lock, River waits +with an asynchronous backoff instead of blocking the event loop, and fails with +a retryable error after `busyTimeout`. For in-memory databases, use +`SqliteDriver.memory()`. An insertion without `{ tx }` holds SQLite's write lock from its first statement until it commits, and its insert middleware and hooks run inside that transaction. On SQLite they must not await I/O after `next()`. See the [driver's README](../driver/sqlite/README.md) for the transaction model. -River's SQLite tables live in the database's main schema. On SQLite, River -for Go's `Config.Schema` can place them in an attached database instead, but -`SqliteDriver` has no schema option, so Go clients sharing queues with -JavaScript on SQLite must leave `Schema` unset, as with River for Rust. +River's SQLite tables live in the database's main schema. `SqliteDriver` has no +schema option, so other River clients sharing queues with JavaScript on SQLite +must keep their tables in the main schema too, not in an attached database. SQLite timestamps have millisecond precision. IDs remain `bigint`, and public instants remain `Temporal.Instant`. Backend-specific capability differences are diff --git a/js/docs/deployment.md b/js/docs/deployment.md index c9b4f1d0e..76975ffa9 100644 --- a/js/docs/deployment.md +++ b/js/docs/deployment.md @@ -8,11 +8,12 @@ JSON payload schemas as service contracts. ## Unique arguments and resumable work Uniqueness hashes depend on the selected argument paths and serialized JSON. -River sorts top-level keys but preserves nested object order, matching Go. -When selecting a whole nested object, match its key order to the Go producer's -struct or map serialization. Select scalar leaf paths (for example, -`byArgs: ["account.id", "account.region"]`) when nested object order should not -be part of identity. Missing selected fields and explicit `null` are distinct. +River sorts top-level keys but preserves nested object order, matching other +River clients. When selecting a whole nested object, match its key order to how +producers in other languages serialize it, such as struct field declaration +order or sorted map keys. Select scalar leaf paths (for example, `byArgs: +["account.id", "account.region"]`) when nested object order should not be part +of identity. Missing selected fields and explicit `null` are distinct. Resumable step names and cursor shapes must also match between implementations. Keep names stable and await steps sequentially; nested steps are supported, @@ -51,11 +52,11 @@ identity and state transitions rather than direct ad hoc table updates. ## Maintenance in mixed fleets -Leadership is shared across languages: whichever client is elected, Go, Rust, -or JavaScript, runs the rescuer, cleaners, and scheduler for the whole -database, and the rescuer handles every stuck job regardless of which language -was working it. As in Go, a stuck job whose kind the leading client has no -worker for is discarded instead of retried. +Leadership is shared across languages: whichever client is elected, Go, Rust, or +JavaScript, runs the rescuer, cleaners, and scheduler for the whole database, +and the rescuer handles every stuck job regardless of which language was working +it. A stuck job whose kind the leading client has no worker for is discarded +instead of retried. When job kinds are specific to one language, keep each language's kinds in queues that only that language's clients work, and register the other diff --git a/js/docs/errors-and-retries.md b/js/docs/errors-and-retries.md index bb007898a..2845995d6 100644 --- a/js/docs/errors-and-retries.md +++ b/js/docs/errors-and-retries.md @@ -12,38 +12,37 @@ A handler's result decides what happens to its job: | returns `discard({ reason })` | `discarded` without further retries | | returns `cancel({ reason })` | `cancelled`, recording `JobCancelError: ` as its error | -Outcomes are closed values built by those helpers; returning any other object -is an error, so output is never recorded by accident. Use `snooze` for "try -again later" conditions such as rate limits, `discard` when retrying can't -help (a permanently invalid request), and `cancel` when the work should no -longer happen at all. A snooze or retry due within the scheduler interval (5 -seconds by default) becomes available immediately, as in River for Go. +Outcomes are closed values built by those helpers; returning any other object is +an error, so output is never recorded by accident. Use `snooze` for "try again +later" conditions such as rate limits, `discard` when retrying can't help (a +permanently invalid request), and `cancel` when the work should no longer happen +at all. A snooze or retry due within the scheduler interval (5 seconds by +default) becomes available immediately. Recorded errors hold the thrown message, bounded in size, and the time the -attempt started. Like River for Go, which records stack traces only for -panics, River records a stack only for JavaScript's runtime faults (a native -`TypeError`, `RangeError`, `ReferenceError`, `SyntaxError`, `EvalError`, or -`URIError`); deliberate errors record their message alone. Don't put secrets +attempt started. River records a stack only for JavaScript's runtime faults (a +native `TypeError`, `RangeError`, `ReferenceError`, `SyntaxError`, `EvalError`, +or `URIError`); deliberate errors record their message alone. Don't put secrets or full payloads in error messages: they are stored on the job. -A job whose row River can't read (for example, metadata another tool rewrote -as a JSON array) is never worked, and neither is a job whose kind has no -worker. Its attempt fails before hooks and middleware run, with an error such -as `job row couldn't be decoded: …`, so `errorHandler` sees it and the retry -schedule applies as usual. `client.jobs.get` reports such a row as an error, -but `client.jobs.list`, `cancel`, `retry`, and `delete` still act on it, as in -River for Go, returning the job with the fields River couldn't read left empty -(`{}` or `[]`). Like River for Go, a recorded error that another tool wrote in -a shape River doesn't, such as an `at` that isn't an RFC 3339 timestamp, an -`attempt` stored as a string, or an `error` that's an object, doesn't make -the row unreadable: River keeps what it can of it, leaving such an `at` as the -zero time `0001-01-01T00:00:00Z` and keeping other values as their JSON text. +A job whose row River can't read (for example, metadata another tool rewrote as +a JSON array) is never worked, and neither is a job whose kind has no worker. +Its attempt fails before hooks and middleware run, with an error such as `job +row couldn't be decoded: …`, so `errorHandler` sees it and the retry schedule +applies as usual. `client.jobs.get` reports such a row as an error, but +`client.jobs.list`, `cancel`, `retry`, and `delete` still act on it, returning +the job with the fields River couldn't read left empty (`{}` or `[]`). A +recorded error that another tool wrote in a shape River doesn't, such as an `at` +that isn't an RFC 3339 timestamp, an `attempt` stored as a string, or an `error` +that's an object, doesn't make the row unreadable: River keeps what it can of +it, leaving such an `at` as the zero time `0001-01-01T00:00:00Z` and keeping +other values as their JSON text. ## Retry schedule By default the delay before attempt _n + 1_ is _n⁴_ seconds with ±10% jitter, -where _n_ is the number of errors so far, the same curve as River for Go. Set -`maxAttempts` per job, per definition (`defaults`), or per client +where _n_ is the number of errors so far, the same curve as other River clients. +Set `maxAttempts` per job, per definition (`defaults`), or per client (`defaultInsertOptions`). Replace the schedule with `retryPolicy`: tests/ui/unique_period_invalid.rs:5:46 | 5 | #[river(kind = "invoice", unique(by_period = "1 hour"))] diff --git a/rust/riverqueue-migrate/README.md b/rust/riverqueue-migrate/README.md index ecf42b1b7..6441972f6 100644 --- a/rust/riverqueue-migrate/README.md +++ b/rust/riverqueue-migrate/README.md @@ -1,9 +1,9 @@ # riverqueue-migrate -River's Postgres and SQLite migrations for Rust, identical to the ones River -for Go applies, so either language can migrate a database the other uses. The -`riverqueue` command from the `riverqueue-cli` crate runs the same migrations -from a shell. +River's Postgres and SQLite migrations for Rust, identical to the ones other +River clients apply, so a client in any language can migrate a database the +others use. The `riverqueue` command from the `riverqueue-cli` crate runs the +same migrations from a shell. Use `PostgresMigrator` for Postgres and `SqliteMigrator` for SQLite: diff --git a/rust/riverqueue/README.md b/rust/riverqueue/README.md index 671b4d20c..ec4cd8c78 100644 --- a/rust/riverqueue/README.md +++ b/rust/riverqueue/README.md @@ -2,10 +2,11 @@ `riverqueue` is the Rust and Tokio client for [River](https://riverqueue.com), a fast and reliable background job system backed by Postgres or SQLite. It -shares River's database schema and job protocol with River for Go, so Rust -and Go services can insert and work jobs in the same database. +shares River's database schema and job protocol with River clients in other +languages, so services written in different languages can insert and work +jobs in the same database. -The [mixed deployment guide](https://docs.rs/riverqueue/latest/riverqueue/guide/mixed_deployments/index.html) covers running both against one database. +The [mixed deployment guide](https://docs.rs/riverqueue/latest/riverqueue/guide/mixed_deployments/index.html) covers running Rust alongside other River clients against one database. ## Installation @@ -229,12 +230,12 @@ completes the job. After the configured stuck threshold, River can abort a Tokio task that yields, which fails its attempt, but it can't stop CPU-bound work or a blocking call already in progress. -Collect workers in [`Workers`], the equivalent of Go's `Workers` bundle. -Use [`Workers::add`] to add a [`Worker`] implementation, like Go's `AddWorker`. -Implement [`Worker`] when a kind needs a custom timeout or next-retry decision. -Use [`Workers::add_fn`] for an async function or capturing closure; it combines -Go's `WorkFunc` adapter and registration in one call. Both methods return -`Result` so registration errors can be propagated with `?`. +Collect workers in a [`Workers`] bundle, keyed by job kind. Use +[`Workers::add`] to add a [`Worker`] implementation. Implement [`Worker`] when +a kind needs a custom timeout or next-retry decision. Use [`Workers::add_fn`] +to register an async function or capturing closure as a worker in one call. +Both methods return `Result` so registration errors can be propagated with +`?`. ## Events @@ -325,7 +326,7 @@ async fn sqlite_client() -> Result> { - [`job`]: arguments, insertion options, persisted rows, outcomes, and unique job configuration. - [`encoding`]: the JSON encoding River uses for job arguments, which keeps - unique keys identical across Rust and Go. + unique keys identical across River clients in every language. - [`worker`]: typed workers, function registration, cancellation, outputs, and resumable work. - [`event`]: event payloads and bounded subscriptions. diff --git a/rust/riverqueue/docs/mixed-deployments.md b/rust/riverqueue/docs/mixed-deployments.md index 119d11211..acad7153c 100644 --- a/rust/riverqueue/docs/mixed-deployments.md +++ b/rust/riverqueue/docs/mixed-deployments.md @@ -44,8 +44,8 @@ the leader election advice below, since the leader's rescuer still handles stuck jobs of every kind. The elected leader runs maintenance for the whole database, including the job -rescuer. Like River Go, the rescuer discards stuck jobs whose kind the leader -has no worker for rather than retrying them. If kinds are split between +rescuer. The rescuer discards stuck jobs whose kind the leader has no worker +for rather than retrying them. If kinds are split between languages, either register a worker for every kind in every client that can be elected leader, or keep clients that don't know every kind out of leader election: `ClientBuilder::without_leader_election` in Rust and @@ -85,7 +85,7 @@ enqueued whenever a client without it is elected. Clients kept out of leader election can't configure periodic jobs, but still work those a leader enqueues in their queues. -Cron schedules use Go River's standard five-field syntax in both languages. +Cron schedules use the same standard five-field syntax in both languages. A schedule with a `CRON_TZ=` prefix naming an IANA zone parses in Rust only with the `chrono-tz` feature enabled; without it, Rust rejects the schedule rather than evaluating it in another zone. diff --git a/rust/riverqueue/examples/mixed_go_rust.rs b/rust/riverqueue/examples/mixed_go_rust.rs index 1e622de1c..5fca37c10 100644 --- a/rust/riverqueue/examples/mixed_go_rust.rs +++ b/rust/riverqueue/examples/mixed_go_rust.rs @@ -92,7 +92,7 @@ async fn main() -> Result<(), Box> { .workers(workers) // Only Rust's queue: Rust never fetches the Go service's jobs. .queue("rust_images", QueueConfig::new(4)) - // Allow inserting kinds only Go works, like Go's SkipUnknownJobCheck. + // Allow inserting kinds that only the Go service works. .allow_unregistered_job_kinds() .build()?; let mut completed = client.subscribe(&[EventKind::JobCompleted])?; diff --git a/rust/riverqueue/examples/periodic.rs b/rust/riverqueue/examples/periodic.rs index e42d600a9..c2407c926 100644 --- a/rust/riverqueue/examples/periodic.rs +++ b/rust/riverqueue/examples/periodic.rs @@ -59,7 +59,7 @@ async fn main() -> Result<(), Box> { .with_id("refresh_cache") .with_run_on_start(true), )) - // Standard five-field cron syntax, as in River Go: 02:30 UTC daily. + // Standard five-field cron syntax: 02:30 UTC daily. .periodic_job(PeriodicJob::with_options( CronSchedule::parse("30 2 * * *")?, || NightlyReport {}, diff --git a/rust/riverqueue/src/client/builder.rs b/rust/riverqueue/src/client/builder.rs index e26b1f668..0c378c36d 100644 --- a/rust/riverqueue/src/client/builder.rs +++ b/rust/riverqueue/src/client/builder.rs @@ -429,9 +429,8 @@ impl ClientBuilder { /// back. A job whose notification was skipped is found by the next fetch /// of its queue, which may wait for the queue's fetch poll interval. /// - /// Like River Go's `Config.FetchCooldown`, it defaults to - /// [`FETCH_COOLDOWN_DEFAULT`](crate::FETCH_COOLDOWN_DEFAULT) (100 - /// milliseconds) and must be at least + /// It defaults to [`FETCH_COOLDOWN_DEFAULT`](crate::FETCH_COOLDOWN_DEFAULT) + /// (100 milliseconds) and must be at least /// [`FETCH_COOLDOWN_MIN`](crate::FETCH_COOLDOWN_MIN) (one millisecond). /// A queue's fetch poll interval can't be shorter than the cooldown it /// uses. @@ -442,9 +441,8 @@ impl ClientBuilder { } /// Restricts claims to the kinds of registered workers, including their - /// aliases, like River Go's `Config.FetchOnlyKnownKinds`. Jobs of other - /// kinds stay available without using attempts, so clients with - /// different workers can share a queue. + /// aliases. Jobs of other kinds stay available without using attempts, so + /// clients with different workers can share a queue. /// /// It only affects claiming. A leader's rescuer still handles stuck jobs /// in every queue and discards those whose kinds it doesn't know, so a diff --git a/rust/riverqueue/src/database.rs b/rust/riverqueue/src/database.rs index f42dcc53d..8dc343350 100644 --- a/rust/riverqueue/src/database.rs +++ b/rust/riverqueue/src/database.rs @@ -367,8 +367,7 @@ impl PostgresReindexConfig { self } - /// Lets index rebuilds run without a time limit, like River Go's - /// `ReindexerTimeout` of `-1`. + /// Lets index rebuilds run without a time limit. #[must_use] pub const fn without_timeout(mut self) -> Self { self.timeout = None; diff --git a/rust/riverqueue/src/encoding.rs b/rust/riverqueue/src/encoding.rs index bfe7f2768..34a80e545 100644 --- a/rust/riverqueue/src/encoding.rs +++ b/rust/riverqueue/src/encoding.rs @@ -1,36 +1,35 @@ -//! Go-compatible JSON encoding for job arguments. +//! River's canonical JSON encoding for job arguments. //! //! River stores job arguments as the exact JSON bytes produced when they were -//! inserted, and River Go hashes those bytes to build unique keys. For a Rust -//! job and an equivalent Go job to share a unique key, they must encode to the -//! same bytes. [`encode_args`] serializes with [`serde_json`] using the output -//! rules of Go's `encoding/json`: +//! inserted, and hashes those bytes to build unique keys. For a job inserted +//! by this client and an equivalent job inserted by another River client +//! sharing the database to share a unique key, they must encode to the same +//! bytes. [`encode_args`] serializes with [`serde_json`] using River's +//! canonical output rules: //! -//! * Floats use Go's shortest round-trip digits, in plain decimal notation -//! when `1e-6 <= |x| < 1e21` and exponent notation otherwise (`1`, `0.1`, +//! * Floats use the shortest digits that round-trip, breaking exact ties +//! toward an even last digit, in plain decimal notation when +//! `1e-6 <= |x| < 1e21` and exponent notation otherwise (`1`, `0.1`, //! `100000000000000000000`, `1e+21`, `1e-7`). Negative zero encodes as `-0`. //! * Strings escape `<`, `>`, `&`, U+2028, and U+2029 as `\u003c`, `\u003e`, //! `\u0026`, `\u2028`, and `\u2029`. Control characters use `\b`, `\f`, //! `\n`, `\r`, and `\t` where available and lowercase `\u00XX` otherwise. -//! * Struct fields keep their declaration order, as in Go. +//! * Struct fields keep their declaration order. //! //! Some differences come from how types serialize rather than from the JSON //! encoder and must be handled in the argument type: //! -//! * Go sorts map keys. Use an ordered map such as -//! [`BTreeMap`](std::collections::BTreeMap) with string keys to match; a -//! `HashMap` serializes in an unspecified order. -//! * Go encodes `[]byte` as a base64 string; a Rust `Vec` encodes as an -//! array of numbers. -//! * Go encodes `time.Time` with RFC 3339 and the shortest fractional -//! seconds, while `chrono` pads fractional seconds to 3, 6, or 9 digits. Use +//! * Map keys must serialize in sorted order. Use an ordered map such as +//! [`BTreeMap`](std::collections::BTreeMap) with string keys; a `HashMap` +//! serializes in an unspecified order. +//! * River clients in other languages may encode byte strings as base64 +//! strings; a Rust `Vec` encodes as an array of numbers. +//! * Canonical timestamps use RFC 3339 with the shortest fractional seconds, +//! while `chrono` pads fractional seconds to 3, 6, or 9 digits. Use //! [`go_time`] for `DateTime` fields that participate in unique keys. //! -//! Like Go, encoding fails for non-finite floats (`NaN` and infinities) -//! instead of writing them as `null` the way [`serde_json`] does. -//! -//! These rules match Go 1.22 and later, which escape backspace and form feed -//! as `\b` and `\f`. +//! Encoding fails for non-finite floats (`NaN` and infinities) instead of +//! writing them as `null` the way [`serde_json`] does. use std::{fmt, fmt::Write as _, io}; @@ -45,8 +44,8 @@ mod finite; pub(crate) use finite::check as check_finite; -/// Encodes job arguments to JSON bytes identical to those Go's -/// `encoding/json` produces for an equivalent Go value. +/// Encodes job arguments to River's canonical JSON bytes, identical to those +/// other River clients produce for an equivalent value. /// /// River uses this encoding for every inserted job, so it is only needed /// directly when constructing [`JobRow`](crate::JobRow) values by hand, such @@ -56,24 +55,24 @@ pub(crate) use finite::check as check_finite; /// /// Returns an error when the value's [`Serialize`] implementation fails, for /// example because a map has non-string keys, or when it contains a -/// non-finite float, which Go's `encoding/json` rejects as an unsupported -/// value. +/// non-finite float, which is rejected as an unsupported value. pub fn encode_args(args: &T) -> Result, serde_json::Error> { RawValue::from_string(to_go_string(args)?) } -/// Serde helpers that encode a `DateTime` the way Go's `encoding/json` -/// encodes a UTC `time.Time`: RFC 3339 with the shortest fractional seconds -/// (`2026-01-02T03:04:05.5Z`). +/// Serde helpers that encode a `DateTime` as River's canonical +/// timestamp: RFC 3339 in UTC with `Z` and the shortest fractional seconds, +/// with no fractional part when it's zero (`2026-01-02T03:04:05.5Z`). /// /// Use it with `#[serde(with = "riverqueue::encoding::go_time")]` on argument -/// fields that participate in Go-compatible unique keys. Deserialization -/// accepts any RFC 3339 timestamp. +/// fields that participate in unique keys shared with other River clients. +/// Deserialization accepts any RFC 3339 timestamp. pub mod go_time { use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer, Serializer}; - /// Serializes a timestamp in Go's `time.RFC3339Nano` form. + /// Serializes a timestamp in RFC 3339 form with the shortest fractional + /// seconds. /// /// # Errors /// diff --git a/rust/riverqueue/src/job.rs b/rust/riverqueue/src/job.rs index 0a879a9ce..df1533dde 100644 --- a/rust/riverqueue/src/job.rs +++ b/rust/riverqueue/src/job.rs @@ -25,7 +25,7 @@ pub trait JobArgs: DeserializeOwned + Send + Serialize + Sync + 'static { /// Job-type insertion defaults. Options set for a single insertion /// replace these. Per-call metadata replaces the defaults' metadata - /// entirely, as in River Go, so it doesn't keep any of their keys. + /// entirely, so it doesn't keep any of their keys. fn default_insert_opts() -> InsertOpts { InsertOpts::default() } @@ -103,8 +103,10 @@ pub(crate) struct InsertBatchItem { /// A failed job attempt persisted in `river_job.errors`. /// -/// Attempt errors deserialize from JSON like River Go's `encoding/json` -/// decodes them, which requires a JSON deserializer such as `serde_json`'s. +/// Attempt errors deserialize from JSON strictly, which requires a JSON +/// deserializer such as `serde_json`'s: field names match ignoring ASCII +/// case, and each field must be `null` or a value of its own type, with `at` +/// an RFC 3339 timestamp. /// Job rows read from the database decode their attempt errors leniently /// instead: an element written by another tool or edited by hand decodes on a /// best effort basis rather than making its row unreadable. diff --git a/rust/riverqueue/src/job/attempt_error.rs b/rust/riverqueue/src/job/attempt_error.rs index 5354cdd30..7f42a64a1 100644 --- a/rust/riverqueue/src/job/attempt_error.rs +++ b/rust/riverqueue/src/job/attempt_error.rs @@ -19,10 +19,15 @@ use serde_json::value::RawValue; use super::AttemptError; impl<'de> Deserialize<'de> for AttemptError { - /// Decodes an attempt error like Go's `encoding/json`: fields match - /// case-insensitively, a repeated field takes its last value, missing, - /// `null`, and unknown fields are accepted, and `at` must be an RFC 3339 - /// timestamp. A field of any other type is an error. + /// Decodes an attempt error strictly. Field names match ignoring ASCII + /// case, and a repeated field takes its last value, although an invalid + /// value it replaces is still an error. Missing, `null`, and unknown + /// fields are accepted, with a missing or `null` field left as the zero + /// value (`at` as 0001-01-01T00:00:00Z, `attempt` as 0, and `error` and + /// `trace` as empty strings). `at` must be an RFC 3339 timestamp, + /// `attempt` an integer literal, and `error` and `trace` strings; a field + /// of any other type is an error. A top-level `null` decodes as an attempt + /// error with every field zero. /// /// Only a JSON deserializer (such as [`serde_json`]'s) can decode an /// attempt error. diff --git a/rust/riverqueue/src/periodic/cron.rs b/rust/riverqueue/src/periodic/cron.rs index 89941d355..19dc8dd20 100644 --- a/rust/riverqueue/src/periodic/cron.rs +++ b/rust/riverqueue/src/periodic/cron.rs @@ -133,8 +133,9 @@ impl CronSchedule { /// /// # Errors /// - /// Returns an error for an expression robfig/cron's `ParseStandard` - /// rejects, and for a `CRON_TZ=` zone this build can't resolve. + /// Returns an error for an expression that isn't a valid standard cron + /// expression or descriptor, and for a `CRON_TZ=` zone this build can't + /// resolve. pub fn parse(expression: &str) -> Result { expression.parse() } @@ -153,11 +154,10 @@ impl CronSchedule { } /// Returns the first occurrence strictly after `after`, or `None` when - /// the schedule never matches within five years (robfig's zero time). + /// the schedule never matches within five years. /// - /// Without an explicit zone, the occurrence is computed in `after`'s own - /// time zone, exactly like robfig's `Next` for a schedule without - /// `CRON_TZ`. + /// Without an explicit zone or `CRON_TZ=` prefix, the occurrence is + /// computed in `after`'s own time zone. #[must_use] pub fn next_after(&self, after: &DateTime) -> Option> { let zone = after.timezone(); diff --git a/rust/riverqueue/src/periodic/mod.rs b/rust/riverqueue/src/periodic/mod.rs index 69ffe8ffb..fc2481626 100644 --- a/rust/riverqueue/src/periodic/mod.rs +++ b/rust/riverqueue/src/periodic/mod.rs @@ -28,10 +28,9 @@ pub trait PeriodicSchedule: Send + Sync + 'static { pub struct IntervalSchedule(Duration); impl IntervalSchedule { - /// Creates a fixed schedule. Like River Go's `PeriodicInterval`, any - /// positive interval is accepted, including sub-second ones, so the same - /// periodic jobs can be configured in Rust and Go clients that share a - /// database. + /// Creates a fixed schedule. Any positive interval is accepted, including + /// sub-second ones, so the same periodic jobs can be configured in every + /// River client sharing a database. /// /// # Errors /// diff --git a/rust/riverqueue/src/unique.rs b/rust/riverqueue/src/unique.rs index 623b12976..0d0bb92d7 100644 --- a/rust/riverqueue/src/unique.rs +++ b/rust/riverqueue/src/unique.rs @@ -247,7 +247,7 @@ fn lookup_path<'a>(encoded_args: &'a str, path: &[&str]) -> Result Result<(), Error> { if segment.is_empty() { return Err(unique_args_error(format!( - "{description} is empty, which River Go cannot hash" + "{description} is empty and can't be part of a unique key" ))); } if segment.bytes().all(|byte| byte.is_ascii_digit()) || segment == "-1" {