pub struct WorkspaceManager { /* private fields */ }Expand description
Owns every loaded workspace plus the admission-accounting state.
Construction spawns the retention reaper task (§G.3). The handle is
stored so Drop can abort it cleanly — on daemon shutdown the
reaper is aborted, then the admission state drops, dropping every
retained Arc<CodeGraph> in one pass. No accounting leak, no
dangling Arc.
Implementations§
Source§impl WorkspaceManager
impl WorkspaceManager
Sourcepub fn new(config: Arc<DaemonConfig>) -> Arc<Self> ⓘ
pub fn new(config: Arc<DaemonConfig>) -> Arc<Self> ⓘ
Construct a fresh manager and spawn the retention reaper.
The reaper is spawned on the current Tokio runtime. Callers
must therefore construct the manager from a Tokio context
(#[tokio::main], an async block driven by Runtime::block_on,
etc.). Tests that don’t need the reaper can use
Self::new_without_reaper.
Sourcepub fn set_hook(&self, hook: SharedHook)
pub fn set_hook(&self, hook: SharedHook)
Install a post-publish hook. Task 9’s daemon binary calls
this once at startup after constructing the shared
QueryDb; unit tests call it to install a recording hook.
The old hook is dropped immediately; no retention semantics
apply.
Sourcepub fn memory_limit_bytes(&self) -> u64
pub fn memory_limit_bytes(&self) -> u64
Memory budget in bytes (derived from config.memory_limit_mb).
Sourcepub fn lookup(&self, key: &WorkspaceKey) -> Option<Arc<LoadedWorkspace>>
pub fn lookup(&self, key: &WorkspaceKey) -> Option<Arc<LoadedWorkspace>>
Look up a loaded workspace by key without acquiring rebuild_lane
or admission.
Returns Some(Arc<LoadedWorkspace>) if a workspace is currently
registered under key, or None otherwise. The workspaces
read guard is acquired and released inside the call — callers
never observe it nested with any other lock.
Added for the Task 7 crate::rebuild::RebuildDispatcher which
needs a cheap handle on Arc<LoadedWorkspace> as a precondition
before entering the canonical §J.4 ordered sequence
(rebuild_lane → admission). This is not part of the
ordered sequence itself — the §J.4 contract only constrains
paths that hold more than one lock simultaneously. Here, the
workspaces guard is dropped before the caller takes
rebuild_lane, so there is no nesting.
Shared lookup: returns the Arc<LoadedWorkspace> keyed by
key if present. Used by RebuildDispatcher::handle_changes
(inside the crate) and by external test harnesses (Task 7
Phase 7b1 rebuild_runner_gate.rs) that need to inspect
workspace-level atomics (rebuild_in_flight, rebuild_cancelled)
or the rebuild_lane mutex directly.
This is NOT a JSON-RPC surface — the IPC layer should use
status() for point-in-time workspace state. Direct lookup
access bypasses the LRU touch that status() performs.
Sourcepub fn reap_once(&self)
pub fn reap_once(&self)
Retention reaper: a single pass over retained_old.
Removes entries whose Arc::strong_count has dropped to 1 —
meaning the admission map is the last holder. Emits a
one-shot WARN log line when an entry exceeds
rebuild_drain_timeout_ms without dropping.
This is the only code path that removes tokens from
retained_old. Any other code that mutates the retention
map is a violation of §G.3.
Sourcepub fn reserve_rebuild(
self: &Arc<Self>,
for_key: &WorkspaceKey,
working_set_estimate: u64,
) -> Result<RebuildReservation, DaemonError>
pub fn reserve_rebuild( self: &Arc<Self>, for_key: &WorkspaceKey, working_set_estimate: u64, ) -> Result<RebuildReservation, DaemonError>
Amendment 2 §G.1 two-phase reservation protocol.
Phase 1 (workspaces read + admission read):
project_total + estimate ≤ limit? → commit
otherwise → pick LRU non-pinned
victims (`for_key` is
exempt — a workspace
cannot evict itself)
Phase 2 (no locks held):
for each victim: execute_eviction()
Phase 3 (admission alone):
re-check projected vs limit → authoritative commit
reserved_bytes += estimate → return RebuildReservationLock order is workspaces → admission in Phase 1, nothing in
Phase 2, admission alone in Phase 3. No nesting of
rebuild_lane — Task 7 adds that layer outside this function.
Returns a RebuildReservation RAII guard on success. On
Err, the admission state is exactly pre-call — either no
eviction happened (headroom already available) or the
eviction cleared retained entries but could not fit.
Sourcepub fn get_or_load(
self: &Arc<Self>,
key: &WorkspaceKey,
builder: &dyn WorkspaceBuilder,
working_set_estimate: u64,
) -> Result<Arc<CodeGraph>, DaemonError>
pub fn get_or_load( self: &Arc<Self>, key: &WorkspaceKey, builder: &dyn WorkspaceBuilder, working_set_estimate: u64, ) -> Result<Arc<CodeGraph>, DaemonError>
Load the workspace’s graph, building it via builder if not
already present.
Lifecycle gate:
- Cache-hit fast path — if the workspace is present AND in
WorkspaceState::Loaded, touch + return. - CAS
Unloaded/Evicted/Failed→Loading. Exactly one caller wins. If another caller already holds the gate (Loading/Rebuilding), return an error — Phase 6c / Task 7 will introduce a wait-for-done notify channel. - The winner arms a [
LoadingGuard] RAII wrapper that transitions the workspace intoWorkspaceState::Failedon any non-success exit (Err, earlyreturn, or panic). This covers the Codex iter-1 MAJOR that a panic frombuilder.build()would leave the workspace stuck in Loading. - Reserve admission headroom (§G.1 three-phase).
- Build the graph via the injected
builder. - Re-check
rebuild_cancelled+ workspace map membership before publishing. If eviction ran during the build, the reservation refunds via RAII and no graph is published. - Publish via
publish_and_retain. Disarm the LoadingGuard- record success + touch.
- Release
workspaces_guard, THEN dispatch the post-publishSqrydHook. The hook fires outside every outer manager lock so a hook impl is free to call back intounload/get_or_load/set_hook/statuswithout deadlocking against the loader that fired it.
Codex Task 6 Phase 6b iter-1 MAJOR (×2): the pre-fix version
clobbered a concurrent eviction’s rebuild_cancelled signal
and could publish into a workspace already removed from the
map. The CAS + post-build re-check + LoadingGuard together
close both holes.
Codex Task 6 Phase 6c iter-2 MAJOR: the pre-fix version
dispatched the hook from inside publish_and_retain while
the caller still held workspaces.read(), giving a hook
impl that needed workspaces.write() (e.g. via unload)
a guaranteed re-entrancy deadlock. Splitting publish and
hook dispatch into Steps 7 and 8 closes that hole.
§Errors
DaemonError::MemoryBudgetExceededif Phase 3 cannot admit the reservation even after LRU eviction.DaemonError::WorkspaceBuildFailedsurfaced from the builder OR synthesised when a concurrent eviction races the load (reason = "workspace evicted mid-load").
Sourcepub fn evict_lru(&self) -> Option<WorkspaceKey>
pub fn evict_lru(&self) -> Option<WorkspaceKey>
Evict the least-recently-accessed non-pinned workspace, if
any. Returns the evicted key on success, None if there are
no eligible candidates.
Sourcepub fn unload(&self, key: &WorkspaceKey) -> bool
pub fn unload(&self, key: &WorkspaceKey) -> bool
Explicitly unload a workspace. Drives a full eviction
(releases graph data + admission accounting via
Self::evict_to_tombstone_locked) and removes the
tombstone entry from the manager map atomically under a
single workspaces.write() critical section.
This is the only path that removes the map entry. LRU
eviction (evict_lru, reserve_rebuild’s Phase 2) leaves
the tombstone in place so per-source-root partial-eviction
state stays observable through daemon/workspaceStatus —
see Self::execute_eviction doc and STEP_6 iter-1 BLOCK.
Returns true if the workspace was present, false if it
was already absent.
Sourcepub fn find_key_and_workspace_by_path(
&self,
path: &Path,
) -> Option<(WorkspaceKey, Arc<LoadedWorkspace>)>
pub fn find_key_and_workspace_by_path( &self, path: &Path, ) -> Option<(WorkspaceKey, Arc<LoadedWorkspace>)>
Find a loaded workspace by its directory path.
Linear scan over all registered workspaces comparing each workspace’s
index_root against path. Callers (e.g. daemon/rebuild) supply a
canonicalised path but not the full WorkspaceKey.
O(n) in the number of loaded workspaces; in practice n is small.
Returns None if no workspace with a matching root is found.
Sourcepub fn status(&self) -> DaemonStatus
pub fn status(&self) -> DaemonStatus
Snapshot of daemon-wide status. Point-in-time, non-transactional.
Sourcepub fn workspace_index_status(
&self,
target_id: &WorkspaceId,
) -> Option<WorkspaceIndexStatus>
pub fn workspace_index_status( &self, target_id: &WorkspaceId, ) -> Option<WorkspaceIndexStatus>
Aggregate daemon/workspaceStatus snapshot for a single
workspace_id (STEP_6 of the workspace-aware-cross-repo plan).
Walks the manager’s workspace map, collects every
WorkspaceKey whose workspace_id == Some(target_id), and
renders a deterministic per-source-root rollup. Per-source-root
LRU eviction means individual entries can carry
WorkspaceState::Evicted while siblings remain
WorkspaceState::Loaded — the aggregate exposes that
“partially evicted” shape unchanged via
sqry_daemon_protocol::WorkspaceIndexStatus::partially_evicted.
Returns None when no entry in the map carries the requested
workspace_id. The IPC layer surfaces that as
DaemonError::WorkspaceNotLoaded; the manager itself does not
classify “no entries” as an error so callers can distinguish a
genuinely absent grouping from an empty workspace.
Sourcepub fn classify_for_serve(
&self,
key: &WorkspaceKey,
now: SystemTime,
) -> Result<ServeVerdict, DaemonError>
pub fn classify_for_serve( &self, key: &WorkspaceKey, now: SystemTime, ) -> Result<ServeVerdict, DaemonError>
Classify a workspace’s readiness to serve a query.
Task 7 Phase 7c. Used by the Task 8 IPC router on every query
dispatch. Pure-read: no mutations, no .await (sync).
§Returns
| Workspace state | Map present | Result |
|---|---|---|
Loaded or Rebuilding | yes | Ok(ServeVerdict::Fresh { graph, state }) |
Failed, age < cap (or cap == 0) | yes | Ok(ServeVerdict::Stale { graph, age_hours, last_good_at, last_error }) |
Failed, age >= cap | yes | Err(WorkspaceStaleExpired { age_hours, cap_hours, last_good_at, last_error }) (→ JSON-RPC -32002) |
Failed, no prior good | yes | Err(WorkspaceBuildFailed { reason }) (→ -32001) |
Unloaded or Loading | yes | Ok(ServeVerdict::NotReady { state }) |
Evicted | yes (transient window) | Err(WorkspaceEvicted) (→ -32004) |
| any | no | Err(WorkspaceEvicted) (→ -32004) |
§Lock order
Task 7 Phase 7c feat iter-1 Codex BLOCKER fix: takes
workspaces.read() across the FULL snapshot — state, graph,
last_good, and last_error_text are all captured inside the
read critical section. Dropping the read lock before reading
the graph would allow execute_eviction (which needs
workspaces.write() for the full graph-swap + state-store +
map-remove sequence) to interleave, surfacing the empty
post-eviction placeholder graph as a Fresh verdict.
Does not acquire admission or rebuild_lane; only
workspaces + per-workspace field locks. §J.4 order preserved.
§Errors
Returns the variants listed in the table above.
Sourcepub fn publish_and_retain(
self: &Arc<Self>,
reservation: RebuildReservation,
workspace: &LoadedWorkspace,
new_graph: CodeGraph,
) -> (OldGraphToken, Arc<CodeGraph>)
pub fn publish_and_retain( self: &Arc<Self>, reservation: RebuildReservation, workspace: &LoadedWorkspace, new_graph: CodeGraph, ) -> (OldGraphToken, Arc<CodeGraph>)
Consume a RebuildReservation plus a freshly-built
CodeGraph and atomically publish it to the workspace.
Implements Amendment 2 §G.2:
- Captures the prior
Arc<CodeGraph>andmemory_bytesinto a [RollbackGuard] before any swap — so a panic at any point before the admission update reverts cleanly. - Swaps the
ArcSwap<CodeGraph>to the new graph. - Swaps the per-workspace
memory_bytesto the new size. - Under the admission mutex: moves
bytes_deltafromreserved_bytesintoloaded_bytes, inserts aRetainedEntryholding the oldArcuntil the retention reaper frees it. - Disarms the [
RollbackGuard] on success.
Sync fn. There is no .await between the first swap and the
admission insert — tokio task cancellation can only interrupt
at .await points, so this sequence is atomic with respect
to cancellation per §G.2.
Returns the minted OldGraphToken for tracing / integration
tests, together with an Arc<CodeGraph> handle to the freshly
published graph. Per Codex Task 6 Phase 6c iter-2 MAJOR the
post-publish SqrydHook dispatch is NOT performed here —
firing on_publish under the workspaces.read() guard
get_or_load holds across this call would nest
self.hook.read() inside workspaces, giving hook impls a
re-entrancy deadlock hole if they call back into manager
methods needing workspaces.write(). The caller is
responsible for dispatching the hook after dropping every
outer workspaces-lock holder.
Trait Implementations§
Source§impl Debug for WorkspaceManager
impl Debug for WorkspaceManager
Auto Trait Implementations§
impl !Freeze for WorkspaceManager
impl !RefUnwindSafe for WorkspaceManager
impl Send for WorkspaceManager
impl Sync for WorkspaceManager
impl Unpin for WorkspaceManager
impl UnsafeUnpin for WorkspaceManager
impl !UnwindSafe for WorkspaceManager
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<D> OwoColorize for D
impl<D> OwoColorize for D
Source§fn fg<C>(&self) -> FgColorDisplay<'_, C, Self>where
C: Color,
fn fg<C>(&self) -> FgColorDisplay<'_, C, Self>where
C: Color,
Source§fn bg<C>(&self) -> BgColorDisplay<'_, C, Self>where
C: Color,
fn bg<C>(&self) -> BgColorDisplay<'_, C, Self>where
C: Color,
Source§fn black(&self) -> FgColorDisplay<'_, Black, Self>
fn black(&self) -> FgColorDisplay<'_, Black, Self>
Source§fn on_black(&self) -> BgColorDisplay<'_, Black, Self>
fn on_black(&self) -> BgColorDisplay<'_, Black, Self>
Source§fn red(&self) -> FgColorDisplay<'_, Red, Self>
fn red(&self) -> FgColorDisplay<'_, Red, Self>
Source§fn on_red(&self) -> BgColorDisplay<'_, Red, Self>
fn on_red(&self) -> BgColorDisplay<'_, Red, Self>
Source§fn green(&self) -> FgColorDisplay<'_, Green, Self>
fn green(&self) -> FgColorDisplay<'_, Green, Self>
Source§fn on_green(&self) -> BgColorDisplay<'_, Green, Self>
fn on_green(&self) -> BgColorDisplay<'_, Green, Self>
Source§fn yellow(&self) -> FgColorDisplay<'_, Yellow, Self>
fn yellow(&self) -> FgColorDisplay<'_, Yellow, Self>
Source§fn on_yellow(&self) -> BgColorDisplay<'_, Yellow, Self>
fn on_yellow(&self) -> BgColorDisplay<'_, Yellow, Self>
Source§fn blue(&self) -> FgColorDisplay<'_, Blue, Self>
fn blue(&self) -> FgColorDisplay<'_, Blue, Self>
Source§fn on_blue(&self) -> BgColorDisplay<'_, Blue, Self>
fn on_blue(&self) -> BgColorDisplay<'_, Blue, Self>
Source§fn magenta(&self) -> FgColorDisplay<'_, Magenta, Self>
fn magenta(&self) -> FgColorDisplay<'_, Magenta, Self>
Source§fn on_magenta(&self) -> BgColorDisplay<'_, Magenta, Self>
fn on_magenta(&self) -> BgColorDisplay<'_, Magenta, Self>
Source§fn purple(&self) -> FgColorDisplay<'_, Magenta, Self>
fn purple(&self) -> FgColorDisplay<'_, Magenta, Self>
Source§fn on_purple(&self) -> BgColorDisplay<'_, Magenta, Self>
fn on_purple(&self) -> BgColorDisplay<'_, Magenta, Self>
Source§fn cyan(&self) -> FgColorDisplay<'_, Cyan, Self>
fn cyan(&self) -> FgColorDisplay<'_, Cyan, Self>
Source§fn on_cyan(&self) -> BgColorDisplay<'_, Cyan, Self>
fn on_cyan(&self) -> BgColorDisplay<'_, Cyan, Self>
Source§fn white(&self) -> FgColorDisplay<'_, White, Self>
fn white(&self) -> FgColorDisplay<'_, White, Self>
Source§fn on_white(&self) -> BgColorDisplay<'_, White, Self>
fn on_white(&self) -> BgColorDisplay<'_, White, Self>
Source§fn default_color(&self) -> FgColorDisplay<'_, Default, Self>
fn default_color(&self) -> FgColorDisplay<'_, Default, Self>
Source§fn on_default_color(&self) -> BgColorDisplay<'_, Default, Self>
fn on_default_color(&self) -> BgColorDisplay<'_, Default, Self>
Source§fn bright_black(&self) -> FgColorDisplay<'_, BrightBlack, Self>
fn bright_black(&self) -> FgColorDisplay<'_, BrightBlack, Self>
Source§fn on_bright_black(&self) -> BgColorDisplay<'_, BrightBlack, Self>
fn on_bright_black(&self) -> BgColorDisplay<'_, BrightBlack, Self>
Source§fn bright_red(&self) -> FgColorDisplay<'_, BrightRed, Self>
fn bright_red(&self) -> FgColorDisplay<'_, BrightRed, Self>
Source§fn on_bright_red(&self) -> BgColorDisplay<'_, BrightRed, Self>
fn on_bright_red(&self) -> BgColorDisplay<'_, BrightRed, Self>
Source§fn bright_green(&self) -> FgColorDisplay<'_, BrightGreen, Self>
fn bright_green(&self) -> FgColorDisplay<'_, BrightGreen, Self>
Source§fn on_bright_green(&self) -> BgColorDisplay<'_, BrightGreen, Self>
fn on_bright_green(&self) -> BgColorDisplay<'_, BrightGreen, Self>
Source§fn bright_yellow(&self) -> FgColorDisplay<'_, BrightYellow, Self>
fn bright_yellow(&self) -> FgColorDisplay<'_, BrightYellow, Self>
Source§fn on_bright_yellow(&self) -> BgColorDisplay<'_, BrightYellow, Self>
fn on_bright_yellow(&self) -> BgColorDisplay<'_, BrightYellow, Self>
Source§fn bright_blue(&self) -> FgColorDisplay<'_, BrightBlue, Self>
fn bright_blue(&self) -> FgColorDisplay<'_, BrightBlue, Self>
Source§fn on_bright_blue(&self) -> BgColorDisplay<'_, BrightBlue, Self>
fn on_bright_blue(&self) -> BgColorDisplay<'_, BrightBlue, Self>
Source§fn bright_magenta(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
fn bright_magenta(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
Source§fn on_bright_magenta(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
fn on_bright_magenta(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
Source§fn bright_purple(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
fn bright_purple(&self) -> FgColorDisplay<'_, BrightMagenta, Self>
Source§fn on_bright_purple(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
fn on_bright_purple(&self) -> BgColorDisplay<'_, BrightMagenta, Self>
Source§fn bright_cyan(&self) -> FgColorDisplay<'_, BrightCyan, Self>
fn bright_cyan(&self) -> FgColorDisplay<'_, BrightCyan, Self>
Source§fn on_bright_cyan(&self) -> BgColorDisplay<'_, BrightCyan, Self>
fn on_bright_cyan(&self) -> BgColorDisplay<'_, BrightCyan, Self>
Source§fn bright_white(&self) -> FgColorDisplay<'_, BrightWhite, Self>
fn bright_white(&self) -> FgColorDisplay<'_, BrightWhite, Self>
Source§fn on_bright_white(&self) -> BgColorDisplay<'_, BrightWhite, Self>
fn on_bright_white(&self) -> BgColorDisplay<'_, BrightWhite, Self>
Source§fn bold(&self) -> BoldDisplay<'_, Self>
fn bold(&self) -> BoldDisplay<'_, Self>
Source§fn dimmed(&self) -> DimDisplay<'_, Self>
fn dimmed(&self) -> DimDisplay<'_, Self>
Source§fn italic(&self) -> ItalicDisplay<'_, Self>
fn italic(&self) -> ItalicDisplay<'_, Self>
Source§fn underline(&self) -> UnderlineDisplay<'_, Self>
fn underline(&self) -> UnderlineDisplay<'_, Self>
Source§fn blink(&self) -> BlinkDisplay<'_, Self>
fn blink(&self) -> BlinkDisplay<'_, Self>
Source§fn blink_fast(&self) -> BlinkFastDisplay<'_, Self>
fn blink_fast(&self) -> BlinkFastDisplay<'_, Self>
Source§fn reversed(&self) -> ReversedDisplay<'_, Self>
fn reversed(&self) -> ReversedDisplay<'_, Self>
Source§fn strikethrough(&self) -> StrikeThroughDisplay<'_, Self>
fn strikethrough(&self) -> StrikeThroughDisplay<'_, Self>
Source§fn color<Color>(&self, color: Color) -> FgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
fn color<Color>(&self, color: Color) -> FgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
OwoColorize::fg or
a color-specific method, such as OwoColorize::green, Read moreSource§fn on_color<Color>(&self, color: Color) -> BgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
fn on_color<Color>(&self, color: Color) -> BgDynColorDisplay<'_, Color, Self>where
Color: DynColor,
OwoColorize::bg or
a color-specific method, such as OwoColorize::on_yellow, Read more