Skip to main content

@siglo/scheduler Adapter Guide

@siglo/scheduler provides background timer polling, distributed lock claiming, and cloud-native webhook dispatchers for Siglo durable workflows. It ensures that time-based durables (wait('24h'), waitForEvent('CONFIRM', { timeout: '7d' })) resume on time across container restarts, node crashes, and multi-worker Kubernetes clusters.

1. Overview & Architecture

Siglo workflows operate in-process without requiring a central coordinator daemon. However, when workflows suspend with long-running timers in durable environments (PostgreSQL, Redis), a mechanism is required to wake sleeping workflows when their expiration timestamps elapse. @siglo/scheduler solves this with three complementary dispatchers:

2. Installation & Requirements

Install @siglo/scheduler alongside @siglo/core and @siglo/postgres:
Peer dependencies:
  • @siglo/core: ^1.0.0
  • @siglo/postgres: ^1.0.0 (for PostgresWaitPoller)
  • pg: ^8.0.0 (or compatible PostgreSQL client with a .query() method)

3. Database Schema & Row-Level Locking

PostgresWaitPoller relies on the siglo_wait_records table. It provides an automated DDL initialization method: poller.ensureTable(). Prefer running migrations or poller.ensureTable() over manual table creation.

DDL Schema

The Advisory Resumption Query & Session Locking

To coordinate execution across concurrent worker pods, PostgresWaitPoller executes an advisory selection query to find due timers and accepted events:
  • Advisory Selection & Session Locks: Selection is advisory. The storage adapter serializes execution using a database-session lock (withWorkflowLock), claiming and completing wait records atomically. If a worker pod crashes mid-resumption, PostgreSQL automatically releases the session lock upon connection disconnect.
  • Accepted-Event Recovery: Records with status = 'RESUMING' and a non-null resume_payload represent accepted resume events that survived process termination and will be picked up by surviving workers.
  • Lock Deprecation: lockTimeoutMs is deprecated for PostgresStorageAdapter as lock lifecycles are tied to database sessions rather than manual timeout windows.

4. PostgresWaitPoller Usage

Starting the Background Poller in Worker Pods


5. MemoryWaitPoller (In-Memory Development)

For local development, integration tests, or single-process prototypes where a PostgreSQL cluster is unnecessary, use MemoryWaitPoller. It automatically inspects in-memory waiting workflows tracked by defaultDurableManager:

6. createSchedulerWebhookHandler (Cloud-Native Schedulers)

In serverless environments (AWS Lambda, Google Cloud Run) where long-running daemon processes cannot run, use external cloud scheduling services:
  • AWS EventBridge Scheduler: Schedules one-time HTTP targets.
  • Google Cloud Tasks / Cloud Scheduler: Dispatches webhook calls with specific timestamps.
  • Cloudflare Queues / Scheduled Workers: Dispatches HTTP POST requests.

Setting Up the Webhook Dispatcher

Ingress Request Specification

The dispatcher expects a POST request with JSON body:

HTTP Response Codes


7. Operational Best Practices

  1. Size lockTimeoutMs Appropriately: Set lockTimeoutMs to at least 3×3\times the maximum expected runtime of the steps following the wait node (default: 60,000ms). If steps perform external HTTP calls with 15s timeouts, 60s prevents premature lock reclamation by peer workers.
  2. Synchronize Clocks with NTP: Ensure all Kubernetes nodes and database instances use NTP synchronization (e.g., Amazon Time Sync Service, Google NTP) to prevent premature lock reclamation.
  3. Graceful Worker Shutdown: Always await poller.stop() in your process SIGTERM handler. The method waits for any active in-flight query loop to finish before resolving.
  4. Partition / Archive Completed Records: Periodically archive records where status IN ('RESUMED', 'FAILED') to keep the idx_siglo_wait_records_due index lean and ensure fast query execution.

8. API Reference

PostgresWaitPoller

  • constructor(options: PostgresPollerOptions)
  • ensureTable(): Promise<void>
  • pollOnce(): Promise<PollerReport>
  • start(): void
  • stop(): Promise<void>
  • isRunning(): boolean

MemoryWaitPoller

  • constructor(options: MemoryPollerOptions)
  • pollOnce(): Promise<PollerReport>
  • start(): void
  • stop(): Promise<void>
  • isRunning(): boolean

createSchedulerWebhookHandler

  • createSchedulerWebhookHandler(options: CloudSchedulerWebhookOptions): WebhookHandler