Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 6
feat: Support persisting SQLite DB, and data refresher#233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
0eb9c6b9dca03557839ea021847b117c1d9c49b1490a02f4a5fd44fdeceb398a3b193aa6e2aacaf3ac48fa828f9118424ed73724acfd6e0379a642d0be04d41a33b81a695cb39bc8870eef2ba991335badd048bcFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -336,3 +336,6 @@ ASALocalRun/ | ||
| # Fake database | ||
| *fakedatabase.db | ||
| # Output path for app publishing | ||
| /web-app-package/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using System; | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is very similar to FakeDataRefresherService from #156, except with the following changes:
| ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
| using OpenActive.FakeDatabase.NET; | ||
| using BookingSystem.AspNetCore.Services; | ||
| namespace BookingSystem | ||
| { | ||
| /// <summary> | ||
| /// A background task which periodically refreshes the data in the | ||
| /// FakeBookingSystem. This means that past data is deleted and new copies | ||
| /// are created in the future. | ||
| /// | ||
| /// More information on background tasks here: | ||
| /// https://docs.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/background-tasks-with-ihostedservice#implementing-ihostedservice-with-a-custom-hosted-service-class-deriving-from-the-backgroundservice-base-class | ||
| /// </summary> | ||
| public class FakeDataRefresherService : BackgroundService | ||
| { | ||
| private readonly ILogger<FakeDataRefresherService> _logger; | ||
| private readonly AppSettings _settings; | ||
| private readonly FakeBookingSystem _bookingSystem; | ||
| private readonly DataRefresherStatusService _statusService; | ||
| public FakeDataRefresherService( | ||
| AppSettings settings, | ||
| ILogger<FakeDataRefresherService> logger, | ||
| FakeBookingSystem bookingSystem, | ||
| DataRefresherStatusService statusService) | ||
| { | ||
| _settings = settings; | ||
| _logger = logger; | ||
| _bookingSystem = bookingSystem; | ||
| _statusService = statusService; | ||
| // Indicate that the refresher service is configured to run | ||
| _statusService.SetRefresherConfigured(true); | ||
| } | ||
| protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
| { | ||
| var interval = TimeSpan.FromHours(_settings.DataRefresherIntervalHours); | ||
| stoppingToken.Register(() => | ||
| _logger.LogInformation($"FakeDataRefresherService background task is stopping.")); | ||
| while (!stoppingToken.IsCancellationRequested) | ||
| { | ||
| _logger.LogInformation($"FakeDataRefresherService is starting.."); | ||
| var (numDeletedOccurrences, numDeletedSlots) = await _bookingSystem | ||
| .Database | ||
| .HardDeleteOldSoftDeletedOccurrencesAndSlots(); | ||
| _logger.LogInformation($"FakeDataRefresherService hard deleted {numDeletedOccurrences} occurrences and {numDeletedSlots} slots that were previously old and soft-deleted."); | ||
| var (numRefreshedOccurrences, numRefreshedSlots) = await _bookingSystem | ||
| .Database | ||
| .SoftDeletePastOpportunitiesAndInsertNewAtEdgeOfWindow(); | ||
| _logger.LogInformation($"FakeDataRefresherService, for {numRefreshedOccurrences} old occurrences and {numRefreshedSlots} old slots, inserted new copies into the future and soft-deleted the old ones."); | ||
| _logger.LogInformation($"FakeDataRefresherService is finished"); | ||
| // Signal that a cycle has completed | ||
| _statusService.SignalCycleCompletion(); | ||
| await Task.Delay(interval, stoppingToken); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
| using BookingSystem.AspNetCore.Services; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.Extensions.Logging; | ||
| /// <summary> | ||
| /// Endpoints which are used to wait for components within | ||
| /// BookingSystem.AspNetCore to be initialized. | ||
| /// </summary> | ||
| namespace BookingSystem.AspNetCore.Controllers | ||
| { | ||
| [ApiController] | ||
| [Route("init-wait")] | ||
| public class InitWaitController : ControllerBase | ||
| { | ||
| private readonly DataRefresherStatusService _statusService; | ||
| private readonly ILogger<InitWaitController> _logger; | ||
| private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(5); | ||
| public InitWaitController( | ||
| DataRefresherStatusService statusService, | ||
| ILogger<InitWaitController> logger) | ||
| { | ||
| _statusService = statusService; | ||
| _logger = logger; | ||
| } | ||
| /// <summary> | ||
| /// Wait for the data refresher to complete its first cycle. | ||
| /// | ||
| /// This makes it possible to write scripts (for CI) which don't start | ||
| /// until the data refresher has completed at least one cycle. | ||
| /// | ||
| /// - Returns 204 when the data refresher has completed its first cycle. | ||
| /// - Returns 503 if the data refresher is not configured to run. | ||
| /// - Returns 504 if the data refresher fails to complete a cycle within | ||
| /// the default timeout. | ||
| /// </summary> | ||
| [HttpGet("data-refresher")] | ||
| public async Task<IActionResult> WaitForDataRefresher() | ||
| { | ||
| _logger.LogDebug("Received request to wait for data refresher completion"); | ||
| // Check if the data refresher is configured to run | ||
| if (!_statusService.IsRefresherConfigured()) | ||
| { | ||
| _logger.LogWarning("Data refresher is not configured to run"); | ||
| return StatusCode(503, "Data refresher service is not configured to run"); | ||
| } | ||
| // If it has already completed a cycle, return immediately | ||
| if (_statusService.HasCompletedCycle()) | ||
| { | ||
| _logger.LogDebug("Data refresher has already completed a cycle"); | ||
| return NoContent(); | ||
| } | ||
| _logger.LogDebug("Waiting for data refresher to complete a cycle..."); | ||
| // Wait for the cycle to complete, with a timeout | ||
| await _statusService.WaitForCycleCompletion(_defaultTimeout); | ||
| if (_statusService.HasCompletedCycle()) | ||
| { | ||
| _logger.LogDebug("Data refresher completed a cycle, returning 204"); | ||
| return NoContent(); | ||
| } | ||
| else | ||
| { | ||
| _logger.LogWarning("Timed out waiting for data refresher to complete a cycle"); | ||
| return StatusCode(504, "Timed out waiting for data refresher to complete a cycle"); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| using System; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| namespace BookingSystem.AspNetCore.Services | ||
| { | ||
| /// <summary> | ||
| /// A service which tracks the status of the data refresher, including | ||
| /// whether it is configured to run and whether a cycle has completed. | ||
| /// </summary> | ||
| public class DataRefresherStatusService | ||
| { | ||
| private readonly SemaphoreSlim _completionSemaphore = new SemaphoreSlim(0, 1); | ||
| private bool _isRefresherConfigured = false; | ||
| private bool _hasCompletedCycle = false; | ||
| public void SetRefresherConfigured(bool isConfigured) | ||
| { | ||
| _isRefresherConfigured = isConfigured; | ||
| } | ||
| public bool IsRefresherConfigured() | ||
| { | ||
| return _isRefresherConfigured; | ||
| } | ||
| public void SignalCycleCompletion() | ||
| { | ||
| _hasCompletedCycle = true; | ||
| // Release the semaphore if someone is waiting on it | ||
| if (_completionSemaphore.CurrentCount == 0) | ||
| { | ||
| _completionSemaphore.Release(); | ||
| } | ||
| } | ||
| /// <summary> | ||
| /// Has the data refresher completed a cycle? | ||
| /// | ||
| /// This makes it possible to write scripts (for CI) which don't start | ||
| /// until the data refresher has completed at least one cycle. | ||
| /// </summary> | ||
| public bool HasCompletedCycle() | ||
| { | ||
| return _hasCompletedCycle; | ||
| } | ||
| public async Task WaitForCycleCompletion(TimeSpan timeout) | ||
| { | ||
| if (_hasCompletedCycle) | ||
| { | ||
| return; | ||
| } | ||
| await _completionSemaphore.WaitAsync(timeout); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this had accidentally entered the git repo at some point