postgres

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 39 Imported by: 1

Documentation

Overview

Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0

Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0

Index

Constants

View Source
const (

	// MaxLockBatchCandidates is the largest batch LockBatch accepts. Each candidate binds its
	// (tx_id, idx) pair, so the statement carries 2n+lockBatchFixedParameters parameters and
	// the protocol bound above caps n here.
	//
	// It is exported because it is a precondition on the caller, not an internal detail: a
	// caller that needs more tokens than this must claim them in several windows, which is
	// what the selector's own, far smaller, window cap does (sherdlock.maxBatchWindow). A
	// batch over the bound is refused with a clear error rather than passed to the driver,
	// where it surfaces as an opaque protocol failure - and, on the selector's store-error
	// path, as a refetch that rebuilds the same oversized window and burns the retry budget.
	MaxLockBatchCandidates = (maxBindParameters - lockBatchFixedParameters) / 2
)

Variables

View Source
var ErrNotifierClosed = errors.New("notifier is closed")

ErrNotifierClosed is returned when the notifier has been closed and can no longer accept subscriptions or report listener errors.

Functions

func NewAdvisoryLockFactoryForID added in v0.21.0

func NewAdvisoryLockFactoryForID(lockID int64) func(context.Context, common5.WriteDB) (tokensdriver.RecoveryLeadership, bool, error)

NewAdvisoryLockFactoryForID returns a recovery leader factory bound to lockID. Binding the id at construction keeps it out of the call path, so no caller has to know how a lock id is made unique and none can accidentally pass one that is shared with another TMS.

func NewCleanupLeaderFactoryForID added in v0.21.0

func NewCleanupLeaderFactoryForID(lockID int64) func(context.Context, common5.WriteDB) (tokensdriver.CleanupLeadership, bool, error)

NewCleanupLeaderFactoryForID returns a cleanup leader factory bound to lockID, so the id is fixed when the store is built rather than supplied per call.

func NewConditionInterpreter

func NewConditionInterpreter() *interpreter

func NewNamedDriver

func NewNamedDriver(config driver3.Config, dbProvider fscPostgres.DbProvider) driver3.NamedDriver

NewNamedDriver returns a NamedDriver for Postgres.

func NewPaginationInterpreter

func NewPaginationInterpreter() common.PagInterpreter

Types

type AdvisoryLock

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

AdvisoryLock implements RecoveryLeadership using PostgreSQL advisory locks. Advisory locks are session-scoped and automatically released when the connection closes.

func NewAdvisoryLock

func NewAdvisoryLock(ctx context.Context, db common5.WriteDB, lockID int64) (*AdvisoryLock, bool, error)

NewAdvisoryLock attempts to acquire a PostgreSQL advisory lock for the given lockID. Returns (lock, true, nil) if the lock was acquired successfully. Returns (nil, false, nil) if the lock is held by another session. Returns (nil, false, error) if an error occurred during acquisition.

The lock is session-scoped and will be automatically released when: - Close() is called explicitly - The connection is closed - The process terminates

db is taken as a common5.WriteDB rather than a *sql.DB so that a backend may decorate its write path; only Conn is used here, and the connection it returns is held for the lock's lifetime.

func (*AdvisoryLock) Close

func (l *AdvisoryLock) Close() error

Close releases the advisory lock and closes the connection. It is safe to call Close multiple times.

type AuditTransactionStore

type AuditTransactionStore struct {
	*sqlcommon.TransactionStore
	// contains filtered or unexported fields
}

AuditTransactionStore wraps common.TransactionStore to add advisory lock to schema creation

func NewAuditTransactionStore

func NewAuditTransactionStore(dbs *scommon.RWDB, tableNames sqlcommon.TableNames) (*AuditTransactionStore, error)

NewAuditTransactionStore creates a new AuditTransactionStore.

func (*AuditTransactionStore) AcquireLeadership added in v0.24.0

