BareGit
#include "scheduler.h"

#include <utility>

bool Scheduler::Later::operator()(const ScheduledTask& left,
                                  const ScheduledTask& right) const
{
    if(left.due == right.due)
    {
        return left.service->config.id > right.service->config.id;
    }
    return left.due > right.due;
}

mw::E<Scheduler::Clock::time_point> Scheduler::nextDueTime(
    Clock::time_point previous, Clock::duration interval, Clock::time_point now)
{
    if(interval <= Clock::duration::zero() ||
       previous < Clock::time_point{} || now < previous)
    {
        return std::unexpected(mw::runtimeError("Invalid task schedule"));
    }
    const auto delay = interval - (now - previous) % interval;
    if(delay > Clock::time_point::max() - now)
    {
        return std::unexpected(mw::runtimeError("Task due time overflow"));
    }
    return now + delay;
}

Scheduler::Scheduler(DataSourceInterface& data_source,
                     ProbeFactory probe_factory)
    : data_source(data_source), probe_factory(std::move(probe_factory))
{}

mw::E<std::unique_ptr<Scheduler>> Scheduler::create(
    const Configuration& configuration, DataSourceInterface& data_source,
    ProbeFactory probe_factory)
{
    if(configuration.worker_count == 0 || !probe_factory)
    {
        return std::unexpected(mw::runtimeError(
            "Scheduler requires workers and a probe factory"));
    }
    auto scheduler = std::unique_ptr<Scheduler>(
        new Scheduler(data_source, std::move(probe_factory)));
    for(const auto& group : configuration.groups)
    {
        for(const auto& config : group.services)
        {
            if(config.id.empty() ||
               config.id.find('\0') != std::string::npos)
            {
                return std::unexpected(mw::runtimeError(
                    "Invalid service ID"));
            }
            if(scheduler->services.contains(config.id))
            {
                return std::unexpected(mw::runtimeError(
                    "Duplicate service ID: " + config.id));
            }
            const auto now = Clock::now();
            auto next = nextDueTime(now, config.interval, now);
            if(!next)
            {
                return std::unexpected(mw::runtimeError(
                    config.id + ": " + next.error().msg()));
            }
            auto service = std::make_unique<Service>();
            service->config = config;
            // Validate without running the probe or the injected factory.
            auto probe = service->createProbe();
            if(!probe)
            {
                return std::unexpected(mw::runtimeError(
                    config.id + ": " + probe.error().msg()));
            }
            scheduler->services.emplace(config.id, std::move(service));
        }
    }
    auto pool = ThreadPool::create(configuration.worker_count);
    if(!pool)
    {
        return std::unexpected(pool.error());
    }
    scheduler->pool = std::move(*pool);
    return scheduler;
}

const Service* Scheduler::findService(const std::string& service_id) const
{
    const auto found = services.find(service_id);
    return found == services.end() ? nullptr : found->second.get();
}

mw::E<void> Scheduler::run(std::stop_token stop_token)
{
    {
        std::lock_guard lock(mutex);
        if(started)
        {
            return std::unexpected(mw::runtimeError(
                "Scheduler has already started"));
        }
        started = true;
    }
    auto result = runLoop(stop_token);
    pool->waitIdle();
    std::lock_guard lock(mutex);
    if(result && error)
    {
        return std::unexpected(*error);
    }
    return result;
}

mw::E<void> Scheduler::runLoop(std::stop_token stop_token)
{
    const auto initial_due = Clock::now();
    for(const auto& entry : services)
    {
        queue.push({entry.second.get(), initial_due});
    }
    while(!stop_token.stop_requested())
    {
        std::unique_lock lock(mutex);
        if(error)
        {
            return std::unexpected(*error);
        }
        const auto now = Clock::now();
        if(!queue.empty() && queue.top().due <= now)
        {
            auto scheduled = queue.top();
            auto& service = *scheduled.service;
            auto next = nextDueTime(
                scheduled.due, service.config.interval, now);
            if(!next)
            {
                return std::unexpected(next.error());
            }
            queue.pop();
            scheduled.due = *next;
            queue.push(scheduled);
            lock.unlock();
            auto dispatched = dispatch(service);
            if(!dispatched)
            {
                return std::unexpected(dispatched.error());
            }
            continue;
        }
        if(!queue.empty())
        {
            const auto next = queue.top().due;
            wake.wait_until(lock, stop_token, next,
                            std::bind_front(&Scheduler::hasError, this));
        }
        else
        {
            wake.wait(lock, stop_token,
                      std::bind_front(&Scheduler::hasError, this));
        }
    }
    return {};
}

namespace
{

struct FlightGuard
{
    std::atomic<bool>& in_flight;
    bool active = true;

    ~FlightGuard()
    {
        if(active)
        {
            in_flight = false;
        }
    }
};

}

mw::E<void> Scheduler::dispatch(Service& service)
{
    if(service.in_flight.exchange(true))
    {
        return {};
    }
    FlightGuard guard{service.in_flight};
    auto submitted = pool->trySubmit(Task{*this, service, nullptr});
    if(!submitted)
    {
        return std::unexpected(submitted.error());
    }
    if(*submitted)
    {
        // The accepted task now clears the flag after probing and persistence.
        guard.active = false;
    }
    return {};
}

void Scheduler::Task::operator()()
{
    scheduler.execute(*this);
}

void Scheduler::execute(Task& task)
{
    FlightGuard guard{task.service.in_flight};
    const auto& service_id = task.service.config.id;
    auto probe = probe_factory(task.service);
    if(!probe)
    {
        reportError(service_id, std::move(probe.error()));
        return;
    }
    if(!*probe)
    {
        reportError(service_id, mw::runtimeError(
            "Probe factory returned a null probe"));
        return;
    }
    task.probe = std::move(*probe);
    auto result = task.probe->probe();
    if(!result)
    {
        reportError(service_id, std::move(result.error()));
        return;
    }
    auto saved = data_source.save({service_id, result->timestamp,
        result->duration_microsecond, result->status});
    if(!saved)
    {
        reportError(service_id, std::move(saved.error()));
    }
}

void Scheduler::reportError(const std::string& service_id, mw::Error failure)
{
    {
        std::lock_guard lock(mutex);
        if(!error)
        {
            error = mw::runtimeError(service_id + ": " + failure.msg());
        }
    }
    wake.notify_one();
}

bool Scheduler::hasError() const
{
    return error.has_value();
}