49 lines
4.4 KiB
C#
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);
|
|
}
|
|
}
|