Adds api_client support for GET /licenses/{key}/schedule with If-None-Match
and target-payload parsing (version/type/daily/weekly + legacy blocks), an
atomic on-disk schedule cache, a pure schedule engine (half-open blocks,
midnight self-wrap for daily, Sunday->Monday spillover for weekly, invalid
blocks skipped with a warning), and a background ScheduleManager that loads
the cache offline and pulls on boot and every 5 minutes. Adds engine/cache
tests against the shared docs/fixtures, including 304, corrupt cache, and
offline cases.
161 lines
4.4 KiB
C++
161 lines
4.4 KiB
C++
#include "schedule/schedule_manager.h"
|
|
#include "db/db_keys.h"
|
|
#include "network/api_client.h"
|
|
#include "network/schedule_cache.h"
|
|
|
|
#include <utility>
|
|
|
|
namespace radiantwave::schedule {
|
|
|
|
ScheduleManager::ScheduleManager()
|
|
: fetch_([](const std::string& licenseKey, const std::string& etag,
|
|
api::SchedulePayload& out, std::string& outEtag) {
|
|
return api::fetchScheduleForLicense(licenseKey, etag, out, outEtag);
|
|
}) {
|
|
}
|
|
|
|
ScheduleManager::~ScheduleManager() {
|
|
shutdown();
|
|
}
|
|
|
|
bool ScheduleManager::hasSchedule() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return payload_.hasSchedule();
|
|
}
|
|
|
|
bool ScheduleManager::enabled() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return enabled_;
|
|
}
|
|
|
|
Decision ScheduleManager::activeBlock(int minutes, Weekday weekday) const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!enabled_) {
|
|
return Decision{};
|
|
}
|
|
return evaluate(payload_, minutes, weekday, nullptr);
|
|
}
|
|
|
|
void ScheduleManager::setFetchForTesting(FetchFn fetch) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
fetch_ = std::move(fetch);
|
|
}
|
|
|
|
void ScheduleManager::applyPayload(api::SchedulePayload payload, const std::string& etag) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
payload_ = std::move(payload);
|
|
etag_ = etag;
|
|
}
|
|
// Cache the last good payload for offline use (atomic write).
|
|
radiantwave::sync::schedule_cache::write(payload_, etag_);
|
|
if (onChanged_) onChanged_();
|
|
}
|
|
|
|
void ScheduleManager::loadCache() {
|
|
api::SchedulePayload cached;
|
|
std::string cachedEtag;
|
|
if (radiantwave::sync::schedule_cache::read(cached, cachedEtag)) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
payload_ = std::move(cached);
|
|
etag_ = std::move(cachedEtag);
|
|
if (onChanged_) onChanged_();
|
|
}
|
|
}
|
|
|
|
void ScheduleManager::refreshOnce() {
|
|
std::string licenseKey;
|
|
std::string etag;
|
|
FetchFn fetch;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
licenseKey = licenseKey_;
|
|
etag = etag_;
|
|
fetch = fetch_;
|
|
}
|
|
if (licenseKey.empty()) return;
|
|
|
|
api::SchedulePayload payload;
|
|
std::string newEtag;
|
|
const api::ScheduleFetchStatus status = fetch(licenseKey, etag, payload, newEtag);
|
|
|
|
if (status == api::ScheduleFetchStatus::Ok) {
|
|
applyPayload(std::move(payload), newEtag);
|
|
}
|
|
// NotModified / NotFound / NetworkError: keep the current cache.
|
|
}
|
|
|
|
void ScheduleManager::start(db::DatabaseManager& db, const std::string& timezone,
|
|
std::function<void()> onChanged) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (started_) return;
|
|
started_ = true;
|
|
db_ = &db;
|
|
timezone_ = timezone;
|
|
onChanged_ = std::move(onChanged);
|
|
licenseKey_ = db.getKV(db::KV_LICENSE_KEY);
|
|
enabled_ = true; // config supplies the toggle via setEnabled()
|
|
|
|
// Offline first: load any cached payload immediately.
|
|
loadCache();
|
|
|
|
stop_.store(false);
|
|
worker_ = std::thread(&ScheduleManager::run, this);
|
|
requested_.store(true);
|
|
cv_.notify_one();
|
|
}
|
|
|
|
void ScheduleManager::shutdown() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!started_) return;
|
|
stop_.store(true);
|
|
}
|
|
cv_.notify_one();
|
|
if (worker_.joinable()) {
|
|
worker_.join();
|
|
}
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
started_ = false;
|
|
}
|
|
|
|
void ScheduleManager::requestRefresh() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!started_) return;
|
|
if (db_) {
|
|
licenseKey_ = db_->getKV(db::KV_LICENSE_KEY);
|
|
}
|
|
requested_.store(true);
|
|
}
|
|
cv_.notify_one();
|
|
}
|
|
|
|
void ScheduleManager::setEnabled(bool enabled) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
enabled_ = enabled;
|
|
}
|
|
|
|
void ScheduleManager::setPayloadForTesting(api::SchedulePayload payload,
|
|
const std::string& etag) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
payload_ = std::move(payload);
|
|
etag_ = etag;
|
|
}
|
|
|
|
void ScheduleManager::run() {
|
|
while (true) {
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
cv_.wait_for(lock, REFRESH_INTERVAL, [this] {
|
|
return stop_.load() || requested_.load();
|
|
});
|
|
if (stop_.load()) return;
|
|
requested_.store(false);
|
|
}
|
|
refreshOnce();
|
|
}
|
|
}
|
|
|
|
} // namespace radiantwave::schedule
|