From 5d1bdf7d3e6c59ed56aabb455d54b1a7c9c3f7fa Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Wed, 15 Apr 2026 19:21:13 -0700 Subject: [PATCH] fix: add random suffix to log filenames to prevent collision When multiple workflow runs start in the same second (common when orchestrating parallel runs), time.strftime('%Y%m%d-%H%M%S') produces identical timestamps, causing all runs to write to the same file. This corrupts event logs, checkpoint files, and CLI log files by interleaving events from different runs. Append a random 8-character hex suffix (via secrets.token_hex(4)) to filenames across all three affected locations: - EventLogSubscriber (event_log.py) - CheckpointManager.save_checkpoint (checkpoint.py) - generate_log_path (cli/run.py) Filenames change from: conductor-workflow-20260416-014816.events.jsonl to: conductor-workflow-20260416-014816-a3b7c9f1.events.jsonl Backward compatible: existing tools that glob *.events.jsonl, *.json, or *.log continue to work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/conductor/cli/run.py | 6 ++++++ src/conductor/engine/checkpoint.py | 6 ++++++ src/conductor/engine/event_log.py | 6 ++++++ tests/test_engine/test_event_log.py | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/src/conductor/cli/run.py b/src/conductor/cli/run.py index 452c8a1..32ac61e 100644 --- a/src/conductor/cli/run.py +++ b/src/conductor/cli/run.py @@ -52,7 +52,13 @@ def generate_log_path(workflow_name: str) -> Path: Returns: Path to the auto-generated log file. """ + import secrets + timestamp = time.strftime("%Y%m%d-%H%M%S") + # Append random suffix to avoid filename collisions + # when multiple runs start in the same second + suffix = secrets.token_hex(4) + timestamp = f"{timestamp}-{suffix}" path = Path(tempfile.gettempdir()) / "conductor" / f"conductor-{workflow_name}-{timestamp}.log" path.parent.mkdir(parents=True, exist_ok=True) return path diff --git a/src/conductor/engine/checkpoint.py b/src/conductor/engine/checkpoint.py index 26ef8b4..27dc2fa 100644 --- a/src/conductor/engine/checkpoint.py +++ b/src/conductor/engine/checkpoint.py @@ -167,7 +167,13 @@ def save_checkpoint( workflow_hash = "sha256:unknown" # Build checkpoint dict + import secrets + timestamp = time.strftime("%Y%m%d-%H%M%S") + # Append random suffix to avoid filename collisions + # when multiple runs start in the same second + suffix = secrets.token_hex(4) + timestamp = f"{timestamp}-{suffix}" created_at = datetime.now(UTC).isoformat() workflow_name = workflow_path.stem diff --git a/src/conductor/engine/event_log.py b/src/conductor/engine/event_log.py index 718d538..0b5432d 100644 --- a/src/conductor/engine/event_log.py +++ b/src/conductor/engine/event_log.py @@ -56,7 +56,13 @@ class EventLogSubscriber: """ def __init__(self, workflow_name: str) -> None: + import secrets + ts = time.strftime("%Y%m%d-%H%M%S") + # Append random suffix to avoid filename collisions + # when multiple runs start in the same second + suffix = secrets.token_hex(4) + ts = f"{ts}-{suffix}" self._path = ( Path(tempfile.gettempdir()) / "conductor" diff --git a/tests/test_engine/test_event_log.py b/tests/test_engine/test_event_log.py index fcf881c..aa02694 100644 --- a/tests/test_engine/test_event_log.py +++ b/tests/test_engine/test_event_log.py @@ -76,6 +76,26 @@ def test_safe_after_close(self, tmp_path, monkeypatch): sub.on_event(WorkflowEvent(type="late", timestamp=time.time(), data={})) sub.close() # Double close should be safe + def test_filenames_unique_for_simultaneous_starts(self, tmp_path, monkeypatch): + monkeypatch.setenv("TMPDIR", str(tmp_path)) + subs = [EventLogSubscriber("same-workflow") for _ in range(3)] + paths = [s.path for s in subs] + # All paths must be distinct even when created in rapid succession + assert len(set(paths)) == len(paths), f"Expected unique paths, got {paths}" + for s in subs: + s.close() + + def test_filename_contains_random_suffix(self, tmp_path, monkeypatch): + monkeypatch.setenv("TMPDIR", str(tmp_path)) + sub = EventLogSubscriber("ts-test") + # Filename should match pattern: conductor--YYYYMMDD-HHMMSS-<8 hex chars>.events.jsonl + import re + + assert re.search(r"\d{8}-\d{6}-[0-9a-f]{8}\.events\.jsonl$", sub.path.name), ( + f"Filename lacks random suffix: {sub.path.name}" + ) + sub.close() + def test_integrates_with_emitter(self, tmp_path, monkeypatch): from conductor.events import WorkflowEventEmitter