alissa-tools-github-revloop 0.31.24__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.24/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.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/janitor.py +82 -21
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/loop.py +4 -3
- alissa_tools_github_revloop-0.31.25/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.24 → 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.24/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → 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.24 → 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.24 → 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.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -117,6 +117,17 @@ HOLD_SKEW_SECONDS = 5 * 60
|
|
|
117
117
|
# round window, and one WARNING names what holds the most bytes.
|
|
118
118
|
BACKSTOP_PCT = 95.0
|
|
119
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
|
+
|
|
120
131
|
# The two reasons a hub-wide hold gives, so the sweep summary groups them.
|
|
121
132
|
HUB_HOLD_BOUNDED = "a live round in this hub names no review task and spawned before this checkout"
|
|
122
133
|
HUB_HOLD_UNBOUNDED = "a live round in this hub names no review task or spawn time"
|
|
@@ -259,6 +270,10 @@ class SweepResult:
|
|
|
259
270
|
# checkouts only it released from a hub-wide hold.
|
|
260
271
|
held_bytes: "dict[str, int]" = field(default_factory=dict)
|
|
261
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
|
|
262
277
|
backstop: bool = False
|
|
263
278
|
backstop_released: int = 0
|
|
264
279
|
|
|
@@ -294,11 +309,16 @@ HELD_REASONS_LOGGED = 10
|
|
|
294
309
|
def held_summary(result: SweepResult) -> str:
|
|
295
310
|
"""The sweep summary's held-bytes tail (issue #185): every hub/reason
|
|
296
311
|
holding bytes, largest first, at most HELD_REASONS_LOGGED of them, and
|
|
297
|
-
how many held folders went unsized."""
|
|
312
|
+
how many held folders were sized from the cache or went unsized."""
|
|
298
313
|
tail = held_reasons(result.top_held(HELD_REASONS_LOGGED))
|
|
299
314
|
rest = len(result.held_bytes) - HELD_REASONS_LOGGED
|
|
300
315
|
if rest > 0:
|
|
301
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
|
+
)
|
|
302
322
|
if result.held_unsized:
|
|
303
323
|
tail += f"; {result.held_unsized} held folder(s) not sized this sweep"
|
|
304
324
|
return tail
|
|
@@ -415,9 +435,23 @@ def dir_bytes(path: "Path | str") -> int:
|
|
|
415
435
|
"""Bytes allocated under `path` (st_blocks, the figure `du` reports and a
|
|
416
436
|
removal frees), following no symlinks. Iterative, and every per-entry
|
|
417
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":
|
|
418
450
|
total = 0
|
|
419
451
|
stack = [os.fspath(path)]
|
|
420
452
|
while stack:
|
|
453
|
+
if stop is not None and stop():
|
|
454
|
+
return None
|
|
421
455
|
current = stack.pop()
|
|
422
456
|
try:
|
|
423
457
|
with os.scandir(current) as entries:
|
|
@@ -528,7 +562,7 @@ def sweep(
|
|
|
528
562
|
pressure_grace_seconds: "float | None" = None,
|
|
529
563
|
under_pressure: "Callable[[], bool] | None" = None,
|
|
530
564
|
stale_round_seconds: "float | None" = None,
|
|
531
|
-
sizes: "
|
|
565
|
+
sizes: "SizeCache | None" = None,
|
|
532
566
|
) -> SweepResult:
|
|
533
567
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
534
568
|
`guard` does not hold, oldest first, then prune each hub's worktree
|
|
@@ -559,10 +593,14 @@ def sweep(
|
|
|
559
593
|
checkout older than that window: no live round can own a checkout older
|
|
560
594
|
than the stale window. `backstop_released` counts what only it freed.
|
|
561
595
|
|
|
562
|
-
Every held folder is sized into `held_bytes` per hub and reason
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
budget
|
|
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`."""
|
|
566
604
|
result = SweepResult()
|
|
567
605
|
started = clock()
|
|
568
606
|
bounded = False
|
|
@@ -576,7 +614,7 @@ def sweep(
|
|
|
576
614
|
if stale_round_seconds is not None:
|
|
577
615
|
result.backstop = True
|
|
578
616
|
void_before = now - stale_round_seconds
|
|
579
|
-
|
|
617
|
+
held_queue: "list[tuple[float, Candidate, str]]" = []
|
|
580
618
|
aged: "list[tuple[float, Candidate]]" = []
|
|
581
619
|
for hub in hub_list:
|
|
582
620
|
result.hubs += 1
|
|
@@ -603,17 +641,7 @@ def sweep(
|
|
|
603
641
|
if why is not None:
|
|
604
642
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
605
643
|
result.held.append((candidate.path, why))
|
|
606
|
-
|
|
607
|
-
cached = (sizes or {}).get(candidate.path)
|
|
608
|
-
if cached is not None and cached[0] == mtime:
|
|
609
|
-
size = cached[1]
|
|
610
|
-
elif clock() - started >= max_seconds:
|
|
611
|
-
result.held_unsized += 1
|
|
612
|
-
continue
|
|
613
|
-
else:
|
|
614
|
-
size = dir_bytes(candidate.path)
|
|
615
|
-
seen_sizes[candidate.path] = (mtime, size)
|
|
616
|
-
result.held_bytes[key] = result.held_bytes.get(key, 0) + size
|
|
644
|
+
held_queue.append((mtime, candidate, f"{candidate.hub_slug}: {why}"))
|
|
617
645
|
continue
|
|
618
646
|
early = age < grace_seconds
|
|
619
647
|
if early and not dry_run and not (under_pressure and under_pressure()):
|
|
@@ -650,9 +678,8 @@ def sweep(
|
|
|
650
678
|
result.removed.append((candidate.path, size))
|
|
651
679
|
if early:
|
|
652
680
|
result.pressure_removed += 1
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
sizes.update(seen_sizes)
|
|
681
|
+
_size_held(held_queue, result, sizes, now=now,
|
|
682
|
+
out_of_time=lambda: clock() - started >= max_seconds)
|
|
656
683
|
if dry_run:
|
|
657
684
|
return result
|
|
658
685
|
for hub in hub_list:
|
|
@@ -662,3 +689,37 @@ def sweep(
|
|
|
662
689
|
except (CommandError, OSError) as exc:
|
|
663
690
|
log.warning("janitor: git worktree prune failed in %s (%s)", hub, exc)
|
|
664
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,9 +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
|
-
# (
|
|
2852
|
-
|
|
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" = {}
|
|
2853
2854
|
# The (repo, number, round) whose alive session the stale-round probe
|
|
2854
2855
|
# just killed as `wedged:first-run-dialog` (issue #136), so the
|
|
2855
2856
|
# respawn site logs its re-enqueue at INFO instead of a second
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.25
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.24
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.24 → 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
|