Replace memory service timestamp sleeps with monotonic busy-wait #10789

Closed
HAL9000 wants to merge 1 commits from fix/issue-9963-memory-service-timestamp-guards into main
3 changed files with 85 additions and 5 deletions
+6
Protected
View File
@@ -0,0 +1,6 @@
Feature: Memory service timestamp guards
Scenario: Wait helper enforces deadline when clock freezes
Given the memory service coverage steps module
When I wait for clock advancement with a frozen clock and deadline 0.01 seconds
Then the wait helper raises a deadline assertion
+62
Protected
View File
@@ -0,0 +1,62 @@
"""Behave steps validating timestamp wait helper for memory service coverage."""
from __future__ import annotations
import importlib
from datetime import UTC, datetime
from typing import Any
from behave import given, then, when
@given("the memory service coverage steps module")
def step_get_memory_service_module(context: Any) -> None:
"""Import the coverage steps module and ensure wait helper exists."""
module = importlib.import_module("features.steps.memory_service_coverage_steps")
context.memory_service_steps_module = module
assert hasattr(
module, "_wait_for_clock_advance"
), "memory_service_coverage_steps._wait_for_clock_advance is required"
@when(
"I wait for clock advancement with a frozen clock and deadline {deadline:f} seconds"
)
def step_wait_with_frozen_clock(context: Any, deadline: float) -> None:
"""Execute the wait helper with a clock that never advances."""
module = context.memory_service_steps_module
before = datetime.now(UTC)
context._wait_deadline = deadline
context._captured_error = None
context._original_datetime = module.datetime
class FrozenDateTime:
"""Stub datetime that always returns the same moment."""
UTC = module.UTC
def __init__(self, value: datetime) -> None:
self._value = value
def now(self, tz: object) -> datetime:
return self._value
module.datetime = FrozenDateTime(before)
try:
module._wait_for_clock_advance(before, deadline_secs=deadline)
except AssertionError as err:
context._captured_error = err
finally:
module.datetime = context._original_datetime
@then("the wait helper raises a deadline assertion")
def step_expect_deadline_assertion(context: Any) -> None:
"""Validate the helper raised AssertionError with deadline details."""
assert context._captured_error is not None
message = str(context._captured_error)
assert f"within {context._wait_deadline}s" in message
+17 -5
Protected
View File
@@ -6,7 +6,7 @@ import asyncio
import shutil
import tempfile
import time
from datetime import UTC
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from unittest.mock import patch
@@ -24,6 +24,18 @@ from cleveragents.application.services.memory_service import (
)
def _wait_for_clock_advance(before: datetime, deadline_secs: float = 2.0) -> None:
"""Spin until the UTC clock advances past ``before`` or raise on timeout."""
deadline = time.monotonic() + deadline_secs
while datetime.now(UTC) <= before:
if time.monotonic() > deadline:
raise AssertionError(
f"Clock did not advance past {before} within {deadline_secs}s"
)
time.sleep(0.001)
@given("a conversation adapter configured to return text values")
def step_adapter_returning_text(context: Any) -> None:
"""Create an adapter that returns buffer strings."""
@@ -434,7 +446,7 @@ def step_have_tracked_file_entity(context: Any, name: str) -> None:
@when("I track the same entity again")
def step_track_same_entity(context: Any) -> None:
"""Track the same entity again to update it."""
time.sleep(0.01) # Small delay to ensure time difference
_wait_for_clock_advance(datetime.now(UTC))
context.tracked_entity = context.memory_service.track_entity(
context.tracked_entity.name, context.tracked_entity.entity_type
)
@@ -503,9 +515,9 @@ def step_memory_service_entities_over_time(context: Any) -> None:
# Track entities with small delays to ensure different timestamps
context.memory_service.track_entity("oldest", EntityType.PROJECT)
time.sleep(0.01)
_wait_for_clock_advance(datetime.now(UTC))
context.memory_service.track_entity("middle", EntityType.PLAN)
time.sleep(0.01)
_wait_for_clock_advance(datetime.now(UTC))
context.memory_service.track_entity("newest", EntityType.FILE)
@@ -760,7 +772,7 @@ def step_track_project_with_initial_metadata(context: Any, name: str) -> None:
@when("I track the same project entity again with additional metadata")
def step_track_same_entity_with_additional_metadata(context: Any) -> None:
"""Track the same entity with additional metadata to trigger update."""
time.sleep(0.01) # Small delay to ensure time difference
_wait_for_clock_advance(datetime.now(UTC))
additional_metadata = {"status": "active", "priority": "high"}
context.tracked_entity = context.memory_service.track_entity(
context.entity_name, EntityType.PROJECT, additional_metadata