Repository navigation
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
RyeMutt
force-pushed
the
rye/aldb
branch
2 times, most recently
from
October 28, 2026 01:22
e01eefd to
3359770
Compare
A pool is closed by joining its threads, and destroying one closes it, so a pool whose thread is stuck -- in a write to a disk that does not answer -- holds whoever closes it for as long as the disk does. The settings files' writer is to run on a pool of one, and the plan for it has a close that waits two seconds and then leaves a stuck writer to finish on its own; nothing in a pool allowed that. ThreadPoolBase::abandon() takes the pool from its owner, in place of destroying it. It closes the queue and stops listening for the viewer's shutdown, there on the owner's thread; then, if every thread has finished, it joins them and destroys the pool, and otherwise lets them go, and the last of them to finish destroys the pool. close() joins only while the queue is open, so abandon() joins finished threads itself. The count of a pool's running threads, and the pool abandon() left to them, are kept in a state the threads share with the pool and keep once it is gone, because a pool's own run() may destroy it: the Win32 window's thread deletes its pool as it ends, once wakeAndDestroy() has let it go. A thread touches nothing of the pool after run() returns but what abandon() left it to destroy. A thread is counted before it starts, and not counted if it fails to. A pool destroyed on its own thread must not reach the event pumps, which are the main thread's: the destructor now stops listening only if the pool listened, which only one that shuts down with the viewer does. threadpool_test gains six cases beside the scheduler's: a pool of three stuck threads let go at once and destroyed by the last of them once released; one never started, destroyed at once; one whose queue's work still runs after it is let go; one listening for the shutdown that stops; a pool its own run() destroys, kept rather than freed, whose bytes nothing writes after; and a thread woken by a ping in under half a millisecond at the median, which the scheduler's own cases, of idle workers and timed waits, leave out. The wake is timed only where no sanitizer slows the run: under AddressSanitizer, beside the other tests on a CI runner, one took 3 ms. The first three fail with the last thread's deletion taken out, the fourth with the listener left; the fifth crashed before the shared state, and fails on any write to the pool after run(); the sixth took 15.5 ms under the scheduler that slept a tick at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's settings are to move from XML written whole at exit onto a SQLite file, and the cache's index and the chat logs onto files of their own after them. aldb is what they share. Nothing links it yet. ALDatabase is one file. Its writes are jobs posted to a thread of the file's own, which writes whatever has queued in one BEGIN IMMEDIATE transaction, each job in a savepoint, so that a job that fails, or throws, is dropped alone and the rest commit. A failure that may pass (busy, locked, the disk full, an I/O error) stops the batch and tries it again, waiting from 100 ms up to 5 s; it is said once, through the log and the owner's onFailure, until a write next succeeds. Where such a failure makes SQLite undo the whole transaction, not the job alone, the jobs that ran before it are tried again with it, not dropped, and none runs on outside a transaction. A job may be posted with a completion, told on the writer thread whether it committed. flush() waits for what was posted for closeWait at most, and says whether it was written. Close writes what is queued and checkpoints the WAL into the file, and gives up after closeWait rather than hang an exit on a disk that does not answer. Reads take a connection from a pool of read-only ones. A file is kept by what losing its last writes would cost: Durable syncs every commit (and fullfsyncs on macOS), Journal survives the process dying, Regenerable is not synced and is mapped into memory. Each is a WAL file, or a DELETE-journal one where the file system has no shared memory to give the WAL its index. Its tables are a list of migrations by user_version, run in one transaction, a VACUUM INTO copy of the file kept before each and the last two copies kept; the version is read again under the transaction's lock, so that two viewers opening a new file at once make its tables once, and a file newer than the viewer is opened and left alone. Its application_id names what kind of file it is, and a file that carries another is refused, not written. A file this process may only read -- marked read-only, owned by another user, on read-only media -- is opened for reading, and the open says so: its reads run, and its writes are refused, said once. After a crash the owner asks for the file to be checked. The check waits for a file another viewer is writing, as every read does, and takes for damage only what says it is damaged, not a check that could not run. A damaged file is moved aside with its WAL, under a .corrupt-<time> name, and what it held salvaged into a new file with SQLite's recover API; a Regenerable file is deleted and made again instead. A file that cannot be used at all, held by another process or on a path that will not open, leaves the database running in memory for the session, and says so. Damage found while writing, which the open looks for only after a crash, is said once for as long as the file is open. ALDbConnection caches its statements by their SQL, a single statement exec() runs among them, so that BEGIN, COMMIT and each job's savepoint are prepared once. It gives a second borrower of one in use a statement of its own, keeps the first failure of a sequence of calls, and binds and reads an LLUUID as its 16 bytes. Its Foreign access is for files that come from someone else -- a backup or a shared preset -- read with SQLite's defensive mode, no triggers or views, untrusted schemas, limits on every value's size and the query's depth, no memory mapping, read-only, and an integrity check before anything is read. aldb_tests: 38 cases, among them the profiles' pragmas, a thousand jobs in one transaction, transaction and savepoint statements prepared once, a busy file waited out, a full disk said once, a hung disk closed in bounded time and a flush that gives up on one, two processes writing one file, a batch SQLite undoes whole, a read-only file, two viewers migrating one file at once, a completion for each job, migrations and their copies, a check that finds the file busy, salvage of a damaged index and of rows only in the WAL, and a foreign file's trigger, view and oversized value. A VFS over the platform's fails or holds writes, finds the file busy, or offers no shared memory, on request. Each case written for a fault was seen to fail before its fix. Passing on Windows, and in CI on Linux x64 and arm64 and under ASan; macOS has not run them yet. sqlite3 comes from alchemy-registry, whose port builds the recover API into the library, at the registry's new baseline; the attribution table gives it a holder line, as its copyright file only says it is public domain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
aldb, the SQLite layer the viewer's settings will move onto, and later the cache's index and the chat logs. Nothing links it yet. Also addsLL::ThreadPoolBase::abandon(), which aldb's writer needs. Built ondevelopafter #596, whose pool scheduler it keeps. Draft until macOS and Linux pass this push's CI.Commits
llcommon). Until now a pool was closed by joining its threads, so a pool whose thread is stuck held whoever closed it.abandon()takes the pool from its owner instead of destroying it:abandon()left them, live in a state the threads share and keep once the pool is gone, since a pool's ownrun()may destroy it (the Win32 window's thread does). A thread touches nothing of the pool afterrun()returns.indra/aldb, the sqlite3 port, wiring, an entry indoc/ARCHITECTURE.md).What aldb does
One writer thread per file, an
LL::ThreadPoolof one: named for the OS and Tracy, not closed by the viewer's quitting.BEGIN IMMEDIATEtransaction, each in a savepoint of its own, so a job that fails or throws is dropped alone.flush()waits up tocloseWaitand says whether everything was written.closeWaitit abandons the pool to a writer stuck on a hung disk rather than hang an exit.Coroutine-safe: the queue, the reader pool and the waits use the coroutines' own locks and promises (
LLCoros::Mutex,ConditionVariable,Promise). A coroutine waiting inpostAndWait,flushor a backup gives its thread's other coroutines their turn, and a write from a coroutine never tripsLLMutex's main-coroutine assert.A pool of read-only connections for reads.
Profiles:
Durablesyncs every commit (fullfsyncon macOS).Journalsurvives a crash.Regenerableis unsynced and memory-mapped.All are WAL, falling back to a DELETE journal where there is no shared memory.
Migrations by
user_versionrun in one transaction, with aVACUUM INTOcopy kept before each (the last two are kept). The version is read again under the transaction's lock, so two viewers opening a new file at once make its tables once. A file written by a newer viewer is opened and left alone, and a file whoseapplication_idnames another kind of file is refused.Read-only files: a file this process may only read (marked read-only, owned by another user, on read-only media) is opened for reading, and
ALDbOpenResult::readOnlysays so. Its reads run; its writes are refused, reported once.Recovery: after a crash the file is checked.
Regenerableone is rebuilt instead.ALDbConnection:exec()runs is cached too, soBEGIN,COMMITand each job's savepoint are prepared once. A script still goes throughsqlite3_exec.LLUUIDas 16 bytes.Foreignaccess is for imported backups and presets. It opens them read-only in defensive mode, with triggers and views disabled, untrusted schemas, no memory mapping, limits on value size and query depth, and an integrity check before anything is read.Since the first push
run()returned, whenrun()may have destroyed it. The shared state above fixes it.user_versionwas read outside the migration's lock, so two viewers could both migrate a new file.exec()of a single statement is now cached.Foreigndefences are read back from SQLite in their test, and the tests' temporary directory says why it could not be made.std::future::get(), which one test had. The thread pool's wake check is no longer timed under AddressSanitizer or ThreadSanitizer, where a busy runner took 3 ms.developafter Pool workers sleep until woken, and UUIDs are random v4 without a lock #596. Pool workers sleep until woken, and UUIDs are random v4 without a lock #596 replaced the Windows pool scheduler that slept a timer tick at a time, which this branch had fixed too. Its scheduler is kept and this branch's is gone. The twothreadpool_test.cppfiles are merged.Dependencies
sqlite33.54.1 comes from alchemy-registry. Its port builds the recover API into the library and sets the build options insqlite3-vcpkg-config.h:The registry baseline moves to f516d94, which adds sqlite3 alone.
attribution.jsongives sqlite3 a holder line, since its copyright file only says it is public domain.Testing
Windows RelWithDebInfo, at this push: all 75
llcommonandaldbctest entries pass, includingllapp_test, which shuts the viewer's pools down.aldb_tests: 38 cases. They include:Each case written for a fault was seen to fail before its fix.
threadpool_test: 9 cases, Pool workers sleep until woken, and UUIDs are random v4 without a lock #596's three and this branch's six:run()destroys, whose memory nothing writes afterwards;The first three fail with the last thread's deletion taken out, the fourth with the listener left, and the fifth crashed before the shared state.
GCC 15.3: every source this PR changes compiles with
-Werrorand the project's warning flags.CI at the previous push (before the rebase): every job passed but ASan, whose only failure was the wake timing above.
Owed: this push's CI.
Left for later
Before the cache index or the chat logs use it, aldb grows what a second and third consumer need and settings does not:
🤖 Generated with Claude Code