Skip to main content

SessionStore

Struct SessionStore 

Source
pub struct SessionStore<S> { /* private fields */ }
Expand description

The sessions this server currently holds open.

Generic over what a session is, because this type is a map and a map does not need to know. The Streamable HTTP adapter instantiates it with whatever the router keeps per session — an engine handle, a protocol state machine, a cancellation token — and the Arc in lookup is what lets a handler hold that state across an .await without borrowing the store.

§Example

use std::sync::Arc;
use protocol_droid::adapters::primary::transport::http::SessionStore;
use protocol_droid::application::session::IdSource;
use protocol_droid::domain::session::state::SessionId;

// A source that hands out a fixed sequence, so the test can say which id it
// expects instead of discovering it.
struct Counting(core::sync::atomic::AtomicU64);
impl IdSource for Counting {
    fn next_session_id(&self) -> SessionId {
        SessionId::new(
            self.0.fetch_add(1, core::sync::atomic::Ordering::SeqCst) + 1,
        )
    }
}
let ids: Arc<dyn IdSource + Send + Sync> =
    Arc::new(Counting(core::sync::atomic::AtomicU64::new(0)));

let store = SessionStore::new(ids);
let id = store.open(Arc::new("the negotiated state"))?;
// The id is a header value, so it round-trips through the header parser.
assert_eq!(id.to_string().parse(), Ok(id));
assert!(store.lookup(&id)?.is_some());

// DELETE removes it, and a later request is a miss rather than a leftover.
assert!(store.terminate(&id)?.is_some());
assert!(store.lookup(&id)?.is_none());

Implementations§

Source§

impl<S> SessionStore<S>

Source

pub fn new(ids: Arc<dyn IdSource + Send + Sync>) -> Self

A store with no sessions, numbering them from ids.

Takes the source rather than constructing one: the engine already has an IdSource for the same reason, and two sources means two sequences for what a reader will reasonably assume is one numbering.

The store built through this constructor is unbounded. A deployment that needs a bound constructs with with_idle_reaper instead, which adds the bookkeeping reap_idle reads.

Source

pub fn with_idle_reaper( ids: Arc<dyn IdSource + Send + Sync>, clock: Arc<dyn Clock + Send + Sync>, ) -> Self

A store that tracks when each session was last touched, so a caller-driven reap_idle can evict the ones a client has stopped driving.

The store does not start a reaper task on its own: a background task started inside a constructor is one a deployment cannot stop, and reaping intervals are an operator decision. The caller is expected to call reap_idle from whichever loop it owns (a periodic timer, a select! on shutdown, an external cron) with the max_age it considers idle.

clock is the source of time the store reads. It is injected rather than read from a global so a test can drive reaping without sleeping; the rest of the crate already takes the same shape for IdSource.

Source

pub fn open(&self, session: Arc<S>) -> Result<McpSessionId, SessionStoreError>

Registers session under a freshly minted id and returns that id.

The caller puts the id in the Mcp-Session-Id response header; this type never formats it, because the buffer-based McpSessionId::as_bytes is how the header gets filled without an allocation per response.

When the store was built with with_idle_reaper, the new entry’s last_touched is the clock’s current reading. The recording happens under the same lock as the insertion, so a reap_idle that runs concurrently with an open cannot see the session without also seeing the timestamp that would spare it.

§Errors

SessionStoreError::DuplicateId if the injected source returned an id this store is already holding. IdSource promises not to repeat one, so a source that has wrapped or one that was seeded twice has broken its contract, and overwriting would hand the second caller a session belonging to the first. One id is tried: retrying a source that is provably out of ids is a livelock with better manners, and the error says what to do.

SessionStoreError::Unavailable if the lock is poisoned.

Source

pub fn open_with_id( &self, session: Arc<S>, id: SessionId, ) -> Result<McpSessionId, SessionStoreError>

Opens a session under an id the caller has already taken.

