Skip to content

Commit a04044c

Browse files
SimonHeybrockclaude
andcommitted
Fix buffer timespan requirement updates and overflow handling
This fixes two related issues with buffer timespan management: 1. Simplify _update_buffer_requirements to work with the updated get_required_timespan() contract (now returns float, not float | None). This prevents stale timespan requirements from persisting when all extractors with timespan requirements are removed. 2. Fix TemporalBuffer overflow when timespan=0.0 by properly dropping all existing data to make room for new values. Previously, the buffer would fail with "exceeds buffer capacity even after trimming" because trimming did nothing when timespan <= 0. Added test_timespan_zero_trims_all_old_data_on_overflow to verify the fix using TDD approach. Original prompt: Consider potential bug in _update_buffer_requirements: If extractor was removed and only LatestValueExtractor remains then timespans will be empty so an old requirement for a timespan will not be cleared? Can you write a test trying to reproduce this? 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 6380966 commit a04044c

File tree

3 files changed

+59
-6
lines changed

3 files changed

+59
-6
lines changed

src/ess/livedata/dashboard/temporal_buffer_manager.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,8 @@ def _update_buffer_requirements(
244244
List of extractors to gather requirements from.
245245
"""
246246
# Compute maximum required timespan
247-
timespans = [
248-
ts for e in extractors if (ts := e.get_required_timespan()) is not None
249-
]
250-
if timespans:
251-
max_timespan = max(timespans)
247+
if extractors:
248+
max_timespan = max(e.get_required_timespan() for e in extractors)
252249
buffer.set_required_timespan(max_timespan)
253250
logger.debug(
254251
"Set buffer required timespan to %.2f seconds (from %d extractors)",

src/ess/livedata/dashboard/temporal_buffers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,14 @@ def _initialize_buffers(self, data: sc.DataArray) -> None:
371371

372372
def _trim_to_timespan(self, new_data: sc.DataArray) -> None:
373373
"""Trim buffer to keep only data within required timespan."""
374-
if self._required_timespan <= 0:
374+
if self._required_timespan < 0:
375+
return
376+
377+
if self._required_timespan == 0.0:
378+
# Keep only the latest value - drop all existing data
379+
drop_count = self._data_buffer.size
380+
self._data_buffer.drop(drop_count)
381+
self._time_buffer.drop(drop_count)
375382
return
376383

377384
# Get latest time from new data

tests/dashboard/temporal_buffers_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,55 @@ def test_capacity_exceeded_even_after_trimming_raises(self):
406406
with pytest.raises(ValueError, match="exceeds buffer capacity even after"):
407407
buffer.add(large_data)
408408

409+
def test_timespan_zero_trims_all_old_data_on_overflow(self):
410+
"""Test that timespan=0.0 trims all data to make room for new data."""
411+
buffer = TemporalBuffer()
412+
buffer.set_required_timespan(0.0) # Keep only latest value
413+
buffer.set_max_memory(100) # Small memory limit to force overflow
414+
415+
# Add first data point
416+
data1 = sc.DataArray(
417+
sc.array(dims=['x'], values=[1.0, 2.0], unit='counts'),
418+
coords={
419+
'x': sc.arange('x', 2, unit='m'),
420+
'time': sc.scalar(0.0, unit='s'),
421+
},
422+
)
423+
buffer.add(data1)
424+
initial_capacity = buffer._data_buffer.max_capacity
425+
426+
# Fill buffer to capacity
427+
for t in range(1, initial_capacity):
428+
data = sc.DataArray(
429+
sc.array(dims=['x'], values=[float(t), float(t)], unit='counts'),
430+
coords={
431+
'x': sc.arange('x', 2, unit='m'),
432+
'time': sc.scalar(float(t), unit='s'),
433+
},
434+
)
435+
buffer.add(data)
436+
437+
# Buffer is now full, verify it has all data
438+
result = buffer.get()
439+
assert result.sizes['time'] == initial_capacity
440+
441+
# Add one more data point - should trigger trimming
442+
# With timespan=0.0, should drop ALL old data to make room
443+
data_new = sc.DataArray(
444+
sc.array(dims=['x'], values=[999.0, 999.0], unit='counts'),
445+
coords={
446+
'x': sc.arange('x', 2, unit='m'),
447+
'time': sc.scalar(999.0, unit='s'),
448+
},
449+
)
450+
buffer.add(data_new) # Should not raise
451+
452+
# Should only have the latest value
453+
result = buffer.get()
454+
assert result.sizes['time'] == 1
455+
assert result.coords['time'].values[0] == 999.0
456+
assert result['time', 0].values[0] == 999.0
457+
409458

410459
class TestVariableBuffer:
411460
"""Tests for VariableBuffer."""

0 commit comments

Comments
 (0)