Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Test hygiene

- Sweep remaining `time.sleep` → `await asyncio.sleep` in async tests
(`test_memory_state.py`, `test_state_postgres.py`). Closes the same
flaky-test hazard fixed for the Redis backend in PR #73.

## 0.4.26.1 (2026-04-23)

Python-only follow-up on `0.4.26`. Still alpha — APIs may change.
Expand Down
15 changes: 8 additions & 7 deletions tests/test_memory_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import asyncio
import time

import pytest
Expand Down Expand Up @@ -139,7 +140,7 @@ async def test_set_with_ttl_expires(self, memory_state: MemoryStateAdapter):
# Set with a very short TTL
await memory_state.set("key", "value", ttl_ms=1)
# Wait for expiry
time.sleep(0.005) # 5ms -- well past 1ms TTL
await asyncio.sleep(0.005) # 5ms -- well past 1ms TTL
assert await memory_state.get("key") is None

@pytest.mark.asyncio
Expand All @@ -151,13 +152,13 @@ async def test_set_with_ttl_available_before_expiry(self, memory_state: MemorySt
async def test_set_without_ttl_never_expires(self, memory_state: MemoryStateAdapter):
await memory_state.set("key", "value")
# Even after a brief wait
time.sleep(0.005)
await asyncio.sleep(0.005)
assert await memory_state.get("key") == "value"

@pytest.mark.asyncio
async def test_set_if_not_exists_respects_expired_key(self, memory_state: MemoryStateAdapter):
await memory_state.set("key", "old", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
result = await memory_state.set_if_not_exists("key", "new")
assert result is True
assert await memory_state.get("key") == "new"
Expand Down Expand Up @@ -191,7 +192,7 @@ async def test_acquire_lock_fails_when_already_held(self, memory_state: MemorySt
async def test_acquire_lock_succeeds_after_expiry(self, memory_state: MemoryStateAdapter):
lock1 = await memory_state.acquire_lock("thread-1", 1) # 1ms TTL
assert lock1 is not None
time.sleep(0.005)
await asyncio.sleep(0.005)

lock2 = await memory_state.acquire_lock("thread-1", 30_000)
assert lock2 is not None
Expand Down Expand Up @@ -234,7 +235,7 @@ async def test_extend_lock(self, memory_state: MemoryStateAdapter):
assert result is True

# Lock should still be held after original TTL would have expired
time.sleep(0.15)
await asyncio.sleep(0.15)
lock2 = await memory_state.acquire_lock("thread-1", 30_000)
assert lock2 is None # Still held due to extension

Expand All @@ -251,7 +252,7 @@ async def test_extend_lock_fails_with_wrong_token(self, memory_state: MemoryStat
async def test_extend_lock_fails_after_expiry(self, memory_state: MemoryStateAdapter):
lock = await memory_state.acquire_lock("thread-1", 1)
assert lock is not None
time.sleep(0.005)
await asyncio.sleep(0.005)

result = await memory_state.extend_lock(lock, 60_000)
assert result is False
Expand Down Expand Up @@ -317,7 +318,7 @@ async def test_append_to_list_with_max_length(self, memory_state: MemoryStateAda
@pytest.mark.asyncio
async def test_list_with_ttl_expires(self, memory_state: MemoryStateAdapter):
await memory_state.append_to_list("key", "a", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
result = await memory_state.get_list("key")
assert result == []

Expand Down
18 changes: 9 additions & 9 deletions tests/test_state_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class simulates the asyncpg pool interface using in-memory dicts to

from __future__ import annotations

import asyncio
import datetime as _dt
import re
import time
Expand Down Expand Up @@ -542,7 +543,7 @@ class TestPostgresStateTTL:
@pytest.mark.asyncio
async def test_set_with_ttl_expires(self, pg_state: PostgresStateAdapter):
await pg_state.set("key", "value", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
assert await pg_state.get("key") is None

@pytest.mark.asyncio
Expand All @@ -553,14 +554,14 @@ async def test_set_with_ttl_available_before_expiry(self, pg_state: PostgresStat
@pytest.mark.asyncio
async def test_set_without_ttl_never_expires(self, pg_state: PostgresStateAdapter):
await pg_state.set("key", "value")
time.sleep(0.005)
await asyncio.sleep(0.005)
assert await pg_state.get("key") == "value"

@pytest.mark.asyncio
async def test_expired_key_cleaned_on_get(self, pg_state: PostgresStateAdapter, mock_pool: MockAsyncpgPool):
"""When get() returns None for an expired key, the adapter runs an opportunistic DELETE."""
await pg_state.set("key", "value", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
await pg_state.get("key")

# After the get, the expired row should have been cleaned up
Expand Down Expand Up @@ -598,7 +599,7 @@ async def test_with_ttl(self, pg_state: PostgresStateAdapter):
@pytest.mark.asyncio
async def test_succeeds_after_expired_key(self, pg_state: PostgresStateAdapter):
await pg_state.set("key", "old", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
# The key is expired; set_if_not_exists should succeed
result = await pg_state.set_if_not_exists("key", "new")
assert result is True
Expand Down Expand Up @@ -632,7 +633,7 @@ async def test_acquire_lock_fails_when_held(self, pg_state: PostgresStateAdapter
async def test_acquire_lock_succeeds_when_expired(self, pg_state: PostgresStateAdapter):
lock1 = await pg_state.acquire_lock("thread-1", 1)
assert lock1 is not None
time.sleep(0.005)
await asyncio.sleep(0.005)
lock2 = await pg_state.acquire_lock("thread-1", 30_000)
assert lock2 is not None
assert lock2.thread_id == "thread-1"
Expand Down Expand Up @@ -670,7 +671,7 @@ async def test_extend_lock(self, pg_state: PostgresStateAdapter):
assert result is True

# Lock should still be held after original TTL
time.sleep(0.15)
await asyncio.sleep(0.15)
lock2 = await pg_state.acquire_lock("thread-1", 30_000)
assert lock2 is None

Expand All @@ -687,7 +688,7 @@ async def test_extend_lock_wrong_token_fails(self, pg_state: PostgresStateAdapte
async def test_extend_lock_after_expiry_fails(self, pg_state: PostgresStateAdapter):
lock = await pg_state.acquire_lock("thread-1", 1)
assert lock is not None
time.sleep(0.005)
await asyncio.sleep(0.005)

result = await pg_state.extend_lock(lock, 60_000)
assert result is False
Expand Down Expand Up @@ -752,7 +753,6 @@ async def test_acquire_lock_uses_single_atomic_upsert(

# Third acquire after expiry: should succeed in single query
mock_pool.executed_queries.clear()
time.sleep(0.005)
# Force-expire the lock for testing
lock_key = ("test", "race-thread")
mock_pool.locks[lock_key]["expires_at"] = _dt.datetime.now(_dt.timezone.utc) - _dt.timedelta(seconds=1)
Expand Down Expand Up @@ -801,7 +801,7 @@ async def test_append_preserves_order(self, pg_state: PostgresStateAdapter):
@pytest.mark.asyncio
async def test_list_with_ttl_expires(self, pg_state: PostgresStateAdapter):
await pg_state.append_to_list("key", "a", ttl_ms=1)
time.sleep(0.005)
await asyncio.sleep(0.005)
result = await pg_state.get_list("key")
assert result == []

Expand Down
Loading