blob: b4ccccec1dfbf351c48769bbecb19b3ca3d03944 [file]
#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HFT_CORE_MANAGER_IMPL_H_
#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HFT_CORE_MANAGER_IMPL_H_
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/thread_annotations.h"
#include "absl/container/btree_set.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/functional/any_invocable.h"
#include "absl/hash/hash.h"
#include "absl/log/log.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/time/time.h"
#include "tlbmc/adapter/data_source.h"
#include "identifier.pb.h"
#include "payload.pb.h"
#include "sensor_identifier.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"
namespace milotic_hft {
class SubscriptionManagerImpl;
struct IdentifierHash {
size_t operator()(const Identifier& v) const {
return v.identifier_case() == Identifier::kSensorIdentifier
? absl::HashOf(1, v.sensor_identifier().name())
: absl::HashOf(2, v.fru_identifier().barepath());
}
};
struct IdentifierEqual {
bool operator()(const Identifier& lhs, const Identifier& rhs) const {
if (lhs.identifier_case() != rhs.identifier_case()) {
return false;
}
return lhs.identifier_case() == Identifier::kSensorIdentifier
? lhs.sensor_identifier().name() ==
rhs.sensor_identifier().name()
: lhs.fru_identifier().barepath() ==
rhs.fru_identifier().barepath();
}
};
class ActiveSubscription
: public SubscriptionManager::Subscription,
public std::enable_shared_from_this<ActiveSubscription> {
public:
using SubscriptionId = std::string;
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);
~ActiveSubscription() override {
if (task_scheduler_ != nullptr) {
task_scheduler_->Cancel(task_id_);
}
LOG(WARNING) << "ActiveSubscription " << id_ << " destroyed.";
}
void Begin();
SubscriptionId GetId() const { return id_; }
const milotic_hft::SubscriptionParams& GetParams() const { return params_; }
void DeliverData(Payload&& data);
bool IsActive() const { return batches_remaining_ != 0; }
// Debug accessors, consumed by the tlbmc-side JSON rendering (the core
// deliberately carries no serialization dependency).
int BatchesRemaining() const { return batches_remaining_; }
std::string GetDataSourceName() const;
// Last sampled time (Unix nanos) per identifier, parallel to
// GetParams().identifiers().
std::vector<int64_t> GetLastSampledTimesNs() const;
private:
// Collects sensor data for the given identifier and updates the
// `identifier_payload`, returns the last sampled time in this collection.
absl::Time ProcessSensorIdentifier(const milotic_hft::Identifier& identifier,
int identifier_index,
Payload& identifier_payload);
// Collects fru data for the given identifier and updates the
// `identifier_payload`, returns the last sampled time in this collection.
absl::Time ProcessFruIdentifier(const milotic_hft::Identifier& identifier,
int identifier_index,
Payload& consolidated_data);
// Appends a status-only sensor entry (no readings) for `identifier` to
// `payload`. Edge-triggered: a marker is emitted when `status` differs from
// the status last signaled for this identifier, so a persistently unhealthy
// sensor does not repeat its marker every export cycle. If a heartbeat is
// configured for the identifier (OnChangeFilter.heartbeat_interval_ms), an
// unchanged non-OK status is additionally re-emitted once every heartbeat
// interval (in whole export windows) to assert liveness. Records the new
// status. Used for the no-fresh-sample paths where the sensor reports
// STALE/MISSING.
void SignalStatusIfChanged(int identifier_index,
const milotic_hft::Identifier& identifier,
milotic_hft::Status status,
absl::string_view status_message,
Payload& payload);
int task_id_ = -1; // -1 means no task is scheduled.
const SubscriptionId id_;
const milotic_hft::SubscriptionParams params_;
absl::AnyInvocable<void(Payload&&)> on_data_callback_;
// Back-pointer used to call Unsubscribe when the subscription drains its
// batch budget. Non-owning: the manager outlives all subscriptions it
// produced.
SubscriptionManagerImpl* manager_;
int batches_remaining_ = -1; // -1 means unlimited.
// Last sampled time (Unix nanos) per identifier, parallel to
// params_.identifiers(). Written by the scheduled task (single writer);
// read by the debug accessors from arbitrary threads. Relaxed atomics
// suffice — readers tolerate a stale snapshot.
std::vector<std::atomic<int64_t>> last_sampled_time_ns_;
// Per-identifier stream state, parallel to params_.identifiers(). Touched
// ONLY by the scheduled task (single-threaded; no atomics needed). Bundling
// these task-private fields keeps them distinct from last_sampled_time_ns_
// above, which is shared with debug readers and therefore atomic.
struct IdentifierStreamState {
// Highest absolute bin index emitted so far. The resampler emits a reading
// only when its bin is strictly greater, which dedups across collect cycles
// (without parking the watermark ahead of real data) and makes decimation
// tolerant of non-monotonic input. INT64_MIN so the first reading emits.
int64_t last_emitted_bin = std::numeric_limits<int64_t>::min();
// Status last signaled to the subscriber. A non-OK status (STALE/MISSING)
// is emitted only on a transition (edge-triggered), so a persistently
// unhealthy sensor does not repeat its marker every cycle. STATUS_UNKNOWN
// initial so the first observed non-OK status is treated as a transition
// and emitted.
milotic_hft::Status last_signaled_status = milotic_hft::STATUS_UNKNOWN;
// Heartbeat cadence for re-emitting an unchanged non-OK status marker,
// expressed in whole export windows; 0 disables it, leaving status purely
// edge-triggered. Taken from the sample filter's HeartbeatWindows() at
// construction, so the heartbeat window is defined in exactly one place.
int status_heartbeat_windows = 0;
// Export windows elapsed since the current status was last conveyed to the
// subscriber. Drives the status heartbeat above; reset whenever the status
// is (re-)signaled or rides along on delivered readings.
int windows_since_status_signal = 0;
// Edge filter (e.g. on-change) for this identifier, or null if none/unknown
// was configured. Built once at construction; carries state across cycles
// and is reset on recovery from a non-OK status.
std::unique_ptr<SampleFilter> sample_filter;
// Records that `status` was just conveyed to the subscriber (either as a
// status-only marker or riding along on delivered readings) and restarts
// the status heartbeat clock.
void MarkStatusSignaled(milotic_hft::Status status) {
last_signaled_status = status;
windows_since_status_signal = 0;
}
};
std::vector<IdentifierStreamState> stream_state_;
Scheduler* task_scheduler_;
DataSource* data_source_;
};
class SubscriptionManagerImpl : public SubscriptionManager {
public:
using SubscriptionId = ActiveSubscription::SubscriptionId;
~SubscriptionManagerImpl() override;
// Creates a subscription manager. `scheduler` runs the per-subscription
// export tasks; it is injected so the core stays independent of any
// concrete scheduler implementation (and its dependencies).
static std::unique_ptr<SubscriptionManagerImpl> Create(
std::unique_ptr<DataSource> data_source,
std::unique_ptr<Scheduler> scheduler) {
return absl::WrapUnique(new SubscriptionManagerImpl(std::move(data_source),
std::move(scheduler)));
}
absl::StatusOr<std::shared_ptr<Subscription>> AddSubscription(
const SubscriptionParams& params,
absl::AnyInvocable<void(Payload&&)> on_data_callback) override
ABSL_LOCKS_EXCLUDED(mutex_);
absl::Status Unsubscribe(const std::shared_ptr<Subscription>& subscription)
override ABSL_LOCKS_EXCLUDED(mutex_);
// Manages the sampling interval of a resource.
// Tracks the effective sampling interval of a resource as the minimum
// over all subscribers and updates it as subscribers are added or removed.
//
// All accesses are serialized by the enclosing SubscriptionManagerImpl's
// mutex_.
class ResourceMonitor {
public:
explicit ResourceMonitor(const milotic_hft::Identifier& identifier)
: identifier_(identifier),
current_sampling_interval_ms_(0),
current_max_batch_size_(0) {}
const milotic_hft::Identifier& GetIdentifier() const { return identifier_; }
absl::Status AddSubscriber(ActiveSubscription* subscription,
SensorMutationBatch* mutation);
absl::Status RemoveSubscriber(ActiveSubscription* subscription,
SensorMutationBatch* mutation);
absl::Status ConfigureDataSource(SensorMutationBatch* mutation);
int GetCurrentSamplingIntervalMs() const {
return current_sampling_interval_ms_;
}
bool HasSubscribers() const { return !subscribers_.empty(); }
// Point-in-time copy of the monitor's state for debug rendering.
struct DebugSnapshot {
Identifier identifier;
int current_sampling_interval_ms = 0;
int current_max_batch_size = 0;
std::vector<SubscriptionId> subscriber_ids;
std::vector<int> subscribed_sampling_intervals_ms;
std::vector<int> subscribed_max_batch_sizes;
};
DebugSnapshot GetDebugSnapshot() const;
private:
void RemoveSubscriberInternal(ActiveSubscription* subscription);
const milotic_hft::Identifier identifier_;
int current_sampling_interval_ms_;
int current_max_batch_size_;
// Non-owning. Subscriptions are owned by their gRPC reactor (or whoever
// called AddSubscription). Erasure happens under the enclosing
// SubscriptionManagerImpl's mutex_, atomic with the caller's drop of the
// owning shared_ptr in Unsubscribe paths.
absl::flat_hash_set<ActiveSubscription*> subscribers_;
absl::btree_multiset<int> lowest_sampling_interval_subscriptions_;
absl::btree_multiset<int, std::greater<>> max_batch_sizes_;
};
// Point-in-time debug snapshot of the manager's state, consumed by the
// tlbmc-side JSON rendering (the core deliberately carries no serialization
// dependency).
struct DebugSnapshot {
size_t next_subscription_id = 0;
std::string data_source_name;
std::vector<ResourceMonitor::DebugSnapshot> resource_monitors;
};
DebugSnapshot GetDebugSnapshot() const ABSL_LOCKS_EXCLUDED(mutex_);
// Debug-only access to the injected scheduler, so a consumer that knows the
// concrete type can render its stats alongside the manager's.
const Scheduler& GetScheduler() const { return *task_scheduler_; }
private:
SubscriptionManagerImpl(std::unique_ptr<DataSource> data_source,
std::unique_ptr<Scheduler> scheduler);
SubscriptionId GenerateSubscriptionId();
// Walks subscription's identifiers and registers it with each
// resource_monitors_ entry, creating new entries as needed. Threads
// `mutation` through to the data source. On failure, rolls back any
// partial registration via RemoveFromAllResources before returning.
absl::Status AddToAllResources(ActiveSubscription* subscription,
SensorMutationBatch* mutation)
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Walks subscription's identifiers and unregisters it from each
// resource_monitors_ entry that contains it; erases entries that
// become empty. Idempotent: missing entries and "subscriber not
// present" both no-op. `mutation` may be nullptr (in-memory only) for
// post-failure rollback, where the data source must not be touched.
void RemoveFromAllResources(ActiveSubscription* subscription,
SensorMutationBatch* mutation)
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
std::atomic<size_t> next_subscription_id_;
std::unique_ptr<Scheduler> task_scheduler_;
std::unique_ptr<DataSource> data_source_;
mutable absl::Mutex mutex_;
absl::flat_hash_map<milotic_hft::Identifier, std::unique_ptr<ResourceMonitor>,
IdentifierHash, IdentifierEqual>
resource_monitors_ ABSL_GUARDED_BY(mutex_);
};
} // namespace milotic_hft
#endif // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HFT_CORE_MANAGER_IMPL_H_