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
Required Methods§
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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 stored — Ok(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.
Sourcefn 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,
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".