func (s *AuditTransactionStore) AcquireLeadership(ctx context.Context, lockID int64) (tokensdriver.RecoveryLeadership, bool, error)

AcquireLeadership overrides the base no-op to take a real PostgreSQL advisory lock for lockID, independent of the recovery lock bound at construction.

func (*AuditTransactionStore) ClaimPendingTransactions added in v0.21.0

ClaimPendingTransactions atomically claims a batch of pending audit transactions.

Without this override the store inherits the permissive SELECT from sqlcommon.TransactionStore, which persists no claim at all: every replica selects the same pending rows on every sweep and processes all of them. The owner path has had the atomic claim since it was introduced; audit was simply never given it.

func (*AuditTransactionStore) CreateSchema

func (s *AuditTransactionStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*AuditTransactionStore) GetSchema

func (s *AuditTransactionStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

func (*AuditTransactionStore) ReleaseRecoveryClaim added in v0.21.0

func (s *AuditTransactionStore) ReleaseRecoveryClaim(ctx context.Context, txID string, owner string, _ string) error

ReleaseRecoveryClaim releases the recovery claim on an audit transaction. The inherited implementation is a no-op, which would leave the claim set until its lease expired even after the sweep finished with the row.

Deliberately drops the caller's message instead of forwarding it to status_message. recoverTransaction (recovery/manager.go) always releases with a claim-bookkeeping message ("recovered successfully", "recovery failed: %v"), not the tx's actual audit-trail reason: applyFinalityLogic (ttx/finality/recovery.go) already writes that reason via SetStatus *before* this release runs whenever a row's status genuinely changes, and returns nil without touching status for a still-Pending tx (Busy/Unknown), a case recoverTransaction cannot tell apart from success. Forwarding message here would overwrite a real ledger rejection reason with "recovered successfully", and would do so on every still-pending row on every sweep, exactly what an audit trail must not do. The owner path keeps forwarding the bookkeeping message; that behavior predates this PR and is unchanged here.

func (*AuditTransactionStore) WriteDB

func (s *AuditTransactionStore) WriteDB() *sql.DB

WriteDB returns the underlying write *sql.DB. Used by the auditor distributed locker to share the connection pool.

type Driver

type Driver struct {

	// Lazy providers for various store types to ensure they are initialized only when needed.
	TokenLock lazy.Provider[fscPostgres.Config, *TokenLockStore]
	Wallet    lazy.Provider[fscPostgres.Config, *WalletStore]
	Identity  lazy.Provider[fscPostgres.Config, *IdentityStore]
	Token     lazy.Provider[fscPostgres.Config, *TokenStore]
	AuditTx   lazy.Provider[fscPostgres.Config, *AuditTransactionStore]
	OwnerTx   lazy.Provider[fscPostgres.Config, *TransactionStore]
	Endorser  lazy.Provider[fscPostgres.Config, *EndorserStore]
	KeyStore  lazy.Provider[fscPostgres.Config, *KeystoreStore]
	// contains filtered or unexported fields
}

Driver implements the token storage driver for Postgres.

func NewDriver

func NewDriver(config driver3.Config) *Driver

NewDriver returns a new Driver for Postgres using the default database provider.

func NewDriverWithDbProvider

func NewDriverWithDbProvider(config driver3.Config, dbProvider fscPostgres.DbProvider) *Driver

NewDriverWithDbProvider returns a new Driver for Postgres using the given database provider.

func (*Driver) NewAuditTransaction

func (d *Driver) NewAuditTransaction(name driver2.PersistenceName, params ...string) (driver3.AuditTransactionStore, error)

NewAuditTransaction returns a new AuditTransactionStore.

func (*Driver) NewEndorser

func (d *Driver) NewEndorser(name driver2.PersistenceName, params ...string) (driver3.EndorserStore, error)

NewEndorser returns a new EndorserStore.

func (*Driver) NewIdentity

func (d *Driver) NewIdentity(name driver2.PersistenceName, params ...string) (driver3.IdentityStore, error)

NewIdentity returns a new IdentityStore.

func (*Driver) NewKeyStore

func (d *Driver) NewKeyStore(name driver2.PersistenceName, params ...string) (driver3.KeyStore, error)

NewKeyStore returns a new KeyStoreStore.

func (*Driver) NewOwnerTransaction

func (d *Driver) NewOwnerTransaction(name driver2.PersistenceName, params ...string) (driver3.TokenTransactionStore, error)

NewOwnerTransaction returns a new TokenTransactionStore.

func (*Driver) NewToken

func (d *Driver) NewToken(name driver2.PersistenceName, params ...string) (driver3.TokenStore, error)

NewToken returns a new TokenStore.

func (*Driver) NewTokenLock

func (d *Driver) NewTokenLock(name driver2.PersistenceName, params ...string) (driver3.TokenLockStore, error)

NewTokenLock returns a new TokenLockStore.

func (*Driver) NewWallet

func (d *Driver) NewWallet(name driver2.PersistenceName, params ...string) (driver3.WalletStore, error)

NewWallet returns a new WalletStore.

type EndorserStore

type EndorserStore struct {
	*common3.EndorserStore
	// contains filtered or unexported fields
}

EndorserStore wraps common.EndorserStore to add advisory lock to schema creation

func NewEndorserStore

func NewEndorserStore(dbs *scommon.RWDB, tables common3.TableNames) (*EndorserStore, error)

NewEndorserStore creates a new EndorserStore for Postgres

func (*EndorserStore) CreateSchema

func (s *EndorserStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*EndorserStore) GetSchema

func (s *EndorserStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

type IdentityNotifier

type IdentityNotifier struct {
	*Notifier
}

IdentityNotifier handles notifications for identity configurations.

func NewIdentityNotifier

func NewIdentityNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, dataSource string) (*IdentityNotifier, error)

NewIdentityNotifier returns a new IdentityNotifier for the given RWDB and table names.

func (*IdentityNotifier) Subscribe

Subscribe registers a callback function to be called when an identity configuration is inserted or updated.

type IdentityStore

type IdentityStore struct {
	*sqlcommon.IdentityStore
	// contains filtered or unexported fields
}

IdentityStore wraps common.IdentityStore to add advisory lock to schema creation

func NewIdentityStore

func NewIdentityStore(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, dataSource string) (*IdentityStore, error)

NewIdentityStore creates a new IdentityStore with advisory lock support

func (*IdentityStore) CreateSchema

func (s *IdentityStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*IdentityStore) GetSchema

func (s *IdentityStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

type KeystoreStore

type KeystoreStore struct {
	*common3.KeystoreStore
	// contains filtered or unexported fields
}

KeystoreStore wraps common.KeystoreStore to add advisory lock to schema creation

func NewKeystoreStore

func NewKeystoreStore(dbs *common2.RWDB, tableNames common3.TableNames) (*KeystoreStore, error)

NewKeystoreStore returns a new KeystoreStore for the given RWDB and table names.

func (*KeystoreStore) CreateSchema

func (s *KeystoreStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*KeystoreStore) GetSchema

func (s *KeystoreStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

type Notifier

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

Notifier implements a simple subscription API to listen for updates on a database table.

Delivery is serialised per table: one goroutine owns the LISTEN connection, reads notifications one at a time and invokes every subscriber inline (see dispatch). A subscriber that blocks therefore stalls delivery of every later notification on this table's channel, for every subscriber. Callbacks must be fast and non-blocking - hand slow work to a goroutine or a queue. See Subscribe for the full contract.

Notifications are also not a durable log. Postgres queues them only for sessions that are already LISTENing and never replays them, so anything emitted before the listener registers, or while a dropped connection is being re-established, is lost. Subscribe closes the first window by waiting for LISTEN; TransportError reports the second. A subscriber that needs to be correct rather than merely prompt must therefore treat a notification as a hint to re-read the table, not as the record of what changed.

func NewNotifier

func NewNotifier(
	writeDB *sql.DB,
	table, dataSource string,
	notifyOperations []driver.Operation,
	primaryKeys ...PrimaryKey,
) *Notifier

NewNotifier returns a new Notifier for the given RWDB and table names.

func (*Notifier) Close

func (db *Notifier) Close() error

Close stops the listener and cleans up resources.

func (*Notifier) CreateSchema

func (db *Notifier) CreateSchema() error

CreateSchema creates the notification objects in the database. It returns an error if the schema creation fails.

func (*Notifier) GetSchema

func (db *Notifier) GetSchema() string

GetSchema returns the SQL schema for creating the notification objects in the database.

func (*Notifier) ListenerError

func (db *Notifier) ListenerError() <-chan error

ListenerError returns a channel that receives errors from the listener. The caller should consume this channel to detect listener failures.

func (*Notifier) Subscribe

func (db *Notifier) Subscribe(callback driver.TriggerCallback) error

Subscribe registers a callback function to be called when a matching database event occurs. It returns an error if the notifier is closed or if the listener fails to start.

The callback is invoked synchronously on the single listener goroutine that owns this table's LISTEN connection, so it must not block: while it runs, no other notification on the channel is delivered, to this or any other subscriber. It must not call back into the notifier either (Subscribe, Close and UnsubscribeAll all take the same lock the dispatch loop reads under). Anything slow - a query, an RPC, a lock - belongs on a goroutine or a queue the callback only hands work to.

The first Subscribe installs the notification trigger and then waits, up to listenReadyTimeout, for the listener to register LISTEN. When that wait succeeds, a row written after Subscribe returns is guaranteed to reach the callback; previously the call returned after a fixed 100ms that had nothing to do with whether the channel was live, so a write racing startup was silently dropped.

A nil return does not by itself carry that guarantee. If the wait times out, the timeout is logged and Subscribe still returns nil - the listener keeps retrying, and failing the subscription would be worse than a late one - so a row written while LISTEN is still unregistered is dropped by Postgres, which queues notifications only for sessions already listening. Rows written between the trigger's installation and the point where the listener is ready - by another process, or by this one before Subscribe returns - are not replayed.

func (*Notifier) TransportError added in v0.18.0

func (db *Notifier) TransportError() <-chan error

TransportError returns a channel that receives non-fatal transport errors reported by the underlying listener (e.g. a dropped LISTEN connection that is being reconnected). The notifier keeps running across such errors; the channel merely lets a consumer learn that notifications may have been missed during the outage. Sends are best-effort and buffered depth 1, so only the most recent error is retained when the channel is not being drained.

func (*Notifier) UnsubscribeAll

func (db *Notifier) UnsubscribeAll() error

UnsubscribeAll removes all subscribers.

type PrimaryKey

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

PrimaryKey represents a primary key column with its value decoder

func NewSimplePrimaryKey

func NewSimplePrimaryKey(name driver.ColumnKey) *PrimaryKey

type TokenLockStore

type TokenLockStore struct {
	*common5.TokenLockStore
	// contains filtered or unexported fields
}

TokenLockStore implements the token lock storage for Postgres.

func NewTokenLockStore

func NewTokenLockStore(dbs *common2.RWDB, tableNames common5.TableNames) (*TokenLockStore, error)

NewTokenLockStore returns a new TokenLockStore for the given RWDB and table names, using the default (insert) lock strategy.

func (*TokenLockStore) AcquireCleanupLeadership added in v0.17.0

func (db *TokenLockStore) AcquireCleanupLeadership(ctx context.Context) (driver.CleanupLeadership, bool, error)

AcquireCleanupLeadership attempts to acquire a Postgres advisory lock so only one replica runs Cleanup per tick for this TMS; others skip the tick and release no held resources, so contention is limited to the acquire attempt itself. The lock id and factory are fixed at construction. Note the winner holds a dedicated connection off writeDB for the tick's duration (see NewAdvisoryLock). This lock, the recovery lock and the keystore-cleanup lock are all now derived per TMS rather than shared node-wide, and FSC keys connection pools by data source alone, so TMSes sharing one database share one *sql.DB. A node running N such TMSes can therefore have up to 3N connections pinned by lock holders at once, before counting the further connections winners need for their own work (e.g. recovery's ClaimPendingTransactions). Size writeDB's maxOpenConns against that floor, not a single spare connection. See #1798.

func (*TokenLockStore) Cleanup

func (db *TokenLockStore) Cleanup(ctx context.Context, leaseExpiry time.Duration) error

Cleanup removes stale token locks that have expired. The deletion itself is the backend-independent one implemented by the embedded store; Postgres only adds the logging of the rows that are about to go.

func (*TokenLockStore) CreateSchema

func (s *TokenLockStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*TokenLockStore) GetSchema

func (s *TokenLockStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

func (*TokenLockStore) Lock added in v0.26.0

func (db *TokenLockStore) Lock(ctx context.Context, tokenID *token.ID, consumerTxID transaction.ID, walletID string) error

Lock locks the token for consumerTxID, using the configured strategy. The default (LockStrategyInsert) delegates unchanged to the embedded store: an INSERT that surfaces a lost race as a unique-constraint violation. LockStrategyOnConflict and LockStrategySkipLocked both use INSERT ... ON CONFLICT DO NOTHING RETURNING instead: a lost race is a normal zero-row result, not a server-side error. Note this overrides Lock, not LockAt: the embedded TokenLockStore.Lock calls LockAt on itself, not on this type (Go has no virtual dispatch), so overriding LockAt here would never be reached from callers that go through Lock.

func (*TokenLockStore) LockBatch added in v0.26.0

func (db *TokenLockStore) LockBatch(ctx context.Context, tokenIDs []*token.ID, consumerTxID transaction.ID, _ string) (driver.BatchLockOutcome, error)

LockBatch attempts to lock, in a single round trip, every token in tokenIDs on behalf of consumerTxID, and reports per candidate how the claim resolved: the tokens it won, and those it refused because they are no longer spendable (driver.BatchLockOutcome). It never claims a token outside tokenIDs, so callers remain responsible for supplying only candidates that are already known to be spendable: LockBatch itself applies no eligibility predicate beyond "not already locked". At most MaxLockBatchCandidates may be claimed per call, since the claim is one statement and the wire protocol bounds how many parameters one carries; a longer batch is refused outright, with nothing locked. Under LockStrategySkipLocked the claim uses FOR UPDATE SKIP LOCKED on the underlying Tokens rows, so a caller walks past rows a concurrent claimant is already processing instead of colliding with them; under LockStrategyOnConflict and LockStrategyInsert it issues the same multi-row INSERT ... ON CONFLICT DO NOTHING as tryInsertOnConflict's single-token callers, for the whole window in one round trip - only the plain LockStrategyInsert single-token Lock path (which must surface a lost race as a unique-constraint violation, not a zero-row result) does not go through this.

func (*TokenLockStore) RoundTrips added in v0.26.0

func (db *TokenLockStore) RoundTrips() int64

RoundTrips returns the number of Lock and LockBatch calls issued against this store instance since construction. Instrumentation only, for benchmarking Phase 6's lock strategies; not part of the driver.TokenLockStore contract.

func (*TokenLockStore) UniqueViolations added in v0.26.0

func (db *TokenLockStore) UniqueViolations() int64

UniqueViolations returns the number of Lock calls that observed a real server-side unique-constraint violation, as opposed to a clean zero-row result - only possible under LockStrategyInsert. Instrumentation only, for benchmarking Phase 6's lock strategies; not part of the driver.TokenLockStore contract.

type TokenNotifier

type TokenNotifier struct {
	*Notifier
}

TokenNotifier handles notifications for tokens.

func NewTokenNotifier

func NewTokenNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, dataSource string) (*TokenNotifier, error)

NewTokenNotifier returns a new TokenNotifier for the given RWDB and table names.

func (*TokenNotifier) Subscribe

Subscribe registers a callback function to be called when a token is inserted, updated, or deleted.

type TokenStore

type TokenStore struct {
	*sqlcommon.TokenStore
	// contains filtered or unexported fields
}

TokenStore wraps common.TokenStore to add advisory lock to schema creation

func NewTokenStoreWithNotifier

func NewTokenStoreWithNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, notifier *TokenNotifier) (*TokenStore, error)

func (*TokenStore) CreateSchema

func (s *TokenStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*TokenStore) GetSchema

func (s *TokenStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with the advisory locks covering every table the schema creates: this store's own, and the TokenLocks table it also emits.

type TransactionNotifier

type TransactionNotifier struct {
	*Notifier
}

TransactionNotifier handles notifications for transaction status changes.

func NewTransactionNotifier

func NewTransactionNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, dataSource string) (*TransactionNotifier, error)

NewTransactionNotifier returns a new TransactionNotifier for the given RWDB and table names.

func (*TransactionNotifier) Subscribe

Subscribe registers a callback function to be called when a transaction request status is updated.

type TransactionStore

type TransactionStore struct {
	*sqlcommon.TransactionStore
	// contains filtered or unexported fields
}

TransactionStore extends the common TransactionStore with PostgreSQL-specific atomic claim operations.

func NewTransactionStoreWithNotifier

func NewTransactionStoreWithNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, notifier *TransactionNotifier) (*TransactionStore, error)

