ReputationService.getLatest and computeAndSave call each other with no base case — a contributor with no snapshot sends the request into an infinite DB loop
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- postgresql, typescript
Research direction
Start with src/reputation/reputation.service.ts and the getLatest and computeAndSave methods, then read src/reputation/reputation.service.spec.ts and the controller’s latest() route. Make the missing-snapshot path terminate, preserve the de-duplication check, and verify that computeAndSave saves once when no snapshot exists; the existing null-snapshot spec should also pass.
Written by the indexing model from the issue text.
Description
Location — src/reputation/reputation.service.ts:30 (computeAndSave, line 37 const latest = await this.getLatest(userId);) and src/reputation/reputation.service.ts:66 (getLatest, line 72 return this.computeAndSave(userId);).
Problem — The two methods are mutually recursive with no terminating condition for a user who has no snapshot row yet:
computeAndSave(userId)computes stats, then callsgetLatest(userId)to decide whether the new values are identical to the previous snapshot (the de-duplication check at lines 37-49) before it saves anything.getLatest(userId)runssnapshotRepo.findOne(...); when that returnsnullit callscomputeAndSave(userId)to lazily create the first snapshot.
So for any user with zero rows in reputation_snapshots, the sequence is: computeAndSave -> getLatest -> findOne returns null -> computeAndSave -> getLatest -> findOne still returns null (nothing was ever saved, because the save at line 51 is never reached) -> and so on. The recursion is await-based, so it does not blow the JS stack quickly; it simply loops forever, issuing one queryContributorCoreStats multi-join aggregate plus one findOne per iteration until the request times out or the process is killed.
Why it matters — This is the first-use path for every new contributor, i.e. the common case, not an edge case. The request never returns, and while it spins it holds a connection and hammers Postgres with an expensive aggregate query in a tight loop. A handful of such requests exhausts the TypeORM pool and takes down unrelated endpoints. It also silently defeats the lazy-snapshot behaviour that was supposed to fix empty profiles: no snapshot is ever written for a first-time user.
Reproduction / failure path
- Create a user who has no
reputation_snapshotsrow (any freshly registered contributor). - Authenticate as that user and call either
GET /reputation/:userIdorPOST /reputation/:userId/recompute. - The request hangs.
DATABASE_LOGGING=trueshows thereputation_snapshotsfindOneand the contributor-stats aggregate repeating indefinitely.
The existing unit test already disagrees with the code and should be failing: src/reputation/reputation.service.spec.ts:122 ('returns null when the user has no snapshot yet') mocks snapshotRepo.findOne to resolve null and asserts getLatest('new-user') resolves to null, but the code path goes into computeAndSave instead. Because queryContributorCoreStats is jest.mocked and has no return value configured in that test, the spec fails with a TypeError on stats.lifetimeEarnings rather than reporting the real recursion.
Suggested fix
- Remove the lazy-compute from
getLatestand make it a pure read that returnsnullwhen there is no snapshot (matching its declaredPromise<ReputationSnapshot | null>type and the existing spec). - Give
computeAndSaveits own private read for the de-duplication check — e.g.private findLatestSnapshot(userId)doing just thefindOne— so it never re-enters the lazy-create path. - Have the controller's
latest()route decide the lazy-create policy explicitly: read, and ifnull, callcomputeAndSaveexactly once and return its result. - Keep the
order: { computedAt: 'DESC' }read but addid: 'DESC'as a tie-break so two snapshots written in the sameCreateDateColumntick resolve deterministically. - Add a spec that calls
computeAndSavewithfindOneresolvingnulland assertssnapshotRepo.saveis called exactly once.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 90
- Avg merge
- 31m
- Merged PRs (30d)
- 20
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing 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.
More from MergeFi/backend
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug testing
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
zapcooking/frontend#946 ·
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
liberation-data/drivine#733 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
makecindy/cindy#10062 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day