Skip to content

Commit ebadb22

Browse files
committed
feat: add configurable service lifetimes, OpenTelemetry instrumentation, and test barriers
## Summary Enhanced GUSTO with flexible dependency injection, comprehensive observability, and integration testing support to provide a production-ready job queue solution. ## Changes ### Service Lifetime Configuration - Added configurable service lifetimes (Scoped/Transient/Singleton) for JobQueue and IJobStorageProvider - Updated JobQueueWorker to resolve dependencies from scoped service providers - Default lifetime is Scoped for optimal database context integration - Supports EF Core DbContext, MongoDB scoped collections, and stateless singleton providers ### OpenTelemetry Instrumentation - Added ActivitySource for distributed tracing with ProcessBatch and ExecuteJob spans - Implemented comprehensive metrics: - `gusto.jobs.processed` - Counter for successful job executions - `gusto.jobs.failed` - Counter for failed jobs with exception types - `gusto.job.duration` - Histogram of job execution times - `gusto.batch.duration` - Histogram of batch processing times - `gusto.batch.size` - Histogram of jobs per batch - Created GustoTelemetry class with ActivitySourceName and MeterName constants - All metrics include detailed tags (job.type, job.method, job.status, exception.type) - Zero performance impact when OpenTelemetry is not configured (~1-10ns overhead when configured) ### Testing Support - Added BatchStartBarrier to pause worker at batch start for test setup - Added BatchCompletedBarrier to signal batch completion for deterministic assertions - Eliminates flaky tests from polling and arbitrary delays - Fully documented with integration test examples ### Code Quality Improvements - Refactored JobQueueWorker for improved readability with extracted methods - Each method now has a single, clear responsibility - Better separation of concerns for batch processing, job execution, and telemetry ### Documentation - Updated README with service lifetime configuration examples - Added OpenTelemetry integration guide with metrics table - Documented test barriers with complete integration test example - Added use cases for different service lifetime scenarios
1 parent 27c9b5e commit ebadb22

6 files changed

Lines changed: 505 additions & 72 deletions

File tree

README.md

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ A lightweight background job processing library for .NET. GUSTO provides a simpl
77

88
- **Minimal Setup**: Implement two interfaces and you're ready
99
- **Storage Agnostic**: Use any database (SQL Server, MongoDB, Redis, etc.)
10+
- **Flexible Service Lifetimes**: Configure services as Scoped, Transient, or Singleton
1011
- **Concurrent Processing**: Configurable parallel job execution
1112
- **Job Scheduling**: Schedule jobs for future execution
1213
- **Failure Handling**: Built-in retry logic with customizable strategies
14+
- **OpenTelemetry Support**: Built-in metrics and distributed tracing
15+
- **Easy Testing**: Test hooks for integration testing without polling
1316

1417
## Quick Start
1518

@@ -96,7 +99,10 @@ public class InMemoryJobStorageProvider : IJobStorageProvider<JobRecord>
9699

97100
```csharp
98101
// Program.cs
99-
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(configuration);
102+
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(
103+
configuration,
104+
lifetime: ServiceLifetime.Scoped // Default: Scoped. Options: Scoped, Transient, Singleton
105+
);
100106
```
101107

