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

46 lines
3.1 KiB
C#

using Dapper;
using Kbx.Shared.Experiments.Generated;
using Npgsql;
namespace Kbx.Shared.Experiments;
public sealed class KbxExperimentAssignmentService(NpgsqlDataSource dataSource)
{
public async Task<KbxExperimentAssignmentsResponse> EvaluateAsync(Guid tenantId,Guid userId,string screenId,CancellationToken ct)
{
var definitions=KbxExperimentCatalog.Experiments.Values.Where(x=>x.ScreenId==screenId).ToArray();
var items=new List<KbxExperimentAssignmentDto>();
await using var connection=await dataSource.OpenConnectionAsync(ct);
foreach(var definition in definitions)
{
var runtime=await connection.QuerySingleOrDefaultAsync<RuntimeRow>(new CommandDefinition("""
select state State,rollout_percent RolloutPercent,kill_switch KillSwitch
from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId;
""",new{TenantId=tenantId,ExperimentId=definition.Id},cancellationToken:ct));
var state=runtime?.State ?? definition.State;
var rollout=runtime?.RolloutPercent ?? definition.RolloutPercent;
var killed=runtime?.KillSwitch ?? false;
// Regular users only receive actively enrolled assignments. Draft/paused/rolled-back definitions stay internal.
if(killed||state!="running"||rollout<=0)continue;
// Rollout percentage remains authoritative even for previously assigned users.
// Assignment is preserved for reproducibility and becomes active again if rollout is raised later.
if(!KbxExperimentAssignmentPolicy.IsEnrolled(tenantId,userId,definition.Id,rollout))continue;
var existing=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("""
select variant from kbx.experiment_assignments where tenant_id=@TenantId and experiment_id=@ExperimentId and user_id=@UserId;
""",new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId},cancellationToken:ct));
if(existing is not null){items.Add(new(definition.Id,definition.FlagId,screenId,true,existing,state));continue;}
var variant=KbxExperimentAssignmentPolicy.ChooseVariant(tenantId,userId,definition);
await connection.ExecuteAsync(new CommandDefinition("""
insert into kbx.experiment_assignments(tenant_id,experiment_id,user_id,variant)
values(@TenantId,@ExperimentId,@UserId,@Variant) on conflict do nothing;
""",new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId,Variant=variant},cancellationToken:ct));
existing=await connection.QuerySingleAsync<string>(new CommandDefinition("""
select variant from kbx.experiment_assignments where tenant_id=@TenantId and experiment_id=@ExperimentId and user_id=@UserId;
""",new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId},cancellationToken:ct));
items.Add(new(definition.Id,definition.FlagId,screenId,true,existing,state));
}
return new(items);
}
private sealed record RuntimeRow(string State,int RolloutPercent,bool KillSwitch);
}