state

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: May 14, 2025 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrStateNotFound      = errors.New("state not found")
	ErrStateNotRegistered = errors.New("state not registered")
	ErrStateNotPointer    = errors.New("state must be a pointer")
	ErrStateIDComponents  = errors.New("state id components must not be empty")
)
View Source
var (
	ErrSnapshotNotFound = errors.New("snapshot not found")
	ErrStorageNotFound  = errors.New("storage not found")
)
View Source
var (
	ErrStateIDAndFieldsMismatch = errors.New("state ID and fields mismatch")
)

Functions

func GetStateID added in v0.2.1

func GetStateID(state State) (stateID string, err error)

func GetStateIDByComponents added in v0.3.0

func GetStateIDByComponents(idMarshaler IDMarshaler, stateIDComponents StateIDComponents) (stateID string, err error)

func GetStateLockerByName added in v0.3.0

func GetStateLockerByName(lockerGenerator locker.SyncLockerGenerator, stateName, stateID string) (sync.Locker, error)

func GetStorageLockerByName added in v0.3.0

func GetStorageLockerByName(lockerGenerator locker.SyncLockerGenerator, storageName string) (sync.Locker, error)

Types

type Base64IDMarshaler

type Base64IDMarshaler struct {
	*JsonIDMarshaler
}

func NewBase64IDMarshaler

func NewBase64IDMarshaler(seperator string) *Base64IDMarshaler

func (*Base64IDMarshaler) MarshalStateID

func (c *Base64IDMarshaler) MarshalStateID(fields ...any) (string, error)

func (*Base64IDMarshaler) UnmarshalStateID

func (c *Base64IDMarshaler) UnmarshalStateID(ID string, fields ...any) error

type BaseState

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

func NewBaseState

func NewBaseState(lockerGenerator locker.SyncLockerGenerator, stateName string, idMarshaler IDMarshaler, idComponents StateIDComponents) (*BaseState, error)

func (*BaseState) GetIDMarshaler

func (s *BaseState) GetIDMarshaler() IDMarshaler

func (*BaseState) GetLocker

func (s *BaseState) GetLocker() sync.Locker

func (*BaseState) GetLockerGenerator added in v0.3.0

func (s *BaseState) GetLockerGenerator() locker.SyncLockerGenerator

func (*BaseState) Initialize added in v0.3.0

func (s *BaseState) Initialize(generator locker.SyncLockerGenerator, stateName string, idMarshaler IDMarshaler, idComponents StateIDComponents) (err error)

func (*BaseState) Lock

func (s *BaseState) Lock()

func (*BaseState) StateIDComponents

func (s *BaseState) StateIDComponents() StateIDComponents

func (*BaseState) StateName

func (s *BaseState) StateName() string

func (*BaseState) Unlock

func (s *BaseState) Unlock()

type CacheAndPersistFinalizer

type CacheAndPersistFinalizer struct {
	StorageSnapshot
	// contains filtered or unexported fields
}

func NewCacheAndPersistFinalizer

func NewCacheAndPersistFinalizer(interval time.Duration, registry Registry, lockerGenerator locker.SyncLockerGenerator, cache Storage, persist Storage, name string) *CacheAndPersistFinalizer

func (*CacheAndPersistFinalizer) ClearAllCachedStates

func (s *CacheAndPersistFinalizer) ClearAllCachedStates() error

func (*CacheAndPersistFinalizer) ClearCacheStates added in v0.2.4

func (s *CacheAndPersistFinalizer) ClearCacheStates(states ...State) error

func (*CacheAndPersistFinalizer) ClearPersistStates added in v0.2.4

func (s *CacheAndPersistFinalizer) ClearPersistStates(states ...State) error

func (*CacheAndPersistFinalizer) ClearStates added in v0.2.4

func (s *CacheAndPersistFinalizer) ClearStates(states ...State) error

func (*CacheAndPersistFinalizer) Close

func (s *CacheAndPersistFinalizer) Close()

func (*CacheAndPersistFinalizer) EnableAutoFinalizeAllCachedStates

func (s *CacheAndPersistFinalizer) EnableAutoFinalizeAllCachedStates(enable bool)

