Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api-reference/scheduler-api/schedule.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The `ScheduleOptions` fluent builder consolidates all optional scheduling parame
| `.Timeout(TimeSpan)` | Job execution timeout. `null` uses global default. |
| `.OnMisfire(MisfirePolicy)` | Misfire policy for missed runs. `FireOnceNow` (default) fires immediately; `DoNothing` skips and waits for the next natural occurrence. Only applies to Cron and Interval types. |
| `.MisfireThreshold(TimeSpan)` | Grace period before the misfire policy takes effect. Overrides the global `DefaultMisfireThreshold`. |
| `.Exclude(Exclusion)` | Adds an exclusion window. The manifest is skipped when any exclusion matches the current time. Multiple can be combined. Use `Exclude.DaysOfWeek(...)`, `Exclude.Dates(...)`, `Exclude.DateRange(...)`, or `Exclude.TimeWindow(...)` factories. See [Exclusion Windows]({{ site.baseurl }}{% link scheduler/exclusions.md %}). |

### Group-level methods

Expand Down
14 changes: 14 additions & 0 deletions api-reference/scheduler-api/scheduling-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ Determines behavior when a scheduled run is missed.

See [Misfire Policies]({{ site.baseurl }}{% link scheduler/scheduling-options.md %}#misfire-policies) for detailed behavior and examples.

### ExclusionType Enum

Defines the kind of exclusion window for a manifest schedule. Used inside the JSONB `exclusions` column.

| Value | Description |
|-------|-------------|
| `DaysOfWeek` | Exclude specific days of the week (e.g., weekends) |
| `Dates` | Exclude specific dates (e.g., holidays) |
| `DateRange` | Exclude a contiguous date range (start–end inclusive) |
| `TimeWindow` | Exclude a daily time window (supports midnight crossover) |

See [Exclusion Windows]({{ site.baseurl }}{% link scheduler/exclusions.md %}) for usage patterns and examples.

---

## ManifestOptions
Expand All @@ -154,6 +167,7 @@ public class ManifestOptions
| `Priority` | `int` | `0` | Manifest-level priority stored on the manifest record. Note: dispatch ordering is primarily determined by **ManifestGroup.Priority** (set from the dashboard). This manifest-level priority is used as the work queue entry's priority when the manifest is queued. For dependent manifests, `DependentPriorityBoost` (default 16) is added on top at dispatch time. Can also be set via the `priority` parameter on scheduling methods. |
| `MisfirePolicy` | `MisfirePolicy?` | `null` | Per-manifest misfire policy override. `null` uses the global `DefaultMisfirePolicy`. Only applies to Cron and Interval schedule types. See [Misfire Policies]({{ site.baseurl }}{% link scheduler/scheduling-options.md %}#misfire-policies). |
| `MisfireThreshold` | `TimeSpan?` | `null` | Per-manifest misfire threshold override. `null` uses the global `DefaultMisfireThreshold` (60 seconds). |
| `Exclusions` | `List<Exclusion>` | `[]` | Exclusion windows for this manifest. When any exclusion matches the current time, the manifest is skipped. Excluded periods are "intentionally skipped", not misfires. See [Exclusion Windows]({{ site.baseurl }}{% link scheduler/exclusions.md %}). |

### Example

Expand Down
8 changes: 4 additions & 4 deletions comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ All three run background work in .NET. They solve different problems.
|---|---|---|---|
| Cron expressions | 5-field (minute granularity) | 6-7 field (second granularity) | 5-field via `Cron.*` helpers |
| Simple intervals | `Every.Minutes(5)` | `SimpleTrigger` with repeat count | `TimeSpan` delay |
| Calendar exclusions | No | Yes — holidays, weekends, custom calendars | No |
| Daily time windows | No | Yes — "9am-5pm Mon-Fri every hour" | No |
| Calendar exclusions | Yes — `Exclude.DaysOfWeek()`, `Exclude.Dates()`, `Exclude.DateRange()` | Yes — holidays, weekends, custom calendars | No |
| Daily time windows | Yes — `Exclude.TimeWindow()` (supports midnight crossover) | Yes — "9am-5pm Mon-Fri every hour" | No |
| DST-aware intervals | No | Yes — `PreserveHourOfDayAcrossDaylightSavings` | No |
| Fire-and-forget | `TriggerAsync(externalId)` | `scheduler.TriggerJob(key)` | `BackgroundJob.Enqueue(() => ...)` |
| Delayed one-off | `ScheduleOnceAsync(input, delay)` or `TriggerAsync(id, delay)` | Trigger with future start time | `BackgroundJob.Schedule(delay)` |
| Misfire policies | Implicit (run if overdue) | 6+ explicit policies per trigger type | N/A (queue-based) |

Quartz.NET has the richest time-based scheduling. If you need "every 30 seconds between 9am and 5pm on weekdays, excluding holidays," Quartz is the only option. Trax covers the common cases — cron and intervals — but doesn't attempt calendar-aware scheduling.
Quartz.NET has the richest time-based scheduling with second-granularity cron and DST-aware triggers. Trax now supports calendar exclusions (days of week, specific dates, date ranges, and daily time windows) alongside cron and interval schedules. For advanced scenarios like "every 30 seconds between 9am and 5pm," Quartz's sub-minute cron is still required.

### Job Composition

Expand Down Expand Up @@ -124,7 +124,7 @@ Hangfire wins on ceremony. You can go from zero to a running background job in f

**You need a simple background job processor.** If the job is "send this email in 5 minutes" or "resize this image asynchronously," Hangfire does it in one line. Trax now supports delayed one-off jobs via `ScheduleOnceAsync`, but the train/step/manifest model still adds ceremony compared to Hangfire's lambda-based approach for simple fire-and-forget work with no internal structure.

**You need precise time-based scheduling.** If the requirement is "every 15 seconds," "only on business days excluding holidays," or "between 9am and 5pm with DST handling," Quartz.NET has scheduling primitives that Trax doesn't. Trax covers cron and intervals — the common cases — but not calendar-aware or sub-minute cron scheduling.
**You need sub-minute or DST-aware scheduling.** Trax now supports calendar exclusions (weekends, holidays, date ranges, time windows), but if you need second-granularity cron ("every 15 seconds") or DST-aware interval handling, Quartz.NET has scheduling primitives that Trax doesn't.

**You're not on PostgreSQL.** Trax's distributed coordination relies on PostgreSQL advisory locks and `FOR UPDATE SKIP LOCKED`. There is no SQL Server, MySQL, or Redis backend. If your infrastructure is on a different database, Quartz.NET or Hangfire will work; Trax won't.

Expand Down
106 changes: 106 additions & 0 deletions scheduler/exclusions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
layout: default
title: Exclusion Windows
parent: Scheduling
nav_order: 8
---

# Exclusion Windows

Exclusion windows let you skip execution during specific periods — weekends, holidays, maintenance windows, or daily time ranges. When any exclusion matches the current time, the manifest is skipped. Excluded periods are treated as **intentionally skipped**, not as misfires.

## Exclusion Types

### DaysOfWeek

Skip specific days of the week:

```csharp
scheduler.Schedule<IReportTrain>(
"daily-report",
new ReportInput(),
Cron.Daily(hour: 3),
o => o.Exclude(Exclude.DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday)));
```

### Dates

Skip specific dates (e.g., holidays):

```csharp
var holidays = new[]
{
new DateOnly(2026, 12, 25), // Christmas
new DateOnly(2027, 1, 1), // New Year's Day
};

scheduler.Schedule<IReportTrain>(
"daily-report",
new ReportInput(),
Cron.Daily(hour: 3),
o => o.Exclude(Exclude.Dates(holidays)));
```

### DateRange

Skip a contiguous date range (start and end inclusive):

```csharp
scheduler.Schedule<IReportTrain>(
"daily-report",
new ReportInput(),
Cron.Daily(hour: 3),
o => o.Exclude(Exclude.DateRange(
new DateOnly(2026, 12, 23),
new DateOnly(2027, 1, 2))));
```

### TimeWindow

Skip a daily time window. Supports midnight crossover (e.g., 23:00-02:00):

```csharp
scheduler.Schedule<ISyncTrain>(
"sync-hourly",
new SyncInput(),
Cron.Hourly(),
o => o.Exclude(Exclude.TimeWindow(
TimeOnly.Parse("02:00"),
TimeOnly.Parse("04:00"))));
```

## Combining Exclusions

Multiple exclusions can be combined. If **any** exclusion matches, the manifest is skipped:

```csharp
scheduler.Schedule<IReportTrain>(
"weekday-report",
new ReportInput(),
Cron.Daily(hour: 8),
o => o
.Exclude(Exclude.DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday))
.Exclude(Exclude.Dates(new DateOnly(2026, 12, 25)))
.Exclude(Exclude.TimeWindow(TimeOnly.Parse("02:00"), TimeOnly.Parse("04:00"))));
```

## Interaction with Misfire Policies

Excluded periods are **intentionally skipped** — they are not considered misfires. When the excluded period ends, normal scheduling resumes. If the manifest is overdue at that point, the existing misfire policy determines what happens:

- **`FireOnceNow`** (default): the manifest fires immediately to catch up
- **`DoNothing`**: the scheduler checks the most recent interval boundary and fires only if within the misfire threshold

This interaction is usually correct:
- A daily report excluded on weekends with `FireOnceNow` fires once on Monday morning to catch up
- The same report with `DoNothing` waits for the next natural 3am occurrence

## Storage

Exclusions are stored as a JSONB column on the manifest table. They are configured per-manifest via the `ScheduleOptions` fluent builder — there are no global exclusion defaults. Exclusions apply to all schedule types (Cron, Interval, Once). Dependent manifests are triggered by parent completion rather than time, so exclusions on dependent manifests are not evaluated during normal scheduling.

## Dashboard

The ManifestDetailPage displays configured exclusion windows in an "Exclusion Windows" card when present, showing the type and formatted description of each exclusion.

*API Reference: [ScheduleOptions — Exclude()]({{ site.baseurl }}{% link api-reference/scheduler-api/schedule.md %}#scheduleoptions)*
23 changes: 23 additions & 0 deletions scheduler/scheduling-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,29 @@ See [Dormant Dependents](dependent-trains.md#dormant-dependents) for full detail

See [Dependent Trains](dependent-trains.md) for details on chaining trains, and [Delayed / One-Off Jobs](delayed-jobs.md) for one-off scheduling.

## Exclusion Windows

Exclusion windows skip execution during specific periods — weekends, holidays, maintenance windows, or daily time ranges. Add exclusions via the `.Exclude()` method on `ScheduleOptions`:

```csharp
scheduler.Schedule<IReportTrain>(
"daily-report",
new ReportInput(),
Cron.Daily(hour: 3),
o => o
.Exclude(Exclude.DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday))
.Exclude(Exclude.Dates(new DateOnly(2026, 12, 25)))
.Exclude(Exclude.TimeWindow(TimeOnly.Parse("02:00"), TimeOnly.Parse("04:00"))));
```

Multiple exclusions can be combined — if ANY matches, the manifest is skipped. Excluded periods are "intentionally skipped", not misfires. When the excluded period ends, normal scheduling resumes and the misfire policy determines catch-up behavior.

Four built-in exclusion types: `DaysOfWeek`, `Dates`, `DateRange`, `TimeWindow` (supports midnight crossover).

See [Exclusion Windows](exclusions.md) for full details, examples, and misfire interaction.

*API Reference: [ScheduleOptions — Exclude()]({{ site.baseurl }}{% link api-reference/scheduler-api/schedule.md %}#scheduleoptions)*

## Misfire Policies

A **misfire** occurs when a scheduled job was supposed to fire but couldn't — the scheduler was down, or the job was blocked by an active execution or dead letter. When the scheduler recovers, the misfire policy determines what happens.
Expand Down