Skip to content

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

  1. Use appropriate intervals - Use TimerInterval constants for readability
  2. Set max runtime - Prevent runaway timers with maxRunTimeInSeconds
  3. Enable retry carefully - Only enable retryOnError for truly transient failures
  4. Use spread start for load - Enable spreadStart when multiple timers hit external services
  5. Create scopes - Always create DI scopes within timer methods for scoped services
  6. Respect cancellation - Check cancellationToken.IsCancellationRequested in long operations
  7. Use blackout periods - Avoid running heavy tasks during peak hours
  8. 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

  1. Startup - TimerInfrastructureHostedService starts
  2. Discovery - Scans assemblies for [Timer] methods
  3. Scheduling - Calculates next execution time for each timer
  4. Execution - Creates scope, instantiates class, invokes method
  5. Cleanup - Disposes scope after execution
  6. 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 definition
  • TimerInterval.cs - Interval constants
  • BlackoutPeriodAttribute.cs - Blackout period definition
  • TimerService.cs - Core orchestration
  • TimerServiceExtensions.cs - Registration extensions

See Also