fix: make default initialization concurrency-safe

This commit is contained in:
cesnimda
2026-09-22 07:33:51 +02:00
parent 38cf37ef96
commit 57a6bd02c6
4 changed files with 187 additions and 15 deletions
+23 -13
View File
@@ -27,13 +27,7 @@ namespace JobTrackerApi.Controllers
// Per-user rule settings when authenticated.
if (!string.IsNullOrWhiteSpace(_db.CurrentUserId))
{
var u = await _db.UserRuleSettings.FirstOrDefaultAsync(x => x.OwnerUserId == _db.CurrentUserId, cancellationToken);
if (u is null)
{
u = new UserRuleSettings { OwnerUserId = _db.CurrentUserId };
_db.UserRuleSettings.Add(u);
await _db.SaveChangesAsync(cancellationToken);
}
var u = await GetOrCreateUserSettingsAsync(_db.CurrentUserId, cancellationToken);
return Ok(new RuleSettings
{
@@ -64,12 +58,7 @@ namespace JobTrackerApi.Controllers
// Per-user rule settings when authenticated.
if (!string.IsNullOrWhiteSpace(_db.CurrentUserId))
{
var s = await _db.UserRuleSettings.FirstOrDefaultAsync(x => x.OwnerUserId == _db.CurrentUserId, cancellationToken);
if (s is null)
{
s = new UserRuleSettings { OwnerUserId = _db.CurrentUserId };
_db.UserRuleSettings.Add(s);
}
var s = await GetOrCreateUserSettingsAsync(_db.CurrentUserId, cancellationToken);
s.AppliedFollowUpDays = Clamp(incoming.AppliedFollowUpDays, 1, 365);
s.AppliedGhostDays = Clamp(incoming.AppliedGhostDays, 1, 365);
@@ -101,6 +90,27 @@ namespace JobTrackerApi.Controllers
return NoContent();
}
private async Task<UserRuleSettings> GetOrCreateUserSettingsAsync(string ownerUserId, CancellationToken cancellationToken)
{
var settings = await _db.UserRuleSettings.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
if (settings is not null) return settings;
settings = new UserRuleSettings { OwnerUserId = ownerUserId };
_db.UserRuleSettings.Add(settings);
try
{
await _db.SaveChangesAsync(cancellationToken);
return settings;
}
catch (DbUpdateException)
{
_db.Entry(settings).State = EntityState.Detached;
settings = await _db.UserRuleSettings.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
if (settings is null) throw;
return settings;
}
}
private static int Clamp(int v, int min, int max) => v < min ? min : v > max ? max : v;
}
}
@@ -357,6 +357,7 @@ public sealed class ApplicationChecklistService : IApplicationChecklistService
var missing = Defaults.Where(t => !existing.Contains(t.Key)).ToList();
if (missing.Count == 0) return items;
var seeded = new List<ApplicationChecklistItem>(missing.Count);
var order = 0;
foreach (var template in Defaults)
{
@@ -377,12 +378,27 @@ public sealed class ApplicationChecklistService : IApplicationChecklistService
};
_db.ApplicationChecklistItems.Add(item);
items.Add(item);
seeded.Add(item);
}
order++;
}
await _db.SaveChangesAsync(ct);
return items;
try
{
await _db.SaveChangesAsync(ct);
return items;
}
catch (DbUpdateException)
{
foreach (var item in seeded) _db.Entry(item).State = EntityState.Detached;
var persisted = await LoadItemsAsync(ownerUserId, jobApplicationId, ct);
var persistedKeys = persisted.Where(i => i.SystemKey is not null)
.Select(i => i.SystemKey!)
.ToHashSet(StringComparer.Ordinal);
if (missing.Any(template => !persistedKeys.Contains(template.Key))) throw;
return persisted;
}
}
private async Task SyncAutoCompletionAsync(List<ApplicationChecklistItem> items, ChecklistSignals signals, CancellationToken ct)