Skip to content
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

feat: Support persisting SQLite DB, and data refresher #233

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0eb9c6b
database persists. Next step - refresher
lukehesluke Feb 20, 2025
9dca035
done the DataRefresher. Next step: test in anger
lukehesluke Feb 20, 2025
57839ea
confirm pls
lukehesluke Feb 20, 2025
021847b
match Retention Period policy
lukehesluke Feb 21, 2025
117c1d9
Improved logs and confirmed FakeDataRefresherService to work
lukehesluke Feb 21, 2025
c49b149
some cleanup
lukehesluke Feb 21, 2025
0a02f4a
remove web-app-package
lukehesluke Feb 24, 2025
5fd44fd
fix .NET SDK version with global.json
lukehesluke Feb 25, 2025
eceb398
fix an RPDE issue - soft-deletes were not updating their modifieds
lukehesluke Feb 25, 2025
a3b193a
able to turn off flags easier with env vars
lukehesluke Feb 26, 2025
a6e2aac
/init-wait/data-refresher
lukehesluke Feb 28, 2025
af3ac48
remove CI scripts which are now in feature/persistent-db-ci
lukehesluke Mar 5, 2025
fa828f9
install libssl?
lukehesluke Mar 5, 2025
118424e
Revert "install libssl?"
lukehesluke Mar 5, 2025
d73724a
remove global.json?
lukehesluke Mar 5, 2025
cfd6e03
TESTING CI
lukehesluke Mar 5, 2025
79a642d
some fixes
lukehesluke Mar 5, 2025
0be04d4
ubuntu-22.04?
lukehesluke Mar 5, 2025
1a33b81
upgrade upload-artifact: v2 -> v4
lukehesluke Mar 5, 2025
a695cb3
various improvements
lukehesluke Mar 5, 2025
9bc8870
Merge branch 'feature/ci-test' into feature/ref-impl-db-2
lukehesluke Mar 5, 2025
eef2ba9
fix test
lukehesluke Mar 5, 2025
91335ba
fix IdentityServer build error
lukehesluke Mar 5, 2025
dd048bc
Merge branch 'master' into feature/ref-impl-db-2
lukehesluke Mar 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/openactive-test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
# output
- name: Run OpenActive.FakeDatabase.NET.Tests
run: dotnet test ./Fakes/OpenActive.FakeDatabase.NET.Tests/OpenActive.FakeDatabase.NET.Tests.csproj --configuration Release --no-build --verbosity normal

core:
# Specifies that this job depends on the successful completion of two other jobs: "test-server" and "test-fake-database"
needs:
Expand Down Expand Up @@ -107,6 +107,7 @@ jobs:

# runs `dotnet restore` to install the dependencies for the "OpenActive.Server.NET" project. It is conditional and
# depends on the "profile" value not being 'no-auth' or 'single-seller'
# LW: Why?
- name: Install OpenActive.Server.NET dependencies
if: ${{ matrix.profile != 'no-auth' && matrix.profile != 'single-seller' }}
run: dotnet restore ./server/
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,6 @@ ASALocalRun/

# Fake database
*fakedatabase.db

# Output path for app publishing
/web-app-package/
Copy link
Contributor Author

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

  • Logs
  • It informs DataRefresherStatusService when a has completed a cycle (which is explained in that file)

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
{
// Background task
// More information: 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
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)
{

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(_settings.DataRefresherInterval, stoppingToken);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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.
/// 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");
}
}
}
}
6 changes: 4 additions & 2 deletions Examples/BookingSystem.AspNetCore/Feeds/FacilitiesFeeds.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Bogus;
using BookingSystem.AspNetCore.Helpers;
using Bogus;
using BookingSystem.AspNetCore.Helpers;
using OpenActive.DatasetSite.NET;
using OpenActive.FakeDatabase.NET;
using OpenActive.NET;
Expand All @@ -26,6 +26,7 @@ public AcmeFacilityUseRpdeGenerator(AppSettings appSettings, FakeBookingSystem f
this._fakeBookingSystem = fakeBookingSystem;
}

