mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 09:56:47 +08:00
* fix(lark): serve bind-session status from Postgres so any backend replica can answer (MUL-7340)
The device-flow install session lived only in RegistrationService's
in-process map, so GET /lark/install/{session_id}/status could be
answered by exactly one backend process — the one that served begin. Any
other replica, or the same one after a restart, returned 404, which the
dialog renders as "安装会话已失效或丢失,请重新扫码" (install session not
found). The browser's first status poll fires ~5s after the QR appears,
which is exactly when users saw the QR die.
Persist the status projection in a new lark_install_session table and
read it there. The device_code is deliberately NOT persisted: it is a
bearer credential and only the process running the polling goroutine
needs it, so that process keeps its working state in memory and the row
carries just what the browser reads.
A row left 'pending' past expires_at — the owning process died mid-flow —
is now reported as expired from the timestamp rather than polling
forever. Both terminal writes are guarded on status = 'pending', so a
racing expiry and poll result resolve first-writer-wins in SQL instead of
under a mutex.
Workspace teardown drops the table's rows alongside channel_installation.
Co-authored-by: multica-agent <[email protected]>
* refactor(lark): move bind-session state from a new SQL table to the shared Redis store
Review feedback: the transient scan state does not need its own table.
The repo already has a home for exactly this shape — a short-lived,
cross-replica request whose status a browser polls — in the runtime
update / model-list / local-skill stores: Redis when configured, an
in-process implementation for single-instance development.
Replaces the lark_install_session table, its sqlc queries, the two
migrations and the sweep with an InstallSessionStore interface plus a
Redis and a memory implementation, and drops the workspace-teardown
entry the table needed. PostgreSQL keeps what it should own: the
committed Bot installation and the installer binding.
First-writer-wins now falls out of SetNX rather than an UPDATE guarded on
status = 'pending'. The record splits across an immutable key written by
Create and a terminal key written once, so a racing expiry and poll
result resolve without a read-modify-write.
Retention is the store's TTL, sized per session as the QR window plus
SessionTTL so the dialog can still read a terminal outcome recorded at
the last second — not a fixed constant that would drift from Lark's
expires_in.
A Redis error surfaces as an error, never as not-found: not-found is
terminal in the dialog, so a blip must not permanently kill a live scan.
Without Redis the service keeps the in-process store and the router logs
a warning, because that configuration reproduces MUL-7340 on more than
one replica and should not be chosen silently.
Co-authored-by: multica-agent <[email protected]>
* fix(lark): make the bind-session terminal write atomic and retried
Two failures found in review of the Redis store.
1. A terminal write that failed was logged and dropped. By the time
markSuccess runs, the installation and the installer binding are
already committed in Postgres, so discarding that write left a user
who had successfully bound watching the dialog sit on 'pending' until
the QR expired and then report failure. The write is now retried with
backoff — 8 attempts over roughly a minute, enough to ride out a Redis
failover rather than just a dropped packet — and the session is held
until it settles. A store that reports the session gone stops the loop
immediately, since no retry can fix that.
2. The record spanned two keys whose TTLs were set by separate commands.
A SETNX that landed followed by a failed EXPIRE left the terminal
status under the base key's original, much longer retention, and the
retry could not repair it because SETNX then reported that someone
else had won. A session that finished early lost its terminal field
first and read as 'pending' again.
The record is now a single hash written by Lua, so status and
retention move together or not at all. The terminal script writes the
status only when none is set — the same first-writer-wins guarantee —
but runs EXPIRE unconditionally, which is what makes a retry
self-repairing rather than a no-op. Create is scripted too, so a
failure can no longer leave a key with no TTL behind.
Also drops a stale comment referencing the removed SQL file.
Co-authored-by: multica-agent <[email protected]>
* docs(lark): correct the retry budget and the atomicity claim
Re-review nits on #8376, both accuracy rather than behaviour.
The retry budget did not add up to the minute the comment claimed: 8
attempts is 7 waits totalling 25.4s, and the 15s cap was never reached.
The intent — ride out a Redis failover, not just a dropped packet — was
the right one, so raise it to 10 attempts (9 waits, 55.4s, cap now in
play) and state the arithmetic plus the per-attempt timeout on top. Also
say plainly that this is a bounded recovery window, not a guarantee.
The store comment implied Lua atomicity made the terminal write
all-or-nothing. Redis does not roll back a script that fails partway
through, so atomicity buys isolation from other clients, not recovery.
What actually removes the original two-key bug is one key with one
expiry, plus a retry that keeps the first status and re-runs EXPIRE
unconditionally. Comments now say that.
The test claiming a 'mid-script' failure injected before execution.
Renamed to what it covers, and added the case that was missing: the
script runs on the server, the client loses the reply, and the replay
must keep the first outcome AND re-assert retention.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>