Documentation
¶
Index ¶
- Constants
- Variables
- func SessionMustFromContainer(serviceContainer containercontract.Container) sessioncontract.Manager
- func SessionStorageMustFromContainer(serviceContainer containercontract.Container) sessioncontract.Storage
- func SessionStorageMustFromResolver(resolver containercontract.Resolver) sessioncontract.Storage
- type FileStorage
- type InMemoryStorage
- func (instance *InMemoryStorage) Clear() error
- func (instance *InMemoryStorage) Close() error
- func (instance *InMemoryStorage) Delete(sessionId string) error
- func (instance *InMemoryStorage) Load(sessionId string) (map[string]any, bool, error)
- func (instance *InMemoryStorage) Save(sessionId string, data map[string]any, ttl time.Duration) error
- type Manager
- func (instance *Manager) Close() error
- func (instance *Manager) DeleteSession(sessionId string) error
- func (instance *Manager) NewSession() sessioncontract.Session
- func (instance *Manager) RegenerateSession(sessionInstance sessioncontract.Session) (sessioncontract.Session, error)
- func (instance *Manager) SaveSession(sessionInstance sessioncontract.Session) error
- func (instance *Manager) Session(sessionId string) sessioncontract.Session
- type Session
- func (instance *Session) All() map[string]any
- func (instance *Session) Clear()
- func (instance *Session) Delete(key string)
- func (instance *Session) Get(key string) any
- func (instance *Session) Has(key string) bool
- func (instance *Session) Id() string
- func (instance *Session) IsCleared() bool
- func (instance *Session) IsModified() bool
- func (instance *Session) Set(key string, value any)
- func (instance *Session) Snapshot() (map[string]any, bool, bool)
- func (instance *Session) String(key string) string
Constants ¶
const ( ServiceSessionManager = "service.session.manager" ServiceSessionStorage = "service.session.storage" )
const (
SessionCookieName = "MELODYSESSID"
)
const TombstoneRetention = 5 * time.Minute
TombstoneRetention is the default for how long a deleted session id is remembered so a request that loaded that session before it was deleted cannot write it back. It has to cover the longest a request can still be holding a snapshot taken before the delete — the lifetime of an in-flight request, not the lifetime of a session — and nothing in the chain bounds that lifetime: the server's socket timeouts cut the connection, not the handler goroutine, so a request that outlives the window can save the deleted session back. A deployment whose slowest legitimate request exceeds five minutes sizes the window to match, through MELODY_HTTP_SESSION_TOMBSTONE_RETENTION on the framework path or NewManagerWithTombstoneRetention when wiring the manager by hand; what the window costs is one remembered entry per deletion inside it, and the record lives in this manager, per process.
Variables ¶
var ErrSessionDeleted = errors.New("session was deleted")
ErrSessionDeleted is the cause carried by the error SaveSession returns for a session that was deleted while the request holding it was still running. It says the session ended, not that the storage failed, and the two need different answers: the response path expires the browser cookie and serves the handler's response, where a storage outage suppresses the cookie and answers 500.
Functions ¶
func SessionMustFromContainer ¶
func SessionMustFromContainer(serviceContainer containercontract.Container) sessioncontract.Manager
func SessionStorageMustFromContainer ¶
func SessionStorageMustFromContainer(serviceContainer containercontract.Container) sessioncontract.Storage
func SessionStorageMustFromResolver ¶
func SessionStorageMustFromResolver(resolver containercontract.Resolver) sessioncontract.Storage
Types ¶
type FileStorage ¶
type FileStorage struct {
// contains filtered or unexported fields
}
FileStorage is recommended for development only. Two reasons are written here because neither shows until the store has been up for a while. Values are flushed as JSON and reloaded at construction, so a session survives a restart with its SHAPES changed — an int comes back float64, a struct comes back map[string]any, a time.Time comes back a string — while the same session read in-process keeps the types the handler stored: a type assertion on a session value therefore holds for the life of a process and starts failing after the first restart. And every write re-encodes and fsyncs the whole snapshot, so what one save costs is set by how many sessions everyone else has: measured at about 6.7ms over 100 sessions, 9.1ms over 1 000 and 30ms over 10 000, which is a few dozen writes a second rather than a few thousand.
func NewFileStorageFromFile ¶
func NewFileStorageFromFile(fileInstance *os.File) (*FileStorage, error)
NewFileStorageFromFile builds the storage over a handle the caller owns and keeps owning: it is not closed here, and every write goes through that same handle rather than through a path. The atomicity its NewFileStorageFromPath sibling guarantees is therefore NOT available to this door — a temp file and a rename would unlink the inode the caller still holds, leaving it writing into a file nothing can reach. What this door guarantees instead: the snapshot is encoded whole before a byte is written, the write precedes the truncation, and the truncation cuts to the length just written, so no crash can leave a zero-length file and lose every persisted session. A kill landing inside the write itself can still leave a torn document, which the next construction reports as a decode failure rather than reading as an empty one.
The handle must be seekable and must not be opened for appending. Both are refused here rather than at the first save, since both are properties of what the caller opened and neither can improve later: appending in particular used to be accepted and then to corrupt silently, because every write landed after the document it was replacing.
func NewFileStorageFromPath ¶
func NewFileStorageFromPath(path string) (*FileStorage, error)
func (*FileStorage) Close ¶
func (instance *FileStorage) Close() error
func (*FileStorage) Delete ¶
func (instance *FileStorage) Delete(sessionId string) error
type InMemoryStorage ¶
type InMemoryStorage struct {
// contains filtered or unexported fields
}
func NewInMemoryStorage ¶
func NewInMemoryStorage() *InMemoryStorage
func NewInMemoryStorageWithCleanupInterval ¶
func NewInMemoryStorageWithCleanupInterval(cleanupInterval time.Duration) *InMemoryStorage
func (*InMemoryStorage) Clear ¶
func (instance *InMemoryStorage) Clear() error
func (*InMemoryStorage) Close ¶
func (instance *InMemoryStorage) Close() error
func (*InMemoryStorage) Delete ¶
func (instance *InMemoryStorage) Delete(sessionId string) error
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
func NewManager ¶
func NewManager(storage sessioncontract.Storage, ttl time.Duration) *Manager
NewManager takes a storage it does not own: Close leaves it open, because a storage handed in was built by someone else and is closed by whoever built it. That is the same rule NewFileStorageFromFile follows for an injected file handle, and it is what the container path needs — the storage is a registered service the container closes itself, so a manager that closed it too would close it twice, which a storage wrapping a connection typically reports as a failure on the second call and turns a clean shutdown into a reported one. Use NewManagerOwningStorage to get the cascade back.
func NewManagerOwningStorage ¶ added in v1.19.0
func NewManagerOwningStorage(storage sessioncontract.Storage, ttl time.Duration) *Manager
NewManagerOwningStorage takes a storage it closes when it is closed itself, for the caller that builds both by hand and wants one Close to end both. Do not use it for a storage that is also registered as a service: the container closes every service it created, so the storage would be closed once by this manager and once by the container.
func NewManagerWithTombstoneRetention ¶ added in v1.19.0
func NewManagerWithTombstoneRetention( storage sessioncontract.Storage, ttl time.Duration, tombstoneRetention time.Duration, ) *Manager
NewManagerWithTombstoneRetention sizes the write-back refusal window to the deployment instead of the default: the window has to cover the longest a request can still be holding a session snapshot loaded before a delete, and only the deployment knows its slowest legitimate request. Only a positive window can refuse anything — zero or negative would disarm the logout defence entirely, so they are refused here the way the negative ttl is, rather than carried silently.
func (*Manager) DeleteSession ¶
func (*Manager) NewSession ¶
func (instance *Manager) NewSession() sessioncontract.Session
func (*Manager) RegenerateSession ¶ added in v1.19.0
func (instance *Manager) RegenerateSession(sessionInstance sessioncontract.Session) (sessioncontract.Session, error)
RegenerateSession rotates a session id, the defence against session fixation: the returned session carries the values over under a fresh id and the entry the previous id pointed at is removed. Rotation lives on the manager because only it holds the storage the candidate id is probed against and the previous entry deleted from — a Session keeps no storage reference. The result is a new object marked modified, so publishing it on the request under http.RequestAttributeSession is what makes the response path store it and emit its cookie — http.RegenerateRequestSession does both. The session passed in is latched cleared — a later write to it cannot lift that — so a caller that forgets to publish the rotated one has the response path expire the browser cookie and hand out a fresh session, instead of leaving the client presenting an id that no longer exists.
func (*Manager) SaveSession ¶
func (instance *Manager) SaveSession(sessionInstance sessioncontract.Session) error
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
func (*Session) All ¶
All hands out a copy that reaches all the way down, the depth both storages already copy at. A copy of only the top level would hand the caller the very map or slice a nested value holds, so mutating it would change the live session without passing through Set — the session would not be marked modified and the change would never be persisted, while a caller that mutates it after the response path has handed the same value to the storage races the copy the storage makes.
func (*Session) Clear ¶
func (instance *Session) Clear()
Clear ends the session, and the ending latches: a later Set puts a value back and marks the session modified, but it cannot make the session look live again. Without the latch a logout handler that clears the session and is followed by anything writing to the same object — a middleware or an event listener leaving a farewell message — had the response path take the save branch instead of the delete branch, so the values were overwritten but the pre-logout id stayed alive in the storage and was re-issued to the browser under the same cookie. A caller that wants a usable session after clearing one asks the manager for a new session.
A Clear must land before the handler returns to be guaranteed effective: the response path decides the session's fate from one Snapshot, and a Clear arriving from a goroutine that outlives the handler can land after that snapshot was taken — the save it raced then persists the pre-logout state and the live cookie is re-issued, with the latch only reaching the NEXT request that loads this session.
func (*Session) Get ¶
Get hands out a copy at the depth All copies at, for the same reason All does: the live nested
value, mutated in place, would change the session without passing through Set — the session would not be marked modified, SaveSession would skip the write and report success, and the mutation would silently never persist. Read, mutate the copy, Set it back.
func (*Session) IsModified ¶
func (*Session) Snapshot ¶ added in v1.19.0
Snapshot reads the values, the modified flag and the cleared flag under one lock acquisition: the response path pairs the branch decision with the values it acts on, and reading them through the individual accessors let a concurrent Clear slip between the reads — the save branch then wrote the emptied map under a live id, a session neither alive nor deleted.