// TODO this method should use async queries so as not to block the main thread
protected override async Task<List<RpdeItem<FacilityUse>>> GetRpdeItems(long? afterTimestamp, long? afterId)
{
var facilityTypeId = Environment.GetEnvironmentVariable("FACILITY_TYPE_ID") ?? "https://openactive.io/facility-types#a1f82b7a-1258-4d9a-8dc5-bfc2ae961651";
Expand Down Expand Up @@ -204,6 +205,7 @@ public AcmeFacilityUseSlotRpdeGenerator(AppSettings appSettings, FakeBookingSyst
this._fakeBookingSystem = fakeBookingSystem;
}

// TODO this method should use async queries so as not to block the main thread
protected override async Task<List<RpdeItem<Slot>>> GetRpdeItems(long? afterTimestamp, long? afterId)
{
using (var db = _fakeBookingSystem.Database.Mem.Database.Open())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace BookingSystem.AspNetCore.Services
{
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();
}
}

public bool HasCompletedCycle()
{
return _hasCompletedCycle;
}

public async Task WaitForCycleCompletion(TimeSpan timeout)
{
if (_hasCompletedCycle)
{
return;
}

await _completionSemaphore.WaitAsync(timeout);
}
}
}
3 changes: 3 additions & 0 deletions Examples/BookingSystem.AspNetCore/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace BookingSystem
{
public class AppSettings
Expand All @@ -6,6 +8,7 @@ public class AppSettings
public string OpenIdIssuerUrl { get; set; }
public FeatureSettings FeatureFlags { get; set; }
public PaymentSettings Payment { get; set; }
public TimeSpan DataRefresherInterval = TimeSpan.FromHours(6);
}

/**
Expand Down
53 changes: 48 additions & 5 deletions Examples/BookingSystem.AspNetCore/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Builder;
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
Expand All @@ -9,6 +10,8 @@
using OpenActive.Server.NET.OpenBookingHelper;
using Microsoft.AspNetCore.Authorization;
using OpenActive.FakeDatabase.NET;
using Microsoft.Extensions.Logging;
using BookingSystem.AspNetCore.Services;

namespace BookingSystem.AspNetCore
{
Expand All @@ -20,22 +23,37 @@ public Startup(IConfiguration configuration)
configuration.Bind(AppSettings);

// Provide a simple way to disable token auth for some testing scenarios
if (System.Environment.GetEnvironmentVariable("DISABLE_TOKEN_AUTH") == "true")
var disableTokenAuthEnvVar = System.Environment.GetEnvironmentVariable("DISABLE_TOKEN_AUTH");
if (disableTokenAuthEnvVar == "true")
{
AppSettings.FeatureFlags.EnableTokenAuth = false;
}
else if (disableTokenAuthEnvVar == "false")
{
AppSettings.FeatureFlags.EnableTokenAuth = true;
}

// Provide a simple way to enable FacilityUseHasSlots for some testing scenarios
if (System.Environment.GetEnvironmentVariable("FACILITY_USE_HAS_SLOTS") == "true")
var facilityUseHasSlotsEnvVar = System.Environment.GetEnvironmentVariable("FACILITY_USE_HAS_SLOTS");
if (facilityUseHasSlotsEnvVar == "true")
{
AppSettings.FeatureFlags.FacilityUseHasSlots = true;
}
else if (facilityUseHasSlotsEnvVar == "false")
{
AppSettings.FeatureFlags.FacilityUseHasSlots = false;
}

// Provide a simple way to enable CI mode
if (System.Environment.GetEnvironmentVariable("IS_LOREM_FITSUM_MODE") == "true")
var isLoremFitsumModeEnvVar = System.Environment.GetEnvironmentVariable("IS_LOREM_FITSUM_MODE");
if (isLoremFitsumModeEnvVar == "true")
{
AppSettings.FeatureFlags.IsLoremFitsumMode = true;
}
else if (isLoremFitsumModeEnvVar == "false")
{
AppSettings.FeatureFlags.IsLoremFitsumMode = false;
}
}

public AppSettings AppSettings { get; }
Expand Down Expand Up @@ -92,7 +110,32 @@ public void ConfigureServices(IServiceCollection services)
.AddControllers()
.AddMvcOptions(options => options.InputFormatters.Insert(0, new OpenBookingInputFormatter()));

services.AddSingleton<IBookingEngine>(sp => EngineConfig.CreateStoreBookingEngine(AppSettings, new FakeBookingSystem(AppSettings.FeatureFlags.FacilityUseHasSlots)));
services.AddSingleton(x => AppSettings);

services.AddSingleton(sp => new FakeBookingSystem(
AppSettings.FeatureFlags.FacilityUseHasSlots,
sp.GetRequiredService<ILogger<FakeBookingSystem>>()
));

// Register our DataRefresherStatusService as a singleton
services.AddSingleton<DataRefresherStatusService>();

// Use the singleton FakeBookingSystem in IBookingEngine registration
services.AddSingleton<IBookingEngine>(sp =>
EngineConfig.CreateStoreBookingEngine(
AppSettings,
sp.GetRequiredService<FakeBookingSystem>()
));

var doRunDataRefresher = Environment.GetEnvironmentVariable("PERIODICALLY_REFRESH_DATA")?.ToLowerInvariant() == "true";
if (doRunDataRefresher) {
services.AddHostedService<FakeDataRefresherService>();
}
else {
// If data refresher is not configured to run, update the status service
var statusService = services.BuildServiceProvider().GetRequiredService<DataRefresherStatusService>();
statusService.SetRefresherConfigured(false);
}
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
Loading
Loading