common

package
v1.4.8 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2026 License: UPL-1.0 Imports: 12 Imported by: 0

Documentation

Overview

Package common provides common utilities used for NoSQL client.

Package common provides common utilities used for NoSQL client.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EnableInstanceMetadataServiceLookup added in v1.4.8

func EnableInstanceMetadataServiceLookup()

EnableInstanceMetadataServiceLookup provides the interface to lookup IMDS region info

Types

type InternalRequestData added in v1.2.2

type InternalRequestData struct {
	RateLimiterPair
	// contains filtered or unexported fields
}

InternalRequestData is the actual struct that gets included in every Request type

func (*InternalRequestData) GetRetryTime added in v1.2.2

func (ird *InternalRequestData) GetRetryTime() time.Duration

GetRetryTime returns the current time spent in the client in retries

func (*InternalRequestData) GetTopoSeqNum added in v1.4.3

func (ird *InternalRequestData) GetTopoSeqNum() int

func (*InternalRequestData) GetTopologyInfo added in v1.4.3

func (ird *InternalRequestData) GetTopologyInfo() *TopologyInfo

GetTopologyInfo returns the entire topology info

func (*InternalRequestData) SetRetryTime added in v1.2.2

func (ird *InternalRequestData) SetRetryTime(d time.Duration)

SetRetryTime sets the current time spent in the client in retries

func (*InternalRequestData) SetTopology added in v1.4.3

func (ird *InternalRequestData) SetTopology(ti *TopologyInfo)

SetTopologyInfo sets the topology info used for the query

type InternalRequestDataInt added in v1.2.2

type InternalRequestDataInt interface {
	RateLimiterPairInt
	GetRetryTime() time.Duration
	SetRetryTime(d time.Duration)
	SetTopology(ti *TopologyInfo)
	GetTopoSeqNum() int
}

InternalRequestDataInt is used to give all requests a set of common internal data (rate limiters, retry stats, etc)

type InternalResultData added in v1.4.3

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

InternalResultData is the actual struct that gets included in every Result type

func (*InternalResultData) GetTopologyInfo added in v1.4.3

func (ird *InternalResultData) GetTopologyInfo() *TopologyInfo

GetTopologyInfo returns the entire topology info

func (*InternalResultData) SetTopology added in v1.4.3

func (ird *InternalResultData) SetTopology(ti *TopologyInfo)

SetTopologyInfo sets the topology info used for the query

func (*InternalResultData) SetTopologyOrErr added in v1.4.3

func (ird *InternalResultData) SetTopologyOrErr(ti *TopologyInfo, err error) error

SetTopologyOrErr sets the topology info used for the query, or returns err if that was set

type InternalResultDataInt added in v1.4.3

type InternalResultDataInt interface {
	SetTopology(ti *TopologyInfo)
	GetTopologyInfo() *TopologyInfo
	GetTopoSeqNum() int
}

InternalResultDataInt is used to give all requests a set of common internal data (rate limiters, retry stats, etc)

type RateLimiter added in v1.2.2

