Skip to content

fix(ingestor): advertise RankSnapshotWriter sync side-effect (#267)#514

Merged
ilyanfraimbault merged 2 commits into
developfrom
claude/great-bell-qhrtlr
Jun 15, 2026
Merged

fix(ingestor): advertise RankSnapshotWriter sync side-effect (#267)#514
ilyanfraimbault merged 2 commits into
developfrom
claude/great-bell-qhrtlr

Conversation

@ilyanfraimbault

Copy link
Copy Markdown
Owner

Summary

Resolves #267. RankSnapshotWriter.Write mutated account.LastRankSyncAtUtc (and Score) even when it returned Unchanged, which read as a surprising side-effect on the no-op path.

The issue offered two fixes: (1) guard the timestamp update behind the changed-state branch, or (2) rename the method to advertise the side-effect. I deliberately chose option 2, because option 1 would introduce a behavioral regression:

  • LastRankSyncAtUtc is the freshness gate in AccountRefreshProcess (RankSyncFreshness, default 15 min, see AccountRefreshProcess.cs:139). It stops the process from re-issuing the League-v4 by-puuid call and dedups against DiscoveryProcess.
  • A fresh reading that happens to match the latest rank is still a successful sync. If the timestamp only advanced when the rank changed, stable-rank accounts (the common case on the ladder) would be re-fetched from Riot every cycle, defeating the gate and burning rate limit.

So the bump on the Unchanged path is load-bearing, not accidental. This PR keeps the behavior and makes it explicit instead:

  • Rename IRankSnapshotWriter.WriteIngest to signal it does more than append a row.
  • Add interface XML docs explaining that every reading advances the account's sync bookkeeping (LastRankSyncAtUtc + Score) regardless of outcome, while a snapshot row is appended only when the rank actually changed.
  • Clarify the inline comment on the unconditional bookkeeping.
  • Update the two call sites (AccountRefreshProcess, DiscoveryProcess).

No runtime behavior changes — this is a rename + documentation change.

Testing

⚠️ The .NET 10 SDK is not available in this environment and the network policy blocks installing it, so I could not run dotnet build/dotnet test locally. The change is a pure method rename plus comments; I verified by inspection that the only .Write(...) call sites on the writer were the two updated, and that all other references are to the type name. CI will compile and run the existing unit/integration suites.

Closes #267


Generated by Claude Code

RankSnapshotWriter.Write mutated account.LastRankSyncAtUtc (and Score) even
when it returned Unchanged, which read as a surprising side-effect on the
no-op path.

The timestamp bump is load-bearing rather than accidental:
LastRankSyncAtUtc is the freshness gate (RankSyncFreshness, 15m) that stops
AccountRefreshProcess from re-issuing the League-v4 by-puuid call and dedups
against DiscoveryProcess. A fresh reading that matches the latest rank is
still a successful sync, so the timestamp must advance on the Unchanged path;
guarding it behind the changed-state branch would silently regress that gate
and re-fetch stable-rank accounts every cycle.

Per the issue's second option, advertise the side-effect instead of hiding
it: rename Write -> Ingest and document on the interface that every reading
advances the account's sync bookkeeping while a snapshot row is appended only
when the rank actually changed.

Closes #267
@ilyanfraimbault ilyanfraimbault marked this pull request as ready for review June 15, 2026 12:55

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

La PR est propre : le renommage est complet, tous les call sites sont mis à jour, et la documentation XML explique clairement pourquoi l'effet de bord sur Unchanged est délibéré.

Vérifications effectuées :

  • AccountRefreshProcess.cs et DiscoveryProcess.cs : les deux appels .Write().Ingest()
  • RankSnapshotWriter.cs : interface + implémentation renommées ✓
  • Tests : aucun fichier de test n'appelle .Write() directement — les tests unitaires mockent l'interface sans configurer d'expectation sur cette méthode ; les tests d'intégration passent par process.RunCoreAsync()

Suggestions (non bloquantes)

  • backend/tests/TrueMain.IntegrationTests/RankSnapshotIngestionTests.cs:240 — le nom de méthode RunCoreAsync_SuccessfulRankWrite_BumpsLastRankSyncAtUtc contient encore Write. À renommer en RunCoreAsync_SuccessfulRankIngest_BumpsLastRankSyncAtUtc pour rester cohérent avec le nouveau vocabulaire (ne bloque pas la compilation, simple cohérence).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: La PR est propre. Renommage complet, call sites mis a jour, docs XML correctes. Aucun point bloquant. Suggestions (non bloquantes) : RankSnapshotIngestionTests.cs:240 — nom de methode RunCoreAsync_SuccessfulRankWrite_BumpsLastRankSyncAtUtc contient encore Write, a renommer en RunCoreAsync_SuccessfulRankIngest_BumpsLastRankSyncAtUtc pour coherence.

Vocabulary consistency follow-up from PR review: the rank-write test name
still referenced the old method name.
// calls (see AccountRefreshProcess), and Score is the denormalised
// leaderboard sort key kept in lock-step with the latest rank so the
// leaderboard can ORDER BY it without recomputing in SQL. Both are
// idempotent — EF only writes when the value actually changes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NON BLOQUANT — Le « Both are idempotent » est légèrement inexact pour LastRankSyncAtUtc. Score est bien idempotent (EF ne génère pas de SQL UPDATE si la valeur calculée est identique à celle trackée), mais LastRankSyncAtUtc = nowUtc avance à chaque appel — EF voit systématiquement un changement et émet toujours l'UPDATE. Ce ne devrait être que Score qui est qualifié d'idempotent.

Suggested change
// idempotent — EF only writes when the value actually changes.
// A fresh reading always advances the account's sync bookkeeping, even on
// the Unchanged path: LastRankSyncAtUtc gates redundant League-v4 by-puuid
// calls (see AccountRefreshProcess), and Score is the denormalised
// leaderboard sort key kept in lock-step with the latest rank so the
// leaderboard can ORDER BY it without recomputing in SQL. Score is
// idempotent — EF only writes when the value actually changes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review - APPROUVE

Rename + documentation pur (Write vers Ingest), aucune regression comportementale. Le raisonnement qui justifie le bump sur le chemin Unchanged est solide et desormais bien documente dans l'XML doc de l'interface.

Le renommage est complet : interface, implementation concrete, deux call sites, et le nom du test d'integration concerne.

Suggestion (non bloquante) : RankSnapshotWriter.cs:53 - 'Both are idempotent' s'applique a Score mais pas a LastRankSyncAtUtc qui avance a chaque appel. Remplacer par 'Score is idempotent' eviterait la confusion. Voir le commentaire inline pour la suggestion de code.

@ilyanfraimbault ilyanfraimbault merged commit 78e9f06 into develop Jun 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ingestor): RankSnapshotWriter must not mutate LastRankSyncAtUtc on Unchanged

2 participants