Files
summercms/modules/pier/store.go
Jakub Zych c1c0c77ddc fix(pier): reserve session slots atomically and build servers outside router locks
- an unexported reserver (Reserve, Release) next to updater: the memory
  store reserves under its mutex, the Redis store in one Lua script
  (prune, SCARD, SET PX plus SADD); serveNew reserves instead of Count
  then Put and releases on a failed connect or initialize (A-WR-03)
- serveNew, lookup's rehydration and the stateless getServer build the
  SDK server with no router-wide lock held and insert or adopt under the
  lock afterwards, so a slow guard or database check no longer stalls
  the instance (A-WR-07)
2026-10-23 15:56:57 +02:00

66 lines
2.7 KiB
Go

package pier
import (
"context"
"encoding/json"
"time"
)
// Record is one stored session: the binding key of the identity that opened
// it, the JSON of the SDK's ServerSessionState once initialised (nil until
// then) and the creation time.
type Record struct {
Binding string `json:"binding"`
State json.RawMessage `json:"state,omitempty"`
Created time.Time `json:"created"`
}
// Event is what instances publish to each other: a session ended by DELETE
// (EventEnded, with SessionID) or a subject change (the pact
// MCPSubjectChanged kinds, with Guard and UserID). Instance names the
// publisher so it can skip its own echo when it has already acted.
type Event struct {
Kind string `json:"kind"`
Guard string `json:"guard,omitempty"`
UserID uint `json:"user_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Instance string `json:"instance,omitempty"`
}
// EventEnded is the kind of a session-ended event.
const EventEnded = "ended"
// Store is the session registry and the event channel shared by every
// instance of the application: memory for one instance (and tests), Redis
// for several. Put writes a record with a TTL, Touch slides it, Count
// answers the live sessions of one binding, Publish and Subscribe carry
// Events.
type Store interface {
Put(ctx context.Context, id string, rec Record, ttl time.Duration) error
Get(ctx context.Context, id string) (Record, bool, error)
Touch(ctx context.Context, id string, ttl time.Duration) error
Delete(ctx context.Context, id string) error
Count(ctx context.Context, binding string) (int, error)
Publish(ctx context.Context, ev Event) error
Subscribe(ctx context.Context, fn func(Event)) error
}
// updater is implemented by the stores pier ships: Update rewrites an
// existing record and never creates one (Redis SET XX), answering false when
// the record is gone, so a request that finishes after its session ended
// (DELETE, revocation, deletion, expiry) cannot write the record back.
type updater interface {
Update(ctx context.Context, id string, rec Record, ttl time.Duration) (bool, error)
}
// reserver is implemented by the stores pier ships: Reserve is the atomic
// form of Count then Put (A-WR-03). It writes rec for id with the TTL and
// adds id to the binding's sessions only when the binding holds fewer than
// max live sessions, answering false (nothing written) at the cap, so
// concurrent initializes of one binding cannot exceed it. Release drops a
// reservation whose session never opened (a failed connect or initialize).
type reserver interface {
Reserve(ctx context.Context, binding, id string, rec Record, max int, ttl time.Duration) (bool, error)
Release(ctx context.Context, binding, id string) error
}