pub struct Workspace { /* private fields */ }Expand description
Many databases in one directory, opened on demand and kept open for a while.
The pool is a flat Vec rather than a map: max_open is a handful, and a
linear scan of a handful is cheaper than hashing — the same reason the
engine’s own structures stay flat.
A returned Database outlives its pool entry. The handle is an Arc,
so eviction drops the pool’s copy and nothing else; the file lock is
released when the last clone goes. A caller that parks a handle for hours
keeps the database locked for hours, whatever the idle timeout says. Hold
one for the length of a request, as the MCP server does, and this never
comes up.
§What the pool lock covers
One Mutex guards the pool, and every path mutates it — a hit writes
last_used_ms, so an RwLock would buy nothing and cost more. It is not
the engine lock: a handle is cloned out and the verb runs with the pool
released, so two threads working in two databases never meet here.
Two things are worth knowing about how long it is held.
Workspace::get holds it across the open — creating the file,
taking the lock, mapping the snapshot, replaying the journal. That is
deliberate (see the comment in the body: dropping it first lets two threads
of one process race for a file and hand the loser a Busy it caused
itself), and the cost is that a cold open of one database queues a hit on
an unrelated one. With a pool that warms in a few requests and a max_open
in the tens, that queue is short.
The closing paths do the opposite: they take the evicted entries out under
the lock and drop them after releasing it. Today that only defers
closing a few file descriptors, since nothing in the handle has a Drop.
It is written that way so it stays true if one ever gains one — a
checkpoint-on-close would otherwise turn a timer tick into disk I/O under a
lock every worker wants, which is a stall with no visible cause.
Implementations§
Source§impl Workspace
impl Workspace
Sourcepub fn describe(
&self,
name: &DbName,
now_ms: u64,
desc: Description<'_>,
) -> Result<(), WorkspaceError>
pub fn describe( &self, name: &DbName, now_ms: u64, desc: Description<'_>, ) -> Result<(), WorkspaceError>
Records what name is for, in the database itself and in the registry.
Creates the database if it does not exist — describing a database into being is a reasonable thing to want, and the alternative is a two-step dance where the first step is forgettable.
Called again for the same database, this revises rather than duplicating: facts are immutable here, so a change is a new revision and the history of what this database used to be for is kept for free. The revision has a new fact id, which is exactly why a database’s identity is its name and never a fact id.
§Errors
Whatever opening or writing either database reports.
Sourcepub fn archive(
&self,
name: &DbName,
now_ms: u64,
) -> Result<bool, WorkspaceError>
pub fn archive( &self, name: &DbName, now_ms: u64, ) -> Result<bool, WorkspaceError>
Marks name archived, keeping its description. Returns whether anything
changed (false when it was already archived).
Archiving does not close, move or delete the database — it is a label, and the caller decides what it means. Deleting is deleting a file, and this crate does not do that on a caller’s behalf.
§Errors
WorkspaceError::NoSuchDatabase when there is no record to archive,
plus whatever writing the registry reports.
Sourcepub fn entry(&self, name: &DbName) -> Result<Option<DbEntry>, WorkspaceError>
pub fn entry(&self, name: &DbName) -> Result<Option<DbEntry>, WorkspaceError>
The registry’s record for name, or None if it has none.
§Errors
Whatever opening or reading the registry reports.
Sourcepub fn entries(&self) -> Result<Vec<DbEntry>, WorkspaceError>
pub fn entries(&self) -> Result<Vec<DbEntry>, WorkspaceError>
Every record in the registry, sorted by name.
A full dump rather than a query: the registry holds one fact per
database, so this is cheap at the scale where listing is what a caller
wants. Past that scale they want Workspace::find.
§Errors
Whatever opening the registry reports.
Sourcepub fn find(
&self,
query: &str,
k: usize,
now_ms: u64,
) -> Result<Vec<DbEntry>, WorkspaceError>
pub fn find( &self, query: &str, k: usize, now_ms: u64, ) -> Result<Vec<DbEntry>, WorkspaceError>
The databases whose descriptions best match query, best first.
This is the answer to “I do not know the name”: ask in words, get names back, then work with the name. Results are ranked by the same fused recall every other search uses — one database’s worth of scoring, so the ranking means something (scores from different databases would not be comparable, which is why nothing here ever merges across them).
The query doubles as a graph anchor, so a person’s name finds what they
own even though an owner is an edge and edges are not text. “Ann” reaches
the Ann entity, the walk crosses owned-by in either direction, and the
records on the other side come back. Nothing special-cases owners: it is
the lexical and graph sources doing what they already do, fused.
§Errors
Whatever opening or querying the registry reports.
Sourcepub fn reindex(&self, now_ms: u64) -> Result<ReindexReport, WorkspaceError>
pub fn reindex(&self, now_ms: u64) -> Result<ReindexReport, WorkspaceError>
Rebuilds the registry from the databases themselves.
The repair path, and the reason the registry is allowed to be a cache. It reads each database’s own description and writes it back into the registry, so a registry that was deleted, corrupted or edited by hand comes back from the data.
A database held open by another process cannot be read here — one
file has one writer — so it is named in the report rather than skipped
silently. That is a real limit of rebuilding a live workspace, and the
normal path (describe keeping the registry current) does not have it.
§Errors
Whatever listing the directory or writing the registry reports. A single unreadable database is reported, not raised.
Sourcepub fn verify(&self, now_ms: u64) -> Result<Vec<WorkspaceIssue>, WorkspaceError>
pub fn verify(&self, now_ms: u64) -> Result<Vec<WorkspaceIssue>, WorkspaceError>
Checks the registry against the directory, reporting every disagreement.
Fixes nothing: see WorkspaceIssue.
§Errors
Whatever listing the directory or opening the registry reports.
Source§impl Workspace
impl Workspace
Sourcepub fn new(
layout: WorkspaceLayout,
open: Opener,
limits: WorkspaceLimits,
) -> Self
pub fn new( layout: WorkspaceLayout, open: Opener, limits: WorkspaceLimits, ) -> Self
A workspace over layout, opening databases with open.
Sourcepub fn registry(&self) -> Result<Database, WorkspaceError>
pub fn registry(&self) -> Result<Database, WorkspaceError>
The registry database, opened on first use.
Lazily, and that matters: a process that only ever resolves names it was given never opens the registry, so it neither creates the file nor holds a lock on it. The registry is a search index — a caller that is not searching should not pay for it, and two processes that never search can share one workspace without contending over it.
It lives outside the handle pool because it is not one of the databases:
it has no DbName, it is never evicted, and it is never handed out by
Workspace::get.
§Errors
WorkspaceError::Io if the root cannot be created, or whatever the
open reports — including HostError::Locked if another process holds
the registry.
Sourcepub fn close_registry(&self) -> bool
pub fn close_registry(&self) -> bool
Closes the registry handle, if one is open. Returns whether there was
one. The same liveness concern as Workspace::close_idle: a held
registry is a registry no other process can write.
Sourcepub fn layout(&self) -> &WorkspaceLayout
pub fn layout(&self) -> &WorkspaceLayout
Where the files are.
Sourcepub fn limits(&self) -> WorkspaceLimits
pub fn limits(&self) -> WorkspaceLimits
The limits in force.
Sourcepub fn open_count(&self) -> usize
pub fn open_count(&self) -> usize
How many databases are open right now. Observability for tests and
stats; not a number to make decisions on, since it moves.
Sourcepub fn get(
&self,
name: &DbName,
now_ms: u64,
missing: IfMissing,
) -> Result<Database, WorkspaceError>
pub fn get( &self, name: &DbName, now_ms: u64, missing: IfMissing, ) -> Result<Database, WorkspaceError>
Resolves name to an open database, opening it if it is not pooled.
now_ms is the host clock (unix milliseconds), used only for the idle
bookkeeping — it is passed in rather than read here for the same reason
every verb takes now: the host owns time.
§Errors
WorkspaceError::NoSuchDatabase when the file is absent and missing
is IfMissing::Fail; WorkspaceError::Busy when another process
holds the writer; WorkspaceError::Io if the directory cannot be
created; WorkspaceError::Host for anything the open itself rejects.
Sourcepub fn close_idle(&self, now_ms: u64) -> usize
pub fn close_idle(&self, now_ms: u64) -> usize
Closes every database unused for longer than the idle timeout, returning
how many were closed. A no-op when the timeout is 0.
Call it on a timer. Nothing else releases a file lock a server is holding on a database nobody is asking about.