← Ascendy 한국어

frontend

It said 'Sync complete' — and uploaded zero. Silent success is the dangerous one

· Ascendy Engineering


TL;DR

Source note. Distilled from two frontend-team intakes (independent diagnosis & convergence / root-cause fix & reviewer catch). Internal identifiers, absolute numbers, API paths, and DB names are generalized (only the product fact — it syncs a photo library — is public domain). Same silent failure vein as the Celery worker that logged not a single INFO line.

The symptom — “complete,” and zero

The operator reported: “photos taken after a certain date won’t sync, auto or manual.” Yet on screen, a “Sync complete” toast appeared. No error, no failure indicator. With, in fact, not a single photo uploaded.

This is the nastiest kind of debugging. A loud failure at least hands you a stack trace. A silent success says “all done” while doing nothing — it won’t even tell you where to start. This regression was the third round that month, each time handed off with a different candidate cause.

Part 1 — 9 hypotheses, from code alone, with no prod logs

The frontend made a call: don’t wait for the backend’s prod logs — start diagnosing with what we have (the code). Waiting for the operator’s device-log capture would have delayed the first containment by days.

So, from code alone, it ranked ~9 hypotheses by suspicion. The top two:

All nine candidates fell into the “fixable by the frontend alone” category. So it closed three of them in one PR — a single-entry lock against parallelism, removing a bug where a cancel flag permanently silenced every background listener, and surfacing the last sync error in the UI.

Two diagnoses met in the same prod log

A few days later, the backend brought structured prod logs and DB counts. Three things were settled at once.

  1. New photos weren’t reaching the server at all. In what the client sent, there were no new items. So the backend was clean; the whole problem was that the client wasn’t sending new photos.
  2. The library had exceeded the client scan cap. For performance, the client scans only a fixed-size window (a top slice). Once the library grows past that cap, the window no longer covers the whole library. If the photos the operator was missing fall outside it, they’re invisible to the scan — they never even make it into the list sent to the server. The prod evidence — library beyond the cap, and no new items reaching the server — pointed exactly there.
  3. The session interrupts showed up in prod. The frontend’s #1 ranked hypothesis (the parallel race) was confirmed verbatim.

The hypotheses the frontend built from code alone and the conclusions the backend confirmed from prod data met on exactly the same two branches.

The real cause wasn’t the race

Here’s the crux. The parallel race was a real bug — but not the user’s bug.

After shipping the race fix to the operator, the report was unchanged: “new photos still don’t upload.” Exactly as expected. Closing a real bug and closing the bug the user reported are two different things. Both were needed.

Part 2 — the root-cause fix, and the bug the reviewer caught

The way to close the root cause was clear. Instead of a fixed “newest N” window, walk the whole library in chunks via a cursor — open only one session at a time, drain it fully, then move to the next chunk. (The backend already handles hashes idempotently, so backend changes: zero.)

We wrote v1 and put it up for review. And here’s the most meaningful thing that happened — an independent LLM reviewer caught a defect (D1): this cursor isn’t tie-safe.

If the cursor key is a single timestamp, trouble starts when more photos than a chunk’s worth share the same instant. A bulk import from an external messenger app, or a one-shot cloud restore, gives hundreds or thousands of photos the same timestamp. When that “plateau” is bigger than a chunk:

Lint, typecheck, and the existing unit tests all missed this — you can’t see it without a test that models that data distribution. It’s invisible in the code itself — it leaned on an unwritten assumption that “timestamps are unique,” and you only see it break by imagining the distribution of the data.

v2 — remove the plateau assumption entirely

Taking the reviewer’s finding, we went to v2:

The nice property: it guarantees forward progress regardless of plateau size. No matter how much bigger the plateau is than a chunk, each chunk strict-less steps exactly one notch into the next region, so nothing is skipped. The “plateau size” assumption disappears. (And we pinned a unit test that explicitly exercises the plateau scenario — the very test that models this data distribution.)

Takeaways


Authorship & citation: Written by Ascendy Engineering; quotable with attribution. Found something wrong? Let us know via a GitHub issue.


Tags: debugging, silent-failure, code-review, frontend, data-distribution