Separate from Self::open because a session’s peer has to know its own id before it is inserted — the peer is part of the value being inserted, and everything the peer keys on (idempotency, in-flight requests) reads the id from inside it. A store that allocated the id on the way in could only hand the peer that id afterwards, by which point the peer has already been constructed with the wrong one.

§Errors

As Self::open: SessionStoreError::DuplicateId if the id is already held, SessionStoreError::Unavailable if the lock is poisoned.

Source

pub fn next_id(&self) -> SessionId

The next id this store would hand out.

For a caller that must build a session around a known id before it can hand that session to Self::open_with_id. The contract is the same as the one open relies on: a source that repeats an id is caught by the duplicate check there, so reading an id early is safe even though nothing reserves it in the meantime.

Two callers racing here get two different ids and both open; the loser is refused by the duplicate check, which is the answer for a source that repeats one.

Source

pub fn lookup( &self, id: &McpSessionId, ) -> Result<Option<Arc<S>>, SessionStoreError>

The session under id, if this server still has it.

A returned Arc is a snapshot of reachability, not of the session’s contents: a concurrent DELETE can remove the map entry while this clone is still alive and being used. That is the intended semantics — the in-flight request finishes against the session it already found, and only the next request sees the 404 — so callers must not treat a Some as a promise that the entry will still be there in a moment.

When the store was built with with_idle_reaper, a successful lookup updates the entry’s last_touched to the clock’s current reading. The bookkeeping is what makes a session “idle” mean “no request has hit it for the window”, and a request hitting it is the only signal the store has that the client is still driving it.

§Errors

SessionStoreError::Unavailable if the lock is poisoned.

Source

pub fn terminate( &self, id: &McpSessionId, ) -> Result<Option<Arc<S>>, SessionStoreError>

Removes the session under id, returning it if it was there.

A None here is the same None lookup returns, and the router maps both to the same outcome. Deciding between “terminate this session” and “this endpoint does not offer termination here” is the router’s, because only the router knows what the endpoint offers.

When the store was built with with_idle_reaper, the corresponding last_touched is also removed: leaving it behind would be a slow leak of a different shape (timestamps whose sessions have already gone), and the next reap_idle would walk past an entry that no longer has a session to evict.

§Errors

SessionStoreError::Unavailable if the lock is poisoned.

Source

pub fn reap_idle( &self, max_age: Duration, ) -> Result<Vec<McpSessionId>, SessionStoreError>

Evicts every session whose last_touched is older than max_age, returning the ids that were reaped.

The store built through new has no reaper and reap_idle is a no-op that returns an empty vector. The store built through with_idle_reaper records the time on every open and lookup, and reap_idle compares it against the clock’s current reading.

The caller drives reaping: a periodic timer, a select! on shutdown, an external cron. There is no background task started inside the store, because a background task started inside a constructor is one a deployment cannot stop.

§Errors

SessionStoreError::Unavailable if the reaper’s lock is poisoned, which the same rule that refuses to recover the sessions map also applies here: answering from state this process cannot vouch for is how one request ends up served another’s.

Source

pub fn len(&self) -> Result<usize, SessionStoreError>

How many sessions are open.

§Errors

SessionStoreError::Unavailable if the lock is poisoned. A made-up number here would be worse than the error: this is the count an operator reads to decide whether something is wrong.

Source

pub fn is_empty(&self) -> Result<bool, SessionStoreError>

Whether no session is open.

§Errors

SessionStoreError::Unavailable if the lock is poisoned.

Trait Implementations§

Source§

impl<S> Debug for SessionStore<S>

Source§

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

Prints the type’s name and nothing else.

Deliberately opaque, and the omission is the point: a derived Debug on this struct would print the whole map, which is a table of session ids — and the project does not put a full session id in a log line. A {:?} in a log statement must not become a way around that.

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<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

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> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + 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: Sized + 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> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Error = !

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

fn try_from(value: U) -> Result<T, !>

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