| #include "tlbmc/hft/core/manager_impl.h" |
| |
| #include <sys/param.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <cstdint> |
| #include <iterator> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/functional/any_invocable.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/strings/substitute.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "g3/macros.h" |
| #include "tlbmc/adapter/data_source.h" |
| #include "fru_identifier.pb.h" |
| #include "fru_payload.pb.h" |
| #include "identifier.pb.h" |
| #include "payload.pb.h" |
| #include "sensor_payload.pb.h" |
| #include "subscription_params.pb.h" |
| #include "tlbmc/hft/core/edge_filter.h" |
| #include "tlbmc/hft/core/manager.h" |
| #include "tlbmc/hft/core/scheduler.h" |
| #include "resource.pb.h" |
| #include "status.pb.h" |
| |
| namespace milotic_hft { |
| |
| namespace { |
| |
| bool IsDataPopulated(const Payload& data) { |
| // For all sensors in the batch, check if there are any readings. |
| if (data.has_high_frequency_sensors_readings_batch()) { |
| for (const auto& sensor : data.high_frequency_sensors_readings_batch() |
| .high_frequency_sensors()) { |
| if (!sensor.timestamped_readings().empty()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| absl::StatusOr<absl::Time> GetLastSampledTime(const Payload& data) { |
| if (data.has_high_frequency_sensors_readings_batch()) { |
| const auto& sensors = |
| data.high_frequency_sensors_readings_batch().high_frequency_sensors(); |
| if (sensors.empty()) { |
| return absl::NotFoundError("No sensors found in the batch."); |
| } |
| const auto& readings = sensors.rbegin()->timestamped_readings(); |
| if (readings.empty()) { |
| return absl::NotFoundError("No readings found."); |
| } |
| |
| // Get the last sampled time from the last sensor. |
| return absl::FromUnixNanos(readings.rbegin()->timestamp_ns()); |
| } |
| return absl::InvalidArgumentError("Unsupported payload type."); |
| } |
| |
| // Returns the absolute, power-of-two-aligned bin index that `timestamp_ns` |
| // falls into for a `sampling_interval_ms`-wide bin. The resampler and EndOfBin |
| // share this so their notion of a bin stays identical. |
| int64_t BinIndex(int64_t timestamp_ns, int64_t sampling_interval_ms) { |
| return absl::ToInt64Milliseconds(absl::Nanoseconds(timestamp_ns)) / |
| sampling_interval_ms; |
| } |
| |
| // Decimates `readings` to one reading per absolute, power-of-two-aligned bin, |
| // keeping the first reading seen in each bin whose index is strictly greater |
| // than `last_emitted_bin`. `last_emitted_bin` is updated to the highest bin |
| // emitted and is carried across collect cycles by the caller. |
| // |
| // The subscription interval is always a power of two (clamped in |
| // AddSubscription). Decimating by the absolute power-of-two bin is correct |
| // regardless of the source poll rate: the data source is not guaranteed to poll |
| // at a power-of-two interval -- it may run at a sensor's |
| // static_refresh_interval, an override, or kDefaultSensorSamplingInterval |
| // (1000ms), faster or slower than this subscription -- but at most one reading |
| // per bin is ever emitted. Because bins are absolute and the interval is a |
| // power of two, a coarser subscription's bins are unions of a finer one's, so |
| // its emitted samples are a strict subset of the finer subscription's: multiple |
| // subscribers to the same sensor do not perturb each other. |
| // |
| // Requiring a strictly greater bin (rather than merely "different from the |
| // last") does double duty: it dedups across collect cycles (a bin straddling a |
| // batch boundary is not re-emitted, since last_emitted_bin persists), and it |
| // makes decimation tolerant of non-monotonic input -- if a wall-clock step |
| // makes readings arrive out of order, readings in an already-emitted bin are |
| // dropped rather than double-emitted. (A large backward step can still hide |
| // readings from the source-buffer slice upstream; fully closing that needs a |
| // monotonic poll clock at the sensor layer.) |
| std::vector<HighFrequencySensorReading> ResampleHighFrequencySensorsReadings( |
| const HighFrequencySensorsReadings& readings, int sampling_interval_ms, |
| int64_t& last_emitted_bin) { |
| std::vector<HighFrequencySensorReading> resampled_readings; |
| const int64_t interval_ms = sampling_interval_ms; |
| for (const auto& reading : readings.timestamped_readings()) { |
| const int64_t bin = BinIndex(reading.timestamp_ns(), interval_ms); |
| if (bin > last_emitted_bin) { |
| resampled_readings.push_back(reading); |
| last_emitted_bin = bin; |
| } |
| } |
| return resampled_readings; |
| } |
| |
| void ResampleReadingsInplace(Payload& raw_data, int sampling_interval_ms, |
| int64_t& last_emitted_bin) { |
| // Resample high frequency sensors readings. A sensor identifier yields a |
| // single sensor per collect, so one last_emitted_bin tracks that sensor. |
| if (raw_data.has_high_frequency_sensors_readings_batch()) { |
| for (auto& sensor : |
| *raw_data.mutable_high_frequency_sensors_readings_batch() |
| ->mutable_high_frequency_sensors()) { |
| if (sensor.timestamped_readings().empty()) { |
| continue; |
| } |
| std::vector<HighFrequencySensorReading> resampled_readings = |
| ResampleHighFrequencySensorsReadings(sensor, sampling_interval_ms, |
| last_emitted_bin); |
| auto* readings = sensor.mutable_timestamped_readings(); |
| readings->Clear(); |
| readings->Reserve(static_cast<int>(resampled_readings.size())); |
| for (auto& resampled_reading : resampled_readings) { |
| (*readings->Add()) = std::move(resampled_reading); |
| } |
| } |
| } |
| } |
| |
| // Whether a per-identifier payload has anything worth delivering: at least one |
| // reading, a STATUS_STALE/STATUS_MISSING marker, or FRU data. A sensor with no |
| // fresh sample this window and a healthy status leaves only an empty OK sensor |
| // entry, which we suppress rather than ship as an empty batch. |
| bool HasDeliverableContent(const Payload& payload) { |
| if (payload.has_fru_batch() && !payload.fru_batch().frus().empty()) { |
| return true; |
| } |
| if (payload.has_high_frequency_sensors_readings_batch()) { |
| for (const auto& sensor : payload.high_frequency_sensors_readings_batch() |
| .high_frequency_sensors()) { |
| if (!sensor.timestamped_readings().empty()) { |
| return true; |
| } |
| const milotic_hft::Status status = sensor.state().status(); |
| if (status == milotic_hft::STATUS_STALE || |
| status == milotic_hft::STATUS_MISSING) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // Returns the State the data source stamped on the (single) sensor entry of a |
| // collected payload, or a default (STATUS_UNKNOWN) State if absent. The store |
| // sets this on every payload regardless of whether readings are present, so it |
| // is the source of truth for the sensor's health. |
| milotic_hft::State GetCarriedState(const Payload& data) { |
| if (data.has_high_frequency_sensors_readings_batch()) { |
| const auto& sensors = |
| data.high_frequency_sensors_readings_batch().high_frequency_sensors(); |
| if (!sensors.empty()) { |
| return sensors.begin()->state(); |
| } |
| } |
| return milotic_hft::State(); |
| } |
| |
| } // namespace |
| |
| // ResourceMonitor implementation. |
| // A nullptr mutation will not update the data source. |
| absl::Status SubscriptionManagerImpl::ResourceMonitor::AddSubscriber( |
| ActiveSubscription* subscription, SensorMutationBatch* mutation) { |
| LOG(INFO) << "Adding subscriber " << subscription->GetId() << " to " |
| << identifier_; |
| subscribers_.insert(subscription); |
| lowest_sampling_interval_subscriptions_.insert( |
| subscription->GetParams().sampling_interval_ms()); |
| // Calculate the batch size for the data source. |
| int current_batch_size = |
| subscription->GetParams().export_interval_ms() / |
| std::max(1, subscription->GetParams().sampling_interval_ms()); |
| max_batch_sizes_.insert(current_batch_size); |
| LOG(INFO) << "Max batch size for " << identifier_ << " is " |
| << current_batch_size; |
| |
| absl::Status status = ConfigureDataSource(mutation); |
| if (!status.ok()) { |
| // If configuration fails, we need to remove the subscriber we just added |
| // so that we don't leave the resource monitor in an inconsistent state. |
| // We do this manually instead of calling RemoveSubscriber because |
| // RemoveSubscriber itself calls ConfigureDataSource, which might fail again |
| // or reset state undesirably. |
| RemoveSubscriberInternal(subscription); |
| |
| return status; |
| } |
| return absl::OkStatus(); |
| } |
| |
| void SubscriptionManagerImpl::ResourceMonitor::RemoveSubscriberInternal( |
| ActiveSubscription* subscription) { |
| // Get the sampling interval for the subscription. |
| int sampling_interval_ms_to_remove = |
| subscription->GetParams().sampling_interval_ms(); |
| |
| // Get the batch size for the subscription. |
| int current_batch_size = |
| subscription->GetParams().export_interval_ms() / |
| std::max(1, subscription->GetParams().sampling_interval_ms()); |
| |
| // Remove the subscription from the set of subscribers. |
| subscribers_.erase(subscription); |
| |
| // Remove the sampling interval from the set of lowest sampling intervals. |
| auto it_interval = lowest_sampling_interval_subscriptions_.find( |
| sampling_interval_ms_to_remove); |
| if (it_interval != lowest_sampling_interval_subscriptions_.end()) { |
| lowest_sampling_interval_subscriptions_.erase(it_interval); |
| } |
| |
| // Remove the batch size from the set of max batch sizes for the data |
| // source. |
| auto it_batch = max_batch_sizes_.find(current_batch_size); |
| if (it_batch != max_batch_sizes_.end()) { |
| max_batch_sizes_.erase(it_batch); |
| } |
| } |
| |
| absl::Status SubscriptionManagerImpl::ResourceMonitor::RemoveSubscriber( |
| ActiveSubscription* subscription, SensorMutationBatch* mutation) { |
| LOG(INFO) << "Removing subscriber " << subscription->GetId() << " from " |
| << identifier_ << " with sampling interval " |
| << current_sampling_interval_ms_; |
| if (!subscribers_.contains(subscription)) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Subscriber ", subscription->GetId(), " not found.")); |
| } |
| RemoveSubscriberInternal(subscription); |
| ECCLESIA_RETURN_IF_ERROR(ConfigureDataSource(mutation)); |
| return absl::OkStatus(); |
| } |
| |
| SubscriptionManagerImpl::ResourceMonitor::DebugSnapshot |
| SubscriptionManagerImpl::ResourceMonitor::GetDebugSnapshot() const { |
| DebugSnapshot snapshot; |
| snapshot.identifier = identifier_; |
| snapshot.current_sampling_interval_ms = current_sampling_interval_ms_; |
| snapshot.current_max_batch_size = current_max_batch_size_; |
| snapshot.subscriber_ids.reserve(subscribers_.size()); |
| for (const ActiveSubscription* sub : subscribers_) { |
| snapshot.subscriber_ids.push_back(sub->GetId()); |
| } |
| snapshot.subscribed_sampling_intervals_ms.assign( |
| lowest_sampling_interval_subscriptions_.begin(), |
| lowest_sampling_interval_subscriptions_.end()); |
| snapshot.subscribed_max_batch_sizes.assign(max_batch_sizes_.begin(), |
| max_batch_sizes_.end()); |
| return snapshot; |
| } |
| |
| absl::Status SubscriptionManagerImpl::ResourceMonitor::ConfigureDataSource( |
| SensorMutationBatch* mutation) { |
| int lowest_sampling_interval_ms = |
| lowest_sampling_interval_subscriptions_.empty() |
| ? -1 |
| : *lowest_sampling_interval_subscriptions_.begin(); |
| // Update the max batch size for the data source. |
| int max_batch_size = |
| max_batch_sizes_.empty() ? -1 : *max_batch_sizes_.begin(); |
| |
| if (mutation != nullptr) { |
| // Reset is always applied in unison for both sampling interval and max |
| // batch size, as lowest_sampling_interval_subscriptions_ and |
| // max_batch_sizes_ are guaranteed to be empty at the same time. |
| if (max_batch_size == -1) { |
| LOG(INFO) << "Resetting sensor " << identifier_; |
| ECCLESIA_RETURN_IF_ERROR(mutation->ResetSensor(identifier_)); |
| |
| } else { |
| if (max_batch_size != current_max_batch_size_) { |
| LOG(INFO) << "Setting max batch size for " << identifier_ << " to " |
| << max_batch_size; |
| ECCLESIA_RETURN_IF_ERROR( |
| mutation->SetBatchSize(identifier_, max_batch_size)); |
| } |
| |
| if (lowest_sampling_interval_ms != current_sampling_interval_ms_) { |
| LOG(INFO) << "Setting effective sampling interval for " << identifier_ |
| << " to " << lowest_sampling_interval_ms; |
| ECCLESIA_RETURN_IF_ERROR(mutation->SetSamplingInterval( |
| identifier_, lowest_sampling_interval_ms)); |
| } |
| } |
| } |
| current_sampling_interval_ms_ = lowest_sampling_interval_ms; |
| current_max_batch_size_ = max_batch_size; |
| return absl::OkStatus(); |
| } |
| |
| SubscriptionManagerImpl::SubscriptionManagerImpl( |
| std::unique_ptr<DataSource> data_source, |
| std::unique_ptr<Scheduler> scheduler) |
| : next_subscription_id_(0), |
| task_scheduler_(std::move(scheduler)), |
| data_source_(std::move(data_source)) {} |
| |
| SubscriptionManagerImpl::~SubscriptionManagerImpl() { task_scheduler_->Stop(); } |
| |
| // ActiveSubscription implementation. |
| void ActiveSubscription::DeliverData(Payload&& raw_data) { |
| if (batches_remaining_ == 0) { |
| return; |
| } |
| |
| // Deliver the data to the client. |
| on_data_callback_(std::move(raw_data)); |
| |
| // If batches_remaining_ is -1, the subscription is indefinite and will be |
| // ended when the client unsubscribes. |
| if (batches_remaining_ < 0) { |
| return; |
| } |
| |
| // Subscription is limited to a fixed number of batches. |
| // If the number of batches is reached, the subscription will be ended. |
| --batches_remaining_; |
| DLOG(INFO) << "Subscription " << id_ << " delivered batch. " |
| << batches_remaining_ << " batches remaining."; |
| if (batches_remaining_ == 0) { |
| (void)manager_->Unsubscribe(shared_from_this()); |
| } |
| } |
| |
| // ActiveSubscription implementation. |
| ActiveSubscription::ActiveSubscription( |
| const SubscriptionId& id, const milotic_hft::SubscriptionParams& params, |
| absl::AnyInvocable<void(Payload&&)> on_data_callback, |
| SubscriptionManagerImpl* manager, Scheduler* task_scheduler, |
| DataSource* data_source, std::vector<std::unique_ptr<SampleFilter>> filters) |
| : id_(id), |
| params_(params), |
| on_data_callback_(std::move(on_data_callback)), |
| manager_(manager), |
| batches_remaining_(params.num_batches() == 0 ? -1 : params.num_batches()), |
| last_sampled_time_ns_(params.identifiers_size()), |
| stream_state_(params.identifiers_size()), |
| task_scheduler_(task_scheduler), |
| data_source_(data_source) { |
| LOG(INFO) << "ActiveSubscription " << id_ |
| << " created. num_batches: " << params_.num_batches(); |
| // Install pre-built edge filters and derive each identifier's |
| // status-heartbeat cadence from the filter's heartbeat window. |
| for (int i = 0; i < params_.identifiers_size(); ++i) { |
| // NO_CDC: stream_state_ allocated parallel to params_.identifiers(). |
| stream_state_[i].sample_filter = std::move(filters[i]); |
| // Reuse the filter's heartbeat cadence for the out-of-band status signal so |
| // the heartbeat window is defined in exactly one place (the filter). |
| if (stream_state_[i].sample_filter != nullptr) { |
| stream_state_[i].status_heartbeat_windows = |
| stream_state_[i].sample_filter->HeartbeatWindows(); |
| } |
| } |
| } |
| |
| // ActiveSubscription implementation. |
| void ActiveSubscription::Begin() { |
| // Schedule all the tasks for this subscription. |
| // The task calls the data source to collect the data and then goes over |
| // the data and downsample to the desired sampling interval in the |
| // subscription params. |
| |
| auto task = [weak_self = weak_from_this()]( |
| absl::AnyInvocable<void()> on_done) mutable { |
| std::shared_ptr<ActiveSubscription> self = weak_self.lock(); |
| if (self == nullptr) { |
| on_done(); |
| return; |
| } |
| bool should_deliver = false; |
| // Collect the data for all the identifiers in the subscription params. |
| Payload consolidated_data; |
| const auto& identifiers = self->params_.identifiers(); |
| for (int i = 0; i < identifiers.size(); ++i) { |
| const auto& identifier = identifiers[i]; |
| Payload identifier_payload; |
| absl::Time last_sampled_time; |
| switch (identifier.identifier_case()) { |
| case milotic_hft::Identifier::kSensorIdentifier: |
| last_sampled_time = |
| self->ProcessSensorIdentifier(identifier, i, identifier_payload); |
| break; |
| case milotic_hft::Identifier::kFruIdentifier: |
| last_sampled_time = |
| self->ProcessFruIdentifier(identifier, i, identifier_payload); |
| break; |
| default: |
| continue; |
| } |
| if (HasDeliverableContent(identifier_payload)) { |
| // Only deliver when this identifier actually contributed: new readings, |
| // a STATUS_MISSING marker, or a FRU snapshot. A steady or fully |
| // edge-filtered sensor adds nothing, so we avoid an empty batch (which |
| // would also wrongly consume the num_batches budget). |
| consolidated_data.MergeFrom(identifier_payload); |
| should_deliver = true; |
| } |
| |
| // NO_CDC: last_sampled_time_ns_ Allocated parallel to |
| // params_.identifiers(). |
| self->last_sampled_time_ns_[i].store(absl::ToUnixNanos(last_sampled_time), |
| std::memory_order_relaxed); |
| } |
| if (should_deliver) { |
| self->DeliverData(std::move(consolidated_data)); |
| } |
| on_done(); |
| }; |
| |
| task_id_ = task_scheduler_->ScheduleAsync( |
| task, absl::Milliseconds(params_.export_interval_ms())); |
| } |
| |
| void ActiveSubscription::SignalStatusIfChanged(int identifier_index, |
| const Identifier& identifier, |
| milotic_hft::Status status, |
| absl::string_view status_message, |
| Payload& payload) { |
| // NO_CDC: stream_state_ allocated parallel to params_.identifiers(). |
| IdentifierStreamState& state = stream_state_[identifier_index]; |
| if (status == state.last_signaled_status) { |
| // Same status as last conveyed. Stay edge-triggered unless a heartbeat is |
| // configured and a full heartbeat interval of export windows has elapsed, |
| // in which case re-emit the marker to assert liveness. |
| if (state.status_heartbeat_windows <= 0 || |
| ++state.windows_since_status_signal < state.status_heartbeat_windows) { |
| return; |
| } |
| } |
| state.MarkStatusSignaled(status); |
| HighFrequencySensorsReadings* sensor_readings = |
| payload.mutable_high_frequency_sensors_readings_batch() |
| ->add_high_frequency_sensors(); |
| *sensor_readings->mutable_sensor_identifier() = |
| identifier.sensor_identifier(); |
| sensor_readings->mutable_state()->set_status(status); |
| if (!status_message.empty()) { |
| sensor_readings->mutable_state()->set_status_message(status_message); |
| } |
| } |
| |
| absl::Time ActiveSubscription::ProcessSensorIdentifier( |
| const Identifier& identifier, int identifier_index, |
| Payload& identifier_payload) { |
| IdentifierStreamState& state = stream_state_[identifier_index]; |
| const absl::Time last_sampled_time = absl::FromUnixNanos( |
| // NO_CDC: last_sampled_time_ns_ allocated parallel to identifiers(). |
| last_sampled_time_ns_[identifier_index].load(std::memory_order_relaxed)); |
| absl::StatusOr<Payload> data = |
| data_source_->Collect(identifier, last_sampled_time); |
| |
| // Collection failed outright. NotFound means the sensor is gone from the |
| // store (MISSING); any other error is a transient/adapter problem with a |
| // sensor that is still present (STALE). Edge-triggered so a persistently |
| // failing sensor does not repeat the marker every cycle; the watermark is |
| // left where it was. |
| if (!data.ok()) { |
| LOG_EVERY_N_SEC(ERROR, 10) << "Failed to collect data for subscription " |
| << id_ << ": " << data.status(); |
| const milotic_hft::Status status = absl::IsNotFound(data.status()) |
| ? milotic_hft::STATUS_MISSING |
| : milotic_hft::STATUS_STALE; |
| SignalStatusIfChanged( |
| identifier_index, identifier, status, |
| absl::StrCat("Failed to collect data for identifier ", identifier, |
| " with status: ", data.status()), |
| identifier_payload); |
| return last_sampled_time; |
| } |
| |
| // Collection succeeded; the payload carries the sensor's own status (OK while |
| // healthy, STALE/MISSING when the sensor reports trouble), set by the store |
| // whether or not readings are present. |
| const milotic_hft::State carried_state = GetCarriedState(*data); |
| |
| if (IsDataPopulated(*data)) { |
| // Always decimate to the subscription's (power-of-two) interval, regardless |
| // of the source poll rate. The data source's configured interval is not |
| // under our control -- it may be a non-power-of-two default and may shift |
| // as other subscribers come and go -- so resampling unconditionally keeps a |
| // subscriber's output deterministic: exactly one sample per absolute bin. |
| // stream_state_.last_emitted_bin carries the highest bin emitted so far |
| // across cycles, so a bin straddling a batch boundary is not re-emitted. |
| ResampleReadingsInplace(*data, params_.sampling_interval_ms(), |
| state.last_emitted_bin); |
| |
| // The watermark is the last emitted reading's own timestamp -- an observed |
| // value, never parked ahead of real data. Cross-cycle bin dedup is handled |
| // by last_emitted_bin above, not by rounding the watermark up to the bin |
| // end. |
| absl::StatusOr<absl::Time> updated_sample_time = GetLastSampledTime(*data); |
| if (updated_sample_time.ok()) { |
| // Recovery: if the sensor was last signaled non-OK and is now OK, reset |
| // the edge filter so the first recovered reading is emitted even if its |
| // value equals the one last emitted before the sensor went unhealthy -- |
| // otherwise the on-change filter would suppress it, leaving the consumer |
| // with a status marker followed by silence. |
| const bool recovered = |
| carried_state.status() == milotic_hft::STATUS_OK && |
| state.last_signaled_status != milotic_hft::STATUS_OK && |
| state.last_signaled_status != milotic_hft::STATUS_UNKNOWN; |
| if (recovered && state.sample_filter != nullptr) { |
| state.sample_filter->Reset(); |
| } |
| |
| // Apply this identifier's edge filter (e.g. on-change) to the decimated |
| // stream. Readings dropped here have already advanced the watermark |
| // above, so they are not reconsidered next cycle. A null filter means |
| // no/unknown edge filter was configured -- the stream passes through |
| // unfiltered. |
| if (state.sample_filter != nullptr && |
| data->has_high_frequency_sensors_readings_batch()) { |
| for (auto& sensor : |
| *data->mutable_high_frequency_sensors_readings_batch() |
| ->mutable_high_frequency_sensors()) { |
| state.sample_filter->Filter(sensor); |
| } |
| } |
| |
| // Fresh, newly-binned readings were available this cycle. The status |
| // rides along on the payload, so record what the subscriber is about to |
| // see and restart the status heartbeat clock. |
| state.MarkStatusSignaled(carried_state.status()); |
| identifier_payload = std::move(*data); |
| return *updated_sample_time; |
| } |
| // else: had raw readings but all fell in already-emitted bins (source |
| // polled faster than the sampling interval, or non-monotonic input). |
| // Nothing survived decimation -- fall through to the stall handling below. |
| } |
| |
| // Nothing survived decimation this export window (no readings at all, or |
| // every reading fell in an already-emitted bin). Because the export interval |
| // is at least twice the sampling interval (enforced in AddSubscription), a |
| // sensor that is actually producing always advances at least one bin per |
| // window, so an empty window means the source is not advancing -- a stall -- |
| // and is signaled STALE. If the sensor already reports a more specific non-OK |
| // status (e.g. MISSING because it is gone from the store), surface that |
| // instead. Edge-triggered: the marker is emitted once on the status |
| // transition, so a persistent stall does not repeat it every window, and a |
| // spurious one-window gap self-heals to OK on the next surviving sample. The |
| // (edge-filtered-to- empty) case never reaches here: it returns above with |
| // the sensor healthy. |
| const milotic_hft::Status carried = carried_state.status(); |
| const bool reported_unhealthy = carried != milotic_hft::STATUS_OK && |
| carried != milotic_hft::STATUS_UNKNOWN; |
| const milotic_hft::Status status = |
| reported_unhealthy ? carried : milotic_hft::STATUS_STALE; |
| const std::string status_message = |
| reported_unhealthy ? carried_state.status_message() |
| : "No sample produced this export window."; |
| SignalStatusIfChanged(identifier_index, identifier, status, status_message, |
| identifier_payload); |
| return last_sampled_time; |
| } |
| |
| absl::Time ActiveSubscription::ProcessFruIdentifier( |
| const Identifier& identifier, int identifier_index, |
| Payload& consolidated_data) { |
| // NO_CDC: last_sampled_time_ns_ allocated parallel to params_.identifiers(). |
| const absl::Time last_sampled_time = absl::FromUnixNanos( |
| last_sampled_time_ns_[identifier_index].load(std::memory_order_relaxed)); |
| absl::StatusOr<Payload> data = |
| data_source_->Collect(identifier, last_sampled_time); |
| if (!data.ok() || !data->has_fru_batch() || |
| data->fru_batch().frus().empty()) { |
| LOG_EVERY_N_SEC(ERROR, 10) << "Failed to collect data for subscription " |
| << id_ << ": " << data.status(); |
| return last_sampled_time; |
| } |
| |
| consolidated_data.MergeFrom(*data); |
| return absl::Now(); |
| } |
| |
| std::string ActiveSubscription::GetDataSourceName() const { |
| return data_source_->GetName(); |
| } |
| |
| std::vector<int64_t> ActiveSubscription::GetLastSampledTimesNs() const { |
| std::vector<int64_t> times; |
| times.reserve(last_sampled_time_ns_.size()); |
| for (const std::atomic<int64_t>& ns : last_sampled_time_ns_) { |
| times.push_back(ns.load(std::memory_order_relaxed)); |
| } |
| return times; |
| } |
| |
| absl::Status SubscriptionManagerImpl::AddToAllResources( |
| ActiveSubscription* subscription, SensorMutationBatch* mutation) { |
| for (const auto& identifier : subscription->GetParams().identifiers()) { |
| auto& monitor = resource_monitors_[identifier]; |
| if (monitor == nullptr) { |
| monitor = std::make_unique<ResourceMonitor>(identifier); |
| } |
| if (absl::Status status = monitor->AddSubscriber(subscription, mutation); |
| !status.ok()) { |
| LOG(ERROR) << "Failed to add subscriber " << subscription->GetId() |
| << " to " << identifier << ": " << status; |
| // Rolls back any partial registration; also cleans the brand-new |
| // empty monitor we may have just inserted for this identifier. |
| RemoveFromAllResources(subscription, nullptr); |
| return status; |
| } |
| } |
| return absl::OkStatus(); |
| } |
| |
| void SubscriptionManagerImpl::RemoveFromAllResources( |
| ActiveSubscription* subscription, SensorMutationBatch* mutation) { |
| for (const auto& identifier : subscription->GetParams().identifiers()) { |
| auto it = resource_monitors_.find(identifier); |
| if (it == resource_monitors_.end()) { |
| // Idempotent: already removed (e.g. concurrent unsubscribe + |
| // completion, or a brand-new empty monitor erased on a prior pass). |
| continue; |
| } |
| absl::Status remove_status = |
| it->second->RemoveSubscriber(subscription, mutation); |
| if (!remove_status.ok() && !absl::IsInvalidArgument(remove_status)) { |
| LOG(ERROR) << "Failed to remove subscriber " << subscription->GetId() |
| << " from resource " << identifier << ": " << remove_status; |
| } |
| if (!it->second->HasSubscribers()) { |
| resource_monitors_.erase(it); |
| } |
| } |
| } |
| |
| absl::StatusOr<std::shared_ptr<SubscriptionManager::Subscription>> |
| SubscriptionManagerImpl::AddSubscription( |
| const milotic_hft::SubscriptionParams& params, |
| absl::AnyInvocable<void(Payload&&)> on_data_callback) { |
| LOG(INFO) << "Add Subscription with params: " << params; |
| if (on_data_callback == nullptr) { |
| return absl::InvalidArgumentError("on_data_callback cannot be null."); |
| } |
| |
| if (!params.subscription_policy().has_resource_type()) { |
| return absl::InvalidArgumentError( |
| "resource_type must be provided in the subscription policy."); |
| } |
| |
| ECCLESIA_ASSIGN_OR_RETURN( |
| std::vector<Identifier> identifiers, |
| data_source_->GetIdentifiersForResourceType( |
| {params.subscription_policy().resource_type()})); |
| |
| // Copy the subscription params to append the identifiers to subscribe to. |
| milotic_hft::SubscriptionParams internal_params = params; |
| if (params.subscription_policy().configuration_type() == |
| milotic_hft::SubscriptionPolicy:: |
| CONFIGURATION_TYPE_CONFIGURE_ALL_RESOURCES) { |
| internal_params.mutable_identifiers()->Add(identifiers.begin(), |
| identifiers.end()); |
| } |
| |
| if (internal_params.identifiers().empty()) { |
| return absl::InvalidArgumentError( |
| "Subscription parameters must include at least one identifier or " |
| "specify CONFIGURATION_TYPE_ALL_RESOURCES."); |
| } |
| |
| // Reject duplicate identifiers. The same identifier listed twice would |
| // register this subscription against one ResourceMonitor more than once. The |
| // monitor counts the subscriber once (a set) but tracks the sampling interval |
| // and batch size in multisets, so each duplicate inserts a surplus entry that |
| // is never reclaimed: removal reconciles a single entry per subscriber, so |
| // the extras leak and pin the resource at a phantom (faster) rate after the |
| // owner unsubscribes. The duplicate would also be collected and delivered |
| // twice per batch. |
| { |
| absl::flat_hash_set<Identifier, IdentifierHash, IdentifierEqual> seen; |
| for (const Identifier& identifier : internal_params.identifiers()) { |
| if (!seen.insert(identifier).second) { |
| return absl::InvalidArgumentError( |
| "Subscription contains duplicate identifiers."); |
| } |
| } |
| } |
| |
| // Validate sensor subscription intervals (FRU subscriptions are full |
| // snapshots re-sampled on export and are exempt; no subscription mixes |
| // sensors and FRUs). Sensor sampling must be a power of two so the lowest |
| // requested period divides every other -- resampling reduces to power-of-two |
| // decimation and a coarser subscriber's samples are a strict subset of a |
| // finer one's. The export interval must be at least twice the sampling |
| // interval: the source polls and the subscription exports on independent |
| // schedulers with jitter, so 2x ensures a producing sensor clears decimation |
| // with at least one sample every export window (a window with none then |
| // reliably signals a stall; see ProcessSensorIdentifier). These are rejected |
| // rather than silently adjusted so a caller's request is never quietly |
| // changed underneath it. |
| if (internal_params.subscription_policy().resource_type() == |
| milotic_hft::SubscriptionPolicy::RESOURCE_TYPE_SENSOR) { |
| const int32_t sampling_ms = internal_params.sampling_interval_ms(); |
| if (!IsValidSensorSamplingIntervalMs(sampling_ms)) { |
| return absl::InvalidArgumentError(absl::Substitute( |
| "Sensor sampling_interval_ms must be a power of two in [1, $0] ms; " |
| "got $1.", |
| kMaxSensorSamplingIntervalMs, sampling_ms)); |
| } |
| if (internal_params.export_interval_ms() < 2 * sampling_ms) { |
| return absl::InvalidArgumentError(absl::Substitute( |
| "Sensor export_interval_ms ($0) must be at least twice " |
| "sampling_interval_ms ($1).", |
| internal_params.export_interval_ms(), sampling_ms)); |
| } |
| } |
| |
| // Build per-identifier edge filters before constructing the subscription so |
| // malformed configs (e.g. negative float_epsilon) surface as errors here. |
| const int export_interval_ms = internal_params.export_interval_ms(); |
| std::vector<std::unique_ptr<SampleFilter>> filters; |
| filters.reserve(internal_params.identifiers_size()); |
| for (const auto& identifier : internal_params.identifiers()) { |
| ECCLESIA_ASSIGN_OR_RETURN( |
| std::unique_ptr<SampleFilter> filter, |
| MakeSampleFilter(identifier.edge_filter(), export_interval_ms)); |
| filters.push_back(std::move(filter)); |
| } |
| |
| SubscriptionId sub_id = GenerateSubscriptionId(); |
| auto active_sub = std::make_shared<ActiveSubscription>( |
| sub_id, internal_params, std::move(on_data_callback), this, |
| task_scheduler_.get(), data_source_.get(), std::move(filters)); |
| |
| { |
| absl::MutexLock lock(mutex_); |
| std::unique_ptr<SensorMutationBatch> mutation = |
| data_source_->CreateSensorMutationBatch( |
| params.subscription_policy().resource_type(), |
| std::move(identifiers)); |
| ECCLESIA_RETURN_IF_ERROR( |
| AddToAllResources(active_sub.get(), mutation.get())); |
| if (auto status = std::move(*mutation).Apply(); !status.ok()) { |
| LOG(ERROR) << "Failed to apply batch sensor mutation: " << status; |
| RemoveFromAllResources(active_sub.get(), nullptr); |
| return status; |
| } |
| } |
| active_sub->Begin(); |
| return active_sub; |
| } |
| absl::Status SubscriptionManagerImpl::Unsubscribe( |
| const std::shared_ptr<Subscription>& subscription) { |
| auto active_sub = std::dynamic_pointer_cast<ActiveSubscription>(subscription); |
| if (active_sub == nullptr) { |
| return absl::InvalidArgumentError( |
| "subscription handle was not produced by this manager"); |
| } |
| absl::MutexLock lock(mutex_); |
| LOG(INFO) << "Removing subscription " << active_sub->GetId(); |
| absl::StatusOr<std::unique_ptr<SensorMutationBatch>> mutation = |
| data_source_->CreateSensorMutationBatch( |
| active_sub->GetParams().subscription_policy().resource_type()); |
| SensorMutationBatch* mutation_ptr = nullptr; |
| if (!mutation.ok()) { |
| LOG(ERROR) << "While removing subscription, failed to create batch sensor " |
| "mutation: " |
| << mutation.status(); |
| } else { |
| mutation_ptr = mutation->get(); |
| } |
| RemoveFromAllResources(active_sub.get(), mutation_ptr); |
| if (mutation.ok()) { |
| if (absl::Status apply_status = std::move(*mutation.value()).Apply(); |
| !apply_status.ok()) { |
| LOG(ERROR) << "While removing subscription, failed to apply batch sensor " |
| "mutation: " |
| << apply_status; |
| } |
| } |
| return absl::OkStatus(); |
| } |
| |
| SubscriptionManagerImpl::SubscriptionId |
| SubscriptionManagerImpl::GenerateSubscriptionId() { |
| return "sub_" + std::to_string(next_subscription_id_++); |
| } |
| |
| SubscriptionManagerImpl::DebugSnapshot |
| SubscriptionManagerImpl::GetDebugSnapshot() const { |
| DebugSnapshot snapshot; |
| snapshot.next_subscription_id = next_subscription_id_.load(); |
| snapshot.data_source_name = data_source_->GetName(); |
| absl::MutexLock lock(mutex_); |
| snapshot.resource_monitors.reserve(resource_monitors_.size()); |
| for (const auto& [identifier, resource_monitor] : resource_monitors_) { |
| snapshot.resource_monitors.push_back(resource_monitor->GetDebugSnapshot()); |
| } |
| return snapshot; |
| } |
| |
| } // namespace milotic_hft |