Help us shape the future of Go! Take the 2026 Go Developer Survey

replica

package
v0.56.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package replica is the orchestration layer that ties the storage ReplicatedDriver to the DB (failure rows, rules, settings) and to the notify subsystem (webhook + bell events).

The split is:

  • internal/storage/replicated.go — the wrapper Driver itself.
  • internal/storage/rules.go — the path-glob rule engine.
  • internal/server storage_cache.go — the resolver's cache, which wraps a storage linked to an enabled replication target.
  • this package — DB-backed adapter that implements storage.FailureRecorder + storage.EventNotifier and also runs the reconciliation queue handler, the initial copy and the cron status report.

Keeping the storage package pure of DB and notify imports lets us unit-test the wrapper in isolation against in-memory recorders.

Index

Constants

This section is empty.

Variables

View Source
var ErrFolderTaken = errors.New("replica: another storage on this target already uses that folder")

ErrFolderTaken is SetFolder's answer for a folder another storage on the same target already writes into.

View Source
var ErrNoReplica = errors.New("no replica configured")

ErrNoReplica is what the repair calls answer when no storage replicates at all (the admin endpoints turn it into 503 "no replica configured").

View Source
var ErrNotLinked = errors.New("replica: storage is not linked to an enabled replication target")

ErrNotLinked is what Wrappers answers for a storage that does not replicate: it has no replication target, its target is switched off or gone, or the storage itself is gone.

Functions

func EnsureFolder added in v0.53.0

func EnsureFolder(ctx context.Context, store db.Store, st *model.Storage, targetID int64) (string, error)

EnsureFolder returns the storage's folder on targetID, choosing and keeping one the first time: its name made safe (FolderName), or - when another storage on the same target already writes into that name - the name with the storage's id appended. A link kept for another target is replaced (the storage was relinked).

func FolderName added in v0.53.0

func FolderName(name string, storageID int64) string

FolderName makes a folder name every backend accepts out of a storage's name: letters (any script) and digits stay, a space, `-`, `_` and `.` stay, everything else - `/ \ : * ? " < > |`, control characters - becomes `-`; runs of `-` are one, the ends lose dots, spaces and dashes (a leading dot would hide it, a trailing one Windows drops), and it is at most 64 characters. A name that ends up empty, or that Windows reserves (CON, LPT1...), becomes storage-<id>.

func InitialCopyDedupKey added in v0.53.0

func InitialCopyDedupKey(storageID int64) string

InitialCopyDedupKey keeps at most one waiting slice per storage.

func RetryDedupKey added in v0.47.0

func RetryDedupKey(storageID int64, path, op string) string

RetryDedupKey coalesces the retries of one failure: while a retry of it is waiting in the queue, another request for it adds nothing.

⚠ Every press of "Repair all" queued a full set again. The list looks the same until a retry has run, which invites another press, and each one doubled the queue and told the bell "retries queued" once more.

Types

type CronScheduler

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

CronScheduler wraps robfig/cron with a Reload primitive so the admin UI can change the cron spec at runtime without a restart.

func NewCronScheduler

func NewCronScheduler(svc *Service) *CronScheduler

NewCronScheduler binds a scheduler to the replica Service.

func (*CronScheduler) Reload

func (c *CronScheduler) Reload(ctx context.Context) error

Reload re-reads the replica_settings row and reschedules. Empty cron spec or report_enabled=false removes any active schedule.

Safe to call concurrently; the mutex serializes spec swaps so the scheduler never has two entries for the same job.

func (*CronScheduler) Start

func (c *CronScheduler) Start()

Start launches the underlying cron loop. Must be called once before the first Reload — Reload is a no-op until Start.

func (*CronScheduler) Stop

func (c *CronScheduler) Stop()

Stop blocks until any in-flight job returns and the scheduler tear down completes.

type FailureRecorder

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

FailureRecorder is a DB-backed storage.FailureRecorder for ONE storage: every row it writes or resolves carries that storage's id (migration 00094).

func NewFailureRecorder

func NewFailureRecorder(store db.Store, storageID int64) *FailureRecorder

NewFailureRecorder binds the recorder to a store and a storage.

func (*FailureRecorder) Record

func (r *FailureRecorder) Record(ctx context.Context, path, op, errCode, errMsg string) error

Record persists or upserts a failure row.

func (*FailureRecorder) Resolve

