Problem
Monitoring/function sensors run user callbacks from scheduled tasks. Recent stability work found two related races:
RestartTimer() could start a new timer while an old callback was still blocked, allowing overlapping callbacks for the same sensor.
- A long-running callback could return after bounded sensor stop and after the data queue had already been drained; the value could then be accepted into a stopped queue and become stranded.
The current fix uses per-sensor in-progress/stopping flags. That is useful, but a more explicit lifecycle generation/epoch model would make restart/stop/dispose behavior easier to reason about and easier to port to C++.
Goal
Make monitoring callback validity explicit: every scheduled callback should know which sensor lifecycle generation it belongs to, and it should verify that the generation is still current before publishing values.
Proposed direction
Introduce a lightweight internal epoch/token for monitoring sensors:
- Increment or replace the epoch when the sensor starts/restarts/stops.
- Timer callbacks capture the epoch when they begin.
- Before sending a value, the callback verifies:
- sensor is not stopped;
- captured epoch still matches current epoch;
- no other callback is currently publishing for this sensor.
- Late callbacks should exit cleanly without enqueueing stale values.
Possible sketch:
private long _lifecycleEpoch;
private int _sendValueInProgress;
private long BeginCallbackEpoch() => Volatile.Read(ref _lifecycleEpoch);
private bool IsCallbackStillCurrent(long epoch) =>
epoch == Volatile.Read(ref _lifecycleEpoch) && !IsStopped;
The exact implementation can differ, but it should encode the lifecycle explicitly rather than relying only on ad hoc flags.
Acceptance criteria
- Function/monitoring callbacks for one sensor do not overlap.
RestartTimer() cannot publish values from an old timer generation after the restart boundary.
Stop() waits for short in-flight callbacks and flushes their values.
- Callbacks that outlive bounded stop wait do not enqueue into stopped queues.
- Existing tests remain green:
- restart under load without overlap;
- restart while callback is blocked;
- short callback is flushed during stop;
- long callback released after data flush is rejected or flushed safely.
- Add or update tests if generation-specific behavior is not already covered.
- Full managed collector tests pass.
C++ port note
The C++ collector should use the same concept early: scheduled callbacks should carry a lifecycle generation or cancellation token and must not publish after stop/restart invalidates them.
Problem
Monitoring/function sensors run user callbacks from scheduled tasks. Recent stability work found two related races:
RestartTimer()could start a new timer while an old callback was still blocked, allowing overlapping callbacks for the same sensor.The current fix uses per-sensor in-progress/stopping flags. That is useful, but a more explicit lifecycle generation/epoch model would make restart/stop/dispose behavior easier to reason about and easier to port to C++.
Goal
Make monitoring callback validity explicit: every scheduled callback should know which sensor lifecycle generation it belongs to, and it should verify that the generation is still current before publishing values.
Proposed direction
Introduce a lightweight internal epoch/token for monitoring sensors:
Possible sketch:
The exact implementation can differ, but it should encode the lifecycle explicitly rather than relying only on ad hoc flags.
Acceptance criteria
RestartTimer()cannot publish values from an old timer generation after the restart boundary.Stop()waits for short in-flight callbacks and flushes their values.C++ port note
The C++ collector should use the same concept early: scheduled callbacks should carry a lifecycle generation or cancellation token and must not publish after stop/restart invalidates them.