Heartbeat API
This is the API reference for the heartbeat subsystem: the heartbeat type and its lifecycle, the
HeartbeatMonitor sampler, how the runtime wires it, and the config model that drives it. For the
conceptual overview — the state keepalive, the sys metric, and the measures table — see the
Heartbeat guide.
What the heartbeat does (identical in all four languages): every intervalSecs tick it
publishes a state keepalive to ecv1/{device}/{component}/state (header name state,
version 1.0, body {"status":"RUNNING","uptimeSecs":n} — optionally extended with a per-connection
instances[] array, see below) through the library’s privileged
reserved-publish path, and emits the enabled system measures as a metric named sys through the
metric subsystem. On graceful shutdown it publishes a best-effort {"status":"STOPPED"} state, at
most once.
Constants (all four languages)
Section titled “Constants (all four languages)”| Constant | Value |
|---|---|
| State message name | state |
| State message version | 1.0 |
| Measures metric name | sys |
| Default interval | 5 seconds |
| Default keepalive destination | local |
Per-instance connectivity
Section titled “Per-instance connectivity”A component that fronts several southbound connections reports each connection’s reachability in the
RUNNING state body’s optional instances[] array — without minting a UNS instance per
connection (identity, data, and lifecycle stay at component scope, with no instance token). You register a connectivity
provider; the heartbeat samples it on every RUNNING tick. This is the terse API surface — for the
concept, the on-wire array, and worked examples, see the
guide’s Per-instance connectivity section.
InstanceConnectivity — one entry per connection: { instance, connected, detail? } (the
connection id, its live reachability, and an optional human detail such as the endpoint or a down
reason).
| Language | Factory / constructor | Provider you register (return type) |
|---|---|---|
| Java | InstanceConnectivity.of(instance, connected[, detail]) |
InstanceConnectivityProvider → List<InstanceConnectivity> instanceConnectivity() |
| Python | InstanceConnectivity.of(instance, connected[, detail]) |
a Callable[[], list[InstanceConnectivity]] (or None to clear) |
| Rust | InstanceConnectivity::of(instance, connected) / ::new(instance, connected, Option<detail>) |
Arc<dyn Fn() -> Vec<InstanceConnectivity> + Send + Sync> |
| TypeScript | InstanceConnectivity.of(instance, connected[, detail]) |
() => InstanceConnectivity[] |
Registering it — note the naming divergence (Java/TypeScript camelCase, Python/Rust snake_case):
| Language | Setter |
|---|---|
| Java | gg.setInstanceConnectivityProvider(provider) |
| Python | gg.set_instance_connectivity_provider(provider) |
| Rust | gg.set_instance_connectivity_provider(Some(Arc::new(provider))) |
| TypeScript | gg.setInstanceConnectivityProvider(provider) |
Rules. The provider is sampled on RUNNING keepalives only — the shutdown STOPPED state
never carries instances[]. It is best-effort: a provider that returns null / None / an
empty list, or one that throws or panics, simply omits the instances[] section for that tick and
never suppresses the keepalive. Registering again replaces the provider; passing
null / None / undefined clears it.
The heartbeat type and lifecycle
Section titled “The heartbeat type and lifecycle”com.mbreissi.edgecommons.heartbeat.Heartbeat implements ConfigurationChangeListener. Its
constructor is package-private; the runtime constructs it via HeartbeatBuilder.
public class Heartbeat implements ConfigurationChangeListener {
// Register (or replace, or clear with null) the per-instance connectivity provider whose // result shapes each RUNNING state body's instances[] array — the ONE runtime lever, wired // from EdgeCommons.setInstanceConnectivityProvider (see "Per-instance connectivity" above). public void setInstanceConnectivityProvider(InstanceConnectivityProvider provider);
// Re-emit the RUNNING state keepalive immediately, out of band from the periodic schedule // (backs the republish-state broadcast re-announce). Respects heartbeat.enabled; best-effort. public void publishStateNow();
// The component's monotonic uptime in whole seconds — the value the RUNNING keepalive carries // as uptimeSecs; backs the command inbox's `ping` verb so ping and keepalives agree. public long getUptimeSecs();
// Stop: cancel the periodic task, publish the best-effort STOPPED state // (at most once; failures are swallowed), and shut down the scheduler. public void close();
// Hot-reload hook: re-initializes the schedule on a config change. public boolean onConfigurationChanged();}// package constants: STATE_MESSAGE_NAME = "state", STATE_MESSAGE_VERSION = "1.0",// SYS_METRIC_NAME = "sys"Beyond close() and onConfigurationChanged(), the Java Heartbeat therefore exposes three more
public methods: setInstanceConnectivityProvider(...) (the runtime lever —
see above), publishStateNow() (an out-of-band RUNNING re-announce
that backs the republish-state broadcast), and getUptimeSecs() (the monotonic uptime the ping
command verb reports).
edgecommons.heartbeat.enhanced_heartbeat.EnhancedHeartbeat extends ConfigurationChangeListener.
class EnhancedHeartbeat(ConfigurationChangeListener): STATE_MESSAGE_NAME = "state" STATE_MESSAGE_VERSION = "1.0" SYS_METRIC_NAME = "sys"
def __init__(self, config_service: "ConfigManager"): ... # raises ValueError if None def set_messaging_service(self, messaging_service) -> None: ... def set_metric_service(self, metric_service) -> None: ... def start(self) -> None: ... def stop(self) -> None: ... # joins the loop thread, publishes the best-effort # STOPPED state (at most once), deregisters the listener def is_running(self) -> bool: ... def on_configuration_change(self, configuration) -> bool: ...edgecommons::heartbeat::Heartbeat wraps the background tokio task. Its start is crate-private
(pub(crate)) — the state class is reserved, so the runtime hands it the crate-private
ReservedMessaging seam; component code cannot construct it. Dropping the value stops the task
(RAII) and publishes the best-effort STOPPED state.
pub struct Heartbeat { /* task + reserved seam */ }
impl Heartbeat { pub(crate) fn start( config: Arc<ArcSwap<Config>>, metrics: Arc<dyn MetricService>, reserved: Option<Arc<dyn ReservedMessaging>>, // None (no transport) skips the keepalive ) -> Heartbeat;}
impl Drop for Heartbeat { /* abort task + best-effort STOPPED */ }
// public constant:pub const SYS_METRIC_NAME: &str = "sys";Heartbeat is exported from edgecommons. Its constructor is private — the runtime uses the static
start factory.
import { Heartbeat } from "@edgecommons/edgecommons";
export type ConfigProvider = () => Config; // live-config getter (mirrors Rust Arc<ArcSwap<Config>>)
export class Heartbeat { static start( configProvider: ConfigProvider, metrics: MetricService, messaging?: IMessagingService, // undefined (no transport) skips the keepalive ): Heartbeat;
// Idempotent: clears the timer and publishes the best-effort STOPPED state (at most once). stop(): Promise<void>;}// module-private constants: STATE_MESSAGE_NAME = "state", STATE_MESSAGE_VERSION = "1.0",// SYS_METRIC_NAME = "sys", DEFAULT_INTERVAL_SECS = 5Lifecycle summary
Section titled “Lifecycle summary”| Language | Type | Construct | Stop | Hot reload |
|---|---|---|---|---|
| Java | Heartbeat |
HeartbeatBuilder.build() (runtime) |
close() (+ best-effort STOPPED) |
onConfigurationChanged() listener |
| Python | EnhancedHeartbeat |
EnhancedHeartbeat(config_service) (runtime) |
stop() (+ best-effort STOPPED) |
on_configuration_change() listener |
| Rust | Heartbeat |
Heartbeat::start(...) — crate-private |
drop (RAII, + best-effort STOPPED) |
re-reads live config each tick |
| TypeScript | Heartbeat |
Heartbeat.start(...) (runtime) |
stop() async (+ best-effort STOPPED) |
re-reads live config each tick |
In every language enabled, measures, and destination changes apply on the next tick, and an
intervalSecs change rebuilds the schedule. Every tick is exception-guarded, and the state and
metric halves are individually best-effort — a failure in one never suppresses the other.
Construction and wiring
Section titled “Construction and wiring”In production you never call these — the runtime wires the heartbeat for you. The snippets show what the runtime does (also the path the tests exercise).
HeartbeatBuilder requires both a messaging client (whose reservedPublisher() performs the
state publish) and a metric emitter (for the sys metric); build() throws
IllegalStateException if either is null. create(null) throws IllegalArgumentException.
import com.mbreissi.edgecommons.heartbeat.Heartbeat;import com.mbreissi.edgecommons.heartbeat.HeartbeatBuilder;
Heartbeat heartbeat = HeartbeatBuilder.create(configManager) .withMessagingService(messagingClient) // the state keepalive path .withMetricService(metricEmitter) // the sys metric path .build(); // first tick fires at delay 0
heartbeat.close(); // cancel the task + best-effort STOPPED + shut down the schedulerThe constructor takes only the ConfigManager and registers as a config listener; messaging and
metric handles are injected via setters. The runtime injects the MessagingClient and
MetricEmitter classes (their operations are static methods).
from edgecommons.heartbeat.enhanced_heartbeat import EnhancedHeartbeat
h = EnhancedHeartbeat(config_service) # registers as a config-change listenerh.set_messaging_service(MessagingClient) # inject the static-method class handlesh.set_metric_service(MetricEmitter) # also defines the "sys" metrich.start()assert h.is_running() is Trueh.stop() # STOPPED state + join the loop thread + deregisterstart is crate-private — only EdgeCommonsBuilder::build() calls it, handing the heartbeat the
crate-private ReservedMessaging seam of the wired messaging service. With reserved = None (no
messaging transport) the keepalive is skipped; the sys metric still flows through the metric
subsystem. The returned Heartbeat must be kept alive — dropping it stops the task.
// inside the runtime builder (not callable from component code):let hb = Heartbeat::start(config, metrics, reserved);// ...drop(hb); // RAII stop + best-effort STOPPEDstart takes a ConfigProvider (a () => Config getter), the metric service, and an optional
messaging service (absent ⇒ the keepalive is skipped; the sys metric still emits).
import { Heartbeat } from "@edgecommons/edgecommons";
const hb = Heartbeat.start(() => currentConfig, metrics, messaging);
await hb.stop(); // idempotent; publishes the best-effort STOPPED stateHeartbeatMonitor and getStats
Section titled “HeartbeatMonitor and getStats”HeartbeatMonitor is the sampler the heartbeat task uses each tick. It collects only the enabled
measures into a nested stats object; the sys metric flattens it to
one value per inner key. You can also use it directly to read process/system health. The
constructor input differs by language: Java takes the HeartbeatConfiguration, Python takes the
ConfigManager, and Rust/TypeScript take a Measures struct of toggles.
import com.mbreissi.edgecommons.heartbeat.HeartbeatMonitor;import com.google.gson.JsonObject;
HeartbeatMonitor monitor = new HeartbeatMonitor(heartbeatConfiguration);JsonObject stats = monitor.getStats(); // nested per-measure object; disabled measures omittedmonitor.updateMetrics(); // advance the OSHI process snapshot for the next // CPU-between-ticks delta (getStats() calls it internally)The Python monitor takes the ConfigManager and reads the heartbeat config from it. get_stats()
returns a dict of the enabled measures (disabled ones are omitted).
from edgecommons.heartbeat.heartbeat_monitor import HeartbeatMonitor
monitor = HeartbeatMonitor(config_service) # reads heartbeat config from the ConfigManagerstats = monitor.get_stats() # dict of enabled measures (psutil-backed)The Rust monitor takes a Measures struct and is mutable (it holds the sysinfo system handle and
the CPU baseline). The first get_stats call establishes the CPU baseline and reports CPU as 0.0.
use edgecommons::heartbeat::HeartbeatMonitor;use edgecommons::config::model::Measures;
let mut monitor = HeartbeatMonitor::new(measures);monitor.set_measures(new_measures); // swap toggles on hot-reloadlet stats = monitor.get_stats(); // serde_json::Value; first CPU sample = 0.0The TypeScript monitor takes a Measures object. CPU is a delta between consecutive getStats
calls, so the first sample reports 0.0.
import { HeartbeatMonitor } from "@edgecommons/edgecommons";
const monitor = new HeartbeatMonitor({ cpu: false, memory: true, disk: false, threads: false, files: false, fds: false,});monitor.setMeasures(newMeasures); // swap toggles on hot-reloadconst stats = monitor.getStats(); // => { memory: { memory_usage: <number> } }Stats shape
Section titled “Stats shape”When a measure is enabled, getStats includes its nested object; disabled measures are omitted. The
shape is identical across all four languages:
{ "cpu": { "cpu_usage": 12.5 }, "memory": { "memory_usage": 84 }, "disk": { "disk_total": 512.0, "disk_used": 210.4, "disk_free": 301.6 }, "threads": { "threads": 14 }, "files": { "files": 23 }, "fds": { "fds": 31 }}The sys metric flattens this to one metric value per inner key (cpu_usage, memory_usage,
disk_total, …). Units and per-platform availability (for example Java fds is -1 on Windows,
and TypeScript threads / files / fds are Linux-only) are documented in the
guide’s measures table.
Configuration model
Section titled “Configuration model”Each SDK has a typed model for the heartbeat config section. The accessors below are read by the
runtime; you rarely touch them directly.
com.mbreissi.edgecommons.config.HeartbeatConfiguration:
public class HeartbeatConfiguration { public static final String DEFAULT_DESTINATION = "local";
public boolean isEnabled(); // default true public int getIntervalSecs(); // default 5, minimum 1 public boolean includeCpu(); public boolean includeMemory(); public boolean includeDisk(); public boolean includeThreads(); public boolean includeFiles(); public boolean includeFds(); public String getDestination(); // "local" | "northbound"}edgecommons.config.heartbeat_config.HeartbeatConfiguration:
class HeartbeatConfiguration: DEFAULT_DESTINATION = "local"
def is_enabled(self) -> bool: ... # default True def get_interval_secs(self) -> int: ... # default 5 def include_cpu(self) -> bool: ... def include_memory(self) -> bool: ... def include_disk(self) -> bool: ... def include_threads(self) -> bool: ... def include_files(self) -> bool: ... def include_fds(self) -> bool: ... def get_destination(self) -> str: ... # "local" | "northbound"edgecommons::config::model:
pub struct HeartbeatConfig { pub enabled: bool, // default true pub interval_secs: Option<u64>, // None -> default 5; clamped to a minimum of 1 pub measures: Measures, pub destination: Option<String>, // None -> "local"}
impl HeartbeatConfig { pub const DEFAULT_DESTINATION: &'static str = "local"; pub fn destination(&self) -> &str; // resolved ("local" | "northbound")}
pub struct Measures { // Default: cpu/memory true, rest false pub cpu: bool, pub memory: bool, pub disk: bool, pub threads: bool, pub files: bool, pub fds: bool,}edgecommons config model:
export class HeartbeatConfig { enabled: boolean; // default true intervalSecs: number; // default 5, minimum 1 (out-of-range falls back to 5) measures: Measures; // cpu/memory default ON, the rest off destination: string; // "local" (default) | "northbound" — the state keepalive only}
export interface Measures { cpu: boolean; memory: boolean; disk: boolean; threads: boolean; files: boolean; fds: boolean;}The heartbeat config shape
Section titled “The heartbeat config shape”The section is validated against the heartbeat schema section of
schema/edgecommons-config-schema.json. The defaults are identical in all four languages
(on / 5 s / cpu+memory / local):
{ "heartbeat": { "enabled": true, "intervalSecs": 5, "measures": { "cpu": true, "memory": true, "disk": false, "threads": false, "files": false, "fds": false }, "destination": "local" }}| Field | Type | Default | Notes |
|---|---|---|---|
enabled |
boolean | true |
Turns the whole heartbeat (keepalive + sys metric) on/off. |
intervalSecs |
integer (min 1) | 5 |
Tick period. |
measures.cpu … measures.fds |
boolean | cpu/memory on, rest off |
Per-measure toggles for the sys metric. |
destination |
string | local |
local | northbound — the state keepalive destination only; measures route via metricEmission. |
The sys metric’s storageResolution is 1 when intervalSecs is under 60, otherwise 60
(all four languages).