How pg_durable Initializes the Duroxide Runtime at Startup via Its Background Worker
The pg_durable extension registers a PostgreSQL background worker in _PG_init that spins up a single-threaded Tokio runtime and, through the initialize_duroxide_runtime function in src/worker.rs, constructs a PostgreSQL-backed store and starts the Duroxide state-machine engine.
The pg_durable extension by Microsoft orchestrates the Duroxide runtime entirely through a dedicated PostgreSQL background worker. At startup, the worker bootstraps a management connection pool, validates extension metadata, and then launches the runtime inside an asynchronous Tokio context. Understanding this initialization path is essential for anyone operating or debugging the microsoft/pg_durable extension.
Registering the Background Worker in _PG_init
In src/lib.rs, the PostgreSQL extension entry point _PG_init delegates worker registration to worker::register_background_worker(). This call instructs PostgreSQL to spawn the pg_durable_worker process once recovery completes.
#[pg_guard]
pub extern "C-unwind" fn _PG_init() {
// … GUC definitions omitted …
worker::register_background_worker();
}
This registration is defined at src/lib.rs:138‑139 in the microsoft/pg_durable source tree.
The Worker Entry Point: duroxide_worker_main
PostgreSQL starts the registered worker by invoking duroxide_worker_main, the C-ABI entry point located in src/worker.rs. The function first attaches signal handlers for SIGHUP and SIGTERM, initializes the tracing subsystem, and then builds a single-threaded Tokio runtime using tokio::runtime::Builder::new_current_thread().
#[pg_guard]
#[no_mangle]
pub extern "C-unwind" fn duroxide_worker_main(_arg: pg_sys::Datum) {
BackgroundWorker::attach_signal_handlers(
SignalWakeFlags::SIGHUP | SignalWakeFlags::SIGTERM,
);
init_tracing(); // capture all logs
log!("pg_durable: duroxide background worker starting...");
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create tokio runtime");
rt.block_on(async { run_duroxide_runtime().await });
rt.shutdown_timeout(Duration::from_secs(5));
}
The runtime blocks on run_duroxide_runtime().await and enforces a five-second shutdown timeout. See src/worker.rs:56‑84 for the full implementation.
Inside run_duroxide_runtime: Management Pool and Extension Gating
Before the Duroxide engine can start, run_duroxide_runtime constructs a management PgPool for polling and epoch handling. It then gates on the existence of the pg_durable extension through wait_for_extension_creation. Only after the extension is confirmed present does control flow reach initialize_duroxide_runtime, where the actual runtime is created.
This intermediate layer ensures the worker never attempts to bootstrap Duroxide against a catalog that has not yet finished installing the extension.
Core Bootstrap: initialize_duroxide_runtime
The initialize_duroxide_runtime async function in src/worker.rs performs the heavy lifting. It accepts a PostgreSQL connection string, a retry interval, and the management pool, and returns Option<Arc<runtime::Runtime>>.
async fn initialize_duroxide_runtime(
pg_conn_str: &str,
retry_interval: Duration,
mgmt_pool: &sqlx::PgPool,
) -> Option<Arc<runtime::Runtime>> {
std::env::set_var(
"DUROXIDE_PG_POOL_MAX",
get_max_duroxide_connections().to_string(),
);
let user_semaphore = Arc::new(tokio::sync::Semaphore::new(
get_max_user_connections() as usize,
));
loop {
if is_shutdown_requested() { return None; }
if !check_extension_exists(mgmt_pool).await { return None; }
if !check_duroxide_schema_owned(mgmt_pool).await {
tokio::time::sleep(retry_interval).await;
continue;
}
if has_extension_owned_duroxide_objects(mgmt_pool).await {
release_extension_owned_duroxide_objects(mgmt_pool).await.ok()?;
}
let store = Arc::new(
PostgresProvider::new_with_config(worker_provider_config(pg_conn_str))
.await
.ok()?,
);
let activities = create_activity_registry(
Arc::new(mgmt_pool.clone()), user_semaphore.clone(),
);
let orchestrations = create_orchestration_registry();
let runtime = runtime::Runtime::start_with_store(
store, activities, orchestrations,
)
.await;
log!("pg_durable: duroxide runtime started");
return Some(runtime);
}
}
The function spans src/worker.rs:5‑86 in the repository.
Configure the Duroxide Environment and User Semaphore
First, the function synchronizes PostgreSQL GUCs with Duroxide’s environment. It sets DUROXIDE_PG_POOL_MAX from pg_durable.max_duroxide_connections—the values are read through GUC wrappers defined in src/types.rs—and allocates a tokio::sync::Semaphore sized to pg_durable.max_user_connections.
std::env::set_var(
"DUROXIDE_PG_POOL_MAX",
get_max_duroxide_connections().to_string(),
);
let user_semaphore = Arc::new(tokio::sync::Semaphore::new(
get_max_user_connections() as usize,
));
These steps are implemented at src/worker.rs:16‑25.
Validate Extension State and Schema Ownership
The function enters a retry loop that checks three conditions in order:
is_shutdown_requested()— abort immediately if the worker receivedSIGTERM.check_extension_exists(mgmt_pool)— verify the extension is still installed.check_duroxide_schema_owned(mgmt_pool)— confirm theduroxideschema belongs to the extension.
If the schema check fails, the worker sleeps for the configured retry_interval and tries again. This logic appears at src/worker.rs:26‑44.
Release Legacy Extension-Owned Objects
On upgrade paths from early versions, lingering extension-owned objects must be released before the new runtime takes over. The worker detects them with has_extension_owned_duroxide_objects and cleans them up via release_extension_owned_duroxide_objects.
if has_extension_owned_duroxide_objects(mgmt_pool).await {
release_extension_owned_duroxide_objects(mgmt_pool).await.ok()?;
}
See src/worker.rs:46‑61.
Create the Duroxide PostgreSQL Provider Store
With the environment validated, the worker instantiates the Duroxide store. It calls PostgresProvider::new_with_config, supplying a connection string from postgres_connection_string() and the provider configuration from worker_provider_config.
let store = Arc::new(
PostgresProvider::new_with_config(worker_provider_config(pg_conn_str))
.await
.ok()?,
);
This store is the persistence layer for the Duroxide runtime. The corresponding code is at src/worker.rs:63‑70.
Build Activity and Orchestration Registries
Next, the worker constructs the callback registries that feed the runtime. create_activity_registry receives a clone of the management pool and the user semaphore, while create_orchestration_registry requires no external state.
let activities = create_activity_registry(
Arc::new(mgmt_pool.clone()), user_semaphore.clone(),
);
let orchestrations = create_orchestration_registry();
These registries are defined in src/registry.rs and invoked at src/worker.rs:78‑81.
Launch the Duroxide Runtime
Finally, the worker passes the store and registries into runtime::Runtime::start_with_store. This call spawns the Duroxide state-machine engine and returns an Arc<Runtime> that the worker retains for its main event loop.
let runtime = runtime::Runtime::start_with_store(
store, activities, orchestrations,
)
.await;
log!("pg_durable: duroxide runtime started");
return Some(runtime);
This culminating step is located at src/worker.rs:83‑88.
Retry Logic and Graceful Shutdown
If initialization succeeds, the worker enters run_until_extension_dropped_or_shutdown to monitor epoch sentinels and the extension catalog. If any step inside the initialization loop fails—for example, if the extension is dropped mid-flight—the worker retries after a short back-off. A concurrent shutdown request, signaled by SIGTERM, sets the internal shutdown flag and causes is_shutdown_requested() to return true, breaking the loop and letting the Tokio runtime drain within its five-second timeout. This makes the pg_durable background worker resilient to transient catalog changes without risking orphan runtime processes.
Summary
_PG_initinsrc/lib.rsregisters thepg_durable_workerbackground worker viaworker::register_background_worker().- PostgreSQL spawns the worker after recovery, entering
duroxide_worker_maininsrc/worker.rs. - The worker attaches signal handlers, starts tracing, and creates a single-threaded Tokio runtime that blocks on
run_duroxide_runtime. run_duroxide_runtimebuilds a management pool, waits for the extension to appear, and callsinitialize_duroxide_runtime.- Inside
initialize_duroxide_runtime, the worker sets environment variables, allocates a user semaphore, validates extension ownership, cleans up legacy objects, creates aPostgresProviderstore, builds registries, and finally starts the DuroxideRuntime. - The worker then monitors epoch sentinels or extension drops through
run_until_extension_dropped_or_shutdown, handling graceful shutdown or recreation.
Frequently Asked Questions
What triggers the pg_durable background worker to start?
PostgreSQL invokes _PG_init when the shared library is loaded. This function registers the background worker, and PostgreSQL later launches it automatically after recovery completes. The worker entry point duroxide_worker_main then begins the Duroxide initialization sequence.
Why does pg_durable use a single-threaded Tokio runtime?
The worker uses tokio::runtime::Builder::new_current_thread() to create a single-threaded runtime because the background worker process runs in its own operating-system process. A current-thread reactor keeps the concurrency model simple while still allowing asynchronous I/O through the SQLx connection pools and the Duroxide engine.
What happens if the pg_durable extension is dropped while the worker is running?
If the extension is dropped, the helper check_extension_exists eventually returns false, which causes initialize_duroxide_runtime to return None or abort the loop. The worker then exits its main event loop and shuts down gracefully, preventing a stale Duroxide runtime from continuing to operate against a dropped catalog.
How does the worker handle graceful shutdown?
The worker installs a SIGTERM handler during startup. When PostgreSQL sends a termination signal, is_shutdown_requested() flips to true, breaking any retry loops and allowing the Tokio runtime to shut down with a five-second timeout. This ensures in-flight Duroxide tasks have a brief window to finish before the process exits.
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 →