func (r *FailureRecorder) Resolve(ctx context.Context, path, op string) error

Resolve flips the matching row's resolved_at.

type InitialCopyStatus added in v0.53.0

type InitialCopyStatus struct {
	*model.ReplicaInitialCopy
	StorageName string `json:"storage_name"`
	TargetName  string `json:"target_name"`
	// Done is Copied+Present+Excluded+Failed.
	Done int64 `json:"done"`
}

InitialCopyStatus is a storage's initial copy as the Replication page shows it: the row, with the storage's and the target's names.

type Link struct {
	StorageID int64
	TargetID  int64
	Driver    *storage.ReplicatedDriver
}

Link is a storage's live replication: the wrapper the running process writes through, and the target it fans out to.

type LinkStatus added in v0.53.0

type LinkStatus struct {
	*model.ReplicaLink
	StorageName string `json:"storage_name"`
	TargetName  string `json:"target_name"`
}

LinkStatus is a storage's link as the Replication page shows it.

type Notifier

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

Notifier is the storage.EventNotifier impl backed by notify.Service.

The service is read when an event is sent, not when the notifier is made: the server builds the storages' wrappers before its notification service exists, and a wrapper made then must still reach the bell.

func NewNotifier

func NewNotifier(svc notify.Service) *Notifier

NewNotifier wires the adapter to a service that already exists.

func NewNotifierFunc added in v0.53.0

func NewNotifierFunc(get func() notify.Service) *Notifier

NewNotifierFunc wires the adapter to a service read at send time.

func (*Notifier) ForStorage added in v0.53.0

func (n *Notifier) ForStorage(id int64, name string) *Notifier

ForStorage returns a copy that names the storage in what it sends.

func (*Notifier) NotifyPrimaryReadFail

func (n *Notifier) NotifyPrimaryReadFail(ctx context.Context, path string, err error)

NotifyPrimaryReadFail emits a primary_read_fail event when the read fallback served from replica.

func (*Notifier) NotifyReplicaFail

func (n *Notifier) NotifyReplicaFail(ctx context.Context, path, op string, err error, attempt int)

NotifyReplicaFail emits a replica_fail event.

type Reconciled added in v0.47.0

type Reconciled struct {
	Queued        int `json:"queued"`
	AlreadyQueued int `json:"already_queued"`
}

Reconciled is what one "Repair all" did: retries it queued, and retries it found already waiting in the queue and left alone.

type RulesReloader

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

RulesReloader is the helper an admin handler calls after rules CRUD.

func NewRulesEngine

func NewRulesEngine(store db.Store) (storage.RuleEngine, *RulesReloader)

NewRulesEngine wires storage.NewRulesEngine with a reload function that pulls from db.Store. Call .Reload() after admin CRUD ops.

func (*RulesReloader) Reload

func (r *RulesReloader) Reload(ctx context.Context) error

Reload pulls the latest rules + settings from DB and refreshes the engine's cache.

type Service

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

Service orchestrates reconciliation, the initial copy and the cron status report. Wires store, queue, notify and the live wrappers.

The queue handler for op type "replica_retry" should be Service. HandleRetry — the bootstrap registers it on the queue Pool; the initial copy's is HandleInitialCopy (TypeInitialCopy).

func New

func New(store db.Store, wrappers Wrappers, q queue.Driver, n notify.Service) *Service

New wires a Service. wrappers may be nil (tests of the report alone): then nothing can be repaired or copied and those calls answer ErrNoReplica.

func (*Service) FixOne

func (s *Service) FixOne(ctx context.Context, storageID int64, path, op string) (queued bool, err error)

FixOne enqueues a single retry for one (storage, path, op), unless one is waiting in the queue already (queued=false, no error).

func (*Service) GenerateReport

func (s *Service) GenerateReport(ctx context.Context) error

GenerateReport computes the singleton replica_status_reports row and sends the full event to the webhook (in-app gets a summary).

The full failed-paths list goes only to webhooks (the user posts it into their own system — F2/F3) — the in-app body keeps the message short so the bell doesn't drown in JSON.

func (*Service) HandleInitialCopy added in v0.53.0

func (s *Service) HandleInitialCopy(ctx context.Context, op queue.Op) error

HandleInitialCopy is the queue.Handler for TypeReplicaInitialCopy: one slice of one storage's initial copy.

func (*Service) HandleRetry

func (s *Service) HandleRetry(ctx context.Context, op queue.Op) error

