250 lines
8.1 KiB
Python
250 lines
8.1 KiB
Python
"""Tests for durable invocation metadata and binary object storage."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
|
|
def test_content_addressed_store_deduplicates_and_validates(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Identical objects should share a path and be committed only once."""
|
|
commits: list[bool] = []
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
commit_callback=lambda: commits.append(True),
|
|
)
|
|
|
|
first_ref = store.put("results", b"durable-result")
|
|
second_ref = store.put("results", b"durable-result")
|
|
|
|
assert first_ref == second_ref
|
|
assert store.get(first_ref) == b"durable-result"
|
|
assert commits == [True]
|
|
|
|
|
|
def test_content_addressed_store_batches_commits_across_multiple_objects(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""One logical invocation should commit all new objects in one callback."""
|
|
commits: list[bool] = []
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
commit_callback=lambda: commits.append(True),
|
|
)
|
|
|
|
with store.batch_commits():
|
|
store.put("bridges", b"bridge-inputs")
|
|
store.put("bridges", b"bridge-output")
|
|
store.put("results", b"invocation-result")
|
|
|
|
assert commits == [True]
|
|
|
|
|
|
def test_content_addressed_store_transfers_deferred_batch_before_commit(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A streaming worker's deferred writes should join its caller's completion batch."""
|
|
commits: list[bool] = []
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
commit_callback=lambda: commits.append(True),
|
|
)
|
|
|
|
with store.batch_commits(commit_on_exit=False) as worker_batch:
|
|
store.put("bridges", b"worker-object")
|
|
with store.batch_commits() as completion_batch:
|
|
completion_batch.absorb(worker_batch)
|
|
store.put("results", b"main-object")
|
|
|
|
assert worker_batch.wrote_object is False
|
|
assert commits == [True]
|
|
|
|
|
|
def test_content_addressed_store_rejects_corruption(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Reads should fail rather than returning corrupted durable output bytes."""
|
|
store = durable_state_module.FileDurableObjectStore(tmp_path)
|
|
object_ref = store.put("results", b"expected")
|
|
(tmp_path / object_ref.object_path).write_bytes(b"corrupt")
|
|
|
|
with pytest.raises(durable_state_module.DurableStateError, match="size mismatch"):
|
|
store.get(object_ref)
|
|
|
|
|
|
def test_content_addressed_store_rejects_unsafe_reference_paths(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Persisted references must never escape the configured object root."""
|
|
store = durable_state_module.FileDurableObjectStore(tmp_path)
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path="../outside.bin",
|
|
sha256="0" * 64,
|
|
size_bytes=0,
|
|
)
|
|
|
|
with pytest.raises(durable_state_module.DurableStateError, match="Unsafe"):
|
|
store.get(object_ref)
|
|
|
|
|
|
def test_content_addressed_store_reads_missing_object_from_committed_storage(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A stale mount should use direct committed reads without materializing files."""
|
|
payload = b"arrived-from-another-container"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path=f"results/{digest[:2]}/{digest}.bin",
|
|
sha256=digest,
|
|
size_bytes=len(payload),
|
|
)
|
|
requested_paths: list[str] = []
|
|
|
|
def read_committed_object(object_path: str) -> bytes:
|
|
"""Return one object from a simulated committed-volume API."""
|
|
requested_paths.append(object_path)
|
|
return payload
|
|
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
committed_read_callback=read_committed_object,
|
|
)
|
|
|
|
assert store.get(object_ref) == payload
|
|
assert requested_paths == [object_ref.object_path]
|
|
assert not (tmp_path / object_ref.object_path).exists()
|
|
|
|
|
|
def test_content_addressed_store_recovers_truncated_mount_from_committed_storage(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A stale partial mount should fall back to authoritative committed bytes."""
|
|
payload = b"complete-durable-object"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path=f"results/{digest[:2]}/{digest}.bin",
|
|
sha256=digest,
|
|
size_bytes=len(payload),
|
|
)
|
|
mounted_path = tmp_path / object_ref.object_path
|
|
mounted_path.parent.mkdir(parents=True)
|
|
mounted_path.write_bytes(payload[:-5])
|
|
requested_paths: list[str] = []
|
|
|
|
def read_committed_object(object_path: str) -> bytes:
|
|
"""Return the complete object from a simulated committed-volume API."""
|
|
requested_paths.append(object_path)
|
|
return payload
|
|
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
committed_read_callback=read_committed_object,
|
|
)
|
|
|
|
assert store.get(object_ref) == payload
|
|
assert requested_paths == [object_ref.object_path]
|
|
|
|
|
|
def test_content_addressed_store_reports_missing_committed_object(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A missing mounted and committed object should remain a durable-state error."""
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path="results/ab/missing.bin",
|
|
sha256="a" * 64,
|
|
size_bytes=1,
|
|
)
|
|
|
|
def read_missing_object(object_path: str) -> bytes:
|
|
"""Simulate Modal reporting that the committed object is absent."""
|
|
raise FileNotFoundError(object_path)
|
|
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
committed_read_callback=read_missing_object,
|
|
)
|
|
|
|
with pytest.raises(durable_state_module.DurableStateError, match="was not found"):
|
|
store.get(object_ref)
|
|
|
|
|
|
def test_content_addressed_store_validates_direct_committed_read(
|
|
durable_state_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Direct committed reads must retain content-address integrity checks."""
|
|
expected_payload = b"expected"
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path="results/ab/expected.bin",
|
|
sha256=hashlib.sha256(expected_payload).hexdigest(),
|
|
size_bytes=len(expected_payload),
|
|
)
|
|
|
|
def read_corrupt_object(object_path: str) -> bytes:
|
|
"""Return same-sized bytes with the wrong content digest."""
|
|
del object_path
|
|
return b"corrupt!"
|
|
|
|
store = durable_state_module.FileDurableObjectStore(
|
|
tmp_path,
|
|
committed_read_callback=read_corrupt_object,
|
|
)
|
|
|
|
with pytest.raises(durable_state_module.DurableStateError, match="SHA256 mismatch"):
|
|
store.get(object_ref)
|
|
|
|
|
|
def test_remote_invocation_id_is_stable_and_input_sensitive(
|
|
durable_state_module: Any,
|
|
) -> None:
|
|
"""Retries should reuse an id while materially different inputs get a new id."""
|
|
payload = {"component_id": "component-1", "payload_kind": "subgraph"}
|
|
|
|
first_id = durable_state_module.stable_remote_invocation_id(payload, b"inputs-a")
|
|
retry_id = durable_state_module.stable_remote_invocation_id(
|
|
{**payload, "invocation_id": "ignored"},
|
|
b"inputs-a",
|
|
)
|
|
changed_id = durable_state_module.stable_remote_invocation_id(payload, b"inputs-b")
|
|
|
|
assert first_id == retry_id
|
|
assert first_id != changed_id
|
|
|
|
|
|
def test_remote_invocation_record_round_trips_object_result(
|
|
durable_state_module: Any,
|
|
) -> None:
|
|
"""Modal Dict-safe records should retain object-backed result metadata."""
|
|
object_ref = durable_state_module.DurableObjectRef(
|
|
object_path="results/ab/abcdef.bin",
|
|
sha256="a" * 64,
|
|
size_bytes=123,
|
|
)
|
|
record = durable_state_module.RemoteInvocationRecord(
|
|
invocation_id="RIV_example",
|
|
state="completed",
|
|
attempt=2,
|
|
created_at=10.0,
|
|
updated_at=20.0,
|
|
result_object=object_ref,
|
|
)
|
|
|
|
restored = durable_state_module.RemoteInvocationRecord.from_payload(
|
|
record.to_payload()
|
|
)
|
|
|
|
assert restored == record
|