Description
FileCheckpointStorage.delete() performs an exists() check followed by unlink(). Those operations are not atomic. If two callers delete the same checkpoint concurrently, both can observe that the file exists; one removes it and the other leaks a raw FileNotFoundError.
The CheckpointStorage.delete() contract says the method returns True when a checkpoint was deleted and False when it does not exist. A concurrent disappearance should therefore produce False, not an undocumented filesystem exception.
Expected behavior: concurrent deletes complete with one True and one False, without leaking FileNotFoundError.
I intend to submit a small fix that attempts unlink() directly, catches FileNotFoundError, and adds a deterministic concurrency regression test.
Code Sample
import asyncio
import tempfile
import threading
from pathlib import Path
from unittest.mock import patch
from agent_framework import FileCheckpointStorage, WorkflowCheckpoint
async def main() -> None:
with tempfile.TemporaryDirectory() as directory:
storage = FileCheckpointStorage(directory)
checkpoint = WorkflowCheckpoint(
workflow_name="wf",
graph_signature_hash="hash",
checkpoint_id="same",
)
await storage.save(checkpoint)
target = (Path(directory) / "same.json").resolve()
original_exists = Path.exists
barrier = threading.Barrier(2)
def synchronized_exists(path: Path) -> bool:
result = original_exists(path)
if path.resolve() == target:
barrier.wait()
return result
with patch.object(Path, "exists", synchronized_exists):
results = await asyncio.gather(
storage.delete("same"),
storage.delete("same"),
return_exceptions=True,
)
print(results) # [True, FileNotFoundError(...)] in either order
asyncio.run(main())
Error Messages / Stack Traces
FileNotFoundError: [WinError 2] The system cannot find the file specified: '.../same.json'
Package Versions
agent-framework-core: source checkout of main at d7823b2
Python Version
Python 3.11.9
Additional Context
This is separate from #7748/#7757, which address concurrent save() operations and shared temporary files. This report concerns only the delete() check-then-unlink race.
AI assistance was used to audit the implementation and prepare the deterministic reproduction; the behavior was verified against the referenced source checkout.
Description
FileCheckpointStorage.delete()performs anexists()check followed byunlink(). Those operations are not atomic. If two callers delete the same checkpoint concurrently, both can observe that the file exists; one removes it and the other leaks a rawFileNotFoundError.The
CheckpointStorage.delete()contract says the method returnsTruewhen a checkpoint was deleted andFalsewhen it does not exist. A concurrent disappearance should therefore produceFalse, not an undocumented filesystem exception.Expected behavior: concurrent deletes complete with one
Trueand oneFalse, without leakingFileNotFoundError.I intend to submit a small fix that attempts
unlink()directly, catchesFileNotFoundError, and adds a deterministic concurrency regression test.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: source checkout ofmainatd7823b2Python Version
Python 3.11.9
Additional Context
This is separate from #7748/#7757, which address concurrent
save()operations and shared temporary files. This report concerns only thedelete()check-then-unlink race.AI assistance was used to audit the implementation and prepare the deterministic reproduction; the behavior was verified against the referenced source checkout.