From 67721a6a98de873d9e5d7fd5e6a59dbbfa6c86e3 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:49:17 +0530 Subject: [PATCH 1/2] docs(changelog): add canonical CHANGELOG.md rendered on docs site --- CHANGELOG.md | 785 ++++++++++++++++++++++ docs/content/docs/resources/changelog.mdx | 28 +- docs/package.json | 5 +- scripts/sync-changelog.mjs | 30 + 4 files changed, 843 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scripts/sync-changelog.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..58218e3e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,785 @@ +# Changelog + +All notable changes to taskito are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). All SDKs (Python, Node) and the +underlying Rust crates are released together, in lock-step. + +## 0.16.4 + +Maintenance release. Upgrades the Rust↔Python binding layer and refreshes project docs; no +public API changes. + +### Changed + +- **PyO3 0.29.** Upgraded the PyO3 binding layer from 0.22 to 0.29 (and `pyo3-log` to 0.13). + Internal only — no Python API change. Migrates to the `Bound` / `IntoPyObject` APIs and + declares `gil_used`, laying the groundwork for Python 3.14 and free-threaded builds. +- **Node SDK default SQLite settings.** Opening a SQLite-backed queue now applies sensible + defaults out of the box (#292). +- **Documentation.** Restructured the READMEs into a project-level root README plus + self-contained per-SDK pages, and introduced this root `CHANGELOG.md` — now also rendered on + the documentation site. + +## 0.16.3 + +Reliability patch — correctness fixes across the scheduler and storage layer, plus the missing Django admin templates. No API changes. + +### Fixed + +- **Django admin templates.** The `taskito.contrib.django` admin views shipped + without their templates, so every admin page raised `TemplateDoesNotExist`. The + templates are now authored and packaged, and the integration guide documents the + required `taskito.contrib.django` entry in `INSTALLED_APPS`. +- **At-most-once retries.** An explicit per-call `max_retries=0` is no longer + overridden by the task-policy default, so a no-retry job is no longer + re-executed up to the policy limit. +- **Unique enqueue.** `enqueue_unique` now validates dependencies inside the + transaction — a missing dependency no longer runs the job and a dead/cancelled + dependency no longer strands it `Pending` — and never returns a phantom + (rolled-back) job under unique-key contention. +- **Dead-letter policy.** DLQ auto-retry is scoped to the worker's namespace and + served queues (no longer resurrecting other workers' entries); per-entry and + per-job result TTLs are purged even without a global `result_ttl`; and job + metadata survives the dead-letter round trip. +- **SQLite transaction safety.** Read-then-write operations (dequeue, enqueue, + rate-limit) use immediate write-priority transactions, avoiding `SQLITE_BUSY` + deadlocks; migration `ALTER TABLE` failures now propagate instead of being + downgraded to warnings. +- **Postgres rate limiting.** The rate-limit row is seeded and locked + `FOR UPDATE`, preventing concurrent workers from over-admitting beyond the + configured limit. + +## 0.16.2 + +Reliability patch — correctness fixes across the scheduler, dashboard, and Redis backend. No API changes. + +### Fixed + +- **Scheduler retry budget.** Soft-gate reschedules (rate limit, circuit breaker, + concurrency cap, channel backpressure) no longer consume a job's retry budget; a + job that never executed is rescheduled without incrementing `retry_count`, so it + is no longer dead-lettered early on its first real failure. +- **Webhook secrets.** Webhook signing secrets and delivery logs (stored under + `webhooks:` keys) are no longer readable through `GET /api/settings`. +- **Structured notes.** The dashboard job detail page no longer crashes on jobs + with notes (the API now emits notes as a canonical JSON string), and non-finite + floats (`NaN`/`Infinity`) are rejected at enqueue instead of producing invalid + JSON that broke the jobs API. +- **Redis write-path atomicity.** Closed a cluster of races in the Redis backend: + the dequeue claim is now an atomic compare-and-set (a cancelled/expired job can + no longer be resurrected and executed); retry/reschedule requeue atomically (no + stranded jobs); `complete` and purge release the unique-key pointer with a + compare-and-delete (no clobbering a reused key); progress/cancel writes are + guarded against resurrecting an archived job; and the dead-letter move commits + the DLQ entry and the archive together. + +## 0.16.1 + +Patch release with no user-facing changes. + +## 0.16.0 + +Feature release: mesh scheduling, DLQ policies, and a full dashboard redesign. + +### Added + +- **Mesh scheduling.** Decentralized worker overlay network for task affinity + and work-stealing. SWIM gossip protocol for peer discovery, consistent-hash + ring for affinity routing, TCP work-stealing for load balancing. New + `taskito-mesh` crate (feature-gated via `mesh`). Python API: + `MeshWorker` class passed to `queue.run_worker(mesh=...)`. The database + remains source of truth — mesh is a dispatch optimization only. +- **DLQ policies.** Dead-letter queue entries now support auto-retry, per-entry + TTL, and explicit delete. New `Queue` params: `dlq_auto_retry_delay` and + `dlq_auto_retry_max`. New methods: `delete_dead()` / `adelete_dead()`. + Storage trait gains `delete_dead`, `purge_dead_with_ttl`, and + `list_dead_for_retry` across all three backends. Dashboard adds a Discard + button and retry-count badge on the Dead Letters page. +- **Dashboard "Friendly ops" redesign.** Full re-skin with warm-paper neutrals, + emerald accent, and IBM Plex typography. New shared primitives: `Switch`, + `QueueBar`, `MeterBar`, `Segmented`, `Stepper`, `KvList`, `Callout`, + `LiveDot`, `StatusBadge`. Overview page gains a health pulse banner, workers + card, and busiest-queues table. All pages restyled. Light/dark toggle + preserved; custom accent override via `useApplyAccent`. +- **Dashboard workflows module.** Workflows page with routes and nav entry; + command palette entries for Tasks, Webhooks, and Settings. +- **Dashboard change-password dialog** in the user menu. + +### Fixed + +- **OAuth callback errors redirect to login** instead of showing a raw error + page. Missing `joserfc` dependency added to the `oauth` extras group. +- **Dashboard timestamp contract enforced.** Webhook store, overrides store, + and auth endpoints now emit Unix milliseconds (matching the documented API + contract) instead of seconds — fixes "56 years ago" display bugs. +- **Dashboard sidebar health derived from stats query**, no longer hardcoded + green. +- **Dashboard sidebar polls at configured interval** on all pages, preventing + stale "Updated Xs ago" timers. +- **Fixed sidebar scrolls independently** of page content. +- **Dead code removed from dashboard** (`QueueBreakdown`, `formatBytes`, unused + webhook queries). +- **Consistent vertical spacing** across all dashboard pages. +- **Workers card shows overflow count** ("+N more" link when >6 workers). +- **Accessibility improvements.** `prefers-reduced-motion` honored for + animations; `aria-live` on Stepper value changes; `aria-pressed` on + live-tail toggle; accessible name on tasks search input. + +### Changed + +- **Dashboard dependencies updated** (22 packages in the dashboard-deps group). + +### Notes + +- `jobs.payload` and `jobs.result` columns (dual-written since 0.15.0 for + rollback safety) are **not** dropped in this release. They remain for one + more cycle to give any lagging workers time to upgrade. Expect removal in + 0.17. + +--- + +## 0.15.2 + +Security-hardening release. No breaking changes. + +### Added + +- **`SignedSerializer`.** Opt-in HMAC-SHA256 integrity wrapper for task payloads + and results — a worker refuses to deserialize bytes not produced with the + shared key, closing the untrusted-storage code-execution path. Compose with + `EncryptedSerializer` for confidentiality + integrity. +- **`Queue.max_payload_bytes`.** Per-queue cap (default 1 MiB) that rejects + oversized serialized payloads at enqueue time. +- **Security guide.** New *Operations → Security* page documenting the trust + model and a production hardening checklist. + +### Changed + +- **Dashboard authorization is enforced server-side.** Mutating API routes now + require the `admin` role; `viewer` sessions are read-only. Session and CSRF + cookies carry `Secure`, and `/metrics` / `/readiness` can be gated behind + `TASKITO_DASHBOARD_METRICS_TOKEN`. +- **Scaler binds to `127.0.0.1` by default.** Use `--host 0.0.0.0` only behind a + trusted network boundary. + +### Fixed + +- **Settings API no longer leaks auth data.** `/api/settings` hides and rejects + internal `auth:` keys (password hashes, sessions, CSRF secret). +- **Webhook SSRF closed.** Target URLs are re-validated against + private/loopback/metadata addresses at delivery time (defeating DNS + rebinding) and redirects are no longer followed. +- **Proxy hardening.** File-proxy allowlist prefix/symlink bypass closed; + argument reconstruction restricted to safe type kinds; a warning is emitted + when proxy recipes are reconstructed without a signing key. +- **Lock owner token no longer disclosed.** `DistributedLock.info()` masks + another holder's `owner_id`; Redis locks set a native TTL with an atomic reap. +- **No duplicate execution on claim errors.** The scheduler skips dispatch when + a claim attempt errors instead of running the job unguarded. +- **Job-list API summarizes tracebacks.** Only the final exception line is shown + in the jobs list; the full traceback stays on the per-job errors endpoint. + +## 0.15.1 + +### Fixed + +- **Mobile navigation drawer closes on navigation.** The drawer previously + stayed open after tapping a link; it now closes on every navigation, + including re-selecting the current route. +- **Mobile menu shows the full navigation.** The Tasks and Webhooks links were + missing from the mobile menu. The sidebar and mobile menu now share a single + navigation definition so the two can no longer drift. +- **Job logs tab virtualized.** The job detail logs view renders only the + visible rows, keeping jobs with hundreds of log lines smooth to scroll. +- **Resources table loading state matches its layout.** The placeholder now + uses the table-shaped skeleton instead of a single opaque block. + +--- + +## 0.15.0 + +See the [Upgrading to 0.15](https://docs.byteveda.org/taskito/guides/operations/upgrading-0.15) guide for +migration steps, rolling-upgrade notes, and the downgrade floor. + +### Breaking + +- **Serializer default changed to `SmartSerializer`.** Payloads written by + 0.15 carry a 1-byte codec tag and cannot be read by pre-0.15 workers. Upgrade + all workers together; if rolling deployment is required, pin + `Queue(serializer=CloudpickleSerializer())` until every worker is on 0.15. + Reading old (untagged cloudpickle) payloads is fully backward compatible. +- **Terminal jobs archived immediately.** Completed, failed, dead-lettered, + and cancelled jobs move to `archived_jobs` on completion. Pre-0.15 binaries + only look in `jobs` and will not see archived rows — treat 0.15 as a + minimum-version floor for any upgraded database. + +### Added + +- **`SmartSerializer` (new default).** msgpack for plain-data payloads + (faster, smaller); cloudpickle fallback for lambdas, closures, and custom + classes. `msgpack` is now a base dependency. Tuples are preserved; namedtuples + fall back to cloudpickle. +- **`Queue(scheduler_batch_size=N)`.** New optional kwarg; defaults to 1 + (unchanged behaviour). Raise it to claim up to N jobs per scheduler + round-trip for higher throughput under sustained load. +- **Opt-in push-based dispatch (`push_dispatch=True`).** Event-driven wakeups + instead of polling — removes the dispatch latency floor and idle DB load. + Off by default; requires building the extension with `--features push-dispatch` + (not included in the default wheel). SQLite and Redis are fully event-driven; + Postgres falls back to a faster poll pending a native LISTEN listener. + +### Changed + +- **Per-task / per-queue stats computed server-side.** Replaces O(N) full + scans with `SINTERCARD`. No API change, no data migration. +- **Job payloads moved to `job_payloads` side table.** The dequeue path no + longer loads payload BLOBs for candidates it does not claim. Transparent — + no API change. `jobs.payload` / `jobs.result` are dual-written in 0.15 for + rollback safety and **will be dropped in 0.16**. Redis is unaffected. + +--- + +## 0.14.2 + +### Fixed + +- **`wait()` no longer returns transient `FAILED` during saga startup.** `WorkflowRun.wait()`'s poll safety-net could observe the run in `Failed` state before the saga orchestrator transitioned it to `Compensating`. When a tracker event is registered but has not yet fired, `wait()` now defers to the event instead of returning the possibly-transient terminal state. The event is set only after the saga decision is fully resolved, so `wait()` can never prematurely return `Failed` for a run that is about to compensate. +- **`_mark_run_compensating` retries on transient SQLite lock.** The `Failed` → `Compensating` storage write could silently fail under SQLite file contention, leaving the run stuck in `Failed`. The method now retries up to 5 times with backoff, matching the existing retry pattern in compensation job dispatch. + +--- + +## 0.14.1 + +### Fixed + +- **Saga compensation race on Windows SQLite.** `start_compensation` transitioned the workflow run from `Failed` to `Compensating` only after a storage I/O call (`get_base_run_node_data`) that could be slow under SQLite file contention — especially on Windows. During that gap, `WorkflowRun.wait()`'s poll loop could observe the transient `Failed` state (which is terminal) and return early, before the saga had a chance to run. The `Compensating` transition now happens immediately after the cheap in-memory eligibility check, closing the race window. A vacuous-compensation edge case (eligible but no nodes to undo) is handled by finalising as `Compensated` immediately and resolving any pending parent saga node inline. + +--- + +## 0.14.0 + +### Added + +- **Bare-metal autoscaler.** `taskito.autoscale` provides an in-process HPA-style control loop that spawns and drains `taskito worker` subprocesses based on queue depth and utilisation. Mirrors the Kubernetes HPA dual-signal formula (`depth_desired` + `util_desired`). Configurable stabilisation windows (default: scale-up immediate, scale-down 5 minutes), tolerance band (10%), overload override, and crash recovery. Use `serve_autoscaler(queue, AutoscaleConfig(...))` as the entry point. See the [Bare-Metal Autoscaler guide](https://docs.byteveda.org/taskito/guides/operations/autoscaler). +- **Dashboard `/api/workflows` backend.** Four new REST endpoints expose workflow run data: `GET /api/workflows/runs` (paginated list, filterable by definition name and state), `GET /api/workflows/runs/{id}` (run header + per-node detail with compensation fields), `GET /api/workflows/runs/{id}/dag` (DAG JSON), and `GET /api/workflows/runs/{id}/children` (sub-workflow runs). All timestamps are Unix milliseconds. +- **Canvas saga compensation.** `chain`, `group`, and `chord` support `.with_compensation([...])`. On failure, compensators for completed steps are enqueued with a deterministic `canvas_compensation:{run_id}:{slot}` idempotency key. Dispatch order: chain → reverse-sequential, group → parallel, chord → callback compensator first then group members. `Signature.with_compensation(compensator)` lets individual signatures carry their own compensator. +- **Per-item batch results.** `@queue.task(batch={"per_item_results": True})` enables per-item tracking for batched tasks. The task returns `list[BatchItemResult]`; each caller's `BatchedJobResult.result()` returns **only that caller's value**. `BatchItemResult.success(item_index, result)` and `BatchItemResult.failure(item_index, error)` are the constructors. `partial_failures()` returns failed items after a successful batch. `BatchPartialFailureError` and `BatchResultTypeError` are the new exception types. Cannot be combined with `idempotent=True`. +- **Saga `compensate_on_continue`.** `Workflow(on_failure="continue", compensate_on_continue=True)` triggers compensation after a continue-mode run terminates with failures. The run transitions through `CompletedWithFailures` before entering `Compensating`, then ends in `Compensated` or `CompensationFailed`. Without the flag, continue-mode runs end in `CompletedWithFailures` with no compensation. +- **`COMPLETED_WITH_FAILURES` workflow state.** New `WorkflowState` variant for `on_failure="continue"` runs that finished with at least one step failure. Emits `WORKFLOW_COMPLETED_WITH_FAILURES` event. Accessible as `"completed_with_failures"` in the REST API `state` field. + +## 0.13.0 + +### Added + +- **Workflow PostgreSQL backend.** `WorkflowPostgresStorage` mirrors the SQLite implementation through the shared `impl_workflow_diesel_ops!` macro. Workflows now run on Postgres-backed queues — the previous `PyRuntimeError` on non-SQLite backends is gone. +- **Workflow Redis backend.** `WorkflowRedisStorage` (in `crates/taskito-workflows/src/redis_store.rs`) stores definitions, runs, and nodes as hashes under a `wf:` prefix, with sorted-set indexes for state/definition lookup and a Lua `FINALIZE_FAN_OUT` script for atomic fan-in CAS. +- **Multi-backend workflow contract suite** (`crates/taskito-workflows/tests/storage_contract.rs`). 27 cases run against every workflow backend. CI now verifies parity on every PR for SQLite, PostgreSQL, and Redis. +- **`@queue.task(batch=...)`.** Producer-side task batching collects per-call items in memory and dispatches them as a single job whose payload is the list. Tunable `max_size` / `max_wait_ms`. See `/docs/guides/core/batching`. +- **Saga compensation.** `@queue.task(compensates=...)` and `Workflow.step(..., compensates=...)` declare a compensator. On forward failure (in `fail_fast` mode) taskito runs the registered compensators in reverse topological order, with idempotency guaranteed via `compensation:{run_id}:{node_name}` dedup keys. New workflow states: `Compensating`, `Compensated`, `CompensationFailed`. See `/docs/guides/workflows/sagas`. +- **Compensator contract fully wired.** Compensators now receive populated `(forward_args, forward_kwargs, forward_result)` for both deferred and static workflow nodes. `current_compensation_context()` returns a populated `CompensationContext` inside compensator bodies (workflow run id, node name, forward job id, forward result). +- **Sub-workflow saga propagation.** When a parent workflow's saga reaches a sub-workflow node whose child run has its own compensators, compensation propagates into the child. Child saga runs to completion, then the parent node is marked accordingly and the parent's wave advances. Depth-capped at 10 levels to prevent runaway recursion. +- **Saga examples + API reference.** `/docs/more/examples/saga-checkout`, `/docs/more/examples/batch-emails`, `/docs/api-reference/saga`, `/docs/api-reference/batching`. + +### Fixed + +- **Atomic fan-out node creation.** `expand_fan_out` now uses the transactional `create_workflow_nodes_batch` to insert all child workflow nodes in one transaction. A crash partway through fan-out expansion no longer leaves half-tracked children. +- **Diesel `?`-placeholder rewriting for Postgres.** `sql_query` does not auto-rewrite `?` → `$N` for PostgreSQL; the workflow crate's macro now does it explicitly via the `pg_rewrite` helper. + +## 0.12.3 + +### Added + +- **Dashboard SSO (OAuth & OIDC).** Native sign-in for Google, GitHub, and any OIDC-compliant provider (Okta, Auth0, Keycloak, Microsoft Entra) sits alongside the existing password login. Multiple named OIDC providers run side-by-side, each rendered as its own button on the login screen. Configuration is env-var driven (`TASKITO_DASHBOARD_OAUTH_*`); the `oauth` extra (`pip install 'taskito[oauth]'`) pulls in `authlib`, `joserfc`, and `requests`. Security: PKCE S256, single-use server-side `state` (5-min TTL), nonce verification, JWKS-validated ID tokens, issuer/audience/expiry checks, open-redirect protection on the post-login `next` URL, HTTPS-only redirect URIs outside `localhost`. Allowlists by Google Workspace domain, GitHub org, or OIDC email domain. Promote OAuth users to `admin` via an explicit `TASKITO_DASHBOARD_OAUTH_ADMIN_EMAILS` list, with a first-user-wins fallback for empty deployments. Password login can be disabled entirely with `TASKITO_DASHBOARD_PASSWORD_AUTH_ENABLED=false`. +- **Dashboard SSO operator guide.** New `Dashboard › SSO (OAuth & OIDC)` doc walks through registering OAuth clients with Google, GitHub, and generic OIDC providers, plus the full env-var reference, allowlist semantics, security model, and troubleshooting cookbook. Includes a Mermaid sequence diagram of the end-to-end flow. + +### Changed + +- **Docs nav: dedicated `Dashboard` section.** The dashboard documentation outgrew `Observability` — five pages (overview, authentication, SSO, task overrides, REST API) versus three actual observability topics. Moved them into their own top-level Guides section and stripped redundant prefixes from page titles (`Dashboard Authentication` → `Authentication`, etc.). All cross-section links updated. + +### Fixed + +- **`WebhookManager` delivery thread leak.** `reload()` unconditionally spawned a daemon thread on every `Queue` construction. With ~800 tests each creating a fresh `Queue`, macOS CI runners blew through the per-process thread limit and panicked in `r2d2`'s reaper / tokio's worker-thread spawn (`Resource temporarily unavailable`). The thread now starts only when at least one subscription exists, matching the pre-0.12.2 behaviour. +- **`EncryptedSerializer.loads` exception handling.** A blanket `except Exception` re-wrapped *every* failure as `ValueError`, including programmer errors like `MemoryError` that should propagate untouched. The catch is now narrowed to `cryptography.exceptions.InvalidTag` (the one expected failure mode); the original exception is preserved on `__cause__` for debugging. The `InvalidTag` class is also pre-cached on `__init__` so `loads` avoids a per-call import. This also fixed two latent test failures (`test_wrong_key_fails`, `test_tampered_ciphertext_fails`) that only surfaced once a release pulled in `cryptography` via the OAuth extra. + +### Internal + +- **`HttpClient` Protocol for OAuth providers.** `GoogleProvider` / `GitHubProvider` / `GenericOIDCProvider` previously typed their `http` parameter as `requests.Session`, forcing every test to use `# type: ignore[arg-type]` to inject a stub. The Protocol captures the small subset of `Session` actually used (one `get` method) so production code passes a `requests.Session` and tests pass an in-memory stub — no nominal-type fight, no runtime casts. +- **CI installs the `oauth` extra.** `uv sync --extra dev` was leaving `authlib` / `joserfc` / `requests` uninstalled, so the OAuth test modules failed collection with `ModuleNotFoundError` once they shipped. Both lint and test jobs now sync `--extra dev --extra oauth`. `requests` is also pinned explicitly in the `oauth` extra (Authlib does not declare it as a hard dep). +- **Cross-version mypy compatibility on JWKS decoding.** `joserfc.jwk.KeySet.import_key_set` was widened to accept dict-shaped JWKS in mypy 2.x; mypy 1.x still requires the `KeySetSerialization` TypedDict. Suppressed with the standard `# type: ignore[arg-type, unused-ignore]` dual pattern so the file lints under both versions. + +### Test counts at release + +- Rust: 95 tests (default), 107 with `--features workflows` +- Python: 896 collected across 74 files (up from 592 / 58 at 0.12.2) +- Dashboard (vitest): 106 tests across 10 files + +--- + +## 0.12.2 + +### Fixed + +- **Inline imports inside the predicate boolean combinators.** `AndPredicate.evaluate`, `OrPredicate.evaluate`, and `NotPredicate.evaluate` were each importing `_resolve_outcome` from `taskito.predicates.evaluate` *inside* the method body. The cycle they were defending against doesn't exist at runtime — `evaluate.py` only references `core` under `TYPE_CHECKING` — so the import was hoisted to module scope, satisfying the project's no-inline-imports rule. + +### Internal + +- **`QueuePredicateMixin` extracted from `app.py`.** Predicate state (six instance dicts and `PredicateMetrics`) plus the three gating methods (`_apply_enqueue_predicate`, `_apply_dispatch_predicate`, `_reenqueue_after_defer`) and the public inspection / registration API (`list_predicates`, `predicate_for`, `register_predicate`) now live on a dedicated mixin under `py_src/taskito/mixins/predicates.py`. Repeated `_emit_event(...)` blocks consolidated into small helpers. `app.py` drops from 901 to 619 LOC. +- **`QueueRuntimeConfigMixin` split from `mixins/decorators.py`.** `register_type`, `set_queue_rate_limit`, and `set_queue_concurrency` are runtime configuration knobs, not decorator surface — they moved to a new mixin alongside the existing `QueueSettingsMixin` (which manages dashboard key/value state). `mixins/decorators.py` drops from 597 to 533 LOC. +- **Migration DDL deduplicated via `diesel_common/migrations.rs`.** The `run_migrations()` methods on `SqliteStorage` and `PostgresStorage` shared ~750 LOC of nearly-identical `CREATE TABLE` / `CREATE INDEX` / `ALTER TABLE` statements. The shared module now exposes `create_tables(&dialect)`, `create_indexes()`, and `alter_statements(&dialect)`; a `Dialect` struct holds per-backend type substitutions (`BLOB`/`BYTEA`, `INTEGER`/`BIGINT`, `REAL`/`DOUBLE PRECISION`, boolean defaults, and the `IF NOT EXISTS` prefix on `ALTER`). Each backend's `run_migrations()` is now a ~10-line driver. `sqlite/mod.rs` drops 502 → 126 LOC; `postgres/mod.rs` drops 508 → 130 LOC. +- **`tests/core/test_predicates.py`** — 30 focused tests covering AST short-circuit semantics, fail-closed evaluation + metric recording, JSON and string-DSL round-trips, recipe behaviour (`after`/`before`/`in_time_window`/`payload_matches`/`env_var_truthy`), the callable adapter, custom predicate registration, and three queue integration tests for enqueue-time cancel/defer. + +### Test counts at release + +- Rust: 95 tests (default), 107 with `--features workflows` +- Python: 592 passed, 9 skipped across 58 files +- Dashboard (vitest): 121 tests across 9 files + +--- + +## 0.12.1 + +### Fixed + +- **PyPI wheels and sdist now bundle the compiled dashboard.** A regression in 0.12.0 shipped wheels with an empty `taskito/static/` directory because the release pipeline ran `maturin build` without first building the dashboard frontend. End users hit `Dashboard assets not bundled` when running `taskito dashboard`. The release pipeline now builds the dashboard once in a dedicated job, distributes it to every wheel/sdist job as a build artifact, and verifies `static/dashboard/index.html` is present before each `maturin build` invocation — preventing the regression class entirely. + +### Internal + +- **`.github/actions/dashboard-build/`** — composite action centralizing pnpm/Node toolchain versions and the dashboard build steps. Consumed by both `publish.yml` (release) and `dashboard.yml` (CI), so the assets shipped in a wheel are produced by the exact same build path that CI tests on every PR. + +--- + +## 0.12.0 + +### Added + +- **Dashboard persistent settings** -- new `/settings` route exposes branding (title + accent), external links (deployment-wide sidebar shortcuts), and integration URLs (Grafana / Sentry / OTel base) backed by four new `Storage` methods (`get_setting`, `set_setting`, `delete_setting`, `list_settings`) on every backend. REST API at `/api/settings` (GET/PUT/DELETE); optimistic TanStack Query mutations with rollback. The dashboard auto-applies the persisted branding and surfaces the configured links on every page load. +- **CLI `--pool prefork`** -- `taskito worker --pool prefork --app myapp:queue` now selects the prefork worker pool from the command line. +- **Dead-letter URL state** -- page number and grouping view persist via TanStack Router `validateSearch`, so reloading or sharing a link preserves the user's place. + +### Changed + +- **Dashboard rewrite** -- replaced the Preact single-file dashboard with a React + Vite + TanStack Router SPA. Same Python entrypoint (`taskito dashboard --app myapp:queue`), richer UX: cmdk command palette (`⌘K`), URL-synced job filters, optimistic cancel/replay mutations, Recharts metrics (lazy-loaded), virtualized live-tail logs, type-to-confirm destructive actions, keyboard-accessible tables. Assets ship as hashed multi-file output at `py_src/taskito/static/dashboard/`; the legacy single-HTML `templates/dashboard.html` is gone. +- **Dashboard dead letters** -- grouping now keys on `(task, exception class)` extracted from the traceback (e.g. `hard_fail::ValueError`) instead of the full error string, so runs that differ only in message text collapse into one actionable group. Group header shows the latest failure timestamp and a "Retry all" button. +- **Dashboard resources** -- `/api/resources` falls back to worker heartbeat snapshots when the dashboard runs in a different process than the worker, so health surfaces correctly across process boundaries. +- **Async-separation boundary enforced** -- `import asyncio` now lives only in `py_src/taskito/async_support/` and `contrib/fastapi.py`. `mixins/decorators.py` switched to `inspect.iscoroutinefunction`. The boundary is machine-checkable: `grep -rn "import asyncio" py_src/taskito/ | grep -v -E "(async_support/|contrib/fastapi\.py)"` returns empty. +- **`run_maybe_async` clear error under a running loop** -- explicit detection of a running event loop with a taskito-specific `RuntimeError` pointing at the async API and `await`, instead of the cryptic `asyncio.run() cannot be called from a running event loop`. +- **Redis status discriminants** -- `archive_old_jobs`, `purge_completed`, `purge_completed_with_ttl`, `reap_stale_jobs`, and `expire_pending_jobs` now cast `JobStatus::Foo as i32` instead of using magic numbers (`[2, 4, 5]`, `"0"`, `"1"`, `"2"`). Reordering or inserting variants in the enum will fail the build instead of silently archiving the wrong buckets. +- **`enqueue_batch` Rust signature widened** -- `priorities`, `max_retries_list`, `timeouts` widened from `Option>` to `Option>>` so callers can omit individual entries (matches the pattern already used by `delay_seconds_list` / `metadata_list` / etc.). Type stub follows; pure Python callers unaffected. + +### Fixed + +- **Scheduler concurrency cap atomicity** -- a TOCTOU race between the cap check and `claim_execution` allowed two schedulers to both pass the cap and over-dispatch. Also fixed an off-by-one (`>=` against a count that already includes the just-dequeued running job). `try_dispatch` was restructured into named helpers (`active_queues`, `check_pre_claim_gates`, `claim_for_dispatch`, `check_post_claim_concurrency`, `rollback_claim_and_retry`); cap check now runs after `claim_execution` with strict `>`. New regression tests cover exact-cap, max-1, and per-queue caps. +- **Scheduler reschedule on full or closed worker channel** -- a naive `try_send` swallowed `TrySendError::Full` / `Closed`, leaving the job in `Running` until the stale-reaper timed it out -- surfacing as a *timeout* in metrics and middleware (wrong outcome for a job that never ran). Replaced with a full match: warn, roll back the claim, and reschedule with a 100 ms backoff. +- **`enqueue_many` middleware contract** -- `on_enqueue` now receives each job's own args and kwargs (was always `args_list[0]`). Mutations to the per-job options dict propagate to the enqueued jobs (was discarded -- middleware ran *after* `enqueue_batch` against a fresh empty dict). Middleware exceptions surface via `logger.exception("middleware on_enqueue() error")` instead of a silent `except: pass`. +- **`result()` / `aresult()` deadline race** -- could raise `TimeoutError` even when the job had already failed/died/cancelled, when the terminal state landed during the final poll-then-deadline-check window. A defensive re-poll inside the deadline branch lets the caller see the real exception class (`TaskFailedError`, `MaxRetriesExceededError`, `TaskCancelledError`). +- **`result_handler.rs` triple-fetch** -- the Failure branch fetched `get_job` up to three times per call (queue context + `!should_retry` DLQ + retry-exhausted DLQ). Now fetches once and reuses the same `Option<&Job>` via a small DLQ closure. +- **`ResourcePool._active_count` underflow** -- the increment moved to *after* the factory call returns successfully. The failure path no longer needs (or has) a decrement, so a wedged factory can't underflow `active` in `stats()`. Failed attempts also stop counting toward `total_acquisitions`. +- **Prefork timeout** -- children that exceed their per-job timeout are killed; previously could hang indefinitely. +- **CI PyO3 finalization SIGABRT** -- eliminated; pip cache warning silenced. +- **Dashboard timestamps** -- every timestamp field (`created_at`, `last_heartbeat`, `logged_at`, etc.) renders as milliseconds consistent with the backend; previously an extra `× 1000` pushed dates into year 58282. The contract is documented at the top of `dashboard/src/lib/api-types.ts` with per-field JSDoc. +- **Dashboard DAG cycle bound** -- BFS layer assignment now uses a `visited` set + max-iterations break, so an accidental cycle in a workflow definition can't loop forever. +- **Dashboard settings storage opacity** -- settings PUT bodies are JSON-encoded server-side so callers see structured values, not stringified JSON. +- **Dashboard dead-letter row keyboard accessibility** -- clickable group rows use `role="button"`, `tabIndex={0}`, and an `onKeyDown` handler that triggers expansion on Enter / Space; `aria-expanded` reflects the open/closed state. Biome `noStaticElementInteractions` and `useKeyWithClickEvents` rules promoted from `off` to `error`. + +### Internal + +- **`app.py` split into `mixins/` package** -- `QueueInspectionMixin`, `QueueOperationsMixin`, `QueueLockMixin`, `QueueWorkflowMixin`, and the decorator/event/resource modules now live under `py_src/taskito/mixins/`. The `Queue` class is now a thin assembly over the mixins. +- **`workflows/tracker.py` split into package** -- `WorkflowTracker` decomposed into `_GateManager`, `_FanOutOrchestrator`, `_SubWorkflowCoordinator`. +- **`redis_backend/jobs.rs` split into submodule** -- separate files for enqueue, query, helpers, maintenance. +- **`py_queue/workflow_ops.rs` split into submodule**. +- **`dashboard.py` split into package** -- handler/router separation. +- **Dashboard health-audit follow-ups** -- extracted `formatAxisTime` shared between metric charts; extracted `job-dag-layout` pure module; centralized log-level color map in `status.ts`; debounced filters via refs (no `eslint-disable`); promoted Biome `useExhaustiveDependencies` from `warn` to `error`; pure helpers (`parseRefreshOption`, `refreshIntervalMs`) extracted from `refresh-interval-provider` for testability; new vitest coverage on `api-client`, `errors`, `settings`, and `refresh-interval-provider` (81 tests at release). +- **Dependency bumps** -- `redis 0.27 → 1.2`, `libsqlite3-sys 0.30 → 0.37`, `thiserror 1 → 2`, `rand 0.8 → 0.10`, `pq-sys`, `cron`, `tailwind-merge`, `@vitejs/plugin-react`, `react`, and Python dep floors to latest stable. +- **CI** -- `dorny/paths-filter v3 → v4`; drop `area/` label prefix and skip jobs by path; floating major tags for action references; per-PR Postgres/Redis service containers run the storage contract suite on every change (PR #73, landed in 0.11.1; now exercised across every release). + +### Test counts at release + +- Rust: 89 tests (up from 78) +- Python: 496 passed, 9 skipped across 49 files (up from 469 / 46) +- Dashboard (vitest): 81 tests + +--- + +## 0.11.1 + +### Fixed + +- **Workflow fan-in race** -- concurrent child completions on different worker threads could both expand fan-in. The `check_fan_out_completion` Rust call now delegates to a new `WorkflowStorage::finalize_fan_out_parent` compare-and-swap, so the parent transitions at most once regardless of how many children complete simultaneously. +- **Sub-workflow compile failure** -- if a child workflow's factory or compile step raised, the parent node was left permanently `SKIPPED`, hanging the outer run. The parent is now promoted to `RUNNING` only after the child's compile + submit succeed, and is marked `FAILED` on error so the run finalizes. +- **Redis `purge_execution_claims`** -- previously a silent no-op. Execution claims are now mirrored into a time-indexed sorted set (`taskito:exec_claims:by_time`) so the scheduler's maintenance loop can reap stale claims in O(log n). Legacy keys still expire via the 24 h `PX` TTL. +- **SQLite `move_to_dlq` cascade** -- cascade-cancel errors on the dependent sweep are now propagated (parity with Postgres and Redis) instead of being swallowed as a warning. Callers see the failure and can decide whether to retry or alert. + +### Performance + +- **Workflow ops release the GIL during SQLite I/O** -- every method in `workflow_ops.rs` now wraps DB round-trips in `py.allow_threads(...)`. Event-bus callbacks that fire from worker threads no longer serialize the rest of the Python runtime on each fan-in / mark-result / cancel call. +- **`WorkflowSqliteStorage` cached per queue** -- migrations run once on first workflow API call via `OnceLock`, instead of re-running `CREATE TABLE IF NOT EXISTS` on every single call. + +### Safety + +- **`cancel_workflow_run` iterative** -- replaced recursive sub-workflow cascade with an iterative BFS plus `visited` set. No recursion deadlock, no connection-pool exhaustion on deep sub-workflow trees, and any accidental cycle in `parent_run_id` terminates safely. +- **Tracker state lock** -- `WorkflowTracker._state_lock` (RLock) now guards every access to `_run_configs`, `_job_to_run`, `_child_to_parent`, and `_gate_timers`, which are touched from worker threads, gate-timeout timers, and user threads. +- **Gate timer cleanup** -- `_cleanup_run` cancels any pending gate timers for the finishing run and drops stale child→parent mappings. Timers no longer fire on already-terminal runs. +- **Workflow metadata JSON escaping** -- `build_metadata_json` uses `serde_json::json!`; node names containing backslashes, control characters, or Unicode are now escaped correctly. Previously they produced malformed JSON that silently dropped the workflow event. +- **Narrower exception handling in the tracker** -- broad `except Exception:` clauses narrowed to `(RuntimeError, ValueError)` on Rust FFI call sites; the remaining broad catches are restricted to user callables and event emission with an explanatory `# noqa`. Silent `let _ = storage.cancel_job(...)` replaced with `log::warn!` via a shared helper. + +### Added + +- **`PrometheusMiddleware(task_filter=...)`** -- parity with `OTelMiddleware` and `SentryMiddleware`. A predicate `(task_name: str) -> bool` toggles metric export per task. + +### Changed + +- **`dagron-core` git dependency pinned** -- `Cargo.toml` now pins `dagron-core` to a specific commit SHA. Upstream pushes no longer cause silent build breakage. +- **`Storage` trait doc comment** -- now lists all three backends (SQLite, Postgres, Redis) instead of just the two Diesel ones. +- **`AsyncQueueMixin.metrics_timeseries` stub** -- parameter name corrected from `interval` to `bucket` to match the real sync signature. Call sites typed via the stub were silently wrong at runtime. + +--- + +## 0.11.0 + +### Features + +- **DAG workflows** -- first-class support for directed acyclic graph workflows built on the new [dagron-core](https://github.com/ByteVeda/dagron) engine; `Workflow` builder with `step()`, `gate()`, and `after=` dependencies; `queue.submit_workflow(wf)` launches a run, `WorkflowRun.wait()` blocks until terminal, `run.status()` returns per-node snapshots, `run.cancel()` halts in-flight execution; workflows are persisted across restarts with full node history +- **Fan-out / fan-in** -- `step(fan_out="each")` expands a list result into N parallel child jobs; `step(fan_in="all")` aggregates all child results into a single downstream step; supports empty lists, single-item lists, and preserves result ordering +- **Conditional execution** -- per-step `condition="on_success" | "on_failure" | "always"` or a callable `(WorkflowContext) -> bool`; combine with `Workflow(on_failure="continue")` so independent branches keep running after a sibling fails; skip propagation respects `always` +- **Approval gates** -- `wf.gate("review", after="evaluate", timeout=3600, on_timeout="reject")` pauses the workflow until `queue.approve_gate(run_id, name)` or `queue.reject_gate(run_id, name)`; timeout enforced with a background timer; emits `WORKFLOW_GATE_REACHED` event +- **Sub-workflows** -- compose workflows by referencing another workflow as a step via `region_etl.as_step(region="eu")`; child workflows have a `parent_run_id` link and propagate cancellation and failure upward; child terminal status feeds into parent DAG evaluation +- **Cron-scheduled workflows** -- `@queue.periodic(cron=...)` now accepts a `WorkflowProxy`; launcher task is auto-registered and submits a fresh workflow run on every tick +- **Incremental re-runs** -- `Workflow(cache_ttl=86400)` hashes step results with SHA-256; `queue.submit_workflow(wf, incremental=True, base_run=prev_run.id)` skips completed steps whose inputs are unchanged; failed steps always re-run; dirty propagation cascades to downstream nodes; new `CACHE_HIT` terminal status distinguishes cached steps from freshly executed ones +- **Graph algorithms** -- `wf.topological_levels()`, `wf.stats()`, `wf.critical_path(durations)`, `wf.bottleneck_analysis(durations)`, and `wf.execution_plan()` for pre-execution analysis; all algorithms operate on the compiled DAG without requiring a live run +- **Visualization** -- `wf.visualize("mermaid")` and `wf.visualize("dot")` render the DAG; `run.visualize("mermaid")` color-codes live node status (running/completed/failed/cache-hit/waiting-approval) +- **Workflow events** -- new event types `WORKFLOW_SUBMITTED`, `WORKFLOW_COMPLETED`, `WORKFLOW_FAILED`, `WORKFLOW_CANCELLED`, `WORKFLOW_GATE_REACHED` for observability hooks +- **Type-safe builder** -- `step()` accepts any object satisfying the `HasTaskName` protocol (runtime-checkable), keeping the builder API strict without coupling to a concrete `TaskWrapper` class + +### Internal + +- New Rust crate `crates/taskito-workflows/` -- workflow engine with `WorkflowDefinition`, `WorkflowRun`, `WorkflowNode`, node status state machine (including `CacheHit` variant), and storage trait with SQLite/Postgres/Redis backends; feature-gated behind `workflows` cargo feature +- `dagron-core` added as git dependency (`https://github.com/ByteVeda/dagron.git`) for DAG construction and traversal +- New PyO3 bindings in `crates/taskito-python/src/py_workflow/` -- `PyWorkflowBuilder`, `PyWorkflowHandle`, `PyWorkflowRunStatus`; `py_queue/workflow_ops.rs` exposes `submit_workflow`, `mark_workflow_node_result`, `expand_fan_out`, `check_fan_out_completion`, `skip_workflow_node`, `set_workflow_node_waiting_approval`, `resolve_workflow_gate`, `finalize_run_if_terminal`, and base-run lookup helpers +- New Python package `py_src/taskito/workflows/` with 11 modules -- `builder.py` (Workflow, GateConfig, WorkflowProxy), `tracker.py` (cascade evaluator), `run.py` (WorkflowRun), `mixins.py` (QueueWorkflowMixin), `fan_out.py`, `context.py` (WorkflowContext), `incremental.py` (dirty-set computation), `analysis.py` (graph algorithms), `visualization.py`, `types.py`, `__init__.py` +- `maturin` CI feature list fixed -- `ci.yml` and `publish.yml` now include `workflows` alongside `extension-module,postgres,redis,native-async` (previously missing, which would have shipped broken wheels) +- CI action versions bumped -- `Swatinem/rust-cache@v2.9.1`, `actions/setup-node@v6` to silence Node.js 20 deprecation warnings +- 74 new Python tests across 10 files covering linear, fan-out, conditions, gates, sub-workflows, cron, analysis, caching, and visualization + +--- + +## 0.10.1 + +### Changed + +- Repository transferred to [ByteVeda](https://github.com/ByteVeda/taskito) org +- Documentation URL updated to [docs.byteveda.org/taskito](https://docs.byteveda.org/taskito) +- All internal links updated from `pratyush618/taskito` to `ByteVeda/taskito` + +--- + +## 0.10.0 + +### Features + +- **Dashboard rebuild** -- full rewrite of the web dashboard using Preact, Vite, and Tailwind CSS; production-grade dark/light UI with lucide icons, toast notifications, loading states, timeseries charts, and 3 new pages (Resources, Queue Management, System Internals); 128KB single-file HTML (32KB gzipped) served from the Python package with zero runtime dependencies +- **Smart scheduling** -- adaptive backpressure polling (50ms base → 200ms max backoff when idle, instant reset on dispatch); per-task duration cache tracks average execution time in-memory; weighted least-loaded dispatch for prefork pool factors in task duration (`score = in_flight × avg_duration`) + +### Internal + +- Dashboard frontend source in `dashboard/` (Preact + Vite + Tailwind CSS + TypeScript); build via `cd dashboard && npm run build`; output inlined into `py_src/taskito/templates/dashboard.html` +- `dashboard.py` simplified to read single pre-built HTML instead of composing from 8 separate template files +- `Scheduler::run()` uses adaptive polling with exponential backoff (50ms → 200ms max); `tick()` returns `bool` for feedback +- `TaskDurationCache` in-memory HashMap tracks per-task avg wall_time_ns, updated on every `handle_result()` +- `weighted_least_loaded()` dispatch strategy in `prefork/dispatch.rs`; `aging_factor` field added to `SchedulerConfig` + +--- + +## 0.9.0 + +### Features + +- **Prefork worker pool** -- `queue.run_worker(pool="prefork", app="myapp:queue")` spawns child Python processes with independent GILs for true CPU parallelism; each child imports the app module, builds its own task registry, and executes tasks in a read-execute-write loop over JSON Lines IPC; the parent Rust scheduler dequeues jobs and dispatches to the least-loaded child via stdin pipes; reader threads parse child stdout and feed results back to the scheduler; graceful shutdown sends shutdown messages to children and waits with timeout before killing +- **Worker discovery** -- `queue.workers()` now returns `hostname`, `pid`, `pool_type`, and `started_at` for each worker, giving operators visibility into multi-machine deployments +- **Worker lifecycle events** -- three new event types: `WORKER_ONLINE` (registered in storage), `WORKER_OFFLINE` (dead worker reaped), `WORKER_UNHEALTHY` (resource health degraded); subscribe via `queue.on_event(EventType.WORKER_OFFLINE, callback)` +- **Worker status transitions** -- workers report `active → draining → stopped` status; shutdown signal sets status to `"draining"` before drain timeout, visible in `queue.workers()` and the dashboard +- **Orphan rescue prep** -- `list_claims_by_worker` storage method enables future orphaned job rescue when dead workers are detected +- **Task result streaming** -- `current_job.publish(data)` streams partial results from inside tasks; `job.stream()` / `await job.astream()` iterates partial results as they arrive; built on existing `task_logs` infrastructure with `level="result"` (no new tables or Rust changes); FastAPI SSE endpoint supports `?include_results=true` to stream partial results alongside progress + +### Internal + +- New Rust module `crates/taskito-python/src/prefork/` with 4 files: `mod.rs` (PreforkPool + WorkerDispatcher impl), `child.rs` (ChildWriter/ChildReader/ChildProcess split handles), `protocol.rs` (ParentMessage/ChildMessage JSON serialization), `dispatch.rs` (least-loaded dispatcher) +- New Python package `py_src/taskito/prefork/` with `child.py` (child process main loop), `__init__.py` (PreforkConfig), `__main__.py` (entry point) +- `base64` and `gethostname` crates added to `taskito-python` dependencies +- `run_worker()` gains `pool` and `app_path` parameters in both Rust (`py_queue/worker.rs`) and Python (`app.py`) +- `workers` table gains 4 columns: `started_at`, `hostname`, `pid`, `pool_type` (all backends + migrations) +- `reap_dead_workers` returns `Vec` (reaped worker IDs) instead of `u64`; enables `WORKER_OFFLINE` event emission +- New storage methods: `update_worker_status`, `list_claims_by_worker` across all 3 backends + +--- + +## 0.8.0 + +### Features + +- **Namespace-based routing** -- `Queue(namespace="team-a")` isolates workloads across teams/services sharing a single database; enqueued jobs carry the namespace, workers only dequeue matching jobs, `list_jobs()` and `list_jobs_filtered()` default to the queue's namespace (pass `namespace=None` for global view); DLQ and archival preserve namespace through the full job lifecycle; periodic tasks inherit namespace from their scheduler; backward compatible (`None` namespace matches only `NULL`-namespace jobs) + +### Internal + +- `namespace` column added to `dead_letter` and `archived_jobs` tables; `DeadLetterRow`, `NewDeadLetterRow`, `ArchivedJobRow` models updated; Redis `DeadJobEntry` uses `#[serde(default)]` for backward compatibility +- `Storage` trait: `dequeue`, `dequeue_from`, `list_jobs`, `list_jobs_filtered` signatures gain `namespace: Option<&str>` parameter; all 3 backends + delegate macro updated +- `Scheduler` struct carries `namespace: Option` field, passes to `dequeue_from` in poller +- `PyQueue` struct carries `namespace: Option` field; `PyJob` exposes `namespace` to Python +- `_UNSET` sentinel in `mixins.py` distinguishes "namespace not passed" from explicit `None` + +--- + +## 0.7.0 + +### Features + +- **Async canvas primitives** -- `Signature.apply_async()`, `chain.apply_async()`, `group.apply_async()`, and `chord.apply_async()` for non-blocking workflow execution from async contexts; `chain` uses `aresult()` for truly async step-by-step execution; `group` uses `asyncio.gather` for concurrent wave awaiting; `chord` awaits all group results then enqueues the callback +- **Sample-based circuit breaker recovery** -- half-open state now allows N probe requests (default 5) instead of a single probe; closes only when the success rate meets a configurable threshold (default 80%); immediately re-opens when the threshold becomes mathematically impossible; timeout safety valve re-opens if probes don't complete within the cooldown period; configure via `circuit_breaker={"half_open_probes": 5, "half_open_success_rate": 0.8}` on `@queue.task()` +- **`enqueue_many()` parity with `enqueue()`** -- batch enqueue now supports per-job `delay`/`delay_list`, `unique_keys`, `metadata`/`metadata_list`, `expires`/`expires_list`, and `result_ttl`/`result_ttl_list` parameters; also emits `JOB_ENQUEUED` events and dispatches `on_enqueue` middleware hooks, matching single-enqueue behavior +- **`TaskFailedError` exception** -- new exception type in the hierarchy for tasks that failed (as opposed to cancelled or dead-lettered); `job.result()` now raises `TaskFailedError`, `TaskCancelledError`, `MaxRetriesExceededError`, or `SerializationError` instead of generic `RuntimeError` +- **`PyResultSender` conditional export** -- `from taskito import PyResultSender` works when built with `native-async` feature; silently unavailable otherwise (no confusing `AttributeError`) + +### Fixes + +- **Middleware context `queue_name` was `"unknown"`** -- `on_retry`, `on_dead_letter`, `on_cancel`, and `on_timeout` middleware hooks now receive the actual queue name from the job instead of a hardcoded `"unknown"` string +- **Redis `KEYS *` in lock reaping** -- `reap_expired_locks` replaced `KEYS` (O(N), blocks Redis server) with cursor-based `SCAN` using `COUNT 100` +- **Redis execution claims never expire** -- `claim_execution` now uses `SET NX PX 86400000` (24-hour TTL); orphaned claims from dead workers auto-expire instead of blocking re-execution forever +- **`_taskito_is_async` fragility** -- `_taskito_is_async` and `_taskito_async_fn` are now declared fields on `TaskWrapper.__init__` instead of dynamically monkey-patched attributes; prevents silent fallback to sync execution path if attributes are missing + +### Internal + +- All production Rust `eprintln!` calls replaced with `log` crate macros (`log::info!`, `log::warn!`, `log::error!`); `log` dependency added to `taskito-python` and `taskito-async` crates +- `ResultOutcome::Retry`, `::DeadLettered`, `::Cancelled` now carry `queue: String` for middleware context +- Ruff `target-version` updated from `py39` to `py310` to match `requires-python = ">=3.10"` +- Fixed UP035 (`Callable` import from `collections.abc`) and B905 (`zip()` without `strict=`) lint warnings +- Circuit breakers schema: 5 new columns on `circuit_breakers` table (`half_open_max_probes`, `half_open_success_rate`, `half_open_probe_count`, `half_open_success_count`, `half_open_failure_count`) with backward-compatible defaults + +--- + +## 0.6.0 + +### Features + +- **Middleware lifecycle hooks wired** -- `on_retry(ctx, error, retry_count)`, `on_dead_letter(ctx, error)`, and `on_cancel(ctx)` are now dispatched from the Rust result handler; they fire for every matching outcome across all registered middleware +- **Expanded middleware hooks** -- `TaskMiddleware` gains four new hooks: `on_enqueue`, `on_dead_letter`, `on_timeout`, `on_cancel`; `on_enqueue` receives a mutable `options` dict that can modify priority, delay, queue, and other enqueue parameters before the job is written +- **`JOB_RETRYING`, `JOB_DEAD`, `JOB_CANCELLED` events now emitted** -- these three event types were previously defined but never fired; they are now emitted from the Rust result handler with payloads `{job_id, task_name, error, retry_count}`, `{job_id, task_name, error}`, and `{job_id, task_name}` respectively +- **Queue-level rate limits** -- `queue.set_queue_rate_limit("name", "100/m")` applies a token-bucket rate limit to an entire queue, checked in the scheduler before per-task limits +- **Queue-level concurrency caps** -- `queue.set_queue_concurrency("name", 10)` limits how many jobs from a queue run simultaneously across all workers, checked before per-task `max_concurrent` +- **Worker lifecycle events** -- `EventType.WORKER_STARTED` and `EventType.WORKER_STOPPED` fired when a worker thread comes online or exits; subscribe via `queue.on_event(EventType.WORKER_STARTED, cb)` +- **Queue pause/resume events** -- `EventType.QUEUE_PAUSED` and `EventType.QUEUE_RESUMED` fired by `queue.pause()` and `queue.resume()` +- **`event_workers` parameter** -- `Queue(event_workers=N)` configures the event bus thread pool size (default 4); raise for high event volume +- **Per-webhook delivery options** -- `queue.add_webhook()` now accepts `max_retries`, `timeout`, and `retry_backoff` per endpoint, replacing the previous hardcoded values +- **OTel customization** -- `OpenTelemetryMiddleware` adds `span_name_fn`, `attribute_prefix`, `extra_attributes_fn`, and `task_filter` parameters +- **Sentry customization** -- `SentryMiddleware` adds `tag_prefix`, `transaction_name_fn`, `task_filter`, and `extra_tags_fn` parameters +- **Prometheus customization** -- `PrometheusMiddleware` and `PrometheusStatsCollector` add `namespace`, `extra_labels_fn`, and `disabled_metrics` parameters; metrics grouped by category (`"jobs"`, `"queue"`, `"resource"`, `"proxy"`, `"intercept"`) +- **FastAPI route selection** -- `TaskitoRouter` adds `include_routes`/`exclude_routes`, `dependencies`, `sse_poll_interval`, `result_timeout`, `default_page_size`, `max_page_size`, and `result_serializer` parameters; new endpoints: `/health`, `/readiness`, `/resources`, `/stats/queues` +- **Flask CLI group** -- `Taskito(app, cli_group="tasks")` renames the CLI command group; `flask taskito info --format json` outputs machine-readable stats +- **Django settings** -- `TASKITO_AUTODISCOVER_MODULE`, `TASKITO_ADMIN_PER_PAGE`, `TASKITO_ADMIN_TITLE`, `TASKITO_ADMIN_HEADER`, `TASKITO_DASHBOARD_HOST`, `TASKITO_DASHBOARD_PORT` control autodiscovery, admin pagination, branding, and dashboard bind address +- **`max_retry_delay` on `@queue.task()`** -- caps exponential backoff at a configurable ceiling in seconds (defaults to 300 s) +- **`max_concurrent` on `@queue.task()`** -- limits how many instances of a task run simultaneously across all workers +- **`serializer` on `@queue.task()`** -- per-task serializer override; falls back to queue-level serializer +- **Per-task serializer full round-trip** -- deserialization now also uses the per-task serializer; previously only enqueue (serialization) did; both the sync and native-async worker paths call `_deserialize_payload(task_name, payload)` instead of cloudpickle directly +- **`on_timeout` middleware hook wired** -- `on_timeout(ctx)` now fires when the Rust maintenance reaper detects a stale job that exceeded its hard timeout; fires before `on_retry` (if retrying) or `on_dead_letter` (if retries exhausted); previously the hook existed in `TaskMiddleware` but was never called +- **`QUEUE_PAUSED` / `QUEUE_RESUMED` events emitted** -- `queue.pause()` and `queue.resume()` now emit these events with payload `{"queue": "..."}` after updating storage; previously the event types were defined but never fired +- **Scheduler tuning** -- `Queue(scheduler_poll_interval_ms=N, scheduler_reap_interval=N, scheduler_cleanup_interval=N)` exposes the three Rust scheduler timing knobs to Python + +--- + +
+Older releases (0.1.0 – 0.5.0) + +## 0.5.0 + +### New Features + +- **Native async tasks** -- `async def` task functions run natively on a dedicated event loop; no wrapping in `asyncio.run()` or thread bridging; dual-dispatch worker pool routes async jobs to `NativeAsyncPool` and sync jobs to the existing thread pool +- **`async_concurrency` parameter** -- `Queue(async_concurrency=100)` caps concurrent async tasks on the event loop; independent of the `workers` (sync thread) count +- **`current_job` in async tasks** -- `current_job.id`, `.log()`, `.update_progress()`, `.check_cancelled()` work inside `async def` tasks via `contextvars`; each concurrent task gets an isolated context +- **KEDA integration** -- `taskito scaler --app myapp:queue --port 9091` starts a lightweight metrics server; `/api/scaler` returns queue depth for KEDA `metrics-api` trigger; `/metrics` exposes Prometheus text format; `/health` for liveness probes +- **KEDA deploy templates** -- `deploy/keda/` contains ready-to-use `ScaledObject`, `ScaledObject` (Prometheus), and `ScaledJob` YAML manifests +- **Argument interception** -- `interception="strict"|"lenient"` on `Queue()` classifies every task argument before serialization; five strategies: PASS, CONVERT, REDIRECT, PROXY, REJECT; built-in rules cover UUID, datetime, Decimal, Pydantic models, dataclasses, SQLAlchemy sessions, Redis clients, file handles, and more +- **Worker resource runtime** -- `@queue.worker_resource("name")` decorator registers a factory initialized once at worker startup; four scopes: `"worker"` (default), `"task"` (pool), `"thread"` (thread-local), `"request"` (per-task fresh) +- **Resource injection** -- `@queue.task(inject=["name"])` or `db: Inject["name"]` annotation syntax injects live resources into tasks without serializing them; `from taskito import Inject` +- **Resource dependencies** -- `depends_on=["other"]` on `@queue.worker_resource()`; topological initialization order, reverse teardown; cycles detected eagerly at registration time (`CircularDependencyError`) +- **Health checking** -- `health_check=` and `health_check_interval=` on `@queue.worker_resource()`; unhealthy resources are recreated up to `max_recreation_attempts` times; `queue.health_check("name")` for manual checks +- **Resource pools** -- task-scoped resources get a semaphore-based pool with `pool_size`, `pool_min`, `acquire_timeout`, `max_lifetime`, `idle_timeout`; `pool_min > 0` pre-warms instances at startup +- **Thread-local resources** -- `scope="thread"` creates one instance per worker thread via `ThreadLocalStore`, torn down on shutdown +- **Frozen resources** -- `frozen=True` wraps the resource in a `FrozenResource` proxy that raises `AttributeError` on attribute writes +- **Hot reload** -- `reloadable=True` marks a resource for reload on `SIGHUP`; `taskito reload --app myapp:queue` CLI subcommand; `queue._resource_runtime.reload()` programmatic reload +- **TOML resource config** -- `queue.load_resources("resources.toml")` loads resource definitions from a TOML file; factory, teardown, and health_check are dotted import paths; Python 3.11+ built-in `tomllib`, older versions need `tomli` +- **Resource proxies** -- transparent deconstruct/reconstruct of non-serializable objects; built-in handlers: `file`, `logger`, `requests_session`, `httpx_client`, `boto3_client`, `gcs_client` +- **Proxy security** -- HMAC-SHA256 recipe signing via `recipe_signing_key=` on `Queue()` or `TASKITO_RECIPE_SECRET` env var; reconstruction timeout via `max_reconstruction_timeout=`; file path allowlist via `file_path_allowlist=`; per-handler opt-out via `disabled_proxies=` +- **`NoProxy` wrapper** -- `from taskito import NoProxy`; opt out of proxy handling for a specific argument, letting the serializer handle it directly +- **Custom type rules** -- `queue.register_type(MyType, "redirect", resource="my_resource")` registers custom types with any strategy (requires interception enabled) +- **Interception metrics** -- `queue.interception_stats()` returns total calls, per-strategy counts, average duration, and max depth reached +- **Proxy metrics** -- `queue.proxy_stats()` returns per-handler deconstruction/reconstruction counts, error counts, and average duration +- **Resource status** -- `queue.resource_status()` returns per-resource health, scope, init duration, and recreation count +- **Test mode resources** -- `queue.test_mode(resources={"db": mock_db})` injects mocks during test mode without worker startup; `MockResource(name, return_value=..., wraps=..., track_calls=True)` adds call tracking +- **Optional cloud dependencies** -- `pip install taskito[aws]` adds boto3>=1.20; `pip install taskito[gcs]` adds google-cloud-storage>=2.0 + +### Breaking Changes + +- **Dropped Python 3.9 support** -- minimum required version is now Python 3.10; Python 3.9 reached EOL in October 2025 + +--- + +## 0.4.0 + +### New Features + +- **Distributed locking** — `queue.lock()` / `await queue.alock()` context managers with auto-extend background thread, acquisition timeout, and cross-process support; `LockNotAcquired` exception for failed acquisitions +- **Exactly-once semantics** — `claim_execution` / `complete_execution` storage layer prevents duplicate task execution across worker restarts +- **Async worker pool** — `AsyncWorkerPool` with `spawn_blocking` and GIL management; `WorkerDispatcher` trait in `taskito-core` future-proofs for other language bindings +- **Queue pause/resume** — `queue.pause()`, `queue.resume()`, `queue.paused_queues()` to suspend and restore processing per named queue +- **Job archival** — `queue.archive()` moves jobs to a persistent archive; `queue.list_archived()` retrieves them +- **Job revocation** — `queue.purge()` removes jobs by filter; `queue.revoke_task()` prevents all future enqueues of a given task name +- **Job replay** — `queue.replay()` re-enqueues a completed or failed job; `queue.replay_history()` returns the replay log +- **Circuit breakers** — `circuit_breaker={"threshold": 5, "window": 60, "cooldown": 120}` on `@queue.task()`; `queue.circuit_breakers()` returns current state of all circuit breakers +- **Structured task logging** — `current_job.log(message)` from inside tasks; `queue.task_logs(job_id)` and `queue.query_logs()` for retrieval +- **Cron timezone support** — `timezone="America/New_York"` on `@queue.periodic()`; uses `chrono-tz` under the hood, defaults to UTC +- **Custom retry delays** — `retry_delays=[1, 5, 30]` on `@queue.task()` for per-attempt delay overrides instead of exponential backoff +- **Soft timeouts** — `soft_timeout=` on `@queue.task()`; checked cooperatively via `current_job.check_timeout()` +- **Worker tags/specialization** — `tags=["gpu", "heavy"]` on `queue.run_worker()`; jobs can be routed to workers with matching tags +- **Worker inspection** — `queue.workers()` / `await queue.aworkers()` return live worker state +- **Job DAG visualization** — `queue.job_dag(job_id)` returns a dependency graph for a job and its ancestors/descendants +- **Metrics timeseries** — `queue.metrics_timeseries()` returns historical throughput/latency data; `queue.metrics()` for current snapshot +- **Extended job filtering** — `queue.list_jobs_filtered()` with `metadata_like`, `error_like`, `created_after`, `created_before` parameters +- **`MsgPackSerializer`** — built-in, requires `pip install msgpack`; faster than cloudpickle, smaller payloads, cross-language compatible +- **`EncryptedSerializer`** — AES-256-GCM encryption, requires `pip install cryptography`; wraps another serializer, payloads in DB are opaque ciphertext +- **`drain_timeout`** — configurable graceful shutdown wait time on `Queue()` constructor (default: 30 seconds) +- **Per-job `result_ttl`** — `result_ttl` override on `.apply_async()` to set cleanup policy per job +- **Dashboard enhancements** — workers tab, circuit breakers panel, job archival UI + +--- + +## 0.3.0 + +### Features + +- **Redis storage backend** — optional Redis backend for distributed workloads (`pip install taskito[redis]`); Lua scripts for atomic operations, sorted sets for indexing +- **Events & webhooks** — event system with webhook delivery support +- **Flask integration** — contrib integration for Flask applications +- **Prometheus integration** — contrib stats collector with `PrometheusStatsCollector` +- **Sentry integration** — contrib middleware for Sentry error tracking + +### Fixes + +- Guard arithmetic overflow across timeout detection, worker reaping, scheduler cleanup, circuit breaker timing, and Redis TTL purging +- Treat cancelled jobs as terminal in `_poll_once` so `result()` raises immediately +- Cap float-to-i64 casts to prevent silent overflow in delay_seconds, expires, retry_delays, retry_backoff +- Reject negative pagination in list_jobs, dead_letters, list_archived, query_task_logs +- Replace deprecated `asyncio.get_event_loop()` with `get_running_loop()` +- Replace Redis `KEYS` with `SCAN` in purge operations +- Fix Redis `enqueue_unique()` race condition with atomic Lua scripts +- Only call middleware `after()` for those whose `before()` succeeded +- Recover from poisoned mutex in scheduler instead of panicking +- Validate `EncryptedSerializer` key type and size before use +- Skip webhook retries on 4xx client errors + +## 0.2.3 + +### Features + +- **Postgres storage backend** — optional PostgreSQL backend for multi-machine workers and higher write throughput (`pip install taskito[postgres]`); full feature parity with SQLite +- **Django integration** — `TASKITO_BACKEND`, `TASKITO_DB_URL`, `TASKITO_SCHEMA` settings for configuring the backend from Django projects + +### Critical Fixes + +- **Dashboard dead routes** — Moved `/logs` and `/replay-history` handlers above the generic catch-all in `dashboard.py`, fixing 404s on these endpoints +- **Stale `__version__`** — Replaced hardcoded version with `importlib.metadata.version()` with fallback +- **`retry_dead` non-atomic** — Wrapped enqueue + delete in a single transaction (SQLite & Postgres), preventing ghost dead letters on partial failure +- **`enqueue_unique` race condition** — Wrapped check + insert in a transaction; catches unique constraint violations to return the existing job instead of erroring +- **`now_millis()` panic** — Replaced `.expect()` with `.unwrap_or(Duration::ZERO)` to prevent scheduler panic on clock issues +- **`reap_stale` double error records** — Removed redundant `storage.fail()` call; `handle_result` already records the failure + +--- + +## 0.2.2 + +- Added `readme` field to `pyproject.toml` so PyPI displays the project description. + +--- + +## 0.2.1 + +Re-release of 0.2.0 — PyPI does not allow re-uploads of deleted versions. + +--- + +## 0.2.0 + +### Core Reliability + +- **Exception hierarchy** -- `TaskitoError` base class with `TaskTimeoutError`, `SoftTimeoutError`, `TaskCancelledError`, `MaxRetriesExceededError`, `SerializationError`, `CircuitBreakerOpenError`, `RateLimitExceededError`, `JobNotFoundError`, `QueueError` +- **Pluggable serializers** -- `CloudpickleSerializer` (default), `JsonSerializer`, or custom `Serializer` protocol +- **Exception filtering** -- `retry_on` and `dont_retry_on` parameters for selective retries +- **Cancel running tasks** -- cooperative cancellation with `queue.cancel_running_job()` and `current_job.check_cancelled()` +- **Soft timeouts** -- `soft_timeout` parameter with `current_job.check_timeout()` for cooperative time limits + +### Developer Experience + +- **Per-task middleware** -- `TaskMiddleware` base class with `before()`, `after()`, `on_retry()` hooks +- **Worker heartbeat** -- `queue.workers()` / `await queue.aworkers()` to monitor worker health +- **Job expiration** -- `expires` parameter on `apply_async()` to skip time-sensitive jobs that weren't started in time +- **Result TTL per job** -- `result_ttl` parameter on `apply_async()` to override global cleanup policy per job + +### Power Features + +- **chunks / starmap** -- `chunks(task, items, chunk_size)` and `starmap(task, args_list)` canvas primitives +- **Group concurrency** -- `max_concurrency` parameter on `group()` to limit parallel execution +- **OpenTelemetry** -- `OpenTelemetryMiddleware` for distributed tracing; install with `pip install taskito[otel]` + +--- + +## 0.1.1 + +### Features + +- **Web dashboard** -- `taskito dashboard --app myapp:queue` serves a built-in monitoring UI with dark mode, auto-refresh, job detail views, and dead letter management +- **FastAPI integration** -- `TaskitoRouter` provides a pre-built `APIRouter` with endpoints for stats, job status, progress streaming (SSE), and dead letter management +- **Testing utilities** -- `queue.test_mode()` context manager for running tasks synchronously without a worker +- **CLI dashboard command** -- `taskito dashboard` command with `--host` and `--port` options +- **Async result awaiting** -- `await job.aresult()` for non-blocking result fetching + +### Changes + +- Renamed `python/` to `py_src/` and `rust/` to `crates/` for clearer project structure +- Default `db_path` now uses `.taskito/` directory, with automatic directory creation + +--- + +## 0.1.0 + +*Initial release* + +### Features + +- **Task queue** — `@queue.task()` decorator with `.delay()` and `.apply_async()` +- **Priority queues** — integer priority levels, higher values processed first +- **Retry with exponential backoff** — configurable max retries, backoff multiplier, and jitter +- **Dead letter queue** — failed jobs preserved for inspection and replay +- **Rate limiting** — token bucket algorithm with `"N/s"`, `"N/m"`, `"N/h"` syntax +- **Task workflows** — `chain`, `group`, and `chord` primitives +- **Periodic tasks** — cron-scheduled tasks with 6-field expressions (seconds granularity) +- **Progress tracking** — `current_job.update_progress()` from inside tasks +- **Job cancellation** — cancel pending jobs before execution +- **Unique tasks** — deduplicate active jobs by key +- **Batch enqueue** — `task.map()` and `queue.enqueue_many()` with single-transaction inserts +- **Named queues** — route tasks to isolated queues, subscribe workers selectively +- **Hooks** — `before_task`, `after_task`, `on_success`, `on_failure` +- **Async support** -- `aresult()`, `astats()`, `arun_worker()`, and more +- **Job context** -- `current_job.id`, `.task_name`, `.retry_count`, `.queue_name` +- **Error history** -- per-attempt error tracking via `job.errors` +- **Result TTL** -- automatic cleanup of completed/dead jobs +- **CLI** -- `taskito worker` and `taskito info --watch` +- **Metadata** -- attach arbitrary JSON to jobs + +### Architecture + +- Rust core with PyO3 bindings +- SQLite storage with WAL mode and Diesel ORM +- Tokio async scheduler with 50ms poll interval +- OS thread worker pool with crossbeam channels +- cloudpickle serialization for arguments and results + +
diff --git a/docs/content/docs/resources/changelog.mdx b/docs/content/docs/resources/changelog.mdx index 5c169386..0fa8140d 100644 --- a/docs/content/docs/resources/changelog.mdx +++ b/docs/content/docs/resources/changelog.mdx @@ -3,7 +3,28 @@ title: Changelog description: "Release history for taskito — every notable change, fix, and feature." --- -All notable changes to taskito are documented here. +{/* AUTO-GENERATED from /CHANGELOG.md by scripts/sync-changelog.mjs — do not edit directly. */} + +All notable changes to taskito are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). All SDKs (Python, Node) and the +underlying Rust crates are released together, in lock-step. + +## 0.16.4 + +Maintenance release. Upgrades the Rust↔Python binding layer and refreshes project docs; no +public API changes. + +### Changed + +- **PyO3 0.29.** Upgraded the PyO3 binding layer from 0.22 to 0.29 (and `pyo3-log` to 0.13). + Internal only — no Python API change. Migrates to the `Bound` / `IntoPyObject` APIs and + declares `gil_used`, laying the groundwork for Python 3.14 and free-threaded builds. +- **Node SDK default SQLite settings.** Opening a SQLite-backed queue now applies sensible + defaults out of the box (#292). +- **Documentation.** Restructured the READMEs into a project-level root README plus + self-contained per-SDK pages, and introduced this root `CHANGELOG.md` — now also rendered on + the documentation site. ## 0.16.3 @@ -183,7 +204,7 @@ Security-hardening release. No breaking changes. ## 0.15.0 -See the Upgrading to 0.15 guide for +See the [Upgrading to 0.15](https://docs.byteveda.org/taskito/guides/operations/upgrading-0.15) guide for migration steps, rolling-upgrade notes, and the downgrade floor. ### Breaking @@ -245,7 +266,7 @@ migration steps, rolling-upgrade notes, and the downgrade floor. ### Added -- **Bare-metal autoscaler.** `taskito.autoscale` provides an in-process HPA-style control loop that spawns and drains `taskito worker` subprocesses based on queue depth and utilisation. Mirrors the Kubernetes HPA dual-signal formula (`depth_desired` + `util_desired`). Configurable stabilisation windows (default: scale-up immediate, scale-down 5 minutes), tolerance band (10%), overload override, and crash recovery. Use `serve_autoscaler(queue, AutoscaleConfig(...))` as the entry point. See the Bare-Metal Autoscaler guide. +- **Bare-metal autoscaler.** `taskito.autoscale` provides an in-process HPA-style control loop that spawns and drains `taskito worker` subprocesses based on queue depth and utilisation. Mirrors the Kubernetes HPA dual-signal formula (`depth_desired` + `util_desired`). Configurable stabilisation windows (default: scale-up immediate, scale-down 5 minutes), tolerance band (10%), overload override, and crash recovery. Use `serve_autoscaler(queue, AutoscaleConfig(...))` as the entry point. See the [Bare-Metal Autoscaler guide](https://docs.byteveda.org/taskito/guides/operations/autoscaler). - **Dashboard `/api/workflows` backend.** Four new REST endpoints expose workflow run data: `GET /api/workflows/runs` (paginated list, filterable by definition name and state), `GET /api/workflows/runs/{id}` (run header + per-node detail with compensation fields), `GET /api/workflows/runs/{id}/dag` (DAG JSON), and `GET /api/workflows/runs/{id}/children` (sub-workflow runs). All timestamps are Unix milliseconds. - **Canvas saga compensation.** `chain`, `group`, and `chord` support `.with_compensation([...])`. On failure, compensators for completed steps are enqueued with a deterministic `canvas_compensation:{run_id}:{slot}` idempotency key. Dispatch order: chain → reverse-sequential, group → parallel, chord → callback compensator first then group members. `Signature.with_compensation(compensator)` lets individual signatures carry their own compensator. - **Per-item batch results.** `@queue.task(batch={"per_item_results": True})` enables per-item tracking for batched tasks. The task returns `list[BatchItemResult]`; each caller's `BatchedJobResult.result()` returns **only that caller's value**. `BatchItemResult.success(item_index, result)` and `BatchItemResult.failure(item_index, error)` are the constructors. `partial_failures()` returns failed items after a successful batch. `BatchPartialFailureError` and `BatchResultTypeError` are the new exception types. Cannot be combined with `idempotent=True`. @@ -767,3 +788,4 @@ Re-release of 0.2.0 — PyPI does not allow re-uploads of deleted versions. - cloudpickle serialization for arguments and results + diff --git a/docs/package.json b/docs/package.json index fa84187f..50a404d7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,8 +4,9 @@ "private": true, "type": "module", "scripts": { - "build": "react-router build", - "dev": "react-router dev", + "sync:changelog": "node ../scripts/sync-changelog.mjs", + "build": "pnpm sync:changelog && react-router build", + "dev": "pnpm sync:changelog && react-router dev", "start": "serve build/client", "typecheck": "react-router typegen && tsc --noEmit", "lint": "biome check", diff --git a/scripts/sync-changelog.mjs b/scripts/sync-changelog.mjs new file mode 100644 index 00000000..15bc4e12 --- /dev/null +++ b/scripts/sync-changelog.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +// Generate the docs changelog page from the canonical root CHANGELOG.md. +// +// CHANGELOG.md (Keep a Changelog) is the single source of truth — edit it there. +// This script wraps its body in the frontmatter the docs site needs and writes +// docs/content/docs/resources/changelog.mdx. Run automatically before `docs` dev/build. +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const repoRoot = new URL("../", import.meta.url); +const source = fileURLToPath(new URL("CHANGELOG.md", repoRoot)); +const target = fileURLToPath( + new URL("docs/content/docs/resources/changelog.mdx", repoRoot), +); + +// Drop the top-level "# Changelog" heading — the frontmatter title renders it. +const body = readFileSync(source, "utf8").replace(/^#\s+Changelog\s*\n/, "").trimStart(); + +const frontmatter = [ + "---", + "title: Changelog", + 'description: "Release history for taskito — every notable change, fix, and feature."', + "---", + "", + "{/* AUTO-GENERATED from /CHANGELOG.md by scripts/sync-changelog.mjs — do not edit directly. */}", + "", +].join("\n"); + +writeFileSync(target, `${frontmatter}\n${body}\n`); +console.log(`synced ${target} from CHANGELOG.md`); From 635a6697556bb250f6c8faf54c97b90b1318ed66 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:49:28 +0530 Subject: [PATCH 2/2] chore(release): 0.16.4 --- Cargo.lock | 10 +++++----- crates/taskito-core/Cargo.toml | 2 +- crates/taskito-mesh/Cargo.toml | 2 +- crates/taskito-node/Cargo.toml | 2 +- crates/taskito-python/Cargo.toml | 2 +- crates/taskito-workflows/Cargo.toml | 2 +- sdks/node/package.json | 2 +- sdks/python/pyproject.toml | 2 +- sdks/python/taskito/__init__.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cabe15e8..54411f13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,7 +1422,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "taskito-core" -version = "0.16.3" +version = "0.16.4" dependencies = [ "async-trait", "chrono", @@ -1446,7 +1446,7 @@ dependencies = [ [[package]] name = "taskito-mesh" -version = "0.16.3" +version = "0.16.4" dependencies = [ "base64", "bincode", @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "taskito-node" -version = "0.16.3" +version = "0.16.4" dependencies = [ "async-trait", "crossbeam-channel", @@ -1480,7 +1480,7 @@ dependencies = [ [[package]] name = "taskito-python" -version = "0.16.3" +version = "0.16.4" dependencies = [ "async-trait", "base64", @@ -1500,7 +1500,7 @@ dependencies = [ [[package]] name = "taskito-workflows" -version = "0.16.3" +version = "0.16.4" dependencies = [ "dagron-core", "diesel", diff --git a/crates/taskito-core/Cargo.toml b/crates/taskito-core/Cargo.toml index 313a5b81..293d78f1 100644 --- a/crates/taskito-core/Cargo.toml +++ b/crates/taskito-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskito-core" -version = "0.16.3" +version = "0.16.4" edition = "2021" [features] diff --git a/crates/taskito-mesh/Cargo.toml b/crates/taskito-mesh/Cargo.toml index 308381aa..5ed7db3c 100644 --- a/crates/taskito-mesh/Cargo.toml +++ b/crates/taskito-mesh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskito-mesh" -version = "0.16.3" +version = "0.16.4" edition = "2021" [dependencies] diff --git a/crates/taskito-node/Cargo.toml b/crates/taskito-node/Cargo.toml index d391414f..bba056d6 100644 --- a/crates/taskito-node/Cargo.toml +++ b/crates/taskito-node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskito-node" -version = "0.16.3" +version = "0.16.4" edition = "2021" description = "Node.js (napi-rs) bindings for the Taskito task-queue core" license = "MIT" diff --git a/crates/taskito-python/Cargo.toml b/crates/taskito-python/Cargo.toml index 8d703bda..27f133e8 100644 --- a/crates/taskito-python/Cargo.toml +++ b/crates/taskito-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskito-python" -version = "0.16.3" +version = "0.16.4" edition = "2021" [features] diff --git a/crates/taskito-workflows/Cargo.toml b/crates/taskito-workflows/Cargo.toml index 4714ab23..a2e4c181 100644 --- a/crates/taskito-workflows/Cargo.toml +++ b/crates/taskito-workflows/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskito-workflows" -version = "0.16.3" +version = "0.16.4" edition = "2021" [features] diff --git a/sdks/node/package.json b/sdks/node/package.json index cd383143..ae165b49 100644 --- a/sdks/node/package.json +++ b/sdks/node/package.json @@ -1,6 +1,6 @@ { "name": "@byteveda/taskito", - "version": "0.16.3", + "version": "0.16.4", "description": "Rust-powered task queue for Node.js — no broker required.", "license": "MIT", "type": "module", diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 00f32a2e..36d91042 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "taskito" -version = "0.16.3" +version = "0.16.4" description = "Rust-powered task queue for Python. No broker required." requires-python = ">=3.10" license = { file = "LICENSE" } diff --git a/sdks/python/taskito/__init__.py b/sdks/python/taskito/__init__.py index 3d32bb56..8151db3b 100644 --- a/sdks/python/taskito/__init__.py +++ b/sdks/python/taskito/__init__.py @@ -122,4 +122,4 @@ __version__ = _get_version("taskito") except PackageNotFoundError: - __version__ = "0.16.3" + __version__ = "0.16.4"