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