Skip to content

feat(membership): retain local health history - #10502

Merged
ReubenBond merged 5 commits into
dotnet:mainfrom
ReubenBond:rb-health-status-history
Aug 12, 2026
Merged

ReubenBond merged 5 commits into
dotnet:mainfrom
ReubenBond:rb-health-status-history

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Aug 11, 2026 •

Copy link
Copy Markdown
Member

Local silo health checks currently run on demand, can overlap, and retain no history. This makes concurrent probe decisions repeat expensive checks and prevents callers from reasoning about recent host degradation.

This change records one minute of typed health events, serializes sampling to at most once per second, and aggregates the worst observation per check over a requested interval. Host-local and network-derived checks are classified separately so probe timeout dilation uses only recent local-host degradation.

Watchdog runtime, GC, component-check, and thread-pool stalls remain distinct events for query-time aggregation. Membership health components now use the keyed membership TimeProvider while preserving the connection-liveness checks already present on main.

Microsoft Reviewers: Open in CodeFlow

@ReubenBond
ReubenBond force-pushed the rb-health-status-history branch 2 times, most recently from 95146d1 to c407cde Compare August 11, 2026 21:14
Copilot AI lite review requested due to automatic review settings August 11, 2026 23:32
@ReubenBond
ReubenBond force-pushed the rb-health-status-history branch from c407cde to 13b9481 Compare August 11, 2026 23:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances Orleans membership health monitoring by retaining a short history of local silo health events and by making time-dependent health/probing behavior testable and deterministic via injected (keyed) TimeProviders.

Changes:

  • Add a typed, 1-minute local health event history and interval-based aggregation (with separate local vs network health categories).
  • Serialize/limit on-demand health sampling cadence (targeting once-per-second) and use aggregated local-host degradation to dilate probe timeouts.
  • Plumb the keyed membership TimeProvider through membership/runtime components and add focused unit tests for the new behavior.
Show a summary per file
File Description
test/Orleans.Runtime.Tests/Diagnostics/WatchdogTests.cs Adds tests validating that watchdog runtime checks emit typed local health events.
test/Orleans.Core.Tests/Membership/SiloHealthMonitorTests.cs Updates mocks and adds coverage for probe-timeout dilation driven by local health status.
test/Orleans.Core.Tests/Membership/ProbeRequestMonitorTests.cs Adds test coverage for ProbeRequestMonitor using injected time and reset semantics.
test/Orleans.Core.Tests/Membership/MembershipSystemTargetTests.cs Adds tests covering indirect probe timing and local health interval queries.
test/Orleans.Core.Tests/Membership/MembershipAgentTests.cs Updates to new ILocalSiloHealthMonitor API and new ClusterHealthMonitor ctor signature.
test/Orleans.Core.Tests/Membership/LocalSiloHealthMonitorTests.cs Adds extensive tests for event retention, aggregation, concurrency, and sampling behavior.
test/Orleans.Core.Tests/Membership/ClusterHealthMonitorTests.cs Updates to new ILocalSiloHealthMonitor API and adds time-provider-based stale-join eviction test.
src/Orleans.Runtime/Silo/Watchdog.cs Records runtime stall/GC pause/component stall events into the local health event recorder; uses injected keyed TimeProvider.
src/Orleans.Runtime/Networking/ProbeRequestMonitor.cs Switches to injected keyed TimeProvider and timestamp-based tracking.
src/Orleans.Runtime/MembershipService/SiloHealthMonitor.cs Uses TimeProvider for timing/cancellation; dilates probe timeout based on aggregated local health status.
src/Orleans.Runtime/MembershipService/MembershipSystemTarget.cs Measures probe timing using TimeProvider and queries aggregated local health over the probe interval.
src/Orleans.Runtime/MembershipService/LocalSiloHealthMonitor.cs Introduces typed health events/status, retention window, on-demand caching cadence, and interval aggregation APIs.
src/Orleans.Runtime/MembershipService/ClusterHealthMonitor.cs Moves membership processing and shutdown timing onto injected keyed TimeProvider.
src/Orleans.Runtime/Hosting/DefaultSiloServices.cs Registers ILocalSiloHealthEventRecorder from the LocalSiloHealthMonitor singleton.
src/Orleans.Core/Configuration/Options/ClusterMembershipOptions.cs Updates option documentation to reflect new sampling/logging behavior.

