Kiosk: schedule client (conditional GET + atomic cache) and engine
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.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user