package fetcher import ( "context" "errors" "fmt" "log/slog" "sync" "time" "git.avsdev.uk/Go/job-search/internal/config" "git.avsdev.uk/Go/job-search/internal/util" ) // ErrNoNewReleases is returned by a source's task when there was nothing new // to fetch; it wraps util.ErrWorkerNoWork. var ErrNoNewReleases = fmt.Errorf("%w: no new releases", util.ErrWorkerNoWork) // Option configures Run. type Option func(*options) // options holds the settings Options change. type options struct { log *slog.Logger } // WithLogger makes Run, its workers and their clients log to logger instead // of slog.Default(). func WithLogger(logger *slog.Logger) Option { return func(opts *options) { opts.log = logger } } // logger returns the logger Run logs to: the given one, or slog.Default(). // It isn't tagged here, as it is also given to the clients the workers // create, which add their own component; see tagged. func (opts options) logger() *slog.Logger { if opts.log == nil { return slog.Default() } return opts.log } // tagged returns logger tagged with the fetcher's component, for the lines // Run and its workers log themselves. func tagged(logger *slog.Logger) *slog.Logger { return logger.With("component", "fetcher") } // runFunc runs one source's worker until ctx is cancelled. logger is // untagged: the worker tags its own lines with tagged, and passes logger as // it is to any client it creates. type runFunc func( ctx context.Context, logger *slog.Logger, conf *config.JobSearchConfig, since time.Time, ) error // Run starts a worker for each enabled source and blocks until every worker // has stopped. Workers stop when ctx is cancelled, or when any worker stops // with an error, which starts a shutdown of all of them. The errors are // logged as they happen and returned joined; nil means a clean shutdown. It // logs to slog.Default() unless given WithLogger. func Run( ctx context.Context, conf *config.JobSearchConfig, since time.Time, opts ...Option, ) error { ctx, cancel := context.WithCancel(ctx) defer cancel() var settings options for _, opt := range opts { opt(&settings) } logger := settings.logger() var ( wg sync.WaitGroup mu sync.Mutex errs []error ) start := func(name string, run runFunc) { wg.Add(1) go func() { defer wg.Done() if err := run(ctx, logger, conf, since); err != nil { tagged(logger).Error( "source stopped, shutting down", "source", name, "error", err, ) mu.Lock() errs = append(errs, fmt.Errorf("%s: %w", name, err)) mu.Unlock() cancel() } }() } if conf.Sources.FindATender.Enabled { start("FindATender", runFindAOpportunity) } if conf.Sources.FTSMock.Enabled { start("FTSMock", runFTSMock) } if conf.Sources.Sleeper.Enabled { start("Sleeper", runSleeper) } wg.Wait() return errors.Join(errs...) }