optimization_job_api/
worker.rs1use crate::{
4 JobSpec, PROTOCOL, Request, Result,
5 result::{self, Outcome, Record},
6 storage,
7};
8use serde_json::{Value, json};
9use std::{
10 collections::BTreeMap,
11 fs,
12 path::Path,
13 sync::{
14 Mutex,
15 atomic::{AtomicBool, AtomicUsize, Ordering},
16 },
17 thread,
18 time::{Duration, Instant},
19};
20
21pub struct TaskResult {
22 pub outcome: Outcome,
23 pub work: BTreeMap<String, u64>,
24}
25pub trait Prepared: Send + Sync {
26 fn tasks(&self) -> usize;
27 fn solve(&self, task: usize, remaining_seconds: f64) -> Result<TaskResult>;
28 fn cancel(&self);
31}
32pub trait Worker {
33 fn describe(&self) -> Value;
34 fn prepare(&self, spec: &mut JobSpec, base: &Path) -> Result<Box<dyn Prepared>>;
37}
38pub fn description(
39 engine: &str,
40 version: &str,
41 profiles: Value,
42 problems: Value,
43 inner_max: usize,
44) -> Value {
45 json!({"protocol":PROTOCOL,"engine":engine,"build":{"engine_version":version,"job_api_version":env!("CARGO_PKG_VERSION")},"profiles":profiles,"problems":problems,"resources":{"inner_threads_max":inner_max,"cpu_slots":"outer_workers * inner_threads <= cpu_slots; no hidden nested parallelism"},"recovery":"journal_only_no_resume","execution":"native registered models; no deserialized callbacks"})
46}
47fn prepared(worker: &dyn Worker, path: &Path) -> Result<(JobSpec, Box<dyn Prepared>)> {
48 let mut s: JobSpec = serde_json::from_slice(&fs::read(path)?)?;
49 s.validate()?;
50 let expected = s.task_count;
51 let p = worker.prepare(&mut s, path.parent().ok_or("spec parent missing")?)?;
52 if expected.is_some_and(|n| n != p.tasks()) {
53 return Err("task_count disagrees with payload".into());
54 }
55 s.task_count = Some(p.tasks());
56 s.validate()?;
57 Ok((s, p))
58}
59pub fn execute(worker: &dyn Worker, dir: &Path) -> Result<()> {
60 let _owner = storage::lock(&dir.join("worker.lock"))?;
61 if dir.join("results.jsonl").exists() || dir.join("run.json").exists() {
62 return Err("existing attempt: journal recovery is not solver resume".into());
63 }
64 let start = Instant::now();
65 let (s, p) = prepared(worker, &dir.join("resolved.json"))?;
66 let persisted: JobSpec = serde_json::from_slice(&fs::read(dir.join("resolved.json"))?)?;
67 if s != persisted {
68 return Err("resolved job changed during worker validation".into());
69 }
70 let digest = s.digest()?;
71 let exe_digest = storage::digest(&fs::read(std::env::current_exe()?)?);
72 let journal = fs::OpenOptions::new()
73 .write(true)
74 .create_new(true)
75 .open(dir.join("results.jsonl"))?;
76 journal.sync_all()?;
77 fs::File::open(dir)?.sync_all()?;
78 let next = AtomicUsize::new(0);
79 let stop = AtomicBool::new(false);
80 let done = AtomicBool::new(false);
81 let output = Mutex::new(());
82 let error = Mutex::new(None::<String>);
83 thread::scope(|scope| {
84 let watcher = scope.spawn(|| {
85 while !done.load(Ordering::Acquire) {
86 if dir.join("cancel.request").exists()
87 || start.elapsed().as_secs_f64() >= s.limits.wall_time_seconds
88 {
89 stop.store(true, Ordering::Release);
90 p.cancel();
91 }
92 thread::sleep(Duration::from_millis(5));
93 }
94 });
95 let mut handles = Vec::new();
96 for _ in 0..s.resources.outer_workers.min(p.tasks()) {
97 handles.push(scope.spawn(|| {
98 while !stop.load(Ordering::Acquire) {
99 if dir.join("cancel.request").exists()
102 || start.elapsed().as_secs_f64() >= s.limits.wall_time_seconds
103 {
104 stop.store(true, Ordering::Release);
105 p.cancel();
106 break;
107 }
108 let task = next.fetch_add(1, Ordering::Relaxed);
109 if task >= p.tasks() {
110 break;
111 }
112 let began = Instant::now();
113 let attempt = (|| -> Result<()> {
114 let r = p.solve(
115 task,
116 (s.limits.wall_time_seconds - start.elapsed().as_secs_f64()).max(0.0),
117 )?;
118 r.outcome
121 .check()
122 .map_err(|e| format!("task {task}: invalid outcome: {e}"))?;
123 let record = Record {
124 task_id: task,
125 spec_digest: digest.clone(),
126 elapsed_seconds: began.elapsed().as_secs_f64(),
127 outcome: r.outcome,
128 work: r.work,
129 };
130 let _guard = output.lock().map_err(|_| "journal lock poisoned")?;
131 storage::append_checked(&dir.join("results.jsonl"), 1, &record)?;
132 Ok(())
133 })();
134 if let Err(e) = attempt {
135 *error.lock().unwrap() = Some(e.to_string());
136 stop.store(true, Ordering::Release);
137 p.cancel();
138 break;
139 }
140 }
141 }));
142 }
143 for handle in handles {
144 if handle.join().is_err() {
145 *error.lock().unwrap() = Some("task panicked".into());
146 stop.store(true, Ordering::Release);
147 p.cancel();
148 }
149 }
150 done.store(true, Ordering::Release);
151 let _ = watcher.join();
152 });
153 if let Some(e) = error.into_inner().map_err(|_| "task error lock poisoned")? {
154 return Err(e.into());
155 }
156 let marker = fs::read(dir.join("cancel.request"))
157 .ok()
158 .and_then(|b| serde_json::from_slice::<Value>(&b).ok());
159 let timed_out = start.elapsed().as_secs_f64() >= s.limits.wall_time_seconds
160 || marker.as_ref().is_some_and(|m| m["reason"] == "timed_out");
161 let status = if timed_out {
162 "timed_out"
163 } else if stop.load(Ordering::Acquire) || dir.join("cancel.request").exists() {
164 "cancelled"
165 } else {
166 "completed"
167 };
168 let run = result::manifest(
169 dir,
170 &Request::Job(s),
171 status,
172 true,
173 start.elapsed().as_secs_f64(),
174 &exe_digest,
175 )?;
176 storage::write_atomic_json(&dir.join("run.json"), &run)?;
177 Ok(())
178}
179pub fn run(worker: impl Worker) {
180 let run = (|| -> Result<()> {
181 let args = std::env::args().collect::<Vec<_>>();
182 let value = match args.get(1).map(String::as_str) {
183 Some("describe") if args.len() == 2 => worker.describe(),
184 Some("validate") if args.len() == 3 => {
185 let (s, _) = prepared(&worker, Path::new(&args[2]))?;
186 json!({"resolved":s})
187 }
188 Some("execute") if args.len() == 3 => {
189 execute(&worker, Path::new(&args[2]))?;
190 json!({"executed":true})
191 }
192 _ => {
193 return Err(
194 "usage: WORKER describe | validate SPEC | execute JOB_DIRECTORY".into(),
195 );
196 }
197 };
198 println!("{}", serde_json::to_string(&value)?);
199 Ok(())
200 })();
201 if let Err(e) = run {
202 eprintln!(
203 "{}",
204 json!({"error":{"code":"worker_error","message":e.to_string()}})
205 );
206 std::process::exit(1);
207 }
208}