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 23 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/create-dependencies-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ on:

jobs:
generate:
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04

steps:
- name: Checkout OpenActive.Server.NET
Expand Down
25 changes: 16 additions & 9 deletions .github/workflows/openactive-test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ on:

jobs:
test-server:
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04
steps:
# Checks out this project ie OpenActive.Server.NET
- name: Checkout
Expand All @@ -32,7 +33,8 @@ jobs:
run: dotnet test ./OpenActive.Server.NET.Tests/OpenActive.Server.NET.Tests.csproj --configuration Release --no-build --verbosity normal

test-fake-database:
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04
steps:
# Checks out this project ie OpenActive.Server.NET
- name: Checkout
Expand All @@ -55,13 +57,14 @@ 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:
- test-server
- test-fake-database
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04

# Defines a matrix strategy for running tests with different combinations of parameters, allowing for parallel test
# runs with various configurations. For more information about these parameters, see `.github/README.md` in Test
Expand Down Expand Up @@ -107,6 +110,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 is it conditional?
- name: Install OpenActive.Server.NET dependencies
if: ${{ matrix.profile != 'no-auth' && matrix.profile != 'single-seller' }}
run: dotnet restore ./server/
Expand Down Expand Up @@ -154,7 +158,7 @@ jobs:

# uploads the test output as an artifact, which can be used for reference or debugging later
- name: Upload test output for ${{ matrix.mode }} mode as artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
if: ${{ success() || failure() }}
with:
name: core.${{ matrix.mode }}.${{ matrix.profile }}
Expand Down Expand Up @@ -234,7 +238,7 @@ jobs:
NODE_APP_INSTANCE: framework
working-directory: tests
- name: Upload test output for ${{ matrix.mode }} mode as artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
if: ${{ success() || failure() }}
with:
name: framework.${{ matrix.mode }}.${{ matrix.profile }}
Expand All @@ -247,7 +251,8 @@ jobs:
if: ${{ github.ref == 'refs/heads/master' }}
needs:
- core
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04
steps:
# Checkout the repo
- uses: actions/checkout@master
Expand Down Expand Up @@ -291,7 +296,8 @@ jobs:
needs:
- core
- framework
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -342,7 +348,8 @@ jobs:
needs:
- core
- framework
runs-on: ubuntu-latest
# ubuntu-latest's SSL library no longer supports .NET 3.1
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
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

7 changes: 6 additions & 1 deletion Examples/BookingSystem.AspNetCore.IdentityServer/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using OpenActive.FakeDatabase.NET;
using Microsoft.Extensions.Logging;

namespace IdentityServer
{
Expand All @@ -27,7 +28,11 @@ public Startup(IWebHostEnvironment environment, IConfiguration configuration)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IClientStore, ClientStore>();
services.AddSingleton(x => new FakeBookingSystem(AppSettings.FeatureFlags.FacilityUseHasSlots));
services.AddSingleton(x => new FakeBookingSystem
(
AppSettings.FeatureFlags.FacilityUseHasSlots,
x.GetRequiredService<ILogger<FakeBookingSystem>>()
));

var builder = services.AddIdentityServer(options =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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
{
/// <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");
}
}
}
}
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
11 changes: 10 additions & 1 deletion Examples/BookingSystem.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ ASPNETCORE_ENVIRONMENT=no-auth dotnet run --no-launch-profile --project ./Bookin

The above example starts the BookingSystem.AspNetCore in `no-auth` mode.

## Env Vars

BookingSystem.AspNetCore uses the following environment variables (note: this list is not exhaustive):

- `PERIODICALLY_REFRESH_DATA`: (optional - default `false`) If set to `true`,
the database will be periodically refreshed, deleting past data and replacing
it with future data.
- `IS_LOREM_FITSUM_MODE`: (optional - default `false`) If set to `true`, data is created in ["Lorem Fitsum" mode](#lorem-fitsum-mode).

## BookingSystem.AspNetCore Data Generation

BookingSystem.AspNetCore has three main uses that make it very important in the OpenActive ecosystem:
Expand Down Expand Up @@ -64,6 +73,6 @@ In the CLI this can be done by running the following command for example:
IS_LOREM_FITSUM_MODE=true dotnet run --no-launch-profile --project ./BookingSystem.AspNetCore.csproj --configuration Release --no-build
```

### Golden Records
#### Golden Records
Golden records are randomly generated records that have maximally enriched properties in the generated data. For example where a record might have one image normally, a golden record will have four.

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);
}
}
}
Loading
Loading