Skip to main content

OperatorSessionStore

Trait OperatorSessionStore 

Source
pub trait OperatorSessionStore: Send + Sync {
    // Required methods
    fn name(&self) -> &str;
    fn put<'life0, 'async_trait>(
        &'life0 self,
        record: OperatorSessionRecord,
    ) -> Pin<Box<dyn Future<Output = Result<(), OperatorSessionStoreError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
    fn delete<'life0, 'life1, 'async_trait>(
        &'life0 self,
        sid: &'life1 SessionId,
    ) -> Pin<Box<dyn Future<Output = Result<(), OperatorSessionStoreError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn get<'life0, 'life1, 'async_trait>(
        &'life0 self,
        sid: &'life1 SessionId,
    ) -> Pin<Box<dyn Future<Output = Result<Option<OperatorSessionRecord>, OperatorSessionStoreError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn list<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<OperatorSessionRecord>, OperatorSessionStoreError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
}
Expand description

Persistence interface for Operator login-flow sessions.

Write-through contract on the server side: POST /v1/operators calls put before answering the mint, teardown (DELETE /v1/operators/:sid) calls delete, and a fresh boot calls list once to rehydrate its in-memory session map.

Required Methods§

Source

fn name(&self) -> &str

Backend name — for diagnostics/logging.

Source

fn put<'life0, 'async_trait>( &'life0 self, record: OperatorSessionRecord, ) -> Pin<Box<dyn Future<Output = Result<(), OperatorSessionStoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Insert or replace the row for record.sid. Upsert semantics: sids are freshly minted so a same-sid overwrite only happens on a deliberate re-put of the same session.

Source

fn delete<'life0, 'life1, 'async_trait>( &'life0 self, sid: &'life1 SessionId, ) -> Pin<Box<dyn Future<Output = Result<(), OperatorSessionStoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Delete the row for sid. NotFound when no such row exists.

Source

fn get<'life0, 'life1, 'async_trait>( &'life0 self, sid: &'life1 SessionId, ) -> Pin<Box<dyn Future<Output = Result<Option<OperatorSessionRecord>, OperatorSessionStoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

The row stored under sid, exactly as storedOk(None) when there is none.

§This one does not apply the horizon, and that is the point

list both filters and deletes, which leaves it unable to answer the question its own contract is written around: was the expired row deleted, or merely withheld? Both produce the same list. Three assertions elsewhere claimed to check the deletion and read it through list, so all three would have passed on a filter-only backend — the load-bearing half of the contract (“Filtering without deleting would hide them from the reader while leaving the file growing”) was untestable through the trait, because the trait exposed no unfiltered read.

This is that read. It reports the backing store’s contents and applies no judgment of its own, so a caller can tell a deleted row from a hidden one.

§It is not a session-resolution path

Nothing in the server resolves a live session through here: a running process answers about sessions out of its in-memory map, and the durable rows are read exactly once, at boot, by list. Handing an expired row back is therefore not a way to revive one — the row goes to a test or a diagnostic, both of which want the truth about the file rather than the truth about who may be served.

Source

fn list<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<OperatorSessionRecord>, OperatorSessionStoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

List the sessions this store can decode and that have not expired, ascending by joined_at_secs (mint order, stable for deterministic rehydration).

§Contract: an expired row is dropped and deleted

A row whose last access is OPERATOR_SESSION_MAX_IDLE_SECS or more in the past is model §4.1’s second exit from Registered: Registered ── 最終アクセスから 24h ──▶ ╳ 削除 (unnumbered — see OPERATOR_SESSION_MAX_IDLE_SECS). Implementations must omit it from the returned vector and remove it from the backing store, reporting each removal with a tracing::info!.

A list that deletes is unusual enough to say why it is here rather than in a reaper. The sole caller is boot-time rehydration, which is also the only moment a persisted session is read from disk at all — so this is where an expired row would otherwise be resurrected, once per restart, forever (the row’s own driver crashed and lost the bearer DELETE /v1/operators/:sid wants, so nothing else can ever remove it). Filtering without deleting would hide them from the reader while leaving the file growing.

The running server sweeps expired sessions on a schedule as well (see OPERATOR_SESSION_MAX_IDLE_SECS), but that job walks the live session map — which, at this moment, is the empty one this call is about to fill. Boot is the one point where a row exists and no session does, so this contract is the sweep’s counterpart across a restart, not a duplicate of it.

Deleting is safe precisely because the row is expired: no live process holds it (it was not in memory — this call is what would have put it there), and nothing else refers to it. A Run.current naming it is repaired by an acquire (A8), the same repair a crashed driver’s seat already needs.

§Contract: per row, not all-or-nothing

A backend that decodes at-rest bytes back into OperatorSessionRecord must not let one undecodable row fail the whole call. Such a row is skipped and reported with a tracing::warn! naming the row and the field that failed; the intact rows are still returned. An Err from this method therefore means the backend failed (the file is unreadable, the connection is gone) — never that one stored session went bad.

This matters because the sole caller is boot-time rehydration, and its own error path is fatal: an Err here takes mse serve down and every healthy session with it. Undecodable rows are reachable in practice — an older build could persist shapes a newer one rejects (sid: "op-<uuid>" predates the S-<hex> shape) — so all-or-nothing decoding means one stale row bricks the boot.

Skipping the row rather than defaulting the field is deliberate: a session restored minus a field it was minted with would come back claiming something other than what it is, and would fail later, elsewhere, and quietly. Dropping it is the observable choice.

§Backends that never decode

InMemoryOperatorSessionStore holds live OperatorSessionRecords, so no row of its can be undecodable and it never skips anything. That is consistent with the contract, not an exemption from it: “the sessions this store can decode” is every session it holds.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§