syn-daemon never ran the background scheduler (rust/lib/src/scheduler): no periodic timer, no startup trigger, and its /syn/node-admin/1 handler had no live JobRunner wired in, so even an explicit remote TriggerSync had nothing to act on. A headless node — the canonical Mirror-policy target per docs/sync-management.md's remote-administration workflow — would sync catalogues (peers push/pull metadata directly) but never proactively pull content of its own accord. Add rust/syn-daemon/src/scheduler_driver.rs: a periodic-timer loop spawned from server::serve once NodeRuntime is up, mirroring gui-app's driver but without a P2P-readiness wait (the daemon's NodeRuntime is already running by the time this spawns). It also wires the daemon's JobRunner into P2PNode::set_job_runner, so remote TriggerSync/ListJobs/CancelJob now have a live runner to act on. Update ADR 0012 with an addendum noting the daemon driver and the still-open gap: no DaemonCommand to trigger a tick manually from the CLI/TUI, only via the timer or a remote node-admin call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134Qxsmkvtr8hSGkhbT1nTg
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b3ff99cd49
commit
22afa72242
@@ -65,3 +65,25 @@ it's already running and skip (not fail) the tick otherwise.
|
||||
not hand-rolled checks) is what makes it safe to run this unattended on a
|
||||
phone at all — this was a hard prerequisite, not an afterthought, before
|
||||
any Android scheduler driver could ship.
|
||||
|
||||
## Update (2026-08-05): `syn-daemon` driver added
|
||||
|
||||
`syn-daemon` (the headless-server modality — the canonical `Mirror`-policy
|
||||
target per `docs/sync-management.md`'s remote-administration section) had no
|
||||
driver at all until now: no periodic timer, no startup trigger, and its
|
||||
`/syn/node-admin/1` handler was never wired to a `JobRunner`, so even an
|
||||
explicit remote `TriggerSync` had nothing live to act on. A node whose
|
||||
policy said `Mirror` would sync catalogues (peers push/pull metadata
|
||||
directly) but never proactively fetch content — indistinguishable, from a
|
||||
user setting `Mirror` on it, from the feature not working.
|
||||
|
||||
`rust/syn-daemon/src/scheduler_driver.rs` fixes this: a periodic-timer loop
|
||||
spawned from `server::serve` after `NodeRuntime::start`, closest in shape to
|
||||
desktop's driver (no network-state gating — a VPS has no metered-connection
|
||||
or battery-charge notion to gate on, same reasoning ADR gave for desktop).
|
||||
It also calls `P2PNode::set_job_runner`, so remote `TriggerSync`/`ListJobs`/
|
||||
`CancelJob` now have a live `JobRunner` to act on rather than finding
|
||||
`JobRunnerCell` empty. Manually triggering a daemon's tick from the CLI/TUI
|
||||
(rather than only remotely, via node-admin, or waiting for the timer) is not
|
||||
yet wired — `DaemonCommand` has no scheduler-facing variant — and remains
|
||||
open work.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod protocol;
|
||||
pub mod scheduler_driver;
|
||||
pub mod server;
|
||||
|
||||
pub const DAEMON_SOCK_FILE: &str = ".daemon.sock";
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Drives the sync scheduler (`synchronicity::scheduler`) on its own tokio
|
||||
//! task — the headless-daemon counterpart of `rust/gui-app/src/scheduler_driver.rs`
|
||||
//! (desktop's periodic-timer loop) and Android's `SchedulerWorker`
|
||||
//! (`docs/sync-management.md` phase 3). Without this, a `syn-daemon` node
|
||||
//! (e.g. a VPS meant to `Mirror` a document type) never proactively pulls
|
||||
//! anything: it only ever answers requests other nodes make of it, and
|
||||
//! `SetSyncPolicy`/`TriggerSync` issued via `/syn/node-admin/1` had nothing
|
||||
//! local to act on `TriggerSync` with.
|
||||
//!
|
||||
//! Simpler than desktop's driver: `syn-daemon`'s [`NodeRuntime`] is already
|
||||
//! running by the time [`spawn`] is called (`server::serve` starts it
|
||||
//! first), so there's no "wait for the P2P endpoint to come up" step, and
|
||||
//! there's exactly one node to drive rather than one per GUI window.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{info, warn};
|
||||
use synchronicity::jobs::JobOrigin;
|
||||
use synchronicity::jobs::runner::JobRunner;
|
||||
use synchronicity::node::NodeRuntime;
|
||||
use synchronicity::scheduler;
|
||||
|
||||
/// Floor under a user-configured interval, so a fat-fingered tiny value
|
||||
/// can't turn this into a busy loop (matches `gui-app`'s driver).
|
||||
const MIN_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Spawn the scheduler's periodic-timer driver on the daemon's tokio
|
||||
/// runtime. Fire-and-forget: runs for the daemon's lifetime, alongside the
|
||||
/// axum server `serve` runs on the same runtime.
|
||||
///
|
||||
/// Also wires `job_runner` into the node-admin protocol handler
|
||||
/// ([`P2PNode::set_job_runner`](synchronicity::p2p::P2PNode::set_job_runner))
|
||||
/// so a remote `TriggerSync`/`ListJobs`/`CancelJob` call shares this same
|
||||
/// runner — and its active-job dedup — rather than finding an unwired
|
||||
/// `JobRunnerCell` and being unable to act.
|
||||
pub fn spawn(runtime: &NodeRuntime, job_runner: Arc<JobRunner>) {
|
||||
runtime.p2p.set_job_runner(Arc::clone(&job_runner));
|
||||
|
||||
let store = Arc::clone(&runtime.store);
|
||||
let endpoint = runtime.p2p.endpoint();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let settings = store
|
||||
.settings_store
|
||||
.batch(|tx| store.settings_store.scheduler_settings(tx))
|
||||
.unwrap_or_default();
|
||||
if settings.enabled {
|
||||
match store.settings_store.local_node_pubkey() {
|
||||
Ok(Some(local_node)) => {
|
||||
let summary = scheduler::run_all_passes(
|
||||
&endpoint,
|
||||
Arc::clone(&store),
|
||||
&job_runner,
|
||||
local_node,
|
||||
JobOrigin::Scheduler,
|
||||
)
|
||||
.await;
|
||||
match summary {
|
||||
Some(summary) => info!(
|
||||
"scheduler: tick complete — {} peer(s) synced, {} thumbnail(s) + \
|
||||
{} artifact(s) fetched, {} blob(s) for {} document(s)",
|
||||
summary.catalogue.peers_synced,
|
||||
summary.catalogue_fetch.thumbnails_received,
|
||||
summary.catalogue_fetch.artifacts_received,
|
||||
summary.content.blobs_fetched,
|
||||
summary.content.wanted_documents,
|
||||
),
|
||||
// A remote `TriggerSync` or another tick is already
|
||||
// mid-run — this tick has nothing to add.
|
||||
None => info!(
|
||||
"scheduler: tick skipped, a background sync is already running"
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(None) => warn!("scheduler: node not initialized, skipping this tick"),
|
||||
Err(e) => warn!("scheduler: failed to load local node pubkey: {e}"),
|
||||
}
|
||||
}
|
||||
let interval = Duration::from_secs(settings.interval_secs as u64).max(MIN_INTERVAL);
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use axum::extract::State;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use log::info;
|
||||
use synchronicity::jobs::runner::JobRunner;
|
||||
use synchronicity::log::LogConfig;
|
||||
use synchronicity::node::NodeRuntime;
|
||||
use synchronicity::stores::Store;
|
||||
@@ -18,6 +19,7 @@ use serde_json::json;
|
||||
|
||||
use crate::commands;
|
||||
use crate::protocol::{CommandResponse, DaemonCommand};
|
||||
use crate::scheduler_driver;
|
||||
|
||||
struct AppState {
|
||||
runtime: NodeRuntime,
|
||||
@@ -239,6 +241,12 @@ async fn serve(store_path: &Path, socket_path: &Path) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// Without this, the daemon only ever answers requests other nodes make
|
||||
// of it — a `Mirror`-policy VPS would never proactively pull the content
|
||||
// its own policy wants (see `scheduler_driver`'s doc comment).
|
||||
let job_runner = Arc::new(JobRunner::new(runtime.store.job_store.clone()));
|
||||
scheduler_driver::spawn(&runtime, job_runner);
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
let state = Arc::new(AppState { runtime, shutdown_tx });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user