Scylla disable control lives at the storage layer, not the feature layer
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 135
- Forks
- 46
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 113
Description
ITEM_INVESTIGATION_AND_STRIKES_ENABLED=false turns off two features — Item Investigation and User Strikes — but the control is implemented in two places, at the wrong layer, and the two disagree.
Current implementation
scylla/noOpScylla.ts— a null-objectScyllareturning empty reads and dropping writes, selected by theScyllaDI factory iniocContainer.services/userStrikeService/userStrikeService.ts:36— anenabled: booleanconstructor parameter that early-returns at line 101.
Why this is wrong
One condition, two encodings. Whether a given code path is disabled by the null object or by the early return is a per-method accident. A method added to UserStrikeService tomorrow inherits the null object's silent-empty behaviour; whether that is correct depends on whether the method has side effects that don't touch Scylla. publishActions is exactly such a side effect, which is why the constructor flag had to exist at all — the no-op driver cannot stop it.
The storage layer can't know the right empty value. NoOpScylla.select() returns no rows because that is the only answer a driver can give. The domain answers differ: a strike count should be 0, a strike history should be empty, "is this user over the threshold" should be a definite no. Today those fall out of empty-rows by coincidence rather than by decision.
A health check becomes a silent success.
workers_jobs/ItemProcessingWorker.ts:47 calls scylla.connect() to fail fast when Scylla is unreachable. Against the null object that always resolves, so the fail-fast is inert — and that consumer is outside both feature services, so it isn't covered by either control.
The failure mode is silent. A deployment that sets SCYLLA_ENABLED=false unintentionally looks healthy: reads return empty, writes are dropped, strike counts read as 0. It is indistinguishable from a working-but-empty cluster. The only signal is one scylla.disabled log line at boot.
Proposed
Move the no-op to the feature boundary:
- Extract
IUserStrikeService/IItemInvestigationService(6 public methods each) and register a no-op implementation of each when the feature is off. - Delete
NoOpScyllaand theenabledconstructor parameter; "disabled" becomes "you were handed the no-op implementation", stated once per method at the layer that knows the domain semantics. - Give
ItemProcessingWorkeran explicit gate rather than relying on a driver whoseconnect()silently resolves.
Roughly cost-neutral: ~30 lines per no-op service against the 83 deleted, with the interface extraction as the one real prerequisite.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading scylla/noOpScylla.ts, services/userStrikeService/userStrikeService.ts, and workers_jobs/ItemProcessingWorker.ts to trace the current disable paths and their consumers. Then map the six public methods in each feature service and the Scylla DI registration. Done means feature-level no-op implementations handle disabled behavior, the storage no-op and constructor flag are removed, and the worker has an explicit gate.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, databases
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100