s3-orchestrator

tickrunner

import "github.com/afreidah/s3-orchestrator/internal/lifecycle/tickrunner"

Index

Constants

MsgPassFailed / MsgPassCompleted / MsgQuotaMetricsRefreshFailed are the canonical terminal-event log messages shared by the periodic “pass” workers (rebalance, replication, over-replication). Held as constants so the three nearly-identical work closures cannot drift.

const (
    MsgPassFailed                = "pass failed"
    MsgPassCompleted             = "pass completed"
    MsgQuotaMetricsRefreshFailed = "quota metrics refresh failed after pass"
)

func ComponentLogger

func ComponentLogger(slug string) *slog.Logger

ComponentLogger returns the canonical scoped logger every Service uses, derived from the snake_case slug so the component attr is the single source of truth for log filtering. Callers typically pass the same slug as Service.Name.

func HandlePassResult

func HandlePassResult(ctx context.Context, log *slog.Logger, manager QuotaMetricsRefresher, count int, err error, countKey string) error

HandlePassResult is the shared post-call handling for the three nearly-identical “pass” workers (rebalance, over-replication, replication). Each one returns (count, err) from a worker call and then either: surfaces non-DB errors as tick failures (so health reporting sees them), or - when work was done - logs a completion message with a work-specific count key and refreshes quota metrics so the dashboard reflects the move. Centralised so the three closures cannot drift, and so coverage of the error and count>0 branches lands in one place.

type AdvisoryLocker

AdvisoryLocker is the consumer-defined slice of the metadata store a Service needs: one TryAdvisoryLock call per tick. The concrete metadata store satisfies this implicitly.

type AdvisoryLocker interface {
    WithAdvisoryLock(ctx context.Context, lockID int64, fn func(ctx context.Context) error) (bool, error)
}

type Config

Config bundles the inputs needed to construct a Service. Held as a struct so callers can pin a few fields and leave the rest at their zero values rather than threading a long argument list. ShouldRun, Startup, and OnError are optional; Work and Name are required.

type Config struct {
    // Locker acquires the advisory lock around each tick.
    Locker AdvisoryLocker
    // Interval is the period between ticks (post-jitter).
    Interval time.Duration
    // LockID is the PostgreSQL advisory lock identifier; defined in
    // internal/store/core (LockRebalancer, LockReplicator, etc.).
    LockID int64
    // Name is the canonical snake_case component slug; both the
    // metrics label and the scoped logger's component attribute use it.
    Name string
    // Log is the scoped logger. Pass ComponentLogger(Name) for the
    // default contract.
    Log *slog.Logger

    // ShouldRun gates each tick; return false to skip the tick without
    // recording it as a failure (used by services whose config can
    // disable them at runtime, e.g. rebalance enabled=false).
    ShouldRun func() bool
    // Startup runs once at Run() entry before the first ticker fires;
    // useful for replication's "kick off a pass immediately on boot."
    Startup func(ctx context.Context) error
    // Work is the per-tick function; required.
    Work func(ctx context.Context) error
    // OnError overrides the default tick-failure logging path (the
    // default writes a slog.Error). Used by the lifecycle service to
    // also bump a per-service error metric.
    OnError func(err error)
}

type QuotaMetricsRefresher

QuotaMetricsRefresher is the single-method subset of *proxy.BackendManager that HandlePassResult calls to push fresh quota gauges after a successful worker pass. Lives here so the worker packages can take it as a typed dep without importing DI.

type QuotaMetricsRefresher interface {
    UpdateQuotaMetrics(ctx context.Context) error
}

type Service

Service runs a function on a fixed interval under an advisory lock. Handles audit context creation, lock acquisition, skip/error logging, and context cancellation. The component identity lives on the scoped logger (component attr) rather than in message text, so logs from every service share the same shape and operators filter by attribute.

Per-service health state (lastSuccess, lastFailure, lastError, consecutiveFailures) is recorded after each tick so operators can query worker liveness through the admin endpoint and alert on staleness through Prometheus.

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

func New

func New(cfg Config) *Service

New constructs a Service from cfg. Required fields (Locker, Interval, LockID, Name, Log, Work) must be non-nil/positive; the constructor trusts the caller to supply them.

func (*Service) Health

func (s *Service) Health() lifecycle.WorkerHealth

Health implements lifecycle.HealthReporter. Returns a snapshot of the service’s last tick outcomes plus its registered name so the admin endpoint can render a per-service status table.

func (*Service) Interval

func (s *Service) Interval() time.Duration

Interval returns the configured tick period. Exposed so DI invariant tests can pin “no interval is zero or pathologically large.”

func (*Service) LockID

func (s *Service) LockID() int64

LockID returns the advisory-lock identifier this service holds per tick. Exposed so DI invariant tests can pin “no two services share a lock ID.”

func (*Service) Name

func (s *Service) Name() string

Name returns the snake_case component slug the service was constructed with. Exposed for tests that validate cross-service invariants (unique lock IDs, sane intervals) and for any future admin tooling that wants per-service identification.

func (*Service) Run

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

Run implements lifecycle.Runner with a jittered first tick to prevent thundering herd on the advisory lock at startup.

func (*Service) Tick

func (s *Service) Tick(ctx context.Context)

Tick drives a single iteration of the work function under the same lock + audit + health-recording path Run uses. Exposed for tests that want to verify per-tick behaviour without spinning the full ticker loop, and for any future admin endpoint that wants to force a tick.

Generated by gomarkdoc