Skip to content

fix(storage): make session, peer cache and config files safe to share between processes - #286

Open
ApexAiOfficial wants to merge 5 commits into
gotd:mainfrom
ApexAiOfficial:up/atomic-storage
Open

ApexAiOfficial wants to merge 5 commits into
gotd:mainfrom
ApexAiOfficial:up/atomic-storage

Conversation

@ApexAiOfficial

@ApexAiOfficial ApexAiOfficial commented Oct 18, 2026 •

Copy link
Copy Markdown

Several tg processes routinely share one session directory: parallel commands, or two agents driving the same account. This makes the files they share safe for that.

Torn reads. gotd's session.FileStorage, the peer cache and the config truncated files in place, so a concurrent reader could load an empty or torn session or cache and fail. New internal/atomicfile: write a temp file in the same directory, fsync, rename over the target, fsync the directory. The cache skips the fsyncs, since it can be rebuilt. The file session store keeps gotd's on-disk format and path.

Lost updates. Atomic replacement alone doesn't stop two processes that both read, merge and replace the peer cache from dropping each other's entries: an overlapping-writers test (8 writers × 25 saves) lost 18–64 of 200. New internal/filelock takes an advisory exclusive lock on a sidecar <file>.lock (flock on Unix, LockFileEx on Windows, no-op where tg is single-process; released by the OS if the holder dies). The cache holds it across the whole read-merge-write cycle, and a corrupt cache is rebuilt instead of failing every command.

Stale session keys. gotd re-saves the session on every reconnect to the primary DC, writing its own auth key into the file. A long-running process (e.g. tg watch) that reconnected after another process ran tg login or tg logout therefore wrote its old key back, undoing the login or resurrecting the logged-out session. The store remembers the key it loaded and, under the lock, skips writing that key over a session someone else re-keyed or removed. Same-key writers stay last-writer-wins, which is safe because their copies differ only in volatile state such as the server salt.

Config creation. writeConfig stat'ed and then wrote, which raced. A crash mid-write also left a partial config that a retried tg init refused to replace. atomicfile.Create writes and syncs a temp file and hard-links it into place: atomic, and it fails if the target exists. It falls back to an exclusive synced create on filesystems without hard links.

Tests cover atomic replace and create, permissions, symlinked targets, lock exclusivity, overlapping cache writers, corrupt-cache recovery, and the session cases: re-login elsewhere, logout elsewhere, same-key writers, and this process re-keying. go test -race and golangci-lint v2.12.1 (Go 1.25) pass.

🤖 Generated with Claude Code

ApexAiOfficial and others added 5 commits October 18, 2026 08:46
Several tg processes routinely share one session directory (parallel agent
commands, or two agents driving the same account). gotd's FileStorage and the
peer cache truncated files in place, so a concurrent reader could load an
empty or torn session or cache and fail.

- internal/atomicfile: write to a temp file in the same directory, then
  rename over the target (fsync for sessions/config; no fsync for the cache).
- File session store keeps gotd's on-disk format and path.
- Peer cache merges entries other processes saved since it was loaded, so
  concurrent commands no longer drop each other's access hashes, and treats a
  corrupt cache as empty instead of failing every command.
- writeConfig refuses to overwrite with O_EXCL instead of stat-then-write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Atomic replacement stopped torn reads, but two tg processes that both read,
merged and replaced the cache still lost each other's entries: an
overlapping-writers test (8 writers x 25 saves) lost 18-64 of 200 entries.

internal/filelock takes an advisory exclusive lock on a sidecar <file>.lock
(flock on Unix, LockFileEx on Windows, no-op where single-process), released
by the OS if the holder dies. The peer cache holds it for the whole
read-merge-write cycle; the regression test now keeps every entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gotd re-saves the session on every (re)connect to the primary DC, re-reading
the file and writing its own auth key into it. A long-running process (e.g.
tg watch) that reconnected after another process ran tg login or tg logout
therefore wrote its old key back, undoing the login or resurrecting the
logged-out session.

The file store now remembers the auth key it loaded and, under a lock on
<file>.lock, skips a write of that key when the file was re-keyed or removed
by someone else. Same-key writers remain last-writer-wins, which is safe: the
copies differ only in volatile state such as the server salt. The guarantees
are documented on fileSessionStore, and tests cover re-login, logout,
same-key writers and this process re-keying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- writeConfig created the final file with O_EXCL and then wrote into it, so a
  crash mid-write left a partial config that a retried tg init refused to
  replace. atomicfile.Create writes and syncs a temp file and hard-links it
  into place (atomic, fails if the target exists), falling back to an
  exclusive synced create on filesystems without hard links.
- Durable writes now also fsync the parent directory, so a completed rename
  survives power loss (skipped on Windows, which cannot sync a directory).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows cannot sync a directory handle; use a build-tagged no-op there
instead of a runtime GOOS check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ApexAiOfficial ApexAiOfficial changed the title fix(storage): replace session, peer cache and config files atomically fix(storage): make session, peer cache and config files safe to share between processes Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant