Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

## Upgrading

- The `ChronoError` variant has been removed from the `ErrorKind` enum. This is a breaking change for code that pattern-matches on `ErrorKind`. The variant had no remaining constructors; the time-delta arithmetic that previously surfaced it is now performed inside the new `WallClockTimer`, and resampling-interval misconfiguration is reported as `InvalidConfig`.

- `LogicalMeterConfig` instances can't be created directly anymore, and need to be created using the `LogicalMeterConfig::new` method. This helps avoid future breaking changes, as we add more config parameters.

- Formula streaming methods in the `LogicalMeterHandle` no longer take metric as a function parameter, but expect a generic argument. For example:
Expand Down Expand Up @@ -36,6 +38,8 @@
- `power()` — a `Formula<Power>` for the pool's aggregated power.
- `power_bounds()` — a `broadcast::Receiver<Vec<Bounds<Power>>>` tracking the pool's power bounds.

- The logical meter now survives NTP-style wall-clock jumps. A new internal `WallClockTimer` schedules resampler ticks against the wall clock while sleeping on tokio's monotonic clock; when the two diverge by more than one interval in either direction, the timer realigns and the actor rebuilds its inner resamplers against the new clock frame. Subscribers see one `None` sample at the resync tick (preserving the every-interval cadence) and real values resume on the next tick. Note: a backward jump produces a sample timestamped in the past relative to the previous one — consumers that assume monotonically increasing `Sample` timestamps must handle this.

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
4 changes: 2 additions & 2 deletions src/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,8 @@ mod tests {
/// single endpoint merge into one.
#[test]
fn squash_merges_touching_endpoints() {
let a = vec![Bounds::new(Some(1.0), Some(5.0))];
let b = vec![Bounds::new(Some(5.0), Some(10.0))];
let a = [Bounds::new(Some(1.0), Some(5.0))];
let b = [Bounds::new(Some(5.0), Some(10.0))];
// `intersect_bounds_sets` runs the pairwise intersect through squash.
let result = intersect_bounds_sets(
&[Bounds::new(Some(0.0), Some(20.0))],
Expand Down
330 changes: 175 additions & 155 deletions src/client/test_utils.rs
Comment thread
llucax marked this conversation as resolved.

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions src/client/test_utils/tokio_synced_clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// License: MIT
// Copyright © 2026 Frequenz Energy-as-a-Service GmbH

//! Test clock for `#[tokio::test(start_paused = true)]` tests.
//!
//! Pins wall-clock time to tokio's (possibly paused) monotonic clock, so
//! advancing tokio time advances wall time too. Clones share state, so a
//! test can hand copies to the actor and a mock telemetry source and jump
//! both by calling
//! [`inject_wall_jump`][TokioSyncedClock::inject_wall_jump] once.
//!
//! Lives under `test_utils` so the `Arc`/`RwLock`/injection machinery
//! isn't compiled into production builds.

use chrono::{DateTime, TimeDelta, Utc};
use tokio::time::Instant;

use crate::wall_clock_timer::Clock;

#[derive(Clone, Debug)]
pub struct TokioSyncedClock {
inner: std::sync::Arc<std::sync::RwLock<TokioSyncedClockInner>>,
}

#[derive(Debug)]
struct TokioSyncedClockInner {
wall_anchor: DateTime<Utc>,
mono_anchor: Instant,
}

impl Default for TokioSyncedClock {
fn default() -> Self {
Self::new()
}
}

impl TokioSyncedClock {
/// Creates a clock anchored to the current `Utc::now()` (and the current
/// tokio monotonic instant). Suitable when the caller doesn't care about
/// a specific starting wall-clock value.
pub fn new() -> Self {
Self::with_wall_anchor(Utc::now())
}

/// Creates a clock whose wall-clock time at the current tokio instant
/// is exactly `wall_anchor`. Useful when the caller needs the anchor
/// aligned to a specific boundary (e.g. a whole-second tick).
pub fn with_wall_anchor(wall_anchor: DateTime<Utc>) -> Self {
Self {
inner: std::sync::Arc::new(std::sync::RwLock::new(TokioSyncedClockInner {
wall_anchor,
mono_anchor: Instant::now(),
})),
}
}

/// Shifts wall-clock time by `offset` relative to the monotonic clock,
/// simulating an NTP jump. Visible to every clone.
pub fn inject_wall_jump(&self, offset: TimeDelta) {
let mut inner = self.inner.write().expect("clock poisoned");
// Only `wall_anchor` moves — `mono_anchor` is intentionally left
// untouched so wall and monotonic diverge, which is what makes
// this simulate an NTP jump rather than a re-anchor of both
// clocks together.
inner.wall_anchor += offset;
Comment thread
llucax marked this conversation as resolved.
}
}

impl Clock for TokioSyncedClock {
fn wall_now(&self) -> DateTime<Utc> {
let inner = self.inner.read().expect("clock poisoned");
let elapsed = Instant::now().duration_since(inner.mono_anchor);
inner.wall_anchor
+ TimeDelta::from_std(elapsed).expect("tokio elapsed fits in TimeDelta (~292 years)")
}
}
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ ErrorKind!(
(ComponentGraphError, component_graph_error),
(ComponentDataError, component_data_error),
(ConnectionFailure, connection_failure),
(ChronoError, chrono_error),
(DroppedUnusedFormulas, dropped_unused_formulas),
(FormulaEngineError, formula_engine_error),
(InvalidComponent, invalid_component),
(InvalidConfig, invalid_config),
Comment thread
llucax marked this conversation as resolved.
(Internal, internal),
(APIServerError, api_server_error),
);
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ pub use logical_meter::{Formula, FormulaSubscriber, LogicalMeterConfig, LogicalM

pub mod metric;

pub(crate) mod wall_clock_timer;

mod microgrid;
pub use microgrid::{BatteryPool, Microgrid};
Loading
Loading