Skip to content

Commit c32b066

Browse files
hemanandrclaude
andcommitted
feat: Implement Issue #27 - 15-minute rollup background job
- Create IRollupService and RollupService with rollup algorithms - Add RollupBackgroundService that runs every 5 minutes - Process raw checks into 15-minute and daily rollups - Calculate UpPct, AvgRttMs, DownEvents per bucket - Use watermark tracking via SettingsService - Handle SQLite DateTimeOffset limitations with in-memory filtering - Add TestRollupController for manual testing - Successfully tested: generates rollup records from monitoring data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a59e7b7 commit c32b066

File tree

11 files changed

+621
-0
lines changed

11 files changed

+621
-0
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"Bash(taskkill:*)",
99
"Bash(dotnet clean:*)",
1010
"Bash(dotnet run:*)",
11+
"Bash(powershell:*)",
12+
"Bash(dotnet build:*)",
1113
"Bash(powershell:*)"
1214
],
1315
"deny": [],
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using ThingConnect.Pulse.Server.Services.Rollup;
3+
4+
namespace ThingConnect.Pulse.Server.Controllers;
5+
6+
/// <summary>
7+
/// Test controller for rollup functionality (development only)
8+
/// </summary>
9+
[ApiController]
10+
[Route("api/test/rollup")]
11+
public sealed class TestRollupController : ControllerBase
12+
{
13+
private readonly IRollupService _rollupService;
14+
private readonly ILogger<TestRollupController> _logger;
15+
16+
public TestRollupController(IRollupService rollupService, ILogger<TestRollupController> logger)
17+
{
18+
_rollupService = rollupService;
19+
_logger = logger;
20+
}
21+
22+
/// <summary>
23+
/// Manually trigger 15-minute rollup processing
24+
/// </summary>
25+
[HttpPost("process-15m")]
26+
public async Task<IActionResult> ProcessRollup15mAsync()
27+
{
28+
try
29+
{
30+
_logger.LogInformation("Manual 15m rollup processing requested");
31+
await _rollupService.ProcessRollup15mAsync();
32+
return Ok(new { message = "15-minute rollup processing completed successfully" });
33+
}
34+
catch (Exception ex)
35+
{
36+
_logger.LogError(ex, "Error processing 15m rollups");
37+
return StatusCode(500, new { message = "Error processing rollups", error = ex.Message });
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Manually trigger daily rollup processing
43+
/// </summary>
44+
[HttpPost("process-daily")]
45+
public async Task<IActionResult> ProcessRollupDailyAsync()
46+
{
47+
try
48+
{
49+
_logger.LogInformation("Manual daily rollup processing requested");
50+
await _rollupService.ProcessRollupDailyAsync();
51+
return Ok(new { message = "Daily rollup processing completed successfully" });
52+
}
53+
catch (Exception ex)
54+
{
55+
_logger.LogError(ex, "Error processing daily rollups");
56+
return StatusCode(500, new { message = "Error processing rollups", error = ex.Message });
57+
}
58+
}
59+
}

ThingConnect.Pulse.Server/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ThingConnect.Pulse.Server.Infrastructure;
55
using ThingConnect.Pulse.Server.Services;
66
using ThingConnect.Pulse.Server.Services.Monitoring;
7+
using ThingConnect.Pulse.Server.Services.Rollup;
78

89
namespace ThingConnect.Pulse.Server;
910

@@ -36,6 +37,10 @@ public static void Main(string[] args)
3637
builder.Services.AddScoped<IHistoryService, HistoryService>();
3738
builder.Services.AddHostedService<MonitoringBackgroundService>();
3839

40+
// Add rollup services
41+
builder.Services.AddScoped<IRollupService, RollupService>();
42+
builder.Services.AddHostedService<RollupBackgroundService>();
43+
3944
builder.Services.AddControllers(options =>
4045
{
4146
options.InputFormatters.Insert(0, new PlainTextInputFormatter());
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace ThingConnect.Pulse.Server.Services.Rollup;
2+
3+
/// <summary>
4+
/// Service for computing data rollups from raw check results
5+
/// </summary>
6+
public interface IRollupService
7+
{
8+
/// <summary>
9+
/// Process 15-minute rollups from raw check results since last watermark
10+
/// </summary>
11+
Task ProcessRollup15mAsync(CancellationToken cancellationToken = default);
12+
13+
/// <summary>
14+
/// Process daily rollups from raw check results since last watermark
15+
/// </summary>
16+
Task ProcessRollupDailyAsync(CancellationToken cancellationToken = default);
17+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace ThingConnect.Pulse.Server.Services.Rollup;
2+
3+
/// <summary>
4+
/// Background service that processes rollups every 5 minutes
5+
/// </summary>
6+
public sealed class RollupBackgroundService : BackgroundService
7+
{
8+
private readonly IServiceProvider _serviceProvider;
9+
private readonly ILogger<RollupBackgroundService> _logger;
10+
private readonly TimeSpan _rollupInterval = TimeSpan.FromMinutes(5);
11+
12+
public RollupBackgroundService(IServiceProvider serviceProvider, ILogger<RollupBackgroundService> logger)
13+
{
14+
_serviceProvider = serviceProvider;
15+
_logger = logger;
16+
}
17+
18+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
19+
{
20+
_logger.LogInformation("RollupBackgroundService starting. Rollup interval: {Interval}", _rollupInterval);
21+
22+
// Wait a bit before starting to let the application fully initialize
23+
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
24+
25+
while (!stoppingToken.IsCancellationRequested)
26+
{
27+
try
28+
{
29+
await ProcessRollupsAsync(stoppingToken);
30+
}
31+
catch (OperationCanceledException)
32+
{
33+
// Expected when cancellation is requested
34+
break;
35+
}
36+
catch (Exception ex)
37+
{
38+
_logger.LogError(ex, "Error in rollup background service");
39+
}
40+
41+
// Wait for next rollup cycle
42+
try
43+
{
44+
await Task.Delay(_rollupInterval, stoppingToken);
45+
}
46+
catch (OperationCanceledException)
47+
{
48+
// Expected when cancellation is requested
49+
break;
50+
}
51+
}
52+
53+
_logger.LogInformation("RollupBackgroundService stopping");
54+
}
55+
56+
private async Task ProcessRollupsAsync(CancellationToken cancellationToken)
57+
{
58+
using IServiceScope scope = _serviceProvider.CreateScope();
59+
IRollupService rollupService = scope.ServiceProvider.GetRequiredService<IRollupService>();
60+
61+
try
62+
{
63+
// Process 15-minute rollups
64+
await rollupService.ProcessRollup15mAsync(cancellationToken);
65+
66+
// Process daily rollups (less frequent, but check each cycle)
67+
await rollupService.ProcessRollupDailyAsync(cancellationToken);
68+
}
69+
catch (Exception ex)
70+
{
71+
_logger.LogError(ex, "Failed to process rollups");
72+
throw;
73+
}
74+
}
75+
76+
public override async Task StopAsync(CancellationToken cancellationToken)
77+
{
78+
_logger.LogInformation("RollupBackgroundService stop requested");
79+
await base.StopAsync(cancellationToken);
80+
_logger.LogInformation("RollupBackgroundService stopped");
81+
}
82+
}

0 commit comments

Comments
 (0)