2024-01-04 19:22:32 +03:00
|
|
|
package agdservice
|
2022-08-26 14:18:35 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-10-14 17:44:24 +03:00
|
|
|
"log/slog"
|
2022-08-26 14:18:35 +03:00
|
|
|
"time"
|
|
|
|
|
2024-01-04 19:22:32 +03:00
|
|
|
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
|
2024-10-14 17:44:24 +03:00
|
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
2024-03-11 12:21:07 +03:00
|
|
|
"github.com/AdguardTeam/golibs/service"
|
2024-10-14 17:44:24 +03:00
|
|
|
"github.com/AdguardTeam/golibs/timeutil"
|
2024-01-04 19:22:32 +03:00
|
|
|
"golang.org/x/exp/rand"
|
2022-08-26 14:18:35 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// Refresher is the interface for entities that can update themselves.
|
|
|
|
type Refresher interface {
|
2024-06-07 14:27:46 +03:00
|
|
|
// Refresh is called by a [RefreshWorker]. The error returned by Refresh is
|
|
|
|
// only returned from [RefreshWorker.Shutdown] and only when
|
|
|
|
// [RefreshWorkerConfig.RefreshOnShutdown] is true. In all other cases, the
|
|
|
|
// error is ignored, and refreshers must handle error reporting themselves.
|
2022-08-26 14:18:35 +03:00
|
|
|
Refresh(ctx context.Context) (err error)
|
|
|
|
}
|
|
|
|
|
2024-12-05 14:19:25 +03:00
|
|
|
// RefresherFunc is an adapter to allow the use of ordinary functions as
|
|
|
|
// [Refresher].
|
|
|
|
type RefresherFunc func(ctx context.Context) (err error)
|
|
|
|
|
|
|
|
// type check
|
|
|
|
var _ Refresher = RefresherFunc(nil)
|
|
|
|
|
|
|
|
// Refresh implements the [Refresher] interface for RefresherFunc.
|
|
|
|
func (f RefresherFunc) Refresh(ctx context.Context) (err error) {
|
|
|
|
return f(ctx)
|
|
|
|
}
|
|
|
|
|
2024-01-04 19:22:32 +03:00
|
|
|
// RefreshWorker is an [Interface] implementation that updates its [Refresher]
|
|
|
|
// every tick of the provided ticker.
|
2022-08-26 14:18:35 +03:00
|
|
|
type RefreshWorker struct {
|
2024-10-14 17:44:24 +03:00
|
|
|
logger *slog.Logger
|
2024-01-04 19:22:32 +03:00
|
|
|
done chan unit
|
|
|
|
context func() (ctx context.Context, cancel context.CancelFunc)
|
|
|
|
tick *time.Ticker
|
|
|
|
rand *rand.Rand
|
|
|
|
refr Refresher
|
|
|
|
maxStartSleep time.Duration
|
2022-08-26 14:18:35 +03:00
|
|
|
|
|
|
|
refrOnShutdown bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// RefreshWorkerConfig is the configuration structure for a *RefreshWorker.
|
|
|
|
type RefreshWorkerConfig struct {
|
|
|
|
// Context is used to provide a context for the Refresh method of Refresher.
|
2024-10-14 17:44:24 +03:00
|
|
|
//
|
|
|
|
// NOTE: It is not used for the shutdown refresh.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Consider ways of fixing that.
|
2022-08-26 14:18:35 +03:00
|
|
|
Context func() (ctx context.Context, cancel context.CancelFunc)
|
|
|
|
|
|
|
|
// Refresher is the entity being refreshed.
|
|
|
|
Refresher Refresher
|
|
|
|
|
2024-10-14 17:44:24 +03:00
|
|
|
// Logger is used for logging the operation of the worker.
|
|
|
|
Logger *slog.Logger
|
2022-08-26 14:18:35 +03:00
|
|
|
|
|
|
|
// Interval is the refresh interval. Must be greater than zero.
|
2024-01-04 19:22:32 +03:00
|
|
|
//
|
|
|
|
// TODO(a.garipov): Consider switching to an interface à la
|
|
|
|
// github.com/robfig/cron/v3.Schedule.
|
2022-08-26 14:18:35 +03:00
|
|
|
Interval time.Duration
|
|
|
|
|
|
|
|
// RefreshOnShutdown, if true, instructs the worker to call the Refresher's
|
|
|
|
// Refresh method before shutting down the worker. This is useful for items
|
|
|
|
// that should persist to disk or remote storage before shutting down.
|
|
|
|
RefreshOnShutdown bool
|
|
|
|
|
2024-01-04 19:22:32 +03:00
|
|
|
// RandomizeStart, if true, instructs the worker to sleep before starting a
|
|
|
|
// refresh. The duration of the sleep is a random duration of up to 10 % of
|
|
|
|
// Interval.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Switch to something like a cron schedule and see if this
|
|
|
|
// is still necessary
|
|
|
|
RandomizeStart bool
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewRefreshWorker returns a new valid *RefreshWorker with the provided
|
|
|
|
// parameters. c must not be nil.
|
|
|
|
func NewRefreshWorker(c *RefreshWorkerConfig) (w *RefreshWorker) {
|
2024-01-04 19:22:32 +03:00
|
|
|
var maxStartSleep time.Duration
|
|
|
|
var rng *rand.Rand
|
|
|
|
if c.RandomizeStart {
|
|
|
|
maxStartSleep = c.Interval / 10
|
2024-10-14 17:44:24 +03:00
|
|
|
// #nosec G115 -- The Unix epoch time is highly unlikely to be negative.
|
2024-01-04 19:22:32 +03:00
|
|
|
rng = rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
|
|
|
|
}
|
|
|
|
|
2022-08-26 14:18:35 +03:00
|
|
|
return &RefreshWorker{
|
2024-10-14 17:44:24 +03:00
|
|
|
logger: c.Logger,
|
2022-08-26 14:18:35 +03:00
|
|
|
done: make(chan unit),
|
|
|
|
context: c.Context,
|
|
|
|
tick: time.NewTicker(c.Interval),
|
2024-01-04 19:22:32 +03:00
|
|
|
rand: rng,
|
2022-08-26 14:18:35 +03:00
|
|
|
refr: c.Refresher,
|
2024-01-04 19:22:32 +03:00
|
|
|
maxStartSleep: maxStartSleep,
|
2022-08-26 14:18:35 +03:00
|
|
|
refrOnShutdown: c.RefreshOnShutdown,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-04 19:22:32 +03:00
|
|
|
// type check
|
2024-03-11 12:21:07 +03:00
|
|
|
var _ service.Interface = (*RefreshWorker)(nil)
|
2024-01-04 19:22:32 +03:00
|
|
|
|
2024-03-11 12:21:07 +03:00
|
|
|
// Start implements the [service.Interface] interface for *RefreshWorker. err
|
|
|
|
// is always nil.
|
2024-01-04 19:22:32 +03:00
|
|
|
func (w *RefreshWorker) Start(_ context.Context) (err error) {
|
2022-08-26 14:18:35 +03:00
|
|
|
go w.refreshInALoop()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-11 12:21:07 +03:00
|
|
|
// Shutdown implements the [service.Interface] interface for *RefreshWorker.
|
2024-10-14 17:44:24 +03:00
|
|
|
//
|
|
|
|
// NOTE: The context provided by [RefreshWorkerConfig.Context] is not used for
|
|
|
|
// the shutdown refresh.
|
2022-08-26 14:18:35 +03:00
|
|
|
func (w *RefreshWorker) Shutdown(ctx context.Context) (err error) {
|
|
|
|
if w.refrOnShutdown {
|
2024-10-14 17:44:24 +03:00
|
|
|
err = w.refr.Refresh(slogutil.ContextWithLogger(ctx, w.logger))
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
close(w.done)
|
|
|
|
|
|
|
|
w.tick.Stop()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("refresh on shutdown: %w", err)
|
|
|
|
} else {
|
2024-10-14 17:44:24 +03:00
|
|
|
w.logger.InfoContext(ctx, "shut down successfully")
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// refreshInALoop refreshes the entity every tick of w.tick until Shutdown is
|
|
|
|
// called.
|
|
|
|
func (w *RefreshWorker) refreshInALoop() {
|
2024-10-14 17:44:24 +03:00
|
|
|
ctx := context.Background()
|
|
|
|
defer slogutil.RecoverAndLog(ctx, w.logger)
|
2022-08-26 14:18:35 +03:00
|
|
|
|
2024-10-14 17:44:24 +03:00
|
|
|
w.logger.InfoContext(ctx, "starting refresh loop")
|
2022-08-26 14:18:35 +03:00
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-w.done:
|
2024-10-14 17:44:24 +03:00
|
|
|
w.logger.InfoContext(ctx, "finished refresh loop")
|
2022-08-26 14:18:35 +03:00
|
|
|
|
|
|
|
return
|
|
|
|
case <-w.tick.C:
|
2024-10-14 17:44:24 +03:00
|
|
|
if w.sleepRandom(ctx) {
|
2024-01-04 19:22:32 +03:00
|
|
|
w.refresh()
|
|
|
|
}
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-04 19:22:32 +03:00
|
|
|
// sleepRandom sleeps for up to maxStartSleep unless it's zero. shouldRefresh
|
|
|
|
// shows if a refresh should be performed once the sleep is finished.
|
2024-10-14 17:44:24 +03:00
|
|
|
func (w *RefreshWorker) sleepRandom(ctx context.Context) (shouldRefresh bool) {
|
2024-01-04 19:22:32 +03:00
|
|
|
if w.maxStartSleep == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
sleepDur := time.Duration(w.rand.Int63n(int64(w.maxStartSleep)))
|
2024-10-14 17:44:24 +03:00
|
|
|
// TODO(a.garipov): Augment our JSON handler to use time.Duration.String
|
|
|
|
// automatically?
|
|
|
|
w.logger.DebugContext(ctx, "sleeping before refresh", "dur", timeutil.Duration{
|
|
|
|
Duration: sleepDur,
|
|
|
|
})
|
2024-01-04 19:22:32 +03:00
|
|
|
|
|
|
|
timer := time.NewTimer(sleepDur)
|
|
|
|
defer func() {
|
|
|
|
if !timer.Stop() {
|
|
|
|
// We don't know if the timer's value has been consumed yet or not,
|
|
|
|
// so use a select with default to make sure that this doesn't
|
|
|
|
// block.
|
|
|
|
select {
|
|
|
|
case <-timer.C:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-w.done:
|
|
|
|
return false
|
|
|
|
case <-timer.C:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-26 14:18:35 +03:00
|
|
|
// refresh refreshes the entity and logs the status of the refresh.
|
|
|
|
func (w *RefreshWorker) refresh() {
|
|
|
|
// TODO(a.garipov): Consider adding a helper for enriching errors with
|
|
|
|
// context deadline data without duplication. See an example in method
|
|
|
|
// filter.refreshableFilter.refresh.
|
|
|
|
ctx, cancel := w.context()
|
|
|
|
defer cancel()
|
|
|
|
|
2024-10-14 17:44:24 +03:00
|
|
|
ctx = slogutil.ContextWithLogger(ctx, w.logger)
|
|
|
|
|
2024-06-07 14:27:46 +03:00
|
|
|
_ = w.refr.Refresh(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RefresherWithErrColl reports all refresh errors to errColl and logs them
|
|
|
|
// using a provided logging function.
|
|
|
|
type RefresherWithErrColl struct {
|
2024-10-14 17:44:24 +03:00
|
|
|
logger *slog.Logger
|
2024-06-07 14:27:46 +03:00
|
|
|
refr Refresher
|
|
|
|
errColl errcoll.Interface
|
|
|
|
prefix string
|
|
|
|
}
|
2022-08-26 14:18:35 +03:00
|
|
|
|
2024-06-07 14:27:46 +03:00
|
|
|
// NewRefresherWithErrColl wraps refr into a refresher that collects errors and
|
|
|
|
// logs them.
|
|
|
|
func NewRefresherWithErrColl(
|
|
|
|
refr Refresher,
|
2024-10-14 17:44:24 +03:00
|
|
|
logger *slog.Logger,
|
2024-06-07 14:27:46 +03:00
|
|
|
errColl errcoll.Interface,
|
|
|
|
prefix string,
|
|
|
|
) (wrapped *RefresherWithErrColl) {
|
|
|
|
return &RefresherWithErrColl{
|
|
|
|
refr: refr,
|
2024-10-14 17:44:24 +03:00
|
|
|
logger: logger,
|
2024-06-07 14:27:46 +03:00
|
|
|
errColl: errColl,
|
|
|
|
prefix: prefix,
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 14:18:35 +03:00
|
|
|
|
2024-06-07 14:27:46 +03:00
|
|
|
// type check
|
|
|
|
var _ Refresher = (*RefresherWithErrColl)(nil)
|
|
|
|
|
|
|
|
// Refresh implements the [Refresher] interface for *RefresherWithErrColl.
|
|
|
|
func (r *RefresherWithErrColl) Refresh(ctx context.Context) (err error) {
|
|
|
|
err = r.refr.Refresh(ctx)
|
|
|
|
if err != nil {
|
2024-10-14 17:44:24 +03:00
|
|
|
errcoll.Collect(ctx, r.errColl, r.logger, "refreshing", err)
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|
|
|
|
|
2024-06-07 14:27:46 +03:00
|
|
|
return err
|
2022-08-26 14:18:35 +03:00
|
|
|
}
|