type RateLimiter interface {

	// ConsumeUnits consumes a number of units, blocking until the units are available.
	// It returns the amount of time blocked. If not blocked, 0 is returned.
	ConsumeUnits(units int64) time.Duration

	// TryConsumeUnits consumes the specified number of units if they can be returned
	// immediately without waiting.
	// It returns true if the units were consumed, false if they were not immediately available.
	// If units was zero, true return means the limiter is currently below its limit.
	// units can be zero to poll if the limiter is currently over its limit. Pass negative units to
	// "give back" units (same as calling ConsumeUnits with a negative value).
	TryConsumeUnits(units int64) bool

	// ConsumeUnitsWithTimeout attempts to consume a number of units, blocking until the units are
	// available or the specified timeout expires. It returns the amount of time blocked.
	// If not blocked, 0 is returned.
	//
	// units can be a negative value to "give back" units.
	// Pass a timeout value of 0 to block indefinitely. To poll if the limiter is
	// currently over its limit, use TryConsumeUnits() instead.
	// If alwaysConsume is true, consume units even on timeout.
	ConsumeUnitsWithTimeout(units int64, timeout time.Duration, alwaysConsume bool) (time.Duration, error)

	// GetLimitPerSecond returns the number of units configured for this rate limiter instance
	// (the max number of units per second this limiter allows).
	GetLimitPerSecond() float64

	// SetDuration sets the duration for this rate limiter instance.
	// The duration specifies how far back in time the limiter will
	// go to consume previously unused units.
	//
	// For example, if a limiter had a limit of 1000 units and a
	// duration of 5 seconds, and had no units consumed for at least
	// 5 seconds, a call to TryConsumeUnits(5000) will succeed
	// immediately with no waiting.
	SetDuration(durationSecs float64)

	// GetDuration returns the duration in seconds configured for this rate limiter instance
	GetDuration() float64

	// Reset resets the rate limiter as if it was newly constructed.
	Reset()

	// SetLimitPerSecond sets a new limit (units per second) on the limiter.
	//
	// Note that implementing rate limiters should fully support non-integer
	// (float64) values internally, to avoid issues when the limits are set
	// very low.
	// Changing the limit may lead to unexpected spiky behavior, and may
	// affect other goroutines currently operating on the same limiter instance.
	SetLimitPerSecond(rateLimitPerSecond float64)

	// ConsumeUnitsUnconditionally consumes units without checking or waiting.
	// the internal amount of units consumed will be updated by the given
	// amount, regardless of its current over/under limit state.
	// The number of units to consume may be negative to "give back" units.
	ConsumeUnitsUnconditionally(units int64)

	// GetCurrentRate returns the current rate as a percentage of current limit.
	GetCurrentRate() float64

	// SetCurrentRate sets the current rate as a percentage of current limit.
	// This modifies the internal limiter values; it does not modify
	// the rate limit.
	// rateToSet may be greater than 100.0 to set the limiter to "over its limit".
	SetCurrentRate(rateToSet float64)
}

RateLimiter interface provides default methods that all rate limiters must implement.

Thread safety

It is expected that all implementing classes of this interface may be used by multiple goroutines concurrently. For example, many goroutines may be using the same rate limiter instance to ensure that all of them together do not exceed a given limit.

Typical usage

The simplest use of the rate limiter is to consume a number of units, blocking until they are successfully consumed:

delay := rateLimiter.ConsumeUnits(units)
// delay indicates how long the consume delayed

To poll a limiter to see if it is currently over the limit:

if rateLimiter.TryConsumeUnits(0) {
  // limiter is below its limit
}

To attempt to consume units, only if they can be immediately consumed without waiting:

if ratelimiter.TryConsumeUnits(units) {
  // successful consume
} else {
  // could not consume units without waiting
}

Usages with timeouts

In cases where the number of units an operation will consume is already known before the operation, a simple one-shot method can be used:

var units int64 = (known units the operation will use)
alwaysConsume := false // don't consume if we time out
delay, err := rateLimiter.ConsumeUnitsWithTimeout(units, timeout, alwaysConsume)
if err != nil {
  // we could not do the operation within the given timeframe.
  ...skip operation...
} else {
  // we waited <delay> for the units to be consumed, and the consume was successful
  ...do operation...
}

In cases where the number of units an operation will consume is not known before the operation, typically two rate limiter calls would be used: one to wait till the limiter is below its limit, and a second to update the limiter with used units:

// wait until we're under the limit
delay, err := rateLimiter.ConsumeUnitsWithTimeout(0, timeout, false)
if err != nil {
  // we could not do the operation within the given timeframe.
  ...skip operation...
}
// we waited <delay> to be under the limit, and were successful
var units int64  = (...do operation, get number of units used...)
// update rate limiter with consumed units. Next operation on this
// limiter may delay due to it being over its limit.
rateLimiter.ConsumeUnitsUnconditionally(units)

Alternately, the operation could be always performed, and then the limiter could try to wait for the units to be consumed:

var units int64  = (...do operation, get number of units used...)
alwaysConsume := true // consume, even if we time out
delay, err := rateLimiter.ConsumeUnitsWithTimeout(units, timeout, alwaysConsume)
if err != nil {
  // the rate limiter could not consume the units and go
  // below the limit in the given timeframe. Units are
  // consumed anyway, and the next call to this limiter
  // will likely delay
} else {
  // we waited <delay> for the units to be consumed, and the
  // consume was successful
}

Limiter duration

