7. Jobs¶
Books get borrowed. Due dates pass. Someone needs to flag the overdue ones. That's a background job — a scheduled task that runs on a cron expression and does its work through the same effect pipeline as everything else.
Define the Job¶
[BackgroundJob(Schedule = "0 2 * * *")]
public record CheckOverdueLoansJob;
That's a cron expression for "2 AM every day." The [BackgroundJob] attribute also supports MaxRetries, RetryDelayMs, TimeoutMs, and Priority — but the defaults are fine for a nightly check.
The Handler¶
public static class CheckOverdueLoansHandler
{
[JobHandler]
public static Eff<LendingRuntime, Unit> Handle(CheckOverdueLoansJob _) =>
from overdueLoans in LendingStore.Loans.QueryPage<LendingRuntime>(
1, 1000,
filter: l => l.ReturnedAt == null && l.DueAt < DateTime.UtcNow && !l.IsOverdue)
from __ in overdueLoans.Data.ForEachEff<LendingRuntime, Loan>(loan =>
LendingStore.Loans.Save<LendingRuntime>(loan with { IsOverdue = true }))
select unit;
}
Query for unreturned, past-due, not-yet-flagged loans. For each one, save it with IsOverdue = true. ForEachEff processes each item sequentially — if one fails, the rest still run (it doesn't short-circuit on individual items).
The with expression creates a new Loan record — the original is never mutated. Immutability all the way down.
What the Generator Produces¶
The [BackgroundJob] + [JobHandler] combination generates:
- A
DeepstagingJobWorkerhosted service that processes queued jobs - A cron registry mapping job types to their schedules
- A
CronSchedulerServicethat enqueues jobs on their cron schedule
Remember the [Uses(typeof(JobsModule))] on LendingRuntime from Chapter 5? That's what makes the job scheduler available as a runtime capability.
Test It¶
Job handlers are just effect methods — test them directly:
[Test]
public async Task CheckOverdueLoans_FlagsOverdueLoans()
{
var runtime = TestLendingRuntime.Create();
runtime.LoanStore.Seed(new Loan
{
Id = LoanId.New(),
PatronId = PatronId.New(),
BookId = BookId.New(),
BorrowedAt = DateTime.UtcNow.AddDays(-30),
DueAt = DateTime.UtcNow.AddDays(-16),
IsOverdue = false
});
var fin = await CheckOverdueLoansHandler
.Handle(new CheckOverdueLoansJob())
.RunAsync(runtime);
await Assert.That(fin).IsSucc();
await Assert.That(runtime.LoanStore.SaveCalls).HasCount().EqualTo(1);
await Assert.That(runtime.LoanStore.SaveCalls[0].IsOverdue).IsTrue();
}
Create() gives you the test runtime with .Seed() for direct data setup and .SaveCalls for verifying what was saved. No database, no scheduler — just the handler logic.