Skip to main content

ActivityLog

Enum ActivityLog 

Source
pub enum ActivityLog {
    Redb {
        db: Arc<Database>,
        next_id: Arc<AtomicU64>,
    },
    Discard,
}
Expand description

Thread-safe handle to the persisted activity log.

Why: held on AppState so every emitting handler (HTTP, MCP, Hook) can record an entry without re-opening the database. redb’s Database already supports concurrent access internally; an Arc clone is cheap and lets the type satisfy AppState: Clone. The Discard variant (issue #225) keeps the daemon usable when no writable directory is available (read-only containers, locked-down sandboxes) by silently dropping every append and returning empty reads — the activity log is documented as best-effort, so falling back to a no-op is the contract the rest of the daemon already assumes. What: an enum with two variants — Redb wraps a backing redb database plus an AtomicU64 next-id counter initialised from the table’s current max key (the counter survives clones because it lives behind the same Arc); Discard is a zero-state variant that drops appends and returns empty reads / zero counts, used when both the primary data root and the tempdir fallback are unwritable. Test: appends_assign_monotonic_ids covers Redb; discard_variant_drops_writes_and_returns_empty_reads covers Discard.

Variants§

§

Redb

redb-backed activity log — the production path.

Fields

§next_id: Arc<AtomicU64>
§

Discard

No-op fallback used when no writable directory is available.

Why: callers should never branch on whether the log is functional; every method on this variant returns a successful empty result so state.emit stays best-effort and the dashboard simply shows an empty feed. What: zero-sized variant — appends are dropped, count returns 0, list returns an empty vec. Test: discard_variant_drops_writes_and_returns_empty_reads.

Implementations§

Source§

impl ActivityLog

Source

pub fn open(data_root: &Path) -> Result<Self>

Open (or create) the activity log at <data_root>/activity.redb.

Why: the daemon may be started against a fresh data dir, so the helper must tolerate the file not existing. On an existing file we initialise next_id from the max key already present so ids stay monotonic across daemon restarts. What: ensures the data dir exists, opens the database, creates the activity table if absent, and seeds next_id from last_key(). Always returns the Redb variant on success; use ActivityLog::discard() to construct the no-op fallback explicitly. Test: activity_log_open_creates_db_file, next_id_resumes_from_max_after_reopen.

Source

pub fn discard() -> Self

Construct a no-op activity log that drops every write (issue #225).

Why: when neither the primary data root nor the tempdir fallback is writable, the daemon must still come up. Returning this variant from open_activity_log_with_fallback keeps the call sites identical — append, count, and list all stay infallible-ish (they return Ok but do nothing) so callers do not need to branch on whether the log is real. What: returns ActivityLog::Discard — a zero-sized enum variant. Test: discard_variant_drops_writes_and_returns_empty_reads.

Source

pub fn is_discard(&self) -> bool

True when this is the Discard (no-op) variant.

Why: exposed for tests and for any future code that wants to surface the degraded state in a health endpoint without taking a hard dependency on the enum shape. What: returns true for ActivityLog::Discard, false otherwise. Test: discard_variant_drops_writes_and_returns_empty_reads.

Source

pub fn alloc_id(&self) -> u64

Pre-allocate the next sequential id without writing anything.

Why: AppState::emit offloads the redb write to spawn_blocking (issue #232). When multiple events are emitted in rapid succession the blocking-pool workers may execute in any order, so if ID assignment happens inside the closure the persisted ordering no longer matches the emission order. Calling alloc_id() synchronously in the emitting thread (before the spawn) reserves the slot in sequence; the closure then calls append_with_id with that pre-allocated id. What: atomically increments next_id with Ordering::SeqCst and returns the old value (the reserved id). Returns 0 for the Discard variant (consistent with append_with_id’s no-op behaviour). Test: ordering invariant covered by web::tests::activity_endpoint_lists_recent_emits.

Source

pub fn append_with_id( &self, id: u64, source: ActivitySource, palace_id: Option<String>, event_type: impl Into<String>, payload: impl Serialize, ) -> Result<u64>

Append a new entry using a caller-supplied id and return it.

Why: companion to alloc_id — the caller reserves an id in the emitting thread so the id sequence matches emission order even when the actual write is deferred to a blocking-pool thread. Callers that do not need ordering guarantees may still call append, which calls alloc_id internally. What: identical to append except it skips the fetch_add and uses the supplied id directly. On the Discard variant, returns Ok(0). Test: appends_assign_monotonic_ids (via append); web::tests::activity_endpoint_lists_recent_emits (ordering path).

Source

pub fn append( &self, source: ActivitySource, palace_id: Option<String>, event_type: impl Into<String>, payload: impl Serialize, ) -> Result<u64>

Append a new entry and return the assigned id.

Why: every mutating handler calls this so the feed has a complete history. Append also triggers FIFO eviction when the row count exceeds MAX_ENTRIES so the table footprint stays bounded. What: on the Redb variant, allocates an id via alloc_id, serialises the entry with serde_json (small overhead, but keeps the schema human-readable for redb’s dump and our own debug tooling), writes it under the allocated id, and prunes the oldest rows past the cap. On the Discard variant, returns Ok(0) without touching any state. Note: callers that need the id assigned in the emitting thread (e.g. AppState::emit which defers the write to spawn_blocking) should call alloc_id() + append_with_id() instead. Test: appends_assign_monotonic_ids, appends_evict_oldest_when_capped, discard_variant_drops_writes_and_returns_empty_reads.

Source

pub fn prune(&self) -> Result<()>

Drop oldest rows until the table is at or below MAX_ENTRIES.

Why: keep the on-disk footprint bounded. Called from append so the cap is enforced on every write; tests can also call it directly. What: counts rows, computes the overflow, and removes the lowest-id rows in batches of [EVICTION_BATCH]. On the Discard variant, returns immediately — there is nothing to evict. Test: appends_evict_oldest_when_capped.

Source

pub fn count(&self) -> Result<u64>

Number of entries currently in the table.

Why: exposed for tests and the prune loop; also handy for the GET /api/v1/activity response so the UI can render a total count. What: opens a read transaction and calls redb’s Table::len on the Redb variant; returns 0 for the Discard variant. Test: appends_evict_oldest_when_capped, discard_variant_drops_writes_and_returns_empty_reads.

Source

pub fn list( &self, filter: &ActivityFilter, limit: usize, offset: usize, ) -> Result<Vec<ActivityEntry>>

List entries newest-first with optional filters and paging.

Why: backs GET /api/v1/activity. Newest-first ordering matches the dashboard’s mental model — the most recent event sits at the top of the feed. What: walks the table in reverse-key order, applies the filters in memory (the dataset is bounded at MAX_ENTRIES, so a linear scan is the simplest correct strategy), and returns at most limit rows starting at offset. limit is clamped at the call site by the handler; this method does not clamp so tests can exercise edge cases. On the Discard variant, returns an empty vec. Test: list_returns_newest_first, list_filters_by_source_palace_and_time, discard_variant_drops_writes_and_returns_empty_reads.

Trait Implementations§

Source§

impl Clone for ActivityLog

Source§

fn clone(&self) -> ActivityLog

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. 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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

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

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more