Implementing rate limiters should support a configurable "duration". This is sometimes referred to as a "burst mode", or a "window time", or "burst duration".

This will allow consumes of units that were not consumed in the recent past. For example, if a limiter allows for 100 units per second, and is not used for 5 seconds, it should allow an immediate consume of 500 units with no delay upon a consume call, assuming that the limiter's duration is set to at least 5 seconds.

The maximum length of time for this duration should be configurable. In all cases a limiter should set a reasonable minimum duration, such that a call to TryConsumeUnits(1) has a chance of succeeding. It is up to the limiter implementation to determine if units from the past before the limiter was created or reset are available for use.

type RateLimiterPair added in v1.2.2

type RateLimiterPair struct {
	ReadLimiter  RateLimiter
	WriteLimiter RateLimiter
}

RateLimiterPair is the actual struct that gets included in every Request type

func (*RateLimiterPair) GetReadRateLimiter added in v1.2.2

func (rlp *RateLimiterPair) GetReadRateLimiter() RateLimiter

GetReadRateLimiter returns the read limiter for a request note this may be nil

func (*RateLimiterPair) GetWriteRateLimiter added in v1.2.2

func (rlp *RateLimiterPair) GetWriteRateLimiter() RateLimiter

GetWriteRateLimiter returns the write limiter for a request note this may be nil

func (*RateLimiterPair) SetReadRateLimiter added in v1.2.2

func (rlp *RateLimiterPair) SetReadRateLimiter(rl RateLimiter)

SetReadRateLimiter sets a read rate limiter instance to use during request execution.

func (*RateLimiterPair) SetWriteRateLimiter added in v1.2.2

func (rlp *RateLimiterPair) SetWriteRateLimiter(rl RateLimiter)

SetWriteRateLimiter sets a write rate limiter instance to use during request execution.

type RateLimiterPairInt added in v1.2.2

type RateLimiterPairInt interface {
	GetReadRateLimiter() RateLimiter
	GetWriteRateLimiter() RateLimiter
	SetReadRateLimiter(rl RateLimiter)
	SetWriteRateLimiter(rl RateLimiter)
}

RateLimiterPairInt interface is used in serializer.go to make all Requests include a rate limiter pair

type Region

type Region string

Region type for OCI regions.

