repository

package
v0.0.0-...-8e9e2d9 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Provide

func Provide(
	cfg Config, lc fx.Lifecycle, urldb *URLDB, tele telemetry.Telemetery, logger *zap.Logger,
) urlrepo.Repository

Provide assembles the repository the service talks to.

The database sits at the bottom; visit buffering wraps it so that counting a redirect no longer writes; the cache wraps that so that a redirect usually does not read either. Both layers are decorators over the same interface, so switching either off in the configuration leaves a shorter stack and changes nothing else.

Types

type BufferedVisits

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

BufferedVisits accumulates visit counts in memory and writes them out in batches.

Recording a visit used to be an UPDATE against a single row on the redirect path, so the busiest links contended on their own row and every read cost a write. Here a visit only increments a counter under a mutex; a background goroutine folds the counters into one statement every FlushInterval, or sooner when too many keys have piled up.

The trade is deliberate: a crash loses at most one interval of counts, and a counter read straight from the database lags by up to one interval. Neither matters for a visit tally, and both buy the redirect path its write back.

func NewBufferedVisits

func NewBufferedVisits(
	cfg VisitsConfig, lc fx.Lifecycle, next urlrepo.Repository, tele telemetry.Telemetery, logger *zap.Logger,
) *BufferedVisits

func (*BufferedVisits) Create

func (r *BufferedVisits) Create(ctx context.Context, u url.URL) error

func (*BufferedVisits) Flush

func (r *BufferedVisits) Flush(ctx context.Context) error

Flush writes everything buffered so far. Counts are taken out of the buffer before the write and put back if it fails, so a database blip delays them rather than dropping them.

func (*BufferedVisits) FromShortURL

func (r *BufferedVisits) FromShortURL(ctx context.Context, key string) (url.URL, error)

func (*BufferedVisits) IncrementVisits

func (r *BufferedVisits) IncrementVisits(_ context.Context, key string) error

IncrementVisits records a visit in memory. It does not touch the database and so does not fail, which is why the redirect path can afford to call it on every request.

func (*BufferedVisits) IncrementVisitsBatch

func (r *BufferedVisits) IncrementVisitsBatch(ctx context.Context, deltas map[string]uint64) error

IncrementVisitsBatch passes through: it is the drain the buffer itself writes into.

func (*BufferedVisits) Pending

func (r *BufferedVisits) Pending() int

Pending reports how many keys are waiting to be written, for tests and for diagnostics.

func (*BufferedVisits) Update

func (r *BufferedVisits) Update(ctx context.Context, u url.URL) error

type CacheConfig

type CacheConfig struct {
	// Enabled turns the cache off entirely, which is useful for seeing what it buys.
	Enabled bool `json:"enabled" koanf:"enabled"`

	// Size is how many entries to keep before evicting the least recently used one.
	Size int `json:"size" koanf:"size"`

	// TTL bounds how long an entry is trusted. Entries are also capped by the expiry of the
	// url they hold, so a url can never outlive its expiry by sitting in the cache.
	TTL time.Duration `json:"ttl" koanf:"ttl"`

	// NegativeTTL is how long a miss is remembered. Keys are short enough to guess at, so
	// without this a scan for keys that do not exist reaches the database on every request.
	// Keep it small: until it lapses, an instance stays blind to a key another instance just
	// created.
	NegativeTTL time.Duration `json:"negative_ttl" koanf:"negative_ttl"`
}

CacheConfig tunes the read path.

A shortener serves far more redirects than it accepts creations, and what a redirect needs — the target of a key — never changes once written. That makes it about the most cacheable thing there is.

type Cached

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

Cached answers lookups from memory where it can.

It is a decorator: it satisfies the same repository interface as the layer beneath it and can be left out of the stack entirely without anything else noticing. Only reads are served from memory; writes always go through, and invalidate the key they touch.

The cache is per instance. Two consequences worth naming: a negative entry keeps one instance from seeing a key another instance just created until it lapses, and the visit counts it hands back are whatever they were when the entry was filled. Nothing serves those counts to a caller, but they should not be mistaken for current.

func NewCached

func NewCached(
	cfg CacheConfig, next urlrepo.Repository, tele telemetry.Telemetery, logger *zap.Logger,
) *Cached

func (*Cached) Create

func (r *Cached) Create(ctx context.Context, u url.URL) error

func (*Cached) FromShortURL

func (r *Cached) FromShortURL(ctx context.Context, key string) (url.URL, error)

func (*Cached) IncrementVisits

func (r *Cached) IncrementVisits(ctx context.Context, key string) error

IncrementVisits passes straight through. Visit counts are not what the cache is protecting, and the layer beneath is where they are folded together.

func (*Cached) IncrementVisitsBatch

func (r *Cached) IncrementVisitsBatch(ctx context.Context, deltas map[string]uint64) error

func (*Cached) Len

func (r *Cached) Len() int

Len reports how many entries are held, for tests and for diagnostics.

func (*Cached) Update

func (r *Cached) Update(ctx context.Context, u url.URL) error

type Config

type Config struct {
	Cache  CacheConfig  `json:"cache"  koanf:"cache"`
	Visits VisitsConfig `json:"visits" koanf:"visits"`
}

Config tunes the layers that sit between the service and the database.

type URLDB

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

func ProvideURLDB

func ProvideURLDB(db *db.DB, tele telemetry.Telemetery, logger *zap.Logger) *URLDB

func (*URLDB) Create

func (r *URLDB) Create(ctx context.Context, u url.URL) error

func (*URLDB) FromShortURL

func (r *URLDB) FromShortURL(ctx context.Context, key string) (url.URL, error)

func (*URLDB) IncrementVisits

func (r *URLDB) IncrementVisits(ctx context.Context, key string) error

func (*URLDB) IncrementVisitsBatch

func (r *URLDB) IncrementVisitsBatch(ctx context.Context, deltas map[string]uint64) error

func (*URLDB) Update

func (r *URLDB) Update(ctx context.Context, u url.URL) error

type VisitsConfig

type VisitsConfig struct {
	// Enabled turns buffering off, restoring one write per redirect.
	Enabled bool `json:"enabled" koanf:"enabled"`

	// FlushInterval is how long a counter may sit in memory before being written out. It also
	// bounds what a crash can lose.
	FlushInterval time.Duration `json:"flush_interval" koanf:"flush_interval"`

	// MaxBuffered is how many distinct keys may accumulate before a flush is triggered early,
	// so that a burst does not grow the buffer without bound.
	MaxBuffered int `json:"max_buffered" koanf:"max_buffered"`
}

VisitsConfig tunes visit counting.

Counting a visit synchronously turns every redirect into a write against a single row, which makes that row a lock hotspot for exactly the links that are most popular. Buffering trades an exact, instantly visible counter — which nobody needs — for one write per batch.

Jump to

Keyboard shortcuts

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