# DurableStack Docs Export (v1.0) ## Language: .NET ### Advanced Options URL: /docs/v1.0/dotnet/configuration/advanced-options # Advanced Options Advanced options are useful once the baseline runtime is stable and observed under real workload. ## Eventing options Use these when connecting to hosted observability or tuning ingestion behavior: - `Eventing.TenantId` - `Eventing.ClientSecret` - `Eventing.IngestionApiBaseUrl` - `Eventing.IngestionPath` - `Eventing.IngestionMaxBatchSize` - `Eventing.IngestionMaxRequestBodyBytes` - `Eventing.IngestionMaxRetryAttempts` - `Eventing.IngestionFlushIntervalSeconds` - `Eventing.IncludeErrorDetail` - `Eventing.MaxErrorDetailLength` Event ingestion is automatically enabled when both `TenantId` and `ClientSecret` are set. ## Retention options Use these to control terminal run cleanup behavior: - `Retention.Enabled` - `Retention.RunRetentionSeconds` - `Retention.SweepIntervalSeconds` - `Retention.DeleteBatchSize` ## Registration options - `JobRegistration.AutoDiscoverJobsFromAssembly`: auto-discover public job types. - `Recurring.RegistrationSync.*`: reconcile code definitions with stored recurring records. ## Example ```csharp builder.Services.AddDurableStackPostgres(connectionString, options => { options.Eventing.IngestionFlushIntervalSeconds = 5; options.Eventing.IncludeErrorDetail = false; options.Retention.Enabled = true; options.Retention.RunRetentionSeconds = 7 * 24 * 60 * 60; options.Retention.SweepIntervalSeconds = 300; options.Retention.DeleteBatchSize = 1000; options.JobRegistration.AutoDiscoverJobsFromAssembly = true; }); ``` ## Guidance - Change advanced options intentionally and validate impact in staging. - Keep sensitive settings (tenant/client secret) in secure secret storage. - Avoid enabling verbose error detail unless required for controlled environments. ### Configuration URL: /docs/v1.0/dotnet/configuration # Configuration This section covers runtime configuration from basic setup to advanced tuning. ## In this section - [Overview](overview) - [Job Discovery and Assemblies](job-discovery-and-assemblies) - [Worker Options](worker-options) - [Retry Policy](retry-policy) - [Lease Configuration](lease-configuration) - [Recurring Settings](recurring-settings) - [Advanced Options](advanced-options) ### Job Discovery and Assemblies URL: /docs/v1.0/dotnet/configuration/job-discovery-and-assemblies ## Overview DurableStack discovers job types at startup. By default, auto-discovery scans the application assembly (the assembly where your host starts). If your jobs live in a different project/assembly, they will not be discovered unless you explicitly register that assembly. ## Common scenario In many real-world solutions, the application host and job implementations are intentionally separated across projects. DurableStack starts from the host assembly, so default auto-discovery only sees job types loaded from that assembly. When jobs are defined in another referenced assembly, explicitly registering that assembly ensures they are discovered and registered at startup. ## Register jobs from another assembly Use `AddDurableJobsFromAssembly` and point it to an assembly that contains your job types. ```csharp services.AddDurableStack(options => { options.UsePostgres(DataSettingsManager.LoadSettings().ConnectionString); }); services.AddDurableJobsFromAssembly(typeof(ClearCacheTask).Assembly); ``` `ClearCacheTask` is just an anchor type. DurableStack scans that assembly and discovers all matching job types there. ## Recommended pattern - Keep your normal `AddDurableStack(...)` registration. - Add one `AddDurableJobsFromAssembly(...)` call per external assembly that contains jobs. - Use a stable, known job type from that project as the anchor (`typeof(SomeJob).Assembly`). ## Multiple assemblies If jobs are split across projects, register each assembly explicitly. ```csharp services.AddDurableJobsFromAssembly(typeof(BillingJobsAssemblyMarker).Assembly); services.AddDurableJobsFromAssembly(typeof(ReportingJobsAssemblyMarker).Assembly); ``` ## Troubleshooting checklist - Confirm the jobs project is referenced by the host project. - Confirm job types are public and implement DurableStack job contracts. - Confirm registration runs during startup before worker execution begins. - If no jobs appear, log startup and verify the external assembly registration path is executed. ### Lease Configuration URL: /docs/v1.0/dotnet/configuration/lease-configuration # Lease Configuration Leases ensure only one worker actively owns a run at a time under normal operation. Lease settings balance two things: - fast recovery when workers die - avoiding premature reclaim for legitimate long-running jobs ## Key option - `LeaseDuration` / `LeaseDurationSeconds`: lease time-to-live for claimed runs. DurableStack extends leases during execution via heartbeat. ## Example ```csharp builder.Services.AddDurableStackPostgres(connectionString, options => { options.LeaseDurationSeconds = 30; }); ``` ## How it behaves - Worker claims run and receives lease ownership. - Heartbeat extends lease while job is running. - If heartbeat stops, lease expires and run can be reclaimed. ## Tuning guidance - Set lease duration above normal execution time for routine jobs. - For mixed workloads, size lease around common-path jobs and keep handlers idempotent. - Validate reclaim behavior in staging by killing workers during execution. ## Warning signs - Frequent unexpected retries for normally healthy jobs (lease may be too short). - Slow recovery after worker crashes (lease may be too long). ### Overview URL: /docs/v1.0/dotnet/configuration/overview # Overview DurableStack configuration is designed to be explicit, predictable, and easy for future developers to reason about. For most teams, the right approach is: - set all important runtime options in C# - keep values close to runtime registration in `Program.cs` - tune only after observing behavior in staging/production ## Configuration flow - Select the storage provider. - Set worker identity and polling behavior. - Configure retry and lease semantics. - Configure recurring job behavior. - Add advanced options only when needed. ## Recommended baseline ```csharp using DurableStack.Core.Options; using DurableStack.Hosting.DependencyInjection; builder.Services.AddDurableStackPostgres(connectionString, options => { options.WorkerName = $"orders-api-{Environment.MachineName}-{Environment.ProcessId}"; options.JobActivation = DurableStackJobActivationMode.ScopedPerExecution; options.PollIntervalSeconds = 5; options.PollJitterEnabled = true; options.PollJitterRatio = 0.2; options.BatchSize = 50; options.MaxConcurrentRuns = 10; options.LeaseDurationSeconds = 30; options.RetryDelay = TimeSpan.FromSeconds(5); options.RetryMaxDelay = TimeSpan.FromMinutes(10); options.RetryJitterEnabled = true; options.RetryJitterRatio = 0.2; options.Recurring.CatchUpPolicy = RecurringCatchUpPolicy.SkipMissed; options.Retention.Enabled = true; options.Retention.RunRetentionSeconds = 86400; options.Retention.SweepIntervalSeconds = 300; options.Retention.DeleteBatchSize = 1000; }); ``` For full option-level details, see [Configuration Schema](../reference/configuration-schema). ### Recurring Settings URL: /docs/v1.0/dotnet/configuration/recurring-settings # Recurring Settings Recurring settings control how DurableStack initializes and maintains recurring schedule definitions over time. ## Key options - `Recurring.CatchUpPolicy`: missed slot behavior (`SkipMissed` or `CatchUp`). - `Recurring.RegistrationSync.ExistingJobBehavior`: behavior when code and stored schedule definitions differ. - `Recurring.RegistrationSync.OrphanedJobBehavior`: behavior when stored recurring definitions are no longer in code. ## Example ```csharp using DurableStack.Core.Options; builder.Services.AddDurableStackPostgres(connectionString, options => { options.Recurring.CatchUpPolicy = RecurringCatchUpPolicy.SkipMissed; options.Recurring.RegistrationSync.ExistingJobBehavior = ExistingRecurringJobBehavior.KeepDatabase; options.Recurring.RegistrationSync.OrphanedJobBehavior = OrphanedRecurringJobBehavior.Disable; }); ``` ## Runtime behavior - Startup initializer upserts recurring definitions. - Scheduler computes next due slot per schedule. - Due slots are materialized into job runs. - Admin operations can disable/enable/run-now/update cron/time zone. ## Registration sync behavior details Registration sync runs at startup and reconciles recurring job definitions in code with records already stored in the provider. ### `ExistingJobBehavior` This option applies when a recurring job exists in both places: - code definition exists - database recurring record already exists Available values: - `KeepDatabase` (default): keep existing stored cron/time-zone/enabled settings as-is. - `UpdateFromCode`: overwrite stored recurring definition fields from current code attributes/registration. When to use: - Use `KeepDatabase` when runtime/admin-updated schedules should remain source of truth. - Use `UpdateFromCode` when code should be authoritative for schedule shape at startup. ### `OrphanedJobBehavior` This option applies when a recurring job exists in the database but no longer exists in code. Available values: - `Disable` (default): disable the orphaned recurring record so it no longer materializes runs. - `Ignore`: leave orphaned recurring records untouched. When to use: - Use `Disable` for safer cleanup when jobs are removed or renamed in code. - Use `Ignore` only when you intentionally manage some recurring definitions outside application code. ## Rename/removal note Renaming a recurring job name is treated as: - old job becomes orphaned - new job is a new definition With default settings (`KeepDatabase` + `Disable`), the old record is disabled and the new one is created. ## Operational guidance - Use `SkipMissed` for most production systems to avoid downtime catch-up bursts. - Keep `ExistingJobBehavior = KeepDatabase` unless code should be authoritative. - Keep `OrphanedJobBehavior = Disable` for safer cleanup when jobs are removed. ### Retry Policy URL: /docs/v1.0/dotnet/configuration/retry-policy # Retry Policy When a job throws, DurableStack evaluates retry eligibility using the current attempt and `MaxAttempts`, then schedules the next try with either fixed delay or backoff. ## Retry rule - Retry when `Attempt < MaxAttempts`. - Reschedule for `UtcNow + calculated delay`. - Mark terminal `failed` when max attempts is reached. `MaxAttempts` includes the initial attempt. Example: `MaxAttempts = 3` allows one initial run + up to two retries. ## Retry behavior modes DurableStack supports two retry modes: - `FixedDelay` (default): every retry uses the same base delay. - `Backoff`: each retry delay grows exponentially from the base delay (`baseDelay * 2^(Attempt - 1)`). `Attempt` is the attempt that just failed. So with backoff and `baseDelay = 5s`: - after attempt 1 fails: `5s` - after attempt 2 fails: `10s` - after attempt 3 fails: `20s` ## Where retry settings come from Retry mode and initial delay can be set per job via `[DurableJob]`, and global limits/tuning come from `AddDurableStack` options. ### Per-job (attribute) ```csharp using DurableStack.Core.Models; using DurableStack.Hosting.DependencyInjection; [DurableJob( Name = "send-digest-email", MaxAttempts = 5, RetryBehavior = RetryBehavior.Backoff, RetryInitialDelaySeconds = 10)] public sealed class SendDigestEmailJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) { return Task.CompletedTask; } } ``` - `RetryBehavior` default: `FixedDelay` - `RetryInitialDelaySeconds` default: unset (`0` on attribute), which falls back to global `options.RetryDelay` ### Global runtime options ```csharp builder.Services.AddDurableStack(options => { options.RetryDelay = TimeSpan.FromSeconds(5); // Default base delay when per-job initial delay is not set. options.RetryMaxDelay = TimeSpan.FromHours(1); // Upper cap applied to computed retry delay. options.RetryJitterEnabled = false; // Enables random jitter when true. options.RetryJitterRatio = 0.2; // +/- jitter ratio (0 to 1) when jitter is enabled. }); ``` ## Delay calculation order - Start with base delay: - per-job `RetryInitialDelaySeconds` if set and > 0 - otherwise global `options.RetryDelay` - Apply behavior: - `FixedDelay`: keep base delay - `Backoff`: exponential growth per failed attempt - Apply jitter if enabled - Clamp to `options.RetryMaxDelay` if exceeded ## Status progression - `pending` -> `leased` -> `succeeded` - `pending` -> `leased` -> `pending` (retry) - `pending` -> `leased` -> `failed` (terminal) ## Guidance - Use `FixedDelay` for predictable retry cadence. - Use `Backoff` for transient dependency failures (API throttling, brief outages). - Keep job handlers idempotent so retries are safe. - Keep `MaxAttempts` lower for non-idempotent side effects. - Monitor retry and failure trends in observability dashboards. ### Worker Options URL: /docs/v1.0/dotnet/configuration/worker-options # Worker Options Worker options control how aggressively each process claims and executes work. These settings are the primary levers for throughput and load distribution. ## Key options - `WorkerName`: unique worker identity for lease ownership and diagnostics. - `PollInterval` / `PollIntervalSeconds`: base polling cadence. - `PollJitterEnabled` and `PollJitterRatio`: randomize poll timing to distribute claim pressure. Default: enabled with ratio `0.2`. - `BatchSize` (`ClaimBatchSize`): max runs claimed per poll. - `MaxConcurrentRuns`: max in-flight runs executed concurrently per worker. - `JobActivation`: DI activation model (`ScopedPerExecution` or `RootProvider`). ## Example ```csharp using DurableStack.Core.Options; builder.Services.AddDurableStackPostgres(connectionString, options => { options.WorkerName = $"billing-api-{Environment.MachineName}-{Environment.ProcessId}"; options.PollIntervalSeconds = 5; options.PollJitterEnabled = true; options.PollJitterRatio = 0.2; options.BatchSize = 50; options.MaxConcurrentRuns = 10; options.JobActivation = DurableStackJobActivationMode.ScopedPerExecution; }); ``` ## How to tune - Increase `MaxConcurrentRuns` before aggressively increasing `BatchSize`. - Keep poll jitter enabled in multi-worker deployments. - Use unique, stable worker names per process instance. - Keep `ScopedPerExecution` unless you intentionally avoid scoped dependencies. ## Common pitfalls - Reusing the same `WorkerName` across containers. - Setting very low poll intervals without jitter in large clusters. - Setting high concurrency without validating database and dependency capacity. `AddDurableStackWithJitter(...)` is deprecated in v1.0 because poll jitter is already enabled by default. ### Distributed Execution URL: /docs/v1.0/dotnet/core-concepts/distributed-execution # Distributed Execution DurableStack is designed for multiple workers competing for work across instances. It distributes load by combining claim limits, jittered polling, and lease ownership. ## Core model Each worker loop does three things: - materializes due recurring runs - claims available due runs up to local capacity - executes claimed runs with lease heartbeat protection Local concurrency is bounded by: - `BatchSize` / `ClaimBatchSize` - `MaxConcurrentRuns` ## Why jitter matters Without jitter, workers poll at the same interval boundaries and create synchronized load spikes. With `PollJitterEnabled`, each worker randomizes poll delay by a bounded ratio: - delay factor = `1 +/- PollJitterRatio` - default ratio is `0.2` (up to +/-20%) This naturally spreads claim attempts over time and reduces thundering herd behavior against the store. ### Example If `PollIntervalSeconds = 5` and `PollJitterRatio = 0.2`, each poll is roughly between 4 and 6 seconds. ```csharp builder.Services.AddDurableStackPostgres(connectionString, options => { options.PollIntervalSeconds = 5; options.PollJitterEnabled = true; options.PollJitterRatio = 0.2; }); ``` ## How lease ownership prevents duplicate active execution When a worker claims a run, the run is leased to that worker for `LeaseDuration`. Only the lease owner should be executing that run at that time. During execution: - a background heartbeat extends lease ownership periodically - extension cadence is half lease duration If the worker crashes or stops heartbeating, lease expires and another worker can reclaim the run. This model limits execution to one active lease owner per run under normal operation while enabling recovery after failures. ## Single execution of each run, practically The system targets single active execution per run through lease ownership. As with any distributed lease model, edge cases (crash/network timing) can lead to re-attempts, so handlers must remain idempotent. Think of the guarantee as: - single active lease owner in normal operation - durable recovery and retry when interruptions occur ## Worker identity in distributed environments Set a unique `WorkerName` per process/container. DurableStack uses worker identity for lease ownership, diagnostics, and operational traceability. ```csharp var workerName = $"{Environment.MachineName}-{Environment.ProcessId}"; builder.Services.AddDurableStackPostgres(connectionString, options => { options.WorkerName = workerName; }); ``` ## Practical tuning guidance - Start with moderate `BatchSize` and `MaxConcurrentRuns`. - Enable poll jitter in multi-worker deployments. - Set `LeaseDuration` above normal execution time for routine jobs. - Monitor retries and lease extensions before increasing throughput aggressively. ### Core Concepts URL: /docs/v1.0/dotnet/core-concepts # Core Concepts This section explains how DurableStack behaves under real production conditions. ## In this section - [Job Model](job-model) - [Scheduling & Cron](scheduling-and-cron) - [Reliability and Execution Model](reliability-and-execution-model) - [Distributed Execution](distributed-execution) - [Time Zones](time-zones) ### Job Model URL: /docs/v1.0/dotnet/core-concepts/job-model # Job Model In DurableStack, a job is a class that implements a supported job contract and is registered in the runtime job registry. Each execution of that job is a run with its own lifecycle, attempt count, and status transitions. ## Core job contracts Use `IDurableJob` when a job has no payload: ```csharp public interface IDurableJob { Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default); } ``` Use `IDurableJob` when a job needs typed input: ```csharp public interface IDurableJob { Task ExecuteAsync(TArgs args, JobContext context, CancellationToken cancellationToken = default); } ``` ## What `JobContext` gives you `JobContext` includes: - `RunId`: unique identifier for the specific run instance. - `JobName`: logical job name from registration. - `Attempt`: current attempt number for this run. - `ScheduledForUtc`: the intended schedule/enqueue time for this run. - `Services`: scoped service provider for resolving dependencies during execution. ## Recommended model: attribute-based job metadata DurableStack discovers public job classes automatically when `options.JobRegistration.AutoDiscoverJobsFromAssembly` is `true` (default). Use attributes for execution and schedule metadata: ```csharp using DurableStack.Core; using DurableStack.Core.Abstractions; using DurableStack.Core.Models; using DurableStack.Hosting.DependencyInjection; [DurableJob( Name = "invoice-sync", MaxAttempts = 5, RetryBehavior = RetryBehavior.Backoff, RetryInitialDelaySeconds = 10)] [RecurringJob( "*/5 * * * *", TimeZone = "UTC", Enabled = true, AllowConcurrentRuns = false)] public sealed class InvoiceSyncJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) { return Task.CompletedTask; } } ``` ## Full attribute options and defaults ### `DurableJobAttribute` - `Name` (`string?`): optional stable job name. Default is class name. - `MaxAttempts` (`int`): total attempts including initial attempt. Default `3`. - `RetryBehavior` (`RetryBehavior`): `FixedDelay` or `Backoff`. Default `FixedDelay`. - `RetryInitialDelaySeconds` (`int`): per-job initial retry delay. Default `0` meaning unset, which falls back to runtime `options.RetryDelay`. ### `RecurringJobAttribute` - `Cron` (`string`, constructor): required cron expression. - `TimeZone` (`string`): IANA time zone ID. Default `UTC`. - `Enabled` (`bool`): whether recurring schedule starts enabled. Default `true`. - `AllowConcurrentRuns` (`bool`): whether overlapping recurring runs are allowed. Default `false`. ## One job, two execution paths A job with only `DurableJob` is enqueue-only. A job with both `DurableJob` and `RecurringJob` supports: - automatic recurring materialization from cron - manual enqueue/run-now using `IDurableStackClient` ```csharp var runId = await durableClient.EnqueueAsync(cancellationToken: cancellationToken); ``` ## Runtime registration behavior During startup, DurableStack builds `DurableJobRegistration` entries from discovered job classes. Key registration rules: - Duplicate job names are rejected. - Duplicate job types are rejected. - Invalid `MaxAttempts` (<= 0) fails startup. - Invalid cron or invalid time zone fails startup. ## Activation model and dependency resolution Jobs are resolved from DI using one of two activation modes: - `ScopedPerExecution` (default): creates a fresh scope per run. - `RootProvider`: resolves from root provider (scoped dependencies are not supported). In most cases, keep `ScopedPerExecution`. ## Explicit registration for advanced scenarios You can explicitly register jobs instead of auto-discovery: ```csharp builder.Services.AddDurableJob( name: "invoice-sync", cronExpression: "*/5 * * * *", timeZone: "UTC", maxAttempts: 5); ``` Or with options: ```csharp builder.Services.AddDurableJob("invoice-sync", options => { options.WithMaxAttempts(5) .WithRetryBehavior(RetryBehavior.Backoff) .WithRetryInitialDelaySeconds(10) .RunOnCron("*/5 * * * *", timeZone: "UTC") .WithAllowConcurrentRuns(false); }); ``` ## Practical guidance - Keep job names stable once production data exists. - Keep handlers idempotent so retries are safe. - Use typed jobs (`IDurableJob`) when payload shape matters. - Keep job classes focused on orchestration and call domain services for business logic. ### Reliability & Execution Model URL: /docs/v1.0/dotnet/core-concepts/reliability-and-execution-model # Reliability & Execution Model DurableStack executes runs through a durable store-backed lifecycle with lease-based ownership. The goal is predictable behavior during normal operation, transient failures, and process interruptions. ## Run lifecycle A run typically moves through: - `pending`: queued and waiting to be claimed. - `leased`: claimed by a worker and currently in execution. - `succeeded`: execution completed successfully. - `failed`: terminal failure after max attempts or non-retry path. If a failure is retry-eligible, the run is re-scheduled as `pending` for a future retry time. ## Claim and execution flow For each processing loop: - Worker claims due runs with a lease (`ClaimDueRunsAsync`). - Worker emits claimed/started events. - Job executes through the configured runner. - On success, run is marked succeeded. - On exception, retry eligibility is evaluated and run is marked failed with or without retry scheduling. ## Retry behavior Retry eligibility is based on attempt count: - Retry while `Attempt < MaxAttempts`. - Transition to terminal `failed` when attempts are exhausted. Delay calculation uses: - per-job retry behavior (`FixedDelay` or `Backoff`) - per-job initial delay when provided - otherwise runtime defaults (`options.RetryDelay`, `options.RetryMaxDelay`) - optional jitter (`options.RetryJitterEnabled`) ## Lease heartbeat and recovery During execution, DurableStack extends the lease periodically. - Heartbeat extension interval is half the lease duration (minimum 250ms). - If a worker dies or stops extending lease, the run becomes reclaimable after lease expiry. This enables automatic recovery from worker interruption. ## Provider-level concurrency model Claiming is implemented with provider-specific concurrency primitives: - PostgreSQL/MySQL: `FOR UPDATE SKIP LOCKED` - SQL Server: lock hints such as `UPDLOCK` + `READPAST` - SQLite: transactional select-and-update semantics ## Practical guarantees DurableStack is designed for effectively-once processing in normal operation with durable retry behavior. Because distributed systems can re-attempt after failures and lease expiration, handlers should be idempotent. ## Operational implications - Keep `WorkerName` unique per process/container. - Set lease duration to exceed normal execution time for common jobs. - Use retries intentionally and monitor failure trends. - Treat idempotency as a required contract for production handlers. ### Scheduling & Cron URL: /docs/v1.0/dotnet/core-concepts/scheduling-and-cron # Scheduling & Cron DurableStack recurring scheduling is cron-driven. Each recurring job stores its schedule definition, and the scheduler materializes due runs into the run queue. ## How scheduling works At runtime: - Recurring job definitions are initialized/upserted at startup. - Scheduler checks for due recurring jobs. - For each due job, DurableStack enqueues a run and computes the next occurrence. - Catch-up behavior is controlled by `options.Recurring.CatchUpPolicy`. The same job can still be manually enqueued through `IDurableStackClient`. ## Supported cron formats DurableStack supports both: - 5-field cron: `minute hour day-of-month month day-of-week` - 6-field cron: `second minute hour day-of-month month day-of-week` Examples: - 5-field: `*/5 * * * *` - 6-field: `*/20 * * * * *` Any other field count is invalid. ## Common examples - Every minute: `* * * * *` - Every 5 minutes: `*/5 * * * *` - Every hour at minute 0: `0 * * * *` - Every day at 02:00: `0 2 * * *` - Weekdays at 08:30: `30 8 * * 1-5` - First day of month at midnight: `0 0 1 * *` - Every 20 seconds (6-field): `*/20 * * * * *` ## Attribute examples ```csharp using DurableStack.Core.Abstractions; using DurableStack.Hosting.DependencyInjection; [DurableJob(Name = "report-hourly")] [RecurringJob("0 * * * *", TimeZone = "UTC")] public sealed class HourlyReportJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; } [DurableJob(Name = "heartbeat-every-20-seconds")] [RecurringJob("*/20 * * * * *", TimeZone = "UTC")] public sealed class SubMinuteHeartbeatJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; } ``` ## Catch-up policy interaction - `SkipMissed` (default): jumps to the next future slot after downtime. - `CatchUp`: materializes missed slots incrementally until current time. Use `SkipMissed` for most production workloads unless strict missed-slot replay is required. ## Time zones Cron expressions are evaluated in the `RecurringJob(TimeZone = "...")` IANA zone. - Default zone is `UTC`. - Windows time zone IDs are not supported. For more detail, see [Time Zones](time-zones). ## Practical guidance - Prefer 5-field cron for normal business schedules. - Use 6-field cron only when sub-minute cadence is truly required. - Keep `AllowConcurrentRuns = false` unless overlap is intentional. - Validate cron expressions in tests for critical schedules. ### Time Zones URL: /docs/v1.0/dotnet/core-concepts/time-zones # Time Zones Recurring schedules require IANA time zone IDs. ## Supported time zone IDs The list below is only a set of examples. DurableStack accepts any valid IANA time zone ID. - `UTC` - `America/Chicago` - `America/New_York` - `America/Los_Angeles` - `Europe/London` - `Europe/Berlin` - `Asia/Tokyo` - `Australia/Sydney` Windows time zone IDs are rejected. Common mistake: `Central Standard Time`, `Pacific Standard Time`, and other Windows IDs are not valid here. Convert them to IANA IDs such as `America/Chicago` or `America/Los_Angeles`. ## Catch-up policy DurableStack supports: - `SkipMissed` (default): jump to the current due slot, then continue future slots. - `CatchUp`: materialize one missed slot per loop until current. ```csharp builder.Services.AddDurableStack(options => { options.Recurring.CatchUpPolicy = RecurringCatchUpPolicy.SkipMissed; }); ``` ### DurableStack v1.0 URL: /docs/v1.0/dotnet # DurableStack v1.0 DurableStack helps you run background jobs that survive restarts, deployment rollouts, and transient failures. Supported runtime targets: `.NET 9` and `.NET 10`. This documentation is organized for future developers who need to find the right answer quickly, understand why a behavior exists, and safely operate the runtime in production. ## Documentation map - [Start Here](start-here) - [Core Concepts](core-concepts) - [Configuration](configuration) - [Integrations](integrations) - [Storage Providers](storage-providers) - [Usage Guide](usage-guide) - [Reference](reference) - [Operations](operations) ## Guiding principle Optimize for ease of use by a future developer: predictable structure, practical examples, and clear separation between concepts, configuration, usage, and operations. ### Events and Event Sinks URL: /docs/v1.0/dotnet/integrations/events-and-event-sinks # Events and Event Sinks DurableStack emits lifecycle and worker events during execution. Event sinks consume those events for logging, telemetry, and hosted observability ingestion. ## Event types - `job_claimed` - `job_started` - `job_succeeded` - `job_failed` - `job_retried` - `retry_scheduled` - `worker_heartbeat` ## How sinks are wired - DurableStack always has a no-op sink by default. - You can add logging sink with `UseDurableStackLoggingEventSink()`. - Hosted ingestion sink is added automatically when both tenant credentials are configured. ## Hosted ingestion behavior - Event sink is enabled only when tenant credentials are configured. - Posted ingestion data includes runtime information when tenant credentials are configured. - Events are buffered in a bounded channel. - Sync service flushes in batches with retry/backoff. ## Example ```csharp builder.Services.AddDurableStackPostgres(connectionString, options => { options.Eventing.TenantId = "tenant_..."; options.Eventing.ClientSecret = "secret_..."; }); builder.Services.UseDurableStackLoggingEventSink(); ``` ## Recommended practice - Configure tenant identity and client secret explicitly. - Keep environment/service labels consistent. - Use correlation IDs for cross-system traceability. ### Integrations URL: /docs/v1.0/dotnet/integrations # Integrations This section covers eventing, OpenTelemetry alignment, and optional hosted observability. ## In this section - [Observability Overview](observability-overview) - [Events and Event Sinks](events-and-event-sinks) - [OpenTelemetry](opentelemetry) - [Optional Hosted Observability](optional-hosted-observability) ### Observability Overview URL: /docs/v1.0/dotnet/integrations/observability-overview # Observability Overview The observability site is the operational console for DurableStack workloads. Access the site at [https://app.durablestack.com](https://app.durablestack.com). From there you can register, create a tenant, and obtain the tenant ID and client secret used by runtime event ingestion. When configured, runtime ingestion payloads include runtime information. ## What it provides - A health-focused dashboard for job throughput, failures, and trend visibility - Views for job runs, schedules, and worker activity - Shared filtering so teams can isolate a project, tenant, or time window - A workspace for operators to troubleshoot and validate runtime behavior ## Who it is for - Engineering teams who need day-to-day runtime visibility - Platform teams who need cross-tenant operations context - Support/on-call teams investigating failures and retry behavior ## What this docs section intentionally avoids This section does not document internal implementation details or service endpoint contracts. It is focused on practical usage and configuration of the site. ### OpenTelemetry URL: /docs/v1.0/dotnet/integrations/opentelemetry # OpenTelemetry DurableStack integrates with OpenTelemetry by registering its activity source and meter. Use this when you want traces/metrics to flow into your existing OpenTelemetry pipeline. ## What `AddDurableStackOpenTelemetry` does - Adds DurableStack tracing source to OpenTelemetry. - Adds DurableStack metrics meter to OpenTelemetry. - Leaves exporter selection to your app's OTel setup. ## Example ```csharp using DurableStack.Hosting.DependencyInjection; builder.Services.AddDurableStackPostgres(connectionString, options => { options.WorkerName = $"orders-api-{Environment.MachineName}-{Environment.ProcessId}"; }); builder.Services.AddDurableStackOpenTelemetry(); ``` ## Integration guidance - Keep resource attributes (`service.name`, environment) consistent across services. - Use traces to correlate run failures with downstream dependency calls. - Use metrics for alerting on claims, failures, retries, and lease extensions. ## Notes - OpenTelemetry integration is optional. - It complements event sinks and hosted observability rather than replacing them. ### Optional Hosted Observability URL: /docs/v1.0/dotnet/integrations/optional-hosted-observability # Optional Hosted Observability Hosted observability is optional. When configured, DurableStack publishes runtime events to the hosted platform. Start at [https://app.durablestack.com](https://app.durablestack.com) to register, create a tenant, and retrieve the tenant ID and client secret needed for runtime integration. ## Required settings Set these options in your `AddDurableStack...` block: - `options.Eventing.TenantId` - `options.Eventing.ClientSecret` When both `TenantId` and `ClientSecret` are set, ingestion sink registration is automatic. In this mode, posted ingestion data includes runtime information. ## Example ```csharp builder.Services.AddDurableStackPostgres(connectionString, options => { options.Eventing.TenantId = "tenant_..."; options.Eventing.ClientSecret = "secret_..."; }); ``` ## Validation checklist ### 1) Confirm runtime identity mapping - Ensure each runtime instance is associated with the intended organization/project/tenant scope. - Keep naming consistent between runtime configuration and site configuration. ### 2) Verify credentials and environment settings - Configure secrets through your secure secret store (not source control). - Validate that production and staging use separate credentials and scopes. ### 3) Validate end-to-end visibility - Trigger a known job run. - Confirm it appears in the observability dashboard and run views. - Confirm failures and retries are represented correctly. ### 4) Operational hardening - Rotate credentials on a regular schedule. - Alert on connection gaps so missing telemetry is detected quickly. - Re-check mapping after tenant/project reorganizations. Detailed ingestion protocol and endpoint contracts are intentionally outside this guide. ### Deployment Checklist URL: /docs/v1.0/dotnet/operations/deployment-checklist # Deployment Checklist Use this checklist before and immediately after production rollout. ## Pre-deployment - Choose and validate the target storage provider. - Verify connection strings and secret injection. - Set unique `WorkerName` format for all instances. - Confirm retention policy values. - Confirm retry and lease settings for expected workload profile. ## Deployment steps - Run store migrations (`IDurableStackStoreMigrator`). - Deploy workers gradually. - Watch logs and run metrics during ramp-up. - Verify recurring schedules are initialized as expected. ## Post-deployment validation - Enqueue a known job and confirm successful completion. - Verify run query APIs show expected status transitions. - Verify retry and failure flows with a controlled fault test. - If observability is enabled, verify event ingestion. ## Rollback readiness - Keep rollback artifact/version ready. - Keep migration compatibility notes for your current release line. - Define clear rollback trigger thresholds (failure rate, queue growth, lease contention). ### Operations URL: /docs/v1.0/dotnet/operations # Operations This section focuses on practical operational readiness and ongoing runtime safety. ## In this section - [Deployment checklist](deployment-checklist) - [Testing strategy](testing-strategy) - [Production guidelines](production-guidelines) - [Troubleshooting](troubleshooting) - [Performance tuning](performance-tuning) ### Performance Tuning URL: /docs/v1.0/dotnet/operations/performance-tuning # Performance Tuning This page covers practical tuning strategies for high-throughput and low-latency workloads. ## Primary tuning levers - `PollIntervalSeconds` - `BatchSize` - `MaxConcurrentRuns` - `LeaseDurationSeconds` - `RetryDelay` / `RetryMaxDelay` / retry jitter ## Throughput tuning sequence - Increase `MaxConcurrentRuns` in small steps. - Increase `BatchSize` after confirming downstream capacity. - Keep poll jitter enabled for multi-worker clusters. ## Latency tuning sequence - Lower poll interval carefully. - Balance lower poll interval with database load. - Avoid synchronized polling by keeping jitter enabled. ## Provider considerations - Validate tuning with your real provider and dataset. - Measure claim, success, retry, and failure trends before/after each change. - Tune one dimension at a time to keep causal analysis clear. ### Production Guidelines URL: /docs/v1.0/dotnet/operations/production-guidelines # Production Guidelines Use these guidelines to keep DurableStack workloads predictable in production. ## Baseline runtime posture - Use a durable shared database provider for distributed workers. - Set a unique `WorkerName` per process/container. - Keep poll jitter enabled in multi-worker environments. - Keep retention enabled with explicit retention windows. ## Deployment and rollout - Run store migrations before serving normal traffic. - Roll out workers gradually and monitor claim/failure trends. - Validate recurring schedule behavior after deployment. ## Reliability practices - Keep handlers idempotent. - Use conservative max attempts for non-idempotent side effects. - Prefer retry backoff for unstable external dependencies. ## Observability and alerting - Alert on failure-rate spikes and missing worker heartbeats. - Track retry volume and queue depth over time. - Validate hosted observability connectivity if enabled. ### Testing Strategy URL: /docs/v1.0/dotnet/operations/testing-strategy # Testing Strategy DurableStack testing should combine fast feedback tests with environment-realistic integration checks. ## Core layers - Unit tests for job logic and domain services. - Integration tests for runtime APIs (`IDurableStackClient`, schedule admin, run query). - Provider tests against your chosen production-like database engine. ## Runtime behavior tests Cover at least: - enqueue + run success path - delayed schedule execution path - retry transitions and terminal failure - recurring materialization behavior - lease recovery after worker interruption ## Provider-focused tests - migration/init in clean and existing schema states - claim behavior with concurrent workers - retention pruning behavior - schedule admin commands (enable/disable/update/run-now) ## CI recommendations - Run unit tests on every PR. - Run provider integration suites on PR or nightly depending on runtime cost. - Include one controlled failure scenario to verify retry + observability signals. ### Troubleshooting URL: /docs/v1.0/dotnet/operations/troubleshooting # Troubleshooting Use this page to debug common issues in local and production environments. ## Jobs are not executing - Confirm worker process is running. - Confirm job is registered and discoverable. - Check run status via `IDurableJobRunQueryService`. - Verify provider connection and migrations completed. ## Unexpected retry behavior - Verify `MaxAttempts` for the job. - Check `RetryBehavior`, delay, and jitter settings. - Confirm exception path actually throws when retry is desired. ## Lease contention or duplicate-appearing work - Confirm unique `WorkerName` per process/container. - Validate `LeaseDurationSeconds` is not shorter than typical execution time. - Review long-running handlers for heartbeat extension timing risks. ## Missing telemetry or hosted observability data - Confirm `Eventing.TenantId` and `Eventing.ClientSecret` are set. - Verify outbound connectivity to ingestion API. - Trigger a known run and confirm event flow end to end. ### Configuration Options URL: /docs/v1.0/dotnet/reference/configuration-options # Configuration Options DurableStack configuration lives under the `DurableStack` section. Use `ConnectionStrings` only if your app prefers storing database connection strings there, then copy/bind those values into `DurableStack:Postgres:ConnectionString` (or the provider equivalent) in your startup logic. ## Top-level options (`DurableStack:*`) - `StorageProvider`: backing store selection (`InMemory`, `Postgres`, `SqlServer`, `Sqlite`, `MySql`). Default: `InMemory`. - `WorkerName`: unique worker identity used for leasing and diagnostics. Default: `"{HOSTNAME-or-machine}-{processId}"`. - `DatabaseTablePrefix`: optional prefix for durable database objects. Default: `null`. - `PollIntervalSeconds`: worker polling cadence in seconds (`<= 0` resets to default). Default: `5`. - `PollJitterEnabled`: enables poll jitter for claim loops. Default: `true`. - `PollJitterRatio`: jitter ratio used for poll timing when poll jitter is enabled. Default: `0.2`. - `BatchSize`: max runs leased per poll cycle. Default: `50`. - `LeaseDurationSeconds`: lease TTL in seconds (`<= 0` resets to default). Default: `30`. - `RetryDelay`: base retry delay (`TimeSpan` format in config binding). Default: `00:00:05`. - `RetryMaxDelay`: maximum retry backoff (`TimeSpan` format in config binding). Default: `01:00:00`. - `RetryJitterEnabled`: enables retry jitter. Default: `false`. - `RetryJitterRatio`: jitter ratio used when jitter is enabled. Default: `0.2`. - `JobActivation`: job activation mode (`ScopedPerExecution`, `RootProvider`). Default: `ScopedPerExecution`. ## Provider connection options Set the matching section for the selected `StorageProvider`: - `Postgres:ConnectionString` (default: `""`) - `SqlServer:ConnectionString` (default: `""`) - `Sqlite:ConnectionString` (default: `""`) - `MySql:ConnectionString` (default: `""`) If a durable provider is selected and the corresponding connection string is empty, startup throws. ## Recurring options (`DurableStack:Recurring:*`) - `CatchUpPolicy`: missed-slot behavior (`SkipMissed`, `CatchUp`). Default: `SkipMissed`. ### Registration sync (`DurableStack:Recurring:RegistrationSync:*`) - `ExistingJobBehavior`: when recurring definitions already exist in storage (`KeepDatabase`, `UpdateFromCode`). Default: `KeepDatabase`. - `OrphanedJobBehavior`: when a DB recurring definition no longer exists in code (`Disable`, `Ignore`). Default: `Disable`. ## Retention options (`DurableStack:Retention:*`) - `Enabled`: enables terminal-run cleanup. Default: `true`. - `RunRetentionSeconds`: run retention window in seconds. Default: `null` (effective default is `3600` for in-memory, `86400` for durable stores). - `SweepIntervalSeconds`: cleanup sweep cadence in seconds (`<= 0` resets to default). Default: `300`. - `DeleteBatchSize`: max rows deleted per sweep batch (`<= 0` resets to default). Default: `1000`. ## Job registration options (`DurableStack:JobRegistration:*`) - `AutoDiscoverJobsFromAssembly`: auto-register public `IDurableJob`/`IDurableJob` classes from the app assembly. Default: `true`. ## Eventing options (`DurableStack:Eventing:*`) - `TenantId`: tenant ID for hosted ingestion auth. Default: `null`. - `ClientSecret`: client secret for hosted ingestion auth. Default: `null`. - `IngestionApiBaseUrl`: ingestion API base URL. Default: `https://api.durablestack.com`. - `IngestionPath`: ingestion API path. Default: `/v1/events/batch`. - `IngestionMaxBatchSize`: max events per post. Default: `100`. - `IngestionMaxRequestBodyBytes`: max request body size. Default: `1000000`. - `IngestionMaxRetryAttempts`: max retries for failed ingestion posts. Default: `5`. - `IngestionFlushIntervalSeconds`: flush cadence in seconds (`<= 0` resets to default). Default: `5`. - `IncludeErrorDetail`: includes captured error details in event payloads. Default: `false`. - `MaxErrorDetailLength`: max captured error-detail length (`<= 0` resets to default). Default: `4096`. Event ingestion is automatically enabled when both `TenantId` and `ClientSecret` are set. When enabled, the runtime includes runtime information in posted ingestion data. ## Example ```json { "DurableStack": { "StorageProvider": "Postgres", "WorkerName": "orders-api-1", "DatabaseTablePrefix": "prod_", "PollIntervalSeconds": 5, "BatchSize": 50, "LeaseDurationSeconds": 30, "RetryDelay": "00:00:05", "RetryMaxDelay": "01:00:00", "PollJitterEnabled": true, "PollJitterRatio": 0.2, "RetryJitterEnabled": false, "RetryJitterRatio": 0.2, "JobActivation": "ScopedPerExecution", "Postgres": { "ConnectionString": "Host=localhost;Port=5432;Database=durable_stack;Username=postgres;Password=postgres" }, "Recurring": { "CatchUpPolicy": "SkipMissed", "RegistrationSync": { "ExistingJobBehavior": "KeepDatabase", "OrphanedJobBehavior": "Disable" } }, "Retention": { "Enabled": true, "RunRetentionSeconds": 86400, "SweepIntervalSeconds": 300, "DeleteBatchSize": 1000 }, "JobRegistration": { "AutoDiscoverJobsFromAssembly": true }, "Eventing": { "TenantId": "tenant_...", "ClientSecret": "secret_...", "IngestionApiBaseUrl": "https://api.durablestack.com", "IngestionPath": "/v1/events/batch", "IngestionMaxBatchSize": 100, "IngestionMaxRequestBodyBytes": 1000000, "IngestionMaxRetryAttempts": 5, "IngestionFlushIntervalSeconds": 5, "IncludeErrorDetail": false, "MaxErrorDetailLength": 4096 } } } ``` ### Configuration Schema URL: /docs/v1.0/dotnet/reference/configuration-schema # Configuration Schema Use this page as the canonical map of the `DurableStack` configuration object. ## Root shape ```text DurableStack StorageProvider WorkerName DatabaseTablePrefix PollIntervalSeconds PollJitterEnabled PollJitterRatio BatchSize MaxConcurrentRuns LeaseDurationSeconds RetryDelay RetryMaxDelay RetryJitterEnabled RetryJitterRatio JobActivation Postgres.ConnectionString SqlServer.ConnectionString Sqlite.ConnectionString MySql.ConnectionString Recurring.CatchUpPolicy Recurring.RegistrationSync.ExistingJobBehavior Recurring.RegistrationSync.OrphanedJobBehavior Retention.Enabled Retention.RunRetentionSeconds Retention.SweepIntervalSeconds Retention.DeleteBatchSize JobRegistration.AutoDiscoverJobsFromAssembly Eventing.TenantId Eventing.ClientSecret Eventing.IngestionApiBaseUrl Eventing.IngestionPath Eventing.IngestionMaxBatchSize Eventing.IngestionMaxRequestBodyBytes Eventing.IngestionMaxRetryAttempts Eventing.IngestionFlushIntervalSeconds Eventing.IncludeErrorDetail Eventing.MaxErrorDetailLength ``` ## Notes - Prefer explicit C# configuration in startup for operational clarity. - Use provider-specific registration helpers when possible (`AddDurableStackPostgres`, etc.). - Keep secret values (connection strings, tenant/client secret) in secure secret stores. For defaults and detailed semantics, see [Configuration options](configuration-options). ### API Surface Reference URL: /docs/v1.0/dotnet/reference/endpoints # API Surface Reference DurableStack runtime packages do not require any specific HTTP route structure. ## Runtime SDK/services `IDurableStackClient`: - `EnqueueAsync()` - `EnqueueAsync(payload)` - `ScheduleAsync(payload, runAtUtc)` - `CancelRunAsync(runId)` `IDurableScheduleAdminService`: - `ListScheduledJobsAsync(includeDisabled)` - `SetScheduledJobEnabledAsync(jobName, enabled)` - `UpdateScheduledJobCronAsync(jobName, cronExpression, timeZone)` - `RunScheduledJobNowAsync(jobName)` `IDurableJobRunQueryService`: - `GetRunAsync(runId)` - `GetRecentRunsAsync(take)` - `GetRunsByStatusAsync(status, take)` - `GetRunsByJobNameAsync(jobName, take)` - `GetEnqueuedRunsAsync(take)` ### Error Handling URL: /docs/v1.0/dotnet/reference/error-handling # Error Handling Error handling in DurableStack is run-centric: each run either succeeds, retries, or becomes terminally failed. ## Failure categories - Transient failures: network timeouts, temporary dependency outages, throttling. - Persistent logic/data failures: invalid payload, invariant violations, unrecoverable contract errors. ## Retry interaction - Retry eligibility: `Attempt < MaxAttempts`. - Retry timing: fixed delay or backoff based on job/runtime settings. - Terminal failure: run marked `failed` when attempts are exhausted. ## Recommended handler patterns - Keep handlers idempotent. - Throw for truly failed execution paths. - Avoid swallowing exceptions that should drive retries. - Log with `RunId`, `JobName`, and `Attempt` for traceability. ## Example ```csharp public async Task ExecuteAsync(JobContext context, CancellationToken cancellationToken) { try { await _externalApiClient.PushAsync(cancellationToken); } catch (HttpRequestException ex) { _logger.LogWarning(ex, "Transient external call failure. Job={JobName} RunId={RunId} Attempt={Attempt}", context.JobName, context.RunId, context.Attempt); throw; } } ``` ## Operational recommendations - Monitor retry and failure rates over time. - Keep max attempts lower for side effects that are hard to deduplicate. - Use backoff for unstable dependencies to reduce cascading load. ### Reference URL: /docs/v1.0/dotnet/reference # Reference Use this section for exact package names, configuration shape, limits, and behavioral guarantees. ## In this section - [Configuration schema](configuration-schema) - [Packages](packages) - [Limits and guarantees](limits-and-guarantees) - [Error handling](error-handling) ### Limits and Guarantees URL: /docs/v1.0/dotnet/reference/limits-and-guarantees # Limits and Guarantees This page summarizes what DurableStack aims to guarantee and where limits apply. ## Practical guarantees - Durable providers persist run state across process restarts. - Lease ownership limits active execution of a run to one worker in normal operation. - Failed runs are retried while `Attempt < MaxAttempts`. - Recurring schedules materialize due runs according to cron/time-zone rules. ## Important limits - In-memory provider is process-local and not durable across restart. - In-memory provider does not support distributed multi-worker coordination. - Throughput is bounded by worker settings (`BatchSize`, `MaxConcurrentRuns`) and provider capacity. - Retry behavior is bounded by `MaxAttempts`, delay strategy, and optional max delay. ## Distributed system reality DurableStack is designed for effectively-once behavior in normal operation. Because of crash/recovery timing in distributed systems, handlers must remain idempotent. ## Retention boundaries - Default run retention: 24h for durable DB providers. - Default run retention: 1h for in-memory provider. - Historical runs are pruned according to retention configuration. ### Packages URL: /docs/v1.0/dotnet/reference/packages # Packages Current runtime package set: - `DurableStack.Core` - `DurableStack.Hosting` - `DurableStack.Worker` - `DurableStack.Postgres` - `DurableStack.MySql` - `DurableStack.SqlServer` - `DurableStack.Sqlite` All packages in this docs line target `v1.0` and support `.NET 9` and `.NET 10`. ### First Job Example URL: /docs/v1.0/dotnet/start-here/first-job-example # First Job Example This walkthrough shows a realistic first job, all options in both job attributes, and why DurableStack separates enqueue metadata from recurring schedule metadata. ## Example job ```csharp using DurableStack.Core; using DurableStack.Core.Abstractions; using DurableStack.Core.Models; using DurableStack.Hosting.DependencyInjection; [DurableJob( Name = "invoice-sync", MaxAttempts = 5, RetryBehavior = RetryBehavior.Backoff, RetryInitialDelaySeconds = 10)] [RecurringJob( "*/5 * * * *", TimeZone = "UTC", Enabled = true, AllowConcurrentRuns = false)] public sealed class InvoiceSyncJob : IDurableJob { private readonly ILogger _logger; public InvoiceSyncJob(ILogger logger) { _logger = logger; } public async Task ExecuteAsync(JobContext context, CancellationToken cancellationToken) { _logger.LogInformation( "Invoice sync run {RunId} attempt {Attempt} started at {StartedUtc}", context.RunId, context.Attempt, context.StartedAtUtc); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); _logger.LogInformation("Invoice sync run {RunId} completed", context.RunId); } } ``` ## Why there are two attributes ### `DurableJob` defines execution behavior - `Name`: stable job identity used for registration and operations. - `MaxAttempts`: total attempts including initial execution. - `RetryBehavior`: `FixedDelay` or `Backoff`. - `RetryInitialDelaySeconds`: per-job retry baseline. This attribute defines what happens when a run executes and fails. ### `RecurringJob` defines schedule behavior - `Cron`: schedule cadence. - `TimeZone`: IANA timezone used for cron evaluation. - `Enabled`: whether schedule emits new runs. - `AllowConcurrentRuns`: whether overlapping scheduled runs are allowed. This attribute defines whether and how new runs are created over time. ## Enqueue versus schedule - Enqueue creates one run immediately. - Recurring schedule materializes runs automatically when cron slots are due. - A job can be enqueue-only (no `RecurringJob`) or recurring (both attributes). ## Enqueue the same job manually ```csharp using DurableStack.Core.Abstractions; app.MapPost("/jobs/invoice-sync/run-now", async ( IDurableStackClient client, CancellationToken cancellationToken) => { var runId = await client.EnqueueAsync(cancellationToken: cancellationToken); return Results.Accepted($"/runs/{runId}", new { runId }); }); ``` With this route plus `[RecurringJob]`, the same handler supports both: - automatic recurring execution - manual run-now execution ## Practical guidance - Keep `DurableJob.Name` stable after production adoption. - Use `AllowConcurrentRuns = false` for jobs that mutate shared resources. - Prefer `RetryBehavior.Backoff` for third-party API dependencies. - Keep handlers idempotent so retries are safe. ## Next step After this page, review [Getting Started](getting-started) for the recommended runtime configuration baseline. ### Getting Started URL: /docs/v1.0/dotnet/start-here/getting-started # Getting Started This guide expands on Quick Start and focuses on the most common production options. All runtime configuration is set in C#. Most teams can rely on defaults for many options. This example sets them explicitly so the behavior is easy to understand. ## Recommended baseline setup ```csharp using DurableStack.Core.Options; using DurableStack.Hosting.DependencyInjection; var builder = WebApplication.CreateBuilder(args); var connectionString = "Host=localhost;Port=5432;Database=durable_stack;Username=postgres;Password=postgres"; var workerName = $"orders-api-{Environment.MachineName}-{Environment.ProcessId}"; builder.Services.AddDurableStackPostgres(connectionString, options => { options.JobActivation = DurableStackJobActivationMode.ScopedPerExecution; // Default: ScopedPerExecution options.WorkerName = workerName; // Default: "{HOSTNAME-or-machine}-{processId}" options.PollIntervalSeconds = 5; // Default: 5 options.BatchSize = 50; // Default: 50 options.MaxConcurrentRuns = 10; // Default: 5 options.LeaseDurationSeconds = 30; // Default: 30 options.RetryDelay = TimeSpan.FromSeconds(5); // Default: 00:00:05 options.RetryMaxDelay = TimeSpan.FromMinutes(60); // Default: 01:00:00 options.RetryJitterEnabled = true; // Default: false options.RetryJitterRatio = 0.20; // Default: 0.2 options.Recurring.CatchUpPolicy = RecurringCatchUpPolicy.SkipMissed; // Default: SkipMissed options.Recurring.RegistrationSync.ExistingJobBehavior = ExistingRecurringJobBehavior.KeepDatabase; // Default: KeepDatabase options.Recurring.RegistrationSync.OrphanedJobBehavior = OrphanedRecurringJobBehavior.Disable; // Default: Disable options.Retention.Enabled = true; // Default: true options.Retention.RunRetentionSeconds = 86400; // Default: null (effective 86400 for durable DB providers) options.Retention.SweepIntervalSeconds = 300; // Default: 300 options.Retention.DeleteBatchSize = 1000; // Default: 1000 options.Eventing.TenantId = null; // Default: null options.Eventing.ClientSecret = null; // Default: null }); ``` ## Option groups that matter most ### Worker and throughput - `WorkerName` (default: `"{HOSTNAME-or-machine}-{processId}"`): unique identity used for lease ownership and diagnostics. - `PollIntervalSeconds` (default: `5`): how often workers check for available runs. - `BatchSize` (default: `50`): maximum runs claimed per poll cycle. - `MaxConcurrentRuns` (default: `5`): concurrency limit per worker instance. - `LeaseDurationSeconds` (default: `30`): ownership timeout before another worker can recover a stalled run. ### Retry behavior - `RetryDelay` (default: `00:00:05`): baseline delay between attempts. - `RetryMaxDelay` (default: `01:00:00`): cap for backoff growth. - `RetryJitterEnabled` (default: `false`) and `RetryJitterRatio` (default: `0.2`): spread retry pressure across time. - `PollJitterEnabled` (default: `true`) and `PollJitterRatio` (default: `0.2`): spread poll pressure across workers. ### Recurring behavior - `Recurring.CatchUpPolicy` (default: `SkipMissed`): avoids large catch-up bursts after downtime. - `ExistingJobBehavior` (default: `KeepDatabase`): keeps existing schedule definitions stable by default. - `OrphanedJobBehavior` (default: `Disable`): safely disables schedules removed from code. ### Retention - `Retention.Enabled` (default: `true`): enables cleanup for terminal runs. - `RunRetentionSeconds` (default: `null`, effective `86400` for durable DB providers and `3600` for in-memory): how long completed/failed runs remain queryable. - `SweepIntervalSeconds` (default: `300`) and `DeleteBatchSize` (default: `1000`): cleanup cadence and batch sizing. ### Optional hosted observability - `Eventing.TenantId` (default: `null`) and `Eventing.ClientSecret` (default: `null`): enable ingestion when both are set. - When ingestion is enabled, runtime information is included in posted ingestion data. ## Defaults-first guidance In most projects, you only need to set: - provider selection and connection string - optional `WorkerName` convention - any non-default values that match your workload goals Everything else can stay at defaults until you have data that justifies tuning. ## What to tune first in production - Increase `MaxConcurrentRuns` and `BatchSize` carefully as workload grows. - Keep `LeaseDurationSeconds` longer than normal job execution for your slowest routine jobs. - Use retry jitter to reduce synchronized retries during external outages. - Set retention windows based on audit/debug needs and database cost profile. ## Full options reference ```csharp using DurableStack.Core.Options; using DurableStack.Hosting.DependencyInjection; builder.Services.AddDurableStack(options => { options.StorageProvider = DurableStackStorageProvider.InMemory; // Backing store selection when using property-based config; default: InMemory. options.Postgres.ConnectionString = "Host=localhost;Port=5432;Database=durable_stack;Username=postgres;Password=postgres"; // PostgreSQL connection string used when StorageProvider is Postgres; default: "". options.SqlServer.ConnectionString = "Server=localhost;Database=durable_stack;Trusted_Connection=True;TrustServerCertificate=True"; // SQL Server connection string used when StorageProvider is SqlServer; default: "". options.Sqlite.ConnectionString = "Data Source=durable_stack.db"; // SQLite connection string used when StorageProvider is Sqlite; default: "". options.MySql.ConnectionString = "Server=localhost;Port=3306;Database=durable_stack;Uid=root;Pwd=password"; // MySQL connection string used when StorageProvider is MySql; default: "". options.JobActivation = DurableStackJobActivationMode.ScopedPerExecution; // Job activation model (ScopedPerExecution or RootProvider); default: ScopedPerExecution. options.WorkerName = DurableStackOptions.CreateDefaultWorkerName(); // Unique worker identity for leasing and diagnostics; default: "{HOSTNAME-or-machine}-{processId}". options.DatabaseTablePrefix = null; // Optional prefix for durable database objects; default: null. options.PollInterval = TimeSpan.FromSeconds(5); // Polling cadence for fetching available work; default: 00:00:05. options.PollIntervalSeconds = 5; // Same as PollInterval but in seconds (<= 0 resets to default 5); default: 5. options.BatchSize = 50; // Max runs leased per poll cycle; default: 50. options.LeaseDuration = TimeSpan.FromSeconds(30); // Lease TTL before another worker can recover stalled runs; default: 00:00:30. options.LeaseDurationSeconds = 30; // Same as LeaseDuration but in seconds (<= 0 resets to default 30); default: 30. options.PollJitterEnabled = true; // Enables random jitter on poll delays; default: true. options.PollJitterRatio = 0.2; // Jitter ratio applied when PollJitterEnabled is true; default: 0.2. options.RetryDelay = TimeSpan.FromSeconds(5); // Base retry delay for failed attempts; default: 00:00:05. options.RetryMaxDelay = TimeSpan.FromHours(1); // Retry backoff upper bound; default: 01:00:00. options.RetryJitterEnabled = false; // Enables random jitter on retry delays; default: false. options.RetryJitterRatio = 0.2; // Jitter ratio applied when RetryJitterEnabled is true; default: 0.2. options.Eventing.TenantId = null; // Tenant ID for hosted ingestion auth; default: null. options.Eventing.ClientSecret = null; // Client secret for hosted ingestion auth; default: null. options.Eventing.IngestionApiBaseUrl = "https://api.durablestack.com"; // Base URL for event ingestion API; default: https://api.durablestack.com. options.Eventing.IngestionPath = "/v1/events/batch"; // Relative ingestion endpoint path; default: /v1/events/batch. options.Eventing.IngestionMaxBatchSize = 100; // Max events sent per ingestion request; default: 100. options.Eventing.IngestionMaxRequestBodyBytes = 1_000_000; // Max request body size per ingestion post; default: 1,000,000. options.Eventing.IngestionMaxRetryAttempts = 5; // Max retry attempts for failed ingestion posts; default: 5. options.Eventing.IngestionFlushInterval = TimeSpan.FromSeconds(5); // Flush interval for telemetry publishing; default: 00:00:05. options.Eventing.IngestionFlushIntervalSeconds = 5; // Same as IngestionFlushInterval in seconds (<= 0 resets to default 5); default: 5. options.Eventing.IncludeErrorDetail = false; // Includes captured error detail text in events when true; default: false. options.Eventing.MaxErrorDetailLength = 4096; // Max captured error-detail characters per event (<= 0 uses 4096); default: 4096. options.Recurring.CatchUpPolicy = RecurringCatchUpPolicy.SkipMissed; // Recurring schedule behavior after downtime (SkipMissed or CatchUp); default: SkipMissed. options.Recurring.RegistrationSync.ExistingJobBehavior = ExistingRecurringJobBehavior.KeepDatabase; // Existing recurring definitions: keep DB values or update from code; default: KeepDatabase. options.Recurring.RegistrationSync.OrphanedJobBehavior = OrphanedRecurringJobBehavior.Disable; // Jobs removed from code: disable or ignore in DB; default: Disable. options.Retention.Enabled = true; // Enables automatic cleanup of terminal runs; default: true. options.Retention.RunRetentionSeconds = null; // Terminal run retention window in seconds; default: null (effective default = 3600 in-memory, 86400 durable stores). options.Retention.SweepIntervalSeconds = 300; // Retention cleanup sweep interval in seconds (<= 0 uses 300); default: 300. options.Retention.DeleteBatchSize = 1000; // Max rows removed per retention cleanup batch (<= 0 uses 1000); default: 1000. options.JobRegistration.AutoDiscoverJobsFromAssembly = true; // Auto-registers public IDurableJob/IDurableJob from the app assembly; default: true. }); ``` ## Store helper methods Use one of these helpers inside the same `AddDurableStack` callback when you prefer method-based store selection. ```csharp builder.Services.AddDurableStack(options => { options.UseInMemory(); // Selects in-memory store; default if no store is selected. // options.UsePostgres("Host=..."); // Selects PostgreSQL store and sets connection string; use only one Use* store method at a time (last call wins). // options.UseSqlServer("Server=..."); // Selects SQL Server store and sets connection string; use only one Use* store method at a time (last call wins). // options.UseSqlite("Data Source=..."); // Selects SQLite store and sets connection string; use only one Use* store method at a time (last call wins). // options.UseMySql("Server=..."); // Selects MySQL store and sets connection string; use only one Use* store method at a time (last call wins). }); ``` ## Notes - Setting both `PollInterval` and `PollIntervalSeconds` is redundant; use one style. - Setting both `LeaseDuration` and `LeaseDurationSeconds` is redundant; use one style. - Setting both `Eventing.IngestionFlushInterval` and `Eventing.IngestionFlushIntervalSeconds` is redundant; use one style. - Event ingestion is automatically enabled when both `Eventing.TenantId` and `Eventing.ClientSecret` are set. - Runtime event ingestion payloads include runtime information when ingestion is enabled. ## Supported .NET versions - `.NET 9` - `.NET 10` ### Start Here URL: /docs/v1.0/dotnet/start-here # Start Here Use this section when you are new to DurableStack or onboarding a teammate. ## In this section - [Why DurableStack](why-durablestack) - [Quick Start](quick-start) - [Getting Started](getting-started) - [First Job Example](first-job-example) ### Quick Start URL: /docs/v1.0/dotnet/start-here/quick-start # Quick Start This is the fastest path to a running recurring job using PostgreSQL. All configuration is set in C#. Supported runtime targets: `.NET 9` and `.NET 10`. ## 1) Install package ```bash dotnet add package DurableStack.Hosting ``` ## 2) Create a recurring job ```csharp using DurableStack.Core.Abstractions; using DurableStack.Core.Models; using DurableStack.Hosting.DependencyInjection; [DurableJob(Name = "heartbeat-every-minute", MaxAttempts = 3)] [RecurringJob("* * * * *", TimeZone = "UTC")] public sealed class HeartbeatJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) { Console.WriteLine($"Heartbeat from run {context.RunId}"); return Task.CompletedTask; } } ``` ## 3) Configure DurableStack in `Program.cs` ```csharp using DurableStack.Core.Abstractions; using DurableStack.Core.Options; using DurableStack.Hosting.DependencyInjection; var builder = WebApplication.CreateBuilder(args); var connectionString = "REAL_CONNECTION_STRING"; builder.Services.AddDurableStackPostgres(connectionString, options => { // Poll jitter is enabled by default; tune ratio or disable explicitly if needed. options.PollJitterRatio = 0.2; // options.PollJitterEnabled = false; // Optional hosted observability options.Eventing.TenantId = null; options.Eventing.ClientSecret = null; }); var app = builder.Build(); app.Run(); ``` ## 4) Run and verify - Run the app. - Wait one minute. - Confirm `HeartbeatJob` runs in application logs. ## 5) Optional: enable hosted observability Set these two values in the same `AddDurableStackPostgres` options block: - `options.Eventing.TenantId = "your-tenant-id";` - `options.Eventing.ClientSecret = "your-client-secret";` When both are set, event ingestion is enabled automatically and posted ingestion data includes runtime information. ## Continue - [Getting Started](getting-started) - [First Job Example](first-job-example) - [Recurring Jobs](../usage-guide/recurring-jobs) ### Why DurableStack URL: /docs/v1.0/dotnet/start-here/why-durablestack # Why DurableStack I built DurableStack because I wanted background job infrastructure that felt simple to adopt but was designed for real distributed production environments from the beginning. ## The problem this solves - In many systems, background work starts simple and then grows into multi-instance deployment. - At that point, correctness and operational visibility become the hard part. - I wanted a runtime that stays practical for small teams while scaling into distributed execution without changing core architecture. ## How DurableStack compares to tools like HangFire DurableStack and HangFire solve related problems, but DurableStack intentionally emphasizes a few design choices: ### 1) Built for distribution from day 1 - Lease ownership, reclaim behavior, and worker identity are first-class runtime concerns. - The default mental model assumes multiple workers and rolling deployments. - This keeps scaling behavior predictable instead of being an afterthought. ### 2) Free observability website - You can connect runtimes to the hosted DurableStack observability site at no cost. - This gives teams run visibility, failure insight, and trend data without building internal dashboards first. - It is optional, but available immediately. ### 3) Uses only your existing database to coordinate runs - DurableStack coordinates job execution using the database you already operate. - Supported providers include PostgreSQL, SQL Server, SQLite, and MySQL. - There is no required sidecar queue system to run the core runtime. ### 4) No new or external runtime dependencies required - You do not need Redis, RabbitMQ, Kafka, or a separate orchestration service just to run jobs. - Adoption can start inside an existing .NET service and existing deployment model. - Optional integrations (observability, telemetry export) layer on top instead of becoming required infrastructure. ## What this means for a future developer - The runtime model is consistent from local development to distributed production. - Operational behavior is explicit instead of hidden behind implicit infrastructure assumptions. - The same codebase can evolve without introducing a second operational stack just for background jobs. ### In-Memory URL: /docs/v1.0/dotnet/storage-providers/in-memory # In-Memory In-memory mode is useful for tests, demos, and fast local iteration. It does not provide shared state across processes. ## Registration ```csharp builder.Services.AddDurableStack(options => { options.UseInMemory(); options.WorkerName = $"local-{Environment.ProcessId}"; }); ``` ## Behavior - No external database required. - Job/run state exists only in process memory. - Data is lost on process restart. ## Important limitations - Not durable across restarts. - Not valid for distributed multi-worker execution. - Default run retention is 1 hour (vs 24 hours for durable DB providers). ### Storage Providers URL: /docs/v1.0/dotnet/storage-providers # Storage Providers DurableStack supports multiple storage providers with a shared contract and provider-specific tradeoffs. For distributed execution, a shared durable database provider is required. Multiple workers coordinate through the shared store. If you use in-memory storage, execution is single-process only. ## In this section - [Overview](overview) - [Provider Contract](provider-contract) - [PostgreSQL](postgresql) - [SQL Server](sql-server) - [SQLite](sqlite) - [MySQL](mysql) - [In-Memory](in-memory) ### MySQL URL: /docs/v1.0/dotnet/storage-providers/mysql # MySQL MySQL is a durable provider option for teams already operating MySQL in production. It supports shared worker coordination through DurableStack's standard run/lease contract. ## Registration ```csharp using DurableStack.Hosting.DependencyInjection; var connectionString = "Server=localhost;Port=3306;Database=durable_stack;User ID=root;Password=postgres;SslMode=Preferred"; builder.Services.AddDurableStackMySql(connectionString, options => { options.WorkerName = $"orders-api-{Environment.MachineName}-{Environment.ProcessId}"; }); ``` ## Operational notes - Validate throughput and lock behavior with expected worker count. - Prefer secure transport and least-privilege database credentials. - Keep retention settings aligned with storage cost and debugging needs. ### Overview URL: /docs/v1.0/dotnet/storage-providers/overview # Overview Provider choice directly determines durability behavior and whether multi-worker distributed execution is possible. ## Distributed execution requirement Distributed execution requires a shared durable database provider. Workers coordinate run claiming, lease ownership, retries, and recurring state through shared store records. If the provider is in-memory, there is no shared cross-process state, so distributed execution is not supported. ## Retention defaults by provider type From runtime code defaults: - Durable database providers default run retention: 24 hours. - In-memory provider default run retention: 1 hour. These are the effective defaults when `Retention.RunRetentionSeconds` is not explicitly set. ## General guidance - Prefer PostgreSQL for most production distributed workloads. - Use SQL Server where organizational standards require it. - Use MySQL where it is already a first-class platform dependency. - Use SQLite for local development or single-instance durable scenarios. - Use in-memory for tests, demos, and short-lived local runs. ## Quick selection matrix - Distributed production: PostgreSQL, SQL Server, or MySQL. - Single-instance durable local app: SQLite. - Unit/integration test harnesses: In-memory. ### PostgreSQL URL: /docs/v1.0/dotnet/storage-providers/postgresql # PostgreSQL PostgreSQL is the recommended default for most production DurableStack deployments. It supports durable state and multi-worker distributed coordination through shared tables. ## Registration ```csharp using DurableStack.Hosting.DependencyInjection; var connectionString = "Host=localhost;Port=5432;Database=durable_stack;Username=postgres;Password=postgres"; builder.Services.AddDurableStackPostgres(connectionString, options => { options.WorkerName = $"orders-api-{Environment.MachineName}-{Environment.ProcessId}"; }); ``` ## Operational notes - Run migrations on startup via `IDurableStackStoreMigrator`. - Keep retention enabled to control run-history growth. - Validate multi-worker behavior in staging before scaling out. ### Provider Contract URL: /docs/v1.0/dotnet/storage-providers/provider-contract # Provider Contract All providers must implement the same functional contract so runtime behavior remains consistent. The contract is represented primarily by `IDurableJobStore` and, for durable providers, `IDurableStackStoreMigrator`. ## Required capabilities - Enqueue immediate and delayed runs. - Claim due runs safely with lease ownership. - Mark runs succeeded or failed with retry metadata. - Query runs by ID, status, and job. - Support recurring job state operations. - Support retention pruning behavior. - Extend run leases during execution heartbeat. ## Distributed safety expectations Durable providers must support safe concurrent claiming semantics so multiple workers do not actively process the same run under normal conditions. Typical provider strategies include row locks and skip-locked style claim paths. ## Migration and initialization Durable providers are expected to support migration/init through `IDurableStackStoreMigrator`. In-memory provider is an exception and uses a no-op migrator. ## Table naming Default logical table names: - `durable_stack_jobs` - `durable_stack_job_runs` - `durable_stack_job_locks` When `DatabaseTablePrefix` is set, providers prepend the prefix to durable objects. ### SQL Server URL: /docs/v1.0/dotnet/storage-providers/sql-server # SQL Server SQL Server is a durable provider option for environments standardized on Microsoft data infrastructure. It supports shared run coordination across workers through the same DurableStack contract. ## Registration ```csharp using DurableStack.Hosting.DependencyInjection; var connectionString = "Server=localhost;Database=durable_stack;User Id=sa;Password=Password123!;TrustServerCertificate=true"; builder.Services.AddDurableStackSqlServer(connectionString, options => { options.WorkerName = $"billing-api-{Environment.MachineName}-{Environment.ProcessId}"; }); ``` ## Operational notes - Prefer secure transport settings in production (`Encrypt=True`, trusted cert chain). - Validate lock/throughput behavior under expected concurrency. - Keep table-prefix strategy consistent across environments if used. ### SQLite URL: /docs/v1.0/dotnet/storage-providers/sqlite # SQLite SQLite provides durable local persistence with minimal setup. It is a strong fit for local development and small single-instance deployments. ## Registration ```csharp using DurableStack.Hosting.DependencyInjection; var connectionString = "Data Source=durable_stack.db"; builder.Services.AddDurableStackSqlite(connectionString, options => { options.WorkerName = $"local-api-{Environment.MachineName}-{Environment.ProcessId}"; }); ``` ## Operational notes - Use persistent volume storage if data must survive container restarts. - SQLite is generally not the first choice for high-scale distributed clusters. - Keep retention/pruning enabled to avoid local file growth. ### Background Processing Patterns URL: /docs/v1.0/dotnet/usage-guide/background-processing-patterns # Background Processing Patterns Use these patterns to keep background processing reliable as load and team size increase. ## Idempotent handlers Design handlers so re-execution is safe. Patterns: - Upsert instead of blind insert. - Check external side effect state before issuing duplicate calls. - Use deterministic business keys for deduplication. ## Thin job, rich domain service Keep job classes focused on orchestration and call domain services for business logic. Benefits: - easier testing - clearer retry boundaries - cleaner dependency graph ## Retry-aware external calls When calling external systems: - treat transient failures as expected - use `RetryBehavior.Backoff` for dependency outages - keep terminal errors explicit and observable ## Concurrency boundaries For recurring jobs that mutate shared resources: - set `AllowConcurrentRuns = false` - keep execution windows shorter than schedule interval when possible ## Operational visibility pattern Track by `RunId` and `JobName` in logs and traces so run-history and app diagnostics connect cleanly. ```csharp _logger.LogInformation("Processing invoice batch. Job={JobName} RunId={RunId} Attempt={Attempt}", context.JobName, context.RunId, context.Attempt); ``` ### Example Tasks URL: /docs/v1.0/dotnet/usage-guide/example-tasks # Example Tasks These examples show common task shapes you can adapt quickly. ## Email notification job (typed payload) ```csharp using DurableStack.Core; using DurableStack.Core.Abstractions; using DurableStack.Hosting.DependencyInjection; [DurableJob(Name = "send-welcome-email", MaxAttempts = 5)] public sealed class SendWelcomeEmailJob : IDurableJob { private readonly ILogger _logger; public SendWelcomeEmailJob(ILogger logger) { _logger = logger; } public Task ExecuteAsync(SendWelcomeEmailArgs args, JobContext context, CancellationToken cancellationToken) { _logger.LogInformation("Sending welcome email to {Email}. RunId={RunId}", args.Email, context.RunId); return Task.CompletedTask; } } public sealed class SendWelcomeEmailArgs { public string Email { get; set; } = string.Empty; } ``` Enqueue: ```csharp var runId = await durableClient.EnqueueAsync(new SendWelcomeEmailArgs { Email = "hello@example.com" }, cancellationToken); ``` ## Recurring cleanup job ```csharp [DurableJob(Name = "cleanup-expired-sessions", MaxAttempts = 3)] [RecurringJob("0 */1 * * *", TimeZone = "UTC", AllowConcurrentRuns = false)] public sealed class CleanupExpiredSessionsJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) { return Task.CompletedTask; } } ``` ## Third-party sync job with backoff ```csharp [DurableJob( Name = "sync-third-party-orders", MaxAttempts = 6, RetryBehavior = RetryBehavior.Backoff, RetryInitialDelaySeconds = 15)] [RecurringJob("*/10 * * * *", TimeZone = "UTC", AllowConcurrentRuns = false)] public sealed class ThirdPartyOrderSyncJob : IDurableJob { public async Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) { await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken); } } ``` ## Guidance - Use typed jobs when payload shape is part of the contract. - Prefer non-overlapping recurring jobs for mutable resources. - Use retry backoff for external dependency instability. ### Usage Guide URL: /docs/v1.0/dotnet/usage-guide # Usage Guide This section focuses on day-to-day usage patterns and examples. The goal is practical implementation guidance you can apply directly in application code. ## In this section - [Example Tasks](example-tasks) - [Background Processing Patterns](background-processing-patterns) - [Recurring Jobs](recurring-jobs) - [One-Off and Delayed Jobs](one-off-and-delayed-jobs) ### One-Off and Delayed Jobs URL: /docs/v1.0/dotnet/usage-guide/one-off-and-delayed-jobs # One-Off and Delayed Jobs Use `IDurableStackClient` for ad-hoc and scheduled-once execution. ## Enqueue immediately ```csharp var runId = await durableClient.EnqueueAsync( new SendWelcomeEmailArgs { Email = "hello@example.com" }, cancellationToken); ``` ## Schedule for later ```csharp var runAtUtc = DateTimeOffset.UtcNow.AddMinutes(30); var runId = await durableClient.ScheduleAsync( new SendWelcomeEmailArgs { Email = "later@example.com" }, runAtUtc, cancellationToken); ``` ## Cancel a run ```csharp var cancelled = await durableClient.CancelRunAsync(runId, cancellationToken); ``` ## Query scheduled and recent runs ```csharp var recent = await runQueryService.GetRecentRunsAsync(50, cancellationToken); var pending = await runQueryService.GetRunsByStatusAsync("pending", 50, cancellationToken); ``` ## Practical guidance - Use delayed jobs for time-based follow-up work that is not recurring. - Keep payload DTOs version-safe and backward-compatible where possible. - Use run query APIs to verify enqueue/schedule behavior in integration tests. ### Recurring Jobs URL: /docs/v1.0/dotnet/usage-guide/recurring-jobs # Recurring Jobs Use recurring attributes for schedule definition and `IDurableScheduleAdminService` for runtime schedule operations. ## Define a recurring job ```csharp using DurableStack.Core.Abstractions; using DurableStack.Hosting.DependencyInjection; [DurableJob(Name = "daily-reconciliation", MaxAttempts = 4)] [RecurringJob("0 2 * * *", TimeZone = "UTC", Enabled = true, AllowConcurrentRuns = false)] public sealed class DailyReconciliationJob : IDurableJob { public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; } ``` ## Supported operations - list all schedules (`includeDisabled` optional) - disable or enable by schedule name - run schedule now (ad-hoc enqueue) - update cron expression and time zone ## Service usage ```csharp var scheduledJobs = await scheduleAdmin.ListScheduledJobsAsync(includeDisabled: true, cancellationToken); var disabled = await scheduleAdmin.SetScheduledJobEnabledAsync( jobName: "heartbeat-every-minute", enabled: false, cancellationToken); var updatedCron = await scheduleAdmin.UpdateScheduledJobCronAsync( jobName: "heartbeat-every-minute", cronExpression: "*/5 * * * *", timeZone: "UTC", cancellationToken); var runNowQueued = await scheduleAdmin.RunScheduledJobNowAsync( jobName: "heartbeat-every-minute", cancellationToken); ``` ## Operational behavior - Disabled schedules set `nextRunAtUtc` to null until re-enabled. - Enabling recomputes `nextRunAtUtc` from current time. - `RunScheduledJobNowAsync` enqueues an ad-hoc run and does not mutate cron schedule. ## Typical integration pattern Most teams wrap this service in their own application command handlers or admin APIs. ```csharp public sealed class ScheduleCommands { private readonly IDurableScheduleAdminService _scheduleAdmin; public ScheduleCommands(IDurableScheduleAdminService scheduleAdmin) { _scheduleAdmin = scheduleAdmin; } public Task DisableAsync(string jobName, CancellationToken ct) => _scheduleAdmin.SetScheduledJobEnabledAsync(jobName, enabled: false, ct); } ``` ## Language: TypeScript ### DurableStack for TypeScript (Planned) URL: /docs/v1.0/typescript # DurableStack for TypeScript DurableStack is a runtime for reliable background jobs, recurring schedules, retries, and operational visibility. TypeScript support is planned, but it is not available in `v1.0` yet. For now, use the `.NET` documentation in this version. ## Language: Python ### DurableStack for Python (Planned) URL: /docs/v1.0/python # DurableStack for Python DurableStack is a runtime for reliable background jobs, recurring schedules, retries, and operational visibility. Python support is planned, but it is not available in `v1.0` yet. For now, use the `.NET` documentation in this version.