alissa-tools-github-revloop 0.31.17__tar.gz → 0.31.19__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alissa_tools_github_revloop-0.31.17/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.19}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/janitor.py +65 -1
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/loop.py +95 -39
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/parking.py +3 -1
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/state.py +6 -3
- alissa_tools_github_revloop-0.31.19/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/server.py +3 -1
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.31.17/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -35,6 +35,12 @@ Three properties everything here is written around:
|
|
|
35
35
|
* **Nothing raises past its own concern.** A folder that vanishes mid-sweep,
|
|
36
36
|
an unreadable hub, a failed removal: logged and skipped, because this runs
|
|
37
37
|
at the end of a poll pass that must complete.
|
|
38
|
+
|
|
39
|
+
Two more bound it (issue #175). The layout is fixed: hubs are found at
|
|
40
|
+
`<root>/<repo>` with the session cwd at `<hub>/main`, so under any other
|
|
41
|
+
`hub_template` (`layout_supported` says no) the caller sweeps nothing rather
|
|
42
|
+
than mis-key the hold. And one sweep removes at most `MAX_REMOVALS` folders in
|
|
43
|
+
`MAX_SWEEP_SECONDS`; the rest wait for the next interval (`deferred`).
|
|
38
44
|
"""
|
|
39
45
|
|
|
40
46
|
from __future__ import annotations
|
|
@@ -43,6 +49,7 @@ import logging
|
|
|
43
49
|
import os
|
|
44
50
|
import re
|
|
45
51
|
import shutil
|
|
52
|
+
import time
|
|
46
53
|
from dataclasses import dataclass, field
|
|
47
54
|
from pathlib import Path
|
|
48
55
|
from typing import Callable, Iterable
|
|
@@ -70,6 +77,23 @@ SOURCE_DIR = ".source"
|
|
|
70
77
|
KIND_SCRATCH = "scratch"
|
|
71
78
|
KIND_REVIEW = "review-checkout"
|
|
72
79
|
|
|
80
|
+
# One sweep's bound (issue #175): at most this many removals, and no removal
|
|
81
|
+
# started past this many seconds into the sweep. It runs synchronously at the
|
|
82
|
+
# end of a poll pass, so a large backlog is drained over several intervals
|
|
83
|
+
# instead of stalling one pass for minutes.
|
|
84
|
+
MAX_REMOVALS = 25
|
|
85
|
+
MAX_SWEEP_SECONDS = 120.0
|
|
86
|
+
|
|
87
|
+
# The one hub layout the janitor knows: `discover_hubs` reads the root's
|
|
88
|
+
# direct children and `scan_hub` reads `<hub>/main`. Named in the caller's
|
|
89
|
+
# refusal WARNING.
|
|
90
|
+
SUPPORTED_TEMPLATE = "{root}/{repo}/main"
|
|
91
|
+
|
|
92
|
+
# What `layout_supported` probes a template with: two owners that differ, so a
|
|
93
|
+
# template that puts the owner in the path resolves to two different places.
|
|
94
|
+
_PROBE_OWNERS = ("janitor-probe-owner-a", "janitor-probe-owner-b")
|
|
95
|
+
_PROBE_REPO = "janitor-probe-repo"
|
|
96
|
+
|
|
73
97
|
Runner = Callable[..., str]
|
|
74
98
|
|
|
75
99
|
|
|
@@ -158,12 +182,34 @@ class SweepResult:
|
|
|
158
182
|
fresh: int = 0
|
|
159
183
|
failed: "list[tuple[Path, str]]" = field(default_factory=list)
|
|
160
184
|
pruned: int = 0
|
|
185
|
+
# Unheld stale candidates the per-sweep bound left for the next interval.
|
|
186
|
+
deferred: int = 0
|
|
161
187
|
|
|
162
188
|
@property
|
|
163
189
|
def freed(self) -> int:
|
|
164
190
|
return sum(size for _, size in self.removed)
|
|
165
191
|
|
|
166
192
|
|
|
193
|
+
def layout_supported(
|
|
194
|
+
hub_for: "Callable[[str, str], Path]", workspace_root: "Path | str"
|
|
195
|
+
) -> bool:
|
|
196
|
+
"""Whether `hub_for` (`Config.hub_for`) places every hub at
|
|
197
|
+
`<root>/<repo>` with the session cwd at `<hub>/main` -- the one layout
|
|
198
|
+
`discover_hubs` and `scan_hub` read, and the one in which a hub's
|
|
199
|
+
directory name is the repo slug the `Guard` keys on.
|
|
200
|
+
|
|
201
|
+
Probed with two distinct owners and one repo name: both must resolve to
|
|
202
|
+
`workspace_root / repo / "main"`. A template that cannot be formatted is
|
|
203
|
+
unsupported. Pure: nothing is read off the disk."""
|
|
204
|
+
expected = Path(os.fspath(workspace_root)).expanduser() / _PROBE_REPO / "main"
|
|
205
|
+
try:
|
|
206
|
+
return all(
|
|
207
|
+
Path(hub_for(owner, _PROBE_REPO)) == expected for owner in _PROBE_OWNERS
|
|
208
|
+
)
|
|
209
|
+
except (KeyError, IndexError, ValueError, AttributeError, TypeError):
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
167
213
|
def discover_hubs(workspace_root: "Path | str") -> "list[Path]":
|
|
168
214
|
"""Every worktree hub directly under the workspace root: a non-symlink
|
|
169
215
|
directory holding a `.source` directory. Read off the disk rather than the
|
|
@@ -323,13 +369,25 @@ def sweep(
|
|
|
323
369
|
now: float,
|
|
324
370
|
dry_run: bool = False,
|
|
325
371
|
runner: Runner = run,
|
|
372
|
+
max_removals: int = MAX_REMOVALS,
|
|
373
|
+
max_seconds: float = MAX_SWEEP_SECONDS,
|
|
374
|
+
clock: "Callable[[], float]" = time.monotonic,
|
|
326
375
|
) -> SweepResult:
|
|
327
376
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
328
377
|
`guard` does not hold, then prune each hub's worktree records.
|
|
329
378
|
|
|
379
|
+
Bounded (issue #175): before each removal, a sweep that has already
|
|
380
|
+
removed `max_removals` folders, or has run `max_seconds` by `clock`,
|
|
381
|
+
stops removing. Every unheld stale candidate past that point is counted
|
|
382
|
+
in `deferred` and left for the next interval; every hub is still scanned
|
|
383
|
+
and pruned.
|
|
384
|
+
|
|
330
385
|
Dry-run removes nothing and prunes nothing: it logs each folder it WOULD
|
|
331
|
-
remove (with its size) and reports them in `would_remove
|
|
386
|
+
remove (with its size) and reports them in `would_remove`, under the
|
|
387
|
+
same bound -- so it reports what one live sweep would do."""
|
|
332
388
|
result = SweepResult()
|
|
389
|
+
started = clock()
|
|
390
|
+
bounded = False
|
|
333
391
|
for hub in hubs:
|
|
334
392
|
result.hubs += 1
|
|
335
393
|
for candidate in scan_hub(hub):
|
|
@@ -344,6 +402,12 @@ def sweep(
|
|
|
344
402
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
345
403
|
result.held.append((candidate.path, why))
|
|
346
404
|
continue
|
|
405
|
+
if not bounded:
|
|
406
|
+
taken = len(result.would_remove if dry_run else result.removed)
|
|
407
|
+
bounded = taken >= max_removals or clock() - started >= max_seconds
|
|
408
|
+
if bounded:
|
|
409
|
+
result.deferred += 1
|
|
410
|
+
continue
|
|
347
411
|
if not _contained(candidate):
|
|
348
412
|
result.failed.append((candidate.path, "not contained in its hub"))
|
|
349
413
|
continue
|
|
@@ -2619,6 +2619,9 @@ class ReviewWatcher:
|
|
|
2619
2619
|
# When the workspace janitor last swept (monotonic), or None before the
|
|
2620
2620
|
# first sweep -- which therefore runs at the end of the first pass.
|
|
2621
2621
|
self._janitor_last: "float | None" = None
|
|
2622
|
+
# Whether the janitor has said, once for this process, that it will
|
|
2623
|
+
# not sweep under this `hub_template` (issue #175).
|
|
2624
|
+
self._janitor_layout_warned = False
|
|
2622
2625
|
# The (repo, number, round) whose alive session the stale-round probe
|
|
2623
2626
|
# just killed as `wedged:first-run-dialog` (issue #136), so the
|
|
2624
2627
|
# respawn site logs its re-enqueue at INFO instead of a second
|
|
@@ -6302,20 +6305,49 @@ class ReviewWatcher:
|
|
|
6302
6305
|
#157): claim the spawn of this round's session against the drain
|
|
6303
6306
|
flag, run `spawn` under the claim, and drop the claim however it ends.
|
|
6304
6307
|
|
|
6305
|
-
The gate sits ABOVE every side effect of the spawn -- the
|
|
6306
|
-
|
|
6307
|
-
activity line --
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6308
|
+
The gate sits ABOVE every side effect of the spawn but one -- the
|
|
6309
|
+
`alissa tmux queue add`, the census bump, the ledger row and the
|
|
6310
|
+
activity line all come after it -- and a round it stands down stays
|
|
6311
|
+
owed: a QUEUED `drained` decision writes no spawn row, burns no round
|
|
6312
|
+
number and spends no attempt, exactly like the slot gate. The claim
|
|
6313
|
+
and the flag are read and written in one immediate transaction on
|
|
6314
|
+
each side (`State.claim_spawn` / `State.set_drain`): a drain that
|
|
6312
6315
|
lands while this spawn is between its gate and its ledger write sees
|
|
6313
|
-
the claim and refuses itself
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
+
the claim and refuses itself. A ledger that cannot answer stands the
|
|
6317
|
+
spawn down too -- a spawn that cannot prove the seat is not drained
|
|
6318
|
+
does not happen.
|
|
6319
|
+
|
|
6320
|
+
The claim only counts for `parking.SPAWN_CLAIM_TTL`, and the hub add
|
|
6321
|
+
(`_ensure_hub`, a clone on a PR's first round) can outlast it. So
|
|
6322
|
+
`_spawn_claimed` re-takes the claim (`_claim_spawn` again, issue
|
|
6323
|
+
#173) between the hub add and the enqueue, which refreshes it and
|
|
6324
|
+
re-reads the flag: a drain that held while the hub was provisioned
|
|
6325
|
+
stands the round down there, the same QUEUED `drained` way. That late
|
|
6326
|
+
re-check is the one stand-down that happens AFTER a side effect --
|
|
6327
|
+
the hub is provisioned and stays -- which is harmless, because the
|
|
6328
|
+
hub is a reusable cache the round's next spawn finds in place.
|
|
6329
|
+
Together the two claims mean a park can never race a spawn, whatever
|
|
6330
|
+
the drain's freshness rule allows.
|
|
6316
6331
|
|
|
6317
6332
|
Dry-run READS the flag and claims nothing: it shares the production
|
|
6318
6333
|
state path, and a dry pass must write no durable row."""
|
|
6334
|
+
refused = self._claim_spawn(pr, round_, name)
|
|
6335
|
+
if refused is not None:
|
|
6336
|
+
return refused
|
|
6337
|
+
try:
|
|
6338
|
+
return spawn()
|
|
6339
|
+
finally:
|
|
6340
|
+
if not self.config.dry_run:
|
|
6341
|
+
self.state.release_spawn(name)
|
|
6342
|
+
|
|
6343
|
+
def _claim_spawn(
|
|
6344
|
+
self, pr: PullRequest, round_: int, name: str, *, late: bool = False,
|
|
6345
|
+
) -> "Decision | None":
|
|
6346
|
+
"""Claim (or, `late`, re-claim) the spawn of `name` against the drain
|
|
6347
|
+
flag: None when the spawn may proceed, else the QUEUED `drained`
|
|
6348
|
+
decision that stands the round down. Dry-run reads the flag and
|
|
6349
|
+
claims nothing. See `_drain_gated`."""
|
|
6350
|
+
where = " after its hub was provisioned" if late else ""
|
|
6319
6351
|
try:
|
|
6320
6352
|
if self.config.dry_run:
|
|
6321
6353
|
drain = self.state.drain()
|
|
@@ -6323,36 +6355,32 @@ class ReviewWatcher:
|
|
|
6323
6355
|
drain = self.state.claim_spawn(name)
|
|
6324
6356
|
except sqlite3.Error as exc:
|
|
6325
6357
|
log.warning(
|
|
6326
|
-
"%s round %d: could not claim the spawn against the drain flag "
|
|
6327
|
-
"(%s) — standing it down this pass", pr.slug, round_, exc,
|
|
6358
|
+
"%s round %d: could not claim the spawn against the drain flag%s "
|
|
6359
|
+
"(%s) — standing it down this pass", pr.slug, round_, where, exc,
|
|
6328
6360
|
)
|
|
6329
6361
|
return Decision(
|
|
6330
6362
|
Action.QUEUED,
|
|
6331
|
-
f"round {round_} stood down — the state ledger could not
|
|
6332
|
-
f"the seat is not drained ({exc}); retrying next poll",
|
|
6363
|
+
f"round {round_} stood down{where} — the state ledger could not "
|
|
6364
|
+
f"prove the seat is not drained ({exc}); retrying next poll",
|
|
6333
6365
|
round_,
|
|
6334
6366
|
drained=True,
|
|
6335
6367
|
)
|
|
6336
|
-
if drain is
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
)
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
return spawn()
|
|
6353
|
-
finally:
|
|
6354
|
-
if not self.config.dry_run:
|
|
6355
|
-
self.state.release_spawn(name)
|
|
6368
|
+
if drain is None:
|
|
6369
|
+
return None
|
|
6370
|
+
since = parking_mod.iso(drain["drained_at"])
|
|
6371
|
+
until = parking_mod.iso(drain["expires_at"])
|
|
6372
|
+
log.info(
|
|
6373
|
+
"%s round %d: not spawning%s — the seat is drained for parking "
|
|
6374
|
+
"since %s (until %s)", pr.slug, round_, where, since, until,
|
|
6375
|
+
)
|
|
6376
|
+
return Decision(
|
|
6377
|
+
Action.QUEUED,
|
|
6378
|
+
f"round {round_} held{where} — the seat is drained for parking since "
|
|
6379
|
+
f"{since} (until {until}: {drain['reason'] or 'no reason given'}); "
|
|
6380
|
+
f"retrying once it is undrained",
|
|
6381
|
+
round_,
|
|
6382
|
+
drained=True,
|
|
6383
|
+
)
|
|
6356
6384
|
|
|
6357
6385
|
def _spawn_claimed(
|
|
6358
6386
|
self,
|
|
@@ -6427,6 +6455,14 @@ class ReviewWatcher:
|
|
|
6427
6455
|
# provisioned -- unknown, never settled.
|
|
6428
6456
|
return Decision(Action.SKIPPED, problem, round_, owed=True)
|
|
6429
6457
|
|
|
6458
|
+
# The hub add can outlast the gate's claim (a clone on a PR's first
|
|
6459
|
+
# round): re-take it, refreshing it and re-reading the drain flag, so
|
|
6460
|
+
# a drain that landed meanwhile stands the round down here (issue
|
|
6461
|
+
# #173). The claim is still released in `_drain_gated`'s finally.
|
|
6462
|
+
refused = self._claim_spawn(pr, round_, name, late=True)
|
|
6463
|
+
if refused is not None:
|
|
6464
|
+
return refused
|
|
6465
|
+
|
|
6430
6466
|
self.alissa.enqueue_reviewer(
|
|
6431
6467
|
session=name,
|
|
6432
6468
|
directive=directive,
|
|
@@ -7913,8 +7949,9 @@ class ReviewWatcher:
|
|
|
7913
7949
|
One INFO line per sweep (folders removed and bytes freed, or what a
|
|
7914
7950
|
dry run WOULD remove), and one WARNING when the workspace volume is
|
|
7915
7951
|
at or above `janitor_warn_pct`. The disk read runs even when the
|
|
7916
|
-
roster cannot be listed
|
|
7917
|
-
|
|
7952
|
+
roster cannot be listed, or the hub layout is not the one the janitor
|
|
7953
|
+
reads, and the removal half is skipped: a full volume is worth saying
|
|
7954
|
+
whatever else went wrong."""
|
|
7918
7955
|
if not self.config.janitor_enabled:
|
|
7919
7956
|
return None
|
|
7920
7957
|
mono = time.monotonic()
|
|
@@ -7936,6 +7973,22 @@ class ReviewWatcher:
|
|
|
7936
7973
|
return result
|
|
7937
7974
|
|
|
7938
7975
|
def _sweep_workspace(self, now: float) -> "janitor_mod.SweepResult | None":
|
|
7976
|
+
if not janitor_mod.layout_supported(
|
|
7977
|
+
self.config.hub_for, self.config.workspace_root
|
|
7978
|
+
):
|
|
7979
|
+
# Fail closed (issue #175): under another layout the hubs on disk
|
|
7980
|
+
# are not keyed by the slug the Guard holds (or are not found at
|
|
7981
|
+
# all), so a sweep could remove a live round's folders. Nothing is
|
|
7982
|
+
# removed or pruned; said once per process, not once per interval.
|
|
7983
|
+
if not self._janitor_layout_warned:
|
|
7984
|
+
self._janitor_layout_warned = True
|
|
7985
|
+
log.warning(
|
|
7986
|
+
"janitor: hub_template %r is not supported — the janitor "
|
|
7987
|
+
"supports only %s, so it removes and prunes nothing in "
|
|
7988
|
+
"this process",
|
|
7989
|
+
self.config.hub_template, janitor_mod.SUPPORTED_TEMPLATE,
|
|
7990
|
+
)
|
|
7991
|
+
return None
|
|
7939
7992
|
guard = self._janitor_guard(now)
|
|
7940
7993
|
if guard is None:
|
|
7941
7994
|
log.warning(
|
|
@@ -7953,17 +8006,20 @@ class ReviewWatcher:
|
|
|
7953
8006
|
if self.config.dry_run:
|
|
7954
8007
|
log.info(
|
|
7955
8008
|
"[dry-run] janitor: would remove %d folder(s) (%s) across %d "
|
|
7956
|
-
"hub(s); %d held by a live round, %d inside the grace"
|
|
8009
|
+
"hub(s); %d held by a live round, %d inside the grace, %d "
|
|
8010
|
+
"deferred to the next interval",
|
|
7957
8011
|
len(result.would_remove),
|
|
7958
8012
|
janitor_mod.human(sum(size for _, size in result.would_remove)),
|
|
7959
|
-
result.hubs, len(result.held), result.fresh,
|
|
8013
|
+
result.hubs, len(result.held), result.fresh, result.deferred,
|
|
7960
8014
|
)
|
|
7961
8015
|
else:
|
|
7962
8016
|
log.info(
|
|
7963
8017
|
"janitor: removed %d folder(s), freed %s across %d hub(s); "
|
|
7964
|
-
"%d held by a live round, %d inside the grace, %d failed"
|
|
8018
|
+
"%d held by a live round, %d inside the grace, %d failed, %d "
|
|
8019
|
+
"deferred to the next interval",
|
|
7965
8020
|
len(result.removed), janitor_mod.human(result.freed),
|
|
7966
8021
|
result.hubs, len(result.held), result.fresh, len(result.failed),
|
|
8022
|
+
result.deferred,
|
|
7967
8023
|
)
|
|
7968
8024
|
return result
|
|
7969
8025
|
|
|
@@ -26,7 +26,9 @@ handshake):
|
|
|
26
26
|
reads every null as a refusal.
|
|
27
27
|
* **The drain.** A flag with a TTL in `state.db` (`State.set_drain`), cleared
|
|
28
28
|
when the daemon boots. While it holds, the round spawn stands down before
|
|
29
|
-
its first side effect (`ReviewWatcher._drain_gated`)
|
|
29
|
+
its first side effect (`ReviewWatcher._drain_gated`), or, when the drain
|
|
30
|
+
lands while the hub is provisioned, after the hub add and before the
|
|
31
|
+
enqueue (the post-hub re-claim).
|
|
30
32
|
"""
|
|
31
33
|
|
|
32
34
|
from __future__ import annotations
|
|
@@ -39,6 +39,8 @@ from collections.abc import Callable, Iterable
|
|
|
39
39
|
from dataclasses import dataclass
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
|
+
from .parking import SPAWN_CLAIM_TTL
|
|
43
|
+
|
|
42
44
|
log = logging.getLogger(__name__)
|
|
43
45
|
|
|
44
46
|
# Poll-snapshot retention: the newest N rows are kept, older ones pruned on
|
|
@@ -2067,11 +2069,12 @@ class State:
|
|
|
2067
2069
|
|
|
2068
2070
|
def set_drain(
|
|
2069
2071
|
self, ttl: int, reason: str, now: "float | None" = None,
|
|
2070
|
-
claim_ttl: int =
|
|
2072
|
+
claim_ttl: int = SPAWN_CLAIM_TTL,
|
|
2071
2073
|
) -> "tuple[dict, list[dict]]":
|
|
2072
2074
|
"""Raise the drain flag for `ttl` seconds and return it with the spawn
|
|
2073
|
-
claims standing at that instant (younger than `claim_ttl
|
|
2074
|
-
one is a spawn whose process
|
|
2075
|
+
claims standing at that instant (younger than `claim_ttl`, by default
|
|
2076
|
+
`parking.SPAWN_CLAIM_TTL` -- an older one is a spawn whose process
|
|
2077
|
+
died mid-way, cleared at the next boot).
|
|
2075
2078
|
ONE immediate transaction, so a spawn either claimed before this (and
|
|
2076
2079
|
is in the list) or claims after it (and sees the flag). A drain that
|
|
2077
2080
|
is already up is replaced: a re-drain restarts the TTL. Raises
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.19
|
|
@@ -332,7 +332,9 @@ class App:
|
|
|
332
332
|
cleared = self.sources.clear_drain()
|
|
333
333
|
except sqlite3.Error as exc:
|
|
334
334
|
self._audit("undrain", {"ok": False, "error": str(exc)})
|
|
335
|
-
|
|
335
|
+
# The flag's state is unknown here, and unknown is never a value
|
|
336
|
+
# (D2): the 503 already says the undrain did not land.
|
|
337
|
+
return 503, {"drained": None, "error": "state_unavailable"}
|
|
336
338
|
self._audit("undrain", {"ok": True, "cleared": cleared})
|
|
337
339
|
return 200, {"drained": False, "cleared": cleared}
|
|
338
340
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.17
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.17 → alissa_tools_github_revloop-0.31.19}/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|