mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(utils):Keep the timestamped output-name fallbacks distinct on a coarse clock (#7071)
* fix(harness): keep timestamped output-name fallbacks distinct `output_name_candidates()` sampled `datetime.now(UTC)` inside the yield loop, so its five timestamped fallbacks were only distinct when the host clock advanced between two generator steps. On a coarse clock (Windows advances in sub-millisecond steps) all five produced the same name, and the capture tools lost retries before failing with `could not reserve an output filename`. Sample the clock once when the first fallback is reached and offset each attempt by one microsecond, so distinctness no longer depends on the host clock. Add a regression test that freezes the clock, and update the exhaustion test to hold all five distinct stamps. * docs: describe the coarse clock accurately --------- Co-authored-by: PerryLink <255665900+PerryLink@users.noreply.github.com>
This commit is contained in:
parent
338cd26dc2
commit
d04ee1d131
@ -3,13 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Cap collision-suffix probing so a saturated outputs directory cannot spin forever.
|
||||
MAX_COLLISION_PROBES = 1000
|
||||
# Retry bound for the timestamped fallback: the microseconds stamp can only repeat
|
||||
# when another writer claimed that exact name within the same microsecond.
|
||||
# Retry bound for the timestamped fallback: each attempt offsets one clock sample,
|
||||
# so the bound does not depend on the host clock advancing between yields.
|
||||
MAX_TIMESTAMP_ATTEMPTS = 5
|
||||
|
||||
|
||||
@ -18,11 +18,16 @@ def output_name_candidates(name: str) -> Iterator[str]:
|
||||
|
||||
Callers claim each candidate atomically and move on when it is taken; the
|
||||
generator never checks the filesystem, so naming cannot race the claim.
|
||||
Timestamps are taken lazily, one per fallback attempt.
|
||||
The clock is sampled lazily, once, when the first fallback is reached; each
|
||||
later fallback offsets that sample by one microsecond. A host clock whose
|
||||
tick is coarser than the microsecond stamp (for example Windows' ~15 ms
|
||||
system-time tick) therefore cannot repeat a stamp inside one burst, and
|
||||
every fallback stays a distinct candidate.
|
||||
"""
|
||||
yield name
|
||||
stem, suffix = Path(name).stem, Path(name).suffix
|
||||
for index in range(1, MAX_COLLISION_PROBES + 1):
|
||||
yield f"{stem}-{index}{suffix}"
|
||||
for _ in range(MAX_TIMESTAMP_ATTEMPTS):
|
||||
yield f"{stem}-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')}{suffix}"
|
||||
stamp = datetime.now(UTC)
|
||||
for attempt in range(MAX_TIMESTAMP_ATTEMPTS):
|
||||
yield f"{stem}-{(stamp + timedelta(microseconds=attempt)).strftime('%Y%m%d-%H%M%S-%f')}{suffix}"
|
||||
|
||||
@ -188,7 +188,13 @@ def test_exhausted_timestamp_claims_refuse_to_overwrite(outputs, monkeypatch):
|
||||
|
||||
monkeypatch.setattr(output_names, "MAX_COLLISION_PROBES", 1)
|
||||
monkeypatch.setattr(output_names, "datetime", _FrozenClock)
|
||||
held = ["report.png", "report-1.png", "report-20261014-120000-123456.png"]
|
||||
# The timestamped fallbacks offset one frozen clock sample, so they are all
|
||||
# distinct names; exhausting them means holding every one of them.
|
||||
held = [
|
||||
"report.png",
|
||||
"report-1.png",
|
||||
*(f"report-20261014-120000-{123456 + offset:06d}.png" for offset in range(output_names.MAX_TIMESTAMP_ATTEMPTS)),
|
||||
]
|
||||
for name in held:
|
||||
(outputs / name).write_bytes(b"earlier")
|
||||
|
||||
|
||||
@ -41,6 +41,28 @@ def test_candidates_keep_the_name_then_number_then_timestamp(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
class _FrozenClock(datetime):
|
||||
"""A coarse host clock: every sample inside one burst returns the same instant."""
|
||||
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime(2026, 10, 18, 12, 0, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def test_timestamped_fallbacks_stay_distinct_on_a_coarse_clock(monkeypatch):
|
||||
# A host clock whose tick is coarser than the microsecond stamp (Windows'
|
||||
# system-time tick is ~15 ms) hands back one value for all five samples in a
|
||||
# burst. The fallback bound must not depend on the clock advancing between
|
||||
# yields.
|
||||
monkeypatch.setattr(output_names, "MAX_COLLISION_PROBES", 1)
|
||||
monkeypatch.setattr(output_names, "datetime", _FrozenClock)
|
||||
|
||||
names = list(output_names.output_name_candidates("report.png"))
|
||||
|
||||
assert len(names) == 1 + 1 + output_names.MAX_TIMESTAMP_ATTEMPTS
|
||||
assert len(set(names)) == len(names)
|
||||
|
||||
|
||||
def test_candidates_read_the_clock_only_when_a_fallback_is_reached(monkeypatch):
|
||||
monkeypatch.setattr(_StepClock, "ticks", 0)
|
||||
monkeypatch.setattr(output_names, "datetime", _StepClock)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user