NewTransactionStoreWithNotifier creates a new TransactionStore with the provided notifier and recovery support.

func (*TransactionStore) AcquireLeadership added in v0.24.0

func (s *TransactionStore) AcquireLeadership(ctx context.Context, lockID int64) (tokensdriver.RecoveryLeadership, bool, error)

AcquireLeadership overrides the base no-op to take a real PostgreSQL advisory lock for lockID, independent of the recovery lock bound at construction.

func (*TransactionStore) ClaimPendingTransactions

func (db *TransactionStore) ClaimPendingTransactions(ctx context.Context, params tokensdriver.RecoveryClaimParams) ([]*tokensdriver.RecoveryClaim, error)

ClaimPendingTransactions atomically claims a batch of pending transactions.

func (*TransactionStore) CleanupExpiredClaims

func (db *TransactionStore) CleanupExpiredClaims(ctx context.Context) (int, error)

CleanupExpiredClaims removes expired recovery claims. Returns the number of claims cleaned up.

func (*TransactionStore) CreateSchema

func (s *TransactionStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*TransactionStore) GetSchema

func (s *TransactionStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

func (*TransactionStore) ReleaseRecoveryClaim

func (db *TransactionStore) ReleaseRecoveryClaim(ctx context.Context, txID string, owner string, message string) error

ReleaseRecoveryClaim releases the recovery claim on a transaction.

type WalletStore

type WalletStore struct {
	*common3.WalletStore
	// contains filtered or unexported fields
}

WalletStore wraps common.WalletStore to add advisory lock to schema creation

func NewWalletStore

func NewWalletStore(dbs *common2.RWDB, tableNames common3.TableNames) (*WalletStore, error)

NewWalletStore returns a new WalletStore for the given RWDB and table names.

func (*WalletStore) CreateSchema

func (s *WalletStore) CreateSchema() error

CreateSchema overrides the base CreateSchema to ensure GetSchema is called on the correct receiver

func (*WalletStore) GetSchema

func (s *WalletStore) GetSchema() string

GetSchema overrides the base GetSchema to prefix with advisory lock

Jump to

Keyboard shortcuts

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