Releases: microsoft/pg_durable
Release list
Release v0.2.4-rc1
Release candidate for v0.2.4. This is a pre-release for validation only — not a final release. The notes below describe the full v0.2.4 changes.
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_filter, keyset pagination (after_cursor/next_cursor), andcreated_at/updated_attimestamps to the result set.
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-rc1
Release v0.2.3
What's new in v0.2.3
Provider-line note: v0.2.3 stays in the duroxide-pg provider compatibility line started in v0.2.2, so the upgrade source is v0.2.2 (sql/pg_durable--0.2.2--0.2.3.sql).
Added
- Debian release packages: AMD64
.debpackages for PostgreSQL 17 and 18, built and validated by the Package Release workflow on tagged releases (#190, #203). - Public Docker images:
ghcr.io/microsoft/pg_durableimages are published from the released.debpackages for PG 17 and 18. These images are for evaluating and learning pg_durable only - not for production (#218, #222, #223).
Changed
- duroxide provider schema: fresh installs now use
_duroxideas the duroxide-pg provider schema, while installations upgraded from earlier versions keep the legacyduroxideschema. The active schema is resolved at runtime viadf.duroxide_schema(), so the change is transparent to existing deployments (#201). - Default worker role: the background worker's default role is now
postgresinstead ofazuresu(#206). df.break()internals:df.break()now carries its value as a typedNodeErrorinstead of a JSON sentinel, with a compatibility fallback for envelopes written before #148 (#229).- JSON conversion: internal SQL-to-JSON value conversion now goes through
try_from_json()for more robust error handling (#235). - Dependencies: bumped
reqwestto 0.13 to match the lockfile (#237) and updated five crates in the cargo dependency group (#236). Added Dependabot for weekly cargo updates (#231).
Fixed
- Reliability audit: fixed a set of correctness and safety bugs found during a reliability audit (#220):
df.if()/df.loop()conditions whose SQL returns zero rows now correctly evaluate as false instead of true (previously the empty result envelope was treated as truthy).- Graphs nested deeper than 256 levels are now rejected, preventing stack overflow from deeply nested operator chains.
- Graphs with more than 10,000 nodes are now rejected, preventing unbounded INSERT storms and out-of-memory conditions.
- Per-user SQL connections now have a 30-second connect timeout, so a stalled connection can no longer hold an execution slot indefinitely.
- Non-finite floats: SQL columns containing
NaNorInfinitynow map to JSONnullinstead of failing the workflow (#144). - Execution-history errors:
df.instance_executionsnow surfaces execution-history lookup failures instead of silently hiding them (#225, closes #168).
Security
- Docker/GHCR hardening: hardened the published Docker image and the GHCR publish workflow, including least-privilege permissions and provenance/SBOM attestations on published images (#223).
Documentation
- Clarified
df.http()security scope versus SQL extension execution (#216). - Corrected stale identity-model documentation and examples (#219, #224).
- Added the documentation website, refreshed the README, and standardized terminology to "durable functions" (#198, #204, #205, #207, #208, #211).
- Referenced the
pg_durable.databaseGUC instead of thePGDATABASEenvironment variable (#200).
New Contributors
- @tjgreen42 made their first contribution in #193
- @AbeOmor made their first contribution in #194
- @darkdevil3610 made their first contribution in #211
- @iemejia made their first contribution in #219
- @crprashant made their first contribution in #225
Full Changelog: v0.2.2...v0.2.3