Fix: Delete the debug SCAN blocks / scope invalidation to domain prefixes; register a singleton IMongoClient; persist the GPU ledger (Redis is in-stack); nack poison messages requeue:false to a DLQ with bounded backoff.
Acceptance criteria: No SCAN */KEYS on hot paths; one shared Mongo client; GPU ledger survives a restart; poison messages dead-letter instead of looping.
**Problem — availability bombs that degrade production without any attacker:**
1. The L2 cache runs a full-cluster `SCAN *` on **every cache miss**; `InvalidateAll` serially deletes every key across the shared cluster.
2. A game-event consumer builds a new `MongoClient` **per message** → connection/socket exhaustion.
3. GpuCoordinator keeps its VRAM ledger only in memory → a restart resets it to zero while workers still hold GPUs → over-allocation / OOM.
4. Poison-message infinite-requeue loops pin CPU on a single bad message.
**Evidence:**
- `SpikerSoft.Business/Services/RedisL2CacheService.cs:123-140,277-288`; `Behaviors/CacheInvalidationBehavior.cs:49-54`
- `SpikerSoft.EventHandlers.GameEvents/Services/GamePersistenceService.cs:56` (`AddScoped`, per-message)
- `SpikerSoft.EventHandlers.GpuCoordinator/Services/LeaseManagerService.cs:161-168`
- Requeue loops: `KeycloakEvents/Services/KeycloakEventHostedService.cs:188-192`, `GpuCoordinator/Services/LeaseManagerService.cs:288-292`
**Fix:** Delete the debug `SCAN` blocks / scope invalidation to domain prefixes; register a singleton `IMongoClient`; persist the GPU ledger (Redis is in-stack); nack poison messages `requeue:false` to a DLQ with bounded backoff.
**Acceptance criteria:** No `SCAN *`/`KEYS` on hot paths; one shared Mongo client; GPU ledger survives a restart; poison messages dead-letter instead of looping.
**Effort:** S–M · Related: #371 (Redis cluster persistence incident).
spikerj
added the agentic label 2026-07-05 20:24:39 +00:00
Triage — "poison requeue" sub-item (reframed after code check)
Update: the MongoClient-per-message bomb is fixed in backend PR #111 (injects the singleton IMongoClient; GamePersistenceService was AddScoped + scoped-per-message).
On poison requeue — it's not a missing-mechanism bug. The messaging layer already has a retry-cap + dead-letter pattern: RabbitMQRetryHelper.GetRetryCount(ea) + ClassifyError(...) gated on _dlqConfig. Verified directly in UploadCoordinator/FileMovedConsumer.cs:
if(_dlqConfigisnull||!_dlqConfig.Enabled){/* requeue: true — fallback only */}varretryCount=RabbitMQRetryHelper.GetRetryCount(ea);varerrorType=RabbitMQRetryHelper.ClassifyError(exception);
So the BasicNack(..., requeue: true) calls a grep flags are mostly the DLQ-disabled fallback branch, not unconditional infinite requeue.
Therefore the real sub-item is a coverage/config audit, not a rewrite:
Confirm DeadLetterQueue.Enabled = true in every consumer's deployed appsettings (a single disabled config re-opens the infinite-requeue path).
Per-consumer audit that each failure path routes through RabbitMQRetryHelper — the RPC/hosted-service consumers (Keycloak, GpuCoordinator lease, CodeExecution, Embeddings, QuizGeneration) are the ones to eyeball, since some legitimately requeue: true for transient backpressure and may or may not cap.
(I couldn't produce a trustworthy per-consumer list by grep here — the repo path contains a space that broke my shell word-splitting — so flagging the method rather than a shaky inventory.) The other #411 bombs (Redis scans/flush, GPU ledger in-memory state) remain separate.
### Triage — "poison requeue" sub-item (reframed after code check)
Update: the **MongoClient-per-message** bomb is fixed in backend PR #111 (injects the singleton `IMongoClient`; `GamePersistenceService` was `AddScoped` + scoped-per-message).
On **poison requeue** — it's *not* a missing-mechanism bug. The messaging layer already has a retry-cap + dead-letter pattern: `RabbitMQRetryHelper.GetRetryCount(ea)` + `ClassifyError(...)` gated on `_dlqConfig`. Verified directly in `UploadCoordinator/FileMovedConsumer.cs`:
```csharp
if (_dlqConfig is null || !_dlqConfig.Enabled) { /* requeue: true — fallback only */ }
var retryCount = RabbitMQRetryHelper.GetRetryCount(ea);
var errorType = RabbitMQRetryHelper.ClassifyError(exception);
```
So the `BasicNack(..., requeue: true)` calls a grep flags are mostly the **DLQ-disabled fallback branch**, not unconditional infinite requeue.
**Therefore the real sub-item is a coverage/config audit, not a rewrite:**
1. Confirm `DeadLetterQueue.Enabled = true` in every consumer's deployed `appsettings` (a single disabled config re-opens the infinite-requeue path).
2. Per-consumer audit that each failure path routes through `RabbitMQRetryHelper` — the RPC/hosted-service consumers (Keycloak, GpuCoordinator lease, CodeExecution, Embeddings, QuizGeneration) are the ones to eyeball, since some legitimately `requeue: true` for transient backpressure and may or may not cap.
(I couldn't produce a trustworthy per-consumer list by grep here — the repo path contains a space that broke my shell word-splitting — so flagging the method rather than a shaky inventory.) The other #411 bombs (Redis scans/flush, GPU ledger in-memory state) remain separate.
Progress: the MongoClient-per-message bomb is now merged (PR #111 → master, GamePersistenceService reuses the singleton IMongoClient). A related per-invocation MongoClient leak in MongoDB_HealthCheck was split into #433 (PR #112, in review).
Keeping this epic open for the remaining availability bombs: Redis scans/FLUSH, the in-memory GPU-lease ledger, and the poison-requeue coverage audit (see prior comment — the DLQ/RabbitMQRetryHelper mechanism exists; needs a config/coverage pass rather than a rewrite).
Progress: the **MongoClient-per-message** bomb is now **merged** (PR #111 → `master`, `GamePersistenceService` reuses the singleton `IMongoClient`). A related per-invocation `MongoClient` leak in `MongoDB_HealthCheck` was split into #433 (PR #112, in review).
Keeping this epic **open** for the remaining availability bombs: Redis scans/`FLUSH`, the in-memory GPU-lease ledger, and the poison-requeue coverage audit (see prior comment — the DLQ/`RabbitMQRetryHelper` mechanism exists; needs a config/coverage pass rather than a rewrite).
Item 4 (poison requeue) — audited both cited sites; findings differ from the ticket
KeycloakEventHostedService (cited 188-192) is already protected — not a bug. Its HandleMessageFailureAsync runs the full machinery: RabbitMQRetryHelper retry-cap, _poisonDetector.RecordFailure, _dlqMetrics, and DLQ routing. The BasicNackAsync(requeue:true) at 188 is only the DLQ-disabled fallback — and DLQ is Enabled: true, MaxRetries: 3 in bothappsettings.Production.json and appsettings.Development.json. So the infinite-requeue path is never reached in any configured environment. No change needed here.
LeaseManagerService (GpuCoordinator) is the real gap. It has no DLQ/DLX/retry infrastructure at all, and three unconditional infinite-requeue poison loops:
Each is catch (Exception) { … BasicNackAsync(deliveryTag, multiple:false, requeue:true); } — a message that always throws pins the consumer.
Why I'm not blind-shipping this: the safe fix is not just flipping requeue:true → false. Without a dead-letter exchange, requeue:falsedrops the message — and dropping a release message would leak a GPU lease (the GPU never gets freed), which is worse than the loop. The correct fix mirrors the Keycloak/EventHandlerHostBuilder pattern: declare the 3 GPU queues with an x-dead-letter-exchange, add RabbitMQRetryHelper retry-cap, and dead-letter after N attempts. That touches queue declaration (and the GpuCoordinator is a bespoke host, not on the shared DLQ infra) and needs broker verification that poison messages land in the DLQ rather than vanish.
Recommendation: scope item 4 to justLeaseManagerService — port it onto the shared DLQ/retry infra (the same DeadLetterQueue config block + RabbitMQRetryHelper Keycloak already uses). I can implement it, but it wants a broker-integration check before merge, so flagging rather than auto-shipping. Items 1 (#122) and 2 (#111) are done; item 3 (GPU ledger persistence) is the other remaining piece.
### Item 4 (poison requeue) — audited both cited sites; findings differ from the ticket
**`KeycloakEventHostedService` (cited 188-192) is already protected — not a bug.** Its `HandleMessageFailureAsync` runs the full machinery: `RabbitMQRetryHelper` retry-cap, `_poisonDetector.RecordFailure`, `_dlqMetrics`, and DLQ routing. The `BasicNackAsync(requeue:true)` at 188 is *only* the `DLQ-disabled` fallback — and DLQ is `Enabled: true, MaxRetries: 3` in **both** `appsettings.Production.json` and `appsettings.Development.json`. So the infinite-requeue path is never reached in any configured environment. No change needed here.
**`LeaseManagerService` (GpuCoordinator) is the real gap.** It has **no** DLQ/DLX/retry infrastructure at all, and **three** unconditional infinite-requeue poison loops:
- `LeaseManagerService.cs:291` — `OnLeaseRequestReceived`
- `LeaseManagerService.cs:355` — `OnTaskCompleteReceived`
- `LeaseManagerService.cs:381` — `OnReleaseReceived`
Each is `catch (Exception) { … BasicNackAsync(deliveryTag, multiple:false, requeue:true); }` — a message that always throws pins the consumer.
**Why I'm not blind-shipping this:** the safe fix is *not* just flipping `requeue:true → false`. Without a dead-letter exchange, `requeue:false` **drops** the message — and dropping a `release` message would **leak a GPU lease** (the GPU never gets freed), which is worse than the loop. The correct fix mirrors the Keycloak/`EventHandlerHostBuilder` pattern: declare the 3 GPU queues with an `x-dead-letter-exchange`, add `RabbitMQRetryHelper` retry-cap, and dead-letter after N attempts. That touches queue declaration (and the GpuCoordinator is a bespoke host, not on the shared DLQ infra) and needs broker verification that poison messages land in the DLQ rather than vanish.
**Recommendation:** scope item 4 to *just* `LeaseManagerService` — port it onto the shared DLQ/retry infra (the same `DeadLetterQueue` config block + `RabbitMQRetryHelper` Keycloak already uses). I can implement it, but it wants a broker-integration check before merge, so flagging rather than auto-shipping. Items 1 (#122) and 2 (#111) are done; item 3 (GPU ledger persistence) is the other remaining piece.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem — availability bombs that degrade production without any attacker:
SCAN *on every cache miss;InvalidateAllserially deletes every key across the shared cluster.MongoClientper message → connection/socket exhaustion.Evidence:
SpikerSoft.Business/Services/RedisL2CacheService.cs:123-140,277-288;Behaviors/CacheInvalidationBehavior.cs:49-54SpikerSoft.EventHandlers.GameEvents/Services/GamePersistenceService.cs:56(AddScoped, per-message)SpikerSoft.EventHandlers.GpuCoordinator/Services/LeaseManagerService.cs:161-168KeycloakEvents/Services/KeycloakEventHostedService.cs:188-192,GpuCoordinator/Services/LeaseManagerService.cs:288-292Fix: Delete the debug
SCANblocks / scope invalidation to domain prefixes; register a singletonIMongoClient; persist the GPU ledger (Redis is in-stack); nack poison messagesrequeue:falseto a DLQ with bounded backoff.Acceptance criteria: No
SCAN */KEYSon hot paths; one shared Mongo client; GPU ledger survives a restart; poison messages dead-letter instead of looping.Effort: S–M · Related: #371 (Redis cluster persistence incident).
Triage — "poison requeue" sub-item (reframed after code check)
Update: the MongoClient-per-message bomb is fixed in backend PR #111 (injects the singleton
IMongoClient;GamePersistenceServicewasAddScoped+ scoped-per-message).On poison requeue — it's not a missing-mechanism bug. The messaging layer already has a retry-cap + dead-letter pattern:
RabbitMQRetryHelper.GetRetryCount(ea)+ClassifyError(...)gated on_dlqConfig. Verified directly inUploadCoordinator/FileMovedConsumer.cs:So the
BasicNack(..., requeue: true)calls a grep flags are mostly the DLQ-disabled fallback branch, not unconditional infinite requeue.Therefore the real sub-item is a coverage/config audit, not a rewrite:
DeadLetterQueue.Enabled = truein every consumer's deployedappsettings(a single disabled config re-opens the infinite-requeue path).RabbitMQRetryHelper— the RPC/hosted-service consumers (Keycloak, GpuCoordinator lease, CodeExecution, Embeddings, QuizGeneration) are the ones to eyeball, since some legitimatelyrequeue: truefor transient backpressure and may or may not cap.(I couldn't produce a trustworthy per-consumer list by grep here — the repo path contains a space that broke my shell word-splitting — so flagging the method rather than a shaky inventory.) The other #411 bombs (Redis scans/flush, GPU ledger in-memory state) remain separate.
Progress: the MongoClient-per-message bomb is now merged (PR #111 →
master,GamePersistenceServicereuses the singletonIMongoClient). A related per-invocationMongoClientleak inMongoDB_HealthCheckwas split into #433 (PR #112, in review).Keeping this epic open for the remaining availability bombs: Redis scans/
FLUSH, the in-memory GPU-lease ledger, and the poison-requeue coverage audit (see prior comment — the DLQ/RabbitMQRetryHelpermechanism exists; needs a config/coverage pass rather than a rewrite).Item 4 (poison requeue) — audited both cited sites; findings differ from the ticket
KeycloakEventHostedService(cited 188-192) is already protected — not a bug. ItsHandleMessageFailureAsyncruns the full machinery:RabbitMQRetryHelperretry-cap,_poisonDetector.RecordFailure,_dlqMetrics, and DLQ routing. TheBasicNackAsync(requeue:true)at 188 is only theDLQ-disabledfallback — and DLQ isEnabled: true, MaxRetries: 3in bothappsettings.Production.jsonandappsettings.Development.json. So the infinite-requeue path is never reached in any configured environment. No change needed here.LeaseManagerService(GpuCoordinator) is the real gap. It has no DLQ/DLX/retry infrastructure at all, and three unconditional infinite-requeue poison loops:LeaseManagerService.cs:291—OnLeaseRequestReceivedLeaseManagerService.cs:355—OnTaskCompleteReceivedLeaseManagerService.cs:381—OnReleaseReceivedEach is
catch (Exception) { … BasicNackAsync(deliveryTag, multiple:false, requeue:true); }— a message that always throws pins the consumer.Why I'm not blind-shipping this: the safe fix is not just flipping
requeue:true → false. Without a dead-letter exchange,requeue:falsedrops the message — and dropping areleasemessage would leak a GPU lease (the GPU never gets freed), which is worse than the loop. The correct fix mirrors the Keycloak/EventHandlerHostBuilderpattern: declare the 3 GPU queues with anx-dead-letter-exchange, addRabbitMQRetryHelperretry-cap, and dead-letter after N attempts. That touches queue declaration (and the GpuCoordinator is a bespoke host, not on the shared DLQ infra) and needs broker verification that poison messages land in the DLQ rather than vanish.Recommendation: scope item 4 to just
LeaseManagerService— port it onto the shared DLQ/retry infra (the sameDeadLetterQueueconfig block +RabbitMQRetryHelperKeycloak already uses). I can implement it, but it wants a broker-integration check before merge, so flagging rather than auto-shipping. Items 1 (#122) and 2 (#111) are done; item 3 (GPU ledger persistence) is the other remaining piece.PR #122 merged to master (
9d74b44) — item 1 (full-clusterSCAN *on every cache miss) eliminated.Epic status: item 1 ✅ (#122), item 2 ✅ (#111, MongoClient-per-message). Remaining: item 3 (GPU ledger persistence) and item 4 (
LeaseManagerServiceDLQ — see the audit above; needs DLX + broker verification). Keeping open.Board-sweep status (2026-07-22): items 1+2 merged (SCAN* removed #122; singleton MongoClient #111). REMAINING: item 3 GPU-ledger persistence + item 4 poison-message DLQ in LeaseManagerService.