Skip to content

An interrupted first launch leaves a library with no samples, for good #102

Description

@vmillet-dev

Seeding the four sample notes is six writes, each its own round trip and its own SQLite transaction:

spaces.create()      → one command, one transaction
write(SEEDED_KEY)    → preferences
notes.create() × 4   → four commands, four transactions

The process can die between any two of them, and SampleNotesService.seedIfFirstRun is then shut for good. Its two guards are the marker and "no space at all" — and an interruption after the first write leaves a space standing, so the second guard fires on every launch that follows. The marker is already down too. Both locks close on a library that has an empty canvas and no way back to one that is not.

The marker is written before the notes on purpose, and the comment says why: "a failure halfway through leaves an incomplete set rather than a second full one". That is the right call given six separate writes. It is a choice about which bad outcome to have, not a way of not having one.

Not theoretical

The end-to-end harness reproduced it, on both CI platforms. WebDriver's per-file browser.refresh() landed inside the seeding window on a slow runner, the reloaded front end read the marker and skipped the seeding it had just interrupted, and the corpus stayed without samples for the rest of the run — a space, no notes. 01-first-launch then asserted against an empty canvas. Fixed on the harness side in #98 by not refreshing ahead of the first file, which leaves the application's own fragility untouched.

What it is worth

Low. What is lost is the first-launch demonstration corpus, never anything the user wrote. The window is a few hundred milliseconds, on the first launch of a given install, and someone has to kill the application inside it. The cost is a bad first impression, not data.

The better argument is the second one: non-atomic seeding is a trap that has already cost half an hour of CI archaeology, and will cost it again to whoever meets it without this context.

Shape

One command that takes the drafts and writes them in a single transactionseed_samples(space_name, drafts) -> Space, or near enough. transfer::bundle::merge is the precedent: it wraps a whole import in connection.transaction(...) for exactly this reason, and its comment says a per-note transaction "left half an import behind".

⚠️ The obvious version of this — move the seeding into Rust — is the wrong one. The sample text is translated, and arrives in the language the application started in. Rust would need a translation table, which this codebase refuses in three other places (tray labels, error codes, note titles). So the strings stay on the front end and travel as NoteDrafts; only the atomicity comes from the back.

With the writes atomic, the marker can move to after the seeding: "it is seeded" becomes something observed rather than hoped for. Both guards stay — they guard different things.

Two cheaper ideas, and why they are worse

  • Write the marker last, without a transaction. It looks sufficient and is not: at the interruption, the next launch finds the marker missing but the space present, so the second guard fires, writes 'skipped', and the samples never come. Strictly worse than today.
  • Re-seed whatever is missing. It needs identity for the samples, and they are deliberately ordinary notes the user may rename or throw away. There is nothing to match on.

Done when

Killing the application at any point during its first launch leaves either the four samples or a database that will seed them at the next start — never a space with nothing in it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions