46 lines
3.1 KiB
C#
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);
|
|
}
|