From 42a770ca4ef92271cf978d81374f71aee3c7ccfd Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 16 Aug 2024 15:36:22 -0700 Subject: [PATCH] rewrite the whole thing to use less memory etc --- .../configs/sp_sim_config.test.toml | 2 +- gateway/src/metrics.rs | 651 +++++++++++------- 2 files changed, 390 insertions(+), 263 deletions(-) diff --git a/gateway-test-utils/configs/sp_sim_config.test.toml b/gateway-test-utils/configs/sp_sim_config.test.toml index fd430f027bd..6be27c28e66 100644 --- a/gateway-test-utils/configs/sp_sim_config.test.toml +++ b/gateway-test-utils/configs/sp_sim_config.test.toml @@ -161,7 +161,7 @@ device = "sp3-host-cpu" description = "FAKE host cpu" capabilities = 0 presence = "Present" -serial_console = "[::1]:0" +# serial_console = "[::1]:0" [[simulated_sps.gimlet.components]] id = "dev-0" diff --git a/gateway/src/metrics.rs b/gateway/src/metrics.rs index 32543632e0f..8ad76533d9e 100644 --- a/gateway/src/metrics.rs +++ b/gateway/src/metrics.rs @@ -21,8 +21,9 @@ use std::collections::HashMap; use std::net::IpAddr; use std::net::SocketAddr; use std::net::SocketAddrV6; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::broadcast; use tokio::sync::oneshot; use tokio::sync::watch; use tokio::task::JoinHandle; @@ -60,39 +61,39 @@ pub struct Args { bind_loopback: bool, } -/// Actually polls SP sensor readings -struct Poller { - samples: Arc>>>, +/// Manages SP pollers, making sure that every SP has a poller task. +struct PollerManager { log: slog::Logger, apictx: Arc, mgs_id: Uuid, + /// Poller tasks + tasks: tokio::task::JoinSet>, + /// The manager doesn't actually produce samples, but it needs to be able to + /// clone a sender for every poller task it spawns. + sample_tx: broadcast::Sender>, } -/// Manages a metrics server and stuff. -struct ServerManager { - log: slog::Logger, - addrs: watch::Receiver>, - registry: ProducerRegistry, - args: Args, -} - +/// Polls sensor readings from an individual SP. struct SpPoller { apictx: Arc, spid: SpIdentifier, - my_understanding: Mutex, + devices: Vec<(SpComponent, component::Component)>, log: slog::Logger, rack_id: Uuid, mgs_id: Uuid, + sample_tx: broadcast::Sender>, } -#[derive(Default)] -struct SpUnderstanding { - state: Option, - devices: Vec<(SpComponent, component::Component)>, +/// Manages a metrics server and stuff. +struct ServerManager { + log: slog::Logger, + addrs: watch::Receiver>, + registry: ProducerRegistry, + args: Args, } #[derive(Debug)] -struct Producer(Arc>>>); +struct Producer(broadcast::Receiver>); /// The interval on which we ask `oximeter` to poll us for metric data. // N.B.: I picked this pretty arbitrarily... @@ -105,6 +106,44 @@ const POLL_INTERVAL: Duration = Duration::from_secs(1); /// The maximum Dropshot request size for the metrics server. const METRIC_REQUEST_MAX_SIZE: usize = 10 * 1024 * 1024; +/// The expected number of SPs in a fully-loaded rack. +/// +/// N.B. that there *might* be more than this; we shouldn't ever panic or +/// otherwise misbehave if we see more than this number. This is just intended +/// for sizing buffers/map allocations and so forth; we can always realloc if we +/// see a bonus SP or two. That's why it's called "normal number of SPs" and not +/// "MAX_SPS" or similar. +/// +/// Additionally, note that we always determine the channel capacity based on +/// the assumption that *someday*, the rack might be fully loaded with compute +/// sleds, even if it isn't *right now*. A rack with 16 sleds could always grow +/// another 16 later! +const NORMAL_NUMBER_OF_SPS: usize = + 32 // 32 compute sleds + + 2 // two switches + + 2 // two power shelves, someday. + ; + +/// Number of sample vectors from individual SPs to buffer. +const SAMPLE_CHANNEL_CAPACITY: usize = { + // Roughly how many times will we poll SPs for each metrics collection + // interval? + let polls_per_metrics_interval = (METRIC_COLLECTION_INTERVAL.as_secs() + / POLL_INTERVAL.as_secs()) + as usize; + // How many sample collection intervals do we want to allow to elapse before + // we start putting stuff on the floor? + // + // Let's say 16. Chosen totally arbitrarily but seems reasonable-ish. + let sloppiness = 16; + let capacity = + NORMAL_NUMBER_OF_SPS * polls_per_metrics_interval * sloppiness; + // Finally, the buffer capacity will probably be allocated in a power of two + // anyway, so let's make sure our thing is a power of two so we don't waste + // the allocation we're gonna get anyway. + capacity.next_power_of_two() +}; + impl Metrics { pub fn new( log: &slog::Logger, @@ -113,15 +152,28 @@ impl Metrics { ) -> anyhow::Result { let &MgsArguments { id, rack_id, ref addresses, metrics_args } = args; let registry = ProducerRegistry::with_id(id); - let samples = Arc::new(Mutex::new(Vec::new())); + + // Create a channel for the SP poller tasks to send samples to the + // Oximeter producer endpoint. + // + // A broadcast channel is used here, not because we are actually + // multi-consumer (`Producer::produce` is never called concurrently), + // but because the broadcast channel has properly ring-buffer-like + // behavior, where earlier messages are discarded, rather than exerting + // backpressure on senders (as Tokio's MPSC channel does). This + // is what we want, as we would prefer a full buffer to result in + // clobbering the oldest measurements, rather than leaving the newest + // ones on the floor. + let (sample_tx, sample_rx) = + broadcast::channel(SAMPLE_CHANNEL_CAPACITY); registry - .register_producer(Producer(samples.clone())) + .register_producer(Producer(sample_rx)) .context("failed to register metrics producer")?; // Using a channel for this is, admittedly, a bit of an end-run around // the `OnceLock` on the `ServerContext` that *also* stores the rack ID, - // but it has the nice benefit of allowing the `Poller` task to _await_ + // but it has the nice benefit of allowing the `PollerManager` task to _await_ // the rack ID being set...we might want to change other code to use a // similar approach in the future. let (rack_id_tx, rack_id_rx) = oneshot::channel(); @@ -135,9 +187,10 @@ impl Metrics { Some(rack_id_tx) }; let poller = tokio::spawn( - Poller { - samples, + PollerManager { + sample_tx, apictx, + tasks: tokio::task::JoinSet::new(), log: log.new(slog::o!("component" => "sensor-poller")), mgs_id: id, } @@ -198,16 +251,37 @@ impl oximeter::Producer for Producer { fn produce( &mut self, ) -> Result>, MetricsError> { - let samples = { - let mut lock = self.0.lock().unwrap(); - std::mem::take(&mut *lock) - }; + // Drain all samples currently in the queue into a `Vec`. + // + // N.B. it may be tempting to pursue an alternative design where we + // implement `Iterator` for a `broadcast::Receiver>` and + // just return that using `Receiver::resubscribe`...DON'T DO THAT! The + // `resubscribe` function creates a receiver at the current *tail* of + // the ringbuffer, so it won't see any samples produced *before* now. + // Which is the opposite of what we want! + let mut samples = Vec::with_capacity(self.0.len()); + use broadcast::error::TryRecvError; + loop { + match self.0.try_recv() { + Ok(sample_chunk) => samples.push(sample_chunk), + // This error indicates that an old ringbuffer entry was + // overwritten. That's fine, just get the next one. + Err(TryRecvError::Lagged(_)) => continue, + // We've drained all currently available samples! We're done here! + Err(TryRecvError::Empty) | Err(TryRecvError::Closed) => break, + } + } + + // There you go, that's all I've got. Ok(Box::new(samples.into_iter().flatten())) } } -impl Poller { - async fn run(self, rack_id: oneshot::Receiver) -> anyhow::Result<()> { +impl PollerManager { + async fn run( + mut self, + rack_id: oneshot::Receiver, + ) -> anyhow::Result<()> { let switch = &self.apictx.mgmt_switch; // First, wait until we know what the rack ID is... @@ -216,19 +290,22 @@ impl Poller { )?; let mut poll_interval = tokio::time::interval(POLL_INTERVAL); - let mut sps_as_i_understand_them = HashMap::new(); - let mut tasks = tokio::task::JoinSet::new(); - loop { + let mut known_sps: HashMap = + HashMap::with_capacity(NORMAL_NUMBER_OF_SPS); + // Wait for SP discovery to complete, if it hasn't already. + // TODO(eliza): presently, we busy-poll here. It would be nicer to + // replace the `OnceLock` in `ManagementSwitch` + // with a `tokio::sync::watch` + while !switch.is_discovery_complete() { poll_interval.tick().await; + } - // Wait for SP discovery to complete, if it hasn't already. - // TODO(eliza): presently, we busy-poll here. It would be nicer to - // replace the `OnceLock` in `ManagementSwitch` - // with a `tokio::sync::watch` - if !switch.is_discovery_complete() { - continue; - } + slog::info!( + &self.log, + "SP discovery complete! starting to poll sensors..." + ); + loop { let sps = match switch.all_sps() { Ok(sps) => sps, Err(e) => { @@ -237,264 +314,314 @@ impl Poller { "failed to enumerate service processors! will try again in a bit"; "error" => %e, ); + poll_interval.tick().await; continue; } }; for (spid, _) in sps { - let understanding = sps_as_i_understand_them - .entry(spid) - .or_insert_with(|| { - slog::debug!( + // Do we know about this li'l guy already? + match known_sps.get(&spid) { + // Okay, and has it got someone checking up on it? Right? + Some(poller) if poller.is_finished() => { + // Welp. + slog::info!( &self.log, - "found a new little friend!"; + "uh-oh! a known SP's poller task has gone AWOL. restarting it..."; "sp_slot" => ?spid.slot, "chassis_type" => ?spid.typ, ); - Arc::new(SpPoller { - spid, - rack_id, - mgs_id: self.mgs_id, - apictx: self.apictx.clone(), - log: self.log.new(slog::o!( - "sp_slot" => spid.slot, - "chassis_type" => format!("{:?}", spid.typ), - )), - my_understanding: Mutex::new(Default::default()), - }) - }) - .clone(); - tasks.spawn(understanding.clone().poll_sp()); - } - - while let Some(result) = tasks.join_next().await { - match result { - Ok(Ok(samples)) => { - // No sense copying all the samples into the big vec thing, - // just push the vec instead. - self.samples.lock().unwrap().push(samples); } - Ok(Err(error)) => { - // TODO(eliza): actually handle errors polling a SP - // nicely... - slog::error!( + Some(_) => continue, + None => { + slog::info!( &self.log, - "something bad happened"; - "error" => %error, + "found a new little friend!"; + "sp_slot" => ?spid.slot, + "chassis_type" => ?spid.typ, ); } - Err(_) => { - unreachable!( - "tasks on the joinset never get aborted, and we \ - compile with panic=abort, so they won't panic" - ) - } } + + let poller = SpPoller { + spid, + rack_id, + mgs_id: self.mgs_id, + apictx: self.apictx.clone(), + log: self.log.new(slog::o!( + "sp_slot" => spid.slot, + "chassis_type" => format!("{:?}", spid.typ), + )), + devices: Vec::new(), + sample_tx: self.sample_tx.clone(), + }; + let poller_handle = self.tasks.spawn(poller.run(POLL_INTERVAL)); + let _prev_poller = known_sps.insert(spid, poller_handle); + debug_assert!( + _prev_poller.map(|p| p.is_finished()).unwrap_or(true), + "if we clobbered an existing poller task, it better have \ + been because it was dead..." + ); + } + + // All pollers started! Now wait to see if any of them have died... + let mut err = self.tasks.join_next().await; + while let Some(Ok(Err(error))) = err { + // TODO(eliza): actually handle errors polling a SP + // nicely... + slog::error!( + &self.log, + "something bad happened while polling a SP..."; + "error" => %error, + ); + // drain any remaining errors + err = self.tasks.try_join_next(); } } } } +impl Drop for PollerManager { + fn drop(&mut self) { + // This is why the `JoinSet` is a field on the `PollerManager` struct + // rather than a local variable in `async fn run()`! + self.tasks.abort_all(); + } +} + impl SpPoller { - async fn poll_sp(self: Arc) -> anyhow::Result> { + async fn run(mut self, poll_interval: Duration) -> anyhow::Result<()> { let switch = &self.apictx.mgmt_switch; let sp = switch.sp(self.spid)?; - // Check if the SP's state has changed. If it has, we need to make sure - // we still know what all of its sensors are. - let current_state = sp.state().await?; - let known_state = self.my_understanding.lock().unwrap().state.clone(); - - let devices = if Some(¤t_state) != known_state.as_ref() { - slog::debug!( - &self.log, - "our little friend seems to have changed in some kind of way"; - "current_state" => ?current_state, - "known_state" => ?known_state, - ); - let inv_devices = sp.inventory().await?.devices; - let mut devices: Vec<(SpComponent, component::Component)> = - Vec::with_capacity(inv_devices.len()); - - // Reimplement this ourselves because we don't really care about - // reading the RoT state at present. This is unfortunately copied - // from `gateway_messages`. - fn stringify_byte_string(bytes: &[u8]) -> String { - // We expect serial and model numbers to be ASCII and 0-padded: find the first 0 - // byte and convert to a string. If that fails, hexlify the entire slice. - let first_zero = - bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - - std::str::from_utf8(&bytes[..first_zero]) - .map(|s| s.to_string()) - .unwrap_or_else(|_err| hex::encode(bytes)) - } - let (model, serial, hubris_archive_id, revision) = - match current_state { - VersionedSpState::V1(v) => ( - stringify_byte_string(&v.model), - stringify_byte_string(&v.serial_number[..]), - hex::encode(v.hubris_archive_id), - v.revision, - ), - VersionedSpState::V2(v) => ( - stringify_byte_string(&v.model), - stringify_byte_string(&v.serial_number[..]), - hex::encode(v.hubris_archive_id), - v.revision, - ), - VersionedSpState::V3(v) => ( - stringify_byte_string(&v.model), - stringify_byte_string(&v.serial_number[..]), - hex::encode(v.hubris_archive_id), - v.revision, - ), - }; - for dev in inv_devices { - // Skip devices which have nothing interesting for us. - if !dev - .capabilities - .contains(DeviceCapabilities::HAS_MEASUREMENT_CHANNELS) - { - continue; + let mut interval = tokio::time::interval(poll_interval); + let mut known_state = None; + loop { + interval.tick().await; + slog::trace!(&self.log, "interval elapsed, polling SP..."); + + // Check if the SP's state has changed. If it has, we need to make sure + // we still know what all of its sensors are. + let current_state = sp.state().await?; + if Some(¤t_state) != known_state.as_ref() { + // The SP's state appears to have changed. Time to make sure our + // understanding of its devices and identity is up to date! + slog::debug!( + &self.log, + "our little friend seems to have changed in some kind of way"; + "current_state" => ?current_state, + "known_state" => ?known_state, + ); + let inv_devices = sp.inventory().await?.devices; + + // Clear out any previously-known devices, and preallocate capacity + // for all the new ones. + self.devices.clear(); + self.devices.reserve(inv_devices.len()); + + // Reimplement this ourselves because we don't really care about + // reading the RoT state at present. This is unfortunately copied + // from `gateway_messages`. + fn stringify_byte_string(bytes: &[u8]) -> String { + // We expect serial and model numbers to be ASCII and 0-padded: find the first 0 + // byte and convert to a string. If that fails, hexlify the entire slice. + let first_zero = bytes + .iter() + .position(|&b| b == 0) + .unwrap_or(bytes.len()); + + std::str::from_utf8(&bytes[..first_zero]) + .map(|s| s.to_string()) + .unwrap_or_else(|_err| hex::encode(bytes)) } - let component_name = match dev.component.as_str() { - Some(c) => c, - None => { - // These are supposed to always be strings. But, if we - // see one that's not a string, bail instead of panicking. - slog::error!(&self.log, "a SP component ID was not a string! this isn't supposed to happen!"; "device" => ?dev); - anyhow::bail!("a SP component ID was not stringy!"); + let (model, serial, hubris_archive_id, revision) = + match current_state { + VersionedSpState::V1(ref v) => ( + stringify_byte_string(&v.model), + stringify_byte_string(&v.serial_number[..]), + hex::encode(v.hubris_archive_id), + v.revision, + ), + VersionedSpState::V2(ref v) => ( + stringify_byte_string(&v.model), + stringify_byte_string(&v.serial_number[..]), + hex::encode(v.hubris_archive_id), + v.revision, + ), + VersionedSpState::V3(ref v) => ( + stringify_byte_string(&v.model), + stringify_byte_string(&v.serial_number[..]), + hex::encode(v.hubris_archive_id), + v.revision, + ), + }; + for dev in inv_devices { + // Skip devices which have nothing interesting for us. + if !dev + .capabilities + .contains(DeviceCapabilities::HAS_MEASUREMENT_CHANNELS) + { + continue; } - }; - // TODO(eliza): i hate having to clone all these strings for - // every device on the SP...it would be cool if Oximeter let us - // reference count them... - let target = component::Component { - chassis_type: match self.spid.typ { - SpType::Sled => Cow::Borrowed("sled"), - SpType::Switch => Cow::Borrowed("switch"), - SpType::Power => Cow::Borrowed("power"), - }, - slot: self.spid.slot as u32, - component: Cow::Owned(component_name.to_string()), - device: Cow::Owned(dev.device), - model: Cow::Owned(model.clone()), - revision, - serial: Cow::Owned(serial.clone()), - rack_id: self.rack_id, - gateway_id: self.mgs_id, - hubris_archive_id: Cow::Owned(hubris_archive_id.clone()), - }; - devices.push((dev.component, target)) + let component_name = match dev.component.as_str() { + Some(c) => c, + None => { + // These are supposed to always be strings. But, if we + // see one that's not a string, bail instead of panicking. + slog::error!( + &self.log, + "a SP component ID was not a string! this isn't supposed to happen!"; + "device" => ?dev, + ); + anyhow::bail!("a SP component ID was not stringy!"); + } + }; + // TODO(eliza): i hate having to clone all these strings for + // every device on the SP...it would be cool if Oximeter let us + // reference count them... + let target = component::Component { + chassis_type: match self.spid.typ { + SpType::Sled => Cow::Borrowed("sled"), + SpType::Switch => Cow::Borrowed("switch"), + SpType::Power => Cow::Borrowed("power"), + }, + slot: self.spid.slot as u32, + component: Cow::Owned(component_name.to_string()), + device: Cow::Owned(dev.device), + model: Cow::Owned(model.clone()), + revision, + serial: Cow::Owned(serial.clone()), + rack_id: self.rack_id, + gateway_id: self.mgs_id, + hubris_archive_id: Cow::Owned( + hubris_archive_id.clone(), + ), + }; + self.devices.push((dev.component, target)) + } + + known_state = Some(current_state); } - devices - } else { - // This is a bit goofy, but we have to release the lock before - // hitting any `await` points, so just move the inventory out of it. - // We'll put it back when we're done. This lock is *actually* owned - // exclusively by this `SpPoller`, but since it lives in a HashMap, - // rust doesn't understand that. - std::mem::take(&mut self.my_understanding.lock().unwrap().devices) - }; - let mut samples = Vec::with_capacity(devices.len()); - for (c, target) in &devices { - let details = match sp.component_details(*c).await { - Ok(deets) => deets, - Err(error) => { + let mut samples = Vec::with_capacity(self.devices.len()); + for (c, target) in &self.devices { + let details = match sp.component_details(*c).await { + Ok(deets) => deets, + Err(error) => { + slog::warn!( + &self.log, + "failed to read details on SP component"; + "sp_component" => %c, + "error" => %error, + ); + // TODO(eliza): we should increment a metric here... + continue; + } + }; + if details.entries.is_empty() { slog::warn!( &self.log, - "failed to read details on SP component"; + "a component which claimed to have measurement channels \ + had empty details. this seems weird..."; "sp_component" => %c, - "error" => %error, ); - // TODO(eliza): we should increment a metric here... - continue; } - }; - if details.entries.is_empty() { - slog::warn!( + for d in details.entries { + let ComponentDetails::Measurement(m) = d else { + // If the component details are switch port details rather + // than measurement channels, ignore it for now. + continue; + }; + let name = Cow::Owned(m.name); + let sample = match (m.value, m.kind) { + (Ok(datum), MeasurementKind::Temperature) => { + Sample::new( + target, + &component::Temperature { name, datum }, + ) + } + (Ok(datum), MeasurementKind::Current) => Sample::new( + target, + &component::Current { name, datum }, + ), + (Ok(datum), MeasurementKind::Voltage) => Sample::new( + target, + &component::Voltage { name, datum }, + ), + (Ok(datum), MeasurementKind::Power) => Sample::new( + target, + &component::Power { name, datum }, + ), + (Ok(datum), MeasurementKind::InputCurrent) => { + Sample::new( + target, + &component::InputCurrent { name, datum }, + ) + } + (Ok(datum), MeasurementKind::InputVoltage) => { + Sample::new( + target, + &component::InputVoltage { name, datum }, + ) + } + (Ok(datum), MeasurementKind::Speed) => Sample::new( + target, + &component::FanSpeed { name, datum }, + ), + (Err(e), kind) => { + let sensor_kind = match kind { + MeasurementKind::Temperature => "temperature", + MeasurementKind::Current => "current", + MeasurementKind::Voltage => "voltage", + MeasurementKind::Power => "power", + MeasurementKind::InputCurrent => { + "input_current" + } + MeasurementKind::InputVoltage => { + "input_voltage" + } + MeasurementKind::Speed => "fan_speed", + }; + let error = match e { + MeasurementError::InvalidSensor => { + "invalid_sensor" + } + MeasurementError::NoReading => "no_reading", + MeasurementError::NotPresent => "not_present", + MeasurementError::DeviceError => "device_error", + MeasurementError::DeviceUnavailable => { + "device_unavailable" + } + MeasurementError::DeviceTimeout => { + "device_timeout" + } + MeasurementError::DeviceOff => "device_off", + }; + Sample::new( + target, + &component::SensorErrorCount { + error: Cow::Borrowed(error), + name, + datum: oximeter::types::Cumulative::new(1), + sensor_kind: Cow::Borrowed(sensor_kind), + }, + ) + } + }?; + samples.push(sample); + } + } + // No sense cluttering the ringbuffer with empty vecs... + if samples.is_empty() { + continue; + } + if let Err(_) = self.sample_tx.send(samples) { + slog::info!( &self.log, - "a component which claimed to have measurement channels \ - had empty details. this seems weird..."; - "sp_component" => %c, + "all sample receiver handles have been dropped! presumably we are shutting down..."; ); - } - for d in details.entries { - let ComponentDetails::Measurement(m) = d else { - // If the component details are switch port details rather - // than measurement channels, ignore it for now. - continue; - }; - let name = Cow::Owned(m.name); - let sample = match (m.value, m.kind) { - (Ok(datum), MeasurementKind::Temperature) => Sample::new( - target, - &component::Temperature { name, datum }, - ), - (Ok(datum), MeasurementKind::Current) => { - Sample::new(target, &component::Current { name, datum }) - } - (Ok(datum), MeasurementKind::Voltage) => { - Sample::new(target, &component::Voltage { name, datum }) - } - (Ok(datum), MeasurementKind::Power) => { - Sample::new(target, &component::Power { name, datum }) - } - (Ok(datum), MeasurementKind::InputCurrent) => Sample::new( - target, - &component::InputCurrent { name, datum }, - ), - (Ok(datum), MeasurementKind::InputVoltage) => Sample::new( - target, - &component::InputVoltage { name, datum }, - ), - (Ok(datum), MeasurementKind::Speed) => Sample::new( - target, - &component::FanSpeed { name, datum }, - ), - (Err(e), kind) => { - let sensor_kind = match kind { - MeasurementKind::Temperature => "temperature", - MeasurementKind::Current => "current", - MeasurementKind::Voltage => "voltage", - MeasurementKind::Power => "power", - MeasurementKind::InputCurrent => "input_current", - MeasurementKind::InputVoltage => "input_voltage", - MeasurementKind::Speed => "fan_speed", - }; - let error = match e { - MeasurementError::InvalidSensor => "invalid_sensor", - MeasurementError::NoReading => "no_reading", - MeasurementError::NotPresent => "not_present", - MeasurementError::DeviceError => "device_error", - MeasurementError::DeviceUnavailable => { - "device_unavailable" - } - MeasurementError::DeviceTimeout => "device_timeout", - MeasurementError::DeviceOff => "device_off", - }; - Sample::new( - target, - &component::SensorErrorCount { - error: Cow::Borrowed(error), - name, - datum: oximeter::types::Cumulative::new(1), - sensor_kind: Cow::Borrowed(sensor_kind), - }, - ) - } - }?; - samples.push(sample); + return Ok(()); } } - - // Update our understanding again. - let mut understanding = self.my_understanding.lock().unwrap(); - understanding.devices = devices; - understanding.state = Some(current_state); - - Ok(samples) } }