How df.cancel() Interacts with the Duroxide Runtime in pg_durable: Cancellation Flow Explained
When you invoke df.cancel() in pg_durable, a PL/pgSQL wrapper calls the Rust cancel_durable_function in src/client.rs, which uses a cached Tokio runtime to request a graceful Duroxide worker shutdown, and then updates df.instances.status to 'cancelled'.
In the microsoft/pg_durable extension for PostgreSQL, stopping a running durable orchestration requires precise coordination between SQL, Rust, and the Duroxide background worker. The df.cancel() function serves as the user-facing entry point that triggers this cross-language cancellation flow. Understanding how df.cancel() interacts with the Duroxide runtime helps operators safely terminate orchestrations without leaving stale state.
SQL Wrapper and Ownership Check
The df.cancel(instance_id TEXT, reason TEXT DEFAULT 'Cancelled by user') signature is defined in the extension’s SQL catalog. When executed, a PL/pgSQL wrapper first verifies that the calling role owns the target instance, then calls into the Rust side of pg_durable. According to the source code documented in docs/rls.md, this ownership check is critical because the subsequent UPDATE runs under the caller’s role and relies on Row-Level Security (RLS) filtering; non-owned rows silently affect zero rows.
Rust Client Call in src/client.rs
After the ownership check passes, PostgreSQL dispatches to the Rust-implemented function cancel_durable_function exposed via pgrx. Inside src/client.rs, this function obtains the cached Tokio runtime via get_client_runtime() and the cached Duroxide client via get_duroxide_client(). It then blocks on an asynchronous request to client.cancel_instance(instance_id, reason), returning a formatted error string to PostgreSQL if the Duroxide request fails.
// Inside src/client.rs – the Rust side that the PL/pgSQL wrapper calls
pub fn cancel_durable_function(instance_id: &str, reason: &str) -> Result<(), String> {
let rt = get_client_runtime();
let client = get_duroxide_client()?;
rt.block_on(async {
client
.cancel_instance(instance_id, reason)
.await
.map_err(|e| format!("Failed to cancel durable function: {e:?}"))?;
Ok(())
})
}
Duroxide Worker Graceful Shutdown
Once the Rust client forwards the request, the Duroxide background worker performs a graceful shutdown of the target orchestration. The worker stops any pending timers, cancels currently running activities, and propagates the cancellation signal to any child instances. This ensures that the entire durable function tree terminates cleanly before PostgreSQL finalizes the row state.
RLS Bypass and Final Status Update
After the Duroxide client call returns, the PL/pgSQL wrapper executes UPDATE df.instances SET status = 'cancelled' through PostgreSQL’s SPI interface. Because the Duroxide worker operates as a super-user-level worker role, it bypasses RLS policies that would otherwise restrict the calling role. If the instance was already terminal or not owned by the caller, the UPDATE affects zero rows and the preceding client call is effectively a no-op, making df.cancel() idempotent.
Using df.cancel() in Practice
Users interact with the flow entirely through SQL, with user-facing semantics documented in docs/api-reference.md. A standard call targets a running instance by its unique ID, while repeated calls against a finished instance leave the status unchanged. End-to-end coverage in tests/e2e/sql/03_loops.sql verifies that the cancellation is reflected instantly in df.instances.status, and the function returns the instance ID on success or an instance not found or access denied message when checks fail.
-- Cancel a specific orchestration
SELECT df.cancel('a1b2c3d4', 'Manual stop');
-- Cancelling an already-completed instance is a no-op (status unchanged)
SELECT df.cancel('finished-id', 'Ignored reason');
Summary
df.cancel()is a PL/pgSQL wrapper that bridges SQL, Rust insrc/client.rs, and the Duroxide background worker.- The Rust function
cancel_durable_functionforwards requests through a cached Tokio runtime and Duroxide client. - The Duroxide worker performs a graceful shutdown of timers, activities, and child instances.
- A final
UPDATEondf.instancespersistsstatus = 'cancelled'and is protected by a worker-role RLS bypass. - Ownership checks make the operation idempotent for non-owned or already-terminal orchestrations.
Frequently Asked Questions
What happens if I call df.cancel() on an orchestration I do not own?
The ownership check causes the subsequent UPDATE df.instances to affect zero rows, and the earlier Duroxide client call becomes a no-op because the instance is filtered out. The function returns an instance not found or access denied message without modifying any state.
Is df.cancel() idempotent for already-completed instances?
Yes. Invoking df.cancel() on an instance that is already terminal is a safe no-op; the status remains unchanged and the Duroxide worker handles the redundant request gracefully.
Which PostgreSQL role performs the final df.instances status update?
The Duroxide background worker runs as a dedicated worker role with elevated privileges, allowing it to bypass Row-Level Security policies and persist the status = 'cancelled' update even when the calling role would normally be restricted.
How does the Duroxide runtime stop in-flight activities during cancellation?
The Duroxide worker performs a graceful shutdown that stops pending timers, cancels currently running activities, and propagates the cancellation signal downward to any child instances in the orchestration tree.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →