Skip to content

A failed DB persist makes the app permanently unquittable, silently — no error handler, no timeout, dead retry path #1133

Description

@Lcstyle

What happened

If frontend.db/persist! fails for any reason, Logseq becomes impossible to close for the rest of
the session, with no message to the user and nothing in logs/main.log beyond a single misleading
line.

I hit this via logseq/logseq#8536 (see my reproduction in logseq/logseq#8536 (comment)) (a cljs-bean Bean in the DB, triggered by a plugin creating a page with
properties) and traced the quit path end to end. The trigger is incidental — the quit machinery has
three independent flaws that turn any persist failure into a hard hang.

My instance sat unquittable for 2 days 16 hours. Nothing was deadlocked or spinning: every thread
was sleeping, and the renderer answered executeJavaScript in 2 ms. The app was simply waiting
forever on a message that had already been sent — to a handler that doesn't exist.

1. :persistent-dbs-error has no handler

electron.listener/persist-dbs! wires the failure path:

:on-error (fn [] (ipc/ipc "persistent-dbs-error"))

In the shipped electron.js, persistent-dbs-saved appears twice; persistent-dbs-error appears
zero times.
It falls through to the handle multimethod's :default, which only logs — and logs
the window object, not the channel name, so the log line doesn't even identify what failed:

[2026-07-31 17:21:53.479] [error] Error: no ipc handler for: {
  setBounds: '[function] …',
  _events: { … 200 lines of BrowserWindow dump … }
}

That single line was the only trace of a 2.5-day outage.

2. The close handshake has no timeout

electron.window/close-handler:

(defn close-handler [win close-watcher-f e]
  (.preventDefault e)                     ;; window will not close
  (.send web-contents "persistent-dbs")   ;; ask renderer to save
  (go (<! chan)                           ;; parks forever if the reply never comes
      (.destroy win)))

An unbounded <! on a channel fed only by the success path. Suggested: alt! with a
(timeout 10000) that destroys the window anyway and warns the user their DB cache wasn't saved.
The markdown files are already on disk, so this is safe — the .transit is only a cache.

3. The retry path is dead after the first attempt

The close listener registered in electron/core.cljs ends with (reset! *win* nil), and the
close-handler call is guarded by (when window_ ...) where window_ is @*win*. So:

  • 1st close: preventDefaultclose-handler → parks → *win* set to nil
  • 2nd and every later close: preventDefaultwindow_ is nilnothing happens at all

Verified on the stuck instance by replaying the close through the Electron main-process inspector:

close event fired : 1
defaultPrevented  : true
6 s later         : windowCount 1, destroyed false
renderer errors   : []          <- persist-dbs! was never even invoked
new log lines     : none

So the user clicks X, Quit, Ctrl+Q — and the application silently discards every one of them. On
macOS there's at least a transient "syncing internal status" toast; on Linux there is no feedback
whatsoever.

Impact

  • Application cannot be closed; force-kill is the only exit.
  • The DB cache silently stops being written. Mine last persisted Jul 25; I discovered this on
    Aug 2. Every launch in between was re-indexing more of the graph from markdown.
  • No user-visible error, and effectively no log trail.
  • Because the failure is invisible, users attribute it to sleep/hibernation/git/sync — see the 23
    comments on Unable to close Logseq, stuck at Syncing internal status logseq#8536 across 3.5 years, none of which found the trigger.

Suggested fix

  1. Register a :persistent-dbs-error handler that puts a distinguishable value on the same channel,
    so the go-block resumes and the window closes.
  2. Surface the failure to the user (dialog or notification), not just console.error in devtools.
  3. Add a timeout to the <! in close-handler — a slow or failed cache write must never make the
    app unquittable.
  4. Make the :default ipc handler log the channel name. Today it dumps the BrowserWindow, which
    actively hides which message was unhandled.
  5. Don't (reset! *win* nil) before the close actually succeeds, so retrying is meaningful.

Environment

Logseq 0.10.15 (Flathub com.logseq.Logseq), Electron 38.4.0, Node 22.20.0,
Fedora Linux 7.1.5, Wayland/COSMIC. File-based graph, 5228 pages.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions