alissa-tools-github-revloop 0.31.24__tar.gz → 0.31.26__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.26}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/janitor.py +150 -23
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/loop.py +4 -3
- alissa_tools_github_revloop-0.31.26/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26/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.26}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.26}/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.26}/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.26}/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.26}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -117,6 +117,34 @@ 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
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class HeldWalk:
|
|
130
|
+
"""A held folder's size walk that ran out of budget (issue #189), kept
|
|
131
|
+
so the next sweep resumes it instead of starting over: the folder's
|
|
132
|
+
newest mtime when the walk began, the sweep `now` it began at, the bytes
|
|
133
|
+
counted so far, and the directories still to read. A resumed walk's
|
|
134
|
+
total may count a directory removed since it was read; it never counts a
|
|
135
|
+
directory twice."""
|
|
136
|
+
|
|
137
|
+
mtime: float
|
|
138
|
+
started: float
|
|
139
|
+
total: int
|
|
140
|
+
stack: "list[str]"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# One held folder's cached size, by path: either a finished walk, as
|
|
144
|
+
# (newest mtime, bytes, the sweep `now` the walk began at), or a `HeldWalk`
|
|
145
|
+
# that the time budget cut short (issue #189).
|
|
146
|
+
SizeCache = dict[Path, "tuple[float, int, float] | HeldWalk"]
|
|
147
|
+
|
|
120
148
|
# The two reasons a hub-wide hold gives, so the sweep summary groups them.
|
|
121
149
|
HUB_HOLD_BOUNDED = "a live round in this hub names no review task and spawned before this checkout"
|
|
122
150
|
HUB_HOLD_UNBOUNDED = "a live round in this hub names no review task or spawn time"
|
|
@@ -259,6 +287,13 @@ class SweepResult:
|
|
|
259
287
|
# checkouts only it released from a hub-wide hold.
|
|
260
288
|
held_bytes: "dict[str, int]" = field(default_factory=dict)
|
|
261
289
|
held_unsized: int = 0
|
|
290
|
+
# Issue #187: held folders whose size came from the cache, not a walk
|
|
291
|
+
# this sweep, so the summary can say its figure may be up to
|
|
292
|
+
# SIZE_CACHE_SECONDS old.
|
|
293
|
+
held_cached: int = 0
|
|
294
|
+
# Issue #189: unsized held folders whose walk is kept to resume next
|
|
295
|
+
# sweep (a subset of `held_unsized`).
|
|
296
|
+
held_resuming: int = 0
|
|
262
297
|
backstop: bool = False
|
|
263
298
|
backstop_released: int = 0
|
|
264
299
|
|
|
@@ -294,13 +329,20 @@ HELD_REASONS_LOGGED = 10
|
|
|
294
329
|
def held_summary(result: SweepResult) -> str:
|
|
295
330
|
"""The sweep summary's held-bytes tail (issue #185): every hub/reason
|
|
296
331
|
holding bytes, largest first, at most HELD_REASONS_LOGGED of them, and
|
|
297
|
-
how many held folders went unsized."""
|
|
332
|
+
how many held folders were sized from the cache or went unsized."""
|
|
298
333
|
tail = held_reasons(result.top_held(HELD_REASONS_LOGGED))
|
|
299
334
|
rest = len(result.held_bytes) - HELD_REASONS_LOGGED
|
|
300
335
|
if rest > 0:
|
|
301
336
|
tail += f", +{rest} more reason(s)"
|
|
337
|
+
if result.held_cached:
|
|
338
|
+
tail += (
|
|
339
|
+
f"; {result.held_cached} held size(s) cached, up to "
|
|
340
|
+
f"{SIZE_CACHE_SECONDS // 60} min old"
|
|
341
|
+
)
|
|
302
342
|
if result.held_unsized:
|
|
303
343
|
tail += f"; {result.held_unsized} held folder(s) not sized this sweep"
|
|
344
|
+
if result.held_resuming:
|
|
345
|
+
tail += f" ({result.held_resuming} walk(s) resume next sweep)"
|
|
304
346
|
return tail
|
|
305
347
|
|
|
306
348
|
|
|
@@ -415,9 +457,35 @@ def dir_bytes(path: "Path | str") -> int:
|
|
|
415
457
|
"""Bytes allocated under `path` (st_blocks, the figure `du` reports and a
|
|
416
458
|
removal frees), following no symlinks. Iterative, and every per-entry
|
|
417
459
|
error is skipped -- devloop's `dir_size` walk, counting blocks."""
|
|
418
|
-
|
|
419
|
-
|
|
460
|
+
return _walk_bytes(path, None) or 0
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def held_bytes_of(
|
|
464
|
+
path: "Path | str",
|
|
465
|
+
stop: "Callable[[], bool]",
|
|
466
|
+
progress: "HeldWalk | None" = None,
|
|
467
|
+
) -> "int | None":
|
|
468
|
+
"""`dir_bytes` for a held folder (issue #187): `stop` is asked before
|
|
469
|
+
each directory is read, and the walk gives up -- None, never a partial
|
|
470
|
+
figure -- the moment it answers True, so one large tree cannot run the
|
|
471
|
+
sweep past its time budget. With `progress` (issue #189) the walk starts
|
|
472
|
+
from its `total` and `stack` rather than from `path`, and an abandoned
|
|
473
|
+
walk leaves its place there, so a later call carries on from it."""
|
|
474
|
+
return _walk_bytes(path, stop, progress)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _walk_bytes(
|
|
478
|
+
path: "Path | str",
|
|
479
|
+
stop: "Callable[[], bool] | None",
|
|
480
|
+
progress: "HeldWalk | None" = None,
|
|
481
|
+
) -> "int | None":
|
|
482
|
+
total = progress.total if progress is not None else 0
|
|
483
|
+
stack = progress.stack if progress is not None else [os.fspath(path)]
|
|
420
484
|
while stack:
|
|
485
|
+
if stop is not None and stop():
|
|
486
|
+
if progress is not None:
|
|
487
|
+
progress.total = total # `stack` is progress.stack, in place
|
|
488
|
+
return None
|
|
421
489
|
current = stack.pop()
|
|
422
490
|
try:
|
|
423
491
|
with os.scandir(current) as entries:
|
|
@@ -528,7 +596,7 @@ def sweep(
|
|
|
528
596
|
pressure_grace_seconds: "float | None" = None,
|
|
529
597
|
under_pressure: "Callable[[], bool] | None" = None,
|
|
530
598
|
stale_round_seconds: "float | None" = None,
|
|
531
|
-
sizes: "
|
|
599
|
+
sizes: "SizeCache | None" = None,
|
|
532
600
|
) -> SweepResult:
|
|
533
601
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
534
602
|
`guard` does not hold, oldest first, then prune each hub's worktree
|
|
@@ -559,10 +627,18 @@ def sweep(
|
|
|
559
627
|
checkout older than that window: no live round can own a checkout older
|
|
560
628
|
than the stale window. `backstop_released` counts what only it freed.
|
|
561
629
|
|
|
562
|
-
Every held folder is sized into `held_bytes` per hub and reason
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
budget
|
|
630
|
+
Every held folder is sized into `held_bytes` per hub and reason, AFTER
|
|
631
|
+
the removal loop and on whatever is left of the time budget (issue
|
|
632
|
+
#187): removal never pays for observability. A walk that reaches the
|
|
633
|
+
budget mid-tree is abandoned, and every folder left unsized is counted in
|
|
634
|
+
`held_unsized`. `sizes` (a `SizeCache`), when given, caches those walks
|
|
635
|
+
across sweeps and is re-keyed to this sweep's held folders; an entry is
|
|
636
|
+
reused while the folder's mtime is unchanged AND it is younger than
|
|
637
|
+
SIZE_CACHE_SECONDS by `now`, and each reuse is counted in `held_cached`.
|
|
638
|
+
An abandoned walk is kept there too (issue #189): the next sweep sizes
|
|
639
|
+
every other held folder first and then resumes it where it stopped, so
|
|
640
|
+
one large tree neither starves the folders behind it nor restarts from
|
|
641
|
+
scratch every sweep."""
|
|
566
642
|
result = SweepResult()
|
|
567
643
|
started = clock()
|
|
568
644
|
bounded = False
|
|
@@ -576,7 +652,7 @@ def sweep(
|
|
|
576
652
|
if stale_round_seconds is not None:
|
|
577
653
|
result.backstop = True
|
|
578
654
|
void_before = now - stale_round_seconds
|
|
579
|
-
|
|
655
|
+
held_queue: "list[tuple[float, Candidate, str]]" = []
|
|
580
656
|
aged: "list[tuple[float, Candidate]]" = []
|
|
581
657
|
for hub in hub_list:
|
|
582
658
|
result.hubs += 1
|
|
@@ -603,17 +679,7 @@ def sweep(
|
|
|
603
679
|
if why is not None:
|
|
604
680
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
605
681
|
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
|
|
682
|
+
held_queue.append((mtime, candidate, f"{candidate.hub_slug}: {why}"))
|
|
617
683
|
continue
|
|
618
684
|
early = age < grace_seconds
|
|
619
685
|
if early and not dry_run and not (under_pressure and under_pressure()):
|
|
@@ -650,9 +716,8 @@ def sweep(
|
|
|
650
716
|
result.removed.append((candidate.path, size))
|
|
651
717
|
if early:
|
|
652
718
|
result.pressure_removed += 1
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
sizes.update(seen_sizes)
|
|
719
|
+
_size_held(held_queue, result, sizes, now=now,
|
|
720
|
+
out_of_time=lambda: clock() - started >= max_seconds)
|
|
656
721
|
if dry_run:
|
|
657
722
|
return result
|
|
658
723
|
for hub in hub_list:
|
|
@@ -662,3 +727,65 @@ def sweep(
|
|
|
662
727
|
except (CommandError, OSError) as exc:
|
|
663
728
|
log.warning("janitor: git worktree prune failed in %s (%s)", hub, exc)
|
|
664
729
|
return result
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _size_held(
|
|
733
|
+
queue: "list[tuple[float, Candidate, str]]",
|
|
734
|
+
result: SweepResult,
|
|
735
|
+
sizes: "SizeCache | None",
|
|
736
|
+
*,
|
|
737
|
+
now: float,
|
|
738
|
+
out_of_time: "Callable[[], bool]",
|
|
739
|
+
) -> None:
|
|
740
|
+
"""Size the sweep's held folders into `result.held_bytes`, after the
|
|
741
|
+
removals (issue #187). A fresh cache entry costs nothing and is always
|
|
742
|
+
used; anything else is walked only while `out_of_time()` answers False,
|
|
743
|
+
and a walk is abandoned the moment it does.
|
|
744
|
+
|
|
745
|
+
An abandoned walk is kept in `sizes` as a `HeldWalk` (issue #189). Next
|
|
746
|
+
sweep its folder goes to the back of the queue, behind every folder
|
|
747
|
+
whose walk was not cut short, and its walk resumes from where it stopped
|
|
748
|
+
-- from scratch if the folder's mtime moved. So a tree too big for one
|
|
749
|
+
sweep's leftover budget no longer leaves the folders behind it unsized
|
|
750
|
+
on every sweep, and still gets its figure after a few. A finished
|
|
751
|
+
resumed walk is cached as of the sweep it began in, so the hourly
|
|
752
|
+
re-walk counts from the oldest directory it read.
|
|
753
|
+
|
|
754
|
+
`sizes` is then re-keyed to this sweep's held folders: entries for gone
|
|
755
|
+
folders drop out, and so do unsized folders whose walk never began."""
|
|
756
|
+
previous = sizes or {}
|
|
757
|
+
|
|
758
|
+
def cut_short(item: "tuple[float, Candidate, str]") -> bool:
|
|
759
|
+
return isinstance(previous.get(item[1].path), HeldWalk)
|
|
760
|
+
|
|
761
|
+
seen: SizeCache = {}
|
|
762
|
+
# Stable: oldest first within each group, as the sweep queued them.
|
|
763
|
+
for mtime, candidate, key in sorted(queue, key=cut_short):
|
|
764
|
+
cached = previous.get(candidate.path)
|
|
765
|
+
if (isinstance(cached, tuple) and cached[0] == mtime
|
|
766
|
+
and now - cached[2] < SIZE_CACHE_SECONDS):
|
|
767
|
+
size = cached[1]
|
|
768
|
+
seen[candidate.path] = cached
|
|
769
|
+
result.held_cached += 1
|
|
770
|
+
result.held_bytes[key] = result.held_bytes.get(key, 0) + size
|
|
771
|
+
continue
|
|
772
|
+
resumable = isinstance(cached, HeldWalk)
|
|
773
|
+
if isinstance(cached, HeldWalk) and cached.mtime == mtime:
|
|
774
|
+
progress = cached
|
|
775
|
+
else:
|
|
776
|
+
progress = HeldWalk(mtime, now, 0, [os.fspath(candidate.path)])
|
|
777
|
+
walked = None
|
|
778
|
+
if not out_of_time():
|
|
779
|
+
walked = held_bytes_of(candidate.path, out_of_time, progress)
|
|
780
|
+
resumable = resumable or walked is None
|
|
781
|
+
if walked is None:
|
|
782
|
+
result.held_unsized += 1
|
|
783
|
+
if resumable:
|
|
784
|
+
seen[candidate.path] = progress
|
|
785
|
+
result.held_resuming += 1
|
|
786
|
+
continue
|
|
787
|
+
seen[candidate.path] = (mtime, walked, progress.started)
|
|
788
|
+
result.held_bytes[key] = result.held_bytes.get(key, 0) + walked
|
|
789
|
+
if sizes is not None:
|
|
790
|
+
sizes.clear()
|
|
791
|
+
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.26
|
|
@@ -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.26}/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
|