Skip to main content

Workspace

Struct Workspace 

Source
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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

Source

pub fn new( layout: WorkspaceLayout, open: Opener, limits: WorkspaceLimits, ) -> Self

A workspace over layout, opening databases with open.

Source

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.

Source

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.

Source

pub fn layout(&self) -> &WorkspaceLayout

Where the files are.

Source

pub fn limits(&self) -> WorkspaceLimits

The limits in force.

Source

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.

Source

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.

Source

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.

Source

pub fn close_all(&self) -> usize

Closes every open handle. The pool’s copies, that is — see the note on Workspace about clones the caller still holds.

Trait Implementations§

Source§

impl Debug for Workspace

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.