const (
	RegionAfJohannesburg1 Region = "af-johannesburg-1"
	RegionApBatam1        Region = "ap-batam-1"
	RegionApChennai1      Region = "ap-chennai-1"
	RegionApChuncheon1    Region = "ap-chuncheon-1"
	RegionApHyderabad1    Region = "ap-hyderabad-1"
	RegionApKulai1        Region = "ap-kulai-1"
	RegionApMelbourne1    Region = "ap-melbourne-1"
	RegionApMumbai1       Region = "ap-mumbai-1"
	RegionApOsaka1        Region = "ap-osaka-1"
	RegionApSeoul1        Region = "ap-seoul-1"
	RegionApSingapore1    Region = "ap-singapore-1"
	RegionApSingapore2    Region = "ap-singapore-2"
	RegionApSydney1       Region = "ap-sydney-1"
	RegionApTokyo1        Region = "ap-tokyo-1"
	RegionCaMontreal1     Region = "ca-montreal-1"
	RegionCaToronto1      Region = "ca-toronto-1"
	RegionEuAmsterdam1    Region = "eu-amsterdam-1"
	RegionEuDublin3       Region = "eu-dublin-3"
	RegionEuFrankfurt1    Region = "eu-frankfurt-1"
	RegionEuMadrid1       Region = "eu-madrid-1"
	RegionEuMadrid3       Region = "eu-madrid-3"
	RegionEuMarseille1    Region = "eu-marseille-1"
	RegionEuMilan1        Region = "eu-milan-1"
	RegionEuParis1        Region = "eu-paris-1"
	RegionEuStockholm1    Region = "eu-stockholm-1"
	RegionEuTurin1        Region = "eu-turin-1"
	RegionEuZurich1       Region = "eu-zurich-1"
	RegionIlJerusalem1    Region = "il-jerusalem-1"
	RegionMeAbudhabi1     Region = "me-abudhabi-1"
	RegionMeDubai1        Region = "me-dubai-1"
	RegionMeJeddah1       Region = "me-jeddah-1"
	RegionMeRiyadh1       Region = "me-riyadh-1"
	RegionMxMonterrey1    Region = "mx-monterrey-1"
	RegionMxQueretaro1    Region = "mx-queretaro-1"
	RegionSaBogota1       Region = "sa-bogota-1"
	RegionSaSantiago1     Region = "sa-santiago-1"
	RegionSaSaopaulo1     Region = "sa-saopaulo-1"
	RegionSaValparaiso1   Region = "sa-valparaiso-1"
	RegionSaVinhedo1      Region = "sa-vinhedo-1"
	RegionUkLondon1       Region = "uk-london-1"
	RegionUkCardiff1      Region = "uk-cardiff-1"
	RegionUsAbilene1      Region = "us-abilene-1"
	RegionUsColumbus1     Region = "us-columbus-1"
	RegionUsQuincy1       Region = "us-quincy-1"
	RegionUsBoardman1     Region = "us-boardman-1"
	RegionUsDallas1       Region = "us-dallas-1"
	RegionUsDesmoines1    Region = "us-desmoines-1"
	RegionUsPhoenix1      Region = "us-phoenix-1"
	RegionUsAshburn1      Region = "us-ashburn-1"
	RegionUsSaltlake2     Region = "us-saltlake-2"
	RegionUsSanjose1      Region = "us-sanjose-1"
	RegionUsShawnee1      Region = "us-shawnee-1"
	RegionUsChicago1      Region = "us-chicago-1"
	RegionApKulai2        Region = "ap-kulai-2"
	RegionApDelhi1        Region = "ap-delhi-1"
	RegionUsLangley1      Region = "us-langley-1"
	RegionUsLuke1         Region = "us-luke-1"
	RegionUsTacoma1       Region = "us-tacoma-1"
	RegionApChiyoda1      Region = "ap-chiyoda-1"
	RegionApIbaraki1      Region = "ap-ibaraki-1"
	RegionMeIbri1         Region = "me-ibri-1"
	RegionUsWestjordan1   Region = "us-westjordan-1"
	RegionEuFrankfurt2    Region = "eu-frankfurt-2"
	RegionEuMadrid2       Region = "eu-madrid-2"
	RegionEuJovanovac1    Region = "eu-jovanovac-1"
	RegionEuMilan2        Region = "eu-milan-2"
	RegionUsSomerset1     Region = "us-somerset-1"
	RegionUsThames1       Region = "us-thames-1"
	RegionEuCrissier1     Region = "eu-crissier-1"
	RegionMeAbudhabi3     Region = "me-abudhabi-3"
	RegionMeAlain1        Region = "me-alain-1"
	RegionMeAbudhabi2     Region = "me-abudhabi-2"
	RegionMeAbudhabi4     Region = "me-abudhabi-4"
	RegionApHobsonville1  Region = "ap-hobsonville-1"
	RegionApSilverdale1   Region = "ap-silverdale-1"
	RegionApSuwon1        Region = "ap-suwon-1"
	RegionApSeoul2        Region = "ap-seoul-2"
	RegionApChuncheon2    Region = "ap-chuncheon-2"
	RegionMeRiyadh2       Region = "me-riyadh-2"
	RegionUsTukwila3      Region = "us-tukwila-3"
	RegionApOsaka2        Region = "ap-osaka-2"
	RegionApTatebayashi1  Region = "ap-tatebayashi-1"
	RegionMeDubai3        Region = "me-dubai-3"
	RegionUsNewark1       Region = "us-newark-1"
	RegionApPathumthani1  Region = "ap-pathumthani-1"
	RegionApSamutprakan1  Region = "ap-samutprakan-1"
	RegionUkLondon2       Region = "uk-london-2"
	RegionUkLondon3       Region = "uk-london-3"
	RegionUkLondon4       Region = "uk-london-4"
	RegionEuBudapest1     Region = "eu-budapest-1"
	RegionSaRiodejaneiro1 Region = "sa-riodejaneiro-1"
	RegionApWesttokyo1    Region = "ap-westtokyo-1"
	RegionMeAbudhabi5     Region = "me-abudhabi-5"
	RegionMeDubai4        Region = "me-dubai-4"
	RegionUsGovAshburn1   Region = "us-gov-ashburn-1"
	RegionUsGovChicago1   Region = "us-gov-chicago-1"
	RegionUsGovPhoenix1   Region = "us-gov-phoenix-1"
	RegionUkGovLondon1    Region = "uk-gov-london-1"
	RegionUkGovCardiff1   Region = "uk-gov-cardiff-1"
	RegionMeDccMuscat1    Region = "me-dcc-muscat-1"
	RegionApDccCanberra1  Region = "ap-dcc-canberra-1"
	RegionEuDccDublin1    Region = "eu-dcc-dublin-1"
	RegionEuDccDublin2    Region = "eu-dcc-dublin-2"
	RegionEuDccMilan1     Region = "eu-dcc-milan-1"
	RegionEuDccMilan2     Region = "eu-dcc-milan-2"
	RegionEuDccRating1    Region = "eu-dcc-rating-1"
	RegionEuDccRating2    Region = "eu-dcc-rating-2"
	RegionApDccGazipur1   Region = "ap-dcc-gazipur-1"
	RegionUsDccPhoenix1   Region = "us-dcc-phoenix-1"
	RegionUsDccPhoenix2   Region = "us-dcc-phoenix-2"
	RegionUsDccPhoenix4   Region = "us-dcc-phoenix-4"
	RegionMeDccDoha1      Region = "me-dcc-doha-1"
	RegionEuDccRome1      Region = "eu-dcc-rome-1"
	RegionEuDccZurich1    Region = "eu-dcc-zurich-1"
	RegionApDccOsaka1     Region = "ap-dcc-osaka-1"
	RegionApDccTokyo1     Region = "ap-dcc-tokyo-1"
	RegionUsDccSwjordan1  Region = "us-dcc-swjordan-1"
	RegionUsDccSwjordan2  Region = "us-dcc-swjordan-2"
)

