From 1147efa915fd5bb18158b3683b965d257a23c21b Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:19:35 +0530 Subject: [PATCH 1/4] chore: release 0.22.0 --- CHANGELOG.md | 75 ++++++++++++++++++- Cargo.lock | 16 ++-- Cargo.toml | 2 +- deploy/helm/taskito-server/Chart.yaml | 4 +- docs/app/lib/version.ts | 2 +- .../docs/java/api-reference/testing.mdx | 2 +- .../java/getting-started/installation.mdx | 14 ++-- .../docs/java/guides/integrations/index.mdx | 2 +- .../docs/java/guides/integrations/spring.mdx | 2 +- .../docs/java/guides/resources/testing.mdx | 2 +- docs/content/docs/resources/changelog.mdx | 75 ++++++++++++++++++- .../docs/shared/guides/operations/testing.mdx | 2 +- sdks/java/README.md | 14 ++-- sdks/java/gradle.properties | 2 +- sdks/node/package.json | 2 +- sdks/python/taskito/__init__.py | 2 +- 16 files changed, 176 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d60cb8c..980758b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ All notable changes to taskito are documented here. The format is based on [Semantic Versioning](https://semver.org/spec/v2.0.0.html). All SDKs (Python, Node, Java) and the underlying Rust crates are released together, in lock-step. -## Unreleased +## 0.22.0 + +Attach and executor release. `taskito-server` becomes a standalone binary that schedules for an +app written against any SDK, workers attach to it over a shared worker protocol, and an executor +side-channel carries progress, logs and middleware toggles back. Namespace scoping is completed +across the id-addressed storage APIs, and cross-SDK parity lands the operator admin surface, +predicates, retry policy and the full event taxonomy in Node and Java. ### Changed @@ -28,9 +34,24 @@ underlying Rust crates are released together, in lock-step. `status` filter (`DeliveryStatus`, Python). Same non-breaking contract — a string still works. Internally, Python's workflow tracker and saga orchestrator now compare node/run status against the enums rather than string literals. +- **The Node native binding is built on napi 3.** An internal upgrade from napi 2 with no change + to the published API; the minimum supported Node version stays 20. ### Added +- **`taskito-server`: a standalone scheduler, attach listener and dashboard.** One Rust binary + runs the scheduler loop and serves the dashboard for an app written against any SDK, so the fat + application image no longer has to be redeployed as a worker. Workers attach to it over a shared + worker protocol — a JSON header line plus raw payload bytes, one format for both the prefork + pipe and the attach socket — dispatched through a `RemoteDispatcher` in the core. Database + credentials stay in the server rather than the app image. +- **A `taskito executor` subcommand in every SDK.** Python, Node and Java each ship an executor + that attaches to a running server, advertises the tasks it can run, and executes only those — + the process side of the attach topology above. +- **An executor side-channel for progress, logs and middleware toggles.** An attached executor + reports `update_progress`, task logs, and middleware enable/disable back over the same + connection, so an attached worker is observable and controllable exactly like an in-process one. + Implemented in the core and wired through all three SDKs. - **A Helm chart for `taskito-server`, with annotation-driven sidecar injection.** `deploy/helm/taskito-server` installs the scheduler, dashboard and injector as one release, and refuses to render the combinations the server rejects at boot — an attach listener with no token, @@ -96,16 +117,36 @@ underlying Rust crates are released together, in lock-step. `durationMs` on Node's, and `duration_ms` on Python's job event payloads. Java also gains `NodeSnapshot.durationMs()` / `compensationDurationMs()` and `TaskContext.elapsedMs()`. - **Full lifecycle event taxonomy in the Node and Java SDKs.** Both grow from the 4 job-outcome - events to the 26-event cross-SDK taxonomy: enqueue, per-attempt failure (`job.failed`), worker + events to the 29-event cross-SDK taxonomy: enqueue, per-attempt failure (`job.failed`), worker lifecycle, queue pause/resume, workflow submit/terminal/gate, saga compensation, and predicate rejection. Node types `queue.on()` per event name (`EventMap`); Java adds queue-level `taskito.onEvent(EventName, Consumer)` with typed event records alongside the unchanged worker-scoped `Worker.Builder.on(...)`. Webhooks can subscribe to every event name. - `predicate.cancelled` stays Python-only, and Python now emits the previously dormant - `workflow.submitted`. + `predicate.cancelled` is reserved in all three — subscribable, typed and webhook-matchable — + but only Python emits it: Node and Java gate at enqueue, where a terminal skip is + `predicate.skipped` because no job exists yet. - **Java webhook deliveries use the dotted wire names.** Bodies that previously said `"event": "success"` now say `"event": "job.completed"`; stored subscriptions with the legacy four names keep matching, and job-outcome bodies gain `duration_ms`. +- **Retention dry-run.** `dry_run_retention()` reports what a purge would delete right now — per + table and in total, against the windows the elected cleaner actually published — so a window can + be sized before it deletes anything. The dashboard echoes those live windows rather than + guessing at the defaults. +- **Retry predicates in the Node and Java SDKs.** A per-task `retryOn` classifies a thrown error: + return `false` and the job dead-letters immediately, whatever retry budget is left. One + predicate covers both directions — test for the errors worth retrying, or negate a test for the + permanent ones. Java can signal the same intent by exception type instead, via + `RetryableException` / `NonRetryableException`, so a domain hierarchy doubles as the policy. +- **Push-dispatch in the Node and Java shells.** Both accept the `pushDispatch` option, so an + enqueue wakes the scheduler immediately instead of waiting out the poll interval. +- **Node standalone health and readiness helpers.** Liveness and readiness checks that run without + the dashboard, for a worker deployment that serves no HTTP of its own. +- **Python periodic-schedule catalog management.** `list_periodic()`, `delete_periodic(name)`, + `pause_periodic(name)` and `resume_periodic(name)` operate the cron catalog at runtime, so a + schedule can be suspended or dropped without a redeploy. +- **Java classifier jars and GraalVM metadata.** Per-platform classifier artifacts publish + alongside the fat jar, and bundled reachability metadata makes the SDK usable from a + GraalVM native image. ### Fixed @@ -132,6 +173,32 @@ underlying Rust crates are released together, in lock-step. default `atexit` teardown. Taskito now initializes OpenSSL itself with that teardown suppressed (`OPENSSL_INIT_NO_ATEXIT`) before the first Postgres connection. +- **Namespace scoping is complete across the id-addressed storage APIs.** Every method that + addresses a row by id now takes the namespace it must match, so one tenant can no longer read, + mutate or complete another's job by guessing its id. Cross-namespace dependencies are rejected + at enqueue, and workflow runs are scoped through the store handle. +- **Dashboard settings are compare-and-set in every SDK.** Two dashboards editing the same JSON + settings document no longer silently overwrite each other; a stale write is rejected instead of + clobbering the newer one. +- **The predicate event taxonomy is complete across SDKs.** The gate outcome names each SDK + recognized diverged, so a webhook subscribed to one could not be registered against another. + All three now carry the same set. Separately, `workflow.submitted` fires on run submission + rather than staying dormant. +- **Java null-safety contract and native platform coverage.** Nullability is declared across the + public API, and the published artifact carries every supported platform's native library rather + than only the build host's. `JobRec` reads are guarded by the backend monitor, and the build no + longer silently disables every compile task. +- **The Redis task-metric retention purge is bounded.** It read its whole below-cutoff window in + one `ZRANGEBYSCORE` and issued a `GET` per id, so the first sweep after a long retention gap + stalled the maintenance tick. It now drains in batches like every other purge, with one `MGET` + per batch. +- **Batch enqueue applies `unique_key` dedup.** A batch carrying any keyed entry now routes + through the unique path rather than the plain multi-row insert, which would hit the partial + unique index and fail the whole batch the moment a key duplicated an active job. Shared by the + Node and Java batch enqueues; a keyless batch keeps the chunked throughput path, and Python + still batch-inserts without dedup. +- Version drift in the Node CLI banner and the Java installation docs. + ## 0.21.0 Overload-controls and retention release. The queue gains admission and load-shedding controls, diff --git a/Cargo.lock b/Cargo.lock index 0a5543a46..81a0165ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3155,7 +3155,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "taskito-core" -version = "0.21.0" +version = "0.22.0" dependencies = [ "async-trait", "chrono", @@ -3180,7 +3180,7 @@ dependencies = [ [[package]] name = "taskito-java" -version = "0.21.0" +version = "0.22.0" dependencies = [ "async-trait", "base64 0.23.0", @@ -3200,7 +3200,7 @@ dependencies = [ [[package]] name = "taskito-mesh" -version = "0.21.0" +version = "0.22.0" dependencies = [ "base64 0.23.0", "bincode", @@ -3215,7 +3215,7 @@ dependencies = [ [[package]] name = "taskito-node" -version = "0.21.0" +version = "0.22.0" dependencies = [ "async-trait", "crossbeam-channel", @@ -3234,7 +3234,7 @@ dependencies = [ [[package]] name = "taskito-python" -version = "0.21.0" +version = "0.22.0" dependencies = [ "async-trait", "crossbeam-channel", @@ -3253,7 +3253,7 @@ dependencies = [ [[package]] name = "taskito-server" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "axum", @@ -3283,7 +3283,7 @@ dependencies = [ [[package]] name = "taskito-tui" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "clap", @@ -3295,7 +3295,7 @@ dependencies = [ [[package]] name = "taskito-workflows" -version = "0.21.0" +version = "0.22.0" dependencies = [ "dagron-core", "diesel", diff --git a/Cargo.toml b/Cargo.toml index 829cb6915..c64d65391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" # release bumps `version` in exactly one place. `rust-version` is verified by # the CI msrv job; sea-query 1.x pins the floor at 1.88. [workspace.package] -version = "0.21.0" +version = "0.22.0" edition = "2021" license = "MIT" repository = "https://github.com/ByteVeda/taskito" diff --git a/deploy/helm/taskito-server/Chart.yaml b/deploy/helm/taskito-server/Chart.yaml index 804876aed..85c653495 100644 --- a/deploy/helm/taskito-server/Chart.yaml +++ b/deploy/helm/taskito-server/Chart.yaml @@ -5,8 +5,8 @@ type: application # Chart version and appVersion move together: the chart only ever ships the # image built from the same commit, and `node scripts/version.mjs --check` # enforces that in CI. -version: 0.21.0 -appVersion: "0.21.0" +version: 0.22.0 +appVersion: "0.22.0" home: https://taskito.grigori.in sources: - https://github.com/ByteVeda/taskito diff --git a/docs/app/lib/version.ts b/docs/app/lib/version.ts index 6387268c1..20ef9c912 100644 --- a/docs/app/lib/version.ts +++ b/docs/app/lib/version.ts @@ -1,2 +1,2 @@ // AUTO-GENERATED from /CHANGELOG.md by scripts/sync-changelog.mjs — do not edit directly. -export const VERSION = "0.21.0"; +export const VERSION = "0.22.0"; diff --git a/docs/content/docs/java/api-reference/testing.mdx b/docs/content/docs/java/api-reference/testing.mdx index 3db13427a..40607970d 100644 --- a/docs/content/docs/java/api-reference/testing.mdx +++ b/docs/content/docs/java/api-reference/testing.mdx @@ -14,7 +14,7 @@ would against a native backend. For a walkthrough with assertions, see the [Testing guide](/java/guides/operations/testing). ```kotlin -testImplementation("org.byteveda:taskito-test:0.21.0") +testImplementation("org.byteveda:taskito-test:0.22.0") ``` ## `InMemoryTaskito` diff --git a/docs/content/docs/java/getting-started/installation.mdx b/docs/content/docs/java/getting-started/installation.mdx index 5dcc5c11f..efc642830 100644 --- a/docs/content/docs/java/getting-started/installation.mdx +++ b/docs/content/docs/java/getting-started/installation.mdx @@ -14,15 +14,15 @@ per-platform classifier artifact next to a native-free main jar — add both. plugins { id("com.google.osdetector") version "1.7.3" } dependencies { - implementation("org.byteveda:taskito:0.21.0") - runtimeOnly("org.byteveda:taskito:0.21.0:${osdetector.classifier}") // native library - annotationProcessor("org.byteveda:taskito-processor:0.21.0") // compile-time TaskHandler bindings + implementation("org.byteveda:taskito:0.22.0") + runtimeOnly("org.byteveda:taskito:0.22.0:${osdetector.classifier}") // native library + annotationProcessor("org.byteveda:taskito-processor:0.22.0") // compile-time TaskHandler bindings } ``` The [osdetector](https://github.com/google/osdetector-gradle-plugin) plugin resolves your platform's classifier; you can also write it literally, e.g. -`runtimeOnly("org.byteveda:taskito:0.21.0:linux-x86_64")`. +`runtimeOnly("org.byteveda:taskito:0.22.0:linux-x86_64")`. @@ -42,12 +42,12 @@ resolves your platform's classifier; you can also write it literally, e.g. org.byteveda taskito - 0.21.0 + 0.22.0 org.byteveda taskito - 0.21.0 + 0.22.0 ${os.detected.classifier} runtime @@ -63,7 +63,7 @@ resolves your platform's classifier; you can also write it literally, e.g. org.byteveda taskito-processor - 0.21.0 + 0.22.0 diff --git a/docs/content/docs/java/guides/integrations/index.mdx b/docs/content/docs/java/guides/integrations/index.mdx index c5dadaef8..864f3ec17 100644 --- a/docs/content/docs/java/guides/integrations/index.mdx +++ b/docs/content/docs/java/guides/integrations/index.mdx @@ -11,7 +11,7 @@ add the matching runtime dependency yourself. ```kotlin // Only add what you use. -implementation("org.byteveda:taskito-spring:0.21.0") // Spring Boot 3 starter +implementation("org.byteveda:taskito-spring:0.22.0") // Spring Boot 3 starter implementation("io.micrometer:micrometer-observation:1.13.6") // contrib.TaskitoObservation implementation("io.sentry:sentry:7.14.0") // contrib.SentryMiddleware ``` diff --git a/docs/content/docs/java/guides/integrations/spring.mdx b/docs/content/docs/java/guides/integrations/spring.mdx index dd27df7c6..6bf1c28f8 100644 --- a/docs/content/docs/java/guides/integrations/spring.mdx +++ b/docs/content/docs/java/guides/integrations/spring.mdx @@ -7,7 +7,7 @@ The `taskito-spring` starter auto-configures a `Taskito` bean for Spring Boot 3 applications. ```kotlin -implementation("org.byteveda:taskito-spring:0.21.0") +implementation("org.byteveda:taskito-spring:0.22.0") ``` When the starter is on the classpath, `TaskitoAutoConfiguration` builds and diff --git a/docs/content/docs/java/guides/resources/testing.mdx b/docs/content/docs/java/guides/resources/testing.mdx index 083ee5560..76249e19a 100644 --- a/docs/content/docs/java/guides/resources/testing.mdx +++ b/docs/content/docs/java/guides/resources/testing.mdx @@ -19,7 +19,7 @@ backend — no JNI, no disk. The resource runtime is backend-independent, so resources build, scope, and tear down exactly the same way over it: ```kotlin -testImplementation("org.byteveda:taskito-test:0.21.0") +testImplementation("org.byteveda:taskito-test:0.22.0") ``` ```java diff --git a/docs/content/docs/resources/changelog.mdx b/docs/content/docs/resources/changelog.mdx index aec3a7bf7..f69af4b60 100644 --- a/docs/content/docs/resources/changelog.mdx +++ b/docs/content/docs/resources/changelog.mdx @@ -12,7 +12,13 @@ All notable changes to taskito are documented here. The format is based on [Semantic Versioning](https://semver.org/spec/v2.0.0.html). All SDKs (Python, Node, Java) and the underlying Rust crates are released together, in lock-step. -## Unreleased +## 0.22.0 + +Attach and executor release. `taskito-server` becomes a standalone binary that schedules for an +app written against any SDK, workers attach to it over a shared worker protocol, and an executor +side-channel carries progress, logs and middleware toggles back. Namespace scoping is completed +across the id-addressed storage APIs, and cross-SDK parity lands the operator admin surface, +predicates, retry policy and the full event taxonomy in Node and Java. ### Changed @@ -35,9 +41,24 @@ underlying Rust crates are released together, in lock-step. `status` filter (`DeliveryStatus`, Python). Same non-breaking contract — a string still works. Internally, Python's workflow tracker and saga orchestrator now compare node/run status against the enums rather than string literals. +- **The Node native binding is built on napi 3.** An internal upgrade from napi 2 with no change + to the published API; the minimum supported Node version stays 20. ### Added +- **`taskito-server`: a standalone scheduler, attach listener and dashboard.** One Rust binary + runs the scheduler loop and serves the dashboard for an app written against any SDK, so the fat + application image no longer has to be redeployed as a worker. Workers attach to it over a shared + worker protocol — a JSON header line plus raw payload bytes, one format for both the prefork + pipe and the attach socket — dispatched through a `RemoteDispatcher` in the core. Database + credentials stay in the server rather than the app image. +- **A `taskito executor` subcommand in every SDK.** Python, Node and Java each ship an executor + that attaches to a running server, advertises the tasks it can run, and executes only those — + the process side of the attach topology above. +- **An executor side-channel for progress, logs and middleware toggles.** An attached executor + reports `update_progress`, task logs, and middleware enable/disable back over the same + connection, so an attached worker is observable and controllable exactly like an in-process one. + Implemented in the core and wired through all three SDKs. - **A Helm chart for `taskito-server`, with annotation-driven sidecar injection.** `deploy/helm/taskito-server` installs the scheduler, dashboard and injector as one release, and refuses to render the combinations the server rejects at boot — an attach listener with no token, @@ -103,16 +124,36 @@ underlying Rust crates are released together, in lock-step. `durationMs` on Node's, and `duration_ms` on Python's job event payloads. Java also gains `NodeSnapshot.durationMs()` / `compensationDurationMs()` and `TaskContext.elapsedMs()`. - **Full lifecycle event taxonomy in the Node and Java SDKs.** Both grow from the 4 job-outcome - events to the 26-event cross-SDK taxonomy: enqueue, per-attempt failure (`job.failed`), worker + events to the 29-event cross-SDK taxonomy: enqueue, per-attempt failure (`job.failed`), worker lifecycle, queue pause/resume, workflow submit/terminal/gate, saga compensation, and predicate rejection. Node types `queue.on()` per event name (`EventMap`); Java adds queue-level `taskito.onEvent(EventName, Consumer)` with typed event records alongside the unchanged worker-scoped `Worker.Builder.on(...)`. Webhooks can subscribe to every event name. - `predicate.cancelled` stays Python-only, and Python now emits the previously dormant - `workflow.submitted`. + `predicate.cancelled` is reserved in all three — subscribable, typed and webhook-matchable — + but only Python emits it: Node and Java gate at enqueue, where a terminal skip is + `predicate.skipped` because no job exists yet. - **Java webhook deliveries use the dotted wire names.** Bodies that previously said `"event": "success"` now say `"event": "job.completed"`; stored subscriptions with the legacy four names keep matching, and job-outcome bodies gain `duration_ms`. +- **Retention dry-run.** `dry_run_retention()` reports what a purge would delete right now — per + table and in total, against the windows the elected cleaner actually published — so a window can + be sized before it deletes anything. The dashboard echoes those live windows rather than + guessing at the defaults. +- **Retry predicates in the Node and Java SDKs.** A per-task `retryOn` classifies a thrown error: + return `false` and the job dead-letters immediately, whatever retry budget is left. One + predicate covers both directions — test for the errors worth retrying, or negate a test for the + permanent ones. Java can signal the same intent by exception type instead, via + `RetryableException` / `NonRetryableException`, so a domain hierarchy doubles as the policy. +- **Push-dispatch in the Node and Java shells.** Both accept the `pushDispatch` option, so an + enqueue wakes the scheduler immediately instead of waiting out the poll interval. +- **Node standalone health and readiness helpers.** Liveness and readiness checks that run without + the dashboard, for a worker deployment that serves no HTTP of its own. +- **Python periodic-schedule catalog management.** `list_periodic()`, `delete_periodic(name)`, + `pause_periodic(name)` and `resume_periodic(name)` operate the cron catalog at runtime, so a + schedule can be suspended or dropped without a redeploy. +- **Java classifier jars and GraalVM metadata.** Per-platform classifier artifacts publish + alongside the fat jar, and bundled reachability metadata makes the SDK usable from a + GraalVM native image. ### Fixed @@ -139,6 +180,32 @@ underlying Rust crates are released together, in lock-step. default `atexit` teardown. Taskito now initializes OpenSSL itself with that teardown suppressed (`OPENSSL_INIT_NO_ATEXIT`) before the first Postgres connection. +- **Namespace scoping is complete across the id-addressed storage APIs.** Every method that + addresses a row by id now takes the namespace it must match, so one tenant can no longer read, + mutate or complete another's job by guessing its id. Cross-namespace dependencies are rejected + at enqueue, and workflow runs are scoped through the store handle. +- **Dashboard settings are compare-and-set in every SDK.** Two dashboards editing the same JSON + settings document no longer silently overwrite each other; a stale write is rejected instead of + clobbering the newer one. +- **The predicate event taxonomy is complete across SDKs.** The gate outcome names each SDK + recognized diverged, so a webhook subscribed to one could not be registered against another. + All three now carry the same set. Separately, `workflow.submitted` fires on run submission + rather than staying dormant. +- **Java null-safety contract and native platform coverage.** Nullability is declared across the + public API, and the published artifact carries every supported platform's native library rather + than only the build host's. `JobRec` reads are guarded by the backend monitor, and the build no + longer silently disables every compile task. +- **The Redis task-metric retention purge is bounded.** It read its whole below-cutoff window in + one `ZRANGEBYSCORE` and issued a `GET` per id, so the first sweep after a long retention gap + stalled the maintenance tick. It now drains in batches like every other purge, with one `MGET` + per batch. +- **Batch enqueue applies `unique_key` dedup.** A batch carrying any keyed entry now routes + through the unique path rather than the plain multi-row insert, which would hit the partial + unique index and fail the whole batch the moment a key duplicated an active job. Shared by the + Node and Java batch enqueues; a keyless batch keeps the chunked throughput path, and Python + still batch-inserts without dedup. +- Version drift in the Node CLI banner and the Java installation docs. + ## 0.21.0 Overload-controls and retention release. The queue gains admission and load-shedding controls, diff --git a/docs/content/docs/shared/guides/operations/testing.mdx b/docs/content/docs/shared/guides/operations/testing.mdx index 0956d1a3f..a63c96d04 100644 --- a/docs/content/docs/shared/guides/operations/testing.mdx +++ b/docs/content/docs/shared/guides/operations/testing.mdx @@ -93,7 +93,7 @@ it("runs a task end to end", async () => { ```kotlin -testImplementation("org.byteveda:taskito-test:0.21.0") +testImplementation("org.byteveda:taskito-test:0.22.0") ``` ```java diff --git a/sdks/java/README.md b/sdks/java/README.md index 7a8050978..ea8f78eb5 100644 --- a/sdks/java/README.md +++ b/sdks/java/README.md @@ -20,9 +20,9 @@ your platform (`linux-x86_64`, `linux-aarch64`, `osx-x86_64`, `osx-aarch64`, ```kotlin // Gradle -implementation("org.byteveda:taskito:0.21.0") -runtimeOnly("org.byteveda:taskito:0.21.0:linux-x86_64") // native library for your platform -annotationProcessor("org.byteveda:taskito-processor:0.21.0") // compile-time TaskHandler bindings +implementation("org.byteveda:taskito:0.22.0") +runtimeOnly("org.byteveda:taskito:0.22.0:linux-x86_64") // native library for your platform +annotationProcessor("org.byteveda:taskito-processor:0.22.0") // compile-time TaskHandler bindings ``` To pick the classifier automatically, use the @@ -32,7 +32,7 @@ To pick the classifier automatically, use the plugins { id("com.google.osdetector") version "1.7.3" } dependencies { - runtimeOnly("org.byteveda:taskito:0.21.0:${osdetector.classifier}") + runtimeOnly("org.byteveda:taskito:0.22.0:${osdetector.classifier}") } ``` @@ -51,12 +51,12 @@ dependencies { org.byteveda taskito - 0.21.0 + 0.22.0 org.byteveda taskito - 0.21.0 + 0.22.0 ${os.detected.classifier} runtime @@ -81,7 +81,7 @@ than loading a binary built for a different one. org.byteveda taskito-processor - 0.21.0 + 0.22.0 diff --git a/sdks/java/gradle.properties b/sdks/java/gradle.properties index b78955642..2b5bbd335 100644 --- a/sdks/java/gradle.properties +++ b/sdks/java/gradle.properties @@ -2,7 +2,7 @@ # project and each subproject. `version` mirrors the root Cargo.toml and is # rewritten by scripts/version.mjs; do not hand-edit it. group=org.byteveda -version=0.21.0 +version=0.22.0 # Null-safety toolchain, shared by every published Java module so the three # build scripts can't drift apart on versions. Error Prone is capped at 2.42.0: diff --git a/sdks/node/package.json b/sdks/node/package.json index b5f007198..f163b801d 100644 --- a/sdks/node/package.json +++ b/sdks/node/package.json @@ -1,6 +1,6 @@ { "name": "@byteveda/taskito", - "version": "0.21.0", + "version": "0.22.0", "description": "Rust-powered task queue for Node.js \u2014 no broker required.", "license": "MIT", "type": "module", diff --git a/sdks/python/taskito/__init__.py b/sdks/python/taskito/__init__.py index 2d1e8efb3..87643ecf0 100644 --- a/sdks/python/taskito/__init__.py +++ b/sdks/python/taskito/__init__.py @@ -158,4 +158,4 @@ except PackageNotFoundError: # Running from a source tree with no installed distribution. Kept in sync # with the root Cargo.toml by scripts/version.mjs — do not hand-edit. - __version__ = "0.21.0" + __version__ = "0.22.0" From 672aecb225780e75f8659dc66a92c5436cd168ae Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:42:19 +0530 Subject: [PATCH 2/4] test: stop the periodic worker when its test ends A once-a-second schedule left running kept firing for the rest of the session, and its lifecycle logs landed in every later test's caplog. --- sdks/python/tests/core/test_periodic.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/sdks/python/tests/core/test_periodic.py b/sdks/python/tests/core/test_periodic.py index 09960c932..55d480514 100644 --- a/sdks/python/tests/core/test_periodic.py +++ b/sdks/python/tests/core/test_periodic.py @@ -91,14 +91,19 @@ def frequent_task() -> str: worker_thread = threading.Thread(target=queue.run_worker, daemon=True) worker_thread.start() + try: + poll_until( + lambda: queue.stats()["completed"] >= 1, + timeout=30, + message="periodic task never triggered", + ) - poll_until( - lambda: queue.stats()["completed"] >= 1, - timeout=30, - message="periodic task never triggered", - ) - - assert queue.stats()["completed"] >= 1 + assert queue.stats()["completed"] >= 1 + finally: + # A once-a-second schedule left running keeps firing for the rest of the + # session, and its lifecycle logs land in every later test's `caplog`. + queue.shutdown() + worker_thread.join(timeout=5) def test_list_periodic_reports_every_field(registered: Queue) -> None: From d6f1bd1c11a8d486b1cf19914221b1460024936e Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:42:39 +0530 Subject: [PATCH 3/4] test: scope lifecycle-log assertions to their own task caplog captures the whole process, so an unscoped record count depends on test order and on whatever worker another test left running. --- .../observability/test_task_lifecycle_logs.py | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/sdks/python/tests/observability/test_task_lifecycle_logs.py b/sdks/python/tests/observability/test_task_lifecycle_logs.py index 04e52aea5..27e23e51f 100644 --- a/sdks/python/tests/observability/test_task_lifecycle_logs.py +++ b/sdks/python/tests/observability/test_task_lifecycle_logs.py @@ -21,6 +21,27 @@ def _taskito_records(caplog: pytest.LogCaptureFixture) -> list[logging.LogRecord return [r for r in caplog.records if r.name == "taskito"] +def _matching( + caplog: pytest.LogCaptureFixture, + pattern: re.Pattern[str], + task_name: str, + level: int, +) -> list[str]: + """Lifecycle messages this test's own task produced, at `level`. + + Scoped by task name because ``caplog`` captures the whole process: any worker + another test left running logs onto the same ``taskito`` logger, and an + unscoped count then depends on test order and timing. + """ + return [ + message + for record in _taskito_records(caplog) + if record.levelno == level + for message in [record.getMessage()] + if (match := pattern.match(message)) is not None and match.group(1) == task_name + ] + + def test_safe_result_repr_truncates_long_values() -> None: long = "x" * (_MAX_RESULT_REPR * 2) rendered = _safe_result_repr(long) @@ -66,15 +87,13 @@ def add(a: int, b: int) -> int: caplog.set_level(logging.INFO, logger="taskito") assert _invoke_registered(queue, add.name, 2, 3) == 5 - messages = [r.getMessage() for r in _taskito_records(caplog) if r.levelno == logging.INFO] - received = [m for m in messages if _RECEIVED.match(m)] - succeeded = [m for m in messages if _SUCCEEDED.match(m)] + received = _matching(caplog, _RECEIVED, add.name, logging.INFO) + succeeded = _matching(caplog, _SUCCEEDED, add.name, logging.INFO) - assert len(received) == 1, messages - assert len(succeeded) == 1, messages + assert len(received) == 1, received + assert len(succeeded) == 1, succeeded m = _SUCCEEDED.match(succeeded[0]) assert m is not None - assert m.group(1) == add.name assert m.group(3) == "5" @@ -89,13 +108,9 @@ def boom() -> None: with pytest.raises(ValueError): _invoke_registered(queue, boom.name) - raised = [ - r - for r in _taskito_records(caplog) - if r.levelno == logging.ERROR and _RAISED.match(r.getMessage()) - ] - assert len(raised) == 1 - m = _RAISED.match(raised[0].getMessage()) + raised = _matching(caplog, _RAISED, boom.name, logging.ERROR) + assert len(raised) == 1, raised + m = _RAISED.match(raised[0]) assert m is not None assert m.group(3) == "ValueError" assert "bad input" in m.group(4) @@ -112,9 +127,14 @@ def stoppable() -> None: with pytest.raises(TaskCancelledError): _invoke_registered(queue, stoppable.name) - cancel_records = [r for r in _taskito_records(caplog) if _CANCELLED.match(r.getMessage())] - assert len(cancel_records) == 1 - assert cancel_records[0].levelno == logging.INFO + cancelled = _matching(caplog, _CANCELLED, stoppable.name, logging.INFO) + assert len(cancelled) == 1, cancelled - error_records = [r for r in _taskito_records(caplog) if r.levelno == logging.ERROR] - assert error_records == [] + # A cancel must not also log an error for this task — scoped for the same + # reason as above, so a stray worker's failure elsewhere cannot fail it. + errors = [ + record.getMessage() + for record in _taskito_records(caplog) + if record.levelno == logging.ERROR and stoppable.name in record.getMessage() + ] + assert errors == [] From 5929ce564c5f4ac89409a6162e94caf804216b13 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:50:46 +0530 Subject: [PATCH 4/4] test: match the cancel assertion on the raised capture A substring check also fires on an unrelated task whose name contains this one's. --- .../tests/observability/test_task_lifecycle_logs.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sdks/python/tests/observability/test_task_lifecycle_logs.py b/sdks/python/tests/observability/test_task_lifecycle_logs.py index 27e23e51f..05fb7507c 100644 --- a/sdks/python/tests/observability/test_task_lifecycle_logs.py +++ b/sdks/python/tests/observability/test_task_lifecycle_logs.py @@ -130,11 +130,8 @@ def stoppable() -> None: cancelled = _matching(caplog, _CANCELLED, stoppable.name, logging.INFO) assert len(cancelled) == 1, cancelled - # A cancel must not also log an error for this task — scoped for the same - # reason as above, so a stray worker's failure elsewhere cannot fail it. - errors = [ - record.getMessage() - for record in _taskito_records(caplog) - if record.levelno == logging.ERROR and stoppable.name in record.getMessage() - ] - assert errors == [] + # A cancel must not also log this task as raised. Matched on the `_RAISED` + # task-name capture rather than a substring, which would also fire on an + # unrelated task whose name merely contains this one's. + raised = _matching(caplog, _RAISED, stoppable.name, logging.ERROR) + assert raised == []