Review details

Tip

Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 15/15 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Orleans.Runtime/MembershipService/MembershipSystemTarget.cs Outdated
Copilot AI review requested due to automatic review settings August 12, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/Orleans.Runtime/MembershipService/SiloHealthMonitor.cs:56

  • ElapsedSinceLastResponse reads _lastSuccessfulResponseTimestamp as a long? without synchronization. Nullable<long> is not an atomic read/write (it is larger than 64-bits), so this can tear when ProbeDirectly/ProbeIndirectly update the field concurrently with ProbingSiloHealthMonitor reading it, resulting in incorrect/negative elapsed values or sporadic HasValue flips. Consider storing a non-nullable long timestamp and reading it atomically via Volatile.Read (the field is always initialized in the ctor anyway).
        public TimeSpan? ElapsedSinceLastResponse
            => _lastSuccessfulResponseTimestamp is { } timestamp ? _timeProvider.GetElapsedTime(timestamp) : null;

src/Orleans.Runtime/MembershipService/LocalSiloHealthMonitor.cs:593

  • SubtractTimestamp converts TimeSpan to a timestamp delta via duration.TotalSeconds * TimestampFrequency. For very small durations (eg TimeSpan.FromTicks(1)), the double conversion can round down and produce a 0 delta, making lookback queries subtly wrong at sub-second granularity. Using integer arithmetic based on duration.Ticks avoids precision loss.
        private long SubtractTimestamp(long timestamp, TimeSpan duration)
            => timestamp - (long)(duration.TotalSeconds * _timeProvider.TimestampFrequency);
  • Files reviewed: 16/16 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 63571f45-a79f-49f9-a50e-296c1355e312
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 63571f45-a79f-49f9-a50e-296c1355e312
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 63571f45-a79f-49f9-a50e-296c1355e312
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 63571f45-a79f-49f9-a50e-296c1355e312
Copilot AI review requested due to automatic review settings August 12, 2026 02:19
@ReubenBond
ReubenBond force-pushed the rb-health-status-history branch from 85c5aae to d76a87a Compare August 12, 2026 02:19
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 63571f45-a79f-49f9-a50e-296c1355e312

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/Orleans.Runtime/MembershipService/SiloHealthMonitor.cs:296

  • ProbeDirectly calls roundTripTimer.GetElapsedTime(...) twice (once when formatting the OperationCanceledException message and again when computing roundTripTime). If time advances between those calls, the exception message and logged round-trip time can disagree, and it does extra timestamp reads.
            catch (OperationCanceledException exception)
            {
                failureException = new OperationCanceledException(
                    $"The ping attempt was cancelled after {roundTripTimer.GetElapsedTime(out _)}. Ping #{id}",
                    exception);
            }
            catch (Exception exception)
            {
                failureException = exception;
            }
            var roundTripTime = roundTripTimer.GetElapsedTime(out _);

src/Orleans.Runtime/MembershipService/LocalSiloHealthMonitor.cs:52

  • LocalSiloHealthStatus.Complaints currently includes any event with a non-null Complaint, even when the event has Score=0 (for example, informational events like GarbageCollectionPause). This can add noise to degradation logs since the complaint list can include non-degrading observations.
    internal readonly record struct LocalSiloHealthStatus(int Score, ImmutableArray<LocalSiloHealthEvent> Events)
    {
        public ImmutableArray<string> Complaints
            => [.. Events.Where(static status => status.Complaint is not null).Select(static status => status.Complaint!)];
  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings August 12, 2026 02:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@github-actions github-actions Bot locked and limited conversation to collaborators Sep 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants