session

package
v2.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ServiceSessionManager = "service.session.manager"
	ServiceSessionStorage = "service.session.storage"
)
View Source
const (
	SessionCookieName = "MELODYSESSID"
)
View Source
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

View Source
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

func (*FileStorage) Load

func (instance *FileStorage) Load(sessionId string) (map[string]any, bool, error)

func (*FileStorage) Save

func (instance *FileStorage) Save(sessionId string, data map[string]any, ttl time.Duration) 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

func (*InMemoryStorage) Load

func (instance *InMemoryStorage) Load(sessionId string) (map[string]any, bool, error)

func (*InMemoryStorage) Save

func (instance *InMemoryStorage) Save(sessionId string, data map[string]any, ttl time.Duration) 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 v2.13.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 v2.13.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) Close

func (instance *Manager) Close() error

func (*Manager) DeleteSession

func (instance *Manager) DeleteSession(sessionId string) error

func (*Manager) NewSession

func (instance *Manager) NewSession() sessioncontract.Session

func (*Manager) RegenerateSession added in v2.13.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

func (*Manager) Session

func (instance *Manager) Session(sessionId string) sessioncontract.Session

type Session

type Session struct {
	// contains filtered or unexported fields
}

func (*Session) All

func (instance *Session) All() map[string]any

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) Delete

func (instance *Session) Delete(key string)

func (*Session) Get

func (instance *Session) Get(key string) any
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) Has

func (instance *Session) Has(key string) bool

func (*Session) Id

func (instance *Session) Id() string

func (*Session) IsCleared

func (instance *Session) IsCleared() bool

func (*Session) IsModified

func (instance *Session) IsModified() bool

func (*Session) Set

func (instance *Session) Set(key string, value any)

func (*Session) Snapshot added in v2.13.0

func (instance *Session) Snapshot() (map[string]any, bool, bool)

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.

func (*Session) String

func (instance *Session) String(key string) string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL