Timer Infrastructure¶
Overview¶
LBS Foundry provides a declarative timer infrastructure for scheduling recurring tasks. Instead of writing manual SystemBackgroundService implementations with while loops, you can use attributes to define scheduled tasks that are automatically discovered and managed.
Core Components¶
TimerAttribute¶
Marks methods for scheduled execution. The method must:
- Be async and return Task
- Accept a single CancellationToken parameter
- Be in a class that can be resolved or instantiated via DI
[Timer(
maxConcurrency: 1, // Max concurrent executions
intervalInSeconds: 300, // Run every 5 minutes
maxRunTimeInSeconds: 60, // Timeout after 60 seconds
retryOnError: true, // Retry on transient errors
spreadStart: false // Randomize initial start time
)]
public async Task MyTimerMethodAsync(CancellationToken cancellationToken)
{
// Your logic here
}
TimerInterval Constants¶
Pre-defined interval constants for common schedules:
TimerInterval.ThirtySeconds = 30
TimerInterval.OneMinute = 60
TimerInterval.TwoMinutes = 120
TimerInterval.FiveMinutes = 300
TimerInterval.TenMinutes = 600
TimerInterval.FifteenMinutes = 900
TimerInterval.ThirtyMinutes = 1800
TimerInterval.Hourly = 3600
TimerInterval.TwoHourly = 7200
TimerInterval.Daily = 86400
BlackoutPeriodAttribute¶
Define time windows when a timer should not run. Can be applied multiple times:
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
[BlackoutPeriod(9, 0, 17, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday })]
public async Task MaintenanceTaskAsync(CancellationToken cancellationToken)
{
// Runs every 10 minutes, but NOT 9 AM - 5 PM on weekdays
}
TimerService¶
Orchestrates timer discovery and execution. Automatically: - Discovers timer methods via reflection - Creates scoped instances per execution - Manages concurrency and timeouts - Handles retries on transient errors - Respects blackout periods
Registration¶
Add timer infrastructure in your Program.cs:
using LBS.Augment.Timers;
// Register timer infrastructure
builder.Services.AddTimerInfrastructure(typeof(Program).Assembly);
The AddTimerInfrastructure method:
1. Registers TimerService as a singleton
2. Registers TimerInfrastructureHostedService (uses SystemHostedService)
3. Discovers all [Timer] methods in specified assemblies on startup
Usage Examples¶
Example 1: Basic Timer¶
public class DataProcessingTimers
{
private readonly ILogger<DataProcessingTimers> logger;
public DataProcessingTimers(ILogger<DataProcessingTimers> logger)
{
this.logger = logger;
}
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.FiveMinutes)]
public async Task ProcessPendingOrdersAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Processing pending orders...");
// Your logic here
await Task.CompletedTask;
}
}
Example 2: Timer with Retry and Timeout¶
[Timer(
maxConcurrency: 1,
intervalInSeconds: TimerInterval.FiveMinutes,
maxRunTimeInSeconds: 60, // Cancel after 60 seconds
retryOnError: true)] // Retry on transient errors
public async Task ProcessWithRetryAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Processing with retry...");
await Task.Delay(2000, cancellationToken);
}
Example 3: Timer with Dependency Injection¶
Timer classes are instantiated per execution with full DI support:
public class BallrContestsLeaderboardImporter
{
private readonly ILogger<BallrContestsLeaderboardImporter> logger;
private readonly IServiceProvider serviceProvider;
public BallrContestsLeaderboardImporter(
ILogger<BallrContestsLeaderboardImporter> logger,
IServiceProvider serviceProvider)
{
this.logger = logger;
this.serviceProvider = serviceProvider;
}
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.ThirtySeconds)]
public async Task ProcessSuperCoachLeagueImportsAsync(CancellationToken cancellationToken)
{
using var scope = this.serviceProvider.CreateScope();
var importExecutor = scope.ServiceProvider.GetRequiredService<IImportExecutor>();
var session = scope.ServiceProvider.GetRequiredService<IDocumentSession>();
// Process imports...
}
}
Note: Timer classes do NOT need to be registered in DI. The TimerService uses ActivatorUtilities.GetServiceOrCreateInstance to instantiate them per execution.
Example 4: Timer with Blackout Period¶
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
[BlackoutPeriod(9, 0, 17, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday })]
public async Task MaintenanceTaskAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Running maintenance outside business hours...");
await Task.Delay(5000, cancellationToken);
}
Example 5: Timer with Spread Start¶
[Timer(
maxConcurrency: 3,
intervalInSeconds: TimerInterval.FiveMinutes,
spreadStart: true)] // Random initial delay to distribute load
public async Task DistributedLoadTimerAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Processing distributed workload...");
await Task.Delay(1000, cancellationToken);
}
Example 6: Static Timer (No DI)¶
public static class StaticTimers
{
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
public static async Task StaticTimerAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Static timer executed at {0}", DateTime.UtcNow);
await Task.Delay(1000, cancellationToken);
}
}
Timer with Advanced Features¶
[Timer(
maxConcurrency: 1,
intervalInSeconds: TimerInterval.TenMinutes,
maxRunTimeInSeconds: 300, // Cancel after 5 minutes
retryOnError: true, // Retry on TimeoutException, IOException, etc.
spreadStart: true // Random initial delay (0 to interval)
)]
[BlackoutPeriod(9, 0, 17, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday })]
public async Task MaintenanceTaskAsync(CancellationToken cancellationToken)
{
// Runs every 10 minutes outside business hours
// With automatic retry and timeout protection
}
Multiple Blackout Periods¶
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.FifteenMinutes)]
[BlackoutPeriod(12, 0, 13, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday })] // Lunch
[BlackoutPeriod(17, 0, 18, 0, new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday })] // End of day
[BlackoutPeriod(0, 0, 23, 59, new[] { DayOfWeek.Saturday, DayOfWeek.Sunday })] // Weekends
public async Task GenerateReportsAsync(CancellationToken cancellationToken)
{
// Skips execution during lunch, end of day, and all weekend
}
Static Timer Methods¶
Timers can be static methods if they don't need DI:
public static class StaticTimers
{
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.TenMinutes)]
public static async Task PerformStaticTaskAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Static timer executed at {0}", DateTime.Now);
await Task.Delay(1000, cancellationToken);
}
}
Manual Timer Registration¶
You can register timers dynamically at runtime:
public class MyService
{
private readonly TimerService timerService;
public MyService(TimerService timerService)
{
this.timerService = timerService;
}
public void RegisterDynamicTimer()
{
this.timerService.RegisterManualTimer(
name: "DynamicImport",
timerFunc: async (ct) => await this.ProcessDataAsync(ct),
interval: TimeSpan.FromMinutes(5),
maxConcurrency: 1,
retryOnError: true,
spreadStart: false);
}
public void UnregisterTimer()
{
this.timerService.UnregisterManualTimer("DynamicImport");
}
}
Timer Monitoring¶
Get status of all running timers:
public class TimerMonitoringService
{
private readonly TimerService timerService;
[Timer(maxConcurrency: 1, intervalInSeconds: TimerInterval.OneMinute)]
public async Task MonitorTimerHealthAsync(CancellationToken cancellationToken)
{
var statuses = this.timerService.GetTimerStatus();
foreach (var status in statuses)
{
if (status.FailureCount > 5)
{
// Alert on repeated failures
}
if (status.CurrentConcurrency == status.MaxConcurrency)
{
// Alert on max concurrency reached
}
}
await Task.CompletedTask;
}
}
Manual Timer Triggering¶
Force a timer to run immediately:
public class ManualTriggerService
{
private readonly TimerService timerService;
public void TriggerReportNow()
{
// Force attribute-based timer to run immediately
this.timerService.RunTimerImmediately(
nameof(DataProcessingTimers),
nameof(DataProcessingTimers.GenerateHourlyReportAsync));
}
public void TriggerManualTimer()
{
// Force manual timer to run immediately
this.timerService.RunTimerImmediately("DynamicImport");
}
public void TriggerWithDelay()
{
// Schedule to run in 30 seconds
this.timerService.RunTimerImmediately(
nameof(DataProcessingTimers),
nameof(DataProcessingTimers.GenerateHourlyReportAsync),
delay: TimeSpan.FromSeconds(30));
}
}
Migration from SystemBackgroundService¶
Before (SystemBackgroundService)¶
public class MyBackgroundWorker : SystemBackgroundService
{
private readonly IServiceProvider serviceProvider;
public MyBackgroundWorker(
IServiceProvider serviceProvider,
ILogger<MyBackgroundWorker> logger) : base(logger)
{
this.serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsSystemAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = this.serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
await service.ProcessAsync(stoppingToken);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error processing");
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
// Registration
services.AddHostedService<MyBackgroundWorker>();
After (Timer)¶
public class MyTimers
{
private readonly IServiceProvider serviceProvider;
private readonly ILogger<MyTimers> logger;
public MyTimers(
IServiceProvider serviceProvider,
ILogger<MyTimers> logger)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}
[Timer(
maxConcurrency: 1,
intervalInSeconds: TimerInterval.FiveMinutes,
retryOnError: true)]
public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
using var scope = this.serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
await service.ProcessAsync(cancellationToken);
}
}
// No registration needed - auto-discovered by TimerService
Benefits¶
- Less boilerplate - No manual while loops or delay logic
- Declarative - Intent is clear from attributes
- Built-in features - Timeout, retry, blackout periods, spread start
- Centralized management - All timers visible and controllable via
TimerService - Better error handling - Automatic retry on transient errors
- No registration - Classes auto-instantiated per execution
Best Practices¶
- Use appropriate intervals - Use
TimerIntervalconstants for readability - Set max runtime - Prevent runaway timers with
maxRunTimeInSeconds - Enable retry carefully - Only enable
retryOnErrorfor truly transient failures - Use spread start for load - Enable
spreadStartwhen multiple timers hit external services - Create scopes - Always create DI scopes within timer methods for scoped services
- Respect cancellation - Check
cancellationToken.IsCancellationRequestedin long operations - Use blackout periods - Avoid running heavy tasks during peak hours
- Monitor timer health - Track failure counts and concurrency
Architecture¶
SystemHostedService vs SystemBackgroundService¶
The timer infrastructure uses SystemHostedService (not SystemBackgroundService) because:
- SystemBackgroundService - For continuous loops in
ExecuteAsync - SystemHostedService - For starting/stopping background operations
The TimerInfrastructureHostedService:
- Starts the timer loop as a background task in StartAsSystemAsync
- Stops and awaits completion in StopAsSystemAsync
- Runs with system privileges via UserContext.RunAsSystem()
Execution Flow¶
- Startup -
TimerInfrastructureHostedServicestarts - Discovery - Scans assemblies for
[Timer]methods - Scheduling - Calculates next execution time for each timer
- Execution - Creates scope, instantiates class, invokes method
- Cleanup - Disposes scope after execution
- Repeat - Reschedules based on interval
Concurrency Control¶
Each timer has its own concurrency limit. When maxConcurrency is reached:
- New executions wait until a slot is available
- Existing executions continue unaffected
- No queuing - just rechecks every 10ms
Timeout Protection¶
If a timer exceeds maxRunTimeInSeconds:
1. Cancellation token is signaled
2. Timer method should respect cancellation
3. If it doesn't, it will eventually complete and log a warning
Location¶
- Core:
src/Core/LBS.Augment/Timers/ TimerAttribute.cs- Attribute definitionTimerInterval.cs- Interval constantsBlackoutPeriodAttribute.cs- Blackout period definitionTimerService.cs- Core orchestrationTimerServiceExtensions.cs- Registration extensions