# Kanject.Core.Recurring

Attribute-driven recurring method invocation with a drift-corrected engine and an AOT-safe source generator. Annotate a method with a rate, and a `{Method}RecurringAsync` extension is generated — call it from a console app, a Lambda handler, or an `IHostedService`. Cadence and lifecycle live on the declaration; the call site is just `await .RecurringAsync(...)`.

## Install

```bash
dotnet add package Kanject.Core
```

The attribute and engine ship in `Kanject.Core` — no separate package, and it works immediately after adding the reference.

## Annotate a method

```csharp
using Kanject.Core.Annotations.Attributes.Recurring;

public sealed class HeartbeatService(IPingClient client)
{
    // Cadence lives on the method. Units: Seconds | Minutes | Hours | Days.
    [Recurring(5, RecurringRateUnit.Seconds)]
    public async Task SendHeartbeatAsync(CancellationToken cancellationToken)
        => await client.PingAsync(cancellationToken);
}
```

The rate is a value plus a `RecurringRateUnit` (`Seconds`, `Minutes`, `Hours`, `Days`). The engine uses `Stopwatch.GetTimestamp` for drift-corrected scheduling — if one iteration runs long, the next still fires on the original cadence rather than accumulating lag.

## Call it

```csharp
// The generator emits {Method}RecurringAsync — call it anywhere: a console
// app, a Lambda handler, or an IHostedService. No timer, no host required.
var service = new HeartbeatService(client);

RecurringResult result = await service.SendHeartbeatRecurringAsync(
    cancellationToken: ct);

Console.WriteLine($"Ran {result.Iterations} times, stopped because {result.StopReason}");
```

The generated extension runs the loop and returns a `RecurringResult` (`Iterations`, `StopReason`, `LastException`). Value-returning methods get three overloads — including a `…RecurringToLastAsync` that returns the final value and a streaming variant. There is no reflection and no DI requirement; the generator wires everything at compile time.

## Bound and tune a run

```csharp
// Bound a run by count or wall-clock, and add jitter to de-sync fleets.
[Recurring(1, RecurringRateUnit.Hours,
    MaxIterations = 24,        // stop after 24 runs
    MaxDurationSeconds = 3600, // …or after an hour, whichever first
    MaxJitterMs = 500,         // spread ticks across instances
    StopOnException = true)]   // surface faults instead of swallowing them
public Task ReconcileAsync(CancellationToken ct) => _ledger.ReconcileAsync(ct);
```

## What ships with it

- `[Recurring(rateValue, RecurringRateUnit.Seconds|Minutes|Hours|Days)]` on a method — cadence as declaration, not embedded config
- Generated `{Method}RecurringAsync` (+ value-returning and streaming overloads) — runs in Lambda, console, or `IHostedService` unchanged
- Drift-corrected scheduling via `Stopwatch.GetTimestamp` — no accumulated lag
- `MaxIterations`, `MaxDurationSeconds`, `MaxJitterMs`, `StopOnException` bounds; overrun and exception policies with per-iteration and global hooks
- `RecurringResult` (`Iterations` / `StopReason` / `LastException`) instead of silently-swallowed loop errors

> **Hosted and distributed variants:** For an always-on loop, `[RecurringHosted]` generates a `BackgroundService` (in-process — not for Lambda). For lease + checkpoint coordination across instances, `[RecurringHosted(LeaseKey=…, CheckpointKey=…)]` is backed by `IRecurringDataProvider`, registered with `AddCacheDbRecurringDataProvider(...)` from `Kanject.Core.Recurring.Provider.CacheDb`.

---
_Source: https://www.kanject.com/docs/core-recurring/ · Kanject Docs_
