Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/backend/Shared/Experiments/KbxExperimentRuntimeStore.cs
T

49 lines
4.4 KiB
C#

using System.Text.Json;
using Dapper;
using Kbx.Shared.Experiments.Generated;
using Npgsql;
namespace Kbx.Shared.Experiments;
public sealed class KbxExperimentRuntimeStore(NpgsqlDataSource dataSource,KbxExperimentRolloutPublisher publisher)
{
public async Task UpdateRolloutAsync(Guid tenantId,Guid actorId,string experimentId,int percent,string state,string reason,string? correlationId,CancellationToken ct)
{
RequireKnownExperiment(experimentId);if(percent is <0 or >100)throw new ArgumentOutOfRangeException(nameof(percent));if(state is not ("running" or "paused"))throw new ArgumentException("state must be running or paused");if(string.IsNullOrWhiteSpace(reason))throw new ArgumentException("reason is required");
await ChangeAsync(tenantId,actorId,experimentId,"rollout",reason,correlationId,ct,async(connection,tx,before)=>{
await connection.ExecuteAsync(new CommandDefinition("""
insert into kbx.experiment_runtime(tenant_id,experiment_id,state,rollout_percent,kill_switch,updated_by)
values(@TenantId,@ExperimentId,@State,@Percent,false,@Actor)
on conflict(tenant_id,experiment_id) do update set state=excluded.state,rollout_percent=excluded.rollout_percent,kill_switch=false,version=kbx.experiment_runtime.version+1,updated_by=excluded.updated_by,updated_at=now();
""",new{TenantId=tenantId,ExperimentId=experimentId,State=state,Percent=percent,Actor=actorId},tx,cancellationToken:ct));
});
await publisher.PublishAsync(tenantId,experimentId,ct);
}
public async Task RollbackAsync(Guid tenantId,Guid actorId,string experimentId,string reason,string? correlationId,CancellationToken ct)
{
RequireKnownExperiment(experimentId);if(string.IsNullOrWhiteSpace(reason))throw new ArgumentException("reason is required");
await ChangeAsync(tenantId,actorId,experimentId,"rollback",reason,correlationId,ct,async(connection,tx,before)=>{
await connection.ExecuteAsync(new CommandDefinition("""
insert into kbx.experiment_runtime(tenant_id,experiment_id,state,rollout_percent,kill_switch,updated_by)
values(@TenantId,@ExperimentId,'rolled-back',0,true,@Actor)
on conflict(tenant_id,experiment_id) do update set state='rolled-back',rollout_percent=0,kill_switch=true,version=kbx.experiment_runtime.version+1,updated_by=excluded.updated_by,updated_at=now();
""",new{TenantId=tenantId,ExperimentId=experimentId,Actor=actorId},tx,cancellationToken:ct));
});
await publisher.PublishAsync(tenantId,experimentId,ct);
}
private static void RequireKnownExperiment(string experimentId)
{
if(!KbxExperimentCatalog.Experiments.ContainsKey(experimentId))throw new ArgumentException($"Unknown experiment: {experimentId}");
}
private async Task ChangeAsync(Guid tenantId,Guid actorId,string experimentId,string action,string reason,string? correlationId,CancellationToken ct,Func<NpgsqlConnection,NpgsqlTransaction,string?,Task> mutate)
{
await using var connection=await dataSource.OpenConnectionAsync(ct);await using var tx=await connection.BeginTransactionAsync(ct);
var before=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("select row_to_json(x)::text from (select state,rollout_percent,kill_switch,version from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId for update) x",new{TenantId=tenantId,ExperimentId=experimentId},tx,cancellationToken:ct));
await mutate(connection,tx,before);
var after=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("select row_to_json(x)::text from (select state,rollout_percent,kill_switch,version from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId) x",new{TenantId=tenantId,ExperimentId=experimentId},tx,cancellationToken:ct));
await connection.ExecuteAsync(new CommandDefinition("""
insert into kbx.experiment_audit(id,tenant_id,experiment_id,action,before_state,after_state,actor_id,reason,correlation_id)
values(@Id,@TenantId,@ExperimentId,@Action,cast(@Before as jsonb),cast(@After as jsonb),@Actor,@Reason,@CorrelationId);
""",new{Id=Guid.NewGuid(),TenantId=tenantId,ExperimentId=experimentId,Action=action,Before=before,After=after,Actor=actorId,Reason=reason,CorrelationId=correlationId},tx,cancellationToken:ct));
await tx.CommitAsync(ct);
}
}