Documentation
¶
Overview ¶
Package session implements Hrana's interactive-transaction streams: a Session pins one *sql.Conn for the life of a BEGIN…COMMIT spanning multiple pipeline requests, addressed by an unforgeable, server-signed *baton*. The baton is an HMAC over (session id, generation); every response rotates the generation, so an old baton is rejected — which enforces the serial, single-in-flight use Hrana requires and guards against replay. Idle streams and streams that exceed a maximum lifetime are reaped (rolling back any open transaction), the reaper skips a session with a request in flight, and the number of concurrent sessions is bounded PER DATABASE.
Index ¶
- Variables
- type Info
- type KillResult
- type Session
- func (s *Session) Capture() *sqlite.Session
- func (s *Session) Conn() *sql.Conn
- func (s *Session) DBName() string
- func (s *Session) DropSQL(id int32)
- func (s *Session) LookupSQL(id int32) (string, bool)
- func (s *Session) Principal() string
- func (s *Session) SetCapture(c *sqlite.Session)
- func (s *Session) StoreSQL(id int32, sql string)
- func (s *Session) TakeCapture() *sqlite.Session
- type Store
- func (st *Store) Baton(s *Session) string
- func (st *Store) Close(s *Session)
- func (st *Store) CloseAll()
- func (st *Store) Count() int
- func (st *Store) Kill(idStr string) KillResult
- func (st *Store) List(dbFilter func(db string) bool) []Info
- func (st *Store) Open(ctx context.Context, db *registry.DB, release func(), principal string, ...) (*Session, error)
- func (st *Store) PeekBaton(s *Session) string
- func (st *Store) Resume(baton, dbName, principal string) (*Session, error)
- func (st *Store) StartReaper(ctx context.Context, interval time.Duration)
Constants ¶
This section is empty.
Variables ¶
var ( // ErrBadBaton covers a forged, malformed, replayed, or expired baton. ErrBadBaton = errors.New("session: invalid or expired baton") // ErrTooMany is returned when a database's concurrent-session cap is reached. ErrTooMany = errors.New("session: too many open sessions for this database") // ErrPrincipalMismatch is returned when a baton is resumed by a different // principal than the one that opened the session — a stolen/misused baton. ErrPrincipalMismatch = errors.New("session: baton belongs to a different principal") )
Functions ¶
This section is empty.
Types ¶
type Info ¶
type Info struct {
ID string
DBName string
Principal string
ReadOnly bool
Busy bool
Age time.Duration
IdleFor time.Duration
}
Info is a point-in-time snapshot of one live session for introspection.
type KillResult ¶
type KillResult int
KillResult reports the outcome of a Kill.
const ( KillNotFound KillResult = iota // no session with that id KillBusy // a request is in flight; refused (bounded by the statement timeout) Killed // rolled back and closed )
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one interactive stream: a pinned connection plus its server-side SQL cache. A single request is in flight at a time (enforced by generation rotation in the Store), so its fields need no per-session lock; busy marks that in-flight request so the reaper won't close the conn under it.
func (*Session) Capture ¶
Capture reports whether a capture is attached (for the "already open" guard).
func (*Session) SetCapture ¶
SetCapture attaches (or clears) a SESSION handle used to capture a changeset on this stream's pinned connection. The Store closes it (via TakeCapture) when the stream ends, so a stream that starts a capture and never fetches the changeset does not leak it.
func (*Session) StoreSQL ¶
StoreSQL / LookupSQL / DropSQL back Hrana's store_sql / sql_id / close_sql.
func (*Session) TakeCapture ¶
TakeCapture atomically clears and returns the attached SESSION handle, so exactly one caller — the request path (session_changeset) or teardown (closeLocked) — owns and closes it, even if shutdown races an in-flight capture.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns all live sessions and mints/verifies batons.
func NewStore ¶
NewStore builds a session store with a random baton-signing key. ttl is the idle timeout; maxLife caps a session's total lifetime (so a client can't hold the writer forever with keepalives); max bounds concurrent sessions per db. Each of ttl/maxLife/max defaults to a safe value when non-positive — in particular a zero ttl must NOT mean "reap every idle session each tick" (reapable treats Since(lastUsed) > ttl as idle), so it defaults like the rest.
func (*Store) Baton ¶
Baton clears the in-flight flag, refreshes the idle clock, and returns the current baton for s (handed back so the client can make the next request).
func (*Store) Close ¶
Close ends a session: rolls back any open transaction and returns the pinned connection to the pool.
func (*Store) CloseAll ¶
func (st *Store) CloseAll()
CloseAll tears down every idle session and WAITS for their connections to be rolled back and returned to their pools. The wait matters at shutdown: the caller closes the registry (which closes the pools, checkpointing WAL) right after, so the pinned connections must be back before that — otherwise the pool would be closed out from under an in-flight ROLLBACK.
A busy session (a request in flight on its pinned conn) is deliberately skipped — the same rule reap and Kill follow: running ROLLBACK/Close on that conn here would race the in-flight request. Listeners are already draining by the time this runs, so a still-busy session's conn is rolled back and closed when its request finishes and the pool closes under it.
func (*Store) Kill ¶
func (st *Store) Kill(idStr string) KillResult
Kill closes the session with the given (base64url) id — the admin KILL: it rolls back any open transaction and returns the pinned connection to the pool, making the baton unresumable. It REFUSES a session with a request in flight (KillBusy): tearing down a connection under an in-flight statement would misuse database/sql, and a runaway statement is already bounded by the per-statement timeout, after which the session becomes killable (or the reaper takes it).
func (*Store) List ¶
List snapshots the live sessions. If dbFilter is non-nil, only sessions on a database for which it returns true are included (the authz view predicate).
func (*Store) Open ¶
func (st *Store) Open(ctx context.Context, db *registry.DB, release func(), principal string, readOnly bool) (*Session, error)
Open starts a session on db by pinning a connection. release is the registry ref for db, owned by the session and dropped on Close/reap (after the pinned conn) so the database can't be closed while a session holds a connection into it. The per-db cap is reserved BEFORE pinning a pool connection, so the cap — not the pool — is the first limit to bite. principal binds the baton to its owner; readOnly puts the pinned connection in read-only mode for the stream's life (a read-only principal's interactive transaction cannot write). The returned session is marked busy (a request is about to run on it); the caller clears that via Baton/Close.
func (*Store) PeekBaton ¶
PeekBaton mints the current baton for s WITHOUT clearing the in-flight flag. The cursor endpoint streams its response and must place the baton in the first line (the prelude) while the batch is still executing: the session stays busy for the rest of the stream — so the reaper and Kill leave the pinned connection alone — and Resume refuses a busy session, so the peeked baton is honored only once the request finishes (Baton clears the flag and keeps the same baton current).
func (*Store) Resume ¶
Resume validates a baton and returns its session, atomically consuming the baton (bumping the generation) so an old baton — a replay or a concurrent second request — is rejected with ErrBadBaton. The session is marked busy for the duration of the request the caller is about to run.
The database and principal bindings are checked BEFORE the generation is bumped: a baton presented for the wrong database or by a different principal is rejected WITHOUT consuming it, so such a request can't burn (invalidate) the legitimate owner's live baton.