fix: lock meta RMW and harden concurrent remix paths
CI / lint-and-test (pull_request) Failing after 9s
CI / lint-and-test (pull_request) Failing after 9s
Prevent worker/web clobbering of meta variants via flock and merge-by-id, make filter timeouts thread-local, harden job-id/stem sanitization, migrate TemplateResponse API, and remove the compare-bg experiment. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,3 +24,21 @@ def test_write_json_atomic_roundtrip(tmp_path: Path) -> None:
|
||||
write_json_atomic(path, payload)
|
||||
assert json.loads(path.read_text(encoding="utf-8")) == payload
|
||||
assert not path.with_suffix(path.suffix + ".tmp").exists()
|
||||
|
||||
|
||||
def test_file_lock_exclusive(tmp_path: Path) -> None:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.io_utils import file_lock
|
||||
|
||||
lock_path = tmp_path / "job.lock"
|
||||
counter = {"n": 0}
|
||||
|
||||
def bump() -> None:
|
||||
with file_lock(lock_path):
|
||||
current = counter["n"]
|
||||
counter["n"] = current + 1
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(pool.map(lambda _: bump(), range(40)))
|
||||
assert counter["n"] == 40
|
||||
|
||||
@@ -21,6 +21,16 @@ def test_validate_job_id_sanitizes_input() -> None:
|
||||
assert _validate_job_id("photo01") == "photo01"
|
||||
|
||||
|
||||
def test_validate_job_id_rejects_path_like() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_validate_job_id("a/b")
|
||||
assert exc.value.status_code == 400
|
||||
with pytest.raises(HTTPException):
|
||||
_validate_job_id("../evil")
|
||||
with pytest.raises(HTTPException):
|
||||
_validate_job_id("")
|
||||
|
||||
|
||||
def test_safe_job_file_rejects_wrong_prefix(data_dir: Path) -> None:
|
||||
stem = "photo01"
|
||||
paths = pipeline.job_paths(stem)
|
||||
|
||||
@@ -13,6 +13,7 @@ from app import pipeline
|
||||
[
|
||||
("photo.jpg", "photo"),
|
||||
("../evil", "evil"),
|
||||
("a/b", "a_b"),
|
||||
("", "photo"),
|
||||
(" spaced ", "spaced"),
|
||||
("foo@bar", "foo_bar"),
|
||||
@@ -78,3 +79,56 @@ def test_list_job_ids(data_dir: Path) -> None:
|
||||
paths = pipeline.job_paths("b")
|
||||
pipeline.write_status(paths, "processing")
|
||||
assert sorted(pipeline.list_job_ids()) == ["a", "b"]
|
||||
|
||||
|
||||
def test_parallel_manifest_merge_keeps_all_variants(data_dir: Path) -> None:
|
||||
"""Concurrent write_manifest callers must not clobber each other's variants."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
paths = pipeline.job_paths("race")
|
||||
pipeline.write_status(paths, "processing")
|
||||
pipeline.write_manifest(
|
||||
paths,
|
||||
{
|
||||
"source_stem": "race",
|
||||
"original_file": "race_original.jpg",
|
||||
"rembg_file": "race_rembg.png",
|
||||
"created_at": pipeline.now_iso(),
|
||||
"variants": [],
|
||||
},
|
||||
)
|
||||
|
||||
def write_one(i: int) -> None:
|
||||
pipeline.write_manifest(
|
||||
paths,
|
||||
{
|
||||
"variants": [{"id": f"v{i}", "file": f"race_v{i}.png"}],
|
||||
},
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(pool.map(write_one, range(20)))
|
||||
|
||||
meta = pipeline.read_meta("race")
|
||||
assert meta is not None
|
||||
ids = {v["id"] for v in meta["variants"]}
|
||||
assert ids == {f"v{i}" for i in range(20)}
|
||||
assert meta["original_file"] == "race_original.jpg"
|
||||
|
||||
|
||||
def test_filter_timeout_is_thread_local() -> None:
|
||||
import threading
|
||||
|
||||
results: dict[str, int] = {}
|
||||
|
||||
def worker(name: str, value: int) -> None:
|
||||
with pipeline.filter_timeout(value):
|
||||
results[name] = pipeline._active_filter_timeout()
|
||||
|
||||
t1 = threading.Thread(target=worker, args=("a", 11))
|
||||
t2 = threading.Thread(target=worker, args=("b", 22))
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
assert results == {"a": 11, "b": 22}
|
||||
|
||||
Reference in New Issue
Block a user