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
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ MinimalWorker is a .NET library for simplified background worker registration on
- `RunPeriodicBackgroundWorker(IHost, TimeSpan, Delegate)` - Runs after each interval
- `RunCronBackgroundWorker(IHost, string, Delegate)` - Runs on cron schedule (UTC)

All return `IWorkerBuilder` for fluent `.WithName()` and `.WithErrorHandler()` configuration.
All return `IWorkerBuilder` for fluent configuration:
- `.WithName(string)` - Set worker name for logs/metrics
- `.WithErrorHandler(Action<Exception>)` - Handle errors (worker continues)
- `.WithTimeout(TimeSpan)` - Cancel execution if it exceeds timeout
- `.WithRetry(int maxAttempts, TimeSpan? delay)` - Retry failed executions

**Source Generator** (`src/MinimalWorker.Generators/`):
- `WorkerGenerator.cs` - IIncrementalGenerator that scans invocations
Expand Down Expand Up @@ -84,6 +88,14 @@ for (int i = 0; i < steps; i++)
}
```

### Timeout and Retry Behavior

- **Timeout**: Throws `TimeoutException`, cancels the delegate's `CancellationToken`
- **Retry**: Only retries on exceptions (not timeouts or `OperationCanceledException`)
- **Combined**: Timeouts are NOT retried when using both `.WithTimeout()` and `.WithRetry()`
- Error handler is called only after all retries are exhausted
- Both timeout and retry delays respect `TimeProvider` for testability with `FakeTimeProvider`

## Common Anti-patterns

- **Continuous workers**: Run exactly once. Include your own `while` loop if you need repetition
Expand Down
73 changes: 72 additions & 1 deletion README.llm
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public interface IWorkerBuilder
{
IWorkerBuilder WithName(string name);
IWorkerBuilder WithErrorHandler(Action<Exception> handler);
IWorkerBuilder WithTimeout(TimeSpan timeout);
IWorkerBuilder WithRetry(int maxAttempts = 3, TimeSpan? delay = null);
}
```

Expand Down Expand Up @@ -211,6 +213,73 @@ app.RunBackgroundWorker((IMissingService missing) =>

---

## Timeout Configuration

Use `.WithTimeout()` to automatically cancel long-running executions:

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(5), async (DataService data, CancellationToken ct) =>
{
await data.ProcessBatch(ct); // Cancelled if exceeds 4 minutes
})
.WithTimeout(TimeSpan.FromMinutes(4))
.WithErrorHandler(ex =>
{
if (ex is TimeoutException) Console.WriteLine("Timed out!");
});
```

**Behavior:**
- `TimeoutException` thrown when timeout exceeded
- CancellationToken passed to delegate is cancelled on timeout
- Timeouts are **NOT retried** when combined with `.WithRetry()`
- Works with all worker types

---

## Retry Configuration

Use `.WithRetry()` to automatically retry failed executions:

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(5), async (ApiClient api, CancellationToken ct) =>
{
await api.SendData(ct); // Retries up to 3 times
})
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromSeconds(5))
.WithErrorHandler(ex =>
{
// Called only after ALL retries exhausted
Console.WriteLine($"All retries failed: {ex.Message}");
});
```

**Parameters:**
| Parameter | Default | Description |
|-----------|---------|-------------|
| `maxAttempts` | 3 | Maximum execution attempts |
| `delay` | 5 seconds | Wait time between retries |

**Behavior:**
- Error handler only called after all retries exhausted
- `OperationCanceledException` (shutdown) is never retried
- Timeouts are **NOT retried**

### Combining Timeout and Retry

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(10), async (SyncService sync, CancellationToken ct) =>
{
await sync.SyncData(ct);
})
.WithTimeout(TimeSpan.FromMinutes(2)) // Each attempt times out after 2 min
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromSeconds(30)) // Retry failures, not timeouts
.WithName("data-sync")
.WithErrorHandler(ex => logger.LogError(ex, "Sync failed"));
```

---

## Testing

### Required Package
Expand Down Expand Up @@ -537,7 +606,9 @@ host.RunPeriodicBackgroundWorker(
{
await notifications.CleanupExpiredAsync(ct);
})
.WithName("notification-cleanup");
.WithName("notification-cleanup")
.WithTimeout(TimeSpan.FromMinutes(4))
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromSeconds(10));

// Cron worker - runs on schedule (UTC), new scope per execution
host.RunCronBackgroundWorker(
Expand Down
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- 🧼 Minimal and clean API
- 📈 Built-in telemetry with automatic metrics and distributed tracing
- 🏎️ AOT Compilation Support
- ⏰ Configurable execution timeouts
- 🔁 Automatic retry with configurable attempts and delays

---

Expand Down Expand Up @@ -154,6 +156,68 @@ app.RunBackgroundWorker(async (CancellationToken token) =>

**Note**: This captures singleton services. For scoped services, this approach has limitations. Native DI support for error handlers is being considered for a future release.

### Timeout Configuration

Use `.WithTimeout()` to automatically cancel long-running worker executions:

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(5), async (DataService data, CancellationToken token) =>
{
await data.ProcessBatch(token); // Will be cancelled if takes > 4 minutes
})
.WithTimeout(TimeSpan.FromMinutes(4))
.WithErrorHandler(ex =>
{
if (ex is TimeoutException)
{
Console.WriteLine("Processing timed out!");
}
});
```

**Behavior**:
- A `TimeoutException` is thrown when the timeout is exceeded
- The `CancellationToken` passed to your delegate is cancelled on timeout
- Timeouts are **not retried** (if using `.WithRetry()`)
- Works with all worker types: continuous, periodic, and cron

### Retry Configuration

Use `.WithRetry()` to automatically retry failed worker executions:

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(5), async (ApiClient api, CancellationToken token) =>
{
await api.SendData(token); // Will retry up to 3 times on failure
})
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromSeconds(5))
.WithErrorHandler(ex =>
{
// Called only after all retries are exhausted
Console.WriteLine($"All retries failed: {ex.Message}");
});
```

**Behavior**:
- `maxAttempts` - Maximum number of execution attempts (default: 3)
- `delay` - Time to wait between retry attempts (default: 5 seconds)
- Error handler is only called after all retries are exhausted
- `OperationCanceledException` (graceful shutdown) is never retried
- Timeouts are **not retried** when combined with `.WithTimeout()`

### Combining Timeout and Retry

```csharp
app.RunPeriodicBackgroundWorker(TimeSpan.FromMinutes(10), async (SyncService sync, CancellationToken token) =>
{
await sync.SyncData(token);
})
.WithTimeout(TimeSpan.FromMinutes(2)) // Each attempt times out after 2 minutes
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromSeconds(30)) // Retry regular failures, not timeouts
.WithName("data-sync")
.WithErrorHandler(ex => logger.LogError(ex, "Sync failed"));
```

#### Startup Dependency Validation

MinimalWorker validates that all required dependencies for your workers are registered **during application startup**. If any dependencies are missing, the application will fail immediately with a clear error message:
Expand Down
Loading
Loading