Changelog
Release history for flexiq — every notable change, fix, and feature.
Release history for flexiq — every notable change, fix, and feature.
All notable changes to FlexiQ are documented here. The format is based on Keep a Changelog; the project follows Semantic Versioning. All SDKs (Python, Node, Java) and the underlying Rust crates are released together, in lock-step.
Releases up to and including 0.23.0 were published under the project's former name, Taskito, and their entries below keep that name.
The project is now FlexiQ. This is a rename, not a redesign — no API was added, removed, or changed in behaviour beyond its name — but it is a hard break with no compatibility shims.
Published under new names on every registry: flexiq (PyPI), @byteveda/flexiq (npm),
org.byteveda:flexiq (Maven), and flexiq/flexiq-core (crates.io). The former packages receive
no further releases.
flexiq / @byteveda/flexiq / org.byteveda.flexiq / flexiq_core, and Java
types follow: Taskito → FlexiQ, TaskitoException → FlexiQException.TASKITO_* environment variable is now FLEXIQ_*, with no fallback.~/.flexiq/flexiq.db; the schema is unchanged.X-Flexiq-Signature, the dashboard token in X-Flexiq-Token, and
the session cookie is flexiq_session. The spelling is X-Flexiq-, not X-FlexiQ-, so the name
survives the title-casing HTTP clients apply to outgoing headers.flexiq_worker, flexiq_dashboard, flexiq_info.flexiq-server.Drain your queue before upgrading. Payload markers changed from __taskito_*__ to
__flexiq_*__, so 0.23.x and 1.0.0 workers must not share a database. Full instructions:
Migrating to FlexiQ.
Version-skew release. A polyglot deployment can now say which SDK and release each worker runs, and the wire format the SDKs exchange is pinned by golden vectors instead of by convention.
Storage::register_worker takes a WorkerRegistration struct, and
WorkerInfo carries two more fields. Registration was already nine positional arguments under a
too_many_arguments allow, so the SDK identity below would have made it eleven. Only direct
users of the taskito-core crate are affected — the Python, Node and Java APIs are unchanged.sdk and sdk_version land on the
workers table (migration 0009_worker_sdk), reach list_workers and /api/workers in every
SDK, and render as an SDK column in the dashboard. In a polyglot deployment the registry is the
only place an operator can tell a stale worker from a current one without going host by host.
A worker registered by an older shell reports null rather than a wrong version.contracts/wire-vectors.json pins the payload envelope byte for byte —
nine encode vectors and three decode-only ones — and every SDK suite asserts against the same
file, so a serializer that drifts fails in its own tests rather than in a mixed deployment.examples/polyglot/ runs a Python producer with Node and Java workers
against one jobs table, documenting the two footguns that setup has: every SDK's default
serializer is same-language-only, and all three processes must opt into the same codec.taskito facade re-exports the workflow and mesh crates, so a Rust consumer depends on
one crate instead of three.rate_limit now raises instead of silently disabling throttling. A
typo like "100/mn" parsed to None and dropped the limit with no error, which is precisely
the failure the limit exists to prevent; retry_budget already rejected the same mistake.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.
ResourceScope.TASK meant checked out of a bounded pool and REQUEST meant fresh per
task — the reverse of what Java and Node call those names, so the same scope="task" code
behaved differently per SDK. Python now uses POOLED (was TASK) and TASK (was REQUEST);
REQUEST is gone, and scope="request" raises. scope="task" still resolves but now builds
per task instead of pooling, so pooled resources must move to scope="pooled" — passing
pool_size/pool_min with any other scope now raises, which catches that case.on_false, log-consumer on_error, gate
on_timeout, workflow diagram format, fan-out strategy, dispatch order, task-log level,
workflow run state, dashboard role, subscription mode, and worker status. Wire and stored
values are unchanged, and existing string callers keep working (Java keeps its String
overloads, deprecated).condition
(WorkflowCondition, Python + Java), storage backend (StorageBackend, Python + Java),
interception register_type(strategy=…) (Strategy, Python), and the webhook delivery-log
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.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,
an unauthenticated dashboard — naming the value to fix instead of leaving a CrashLoopBackOff.
With webhook.enabled=true, annotating a pod with taskito.dev/inject adds an executor sidecar
built from the pod's own image, so nothing new is pulled and the pattern works for any
language. The webhook serves a chart-generated certificate by default, or cert-manager's.
taskito-server ships as a container image. docker/scheduler.Dockerfile builds a
distroless image around a static binary — no libc and no interpreter, with Postgres and Redis
compiled in — published as a linux/amd64 + linux/arm64 manifest at
ghcr.io/byteveda/taskito-server. One image schedules for apps written against any SDK, so the
fat application image no longer has to be duplicated for a worker deployment.
Java operator admin surface. taskito.requeueJob(id) forces a stuck running job back to
pending and releases its execution claim, preserving the retry budget — the only recovery path
for a job that neither the timeout reaper nor orphan recovery will touch (use it only once the
owning worker is confirmed dead or hung, or the job can run twice). Alongside it:
taskito.reloadResources(names) hot-reloads resources built with withReloadable(true),
rebuilding dependencies before the resources that used them; taskito.predicateStats() reports
what the enqueue gates decided; taskito.analyzeArguments(taskName, payload) dry-runs the
interceptor chain without enqueuing; and taskito.shutdown() closes every worker started from
the client, so a shutdown hook no longer has to hold each Worker.
Node operator admin surface. queue.requeueJob(id) forces a stuck running job back to
pending and releases its execution claim, preserving the retry budget — the only recovery path
for a job that neither the timeout reaper nor orphan recovery will touch (use it only once the
owning worker is confirmed dead or hung, or the job can run twice). Alongside it:
queue.reloadResources(names?) hot-reloads resources marked reloadable, rebuilding
dependencies before the resources that used them; queue.analyzeArguments(taskName, args)
dry-runs the interceptor chain without enqueuing or moving interceptionStats(); and
queue.shutdown() stops every worker started from the queue, so a SIGTERM handler no longer has
to hold each Worker.
Node predicate recipes, decisions, and registry. A gate could only allow or reject; it can
now return a Decision that also skips (no job — queue.tryEnqueue() reports null, enqueue
throws the new EnqueueSkippedError) or defers (job created with the gate's delay). Nine
ready-made gates ship as Recipes: businessHours, timeWindow, dayOfWeek, isWeekend,
after, before, payloadMatches, featureFlag, envVarTruthy — time zones resolved through
Intl, so no new dependency, and DST-correct. Gates can be registered by name
(registerPredicate) and referenced as gate(task, "name") for config-driven gating,
queue.predicateStats() reports what they decided, and predicate.skipped /
predicate.deferred join predicate.rejected on the event bus.
Node producer-side batch accumulator. queue.batcher(task, { maxSize, maxWaitMs }) buffers
one-at-a-time enqueues and flushes them as a single enqueueMany call on a size or time
trigger — the counterpart of Python's BatchAccumulator and Java's Batcher<T>. Entries keep
their own args and EnqueueOptions, a failed flush keeps them buffered (and a timed one
retries, reporting to onError), and Symbol.dispose means using flushes the tail at block
exit.
Node bare-metal autoscaler. serveAutoscaler(queue, { app }) and taskito autoscale <app>
spawn and drain worker processes to track queue depth on hosts without Kubernetes, closing the
last scaling gap against Python's AutoscaleController. Same HPA-shaped formula — depth and
utilisation signals, per-direction stabilisation windows, a tolerance band — plus crash
replacement, and concurrencyPerWorker is applied to the workers it spawns rather than merely
declared. Autoscaler, computeDesiredWorkers, and WorkerProcessManager are exported for
embedding.
Node request resource scope. A fresh instance on every useResource() call, each
disposed when the task ends — matching the Java scope of the same name.
Pub/sub backlog stats in the Node and Java SDKs. queue.topicStats(topic?) (Node) and
topicStats() / topicStats(topic) (Java) return the per-subscription snapshot Python has
as topic_stats(): pending, running, dead, and oldestPendingAgeMs, with a row for
every registered subscription even at zero backlog.
Job outcome events report how long the task ran: durationMs() on Java's OutcomeEvent,
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 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<TaskitoEvent>) with typed event records alongside the
unchanged worker-scoped Worker.Builder.on(...). Webhooks can subscribe to every event name.
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.
A readiness probe that can actually run. /readiness is gated alongside /metrics, so a
Kubernetes probe — which carries no credential, and cannot be given one, since a probe header is
a literal string in the manifest — got 401 and the pod never went Ready.
TASKITO_DASHBOARD_PUBLIC_READINESS=1 answers that one route without a credential; /metrics
stays gated. The Helm chart sets it by default and probes /readiness; turn it off and readiness
falls back to /health.
The attach socket admits a shared group. bind left it at the umask-derived 0755, which
denies the group write — and connect(2) needs write — so only the binding uid could attach.
It is now chmodded to 0660, which is what makes a same-pod attach configurable: two containers
can be given a shared fsGroup, where matching uids is rarely practical.
Node worker.stop() is awaitable. It now returns a promise that resolves once
worker-scoped resources have been disposed, instead of firing that teardown and returning.
Dispatch, the heartbeat and log consumers still halt synchronously, so callers that ignore
the result behave exactly as before.
Node event listeners that throw or reject are logged at debug rather than silently swallowed; sibling listeners stay isolated from the failure as before.
Postgres segfault on process exit. The connection pool opens connections on background
threads that can still be running libpq/OpenSSL when the process exits, racing OpenSSL's
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.
Overload-controls and retention release. The queue gains admission and load-shedding controls,
retention runs by default with elected cleaners, an opt-in log/cursor pub/sub model lands, and
taskito-core is prepared for standalone publication. Cross-SDK parity work and Node webhook
hardening round it out.
log topic mode writes one message row per publish (O(1))
and is consumed by pull — read, process, then ack — with per-message acknowledgement, a
first-class topic registry, and a managed consumer that tracks cursors for you.max_pending admission cap rejects enqueues past a ceiling,
opt-in CoDel load shedding drops work that has dwelled too long, and an opt-in LIFO dispatch
order serves newest-first.retry_budget caps how fast a task may retry,
independently of max_retries.taskito-core for standalone use.result_ttl is rejected rather than silently accepted (Python)./taskito without a doubled path prefix.Scaling release: the queue survives higher task ingest/dispatch and pub/sub fan-out rates, and schema management moves to code-first migrations. Worker dispatch now respects each pool's execution capacity, so a worker no longer claims more than it can run and starve its peers.
sea-query (no raw
SQL) and applied by a versioned migrator with path-based auto-discovery — dropping a
migrations/mXXXX_*.rs file registers it automatically. Migration recording is atomic and
idempotent, so concurrent startups and interruptions are safe. New scaling indexes ship as
part of the baseline.Running on itself while peers sharing the database sat idle; in-flight dispatch is
now bounded to the pool size.max_retries, and the
tracker reacts only to a node's terminal job failure — closing an exactly-once violation under
load.Pending immediately rather than waiting
out the stale-job reaper.Parity release: the Java SDK reaches feature parity with the other SDKs (and its baseline moves to Java 17), payload codecs land as a cross-SDK wire contract, and the resource system gains a bounded pooled scope plus health-check-driven recreation. One breaking change in the Node SDK: scan-heavy queue methods now return Promises.
healthCheck with automatic recreation and a
terminal unhealthy state after the recreation budget is spent. The worker heartbeat now
carries per-resource health, surfaced through listWorkers().stats*, listJobs,
deadLetters*, purge*, listWorkers, getMetrics, and getJobErrors return Promises and
no longer block the event loop; webhook Delivery was reshaped to the dashboard contract.Reliability and storage release: Postgres dequeues with SKIP LOCKED, crashed workers'
in-flight jobs are recovered within ~30s instead of waiting their full timeout, and the
dead-letter and filter paths are indexed. The internal job_payloads side table was removed
(payloads inline again). No public API or wire-format change.
FOR UPDATE SKIP LOCKED,
so concurrent workers claim disjoint jobs instead of all scanning the same
candidates and racing on the claim. dequeue_batch no longer reads payload blobs
during its candidate scan, and expired batch candidates are archived instead of
left stranded in the live table. No API or wire change.Running jobs within ~30s of the missed
heartbeat (via retry / dead-letter) instead of waiting the job's full timeout. The
scheduler claims execution under its real worker_id and a new maintenance step
reclaims orphaned claims atomically, so concurrent survivors never double-rescue.
Works across Python, Node, and Java; SQLite/Postgres/Redis. No API change.dead_letter (previously unindexed)
gains indexes on failed_at and task_name; jobs gains (task_name, status)
and partial indexes on expires_at / namespace; archived_jobs gains
created_at and namespace. Keeps DLQ, listing, and maintenance queries flat as
tables grow.job_payloads side table. Reverted the 0.15 payload side table — payload and
result live inline on jobs/archived_jobs again. The narrow dequeue scan already
excludes the blobs by column selection (Postgres TOAST / SQLite overflow keep them
off the scanned pages), so the side table bought nothing. It is dropped on
migration; the inline columns were never removed, so no data moves.Feature release across the SDKs: periodic-task management, task-scoped dead-letter queries, and the first Java SDK release.
org.byteveda:taskito. A JNI binding
over the Taskito core with the full producer / worker / inspection / admin surface, workflows,
distributed locks, periodic tasks, an in-memory taskito-test backend, and a @TaskHandler
annotation processor. Verified under GraalVM native-image.listPeriodic / deletePeriodic / pausePeriodic / resumePeriodic (Java, Node) and the
equivalent native bindings (Python). Pause toggles the task's enabled flag without removing it
(#313).listDeadByTask / purgeDeadByTask (Java), deadLettersByTask / purgeDeadByTask (Node), and
native bindings (Python). Filtering happens server-side so pagination stays correct (#314).Task.retryPolicy(...) surfaces the core retry engine's
backoff curve — exponential with jitter, or exact explicit delays — with the retry budget still
set per enqueue (#311).engines floor and tsup target were raised accordingly (#312).Maintenance release. Upgrades the Rust↔Python binding layer and refreshes project docs; no public API changes.
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.CHANGELOG.md — now also rendered on
the documentation site.Reliability patch — correctness fixes across the scheduler and storage layer, plus the missing Django admin templates. No API changes.
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.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.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.result_ttl; and job
metadata survives the dead-letter round trip.SQLITE_BUSY
deadlocks; migration ALTER TABLE failures now propagate instead of being
downgraded to warnings.FOR UPDATE, preventing concurrent workers from over-admitting beyond the
configured limit.Reliability patch — correctness fixes across the scheduler, dashboard, and Redis backend. No API changes.
retry_count, so it
is no longer dead-lettered early on its first real failure.webhooks: keys) are no longer readable through GET /api/settings.NaN/Infinity) are rejected at enqueue instead of producing invalid
JSON that broke the jobs API.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.Patch release with no user-facing changes.
Feature release: mesh scheduling, DLQ policies, and a full dashboard redesign.
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.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.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.joserfc dependency added to the oauth extras group.QueueBreakdown, formatBytes, unused
webhook queries).prefers-reduced-motion honored for
animations; aria-live on Stepper value changes; aria-pressed on
live-tail toggle; accessible name on tasks search input.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.Security-hardening release. No breaking changes.
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.admin role; viewer sessions are read-only. Session and CSRF
cookies carry Secure, and /metrics / /readiness can be gated behind
TASKITO_DASHBOARD_METRICS_TOKEN.127.0.0.1 by default. Use --host 0.0.0.0 only behind a
trusted network boundary./api/settings hides and rejects
internal auth: keys (password hashes, sessions, CSRF secret).DistributedLock.info() masks
another holder's owner_id; Redis locks set a native TTL with an atomic reap.See the Upgrading to 0.15 guide for migration steps, rolling-upgrade notes, and the downgrade floor.
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.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.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.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.SINTERCARD. No API change, no data migration.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.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.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.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./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.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.@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.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.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.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.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.@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.(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)./docs/more/examples/saga-checkout, /docs/more/examples/batch-emails, /docs/api-reference/saga, /docs/api-reference/batching.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.?-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.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 (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.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.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.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.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).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.--features workflowsAndPredicate.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.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.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.--features workflowstaskito/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..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./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.--pool prefork -- taskito worker --pool prefork --app myapp:queue now selects the prefork worker pool from the command line.validateSearch, so reloading or sharing a link preserves the user's place.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.(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./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.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.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<Vec<i32>> to Option<Vec<Option<i32>>> 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.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.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.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.visited set + max-iterations break, so an accidental cycle in a workflow definition can't loop forever.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.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.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).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.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).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.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.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.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.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.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.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._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.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.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.PrometheusMiddleware(task_filter=...) -- parity with OTelMiddleware and SentryMiddleware. A predicate (task_name: str) -> bool toggles metric export per task.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.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 historystep(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 orderingcondition="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 alwayswf.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 eventregion_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@queue.periodic(cron=...) now accepts a WorkflowProxy; launcher task is auto-registered and submits a fresh workflow run on every tickWorkflow(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 oneswf.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 runwf.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_SUBMITTED, WORKFLOW_COMPLETED, WORKFLOW_FAILED, WORKFLOW_CANCELLED, WORKFLOW_GATE_REACHED for observability hooksstep() accepts any object satisfying the HasTaskName protocol (runtime-checkable), keeping the builder API strict without coupling to a concrete TaskWrapper classcrates/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 featuredagron-core added as git dependency (https://github.com/ByteVeda/dagron.git) for DAG construction and traversalcrates/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 helperspy_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__.pymaturin 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)Swatinem/rust-cache@v2.9.1, actions/setup-node@v6 to silence Node.js 20 deprecation warningspratyush618/taskito to ByteVeda/taskitoscore = in_flight × avg_duration)dashboard/ (Preact + Vite + Tailwind CSS + TypeScript); build via cd dashboard && npm run build; output inlined into py_src/taskito/templates/dashboard.htmldashboard.py simplified to read single pre-built HTML instead of composing from 8 separate template filesScheduler::run() uses adaptive polling with exponential backoff (50ms → 200ms max); tick() returns bool for feedbackTaskDurationCache 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 SchedulerConfigqueue.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 killingqueue.workers() now returns hostname, pid, pool_type, and started_at for each worker, giving operators visibility into multi-machine deploymentsWORKER_ONLINE (registered in storage), WORKER_OFFLINE (dead worker reaped), WORKER_UNHEALTHY (resource health degraded); subscribe via queue.on_event(EventType.WORKER_OFFLINE, callback)active → draining → stopped status; shutdown signal sets status to "draining" before drain timeout, visible in queue.workers() and the dashboardlist_claims_by_worker storage method enables future orphaned job rescue when dead workers are detectedcurrent_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 progresscrates/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)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 dependenciesrun_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<String> (reaped worker IDs) instead of u64; enables WORKER_OFFLINE event emissionupdate_worker_status, list_claims_by_worker across all 3 backendsQueue(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)namespace column added to dead_letter and archived_jobs tables; DeadLetterRow, NewDeadLetterRow, ArchivedJobRow models updated; Redis DeadJobEntry uses #[serde(default)] for backward compatibilityStorage trait: dequeue, dequeue_from, list_jobs, list_jobs_filtered signatures gain namespace: Option<&str> parameter; all 3 backends + delegate macro updatedScheduler struct carries namespace: Option<String> field, passes to dequeue_from in pollerPyQueue struct carries namespace: Option<String> field; PyJob exposes namespace to Python_UNSET sentinel in mixins.py distinguishes "namespace not passed" from explicit NoneSignature.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 callbackcircuit_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 behaviorTaskFailedError 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 RuntimeErrorPyResultSender conditional export -- from taskito import PyResultSender works when built with native-async feature; silently unavailable otherwise (no confusing AttributeError)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" stringKEYS * in lock reaping -- reap_expired_locks replaced KEYS (O(N), blocks Redis server) with cursor-based SCAN using COUNT 100claim_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 missingeprintln! calls replaced with log crate macros (log::info!, log::warn!, log::error!); log dependency added to taskito-python and taskito-async cratesResultOutcome::Retry, ::DeadLettered, ::Cancelled now carry queue: String for middleware contexttarget-version updated from py39 to py310 to match requires-python = ">=3.10"Callable import from collections.abc) and B905 (zip() without strict=) lint warningscircuit_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 defaultson_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 middlewareTaskMiddleware 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 writtenJOB_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} respectivelyqueue.set_queue_rate_limit("name", "100/m") applies a token-bucket rate limit to an entire queue, checked in the scheduler before per-task limitsqueue.set_queue_concurrency("name", 10) limits how many jobs from a queue run simultaneously across all workers, checked before per-task max_concurrentEventType.WORKER_STARTED and EventType.WORKER_STOPPED fired when a worker thread comes online or exits; subscribe via queue.on_event(EventType.WORKER_STARTED, cb)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 volumequeue.add_webhook() now accepts max_retries, timeout, and retry_backoff per endpoint, replacing the previous hardcoded valuesOpenTelemetryMiddleware adds span_name_fn, attribute_prefix, extra_attributes_fn, and task_filter parametersSentryMiddleware adds tag_prefix, transaction_name_fn, task_filter, and extra_tags_fn parametersPrometheusMiddleware and PrometheusStatsCollector add namespace, extra_labels_fn, and disabled_metrics parameters; metrics grouped by category ("jobs", "queue", "resource", "proxy", "intercept")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/queuesTaskito(app, cli_group="tasks") renames the CLI command group; flask taskito info --format json outputs machine-readable statsTASKITO_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 addressmax_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 workersserializer on @queue.task() -- per-task serializer override; falls back to queue-level serializer_deserialize_payload(task_name, payload) instead of cloudpickle directlyon_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 calledQUEUE_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 firedQueue(scheduler_poll_interval_ms=N, scheduler_reap_interval=N, scheduler_cleanup_interval=N) exposes the three Rust scheduler timing knobs to Pythonasync 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 poolasync_concurrency parameter -- Queue(async_concurrency=100) caps concurrent async tasks on the event loop; independent of the workers (sync thread) countcurrent_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 contexttaskito 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 probesdeploy/keda/ contains ready-to-use ScaledObject, ScaledObject (Prometheus), and ScaledJob YAML manifestsinterception="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@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)@queue.task(inject=["name"]) or db: Inject["name"] annotation syntax injects live resources into tasks without serializing them; from taskito import Injectdepends_on=["other"] on @queue.worker_resource(); topological initialization order, reverse teardown; cycles detected eagerly at registration time (CircularDependencyError)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 checkspool_size, pool_min, acquire_timeout, max_lifetime, idle_timeout; pool_min > 0 pre-warms instances at startupscope="thread" creates one instance per worker thread via ThreadLocalStore, torn down on shutdownfrozen=True wraps the resource in a FrozenResource proxy that raises AttributeError on attribute writesreloadable=True marks a resource for reload on SIGHUP; taskito reload --app myapp:queue CLI subcommand; queue._resource_runtime.reload() programmatic reloadqueue.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 tomlifile, logger, requests_session, httpx_client, boto3_client, gcs_clientrecipe_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 directlyqueue.register_type(MyType, "redirect", resource="my_resource") registers custom types with any strategy (requires interception enabled)queue.interception_stats() returns total calls, per-strategy counts, average duration, and max depth reachedqueue.proxy_stats() returns per-handler deconstruction/reconstruction counts, error counts, and average durationqueue.resource_status() returns per-resource health, scope, init duration, and recreation countqueue.test_mode(resources={"db": mock_db}) injects mocks during test mode without worker startup; MockResource(name, return_value=..., wraps=..., track_calls=True) adds call trackingpip install taskito[aws] adds boto3>=1.20; pip install taskito[gcs] adds google-cloud-storage>=2.0queue.lock() / await queue.alock() context managers with auto-extend background thread, acquisition timeout, and cross-process support; LockNotAcquired exception for failed acquisitionsclaim_execution / complete_execution storage layer prevents duplicate task execution across worker restartsAsyncWorkerPool with spawn_blocking and GIL management; WorkerDispatcher trait in taskito-core future-proofs for other language bindingsqueue.pause(), queue.resume(), queue.paused_queues() to suspend and restore processing per named queuequeue.archive() moves jobs to a persistent archive; queue.list_archived() retrieves themqueue.purge() removes jobs by filter; queue.revoke_task() prevents all future enqueues of a given task namequeue.replay() re-enqueues a completed or failed job; queue.replay_history() returns the replay logcircuit_breaker={"threshold": 5, "window": 60, "cooldown": 120} on @queue.task(); queue.circuit_breakers() returns current state of all circuit breakerscurrent_job.log(message) from inside tasks; queue.task_logs(job_id) and queue.query_logs() for retrievaltimezone="America/New_York" on @queue.periodic(); uses chrono-tz under the hood, defaults to UTCretry_delays=[1, 5, 30] on @queue.task() for per-attempt delay overrides instead of exponential backoffsoft_timeout= on @queue.task(); checked cooperatively via current_job.check_timeout()tags=["gpu", "heavy"] on queue.run_worker(); jobs can be routed to workers with matching tagsqueue.workers() / await queue.aworkers() return live worker statequeue.job_dag(job_id) returns a dependency graph for a job and its ancestors/descendantsqueue.metrics_timeseries() returns historical throughput/latency data; queue.metrics() for current snapshotqueue.list_jobs_filtered() with metadata_like, error_like, created_after, created_before parametersMsgPackSerializer — built-in, requires pip install msgpack; faster than cloudpickle, smaller payloads, cross-language compatibleEncryptedSerializer — AES-256-GCM encryption, requires pip install cryptography; wraps another serializer, payloads in DB are opaque ciphertextdrain_timeout — configurable graceful shutdown wait time on Queue() constructor (default: 30 seconds)result_ttl — result_ttl override on .apply_async() to set cleanup policy per jobpip install taskito[redis]); Lua scripts for atomic operations, sorted sets for indexingPrometheusStatsCollector_poll_once so result() raises immediatelyasyncio.get_event_loop() with get_running_loop()KEYS with SCAN in purge operationsenqueue_unique() race condition with atomic Lua scriptsafter() for those whose before() succeededEncryptedSerializer key type and size before usepip install taskito[postgres]); full feature parity with SQLiteTASKITO_BACKEND, TASKITO_DB_URL, TASKITO_SCHEMA settings for configuring the backend from Django projects/logs and /replay-history handlers above the generic catch-all in dashboard.py, fixing 404s on these endpoints__version__ — Replaced hardcoded version with importlib.metadata.version() with fallbackretry_dead non-atomic — Wrapped enqueue + delete in a single transaction (SQLite & Postgres), preventing ghost dead letters on partial failureenqueue_unique race condition — Wrapped check + insert in a transaction; catches unique constraint violations to return the existing job instead of erroringnow_millis() panic — Replaced .expect() with .unwrap_or(Duration::ZERO) to prevent scheduler panic on clock issuesreap_stale double error records — Removed redundant storage.fail() call; handle_result already records the failurereadme field to pyproject.toml so PyPI displays the project description.Re-release of 0.2.0 — PyPI does not allow re-uploads of deleted versions.
TaskitoError base class with TaskTimeoutError, SoftTimeoutError, TaskCancelledError, MaxRetriesExceededError, SerializationError, CircuitBreakerOpenError, RateLimitExceededError, JobNotFoundError, QueueErrorCloudpickleSerializer (default), JsonSerializer, or custom Serializer protocolretry_on and dont_retry_on parameters for selective retriesqueue.cancel_running_job() and current_job.check_cancelled()soft_timeout parameter with current_job.check_timeout() for cooperative time limitsTaskMiddleware base class with before(), after(), on_retry() hooksqueue.workers() / await queue.aworkers() to monitor worker healthexpires parameter on apply_async() to skip time-sensitive jobs that weren't started in timeresult_ttl parameter on apply_async() to override global cleanup policy per jobchunks(task, items, chunk_size) and starmap(task, args_list) canvas primitivesmax_concurrency parameter on group() to limit parallel executionOpenTelemetryMiddleware for distributed tracing; install with pip install taskito[otel]taskito dashboard --app myapp:queue serves a built-in monitoring UI with dark mode, auto-refresh, job detail views, and dead letter managementTaskitoRouter provides a pre-built APIRouter with endpoints for stats, job status, progress streaming (SSE), and dead letter managementqueue.test_mode() context manager for running tasks synchronously without a workertaskito dashboard command with --host and --port optionsawait job.aresult() for non-blocking result fetchingpython/ to py_src/ and rust/ to crates/ for clearer project structuredb_path now uses .taskito/ directory, with automatic directory creationInitial release
@queue.task() decorator with .delay() and .apply_async()"N/s", "N/m", "N/h" syntaxchain, group, and chord primitivescurrent_job.update_progress() from inside taskstask.map() and queue.enqueue_many() with single-transaction insertsbefore_task, after_task, on_success, on_failurearesult(), astats(), arun_worker(), and morecurrent_job.id, .task_name, .retry_count, .queue_namejob.errorstaskito worker and taskito info --watch