All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Bumped
duroxide-pgfrom0.1.36to0.1.37.
- Bumped
duroxidedependency —0.1.30→0.1.32(race fixes and shutdown, below).0.1.31was reserved and never published. - Bumped
duroxide-pgdependency —0.1.35→0.1.36. Under heavy load, an orchestration fetched from PostgreSQL could come with a lock that had already expired. Every ack then failed withInvalid lock token, and the message was retried for as long as the load lasted. The lock now starts when the instance is claimed (migration0025). An SDK version built withduroxide-pg0.1.35 refuses a database that has this migration. runtime.shutdown(timeout_ms)returns as soon as the runtime stops.timeout_ms(default 1000) is now the longest it waits for in-flight work before cancelling the rest, not a fixed delay. Shutdown also stops every task the runtime started, so no poller keeps a database connection open after it returns.- A signal sent to a dropped
wait_for_eventis discarded. When await_for_eventloses actx.race(), a signal already given to it is no longer handed to a later wait for the same event. The later wait receives the next signal. Before, it could receive the stale value, fail replay, or wait forever. - A duplicate start does not change a running instance.
client.start_orchestration()for an instance that already has history is ignored; it can no longer replace the instance's orchestration name, version or input. A sub-orchestration whose instance id collides with a running instance now fails its parent, as it already did for a finished one.
- A queue read that loses a race takes no message. A drain loop that races
ctx.dequeue_event()against a short timer could fail withnondeterministic: history schedule but no emitted action, skip a message, or read one twice when the timer won and a message arrived in the same batch. A dropped read is now cancelled where it is dropped, so replay picks the same winners as the first run. continue_as_newcarries exactly the unread messages. A dropped or still-pending read counted as a consumer, so a message could be lost at continue-as-new. The runtime now carries every message that no read returned (at most 100, as before).- Inputs that arrive while an instance continues as new are kept. Queue messages, signals and cancellation requests that arrived between two executions could be deleted. They now go to the next execution.
ctx.get_kv_all_keys()andctx.prune_kv_values_updated_before()use key order. Replay could fail withnondeterministic: kv clear mismatchafter a prune.
The queue, signal and continue-as-new changes apply to executions started by duroxide 0.1.32
or later. An execution started by an older runtime keeps the old behaviour until it completes or
continues as new. See the duroxide migration guide for details and rolling upgrades.
tests/test_races.py— drain loops that racectx.dequeue_event()against a timer: every message arrives once and in order, also acrosscontinue_as_new.tests/test_kv_store.py— KV keys are listed and pruned in key order, and a replayed prune matches.
- Breaking:
ctx.race()andctx.race_typed()raise when the winner failed. Before, a winning activity or sub-orchestration that failed came back like a success:{"index": ..., "value": ...}, with the error text invalue. Nothing marked it as an error. Now theyieldraises the same exception as yielding that task on its own: same type, same message. If the orchestration does not catch it, the orchestration fails.- Winners that succeeded, timers, events and dequeued messages return
{"index": ..., "value": ...}as before. - Code that read the error text from
valuemust catch the exception instead. - A race over a task type that
race()does not support now raisesunsupported task in select. Before, it returned that text as the value. - Instances in flight: if a history already holds a race won by a failed task, the replay now raises at that race. If the code then takes a different path than the first run did, the instance fails. Upgrade when no such instance is running.
- Winners that succeeded, timers, events and dequeued messages return
worker_lock_renewal_buffer_ms,session_lock_timeout_ms,session_lock_renewal_buffer_msruntime options — map toworker_lock_renewal_buffer,session_lock_timeoutandsession_lock_renewal_bufferof the Rust runtime. Keyword arguments ofRuntimeOptions, added after the existing ones. With these, every lock timeout and renewal buffer of the runtime can be set from Python.tests/test_lock_options.py— checks that every lock option reaches the runtime, from the renewal intervals the runtime logs.
- Replays of the same instance no longer share a native context. The synchronous
context calls (
ctx.set_kv_value/ctx.get_kv_valueand the other KV calls,ctx.set_custom_status/ctx.get_custom_status, andctx.trace_*) found the RustOrchestrationContextby instance ID. Two replays of one instance can be alive in the same process: a replay that lost its orchestration lock (for example after the process stalled for longer than the lock timeout) keeps running until its commit is rejected, and another dispatcher slot can fetch the instance again. Calls from one replay then reached the other. The replay that held the lock could commit events it never produced, and the next replay failed withnondeterministic: kv set mismatchornondeterministic: schedule mismatch. The context is now looked up by a token that is new for every invocation, the same way activity contexts already work.
- Bumped
duroxide-pgdependency —0.1.34→0.1.35.
orchestrator_lock_timeout_ms,orchestrator_lock_renewal_buffer_ms,max_attemptsruntime options — map toorchestrator_lock_timeout,orchestrator_lock_renewal_bufferandmax_attemptsof the Rust runtime. Keyword arguments ofRuntimeOptions, added after the existing ones.tests/test_replay_isolation.py— regression tests that stop the process in the middle of a replay for longer than the orchestration lock timeout (POSIX only).
- Stable ABI wheels — Publish
cp39-abi3wheels so each platform wheel supports Python 3.9 through 3.14.
- Bumped
duroxidedependency —0.1.29→0.1.30.
- Bumped
duroxide-pgdependency —0.1.32→0.1.34 - Updated PostgreSQL Rust bindings to construct providers through the new
ProviderConfig::url(...)/ProviderConfig::entra(...)plusPostgresProvider::new_with_config(config)initialization API. - Kept
duroxidedependency pinned at0.1.29.
PostgresEntraOptions— new Python class for tuning Microsoft Entra ID (Azure AD) authentication: keyword-only optional fieldsaudience,max_connections,acquire_timeout_ms, andrefresh_interval_ms.PostgresProvider.connect_with_entra(host, port, database, user, options=None)— connect to Azure Database for PostgreSQL Flexible Server via Entra token auth. Tokens are fetched and refreshed automatically via theDefaultAzureCredentialchain (managed identity, environment variables, Azure CLI, etc.).PostgresProvider.connect_with_schema_and_entra(host, port, database, user, schema, options=None)— same as above with a custom schema for multi-tenant isolation.
- Bumped
duroxidedependency —0.1.28→0.1.29 - Bumped
duroxide-pgdependency —0.1.30→0.1.32
- Linux ARM64 wheel - Build and publish a manylinux
aarch64(aarch64-unknown-linux-gnu) wheel sopip install duroxideworks in Linux ARM64 containers without compiling from source. - Linux ARM64 CI smoke - Run local wheel and post-publish registry smoke
on
ubuntu-24.04-arm.
- Linux wheel build — Install
openssl-devel(manylinux_2_28) /libssl-dev(Debian) in thebefore-script-linuxstep so the newnative-tlsbackend can find OpenSSL via pkg-config. Previous attempt (0.1.21) tagged but never published because every Linux wheel job failed with "Could not find openssl via pkg-config".
- Bumped
duroxidedependency —0.1.27→0.1.28(drops transitiveringcrate via SQLx native-tls upstream) - Bumped
duroxide-pgdependency —0.1.29→0.1.30(samering-drop cascade) - No source changes in the Python SDK; pure dependency uplift
⚠️ Do not use 0.1.21 — never published to PyPI. Use 0.1.22.
- Cross-platform packaging smoke tests in
ci/smoke/that install the built wheel on ubuntu, macos-14, macos-13, and windows-latest before publish, plus a post-publish registry smoke. Publish is gated on all-platform smoke success.
- Pinned
manylinux_2_28with an explicitpython3.12interpreter for the Linux wheel build (upstreammanylinux: autoregressed).
- Bumped
duroxide-pgfrom 0.1.28 to 0.1.29. - Fixes migration race condition on concurrent startup (microsoft/duroxide#10).
- Cached plan invalidation (
0A000) now retryable.
client.get_orchestration_stats(instance_id)exposing per-instance history, queue carry-forward, and KV usage counters.
- Bumped
duroxideto 0.1.27 andduroxide-pgto 0.1.28. - Updated exported KV limits to
MAX_KV_KEYS = 150andMAX_KV_VALUE_BYTES = 65536.
- Bumped
duroxideto 0.1.26,duroxide-pgto 0.1.27. - KV delta table support: fixes read-modify-write replay poisoning by separating current-execution mutations from prior-execution snapshots.
- KV read-modify-write counter e2e test (
sample_kv_read_modify_write_counter).
- Bumped
duroxidedependency to 0.1.25. - Patched local builds to use the sibling
../../providers/duroxide-pgcheckout until a crates.io release includes the KV snapshot changes required byduroxide0.1.25. - Renamed KV APIs to the new
kv_-prefixed surface:ctx.set_kv_value(),ctx.get_kv_value(),ctx.clear_kv_value(),ctx.clear_all_kv_values(),ctx.get_kv_value_from_instance(),client.get_kv_value(), andclient.wait_for_kv_value(). - Raised
MAX_KV_KEYSfrom 10 to 100.
- New orchestration KV helpers:
ctx.get_kv_all_values(),ctx.get_kv_all_keys(),ctx.get_kv_length(), andctx.prune_kv_values_updated_before(cutoff_ms). - New typed client conveniences:
client.get_kv_value_typed()andclient.wait_for_kv_value_typed().
- KV store support: Durable key-value store for per-instance state
OrchestrationContext:set_value(),get_value(),clear_value(),clear_all_values()Client:get_value(),wait_for_value()- Constants:
MAX_KV_KEYS,MAX_KV_VALUE_BYTES
- Added KV e2e tests (
test_kv_store.py) - Bumped duroxide to 0.1.24, duroxide-pg to 0.1.25
- 5 new e2e tests: heterogeneous workers pipeline, starvation-safe tagged activity fallback, dual runtime tag cooperation, nested error handling propagation, error recovery with logging.
MAX_WORKER_TAGS(5) andMAX_TAG_NAME_BYTES(256) constants exported.
- Bumped
duroxidecore from 0.1.22 to 0.1.23 (activity tag ack validation test). - Bumped
duroxide-pgfrom local path to published 0.1.24 (tag routing + migration fixes).
- Typed APIs parity pass: typed scheduling/wait/event/dequeue variants and typed-path tests.
- Async block and advanced-feature parity suites aligned with Node/Java/.NET structure.
- Unified schedule/whenAll/whenAny terminology across docs and examples.
- Rust bridge and SDK parity hardening updates for cross-SDK consistency.
ctx.get_custom_status()— read the current custom status value from within an orchestration. Returns the status string orNoneif none has been set. Reflects allset_custom_status/reset_custom_statuscalls, including across turn boundaries and continue-as-new.
- Upgraded duroxide to 0.1.20, duroxide-pg to 0.1.22
- Custom Status documentation — user guide and architecture docs for
ctx.set_custom_status(),ctx.reset_custom_status(), andclient.wait_for_status_change() - Event Queues documentation — user guide and architecture docs for
ctx.dequeue_event()andclient.enqueue_event() - Retry on Session documentation — user guide docs for
ctx.schedule_activity_with_retry_on_session() - Copilot Chat pattern — real-world example combining event queues, custom status, and continue-as-new for interactive chat bots
- Updated README with custom status, event queues, and retry-on-session API examples
- Bumped
duroxidedependency to 0.1.19
client.raise_event_persistent()— useclient.enqueue_event()insteadctx.schedule_wait_persistent()— usectx.dequeue_event()instead
- Custom Status API —
ctx.set_custom_status(status)andctx.reset_custom_status()for setting/clearing custom status from within orchestrations (fire-and-forget, no yield needed) - Custom Status on OrchestrationResult —
custom_statusandcustom_status_versionfields now available on all status results (get_status,wait_for_orchestration,wait_for_status_change) client.wait_for_status_change(instance_id, last_seen_version, poll_interval_ms, timeout_ms)— polls for custom status changes on an orchestration instance- Event Queue API —
ctx.dequeue_event(queue_name)for FIFO mailbox-style event consumption in orchestrations, paired withclient.enqueue_event(instance_id, queue_name, data)for sending messages ctx.schedule_activity_with_retry_on_session(name, input, retry, session_id)— retry with session affinity (all attempts pinned to the same worker)- Queue event kinds (
QueueSubscribed,QueueEventDelivered) now included inread_execution_historyevent data
- Bumped
duroxidedependency to 0.1.19 - Bumped
duroxide-pgdependency to 0.1.21
init_tracing(log_file, log_level?, log_format?)— install a file-based tracing subscriber beforeruntime.start(). Uses first-writer-wins (try_init) so the runtime's built-in subscriber silently no-ops if one is already installed. Supports"json","pretty", and"compact"(default) log formats.- 3 new tests for
init_tracing: import check, file write, invalid path error
- Reverted premature tracing init change from 0.1.4 (library handles this correctly)
datafield on history events (read_execution_history) — exposes activity results, inputs, errors, timer fire times, and all event-specific content as JSON strings- Execution history example in README Admin APIs section
- New test:
test_read_execution_history_data
ctx.get_client()on ActivityContext — activities can now start orchestrations, raise events, etc.runtime.metrics_snapshot()— get runtime metric counters (orchestration starts/completions, activity results, provider errors)- Observability options on
PyRuntimeOptions:log_level,log_format,service_name,service_version - 5 new e2e tests: retry exhaustion, continue-as-new version upgrade, version routing, activity get_client, metrics snapshot (54 total)
- README doc links now use absolute GitHub URLs so they work on PyPI
- Generator-based orchestration API with deterministic replay
- Activity support with cooperative cancellation (
ctx.is_cancelled()) all()(fan-out/fan-in) andrace()(select) composition primitives- Durable timers (
schedule_timer) and external events (wait_for_event) - Sub-orchestrations (blocking and fire-and-forget)
- Continue-as-new for eternal orchestrations
- Versioned orchestration registration
- Deterministic
utc_now()andnew_guid() - SQLite provider (in-memory and file-based)
- PostgreSQL provider via duroxide-pg-opt (long-polling with LISTEN/NOTIFY)
- Schema isolation for PostgreSQL (
connect_with_schema) - Full admin API: list instances, get info, executions, history, tree, delete, prune
- Structured tracing via Rust
tracingcrate (controlled byRUST_LOG) - Activity retry with configurable backoff policy
- Decorator-based registration (
@runtime.register_activity,@runtime.register_orchestration) - Runtime with configurable concurrency and poll intervals
- 49 tests across e2e, races, admin API, and scenario suites
- Documentation: user guide, architecture guide