← Ascendy 한국어

frontend

Commenting out a feature doesn't bring it back — the double-trigger race we found restoring it

· Ascendy Engineering


TL;DR

Background — toggled on, but it doesn’t run

A report came in — native gallery auto-sync wasn’t working. The toggle was clearly on. Tracing it down, the culprit was two auto-retrigger listeners, left commented out:

The only one still alive was the cold-start trigger that runs when the app first launches. So unless Wi-Fi and new photos happened to align at launch time, auto-sync effectively never ran.

Why it was commented out — the trace of an emergency block

Auto-sync used to work fine. But we had to urgently block sync from starting at a certain entry point where it wasn’t wanted, and that quick block was done by commenting out the listeners. The intended follow-up was “off by default + sync only once a condition gate is on.”

The condition gate did go in (both sync entry points check it). But the trigger restore was missed. The result: “the gate is saved, but the trigger that reads it and actually starts is dead” — for a long stretch.

Lesson 1 — commenting out makes restoration go missing

Emergency-blocking by commenting code out tends to lose the restoration. A commented block drops out of the executable paths and reads as intentionally dead code in review, so it never comes back (the text still turns up in grep, but nothing marks it as code that must be restored). An emergency block is better done by:

And one more thing — the condition gate and the trigger are separate axes. “The gate flag is saved” and “the flag is read to start the action” are different verification points. Test only the gate’s storage and you’ll miss the trigger-side regression.

Lesson 2 — the double-trigger race (an await defeats the lock)

The two listeners we revived hid one more trap. Both check the same global lock (autoSyncRunning), but the problem was that there was an await between the check and the lock set (act). If the two events (app resume + Wi-Fi reconnect) fire nearly together, both pass the if (autoSyncRunning) return guard, then much later each sets the lock → a race that starts the same sync twice.

// BEFORE (race): the lock is set only after the dynamic import + store checks
Network.addListener('networkStatusChange', async (status) => {
  if (!status.connected || status.connectionType !== 'wifi') return;
  if (autoSyncRunning || syncCancelled) return;          // ← check
  const { useSettingsStore } = await import('~/stores/settings'); // ← await (gap!)
  // … store checks …
  autoSyncRunning = true;                                // ← act (too late)
  await startFullSync({ wifiOnly: true });
});

// AFTER (fixed): claim the lock right after the predicate, before any await; gate checks go inside try → release in finally
Network.addListener('networkStatusChange', async (status) => {
  if (!status.connected || status.connectionType !== 'wifi') return;
  if (autoSyncRunning || syncCancelled) return;
  autoSyncRunning = true;                                // ← claim synchronously, before any await
  try {
    const { useSettingsStore } = await import('~/stores/settings');
    const { useAuthStore } = await import('~/stores/auth');
    const settingsStore = useSettingsStore();
    const authStore = useAuthStore();
    if (!authStore.isLoggedIn || !settingsStore.gallerySyncEnabled) return;
    if (settingsStore.syncInProgress) return;
    await startFullSync({ wifiOnly: true });
  } catch (e) {
    console.error('[GallerySync] Wi-Fi auto sync failed:', e);
  } finally {
    autoSyncRunning = false;                             // released here even on early return
  }
});

The key point is that a race happens even though JS is single-threaded. Another callback can interleave at every await boundary, so if there’s an await between the two steps of “check-then-act,” that lock is useless. Claim the lock synchronously, right after a cheap predicate and before the first await; put the gate checks inside try so an early return still releases in finally.

What’s next


Authorship & citation: This post was written by Ascendy Engineering and may be re-cited with attribution. If you find an error, please let us know via a GitHub issue.


Tags: capacitor, concurrency, race-condition, incident-prevention, vue