func StringToRegion

func StringToRegion(regionKeyOrID string) (r Region, err error)

StringToRegion converts a string that represents the region key or region identifier to Region type. The supported region keys and region identifiers are available at https://docs.cloud.oracle.com/iaas/Content/General/Concepts/regions.htm.

func (Region) Endpoint

func (region Region) Endpoint() (string, error)

Endpoint returns the NoSQL service endpoint for the region. An error is returned if region is invalid.

func (Region) EndpointForService

func (region Region) EndpointForService(service string) (string, error)

EndpointForService returns an endpoint for a service. An error is returned if region is invalid.

type SimpleRateLimiter added in v1.2.2

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

SimpleRateLimiter is an implementation of RateLimiter interface. Despite its name, it also has methods for percentage-based operations as well as the methods in RateLimiter. Currently the percentage-based methods are only used internally.

func NewSimpleRateLimiter added in v1.2.2

func NewSimpleRateLimiter(rateLimitPerSec float64) (srl *SimpleRateLimiter)

NewSimpleRateLimiter Creates a simple time-based rate limiter.

This limiter will allow for one second of duration; that is, it will allow unused units from within the last second to be used.

func NewSimpleRateLimiterWithDuration added in v1.2.2

func NewSimpleRateLimiterWithDuration(rateLimitPerSec float64, durationSecs float64) (srl *SimpleRateLimiter)

NewSimpleRateLimiterWithDuration creates a simple time-based rate limiter with a specified duration.

func (*SimpleRateLimiter) ConsumeUnits added in v1.2.2

func (srl *SimpleRateLimiter) ConsumeUnits(units int64) time.Duration

ConsumeUnits consumes a number of units, blocking until the units are available. It returns the amount of time blocked. If not blocked, 0 is returned.

func (*SimpleRateLimiter) ConsumeUnitsUnconditionally added in v1.2.2

func (srl *SimpleRateLimiter) ConsumeUnitsUnconditionally(units int64)

ConsumeUnitsUnconditionally consumes units without checking or waiting. The internal amount of units consumed will be updated by the given amount, regardless of its current over/under limit state. The number of units to consume may be negative to "give back" units.

func (*SimpleRateLimiter) ConsumeUnitsWithTimeout added in v1.2.2

func (srl *SimpleRateLimiter) ConsumeUnitsWithTimeout(units int64, timeout time.Duration, alwaysConsume bool) (time.Duration, error)

