fix(tracing): a temporary file of its own per writer; the docs name deployments

- record_last_run wrote every record through one `.tmp` name, so two crews
  finishing together in one project could replace each other's temporary
  file and one record was lost. Each write now goes through
  tempfile.mkstemp in the same directory, then os.replace; a failed write
  removes its own temporary file. Tests: two writers use two names and
  leave only last_run.json; a failed replace leaves nothing behind.
- tracing.mdx (en, ar, ko, pt-BR): nothing is recorded inside a
  deployment, where the platform owns the trace.

87 passed (test_last_run + test_session_trace_export); ruff and mypy clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Joao Moura
2026-09-20 10:10:59 -07:00
parent c7e50bdaa2
commit 97eceda88d
6 changed files with 39 additions and 8 deletions

View File

@@ -12,11 +12,13 @@ before crewAI would start its own tracing, so this code never runs there.
from __future__ import annotations
import contextlib
from datetime import datetime, timezone
import json
import logging
import os
from pathlib import Path
import tempfile
from typing import Any
@@ -72,9 +74,19 @@ def record_last_run(
try:
path = last_run_path()
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
os.replace(temporary, path)
# A temporary file of its own per writer: two crews finishing together in one
# project must not write through the same name, or one record is lost.
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
handle.write(json.dumps(record, indent=2) + "\n")
os.replace(temporary, path)
except OSError:
with contextlib.suppress(OSError):
os.unlink(temporary)
raise
except OSError as error: # a vanished cwd fails last_run_path() too; the run is never failed by this
logger.debug(
"Could not record the last run in %s: %s",

View File

@@ -25,7 +25,7 @@ def test_a_run_is_recorded_atomically_and_read_back(project):
amp_base_url="https://app.crewai.com",
)
assert path == project / ".crewai" / "last_run.json"
assert not path.with_name("last_run.json.tmp").exists()
assert [child.name for child in path.parent.iterdir()] == ["last_run.json"] # no temporary file left behind
written = json.loads(path.read_text(encoding="utf-8"))
assert written["execution_id"] == "6f31fe1a-20bd-4bfe-a011-25d6b9341f62"
assert written["tier"] == "ephemeral"
@@ -63,3 +63,22 @@ def test_a_missing_or_broken_record_reads_as_none(project):
def test_a_write_failure_never_raises(project, monkeypatch):
(project / ".crewai").write_text("a file where the directory should be", encoding="utf-8")
assert last_run.record_last_run(execution_id="x", tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None) is None
def test_each_writer_uses_a_temporary_file_of_its_own(project, monkeypatch):
"""Two crews finishing together in one project: neither may replace the other's temporary file."""
replaced: list[str] = []
real_replace = last_run.os.replace
monkeypatch.setattr(last_run.os, "replace", lambda src, dst: replaced.append(str(src)) or real_replace(src, dst))
for execution_id in ("first", "second"):
last_run.record_last_run(execution_id=execution_id, tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None)
assert len(replaced) == 2 and replaced[0] != replaced[1]
names = [source.rsplit("/", 1)[-1] for source in replaced]
assert all(name.startswith(".last_run.json.") and name.endswith(".tmp") for name in names)
assert [child.name for child in (project / ".crewai").iterdir()] == ["last_run.json"]
def test_a_failed_write_leaves_no_temporary_file(project, monkeypatch):
monkeypatch.setattr(last_run.os, "replace", lambda src, dst: (_ for _ in ()).throw(OSError("disk full")))
assert last_run.record_last_run(execution_id="x", tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None) is None
assert list((project / ".crewai").iterdir()) == []