func (*CacheAndPersistFinalizer) FinalizeAllCachedStates

func (s *CacheAndPersistFinalizer) FinalizeAllCachedStates() (err error)

func (*CacheAndPersistFinalizer) FinalizeSnapshot

func (s *CacheAndPersistFinalizer) FinalizeSnapshot(snapshotID string) (err error)

func (*CacheAndPersistFinalizer) GetAutoFinalizeInterval added in v0.3.0

func (s *CacheAndPersistFinalizer) GetAutoFinalizeInterval() time.Duration

func (*CacheAndPersistFinalizer) GetCacheStorage added in v0.3.0

func (s *CacheAndPersistFinalizer) GetCacheStorage() Storage

func (*CacheAndPersistFinalizer) GetPersistStorage added in v0.3.0

func (s *CacheAndPersistFinalizer) GetPersistStorage() Storage

func (*CacheAndPersistFinalizer) LoadState

func (s *CacheAndPersistFinalizer) LoadState(name string, id string) (State, error)

func (*CacheAndPersistFinalizer) SaveState

func (s *CacheAndPersistFinalizer) SaveState(state State) error

func (*CacheAndPersistFinalizer) SaveStates added in v0.3.0

func (s *CacheAndPersistFinalizer) SaveStates(states ...State) error

type FinalizeState added in v0.3.0

type FinalizeState struct {
	BaseState
	Name             string
	LastFinalizeTime time.Time
}

func MustNewFinalizeState added in v0.3.0

func MustNewFinalizeState(lockerGenerator locker.SyncLockerGenerator, name string) *FinalizeState

func (*FinalizeState) StateIDComponents added in v0.3.0

func (u *FinalizeState) StateIDComponents() StateIDComponents

type Finalizer

type Finalizer interface {
	StorageSnapshot

	GetCacheStorage() Storage
	GetPersistStorage() Storage

	Close()

	// LoadState loads state from cache first, if not found, load from persist
	// if not found, return ErrStateNotFound
	LoadState(name string, id string) (State, error)

	// SaveState/SaveStates saves state(s) to cache
	SaveState(state State) error
	SaveStates(state ...State) error

	ClearCacheStates(states ...State) error
	ClearPersistStates(states ...State) error
	ClearStates(states ...State) error
	ClearAllCachedStates() error

	// Fianlize* functions are used to finalize snapshot/cached states into persist,
	// and they are only called once at the same time until all states are finalized.
	// It's safe for distributed system if you're using distributed locker like
	// `RedSyncMutexWrapper` implemented in package `locker`
	FinalizeSnapshot(snapshotID string) error
	FinalizeAllCachedStates() error

	// EnableAutoFinalizeAllCachedStates allows you to enable/disable auto finalize.
	// false by default.
	// If you want to finalize all cached states manually, use `FinalizeAllCachedStates`
	// and disable auto finalize feature.
	EnableAutoFinalizeAllCachedStates(enable bool)
	GetAutoFinalizeInterval() time.Duration
}

Finalize uses two types of storage: cache and persist read/write from/to cache first and then write all cached states into persist storage.

type GORMStorage

type GORMStorage struct {
	Registry
	StorageSnapshot
	// contains filtered or unexported fields
}

TODO: Unit test DO NOT use this to create snapshot.

func NewGORMStorage

func NewGORMStorage(lockerGenerator locker.SyncLockerGenerator, db *gorm.DB, registry Registry, snapshot StorageSnapshot, partition string) (*GORMStorage, error)

func (*GORMStorage) BatchDelete

func (s *GORMStorage) BatchDelete(models ...any) error

func (*GORMStorage) BatchSave

func (s *GORMStorage) BatchSave(models ...any) error

func (*GORMStorage) ClearAllStates

func (s *GORMStorage) ClearAllStates() error

func (*GORMStorage) ClearStates

func (s *GORMStorage) ClearStates(states ...State) error

func (*GORMStorage) GetStateIDs

func (s *GORMStorage) GetStateIDs(name string) ([]string, error)

func (*GORMStorage) GetStateNames

func (s *GORMStorage) GetStateNames() ([]string, error)

func (*GORMStorage) LoadAllStates

func (s *GORMStorage) LoadAllStates() ([]State, error)

