forked from gongxuegit/tiku-backend.net
refactor: consolidate backend into modular monolith
This commit is contained in:
192
Tiku.IntegrationTests/Api/MonolithBackgroundProcessingTests.cs
Normal file
192
Tiku.IntegrationTests/Api/MonolithBackgroundProcessingTests.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.Text.Json;
|
||||
using System.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.BackgroundProcessing;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class MonolithBackgroundProcessingTests
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string?> EnabledConfiguration =
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
["BackgroundProcessing:Enabled"] = "true",
|
||||
["BackgroundProcessing:JobPollSeconds"] = "1",
|
||||
["BackgroundProcessing:JobParallelism"] = "2",
|
||||
["BackgroundProcessing:JobBatchSize"] = "2",
|
||||
["TenantDomains:Enabled"] = "false",
|
||||
["SaasSubscriptions:Enabled"] = "false",
|
||||
["FeatureUsageReconciliation:Enabled"] = "false"
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Readiness_reports_only_postgres_and_redis_dependencies()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/health/ready");
|
||||
using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.True(document.RootElement.TryGetProperty("database", out _));
|
||||
Assert.True(document.RootElement.TryGetProperty("redis", out _));
|
||||
Assert.False(document.RootElement.TryGetProperty("rabbitMq", out _));
|
||||
Assert.False(document.RootElement.TryGetProperty("outbox", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Background_processing_registration_obeys_master_switch()
|
||||
{
|
||||
await using var disabledFactory = new ApiTestFactory();
|
||||
using var disabledClient = disabledFactory.CreateClient();
|
||||
var disabledNames = disabledFactory.Services.GetServices<IHostedService>()
|
||||
.Select(service => service.GetType().Name)
|
||||
.ToArray();
|
||||
|
||||
Assert.False(disabledFactory.Services.GetRequiredService<IOptions<BackgroundProcessingOptions>>().Value.Enabled);
|
||||
Assert.Contains("TenantDomainBackgroundService", disabledNames);
|
||||
Assert.Contains("SaasSubscriptionBackgroundService", disabledNames);
|
||||
Assert.Contains("FeatureUsageBackgroundService", disabledNames);
|
||||
Assert.Contains("BackgroundJobsBackgroundService", disabledNames);
|
||||
|
||||
await using var enabledFactory = new ApiTestFactory(configurationOverrides: EnabledConfiguration);
|
||||
using var enabledClient = enabledFactory.CreateClient();
|
||||
var enabledNames = enabledFactory.Services.GetServices<IHostedService>()
|
||||
.Select(service => service.GetType().Name)
|
||||
.ToArray();
|
||||
|
||||
Assert.True(enabledFactory.Services.GetRequiredService<IOptions<BackgroundProcessingOptions>>().Value.Enabled);
|
||||
Assert.Contains("TenantDomainBackgroundService", enabledNames);
|
||||
Assert.Contains("SaasSubscriptionBackgroundService", enabledNames);
|
||||
Assert.Contains("FeatureUsageBackgroundService", enabledNames);
|
||||
Assert.Contains("BackgroundJobsBackgroundService", enabledNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Api_host_processes_immediate_and_due_postgres_jobs()
|
||||
{
|
||||
await using var factory = new ApiTestFactory(configurationOverrides: EnabledConfiguration);
|
||||
using var client = factory.CreateClient();
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "Monolith Background Processing"
|
||||
});
|
||||
|
||||
BackgroundJobItem immediate;
|
||||
BackgroundJobItem delayed;
|
||||
using (var scope = factory.CreateSystemScope("Queue monolith background jobs"))
|
||||
{
|
||||
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
immediate = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId,
|
||||
"statistics_aggregation",
|
||||
JsonSerializer.SerializeToElement(new { scope = "tenant" })));
|
||||
delayed = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId,
|
||||
"statistics_aggregation",
|
||||
JsonSerializer.SerializeToElement(new { scope = "tenant" }),
|
||||
DateTimeOffset.UtcNow.AddMinutes(5)));
|
||||
}
|
||||
|
||||
Assert.Equal(BackgroundJobStatus.Succeeded, await WaitForStatusAsync(factory, immediate.Id));
|
||||
Assert.Equal(BackgroundJobStatus.Pending, await ReadStatusAsync(factory, delayed.Id));
|
||||
|
||||
using (var scope = factory.CreateSystemScope("Make delayed monolith job due"))
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
await dbContext.BackgroundJobs
|
||||
.Where(job => job.Id == delayed.Id)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(job => job.RunAfter, DateTimeOffset.UtcNow.AddSeconds(-1)));
|
||||
}
|
||||
|
||||
Assert.Equal(BackgroundJobStatus.Succeeded, await WaitForStatusAsync(factory, delayed.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Postgres_leases_recover_expired_work_without_duplicate_claims()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "Monolith Lease Recovery"
|
||||
});
|
||||
|
||||
Guid expiredJobId;
|
||||
Guid pendingJobId;
|
||||
using (var scope = factory.CreateSystemScope("Seed lease recovery jobs"))
|
||||
{
|
||||
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
|
||||
expiredJobId = (await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId,
|
||||
"statistics_aggregation",
|
||||
JsonSerializer.SerializeToElement(new { scope = "tenant" })))).Id;
|
||||
pendingJobId = (await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
|
||||
tenantId,
|
||||
"statistics_aggregation",
|
||||
JsonSerializer.SerializeToElement(new { scope = "tenant" })))).Id;
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
await dbContext.BackgroundJobs
|
||||
.Where(job => job.Id == expiredJobId)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(job => job.Status, BackgroundJobStatus.Processing)
|
||||
.SetProperty(job => job.LockedBy, "stopped-worker")
|
||||
.SetProperty(job => job.LockExpiresAt, DateTimeOffset.UtcNow.AddSeconds(-1)));
|
||||
}
|
||||
|
||||
var processed = await Task.WhenAll(
|
||||
ProcessPendingAsync(factory, "monolith-lease-a"),
|
||||
ProcessPendingAsync(factory, "monolith-lease-b"));
|
||||
|
||||
Assert.Equal(2, processed.Sum());
|
||||
Assert.Equal(BackgroundJobStatus.Succeeded, await ReadStatusAsync(factory, expiredJobId));
|
||||
Assert.Equal(BackgroundJobStatus.Succeeded, await ReadStatusAsync(factory, pendingJobId));
|
||||
}
|
||||
|
||||
private static async Task<int> ProcessPendingAsync(ApiTestFactory factory, string workerId)
|
||||
{
|
||||
using var scope = factory.CreateSystemScope($"Process jobs with {workerId}");
|
||||
return await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>()
|
||||
.ProcessPendingAsync(workerId, 1);
|
||||
}
|
||||
|
||||
private static async Task<BackgroundJobStatus> WaitForStatusAsync(ApiTestFactory factory, Guid jobId)
|
||||
{
|
||||
var timeout = DateTimeOffset.UtcNow.AddSeconds(10);
|
||||
while (DateTimeOffset.UtcNow < timeout)
|
||||
{
|
||||
var status = await ReadStatusAsync(factory, jobId);
|
||||
if (status is BackgroundJobStatus.Succeeded or BackgroundJobStatus.Failed)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
return await ReadStatusAsync(factory, jobId);
|
||||
}
|
||||
|
||||
private static async Task<BackgroundJobStatus> ReadStatusAsync(ApiTestFactory factory, Guid jobId)
|
||||
{
|
||||
using var scope = factory.CreateSystemScope("Read monolith background job status");
|
||||
return await scope.ServiceProvider.GetRequiredService<TikuDbContext>()
|
||||
.BackgroundJobs.AsNoTracking()
|
||||
.Where(job => job.Id == jobId)
|
||||
.Select(job => job.Status)
|
||||
.SingleAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user