Provider-line note: v0.2.4 stays in the duroxide-pg provider compatibility line, so the upgrade source is v0.2.3 (sql/pg_durable--0.2.3--0.2.4.sql).
Added
- Instance retention/pruning (#265): terminal instances are now pruned by a hard cap and a retention window, bounding unbounded growth of
df.instances. df.list_instances()pagination & filtering (#278): added alabel_filterand a paginated overload with keyset pagination (after_cursor/next_cursor) that also returnscreated_at/completed_attimestamps and anext_cursorcolumn.
Changed
-
df.wait_for_schedule()cron timing: the next cron tick is now computed at execution time using duroxide's deterministic clock (ctx.utc_now()) inside theexecute_function_graphorchestration, instead of being pre-computed atdf.start()time. This makes recurring@>schedules and any start-to-execution delay target the correct upcoming tick (#130).⚠️ Replay-breaking for in-flightwait_for_scheduleinstances. This change adds a recordedutc_now()decision before the WAIT_SCHEDULE timer, altering the orchestration's history sequence. Any durable function that was started under a<= 0.2.3binary and is mid-wait_for_schedule(parked on its timer) when this.sois loaded will fail with a duroxide nondeterminism error on replay, because its recorded history no longer matches the new code. Drain or allow such in-flightwait_for_scheduleinstances to complete before upgrading. Instances that are not currently inside await_for_schedulenode are unaffected. We accepted this break (rather than introducing orchestration versioning) given the early pre-1.0 stage of the project. -
Instance/node ID collision hardening (#129):
df.start()now reserves IDs withINSERT ... ON CONFLICT DO NOTHING RETURNING idand re-rolls the random 8-hex value on collision — instances arbitrate on thedf.instancesprimary key (id), nodes on the new compositePRIMARY KEY (instance_id, id)— replacing the previousSELECT EXISTSpre-check. Doing the conflict check at the index level (rather than a pre-checkSELECT) closes a TOCTOU window and, for instances, an RLS blind spot where the pre-check could not see another role's rows.df.nodesnow uses the compositePRIMARY KEY (instance_id, id)instead of a global single-column key, so the random 8-hex node ID is no longer the sole cross-instance collision guard. Theupdate-node-statusactivity now scopes itsdf.nodesupdate byinstance_id(a required activity-input field) and asserts it affects exactly one row. IDs stayVARCHAR(8)HEX; the0.2.3 → 0.2.4upgrade restructures thedf.nodeskeys in place (#238).- Breaking for in-flight work: the new activity-input shape changes the string duroxide records in orchestration history, and duroxide validates activity inputs by exact equality on replay, so any instance left in flight across the 0.2.3 → 0.2.4 binary upgrade cannot complete. Drain or cancel in-flight instances before deploying 0.2.4. The in-place
df.nodeskey restructure also takes anACCESS EXCLUSIVElock whose duration scales with table size — run the upgrade in a maintenance window. See the #129 section ofdocs/upgrade-testing.mdfor the full drain-before-upgrade contract.
- Breaking for in-flight work: the new activity-input shape changes the string duroxide records in orchestration history, and duroxide validates activity inputs by exact equality on replay, so any instance left in flight across the 0.2.3 → 0.2.4 binary upgrade cannot complete. Drain or cancel in-flight instances before deploying 0.2.4. The in-place
-
df.grant_usage()/df.revoke_usage(): dropped the explicit per-functionEXECUTEallowlist. SchemaUSAGEondfis the real access gate for ordinarydf.*functions, so the helpers now grant/revoke schemaUSAGE, the table privileges, andEXECUTEonly on the sensitive functions (df.http,df.grant_usage,df.revoke_usage). Function signatures are unchanged and existing privileges are unaffected (#242). -
df.list_instances()page-size cap is now a loud error (#146):df.list_instances()previously truncatedlimit_countsilently to a fixed ceiling of 10000. It now raises an error whenlimit_countexceeds the newpg_durable.list_instances_max_limitGUC (SUSETcontext, default1000, range1–1000000), so an over-cap request fails fast instead of returning a silently short page that is indistinguishable from "no more rows". Both the basic and paginated overloads enforce the cap; clients needing more rows should lowerlimit_countor use the paginated overload (after_cursor/next_cursor). A superuser can change the cap at runtime without a restart; by default an ordinary caller cannot. -
Renamed
df.wait_for_completion()(#164): the function was renamed and hardened against unsafe use. Breaking: callers of the old name must update to the new name. -
Node statuses derived from execution lineage (#263, #283): node status is now derived from the durable engine's execution lineage, reconciling the
dfcontrol-plane with the engine so reported statuses match actual execution. -
df.start()fails fast on engine hand-off failure (#282): if the hand-off to the durable engine fails,df.start()now returns an error immediately instead of leaving a stuck instance behind. -
Dependencies: bumped
reqwestto 0.13.4 (#260) anduuidto 1.23.4 (#273).
Fixed
explainrace branches (#276): race (|) branches now render correctly indf.explain()output.- Loop safety (#254):
df.loop()now enforces a max-iteration guard and detects malformed loop configuration instead of looping unboundedly or misbehaving. $name.*expansion cap (#255): a row-count limit (10,000) is now enforced when expanding$name.*, preventing unbounded expansion.df.http()User-Agent (#270): requests now send a defaultUser-Agentheader.- Connection reliability (#251, #252): the client is now recoverable after a connection failure, and epoch/extension polling is isolated onto a dedicated connection pool so it can no longer contend with execution work.
df.list_instances()N+1 (#275): instance-info lookups are now batched, removing an N+1 query pattern.- Indexes (#271): added a
created_atindex and a composite status index ondf.instancesto speed up listing and status queries.
Security
- SSRF CGNAT range (#253): the
100.64.0.0/10CGNAT range is now blocked by SSRF protection indf.http(). df.metrics()access (#184):df.metrics()is now gated behind an explicitEXECUTEgrant rather than being callable by default.
Removed
df.debug_connection(): removed from the SQL surface as non-security, surface-reduction cleanup (#110). The function returned the worker connection string (postgres://role@host:port/db) with no password or credential, and the worker role is already visible through native PostgreSQL channels (the world-readablepg_durable.worker_roleGUC andpg_stat_activity.usename) — so issue #110 is reclassified from a security finding to cleanup. Fresh installs no longer create the function and the0.2.3 → 0.2.4upgrade drops it; a binary-compatibility shim retains the underlying C symbol so pre-0.2.4 schemas keep resolving the function untilALTER EXTENSION pg_durable UPDATEruns.
Documentation
- Documented
SECURITY DEFINER df.start()behavior (#185), corrected documentation examples (#257), and clarified thatdf.status()/df.result()take aninstance_idrather than a label (#167).
Acknowledgements
Thanks to everyone who contributed to this release: @crprashant @iemejia @nanookclaw @pinodeca @Saithej2k @snvtac @tjgreen42.
New Contributors
- @snvtac made their first contribution in #262
- @nanookclaw made their first contribution in #265
- @Saithej2k made their first contribution in #276
Full Changelog: v0.2.3...v0.2.4