func (*GORMStorage) LoadState

func (s *GORMStorage) LoadState(name string, id string) (State, error)

func (*GORMStorage) Lock

func (s *GORMStorage) Lock()

func (*GORMStorage) SaveStates

func (s *GORMStorage) SaveStates(states ...State) error

func (*GORMStorage) StorageName

func (s *GORMStorage) StorageName() string

func (*GORMStorage) StorageType

func (s *GORMStorage) StorageType() string

func (*GORMStorage) Unlock

func (s *GORMStorage) Unlock()

type GORMStorageFactory

type GORMStorageFactory struct {
	locker.SyncLockerGenerator
	// contains filtered or unexported fields
}

func NewGORMStorageFactory

func NewGORMStorageFactory(db *gorm.DB, registry Registry, lockerGenerator locker.SyncLockerGenerator, newSnapshot func(storageFactory StorageFactory) StorageSnapshot) *GORMStorageFactory

func (*GORMStorageFactory) GetOrCreateStorage

func (f *GORMStorageFactory) GetOrCreateStorage(name string) (Storage, error)

type GormModel

type GormModel struct {
	ID        string `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

func (*GormModel) FillID

func (m *GormModel) FillID(state State) error

type IDMarshaler

type IDMarshaler interface {
	MarshalStateID(fields ...any) (string, error)
	UnmarshalStateID(ID string, fields ...any) error
}

type JsonIDMarshaler

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

func NewJsonIDMarshaler

func NewJsonIDMarshaler(separator string) *JsonIDMarshaler

func (*JsonIDMarshaler) MarshalStateID

func (c *JsonIDMarshaler) MarshalStateID(fields ...any) (string, error)

func (*JsonIDMarshaler) UnmarshalStateID

func (c *JsonIDMarshaler) UnmarshalStateID(ID string, fields ...any) error

type MemoryStorage

type MemoryStorage struct {
	StorageSnapshot

	States map[string]map[string]State
	// contains filtered or unexported fields
}

func NewMemoryStorage

func NewMemoryStorage(lockerGenerator locker.SyncLockerGenerator, registry Registry, snapshot StorageSnapshot, name string) (*MemoryStorage, error)

func (*MemoryStorage) ClearAllStates

func (s *MemoryStorage) ClearAllStates() error

func (*MemoryStorage) ClearStates

func (s *MemoryStorage) ClearStates(states ...State) error

func (*MemoryStorage) GetStateIDs

func (s *MemoryStorage) GetStateIDs(name string) ([]string, error)

func (*MemoryStorage) GetStateNames

func (s *MemoryStorage) GetStateNames() ([]string, error)

func (*MemoryStorage) LoadAllStates

func (s *MemoryStorage) LoadAllStates() ([]State, error)

func (*MemoryStorage) LoadState

func (s *MemoryStorage) LoadState(name string, id string) (State, error)

func (*MemoryStorage) Lock

func (s *MemoryStorage) Lock()

func (*MemoryStorage) SaveStates

func (s *MemoryStorage) SaveStates(states ...State) error

func (*MemoryStorage) StorageName

func (s *MemoryStorage) StorageName() string

func (*MemoryStorage) StorageType

func (s *MemoryStorage) StorageType() string

func (*MemoryStorage) Unlock

func (s *MemoryStorage) Unlock()

type MemoryStorageFactory

type MemoryStorageFactory struct {
	locker.SyncLockerGenerator
	// contains filtered or unexported fields
}

func NewMemoryStorageFactory

func NewMemoryStorageFactory(registry Registry, lockerGenerator locker.SyncLockerGenerator, newSnapshot func(storageFactory StorageFactory) StorageSnapshot) *MemoryStorageFactory

func (*MemoryStorageFactory) GetOrCreateStorage

func (f *MemoryStorageFactory) GetOrCreateStorage(name string) (Storage, error)

type RedisStorage

type RedisStorage struct {
	StorageSnapshot
	// contains filtered or unexported fields
}

func NewRedisStorage

func NewRedisStorage(lockerGenerator locker.SyncLockerGenerator, redisClient *redis.Client, registry Registry, snapshot StorageSnapshot, partition string) (*RedisStorage, error)

func (*RedisStorage) ClearAllStates

func (s *RedisStorage) ClearAllStates() error

func (*RedisStorage) ClearStates

func (s *RedisStorage) ClearStates(states ...State) (err error)

func (*RedisStorage) GetStateIDs

func (s *RedisStorage) GetStateIDs(name string) ([]string, error)

func (*RedisStorage) GetStateNames

func (s *RedisStorage) GetStateNames() ([]string, error)

func (*RedisStorage) LoadAllStates

func (s *RedisStorage) LoadAllStates() ([]State, error)

func (*RedisStorage) LoadState

func (s *RedisStorage) LoadState(name string, id string) (State, error)

func (*RedisStorage) Lock

func (s *RedisStorage) Lock()

func (*RedisStorage) SaveStates

func (s *RedisStorage) SaveStates(states ...State) error

func (*RedisStorage) StorageName

func (s *RedisStorage) StorageName() string

func (*RedisStorage) StorageType

func (s *RedisStorage) StorageType() string

func (*RedisStorage) Unlock

func (s *RedisStorage) Unlock()

type RedisStorageFactory

type RedisStorageFactory struct {
	locker.SyncLockerGenerator
	// contains filtered or unexported fields
}

func NewRedisStorageFactory

func NewRedisStorageFactory(redisClient *redis.Client, registry Registry, lockerGenerator locker.SyncLockerGenerator, newSnapshot func(storageFactory StorageFactory) StorageSnapshot) *RedisStorageFactory

func (*RedisStorageFactory) GetOrCreateStorage

func (f *RedisStorageFactory) GetOrCreateStorage(name string) (Storage, error)

type Registry

type Registry interface {
	RegisterState(state State) error
	NewState(name string) (State, error)
}

type SimpleRegistry

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

func GetSimpleStateRegistry

func GetSimpleStateRegistry() *SimpleRegistry

func NewSimpleRegistry

func NewSimpleRegistry() *SimpleRegistry

func (*SimpleRegistry) NewState

func (s *SimpleRegistry) NewState(name string) (State, error)

func (*SimpleRegistry) RegisterState

func (s *SimpleRegistry) RegisterState(state State) error

type SimpleStorageSnapshot

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

func NewSimpleStorageSnapshot

func NewSimpleStorageSnapshot(registry Registry, storageFactory StorageFactory, lockerGenerator locker.SyncLockerGenerator) *SimpleStorageSnapshot

func (*SimpleStorageSnapshot) ClearSnapshots

func (s *SimpleStorageSnapshot) ClearSnapshots() (err error)

func (*SimpleStorageSnapshot) DeleteSnapshot

func (s *SimpleStorageSnapshot) DeleteSnapshot(snapshotID string) (err error)

func (*SimpleStorageSnapshot) GetSnapshot

func (s *SimpleStorageSnapshot) GetSnapshot(snapshotID string) (storage Storage, err error)

func (*SimpleStorageSnapshot) GetSnapshotIDs

func (s *SimpleStorageSnapshot) GetSnapshotIDs() (snapshotIDs []string, err error)

func (*SimpleStorageSnapshot) GetStorageForSnapshot

func (s *SimpleStorageSnapshot) GetStorageForSnapshot() (storage Storage)

func (*SimpleStorageSnapshot) RevertStatesToSnapshot

func (s *SimpleStorageSnapshot) RevertStatesToSnapshot(snapshotID string) (err error)

func (*SimpleStorageSnapshot) SetStorageForSnapshot

func (s *SimpleStorageSnapshot) SetStorageForSnapshot(storage Storage)

func (*SimpleStorageSnapshot) SnapshotStates

func (s *SimpleStorageSnapshot) SnapshotStates() (snapshotID string, err error)

type SnapshotState

type SnapshotState struct {
	GormModel
	BaseState

	SnapshotID string `gorm:"not null;uniqueIndex"`
}

func MustNewSnapshotState

func MustNewSnapshotState(lockerGenerator locker.SyncLockerGenerator, snapshotId string) *SnapshotState

func (*SnapshotState) StateIDComponents

func (u *SnapshotState) StateIDComponents() StateIDComponents

type State

type State interface {
	sync.Locker
	GetLocker() sync.Locker
	GetLockerGenerator() locker.SyncLockerGenerator

	StateName() string

	GetIDMarshaler() IDMarshaler
	StateIDComponents() StateIDComponents

	// To avoid use it outside of initialization.
	Initialize(generator locker.SyncLockerGenerator, stateName string, idMarshaler IDMarshaler, idComponents StateIDComponents) error
}

State is a state interface. It aims to provide a simple way to work with state. What does it mean? If a record has a unique identifier, and it can be generated through mulitple other fields. e.g, a user id can be generated through user name, location, etc., then we can use it as a state id. It's very simular to multiple primary keys in SQL databases.

type StateContainer

type StateContainer[T State] struct {
	// contains filtered or unexported fields
}

StateContainer is helpful to work with state. It's a wrapper for state, that provides some useful methods to simplify work with state no matter what storage you are using. It uses `Finalizer` to finalize state into persist storage to speed up your application even distributed system.

func NewStateContainer

func NewStateContainer[T State](finalizer Finalizer, state T) *StateContainer[T]

func (*StateContainer[T]) Delete added in v0.2.4

func (s *StateContainer[T]) Delete() error

func (*StateContainer[T]) DeleteCache added in v0.2.4

func (s *StateContainer[T]) DeleteCache() error

func (*StateContainer[T]) Get

func (s *StateContainer[T]) Get() (T, error)

func (*StateContainer[T]) GetAndLock added in v0.3.0

func (s *StateContainer[T]) GetAndLock() (T, error)

func (*StateContainer[T]) GetFromPersist added in v0.3.0

func (s *StateContainer[T]) GetFromPersist() (T, error)

func (*StateContainer[T]) GetLocker added in v0.3.0

func (s *StateContainer[T]) GetLocker() (sync.Locker, error)

func (*StateContainer[T]) Save

func (s *StateContainer[T]) Save() error

func (*StateContainer[T]) StateID added in v0.3.0

func (s *StateContainer[T]) StateID() (stateID string, err error)

func (*StateContainer[T]) Unwrap

func (s *StateContainer[T]) Unwrap() T

func (*StateContainer[T]) Wrap

func (s *StateContainer[T]) Wrap(state T) *StateContainer[T]

type StateIDComponents added in v0.3.0

type StateIDComponents []any

type StateManagement

type StateManagement struct {
	GormModel
	BaseState

	StateNamee string `gorm:"not null; uniqueIndex:statename_stateid_partition"`
	StateID    string `gorm:"not null; uniqueIndex:statename_stateid_partition"`
	Partition  string `gorm:"not null; uniqueIndex:statename_stateid_partition"`
}

func MustNewStateManagement added in v0.2.1

func MustNewStateManagement(lockerGenerator locker.SyncLockerGenerator, stateName, stateID, partition string) *StateManagement

func (*StateManagement) StateIDComponents added in v0.2.1

func (u *StateManagement) StateIDComponents() StateIDComponents

type Storage

type Storage interface {
	// Please use locker to protect the storage in concurrent
	sync.Locker

	StorageSnapshot

	// Used for distinguishing different storages implementations.
	StorageType() string

	// Used for distinguishing different storages.
	StorageName() string

	LoadState(name string, id string) (State, error)
	LoadAllStates() ([]State, error)
	SaveStates(states ...State) error
	ClearStates(states ...State) error
	ClearAllStates() error

	GetStateIDs(name string) ([]string, error)
	GetStateNames() ([]string, error)
}

type StorageFactory

type StorageFactory interface {
	locker.SyncLockerGenerator

	GetOrCreateStorage(name string) (Storage, error)
}

type StorageSnapshot

type StorageSnapshot interface {
	SetStorageForSnapshot(storage Storage)
	GetStorageForSnapshot() (storage Storage)

	SnapshotStates() (snapshotID string, err error)
	RevertStatesToSnapshot(snapshotID string) (err error)
	GetSnapshot(snapshotID string) (storage Storage, err error)
	GetSnapshotIDs() (snapshotIDs []string, err error)
	DeleteSnapshot(snapshotID string) (err error)
	ClearSnapshots() (err error)
}

type StorageSnapshotWithMetrics

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

func NewStorageSnapshotWithMetrics

func NewStorageSnapshotWithMetrics(snapshot StorageSnapshot) *StorageSnapshotWithMetrics

func (StorageSnapshotWithMetrics) ClearSnapshots

func (s StorageSnapshotWithMetrics) ClearSnapshots() (err error)

func (StorageSnapshotWithMetrics) DeleteSnapshot

func (s StorageSnapshotWithMetrics) DeleteSnapshot(snapshotID string) (err error)

func (StorageSnapshotWithMetrics) GetSnapshot

func (s StorageSnapshotWithMetrics) GetSnapshot(snapshotID string) (storage Storage, err error)

func (StorageSnapshotWithMetrics) GetSnapshotIDs

func (s StorageSnapshotWithMetrics) GetSnapshotIDs() (snapshotIDs []string, err error)

func (StorageSnapshotWithMetrics) GetStorageForSnapshot

func (s StorageSnapshotWithMetrics) GetStorageForSnapshot() (storage Storage)

func (StorageSnapshotWithMetrics) RevertStatesToSnapshot

func (s StorageSnapshotWithMetrics) RevertStatesToSnapshot(snapshotID string) (err error)

func (StorageSnapshotWithMetrics) SetStorageForSnapshot

func (s StorageSnapshotWithMetrics) SetStorageForSnapshot(storage Storage)

func (StorageSnapshotWithMetrics) SnapshotStates

func (s StorageSnapshotWithMetrics) SnapshotStates() (snapshotID string, err error)

type StorageWithMetrics

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

It's a wrapper for Storage with Prometheus metrics

func NewStorageWithMetrics

func NewStorageWithMetrics(storage Storage) StorageWithMetrics

func (StorageWithMetrics) ClearAllStates

func (s StorageWithMetrics) ClearAllStates() (err error)

func (StorageWithMetrics) ClearSnapshots

func (s StorageWithMetrics) ClearSnapshots() (err error)

func (StorageWithMetrics) ClearStates

func (s StorageWithMetrics) ClearStates(states ...State) (err error)

func (StorageWithMetrics) DeleteSnapshot

func (s StorageWithMetrics) DeleteSnapshot(snapshotID string) (err error)

func (StorageWithMetrics) GetSnapshot

func (s StorageWithMetrics) GetSnapshot(snapshotID string) (storage Storage, err error)

func (StorageWithMetrics) GetSnapshotIDs

func (s StorageWithMetrics) GetSnapshotIDs() (snapshotIDs []string, err error)

func (StorageWithMetrics) GetStateIDs

func (s StorageWithMetrics) GetStateIDs(name string) (ids []string, err error)

func (StorageWithMetrics) GetStateNames

func (s StorageWithMetrics) GetStateNames() (names []string, err error)

func (StorageWithMetrics) GetStorageForSnapshot

func (s StorageWithMetrics) GetStorageForSnapshot() (storage Storage)

func (StorageWithMetrics) LoadAllStates

func (s StorageWithMetrics) LoadAllStates() (state []State, err error)

func (StorageWithMetrics) LoadState

func (s StorageWithMetrics) LoadState(name string, id string) (state State, err error)

func (StorageWithMetrics) Lock

func (s StorageWithMetrics) Lock()

func (StorageWithMetrics) RevertStatesToSnapshot

func (s StorageWithMetrics) RevertStatesToSnapshot(snapshotID string) (err error)

func (StorageWithMetrics) SaveStates

func (s StorageWithMetrics) SaveStates(states ...State) (err error)

func (StorageWithMetrics) SetStorageForSnapshot

func (s StorageWithMetrics) SetStorageForSnapshot(storage Storage)

func (StorageWithMetrics) SnapshotStates

func (s StorageWithMetrics) SnapshotStates() (snapshotID string, err error)

func (StorageWithMetrics) StorageName

func (s StorageWithMetrics) StorageName() string

func (StorageWithMetrics) StorageType

func (s StorageWithMetrics) StorageType() string

func (StorageWithMetrics) Unlock

func (s StorageWithMetrics) Unlock()

Jump to

Keyboard shortcuts

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