Skip to main content

ciplink_server/
worker.rs

1//! One thread per PLC: owns the driver, polls the subscribed tags by tier and poll group, runs the queued writes.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, RwLock};
6use std::thread::JoinHandle;
7use std::time::{Duration, Instant};
8
9use ciplink::Value;
10use ciplink::cip::TagResult;
11use indexmap::IndexMap;
12use serde_json::{Map, Value as Json, json};
13
14use crate::config::PlcConfig;
15use crate::planner::{self, Read};
16use crate::plc::{Catalog, Factory, Plc};
17use crate::rules::{Group, Rules, split_bit, truthy};
18use crate::values::{Entry, merge, now_ms, to_value};
19
20/// Seconds between reconnect attempts, the last one repeats.
21#[cfg_attr(test, allow(dead_code))]
22const BACKOFF: [f64; 4] = [1.0, 2.0, 5.0, 10.0];
23/// Seconds before a tag the PLC doesn't have is asked for again, unless the tag table reloads.
24const MISSING_RETRY: f64 = 10.0;
25/// Seconds before a struct whose whole read failed is read whole again, unless it reloads.
26const GROUP_RETRY: f64 = 60.0;
27
28/// Test hooks: what the Python tests monkeypatch on `PlcWorker` (`backoff`, `missing_retry`, `group_retry`) and the
29/// clock and wait they swap for fake ones.
30#[cfg(test)]
31struct Hooks {
32    backoff: Vec<f64>,
33    missing_retry: f64,
34    group_retry: f64,
35    time: Option<Arc<dyn FakeTime>>,
36}
37
38#[cfg(test)]
39impl Default for Hooks {
40    fn default() -> Self {
41        Hooks {
42            backoff: vec![0.01, 0.02, 0.05, 0.1], // as the Python tests' autouse fixture
43            missing_retry: MISSING_RETRY,
44            group_retry: GROUP_RETRY,
45            time: None,
46        }
47    }
48}
49
50/// A fake clock: `now` replaces the clock, `wait` the wait (true once stopped).
51#[cfg(test)]
52trait FakeTime: Send + Sync {
53    fn now(&self) -> f64;
54    fn wait(&self, shared: &Shared, seconds: f64) -> bool;
55}
56
57/// What a worker tells the hub, from its thread.
58#[derive(Clone, Debug, PartialEq)]
59pub enum Event {
60    /// Changed cache entries of the PLC's tags.
61    Values(String, Vec<(String, Entry)>),
62    /// The PLC's connection state changed.
63    Status(String, &'static str),
64    /// The PLC's tag table was reloaded (online edit, download).
65    Meta(String),
66    /// Controller descriptions of these tags were read.
67    Info(String, Vec<String>),
68}
69
70/// The outcome of a write: (ok, error, kind, read-back value).
71#[derive(Clone, Debug, PartialEq)]
72pub struct WriteResult {
73    /// The write went through.
74    pub ok: bool,
75    /// Why it failed.
76    pub error: Option<String>,
77    /// The error kind (`refused`, `comm`, ciplink's kinds, or the hub's checks).
78    pub kind: Option<String>,
79    /// The value read back after the write, null if it could not be read.
80    pub value: Json,
81}
82
83impl WriteResult {
84    /// A failed write.
85    pub fn failed(error: impl Into<String>, kind: &str) -> Self {
86        WriteResult {
87            ok: false,
88            error: Some(error.into()),
89            kind: Some(kind.to_owned()),
90            value: Json::Null,
91        }
92    }
93}
94
95/// Called once with a write's result, from whichever thread finishes it.
96pub type WriteDone = Box<dyn FnOnce(WriteResult) + Send>;
97
98struct WriteRequest {
99    tag: String,
100    value: Value,
101    done: WriteDone,
102}
103
104/// The type of a tag as tag info shows it: null when unknown, dims of a whole array.
105#[derive(Clone, Debug, Default, PartialEq, Eq)]
106pub struct TypeInfo {
107    /// The type name, None while unknown.
108    pub type_: Option<String>,
109    /// Dims of a whole array.
110    pub dims: Option<Vec<u64>>,
111}
112
113/// The controller structure the worker last loaded, with its `/tags` JSON.
114#[derive(Debug, Default)]
115pub struct Structure {
116    /// Tags and types.
117    pub catalog: Catalog,
118    /// `GET /tags` `tags`.
119    pub tag_list: Vec<Json>,
120    /// `GET /tags` `types`.
121    pub types: Map<String, Json>,
122}
123
124/// Counters for `/health` and `/stats`.
125#[derive(Clone, Debug, Default)]
126pub struct Stats {
127    /// Duration of the last tick.
128    pub last_cycle_ms: Option<f64>,
129    /// Time of the last good tick, ms since the epoch.
130    pub last_ok_ts: Option<i64>,
131    /// Read services sent in the last tick, after planning.
132    pub reads_per_cycle: usize,
133    /// Tags read in the last tick.
134    pub tags_per_cycle: usize,
135    /// Whole struct reads that failed, their members read one by one instead.
136    pub group_fallbacks: u64,
137    /// Ticks that took longer than the tick.
138    pub overruns: u64,
139    /// Reconnect attempts.
140    pub reconnects: u64,
141    /// Tag table reloads seen.
142    pub reloads: u64,
143}
144
145#[derive(Default)]
146struct Inner {
147    /// tag -> 1 (read at its rate), 2 (round robin, every tier2_period) or 3 (not read)
148    tiers: IndexMap<String, u8>,
149    /// tag that left tier 1 -> (clock time it leaves tier 2, tier after that)
150    grace: IndexMap<String, (f64, Option<u8>)>,
151    /// tier-2 tags in name order, None when to be rebuilt
152    tier2: Option<Vec<String>>,
153    slice: usize,
154    /// tier-1 tag -> clock time of its last read
155    last_read: HashMap<String, f64>,
156    /// clock time of the last cycle_ms boundary
157    last_cycle: Option<f64>,
158    /// tag -> its poll group, memoized
159    groups: HashMap<String, Option<Group>>,
160    /// group name -> (previous, last) clock time it was read
161    group_reads: HashMap<String, (Option<f64>, f64)>,
162    /// group name -> rate_ms, of the groups read since the start
163    group_rates: IndexMap<String, u64>,
164    /// sorted tags -> read chunks, cleared when the subscriptions or the tag table change
165    plans: HashMap<Vec<String>, Arc<Vec<Vec<Read>>>>,
166    /// lowercase struct path whose whole read failed -> clock time it failed
167    no_group: HashMap<String, f64>,
168    /// tags just added at tier 2: read in the next tick, then in their slice
169    new_tier2: HashSet<String>,
170    /// tag -> the latest entry
171    cache: HashMap<String, Entry>,
172    /// tag -> the entry last given to the hub (deadband reference)
173    published: HashMap<String, Entry>,
174    /// tag -> clock time to ask for it again; skipped until then
175    missing: HashMap<String, f64>,
176    /// tags that entered tiers, described in the next tick
177    want_descriptions: HashSet<String>,
178}
179
180/// Static settings of a worker.
181#[derive(Clone)]
182pub struct WorkerOptions {
183    /// Poll period in seconds.
184    pub cycle: f64,
185    /// Each tier-2 tag is read about this often.
186    pub tier2_period: f64,
187    /// A tag leaving tier 1 stays at tier 2 this long.
188    pub grace_s: f64,
189    /// Read structs whole when that saves reads.
190    pub planner: bool,
191    /// Largest struct the planner reads whole.
192    pub whole_max_bytes: u64,
193}
194
195struct Shared {
196    name: String,
197    cfg: PlcConfig,
198    factory: Factory,
199    options: WorkerOptions,
200    rules: Arc<Rules>,
201    on_event: Arc<dyn Fn(Event) + Send + Sync>,
202    epoch: Instant,
203    state: Mutex<&'static str>,
204    inner: Mutex<Inner>,
205    writes: Mutex<VecDeque<WriteRequest>>,
206    wake: Condvar,
207    stop: AtomicBool,
208    structure: RwLock<Arc<Structure>>,
209    version: Mutex<Json>,
210    descriptions: Mutex<HashMap<String, Option<String>>>,
211    stats: Mutex<Stats>,
212    #[cfg(test)]
213    hooks: Mutex<Hooks>,
214}
215
216/// The poller of one PLC; methods are called from the hub, the thread does the I/O.
217pub struct PlcWorker {
218    shared: Arc<Shared>,
219    thread: Mutex<Option<JoinHandle<()>>>,
220}
221
222fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
223    m.lock().unwrap_or_else(PoisonError::into_inner)
224}
225
226/// Python's `round(x, 1)` for display.
227fn round1(x: f64) -> f64 {
228    (x * 10.0).round_ties_even() / 10.0
229}
230
231impl PlcWorker {
232    /// A worker of `cfg`, not started.
233    pub fn new(
234        cfg: PlcConfig,
235        factory: Factory,
236        options: WorkerOptions,
237        rules: Arc<Rules>,
238        on_event: Arc<dyn Fn(Event) + Send + Sync>,
239    ) -> Self {
240        PlcWorker {
241            shared: Arc::new(Shared {
242                name: cfg.name.clone(),
243                cfg,
244                factory,
245                options,
246                rules,
247                on_event,
248                epoch: Instant::now(),
249                state: Mutex::new("connecting"),
250                inner: Mutex::new(Inner::default()),
251                writes: Mutex::new(VecDeque::new()),
252                wake: Condvar::new(),
253                stop: AtomicBool::new(false),
254                structure: RwLock::new(Arc::new(Structure::default())),
255                version: Mutex::new(Json::Null),
256                descriptions: Mutex::new(HashMap::new()),
257                stats: Mutex::new(Stats::default()),
258                #[cfg(test)]
259                hooks: Mutex::default(),
260            }),
261            thread: Mutex::new(None),
262        }
263    }
264
265    /// Starts the thread.
266    pub fn start(&self) {
267        let shared = Arc::clone(&self.shared);
268        let handle = std::thread::Builder::new()
269            .name(format!("plc-{}", self.shared.name))
270            .spawn(move || shared.run())
271            .ok();
272        *lock(&self.thread) = handle;
273    }
274
275    /// Stops the thread; writes queued until then still run (a momentary release must reach the PLC).
276    pub fn stop(&self) {
277        self.shared.stop.store(true, Ordering::SeqCst);
278        {
279            let _writes = lock(&self.shared.writes);
280            self.shared.wake.notify_all();
281        }
282        if let Some(handle) = lock(&self.thread).take() {
283            let _ = handle.join();
284        }
285        self.shared.fail_writes("server stopping"); // any queued after the thread ended
286    }
287
288    /// `connecting`, `connected`, `disconnected` or `stopped`.
289    pub fn state(&self) -> &'static str {
290        *lock(&self.shared.state)
291    }
292
293    /// The tag table version, null when unknown.
294    pub fn version(&self) -> Json {
295        lock(&self.shared.version).clone()
296    }
297
298    /// The controller structure last loaded.
299    pub fn structure(&self) -> Arc<Structure> {
300        Arc::clone(
301            &self
302                .shared
303                .structure
304                .read()
305                .unwrap_or_else(PoisonError::into_inner),
306        )
307    }
308
309    /// Counters for `/health` and `/stats`.
310    pub fn stats(&self) -> Stats {
311        lock(&self.shared.stats).clone()
312    }
313
314    /// Number of tags with a tier.
315    pub fn subscriptions(&self) -> usize {
316        lock(&self.shared.inner).tiers.len()
317    }
318
319    /// `{"tier1": n, "tier2": n, "tier3": n}`.
320    pub fn tier_counts(&self) -> Map<String, Json> {
321        let inner = lock(&self.shared.inner);
322        (1..=3u8)
323            .map(|n| {
324                let count = inner.tiers.values().filter(|&&t| t == n).count();
325                (format!("tier{n}"), json!(count))
326            })
327            .collect()
328    }
329
330    /// `{group: {rate_ms, tags (at tier 1 now), last_period_ms}}` for `/stats`: the poll groups, and the decorators
331    /// (or `"tag"` for a rate_ms set inline) whose rate a tag was read at.
332    pub fn group_stats(&self) -> Map<String, Json> {
333        let shared = &self.shared;
334        let (counts, reads, rates) = {
335            let mut inner = lock(&shared.inner);
336            let mut counts: HashMap<String, u64> = HashMap::new();
337            let tier1: Vec<String> = inner
338                .tiers
339                .iter()
340                .filter(|(_, t)| **t == 1)
341                .map(|(tag, _)| tag.clone())
342                .collect();
343            for tag in tier1 {
344                if let Some(group) = shared.group(&mut inner, &tag) {
345                    *counts.entry(group.name).or_default() += 1;
346                }
347            }
348            let mut rates: IndexMap<String, u64> = IndexMap::new();
349            for g in shared.rules.groups(&shared.name) {
350                if let (Some(name), Some(rate)) =
351                    (g.get("name"), g.get("rate_ms").and_then(Json::as_u64))
352                {
353                    let name = name
354                        .as_str()
355                        .map_or_else(|| name.to_string(), str::to_owned);
356                    rates.insert(name, rate);
357                }
358            }
359            for (name, rate) in &inner.group_rates {
360                rates.insert(name.clone(), *rate);
361            }
362            (counts, inner.group_reads.clone(), rates)
363        };
364        rates
365            .into_iter()
366            .map(|(name, rate)| {
367                let period = match reads.get(&name) {
368                    Some((Some(prev), last)) => json!(round1((last - prev) * 1000.0)),
369                    _ => Json::Null,
370                };
371                let tags = counts.get(&name).copied().unwrap_or(0);
372                (
373                    name,
374                    json!({"rate_ms": rate, "tags": tags, "last_period_ms": period}),
375                )
376            })
377            .collect()
378    }
379
380    /// A tag's tier, None when nothing subscribes it. A tag leaving tier 1 for tier 3 or nothing is read at tier 2
381    /// for grace_s first, so a page visited again finds its values fresh.
382    pub fn set_tier(&self, tag: &str, tier: Option<u8>) {
383        let shared = &self.shared;
384        let mut inner = lock(&shared.inner);
385        if tier.is_some_and(|t| t <= 2) || shared.options.grace_s <= 0.0 {
386            shared.set(&mut inner, tag, tier);
387        } else if inner.tiers.get(tag) == Some(&1) {
388            shared.set(&mut inner, tag, Some(2));
389            let deadline = shared.clock() + shared.options.grace_s;
390            inner.grace.insert(tag.to_owned(), (deadline, tier));
391        } else if let Some(grace) = inner.grace.get_mut(tag) {
392            grace.1 = tier;
393        } else {
394            shared.set(&mut inner, tag, tier);
395        }
396    }
397
398    /// The latest cache entry of a tag.
399    pub fn snapshot(&self, tag: &str) -> Option<Entry> {
400        lock(&self.shared.inner).cache.get(tag).cloned()
401    }
402
403    /// Queues a write, run between the packets of the current tick; `done` gets the result. Never retried.
404    pub fn write(&self, tag: &str, value: Value, done: WriteDone) {
405        if self.state() != "connected" {
406            done(WriteResult::failed("PLC not connected", "refused"));
407            return;
408        }
409        let mut writes = lock(&self.shared.writes);
410        writes.push_back(WriteRequest {
411            tag: tag.to_owned(),
412            value,
413            done,
414        });
415        self.shared.wake.notify_all();
416    }
417
418    /// The type of a tag from the connected controller; a bit is BOOL, the type is None while not connected.
419    pub fn type_info(&self, tag: &str) -> TypeInfo {
420        if split_bit(tag).is_some() {
421            return TypeInfo {
422                type_: Some("BOOL".into()),
423                dims: None,
424            };
425        }
426        if self.state() != "connected" {
427            return TypeInfo::default();
428        }
429        let structure = self.structure();
430        let Some(node) = structure.catalog.info(tag) else {
431            return TypeInfo::default();
432        };
433        let type_ = Some(node.client_type().to_owned()).filter(|t| !t.is_empty());
434        let dims = (!tag.ends_with(']') && !node.dims.is_empty()).then(|| node.client_dims());
435        TypeInfo { type_, dims }
436    }
437
438    /// The controller description of a tag, if read.
439    pub fn description(&self, tag: &str) -> Option<String> {
440        lock(&self.shared.descriptions).get(tag).cloned().flatten()
441    }
442}
443
444/// Test hooks for the hub's tests, which drive a worker without its thread (as the Python tests' fake worker).
445#[cfg(test)]
446impl PlcWorker {
447    /// Tag -> tier.
448    pub(crate) fn tiers(&self) -> IndexMap<String, u8> {
449        lock(&self.shared.inner).tiers.clone()
450    }
451
452    /// Loads the controller structure from `plc` and marks the worker connected.
453    pub(crate) fn load(&self, plc: &mut dyn Plc) {
454        self.shared.check_version(plc, true);
455        *lock(&self.shared.state) = "connected";
456    }
457
458    /// Sets the connection state, without an event.
459    pub(crate) fn set_state(&self, state: &'static str) {
460        *lock(&self.shared.state) = state;
461    }
462
463    /// Sets a cache entry.
464    pub(crate) fn set_cache(&self, tag: &str, entry: Entry) {
465        lock(&self.shared.inner).cache.insert(tag.to_owned(), entry);
466    }
467
468    /// Sets a controller description.
469    pub(crate) fn set_description(&self, tag: &str, description: Option<&str>) {
470        lock(&self.shared.descriptions).insert(tag.to_owned(), description.map(str::to_owned));
471    }
472
473    /// Runs the queued writes on `plc`, as the thread does between packets.
474    pub(crate) fn run_writes(&self, plc: &mut dyn Plc) -> Result<(), String> {
475        self.shared.run_writes(plc)
476    }
477}
478
479impl Shared {
480    fn clock(&self) -> f64 {
481        #[cfg(test)]
482        if let Some(time) = self.fake_time() {
483            return time.now();
484        }
485        self.epoch.elapsed().as_secs_f64()
486    }
487
488    #[cfg(test)]
489    fn fake_time(&self) -> Option<Arc<dyn FakeTime>> {
490        lock(&self.hooks).time.clone()
491    }
492
493    fn backoff(&self, attempt: usize) -> f64 {
494        #[cfg(test)]
495        {
496            let hooks = lock(&self.hooks);
497            hooks.backoff[attempt.min(hooks.backoff.len() - 1)]
498        }
499        #[cfg(not(test))]
500        BACKOFF[attempt.min(BACKOFF.len() - 1)]
501    }
502
503    fn missing_retry(&self) -> f64 {
504        #[cfg(test)]
505        return lock(&self.hooks).missing_retry;
506        #[cfg(not(test))]
507        MISSING_RETRY
508    }
509
510    fn group_retry(&self) -> f64 {
511        #[cfg(test)]
512        return lock(&self.hooks).group_retry;
513        #[cfg(not(test))]
514        GROUP_RETRY
515    }
516
517    fn emit(&self, event: Event) {
518        (self.on_event)(event);
519    }
520
521    fn set_state(&self, state: &'static str) {
522        let changed = {
523            let mut current = lock(&self.state);
524            let changed = *current != state;
525            *current = state;
526            changed
527        };
528        if changed {
529            log::info!("{}: {state}", self.name);
530            self.emit(Event::Status(self.name.clone(), state));
531        }
532    }
533
534    /// Sets a tag's tier, holding the lock.
535    fn set(&self, inner: &mut Inner, tag: &str, tier: Option<u8>) {
536        inner.grace.shift_remove(tag);
537        match tier {
538            None => {
539                inner.tiers.shift_remove(tag);
540                inner.cache.remove(tag);
541                inner.published.remove(tag);
542                inner.missing.remove(tag);
543                inner.groups.remove(tag);
544                inner.want_descriptions.remove(tag);
545            }
546            Some(t) => {
547                if t == 2 && matches!(inner.tiers.get(tag), None | Some(3)) {
548                    inner.new_tier2.insert(tag.to_owned()); // never read yet: once now, not a period later
549                }
550                if !inner.tiers.contains_key(tag) && split_bit(tag).is_none() {
551                    inner.want_descriptions.insert(tag.to_owned());
552                }
553                inner.tiers.insert(tag.to_owned(), t);
554            }
555        }
556        if tier != Some(2) {
557            inner.new_tier2.remove(tag);
558        }
559        if tier != Some(1) {
560            inner.last_read.remove(tag); // back at tier 1 it is due at the next tick
561        }
562        inner.tier2 = None;
563        inner.plans.clear();
564    }
565
566    fn group(&self, inner: &mut Inner, tag: &str) -> Option<Group> {
567        if let Some(g) = inner.groups.get(tag) {
568            return g.clone();
569        }
570        let g = self.rules.group(&self.name, tag);
571        inner.groups.insert(tag.to_owned(), g.clone());
572        g
573    }
574
575    fn rate(&self, inner: &mut Inner, tag: &str) -> f64 {
576        self.group(inner, tag)
577            .map_or(self.options.cycle, |g| g.rate_ms as f64 / 1000.0)
578    }
579
580    /// (tags to read this tick, tick in seconds, whether it is a cycle_ms boundary), holding the lock. Tier-1 tags
581    /// when due at their rate; the tier-2 slice, grace expiry and version check on boundaries.
582    fn cycle_tags(&self, inner: &mut Inner, now: f64) -> (Vec<String>, f64, bool) {
583        let cycle = self.options.cycle;
584        let tier1 = |inner: &Inner| -> Vec<String> {
585            inner
586                .tiers
587                .iter()
588                .filter(|(_, t)| **t == 1)
589                .map(|(tag, _)| tag.clone())
590                .collect()
591        };
592        let mut tier1_tags = tier1(inner);
593        let mut tick = cycle;
594        for tag in &tier1_tags {
595            tick = tick.min(self.rate(inner, tag));
596        }
597        let half = tick / 2.0;
598        let boundary = inner
599            .last_cycle
600            .is_none_or(|last| now + half >= last + cycle);
601        let mut tags = Vec::new();
602        if boundary {
603            inner.last_cycle = Some(now);
604            let expired: Vec<(String, Option<u8>)> = inner
605                .grace
606                .iter()
607                .filter(|(_, (deadline, _))| *deadline <= now)
608                .map(|(tag, (_, tier))| (tag.clone(), *tier))
609                .collect();
610            for (tag, tier) in expired {
611                self.set(inner, &tag, tier);
612            }
613            tier1_tags = tier1(inner);
614        }
615        let mut groups = HashSet::new();
616        for tag in tier1_tags {
617            let rate = self.rate(inner, &tag);
618            let last = inner
619                .last_read
620                .get(&tag)
621                .copied()
622                .unwrap_or(f64::NEG_INFINITY);
623            if now + half >= last + rate {
624                inner.last_read.insert(tag.clone(), now);
625                if let Some(group) = self.group(inner, &tag) {
626                    inner.group_rates.insert(group.name.clone(), group.rate_ms);
627                    groups.insert(group.name);
628                }
629                tags.push(tag);
630            }
631        }
632        for name in groups {
633            let prev = inner.group_reads.get(&name).map(|r| r.1);
634            inner.group_reads.insert(name, (prev, now));
635        }
636        if boundary {
637            if inner.tier2.is_none() {
638                // in name order, so members of one struct tend to share a slice
639                let mut tier2: Vec<String> = inner
640                    .tiers
641                    .iter()
642                    .filter(|(_, t)| **t == 2)
643                    .map(|(tag, _)| tag.clone())
644                    .collect();
645                tier2.sort_by_key(|t| t.to_lowercase());
646                inner.tier2 = Some(tier2);
647            }
648            let tier2 = inner.tier2.clone().unwrap_or_default();
649            if !tier2.is_empty() {
650                let slices = if cycle > 0.0 {
651                    ((self.options.tier2_period / cycle).round_ties_even() as usize).max(1)
652                } else {
653                    1
654                };
655                let size = tier2.len().div_ceil(slices);
656                let i = inner.slice % slices;
657                inner.slice += 1;
658                let start = (i * size).min(tier2.len());
659                let end = ((i + 1) * size).min(tier2.len());
660                tags.extend_from_slice(&tier2[start..end]);
661            }
662        }
663        if !inner.new_tier2.is_empty() {
664            let have: HashSet<&String> = tags.iter().collect();
665            let mut new: Vec<String> = inner
666                .new_tier2
667                .iter()
668                .filter(|t| !have.contains(t))
669                .cloned()
670                .collect();
671            new.sort();
672            tags.extend(new);
673            inner.new_tier2.clear();
674        }
675        let tags = tags
676            .into_iter()
677            .filter(|tag| inner.missing.get(tag).is_none_or(|&at| at <= now))
678            .collect();
679        (tags, tick, boundary)
680    }
681
682    // ------------------------------------------------------------------------------------------ the thread
683
684    /// The default wait: returns early when a write is queued, true once stopped.
685    fn wait(&self, seconds: f64) -> bool {
686        #[cfg(test)]
687        if let Some(time) = self.fake_time() {
688            return time.wait(self, seconds);
689        }
690        let writes = lock(&self.writes);
691        if writes.is_empty() && !self.stop.load(Ordering::SeqCst) {
692            let _ = self
693                .wake
694                .wait_timeout(writes, Duration::from_secs_f64(seconds.max(0.0)))
695                .unwrap_or_else(PoisonError::into_inner);
696        }
697        self.stop.load(Ordering::SeqCst)
698    }
699
700    /// Sleeps until the next tick, running each write as it is queued; true once stopped.
701    fn idle(&self, plc: &mut dyn Plc, start: f64, tick: f64) -> Result<bool, String> {
702        loop {
703            let remaining = tick - (self.clock() - start);
704            if remaining <= 1e-4 {
705                return Ok(false);
706            }
707            if self.wait(remaining) {
708                return Ok(true);
709            }
710            self.run_writes(plc)?;
711        }
712    }
713
714    fn run(&self) {
715        let mut attempt = 0;
716        while !self.stop.load(Ordering::SeqCst) {
717            match (self.factory)(&self.cfg) {
718                Ok(mut plc) => {
719                    if let Err(err) = self.connected(plc.as_mut(), &mut attempt) {
720                        log::warn!("{}: {err}", self.name);
721                    }
722                    plc.close();
723                }
724                Err(err) => log::warn!("{}: {err}", self.name),
725            }
726            if self.stop.load(Ordering::SeqCst) {
727                break;
728            }
729            self.fail_writes("PLC disconnected");
730            self.mark_stale();
731            self.set_state("disconnected");
732            let delay = self.backoff(attempt);
733            attempt += 1;
734            if self.wait(delay) {
735                break;
736            }
737            lock(&self.stats).reconnects += 1;
738            self.set_state("connecting");
739        }
740        *lock(&self.state) = "stopped"; // later writes are refused
741        self.fail_writes("server stopping");
742    }
743
744    fn connected(&self, plc: &mut dyn Plc, attempt: &mut usize) -> Result<(), String> {
745        plc.open()?;
746        self.fail_writes("PLC disconnected"); // any left over from before the connect
747        self.check_version(plc, true);
748        *attempt = 0;
749        self.set_state("connected");
750        self.poll(plc)
751    }
752
753    fn poll(&self, plc: &mut dyn Plc) -> Result<(), String> {
754        while !self.stop.load(Ordering::SeqCst) {
755            let start = self.clock();
756            let (tags, tick, boundary) = self.cycle_tags(&mut lock(&self.inner), start);
757            let mut services = 0;
758            if !tags.is_empty() {
759                let (results, sent) = self.read(plc, &tags, start)?;
760                services = sent;
761                self.apply(results, start, true);
762            }
763            {
764                let mut stats = lock(&self.stats);
765                stats.reads_per_cycle = services;
766                stats.tags_per_cycle = tags.len();
767            }
768            self.describe(plc);
769            if boundary {
770                self.check_version(plc, false);
771            }
772            self.run_writes(plc)?;
773            let elapsed = self.clock() - start;
774            {
775                let mut stats = lock(&self.stats);
776                stats.last_ok_ts = Some(now_ms());
777                stats.last_cycle_ms = Some(round1(elapsed * 1000.0));
778                if elapsed > tick {
779                    stats.overruns += 1;
780                }
781            }
782            if elapsed <= tick && self.idle(plc, start, tick)? {
783                break;
784            }
785        }
786        self.run_writes(plc) // stopping: the last queued writes (a momentary release) still go out
787    }
788
789    /// Caches read results and publishes the changes: quality or error changed, or the value moved past the
790    /// deadband from the last published value. A read-back (no deadband) is published unless already published.
791    fn apply(&self, results: HashMap<String, TagResult>, start: f64, deadband: bool) {
792        let t = now_ms();
793        let mut changes = Vec::new();
794        {
795            let mut inner = lock(&self.inner);
796            let mut ordered: Vec<(String, TagResult)> = results.into_iter().collect();
797            ordered.sort_by(|a, b| a.0.cmp(&b.0));
798            for (tag, result) in ordered {
799                if !inner.tiers.contains_key(&tag) {
800                    continue; // unsubscribed during the read
801                }
802                let old = inner.cache.get(&tag).cloned();
803                let entry = merge(old.as_ref(), &result, t);
804                if entry.quality == "missing" {
805                    inner
806                        .missing
807                        .insert(tag.clone(), start + self.missing_retry()); // don't ask every tick
808                } else {
809                    inner.missing.remove(&tag);
810                }
811                if old.as_ref().is_none_or(|o| !o.same(&entry)) {
812                    inner.cache.insert(tag.clone(), entry.clone());
813                } else if deadband {
814                    continue;
815                }
816                if let Some(published) = inner.published.get(&tag) {
817                    let skip = if deadband {
818                        self.within_deadband(&tag, published, &entry)
819                    } else {
820                        published.same(&entry)
821                    };
822                    if skip {
823                        continue;
824                    }
825                }
826                inner.published.insert(tag.clone(), entry.clone());
827                changes.push((tag, entry));
828            }
829        }
830        if !changes.is_empty() {
831            self.emit(Event::Values(self.name.clone(), changes));
832        }
833    }
834
835    fn within_deadband(&self, tag: &str, published: &Entry, entry: &Entry) -> bool {
836        if published.quality != "good" || entry.quality != "good" || published.error != entry.error
837        {
838            return false;
839        }
840        let settings = self.rules.effective(&self.name, tag);
841        let band = settings
842            .get("deadband")
843            .and_then(Json::as_f64)
844            .unwrap_or(0.0);
845        match (published.value.as_f64(), entry.value.as_f64()) {
846            (Some(a), Some(b)) if truthy(settings.get("deadband")) => (b - a).abs() < band,
847            _ => false,
848        }
849    }
850
851    /// (results by tag, read services sent), through the planner unless it is off. Queued writes run between the
852    /// packet chunks, after the chunk's results are published.
853    fn read(
854        &self,
855        plc: &mut dyn Plc,
856        tags: &[String],
857        start: f64,
858    ) -> Result<(HashMap<String, TagResult>, usize), String> {
859        let parts: Arc<Vec<Vec<Read>>> = if self.options.planner {
860            let structure = Arc::clone(
861                &self
862                    .structure
863                    .read()
864                    .unwrap_or_else(PoisonError::into_inner),
865            );
866            let mut inner = lock(&self.inner);
867            let group_retry = self.group_retry();
868            let retry: Vec<String> = inner
869                .no_group
870                .iter()
871                .filter(|(_, at)| start - **at >= group_retry)
872                .map(|(name, _)| name.clone())
873                .collect();
874            if !retry.is_empty() {
875                for name in retry {
876                    inner.no_group.remove(&name); // read those whole again
877                }
878                inner.plans.clear();
879            }
880            let mut key = tags.to_vec();
881            key.sort();
882            if let Some(parts) = inner.plans.get(&key) {
883                Arc::clone(parts)
884            } else {
885                if inner.plans.len() > 100 {
886                    inner.plans.clear(); // tier-2 slices and groups make a few sets per subscription change
887                }
888                let budget = match plc.connection_size() {
889                    0 => planner::PACKET_BYTES,
890                    n => n,
891                };
892                let no_group: HashSet<String> = inner.no_group.keys().cloned().collect();
893                let together =
894                    |path: &str| truthy(self.rules.effective(&self.name, path).get("together"));
895                let reads = planner::plan(
896                    tags,
897                    &structure.catalog,
898                    self.options.whole_max_bytes,
899                    &no_group,
900                    budget,
901                    Some(&together),
902                );
903                let parts = Arc::new(planner::chunks(reads, &structure.catalog, budget));
904                inner.plans.insert(key, Arc::clone(&parts));
905                parts
906            }
907        } else {
908            let reads = tags
909                .iter()
910                .map(|tag| Read {
911                    name: tag.clone(),
912                    members: vec![(tag.clone(), None)],
913                })
914                .collect();
915            Arc::new(vec![reads])
916        };
917        let mut between =
918            |plc: &mut dyn Plc, partial: HashMap<String, TagResult>| -> Result<(), String> {
919                self.apply(partial, start, true);
920                self.run_writes(plc)
921            };
922        let (results, failed, services) = planner::execute(plc, &parts, &mut between)?;
923        if !failed.is_empty() {
924            lock(&self.stats).group_fallbacks += failed.len() as u64;
925            log::warn!(
926                "{}: whole read of {} failed, reading the members instead",
927                self.name,
928                failed.join(", ")
929            );
930            let mut inner = lock(&self.inner);
931            for name in failed {
932                inner.no_group.insert(name.to_lowercase(), start);
933            }
934            inner.plans.clear();
935        }
936        Ok((results, services))
937    }
938
939    fn run_writes(&self, plc: &mut dyn Plc) -> Result<(), String> {
940        loop {
941            let Some(request) = lock(&self.writes).pop_front() else {
942                return Ok(());
943            };
944            let WriteRequest { tag, value, done } = request;
945            let result = match plc.write(&tag, &value) {
946                Ok(result) => result,
947                Err(err) => {
948                    done(WriteResult::failed(err.clone(), "comm"));
949                    return Err(err);
950                }
951            };
952            if let Some(error) = result.error {
953                done(WriteResult::failed(error.message, error.kind.as_str()));
954                continue;
955            }
956            let mut names = vec![tag.clone()];
957            if let Some((word, _)) = split_bit(&tag) {
958                names.push(word.to_owned());
959            }
960            let refs: Vec<&str> = names.iter().map(String::as_str).collect();
961            let results = match plc.read(&refs) {
962                Ok(results) => results,
963                Err(err) => {
964                    done(WriteResult {
965                        ok: true,
966                        error: None,
967                        kind: None,
968                        value: Json::Null,
969                    });
970                    return Err(err);
971                }
972            };
973            let results: HashMap<String, TagResult> = names.into_iter().zip(results).collect();
974            let value = results.get(&tag).map_or(Json::Null, |r| {
975                let (value, quality, _) = to_value(r);
976                if quality == "good" { value } else { Json::Null }
977            });
978            done(WriteResult {
979                ok: true,
980                error: None,
981                kind: None,
982                value,
983            });
984            self.apply(results, self.clock(), false);
985        }
986    }
987
988    /// Reads the controller descriptions of tags that entered tiers, in one call, and tells the hub.
989    fn describe(&self, plc: &mut dyn Plc) {
990        let mut tags: Vec<String> = {
991            let mut inner = lock(&self.inner);
992            inner.want_descriptions.drain().collect()
993        };
994        tags.sort();
995        // a rule description wins: no need to ask
996        tags.retain(|t| !truthy(self.rules.effective(&self.name, t).get("description")));
997        {
998            let known = lock(&self.descriptions);
999            tags.retain(|t| !known.contains_key(t));
1000        }
1001        if tags.is_empty() || !plc.describes() {
1002            return; // a driver without descriptions (the mock): no info to send again
1003        }
1004        let refs: Vec<&str> = tags.iter().map(String::as_str).collect();
1005        let found = match plc.descriptions(&refs) {
1006            Ok(found) => found,
1007            Err(err) => {
1008                log::info!("{}: descriptions not read: {err}", self.name);
1009                vec![None; tags.len()]
1010            }
1011        };
1012        {
1013            let mut known = lock(&self.descriptions);
1014            for (tag, description) in tags.iter().zip(found) {
1015                known.insert(tag.clone(), description);
1016            }
1017        }
1018        self.emit(Event::Info(self.name.clone(), tags));
1019    }
1020
1021    /// Counts a reload and tells the clients when the driver's tag table version changed.
1022    fn check_version(&self, plc: &mut dyn Plc, connecting: bool) {
1023        let version = plc.version();
1024        let (changed, reloaded) = {
1025            let mut current = lock(&self.version);
1026            let changed = version != *current;
1027            let reloaded = changed && !current.is_null();
1028            *current = version.clone();
1029            (changed, reloaded)
1030        };
1031        if reloaded {
1032            lock(&self.stats).reloads += 1;
1033            log::info!("{}: tag table reloaded (version {version})", self.name);
1034        }
1035        if changed || connecting {
1036            let catalog = plc.catalog();
1037            let tag_list = catalog.tag_list();
1038            let types = catalog.type_map();
1039            self.rules.set_types(&self.name, &tag_list, &types); // UDT entries apply from now on
1040            *self
1041                .structure
1042                .write()
1043                .unwrap_or_else(PoisonError::into_inner) = Arc::new(Structure {
1044                catalog,
1045                tag_list,
1046                types,
1047            });
1048            let mut inner = lock(&self.inner);
1049            inner.groups.clear();
1050            inner.missing.clear(); // a new or renamed tag may exist now
1051            inner.no_group.clear();
1052            inner.plans.clear(); // members or struct sizes may have changed
1053            if changed {
1054                // descriptions are cached per version: ask again for the subscribed tags
1055                lock(&self.descriptions).clear();
1056                let tags: Vec<String> = inner
1057                    .tiers
1058                    .keys()
1059                    .filter(|t| split_bit(t).is_none())
1060                    .cloned()
1061                    .collect();
1062                inner.want_descriptions.extend(tags);
1063            }
1064        }
1065        if reloaded {
1066            self.emit(Event::Meta(self.name.clone()));
1067        }
1068    }
1069
1070    fn fail_writes(&self, error: &str) {
1071        loop {
1072            let Some(request) = lock(&self.writes).pop_front() else {
1073                return;
1074            };
1075            (request.done)(WriteResult::failed(error, "comm"));
1076        }
1077    }
1078
1079    fn mark_stale(&self) {
1080        let mut changes = Vec::new();
1081        {
1082            let mut inner = lock(&self.inner);
1083            let Inner {
1084                cache, published, ..
1085            } = &mut *inner;
1086            for (tag, entry) in cache.iter_mut() {
1087                if entry.quality != "stale" {
1088                    entry.quality = "stale";
1089                    entry.error = Json::Null;
1090                    published.insert(tag.clone(), entry.clone());
1091                    changes.push((tag.clone(), entry.clone()));
1092                }
1093            }
1094        }
1095        if !changes.is_empty() {
1096            changes.sort_by(|a, b| a.0.cmp(&b.0));
1097            self.emit(Event::Values(self.name.clone(), changes));
1098        }
1099    }
1100}
1101
1102#[cfg(test)]
1103#[path = "worker_tests.rs"]
1104mod tests;