Skip to content

Commit b5557cb

Browse files
committed
docs: keep pages that finish while a translate run is stopping
When the API rejects the credentials part-way through a parallel run, stop starting pages but still collect the ones already in flight and write those that succeed, then report usage and exit 2. Previously a page that had already finished in the same batch could be dropped depending on set iteration order. The test fake's call counter is now guarded by a lock.
1 parent d89a3b0 commit b5557cb

2 files changed

Lines changed: 38 additions & 21 deletions

File tree

scripts/docs/translations.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -988,18 +988,23 @@ def produce(inputs: Inputs, job: Job) -> tuple[str | PageError, Usage]:
988988
usage, failed = Usage(), False
989989
queue = deque(work)
990990
running: dict[Future[tuple[str | PageError, Usage]], tuple[Language, Job]] = {}
991+
stopped: ConfigError | None = None # rejected credentials: start nothing more, keep what still lands
991992
pool = ThreadPoolExecutor(max_workers=args.jobs)
992993
try:
993994
# The window is refilled here rather than handing the pool every page up front, so
994-
# once a page raises (rejected credentials) no further page starts; the ones already
995-
# in flight run out unreported, and the process exits when they have.
996-
while queue or running:
997-
while queue and len(running) < args.jobs:
995+
# that once the credentials are rejected no further page starts; pages already in
996+
# flight are still collected, and written if they made it, before the run stops.
997+
while (queue and stopped is None) or running:
998+
while queue and stopped is None and len(running) < args.jobs:
998999
inputs, job = queue.popleft()
9991000
running[pool.submit(produce, inputs, job)] = (inputs.language, job)
10001001
for future in wait(running, return_when=FIRST_COMPLETED).done:
10011002
language, job = running.pop(future)
1002-
result, spent = future.result()
1003+
try:
1004+
result, spent = future.result()
1005+
except ConfigError as exc:
1006+
stopped = stopped or exc
1007+
continue
10031008
usage.add(spent)
10041009
page = job.state.page
10051010
if isinstance(result, PageError):
@@ -1019,6 +1024,8 @@ def produce(inputs: Inputs, job: Job) -> tuple[str | PageError, Usage]:
10191024
finally:
10201025
pool.shutdown(wait=False)
10211026
print(f"usage: {usage}")
1027+
if stopped is not None:
1028+
raise stopped
10221029
return 1 if failed else 0
10231030

10241031

tests/docs/test_translations.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,10 @@ def test_rejected_credentials_stop_the_run_with_exit_2(tmp_path: Path, capsys: p
900900
assert (code, out, err) == snapshot(
901901
(
902902
2,
903-
"translating 4 pages (ja), 1 at a time, with test-model\n",
903+
"""\
904+
translating 4 pages (ja), 1 at a time, with test-model
905+
usage: 0 input / 0 output / 0 cache-write / 0 cache-read tokens
906+
""",
904907
"translations: the API rejected the credentials: invalid x-api-key\n",
905908
)
906909
)
@@ -957,30 +960,37 @@ def complete(self, *, model: str, system: str, messages: Sequence[t.Message], ma
957960
assert run(capsys, root, "status") == (0, "ja (日本語): 0 missing, 0 outdated, 4 current, 0 removable\n", "")
958961

959962

960-
def test_rejected_credentials_with_pages_in_flight_start_no_further_page(
963+
def test_rejected_credentials_with_pages_in_flight_start_no_further_page_but_a_finished_one_is_kept(
961964
tmp_path: Path, capsys: pytest.CaptureFixture[str]
962965
) -> None:
963-
"""Tool-defined: with `--jobs 2`, when the API rejects the credentials (here for both pages in flight,
964-
once both are waiting) the run stops with exit 2 and the two remaining pages are never requested."""
966+
"""Tool-defined: with `--jobs 2`, when the API rejects the credentials for one page while another is in
967+
flight (here both are waiting before either is answered), the other page still lands and is written,
968+
the two remaining pages are never requested, usage is reported, and the run stops with exit 2."""
965969
root = make_repo(tmp_path)
966970

967-
class Rejected:
968-
both, calls = threading.Barrier(2, timeout=5), 0
971+
class RejectsHome:
972+
def __init__(self) -> None:
973+
self.both, self.lock, self.calls = threading.Barrier(2, timeout=5), threading.Lock(), 0
969974

970975
def complete(self, *, model: str, system: str, messages: Sequence[t.Message], max_tokens: int) -> t.Completion:
971-
type(self).calls += 1
976+
with self.lock:
977+
self.calls += 1
972978
self.both.wait()
973-
raise t.ConfigError("the API rejected the credentials: invalid bearer token")
979+
if "# Home" in messages[0].content:
980+
raise t.ConfigError("the API rejected the credentials: invalid bearer token")
981+
return t.Completion(TOOLS_JA, t.Usage(10, 4, 0, 9))
974982

975-
code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=Rejected())
983+
fake = RejectsHome()
976984

977-
assert (code, Rejected.calls, err) == (
978-
2,
979-
2,
980-
"translations: the API rejected the credentials: invalid bearer token\n",
981-
)
982-
assert out == "translating 4 pages (ja), 2 at a time, with test-model\n"
983-
assert not (root / "i18n" / "ja" / "pages").exists()
985+
code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=fake)
986+
987+
assert (code, fake.calls, err) == (2, 2, "translations: the API rejected the credentials: invalid bearer token\n")
988+
assert out == snapshot("""\
989+
translating 4 pages (ja), 2 at a time, with test-model
990+
ja: translated tools.md (3 of 3 sections)
991+
usage: 10 input / 4 output / 0 cache-write / 9 cache-read tokens
992+
""")
993+
assert sorted(path.name for path in (root / "i18n" / "ja" / "pages").glob("*.md")) == ["tools.md"]
984994

985995

986996
def test_translate_without_lang_works_through_every_language_with_that_languages_own_prompt(

0 commit comments

Comments
 (0)