fix(rules): read saved thresholds without stale cache

This commit is contained in:
cesnimda
2026-11-10 23:18:37 +01:00
parent 64603f5602
commit e286be48ba
4 changed files with 466 additions and 24 deletions
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
@@ -31,10 +30,9 @@ namespace JobTrackerApi.Controllers
private readonly ICvTemplateRenderer _cvTemplateRenderer;
private readonly ICvPdfExporter _cvPdfExporter;
private readonly IJobCvMatchService _matchService;
private readonly IMemoryCache _cache;
private readonly IApplicationChecklistService _checklist;
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, UserManager<ApplicationUser> users, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null, IApplicationChecklistService? checklist = null)
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, UserManager<ApplicationUser> users, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, IJobCvMatchService? matchService = null, IApplicationChecklistService? checklist = null)
{
_checklist = checklist ?? new ApplicationChecklistService(db);
_db = db;
@@ -43,21 +41,6 @@ namespace JobTrackerApi.Controllers
_cvTemplateRenderer = cvTemplateRenderer ?? new CvTemplateRenderer();
_cvPdfExporter = cvPdfExporter ?? new ThrowingCvPdfExporter();
_matchService = matchService ?? new JobCvMatchService();
_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
}
// ponytail: RulesEngine.GetSettings is per-user (falls back to the global RuleSettings
// singleton only when the user has no override), so the cache key must include the
// current user id -- a single global key would leak one user's follow-up rules to another.
private async Task<RuleSettings> GetCachedRuleSettingsAsync(CancellationToken cancellationToken)
{
var cacheKey = $"rulesettings:{_db.CurrentUserId ?? "anon"}";
var cached = await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
return await RulesEngine.GetSettings(_db, cancellationToken);
});
return cached!;
}
private sealed class ThrowingCvPdfExporter : ICvPdfExporter
@@ -539,7 +522,7 @@ Canonical profile:
query = query.Where(j => j.Location != null && EF.Functions.Like(j.Location, like));
}
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var now = DateTime.Now;
Task<Dictionary<int, DateTime?>> LoadLatestMessagesAsync(IQueryable<Correspondence> messages) => messages
@@ -650,7 +633,7 @@ Canonical profile:
if (job is null) return NotFound();
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var now = DateTime.Now;
var lm = await _db.Correspondences
.AsNoTracking()
@@ -693,7 +676,7 @@ Canonical profile:
if (upcomingDays < 1) upcomingDays = 1;
if (upcomingDays > 90) upcomingDays = 90;
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var now = DateTime.Now;
var upcomingTo = now.AddDays(upcomingDays);
@@ -839,7 +822,7 @@ Canonical profile:
.Include(j => j.Company)
.FirstAsync(j => j.Id == job.Id, cancellationToken);
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var followUp = RulesEngine.Evaluate(settings, created, DateTime.Now, lastMessageAt: null);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, BuildJobApplicationDto(created, followUp));
}
@@ -1029,7 +1012,7 @@ Canonical profile:
await _db.SaveChangesAsync(cancellationToken);
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var lastMsg = await _db.Correspondences
.AsNoTracking()
.Where(c => c.JobApplicationId == id)
@@ -1383,7 +1366,7 @@ Candidate CV/profile:
.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
if (job is null) return NotFound();
var settings = await GetCachedRuleSettingsAsync(cancellationToken);
var settings = await RulesEngine.GetSettings(_db, cancellationToken);
var now = DateTime.Now;
var lastMessageAt = await _db.Correspondences
.AsNoTracking()