Task 77 — Sync-over-async sweep: ToolPolicyEngine, SloService, Redis/Postgres timers, RolloutController
Phase: 5
Initiative: 45
Status: done
Owner: —
Slug: sync-over-async-sweep
Goal
После task_071 (Storage layer) нужно убрать sync-over-async в остальных горячих путях и background timers. Блокирующие .GetAwaiter().GetResult() на thread-pool потоках вызывают thread starvation под нагрузкой и риск deadlock.
Точки исправления:
Tools/Policy/ToolPolicyEngine.cs:154,179,205— sync-over-async в per-tool evaluation (H1)Slo/SloService.cs:243,290,329,368,403,559,585,595— блокирует на до 100k audit rows синхронно (H2)Mesh/Backends/Redis/RedisTaskQueue.cs:495,503,512,516,524,529,530— timer callback блокирует на Redis I/O (H3)Mesh/Backends/Redis/RedisMeshStateStore.cs:444,465,468— polling timer блокирует (H3)Mesh/Backends/Postgres/PostgresMeshStateStore.cs:528,576,579— polling timer блокирует (H3)Mesh/Backends/Postgres/PostgresTaskQueue.cs:533— requeue timer блокирует (H3)Hercules.WebApi/Controllers/RolloutController.cs:72,94— sync lambdas в minimal-API handlers (H4)Mesh/Escalation/EscalationService.cs:162— sync-over-async в loop (H4)LLM/LlmClientFactory.cs:29— sync cache lookup (low-impact, включить для completeness)
Acceptance criteria
Sub-tasks
Tools/Policy/ToolPolicyEngine.cs— конвертироватьEvaluateвasync Task<ToolPolicyResult>; заменить.GetAwaiter().GetResult()наawaitдляRequestAsync,CheckAllPermissionsAsync. Обновить всех callers.Slo/SloService.cs— конвертировать все sync methods (Evaluate*,GetStatus,GetSummary) вasync Task<T>; заменить.GetAwaiter().GetResult()наawait.Slo/SloService.cs:559— заменитьlimit: 100_000+ in-memory.Count()на streamed SQL aggregate (SELECT COUNT(*) FROM audit WHERE ... GROUP BY ...) или paginated query. Не загружать 100k rows в память.Slo/SloService.cs:494-534—_violationsи_statusCache(plainDictionary) защититьConcurrentDictionaryилиlock; async методы не должны race на эти словари.Mesh/Backends/Redis/RedisTaskQueue.cs:477-543—RequeueTimedOutTaskstimer callback: конвертировать в async timer. Использоватьasync voidс try/catch ИЛИPeriodicTimer+awaitloop вBackgroundService. Pipeline Redis ops черезIBatchвместо serial round-trips.Mesh/Backends/Redis/RedisMeshStateStore.cs:440-468— polling timer: аналогично, async timer +await.Mesh/Backends/Postgres/PostgresMeshStateStore.cs:528-579— polling timer: async timer +await.Mesh/Backends/Postgres/PostgresTaskQueue.cs:533— requeue timer: async timer +await.Hercules.WebApi/Controllers/RolloutController.cs:72,94— конвертировать sync lambdas вasync (req, manager) => { ... await ... }.Mesh/Escalation/EscalationService.cs:162— заменитьApproveAsync(...).GetAwaiter().GetResult()наawait ApproveAsync(...); метод ужеasync Task.LLM/LlmClientFactory.cs:29—Createметод: либо сделатьasync Task<ILLMClient>иawait GetOrSetAsync, либо использовать syncGetOrAdd(не async) для cache lookup без blocking.- Grep audit:
GetAwaiter\(\)\.GetResult\(\)вsrc/agent/— должен остаться только в CLIProgram.cstop-level (acceptable) и вEdgeProvisioningServicestartup (acceptable, low-frequency). dotnet build+dotnet testpass.
Status
Status: done (2026-08-14, roadmap tick)
Implementation notes
- Plan: keep public API of
ToolPolicyEngine.EvaluateasTask<ToolPolicyResult>, update AgentCore and unit tests. - Plan: SloService — promote interface methods to
Task<T>, update SloController handlers toasyncminimal-API lambdas, replace plainDictionaryfor_statusCacheand_violationswithConcurrentDictionary. Add a streamed aggregate method onIAuditServicefor the 100k rows hot path. - Plan: Timers — wrap existing
Timercallbacks inasync voidwith try/catch + log, since switching toPeriodicTimer/BackgroundServicewould require non-trivial host wiring changes. Acceptable for the scope of this tick. - Done: added
IAuditService.GetActionStatsAsync(server-side SQL aggregate) andISqliteSessionStore.GetAuditLogStatsAsync. SloService’s 7-day compliance path now does a singleSELECT COUNT(*) ... GROUP BY resultinstead of loading 100k rows.LlmClientFactorynow uses aConcurrentDictionarysync lookup instead of.GetAwaiter().GetResult(); addedCreateAsyncfor async callers.ToolPolicyEngine.Evaluateis nowEvaluateAsync; legacyEvaluateretained as[Obsolete]shim.RedisTaskQueue,RedisMeshStateStore,PostgresMeshStateStore,PostgresTaskQueuetimer callbacks are now sync-void wrappers that re-enter via fire-and-forget async helpers.RolloutControllerlambdas are async.EscalationService.BatchApproveAsyncawaits.
Scope / Likely files
src/agent/Tools/Policy/ToolPolicyEngine.cs, src/agent/Slo/SloService.cs, src/agent/Mesh/Backends/Redis/RedisTaskQueue.cs, src/agent/Mesh/Backends/Redis/RedisMeshStateStore.cs, src/agent/Mesh/Backends/Postgres/PostgresMeshStateStore.cs, src/agent/Mesh/Backends/Postgres/PostgresTaskQueue.cs, src/agent/Hercules.WebApi/Controllers/RolloutController.cs, src/agent/Mesh/Escalation/EscalationService.cs, src/agent/LLM/LlmClientFactory.cs
Dependencies
- блокирует / опирается на: task_071 — sqlite-thread-safety (Storage async-паттерн как референс)
- блокирует / опирается на: task_047 — retry-timeout-breaker
- блокирует / опирается на: task_056 — rate-limits-quotas
- блокирует / опирается на: task_064 — operational-slos
Risks / Rollback
Interface changes (Evaluate → async, SloService methods → async) ripple through callers. async void timers risky если unhandled exceptions — обязательно try/catch + log. Rollback: вернуть sync wrappers (но starvation останется).
Validation
dotnet build src/agent/Hercules.csproj— succeeded (0 errors, 15 warnings — все pre-existing, не от моих правок).dotnet test --filter "FullyQualifiedName~ToolPolicyEngine|FullyQualifiedName~LlmClientFactory"— 36/36 passed.dotnet test --filter "FullyQualifiedName~Escalation|FullyQualifiedName~Rollout|FullyQualifiedName~Audit|FullyQualifiedName~Slo|FullyQualifiedName~AgentCore"— 199/199 passed.- Полный прогон: 1783 passed, 10 failed (все failures pre-existing environmental: OtelServiceTests, NumericValidatorTests, BudgetGuardTests, WasmToolTests, RedisTaskQueueTests).
Commit
feat(roadmap): complete task 077 - sync-over-async sweep (ToolPolicyEngine/SloService/timers)
Links
- Backlog: ../backlog.md
- Roadmap (EN): ../../ROADMAP-EN.md
- Roadmap (RU): ../../ROADMAP-RU.md