102108
```json
@@ -259,21 +265,132 @@ public async Task OnHandlerExecutionFailureAsync(MongoJobRecord jobStorageRecord
259265
public async Task OnHandlerExecutionFailureAsync(MongoJobRecord jobStorageRecord, Exception exception, CancellationToken cancellationToken)
260266
{
261267
jobStorageRecord.RetryCount = (jobStorageRecord.RetryCount ?? 0) + 1;
262-
268+
263269
if (jobStorageRecord.RetryCount > 3)
264270
{
265271
// Move to dead letter collection for manual inspection
266-
await _deadLetterCollection.InsertOneAsync(new DeadLetterJob
272+
await _deadLetterCollection.InsertOneAsync(new DeadLetterJob
267273
{
268274
OriginalJob = jobStorageRecord,
269275
FailureReason = exception.Message,
270276
FailedAt = DateTime.UtcNow
271277
}, cancellationToken: cancellationToken);
272-
278+
273279
await MarkJobAsCompleteAsync(jobStorageRecord, cancellationToken);
274280
return;
275281
}
276-
282+
277283
// Continue with retry logic...
278284
}
279285
```
286+
287+
### Service Lifetime Configuration
288+
289+
GUSTO supports Scoped, Transient, and Singleton service lifetimes for `JobQueue` and `IJobStorageProvider`:
290+
291+
```csharp
292+
// Scoped (default) - New instance per batch
293+
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(
294+
configuration,
295+
ServiceLifetime.Scoped);
296+
297+
// Transient - New instance for every resolution
298+
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(
299+
configuration,
300+
ServiceLifetime.Transient);
301+
302+
// Singleton - Single instance for application lifetime
303+
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(
304+
configuration,
305+
ServiceLifetime.Singleton);
306+
```
307+
308+
**Use Cases:**
309+
- **Scoped**: Best for database contexts (EF Core DbContext, MongoDB scoped collections)
310+
- **Singleton**: Best for thread-safe in-memory implementations or stateless providers
311+
- **Transient**: Rarely needed, but available for special cases
312+
313+
## OpenTelemetry Integration
314+
315+
GUSTO includes built-in OpenTelemetry support for metrics and distributed tracing.
316+
317+
### Available Metrics
318+
319+
| Metric | Type | Description | Tags |
320+
|--------|------|-------------|------|
321+
| `gusto.jobs.processed` | Counter | Total successful jobs | `job.type`, `job.method` |
322+
| `gusto.jobs.failed` | Counter | Total failed jobs | `job.type`, `job.method`, `exception.type` |
323+
| `gusto.job.duration` | Histogram | Job execution time (ms) | `job.type`, `job.method`, `job.status` |
324+
| `gusto.batch.duration` | Histogram | Batch processing time (ms) | - |
325+
| `gusto.batch.size` | Histogram | Jobs per batch | - |
326+
327+
### Available Traces
328+
329+
- **ProcessBatch** - Span for entire batch with `batch.size` tag
330+
- **ExecuteJob** - Span for individual jobs with `job.tracking_id`, `job.type`, `job.method` tags
331+
332+
### Configuration
333+
334+
Add GUSTO telemetry to your OpenTelemetry configuration:
335+
336+
```csharp
337+
builder.Services.AddOpenTelemetry()
338+
.WithTracing(tracing => tracing
339+
.AddSource(GustoTelemetry.ActivitySourceName)
340+
.AddAspNetCoreInstrumentation()
341+
.AddOtlpExporter())
342+
.WithMetrics(metrics => metrics
343+
.AddMeter(GustoTelemetry.MeterName)
344+
.AddAspNetCoreInstrumentation()
345+
.AddOtlpExporter());
346+
```
347+
348+
Export to Prometheus, Grafana, Jaeger, or any OpenTelemetry-compatible backend.
349+
350+
## Testing
351+
352+
GUSTO provides test barriers for easy integration testing without polling or delays.
353+
354+
### Integration Test Example
355+
356+
```csharp
357+
[Fact]
358+
public async Task JobQueue_ProcessesJobSuccessfully()
359+
{
360+
// Arrange
361+
var services = new ServiceCollection();
362+
services.AddGusto<JobRecord, InMemoryJobStorageProvider>(configuration);
363+
services.AddScoped<MyService>();
364+
365+
var serviceProvider = services.BuildServiceProvider();
366+
var jobQueue = serviceProvider.GetRequiredService<JobQueue<JobRecord>>();
367+
var hostedService = serviceProvider.GetRequiredService<IHostedService>();
368+
369+
// Set up test barriers
370+
JobQueueWorker<JobRecord>.BatchStartBarrier = new TaskCompletionSource();
371+
JobQueueWorker<JobRecord>.BatchCompletedBarrier = new TaskCompletionSource();
372+
373+
// Act
374+
await jobQueue.EnqueueAsync<MyService>(s => s.DoWork("test"));
375+
376+
await hostedService.StartAsync(CancellationToken.None);
377+
378+
// Let worker process the batch
379+
JobQueueWorker<JobRecord>.BatchStartBarrier.SetResult();
380+
381+
// Wait for batch to complete
382+
await JobQueueWorker<JobRecord>.BatchCompletedBarrier.Task;
383+
384+
await hostedService.StopAsync(CancellationToken.None);
385+
386+
// Assert
387+
Assert.True(MyService.WorkCompleted);
388+
}
389+
```
390+
391+
### Test Barriers
392+
393+
- **`BatchStartBarrier`** - Pauses worker at start of batch cycle until signaled
394+
- **`BatchCompletedBarrier`** - Signals when batch processing completes
395+
396+
This eliminates flaky tests from polling and arbitrary delays.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ByteBard.GUSTO;
2+
3+
/// <summary>
4+
/// OpenTelemetry constants for GUSTO job queue instrumentation.
5+
/// </summary>
6+
public static class GustoTelemetry
7+
{
8+
/// <summary>
9+
/// The name of the OpenTelemetry ActivitySource for distributed tracing.
10+
/// Use this when configuring OpenTelemetry: builder.AddSource(GustoTelemetry.ActivitySourceName)
11+
/// </summary>
12+
public const string ActivitySourceName = "ByteBard.GUSTO.JobQueue";
13+
14+
/// <summary>
15+
/// The name of the OpenTelemetry Meter for metrics.
16+
/// Use this when configuring OpenTelemetry: builder.AddMeter(GustoTelemetry.MeterName)
17+
/// </summary>
18+
public const string MeterName = "ByteBard.GUSTO.JobQueue";
19+
}

0 commit comments

Comments
 (0)