ConsumeUnitsWithTimeout attempts to consume a number of units, blocking until the units are available or the specified timeout expires. It returns the amount of time blocked. If not blocked, 0 is returned.

units can be a negative value to "give back" units. Pass a timeout value of 0 to block indefinitely. To poll if the limiter is currently over its limit, use TryConsumeUnits() instead. If alwaysConsume is true, consume units even on timeout.

func (*SimpleRateLimiter) GetCurrentRate added in v1.2.2

func (srl *SimpleRateLimiter) GetCurrentRate() float64

GetCurrentRate returns the current rate as a percentage of current limit (0.0 - 100.0).

func (*SimpleRateLimiter) GetDuration added in v1.2.2

func (srl *SimpleRateLimiter) GetDuration() float64

GetDuration returns the duration in seconds configured for this rate limiter instance

func (*SimpleRateLimiter) GetLimitPerSecond added in v1.2.2

func (srl *SimpleRateLimiter) GetLimitPerSecond() float64

GetLimitPerSecond returns the number of units configured for this rate limiter instance (the max number of units per second this limiter allows).

func (*SimpleRateLimiter) Reset added in v1.2.2

func (srl *SimpleRateLimiter) Reset()

Reset resets the rate limiter as if it was newly constructed.

func (*SimpleRateLimiter) SetCurrentRate added in v1.2.2

func (srl *SimpleRateLimiter) SetCurrentRate(percent float64)

SetCurrentRate sets the current rate as a percentage of current limit. This modifies the internal limiter values; it does not modify the rate limit. rateToSet may be greater than 100.0 to set the limiter to "over its limit".

func (*SimpleRateLimiter) SetDuration added in v1.2.2

func (srl *SimpleRateLimiter) SetDuration(durationSecs float64)

SetDuration sets the duration for this rate limiter instance. The duration specifies how far back in time the limiter will go to consume previously unused units.

For example, if a limiter had a limit of 1000 units and a duration of 5 seconds, and had no units consumed for at least 5 seconds, a call to TryConsumeUnits(5000) will succeed immediately with no waiting.

func (*SimpleRateLimiter) SetLimitPerSecond added in v1.2.2

func (srl *SimpleRateLimiter) SetLimitPerSecond(rateLimitPerSec float64)

SetLimitPerSecond sets a new limit (units per second) on the limiter. Changing the limit may lead to unexpected spiky behavior, and may affect other goroutines currently operating on the same limiter instance.

func (*SimpleRateLimiter) String added in v1.2.2

func (srl *SimpleRateLimiter) String() string

func (*SimpleRateLimiter) TryConsumeUnits added in v1.2.2

func (srl *SimpleRateLimiter) TryConsumeUnits(units int64) bool

TryConsumeUnits consumes the specified number of units if they can be returned immediately without waiting. It returns true if the units were consumed, false if they were not immediately available. If units was zero, true return means the limiter is currently below its limit. units can be zero to poll if the limiter is currently over its limit. Pass negative units to "give back" units (same as calling ConsumeUnits with a negative value).

type TopologyInfo added in v1.4.3

type TopologyInfo struct {
	// seqNum represents the sequence number of the topology.
	SeqNum int

	// shardIDs specifies a slice of int values that represent the shard IDs.
	ShardIDs []int
}

TopologyInfo represents the NoSQL database topology information required for execution.

func (*TopologyInfo) Equals added in v1.4.3

func (ti *TopologyInfo) Equals(otherTopo *TopologyInfo) bool

equals checks if ti equals to the specified otherTopo.

func (*TopologyInfo) GetLastShardID added in v1.4.3

func (ti *TopologyInfo) GetLastShardID() int

GetLastShardID returns the last shard in the ShardIDs array

func (*TopologyInfo) GetNumShards added in v1.4.3

func (ti *TopologyInfo) GetNumShards() int

GetNumShards returns the number of shards.

func (*TopologyInfo) GetShardID added in v1.4.3

func (ti *TopologyInfo) GetShardID(index int) int

GetShardID returns the shard id associated with the specified index.

func (*TopologyInfo) GetTopologyInfo added in v1.4.3

func (ti *TopologyInfo) GetTopologyInfo() *TopologyInfo

GetTopologyInfo returns the entire topology info

Jump to

Keyboard shortcuts

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