alissa-tools-github-revloop 0.31.23__tar.gz → 0.31.25__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.23/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.25}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/janitor.py +206 -12
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/loop.py +40 -13
- alissa_tools_github_revloop-0.31.25/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.31.23/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -42,6 +42,9 @@ Three properties everything here is written around:
|
|
|
42
42
|
and the grace is a day by default. A held folder is kept whatever its age.
|
|
43
43
|
Under disk PRESSURE (issue #177) the grace drops -- to two hours by default
|
|
44
44
|
-- oldest first, only while the volume is still at or above its warn line.
|
|
45
|
+
A round that names no review task yet holds only its hub's checkouts made
|
|
46
|
+
since it spawned (issue #185), and at or above `BACKSTOP_PCT` no such
|
|
47
|
+
hub-wide hold reaches a checkout older than the stale-round window.
|
|
45
48
|
* **Nothing raises past its own concern.** A folder that vanishes mid-sweep,
|
|
46
49
|
an unreadable hub, a failed removal: logged and skipped, because this runs
|
|
47
50
|
at the end of a poll pass that must complete.
|
|
@@ -104,6 +107,31 @@ SUPPORTED_TEMPLATE = "{root}/{repo}/main"
|
|
|
104
107
|
_PROBE_OWNERS = ("janitor-probe-owner-a", "janitor-probe-owner-b")
|
|
105
108
|
_PROBE_REPO = "janitor-probe-repo"
|
|
106
109
|
|
|
110
|
+
# How far before a round's spawn time a checkout may be stamped and still
|
|
111
|
+
# be held by that round when its review-task ref is unknown (issue #185):
|
|
112
|
+
# clock skew between the ledger's stamp and the filesystem's.
|
|
113
|
+
HOLD_SKEW_SECONDS = 5 * 60
|
|
114
|
+
|
|
115
|
+
# The volume used percentage at or above which the pressure backstop applies
|
|
116
|
+
# (issue #185): a hub-wide hold is void for checkouts older than the stale-
|
|
117
|
+
# round window, and one WARNING names what holds the most bytes.
|
|
118
|
+
BACKSTOP_PCT = 95.0
|
|
119
|
+
|
|
120
|
+
# How long a held folder's cached size is trusted (issue #187). The cache is
|
|
121
|
+
# keyed on the folder's newest mtime, which only sees the folder and its
|
|
122
|
+
# direct children: a write deeper down (an `npm ci` filling `node_modules`)
|
|
123
|
+
# leaves it unchanged. So an entry is re-walked once it is this old, mtime
|
|
124
|
+
# or not.
|
|
125
|
+
SIZE_CACHE_SECONDS = 60 * 60
|
|
126
|
+
|
|
127
|
+
# One held folder's cached size: (newest mtime, bytes, the sweep `now` it
|
|
128
|
+
# was walked at).
|
|
129
|
+
SizeCache = dict[Path, tuple[float, int, float]]
|
|
130
|
+
|
|
131
|
+
# The two reasons a hub-wide hold gives, so the sweep summary groups them.
|
|
132
|
+
HUB_HOLD_BOUNDED = "a live round in this hub names no review task and spawned before this checkout"
|
|
133
|
+
HUB_HOLD_UNBOUNDED = "a live round in this hub names no review task or spawn time"
|
|
134
|
+
|
|
107
135
|
Runner = Callable[..., str]
|
|
108
136
|
|
|
109
137
|
|
|
@@ -138,25 +166,43 @@ class Guard:
|
|
|
138
166
|
`review-pr-<n>` session names no repo, and holds its PR number anywhere.
|
|
139
167
|
|
|
140
168
|
A round whose review-task ref is unknown cannot be mapped to a single
|
|
141
|
-
REVIEW- checkout
|
|
169
|
+
REVIEW- checkout -- every fresh round starts that way, since the reviewer
|
|
170
|
+
creates its review task after it spawns. Such a round holds the checkouts
|
|
171
|
+
in its hub that are NEWER than its spawn time less `HOLD_SKEW_SECONDS`
|
|
172
|
+
(issue #185): a checkout older than every unknown-ref round in the hub
|
|
173
|
+
cannot belong to any of them. `all_checkouts` keeps, per hub, the
|
|
174
|
+
earliest such bound; None there means a round whose spawn time is not
|
|
175
|
+
known either, and holds every checkout in the hub, as before issue #185
|
|
176
|
+
-- the hold that, with studio reviews running near-continuously, kept 78
|
|
177
|
+
dead checkouts (~44 GB) and filled the fleet's volume on 2026-10-21.
|
|
142
178
|
"""
|
|
143
179
|
|
|
144
180
|
numbers: "dict[str | None, set[int]]" = field(default_factory=dict)
|
|
145
181
|
tasks: "dict[str | None, set[str]]" = field(default_factory=dict)
|
|
146
|
-
all_checkouts: "
|
|
182
|
+
all_checkouts: "dict[str | None, float | None]" = field(default_factory=dict)
|
|
147
183
|
|
|
148
184
|
def hold(
|
|
149
|
-
self,
|
|
185
|
+
self,
|
|
186
|
+
repo: "str | None",
|
|
187
|
+
number: int,
|
|
188
|
+
task_ref: "str | None" = None,
|
|
189
|
+
since: "float | None" = None,
|
|
150
190
|
) -> None:
|
|
151
191
|
"""Hold PR `number` of `repo` (`owner/name`, a bare name, or a
|
|
152
|
-
session-name slug; None = any hub) and the checkout its task names.
|
|
192
|
+
session-name slug; None = any hub) and the checkout its task names.
|
|
193
|
+
With no task ref, `since` (the round's spawn time, epoch seconds)
|
|
194
|
+
bounds the checkouts held in the hub; without it, every one is."""
|
|
153
195
|
slug = None if repo is None else session_repo_slug(repo.rpartition("/")[2])
|
|
154
196
|
self.numbers.setdefault(slug, set()).add(int(number))
|
|
155
197
|
ref = (task_ref or "").strip()
|
|
156
198
|
if ref:
|
|
157
199
|
self.tasks.setdefault(slug, set()).add(ref)
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
return
|
|
201
|
+
bound = None if since is None else float(since) - HOLD_SKEW_SECONDS
|
|
202
|
+
if slug in self.all_checkouts:
|
|
203
|
+
prev = self.all_checkouts[slug]
|
|
204
|
+
bound = None if prev is None or bound is None else min(prev, bound)
|
|
205
|
+
self.all_checkouts[slug] = bound
|
|
160
206
|
|
|
161
207
|
def _in(self, table: "dict[str | None, set]", slug: str, value: object) -> bool:
|
|
162
208
|
return value in table.get(slug, ()) or value in table.get(None, ())
|
|
@@ -164,8 +210,19 @@ class Guard:
|
|
|
164
210
|
def _any(self, slug: str) -> bool:
|
|
165
211
|
return bool(self.numbers.get(slug) or self.numbers.get(None))
|
|
166
212
|
|
|
167
|
-
def holds(
|
|
168
|
-
|
|
213
|
+
def holds(
|
|
214
|
+
self,
|
|
215
|
+
candidate: Candidate,
|
|
216
|
+
mtime: "float | None" = None,
|
|
217
|
+
void_before: "float | None" = None,
|
|
218
|
+
) -> "str | None":
|
|
219
|
+
"""Why `candidate` must stay, or None when nothing holds it.
|
|
220
|
+
|
|
221
|
+
`mtime` is the candidate's age stamp (`newest_mtime`); a review
|
|
222
|
+
checkout with none is held by every hub-wide hold, bounded or not.
|
|
223
|
+
`void_before` is the pressure backstop (issue #185): a hub-wide hold
|
|
224
|
+
does not reach a checkout whose stamp is older than it. A known task
|
|
225
|
+
ref's hold and a scratch folder's are never voided."""
|
|
169
226
|
slug = candidate.hub_slug
|
|
170
227
|
if candidate.kind == KIND_SCRATCH:
|
|
171
228
|
if candidate.number is None:
|
|
@@ -173,10 +230,18 @@ class Guard:
|
|
|
173
230
|
if self._in(self.numbers, slug, candidate.number):
|
|
174
231
|
return f"PR #{candidate.number} has a live session or open round"
|
|
175
232
|
return None
|
|
176
|
-
if slug in self.all_checkouts or None in self.all_checkouts:
|
|
177
|
-
return "a live round in this hub names no review task"
|
|
178
233
|
if candidate.task_ref and self._in(self.tasks, slug, candidate.task_ref):
|
|
179
234
|
return f"{candidate.task_ref} has a live session or open round"
|
|
235
|
+
if mtime is not None and void_before is not None and mtime < void_before:
|
|
236
|
+
return None
|
|
237
|
+
for key in (slug, None):
|
|
238
|
+
if key not in self.all_checkouts:
|
|
239
|
+
continue
|
|
240
|
+
bound = self.all_checkouts[key]
|
|
241
|
+
if bound is None:
|
|
242
|
+
return HUB_HOLD_UNBOUNDED
|
|
243
|
+
if mtime is None or mtime >= bound:
|
|
244
|
+
return HUB_HOLD_BOUNDED
|
|
180
245
|
return None
|
|
181
246
|
|
|
182
247
|
|
|
@@ -199,11 +264,31 @@ class SweepResult:
|
|
|
199
264
|
# in a dry run, would-be removals) only that shorter grace allowed.
|
|
200
265
|
pressure: bool = False
|
|
201
266
|
pressure_removed: int = 0
|
|
267
|
+
# Issue #185: bytes held per "<hub slug>: <reason>", so held growth is
|
|
268
|
+
# visible before it fills the disk; held folders not sized because the
|
|
269
|
+
# sweep's time budget ran out; and, when the backstop applied, how many
|
|
270
|
+
# checkouts only it released from a hub-wide hold.
|
|
271
|
+
held_bytes: "dict[str, int]" = field(default_factory=dict)
|
|
272
|
+
held_unsized: int = 0
|
|
273
|
+
# Issue #187: held folders whose size came from the cache, not a walk
|
|
274
|
+
# this sweep, so the summary can say its figure may be up to
|
|
275
|
+
# SIZE_CACHE_SECONDS old.
|
|
276
|
+
held_cached: int = 0
|
|
277
|
+
backstop: bool = False
|
|
278
|
+
backstop_released: int = 0
|
|
202
279
|
|
|
203
280
|
@property
|
|
204
281
|
def freed(self) -> int:
|
|
205
282
|
return sum(size for _, size in self.removed)
|
|
206
283
|
|
|
284
|
+
@property
|
|
285
|
+
def held_total(self) -> int:
|
|
286
|
+
return sum(self.held_bytes.values())
|
|
287
|
+
|
|
288
|
+
def top_held(self, limit: int = 5) -> "list[tuple[str, int]]":
|
|
289
|
+
"""The reasons holding the most bytes, largest first."""
|
|
290
|
+
return sorted(self.held_bytes.items(), key=lambda kv: (-kv[1], kv[0]))[:limit]
|
|
291
|
+
|
|
207
292
|
@property
|
|
208
293
|
def left(self) -> int:
|
|
209
294
|
"""Candidate folders still on disk after a live sweep: inside the
|
|
@@ -211,6 +296,34 @@ class SweepResult:
|
|
|
211
296
|
return self.fresh + len(self.held) + self.deferred + len(self.failed)
|
|
212
297
|
|
|
213
298
|
|
|
299
|
+
def held_reasons(pairs: "Iterable[tuple[str, int]]") -> str:
|
|
300
|
+
"""`; <hub>: <reason> = <size>, ...` for the log lines, or '' when none."""
|
|
301
|
+
parts = [f"{key} = {human(size)}" for key, size in pairs]
|
|
302
|
+
return ("; " + ", ".join(parts)) if parts else ""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# How many held reasons the sweep summary names before folding the rest.
|
|
306
|
+
HELD_REASONS_LOGGED = 10
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def held_summary(result: SweepResult) -> str:
|
|
310
|
+
"""The sweep summary's held-bytes tail (issue #185): every hub/reason
|
|
311
|
+
holding bytes, largest first, at most HELD_REASONS_LOGGED of them, and
|
|
312
|
+
how many held folders were sized from the cache or went unsized."""
|
|
313
|
+
tail = held_reasons(result.top_held(HELD_REASONS_LOGGED))
|
|
314
|
+
rest = len(result.held_bytes) - HELD_REASONS_LOGGED
|
|
315
|
+
if rest > 0:
|
|
316
|
+
tail += f", +{rest} more reason(s)"
|
|
317
|
+
if result.held_cached:
|
|
318
|
+
tail += (
|
|
319
|
+
f"; {result.held_cached} held size(s) cached, up to "
|
|
320
|
+
f"{SIZE_CACHE_SECONDS // 60} min old"
|
|
321
|
+
)
|
|
322
|
+
if result.held_unsized:
|
|
323
|
+
tail += f"; {result.held_unsized} held folder(s) not sized this sweep"
|
|
324
|
+
return tail
|
|
325
|
+
|
|
326
|
+
|
|
214
327
|
def layout_supported(
|
|
215
328
|
hub_for: "Callable[[str, str], Path]", workspace_root: "Path | str"
|
|
216
329
|
) -> bool:
|
|
@@ -322,9 +435,23 @@ def dir_bytes(path: "Path | str") -> int:
|
|
|
322
435
|
"""Bytes allocated under `path` (st_blocks, the figure `du` reports and a
|
|
323
436
|
removal frees), following no symlinks. Iterative, and every per-entry
|
|
324
437
|
error is skipped -- devloop's `dir_size` walk, counting blocks."""
|
|
438
|
+
return _walk_bytes(path, None) or 0
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def held_bytes_of(path: "Path | str", stop: "Callable[[], bool]") -> "int | None":
|
|
442
|
+
"""`dir_bytes` for a held folder (issue #187): `stop` is asked before
|
|
443
|
+
each directory is read, and the walk gives up -- None, never a partial
|
|
444
|
+
figure -- the moment it answers True, so one large tree cannot run the
|
|
445
|
+
sweep past its time budget."""
|
|
446
|
+
return _walk_bytes(path, stop)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _walk_bytes(path: "Path | str", stop: "Callable[[], bool] | None") -> "int | None":
|
|
325
450
|
total = 0
|
|
326
451
|
stack = [os.fspath(path)]
|
|
327
452
|
while stack:
|
|
453
|
+
if stop is not None and stop():
|
|
454
|
+
return None
|
|
328
455
|
current = stack.pop()
|
|
329
456
|
try:
|
|
330
457
|
with os.scandir(current) as entries:
|
|
@@ -434,6 +561,8 @@ def sweep(
|
|
|
434
561
|
clock: "Callable[[], float]" = time.monotonic,
|
|
435
562
|
pressure_grace_seconds: "float | None" = None,
|
|
436
563
|
under_pressure: "Callable[[], bool] | None" = None,
|
|
564
|
+
stale_round_seconds: "float | None" = None,
|
|
565
|
+
sizes: "SizeCache | None" = None,
|
|
437
566
|
) -> SweepResult:
|
|
438
567
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
439
568
|
`guard` does not hold, oldest first, then prune each hub's worktree
|
|
@@ -456,7 +585,22 @@ def sweep(
|
|
|
456
585
|
Dry-run removes nothing and prunes nothing: it logs each folder it WOULD
|
|
457
586
|
remove (with its size) and reports them in `would_remove`, under the
|
|
458
587
|
same bound -- so it reports what one live sweep would do (a dry run frees
|
|
459
|
-
nothing, so under pressure it reports every pressure-eligible folder).
|
|
588
|
+
nothing, so under pressure it reports every pressure-eligible folder).
|
|
589
|
+
|
|
590
|
+
The pressure backstop (issue #185): when `stale_round_seconds` is given
|
|
591
|
+
(the caller passes STALE_ROUND_SECONDS at or above `BACKSTOP_PCT`), a
|
|
592
|
+
hub-wide hold -- a round with no review-task ref -- is void for a
|
|
593
|
+
checkout older than that window: no live round can own a checkout older
|
|
594
|
+
than the stale window. `backstop_released` counts what only it freed.
|
|
595
|
+
|
|
596
|
+
Every held folder is sized into `held_bytes` per hub and reason, AFTER
|
|
597
|
+
the removal loop and on whatever is left of the time budget (issue
|
|
598
|
+
#187): removal never pays for observability. A walk that reaches the
|
|
599
|
+
budget mid-tree is abandoned, and every folder left unsized is counted in
|
|
600
|
+
`held_unsized`. `sizes` (a `SizeCache`), when given, caches those walks
|
|
601
|
+
across sweeps and is re-keyed to this sweep's held folders; an entry is
|
|
602
|
+
reused while the folder's mtime is unchanged AND it is younger than
|
|
603
|
+
SIZE_CACHE_SECONDS by `now`, and each reuse is counted in `held_cached`."""
|
|
460
604
|
result = SweepResult()
|
|
461
605
|
started = clock()
|
|
462
606
|
bounded = False
|
|
@@ -466,6 +610,11 @@ def sweep(
|
|
|
466
610
|
result.pressure = under_pressure()
|
|
467
611
|
if result.pressure:
|
|
468
612
|
shrunk = min(grace_seconds, pressure_grace_seconds)
|
|
613
|
+
void_before = None
|
|
614
|
+
if stale_round_seconds is not None:
|
|
615
|
+
result.backstop = True
|
|
616
|
+
void_before = now - stale_round_seconds
|
|
617
|
+
held_queue: "list[tuple[float, Candidate, str]]" = []
|
|
469
618
|
aged: "list[tuple[float, Candidate]]" = []
|
|
470
619
|
for hub in hub_list:
|
|
471
620
|
result.hubs += 1
|
|
@@ -480,10 +629,19 @@ def sweep(
|
|
|
480
629
|
if age < shrunk:
|
|
481
630
|
result.fresh += 1
|
|
482
631
|
continue
|
|
483
|
-
why = guard.holds(candidate)
|
|
632
|
+
why = guard.holds(candidate, mtime)
|
|
633
|
+
if why is not None and void_before is not None:
|
|
634
|
+
if guard.holds(candidate, mtime, void_before) is None:
|
|
635
|
+
result.backstop_released += 1
|
|
636
|
+
log.info(
|
|
637
|
+
"janitor: backstop releases %s (%.1f h old) from a hub-wide "
|
|
638
|
+
"hold — %s", candidate.path, age / 3600, why,
|
|
639
|
+
)
|
|
640
|
+
why = None
|
|
484
641
|
if why is not None:
|
|
485
642
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
486
643
|
result.held.append((candidate.path, why))
|
|
644
|
+
held_queue.append((mtime, candidate, f"{candidate.hub_slug}: {why}"))
|
|
487
645
|
continue
|
|
488
646
|
early = age < grace_seconds
|
|
489
647
|
if early and not dry_run and not (under_pressure and under_pressure()):
|
|
@@ -520,6 +678,8 @@ def sweep(
|
|
|
520
678
|
result.removed.append((candidate.path, size))
|
|
521
679
|
if early:
|
|
522
680
|
result.pressure_removed += 1
|
|
681
|
+
_size_held(held_queue, result, sizes, now=now,
|
|
682
|
+
out_of_time=lambda: clock() - started >= max_seconds)
|
|
523
683
|
if dry_run:
|
|
524
684
|
return result
|
|
525
685
|
for hub in hub_list:
|
|
@@ -529,3 +689,37 @@ def sweep(
|
|
|
529
689
|
except (CommandError, OSError) as exc:
|
|
530
690
|
log.warning("janitor: git worktree prune failed in %s (%s)", hub, exc)
|
|
531
691
|
return result
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _size_held(
|
|
695
|
+
queue: "list[tuple[float, Candidate, str]]",
|
|
696
|
+
result: SweepResult,
|
|
697
|
+
sizes: "SizeCache | None",
|
|
698
|
+
*,
|
|
699
|
+
now: float,
|
|
700
|
+
out_of_time: "Callable[[], bool]",
|
|
701
|
+
) -> None:
|
|
702
|
+
"""Size the sweep's held folders into `result.held_bytes`, after the
|
|
703
|
+
removals (issue #187). A fresh cache entry costs nothing and is always
|
|
704
|
+
used; anything else is walked only while `out_of_time()` answers False,
|
|
705
|
+
and a walk is abandoned the moment it does. `sizes` is then re-keyed to
|
|
706
|
+
what this sweep sized, so entries for gone or unsized folders drop out."""
|
|
707
|
+
seen: SizeCache = {}
|
|
708
|
+
for mtime, candidate, key in queue:
|
|
709
|
+
cached = (sizes or {}).get(candidate.path)
|
|
710
|
+
if (cached is not None and cached[0] == mtime
|
|
711
|
+
and now - cached[2] < SIZE_CACHE_SECONDS):
|
|
712
|
+
size = cached[1]
|
|
713
|
+
seen[candidate.path] = cached
|
|
714
|
+
result.held_cached += 1
|
|
715
|
+
else:
|
|
716
|
+
walked = None if out_of_time() else held_bytes_of(candidate.path, out_of_time)
|
|
717
|
+
if walked is None:
|
|
718
|
+
result.held_unsized += 1
|
|
719
|
+
continue
|
|
720
|
+
size = walked
|
|
721
|
+
seen[candidate.path] = (mtime, size, now)
|
|
722
|
+
result.held_bytes[key] = result.held_bytes.get(key, 0) + size
|
|
723
|
+
if sizes is not None:
|
|
724
|
+
sizes.clear()
|
|
725
|
+
sizes.update(seen)
|
|
@@ -2847,6 +2847,10 @@ class ReviewWatcher:
|
|
|
2847
2847
|
# under the line closes it. In memory, so a restart under pressure
|
|
2848
2848
|
# opens (and reports) a new episode.
|
|
2849
2849
|
self._janitor_pressure_since: "float | None" = None
|
|
2850
|
+
# Held folders' sizes from the last sweep, path -> (mtime, bytes,
|
|
2851
|
+
# sized at) (issues #185, #187): the held-bytes summary re-walks only
|
|
2852
|
+
# what changed or is older than janitor_mod.SIZE_CACHE_SECONDS.
|
|
2853
|
+
self._janitor_sizes: "janitor_mod.SizeCache" = {}
|
|
2850
2854
|
# The (repo, number, round) whose alive session the stale-round probe
|
|
2851
2855
|
# just killed as `wedged:first-run-dialog` (issue #136), so the
|
|
2852
2856
|
# respawn site logs its re-enqueue at INFO instead of a second
|
|
@@ -8363,8 +8367,9 @@ class ReviewWatcher:
|
|
|
8363
8367
|
stamp = time.time() if now is None else now
|
|
8364
8368
|
pct = self._volume_used_pct()
|
|
8365
8369
|
pressed = pct is not None and pct >= self.config.janitor_warn_pct
|
|
8370
|
+
backstop = pct is not None and pct >= janitor_mod.BACKSTOP_PCT
|
|
8366
8371
|
try:
|
|
8367
|
-
result = self._sweep_workspace(stamp, pressed=pressed)
|
|
8372
|
+
result = self._sweep_workspace(stamp, pressed=pressed, backstop=backstop)
|
|
8368
8373
|
except Exception as exc:
|
|
8369
8374
|
log.warning(
|
|
8370
8375
|
"janitor: sweep failed unexpectedly (%s: %s) — nothing more is "
|
|
@@ -8391,7 +8396,7 @@ class ReviewWatcher:
|
|
|
8391
8396
|
return pct is not None and pct >= self.config.janitor_warn_pct
|
|
8392
8397
|
|
|
8393
8398
|
def _sweep_workspace(
|
|
8394
|
-
self, now: float, *, pressed: bool = False
|
|
8399
|
+
self, now: float, *, pressed: bool = False, backstop: bool = False
|
|
8395
8400
|
) -> "janitor_mod.SweepResult | None":
|
|
8396
8401
|
if not janitor_mod.layout_supported(
|
|
8397
8402
|
self.config.hub_for, self.config.workspace_root
|
|
@@ -8426,7 +8431,17 @@ class ReviewWatcher:
|
|
|
8426
8431
|
self.config.janitor_pressure_grace_minutes * 60 if pressed else None
|
|
8427
8432
|
),
|
|
8428
8433
|
under_pressure=self._volume_pressed if pressed else None,
|
|
8434
|
+
stale_round_seconds=STALE_ROUND_SECONDS if backstop else None,
|
|
8435
|
+
sizes=self._janitor_sizes,
|
|
8429
8436
|
)
|
|
8437
|
+
if result.backstop:
|
|
8438
|
+
log.warning(
|
|
8439
|
+
"janitor: volume at or above %d%% — hub-wide holds are void for "
|
|
8440
|
+
"checkouts older than %d minute(s); %d released; %s held%s",
|
|
8441
|
+
int(janitor_mod.BACKSTOP_PCT), STALE_ROUND_SECONDS // 60,
|
|
8442
|
+
result.backstop_released, janitor_mod.human(result.held_total),
|
|
8443
|
+
janitor_mod.held_reasons(result.top_held()),
|
|
8444
|
+
)
|
|
8430
8445
|
if result.pressure:
|
|
8431
8446
|
log.info(
|
|
8432
8447
|
"janitor: volume at or above %d%% — the grace dropped to %d "
|
|
@@ -8439,20 +8454,22 @@ class ReviewWatcher:
|
|
|
8439
8454
|
if self.config.dry_run:
|
|
8440
8455
|
log.info(
|
|
8441
8456
|
"[dry-run] janitor: would remove %d folder(s) (%s) across %d "
|
|
8442
|
-
"hub(s); %d held by a live round, %d inside the grace, %d "
|
|
8443
|
-
"deferred to the next interval",
|
|
8457
|
+
"hub(s); %d held by a live round (%s), %d inside the grace, %d "
|
|
8458
|
+
"deferred to the next interval%s",
|
|
8444
8459
|
len(result.would_remove),
|
|
8445
8460
|
janitor_mod.human(sum(size for _, size in result.would_remove)),
|
|
8446
|
-
result.hubs, len(result.held),
|
|
8461
|
+
result.hubs, len(result.held), janitor_mod.human(result.held_total),
|
|
8462
|
+
result.fresh, result.deferred, janitor_mod.held_summary(result),
|
|
8447
8463
|
)
|
|
8448
8464
|
else:
|
|
8449
8465
|
log.info(
|
|
8450
8466
|
"janitor: removed %d folder(s), freed %s across %d hub(s); "
|
|
8451
|
-
"%d held by a live round, %d inside the grace, %d failed, %d "
|
|
8452
|
-
"deferred to the next interval",
|
|
8467
|
+
"%d held by a live round (%s), %d inside the grace, %d failed, %d "
|
|
8468
|
+
"deferred to the next interval%s",
|
|
8453
8469
|
len(result.removed), janitor_mod.human(result.freed),
|
|
8454
|
-
result.hubs, len(result.held),
|
|
8455
|
-
result.deferred,
|
|
8470
|
+
result.hubs, len(result.held), janitor_mod.human(result.held_total),
|
|
8471
|
+
result.fresh, len(result.failed), result.deferred,
|
|
8472
|
+
janitor_mod.held_summary(result),
|
|
8456
8473
|
)
|
|
8457
8474
|
return result
|
|
8458
8475
|
|
|
@@ -8465,7 +8482,11 @@ class ReviewWatcher:
|
|
|
8465
8482
|
Open rounds are the ledger's own definition (`in_flight_repos`): a
|
|
8466
8483
|
spawn younger than the stale-round window whose session was not
|
|
8467
8484
|
reaped, and a verdict post still owed. The roster is the pass's
|
|
8468
|
-
post-reap listing when the sweep produced one.
|
|
8485
|
+
post-reap listing when the sweep produced one.
|
|
8486
|
+
|
|
8487
|
+
Every hold carries its round's spawn time (issue #185), so a round
|
|
8488
|
+
whose review-task ref is not known yet holds only the checkouts in
|
|
8489
|
+
its hub made since it spawned."""
|
|
8469
8490
|
sessions = self._pass_roster
|
|
8470
8491
|
if sessions is None:
|
|
8471
8492
|
try:
|
|
@@ -8480,17 +8501,22 @@ class ReviewWatcher:
|
|
|
8480
8501
|
continue
|
|
8481
8502
|
row = self.state.find_spawn_by_session(ses.name)
|
|
8482
8503
|
if row is not None:
|
|
8483
|
-
guard.hold(
|
|
8504
|
+
guard.hold(
|
|
8505
|
+
str(row["repo"]), int(row["number"]), row["task_ref"],
|
|
8506
|
+
since=row["spawned_at"],
|
|
8507
|
+
)
|
|
8484
8508
|
else:
|
|
8485
8509
|
# No ledger row (a skill-shaped `review-pr-<n>`): the name is
|
|
8486
8510
|
# the only evidence, and a name without a repo holds its PR
|
|
8487
|
-
# number in every hub.
|
|
8511
|
+
# number in every hub. The roster reports no start time, so
|
|
8512
|
+
# its hold on the hub's checkouts stays unbounded (issue
|
|
8513
|
+
# #185); the >=95% backstop is what reaches past it.
|
|
8488
8514
|
guard.hold(ref.repo, ref.number)
|
|
8489
8515
|
for spawn_row in self.state.read_spawns(since=now - STALE_ROUND_SECONDS):
|
|
8490
8516
|
if not self.state.is_reaped(str(spawn_row["session"])):
|
|
8491
8517
|
guard.hold(
|
|
8492
8518
|
str(spawn_row["repo"]), int(spawn_row["number"]),
|
|
8493
|
-
spawn_row["task_ref"],
|
|
8519
|
+
spawn_row["task_ref"], since=spawn_row["spawned_at"],
|
|
8494
8520
|
)
|
|
8495
8521
|
for post in self.state.read_verdict_posts():
|
|
8496
8522
|
if post["posted_at"] is None and post["abandoned_at"] is None:
|
|
@@ -8500,6 +8526,7 @@ class ReviewWatcher:
|
|
|
8500
8526
|
guard.hold(
|
|
8501
8527
|
str(post["repo"]), int(post["number"]),
|
|
8502
8528
|
spawn["task_ref"] if spawn is not None else None,
|
|
8529
|
+
since=spawn["spawned_at"] if spawn is not None else None,
|
|
8503
8530
|
)
|
|
8504
8531
|
return guard
|
|
8505
8532
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.25
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.23
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.23 → alissa_tools_github_revloop-0.31.25}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|