HandleRetry is the queue.Handler for op type "replica_retry". The bootstrap calls Pool.Register(queue.TypeReplicaRetry, svc.HandleRetry).

The retry goes through the storage's LIVE wrapper (Wrappers), so it writes to the target the storage is linked to now. A storage that no longer replicates - unlinked, its target switched off or deleted, the storage gone - leaves nothing to repair: its failure is resolved and the op succeeds.

The handler is structured to never panic and always either:

  • succeed (success → Ack on the queue side)
  • return an error so the queue requeues with backoff

func (*Service) HasLinked added in v0.53.0

func (s *Service) HasLinked(ctx context.Context) (bool, error)

HasLinked reports whether any storage replicates. The repair endpoints answer 503 "no replica configured" only when none does.

func (*Service) InitialCopies added in v0.53.0

func (s *Service) InitialCopies(ctx context.Context) ([]InitialCopyStatus, error)

InitialCopies lists every storage's initial copy, for the page.

func (*Service) LinkChanged added in v0.53.0

func (s *Service) LinkChanged(ctx context.Context, storageID int64) error

LinkChanged brings the storage's initial copy in line with its link as the row says now: linked to an enabled target - a copy to that target (a new one when the target changed); not linked - none.

func (*Service) Linked added in v0.53.0

func (s *Service) Linked(ctx context.Context) ([]*model.Storage, error)

Linked returns the storages that replicate: linked to a replication target that exists and is enabled.

func (s *Service) Links(ctx context.Context) ([]LinkStatus, error)

Links lists every storage's folder on its target.

func (*Service) ReconcileAll

func (s *Service) ReconcileAll(ctx context.Context) (Reconciled, error)

ReconcileAll enqueues a replica_retry op for every unresolved failure currently recorded that has none waiting already.

func (*Service) RestartInitialCopy added in v0.53.0

func (s *Service) RestartInitialCopy(ctx context.Context, storageID int64) error

RestartInitialCopy is LinkChanged that begins the copy anew even when one to the same target finished: the target's configuration changed (another bucket, another share), or it was switched back on after the changes made while it was off went nowhere.

func (*Service) ResumeInitialCopies added in v0.53.0

func (s *Service) ResumeInitialCopies(ctx context.Context) error

ResumeInitialCopies runs once at start: every storage that replicates gets its initial copy - a new one when it has none (the upgrade from a version whose replication never ran: the copy runs ONCE, for links that never had one) - and every copy still under way gets its next slice queued. Rows of storages that no longer replicate are dropped.

func (*Service) SetClock added in v0.53.0

func (s *Service) SetClock(now func() time.Time)

SetClock replaces the clock the initial copy reads (tests).

func (*Service) SetFolder added in v0.53.0

func (s *Service) SetFolder(ctx context.Context, storageID int64, folder string) (*model.ReplicaLink, error)

SetFolder changes the storage's folder on its target to folder (made safe the way FolderName makes a name safe) and returns the one kept. Another storage on the same target writing into it is ErrFolderTaken. The caller rebuilds the storage's driver and begins its initial copy again; nothing is moved on the target.

func (*Service) SetSlice added in v0.53.0

func (s *Service) SetSlice(d time.Duration, files int)

SetSlice shortens or lengthens a slice (tests; files <= 0 means no file limit).

func (*Service) StartInitialCopy added in v0.53.0

func (s *Service) StartInitialCopy(ctx context.Context, storageID, targetID int64, restart bool) (*model.ReplicaInitialCopy, error)

StartInitialCopy makes sure the storage has an initial copy to targetID and queues its next slice. restart=false keeps a copy already there for that target (a finished one is not run again); restart=true begins it anew.

func (*Service) Stop

func (s *Service) Stop()

Stop signals the cron goroutine to exit.

type Wrappers added in v0.53.0

type Wrappers interface {
	// Replicated answers ErrNotLinked for a storage that does not replicate.
	Replicated(ctx context.Context, storageID int64) (Link, error)
}

Wrappers finds the live replication wrapper of a storage - the one the server's resolver hands every writer (internal/server storage_cache.go).

⚠ Repairs and the initial copy go through THIS, not through a wrapper of their own: the Service was once built with a nil wrapper ("v0.1 skips that"), so every Fix all answered "no replica configured" whatever was linked (#186).

Jump to

Keyboard shortcuts

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