Skip to content
Open
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
7 changes: 6 additions & 1 deletion graphify/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations
import json
import re
import uuid
import urllib.error
import urllib.parse
from datetime import datetime, timezone
Expand Down Expand Up @@ -299,7 +300,11 @@ def save_query_result(

now = datetime.now(timezone.utc)
slug = re.sub(r"[^\w]", "_", question.lower())[:50].strip("_")
filename = f"query_{now.strftime('%Y%m%d_%H%M%S')}_{slug}.md"
# A second-granularity stamp plus a 50-char slug is not unique: two saves in
# the same second whose questions share a prefix resolve to one path, and the
# later write_text silently replaces the earlier one (#3301). The short uuid
# makes every save its own file; the query_ prefix and .md suffix are kept.
filename = f"query_{now.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}_{slug}.md"

frontmatter_lines = [
"---",
Expand Down
12 changes: 12 additions & 0 deletions tests/test_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,15 @@ def test_no_outcome_means_no_outcome_section(tmp_path):
def test_invalid_outcome_rejected(tmp_path):
with pytest.raises(ValueError):
save_query_result("q", "a", tmp_path / "memory", outcome="great")


def test_concurrent_saves_of_the_same_question_do_not_overwrite(tmp_path):
"""Regression for #3301: a second-granularity stamp plus a 50-char slug is
not unique, so saves in the same second sharing a prefix collapsed into one
file and the earlier ones were silently lost."""
from concurrent.futures import ThreadPoolExecutor
mem = tmp_path / "memory"
with ThreadPoolExecutor(max_workers=20) as ex:
paths = list(ex.map(lambda _: save_query_result("how does auth work", "a", mem), range(20)))
assert len({p.name for p in paths}) == 20
assert len(list(mem.glob("*.md"))) == 20