Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit e4127cb

Browse files
committed
wait spinner: throttle
1 parent afaf058 commit e4127cb

2 files changed

Lines changed: 122 additions & 6 deletions

File tree

packages/jumpstarter/jumpstarter/client/lease.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def _acquire(self):
141141
# lease ready
142142
if condition_true(result.conditions, "Ready"):
143143
logger.debug("Lease %s acquired", self.name)
144-
spinner.update_status(f"Lease {self.name} acquired successfully!")
144+
spinner.update_status(f"Lease {self.name} acquired successfully!", force=True)
145145
self.exporter_name = result.exporter
146146
break
147147

@@ -314,6 +314,8 @@ def __init__(self, lease_name: str | None = None):
314314
self.start_time = None
315315
self._should_show_spinner = self._is_terminal_available() and not self._is_non_interactive()
316316
self._current_message = None
317+
self._last_log_time = None
318+
self._log_throttle_interval = timedelta(minutes=5)
317319

318320
def _is_non_interactive(self) -> bool:
319321
"""Check if the user desires a NONINTERACTIVE environment."""
@@ -341,18 +343,32 @@ def __exit__(self, exc_type, exc_val, exc_tb):
341343
if self.spinner:
342344
self.spinner.stop()
343345

344-
def update_status(self, message: str):
345-
"""Update the spinner status message."""
346+
def update_status(self, message: str, force: bool = False):
347+
"""Update the spinner status message.
348+
349+
:param message: The status message to display
350+
:param force: If True, always log the message even when throttling (default: False)
351+
"""
346352
if self.spinner and self._should_show_spinner:
347353
self._current_message = f"[blue]{message}[/blue]"
348354
elapsed = datetime.now() - self.start_time
349355
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
350356
self.spinner.update(f"{self._current_message} [dim]({elapsed_str})[/dim]")
351357
else:
352358
# Log info message when no console is available
353-
elapsed = datetime.now() - self.start_time
354-
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
355-
logger.info(f"{message} ({elapsed_str})")
359+
# Throttle updates to at most every 5 minutes unless forced
360+
now = datetime.now()
361+
should_log = (
362+
force
363+
or self._last_log_time is None
364+
or (now - self._last_log_time) >= self._log_throttle_interval
365+
)
366+
367+
if should_log:
368+
elapsed = now - self.start_time
369+
elapsed_str = str(elapsed).split(".")[0] # Remove microseconds
370+
logger.info(f"{message} ({elapsed_str})")
371+
self._last_log_time = now
356372

357373
def tick(self):
358374
"""Update the spinner with current elapsed time without changing the message."""

packages/jumpstarter/jumpstarter/client/lease_test.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,103 @@ def test_start_time_initialization_in_context(self):
228228
with spinner:
229229
assert spinner.start_time is not None
230230
assert isinstance(spinner.start_time, datetime)
231+
232+
def test_throttling_first_update_logged(self, caplog):
233+
"""Test that the first update is always logged when console is not available."""
234+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=False):
235+
spinner = LeaseAcquisitionSpinner("test-lease")
236+
spinner.start_time = datetime.now()
237+
238+
with caplog.at_level(logging.INFO):
239+
spinner.update_status("First message")
240+
241+
assert "First message" in caplog.text
242+
assert spinner._last_log_time is not None
243+
244+
def test_throttling_second_update_within_interval_not_logged(self, caplog):
245+
"""Test that updates within 5 minutes are not logged."""
246+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=False):
247+
spinner = LeaseAcquisitionSpinner("test-lease")
248+
spinner.start_time = datetime.now()
249+
spinner._last_log_time = datetime.now() - timedelta(minutes=2) # 2 minutes ago
250+
251+
with caplog.at_level(logging.INFO):
252+
spinner.update_status("Second message")
253+
254+
# Should not log because only 2 minutes have passed
255+
assert "Second message" not in caplog.text
256+
257+
def test_throttling_update_after_interval_logged(self, caplog):
258+
"""Test that updates after 5 minutes are logged."""
259+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=False):
260+
spinner = LeaseAcquisitionSpinner("test-lease")
261+
spinner.start_time = datetime.now()
262+
spinner._last_log_time = datetime.now() - timedelta(minutes=6) # 6 minutes ago
263+
264+
with caplog.at_level(logging.INFO):
265+
spinner.update_status("After interval message")
266+
267+
assert "After interval message" in caplog.text
268+
assert spinner._last_log_time is not None
269+
270+
def test_throttling_forced_update_always_logged(self, caplog):
271+
"""Test that forced updates are always logged regardless of throttle interval."""
272+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=False):
273+
spinner = LeaseAcquisitionSpinner("test-lease")
274+
spinner.start_time = datetime.now()
275+
spinner._last_log_time = datetime.now() - timedelta(minutes=1) # 1 minute ago
276+
277+
with caplog.at_level(logging.INFO):
278+
spinner.update_status("Forced message", force=True)
279+
280+
assert "Forced message" in caplog.text
281+
assert spinner._last_log_time is not None
282+
283+
def test_throttling_multiple_updates_only_logs_when_needed(self, caplog):
284+
"""Test that multiple rapid updates only log at appropriate intervals."""
285+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=False):
286+
spinner = LeaseAcquisitionSpinner("test-lease")
287+
spinner.start_time = datetime.now()
288+
289+
with caplog.at_level(logging.INFO):
290+
# First update should be logged
291+
spinner.update_status("Message 1")
292+
assert "Message 1" in caplog.text
293+
294+
# Set last log time to recent
295+
spinner._last_log_time = datetime.now() - timedelta(minutes=1)
296+
297+
# Second update should not be logged (within interval)
298+
spinner.update_status("Message 2")
299+
assert "Message 2" not in caplog.text
300+
301+
# Third update should not be logged (within interval)
302+
spinner.update_status("Message 3")
303+
assert "Message 3" not in caplog.text
304+
305+
# Set last log time to past the interval
306+
spinner._last_log_time = datetime.now() - timedelta(minutes=6)
307+
308+
# Fourth update should be logged (past interval)
309+
spinner.update_status("Message 4")
310+
assert "Message 4" in caplog.text
311+
312+
def test_throttling_not_applied_when_console_available(self):
313+
"""Test that throttling is not applied when console is available."""
314+
with patch.object(LeaseAcquisitionSpinner, '_is_terminal_available', return_value=True):
315+
spinner = LeaseAcquisitionSpinner("test-lease")
316+
spinner.start_time = datetime.now()
317+
318+
mock_spinner = Mock()
319+
spinner.spinner = mock_spinner
320+
321+
# Multiple updates should all call update() regardless of throttle
322+
spinner.update_status("Message 1")
323+
spinner.update_status("Message 2")
324+
spinner.update_status("Message 3")
325+
326+
# All should be called even if we set a recent last_log_time
327+
spinner._last_log_time = datetime.now() - timedelta(minutes=1)
328+
spinner.update_status("Message 4")
329+
330+
assert mock_spinner.update.call_count == 4

0 commit comments

Comments
 (0)