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.
Files changed (40) hide show
  1. {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
  2. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/janitor.py +82 -21
  3. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/loop.py +4 -3
  4. alissa_tools_github_revloop-0.31.25/src/main/alissa/tools/github/revloop/version +1 -0
  5. {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
  6. alissa_tools_github_revloop-0.31.24/src/main/alissa/tools/github/revloop/version +0 -1
  7. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/LICENSE +0 -0
  8. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/MANIFEST.in +0 -0
  9. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/NOTICE +0 -0
  10. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/README.md +0 -0
  11. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/requirements.txt +0 -0
  12. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/setup.cfg +0 -0
  13. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/setup.py +0 -0
  14. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  15. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  16. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  17. {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
  18. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  19. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/config.py +0 -0
  20. {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
  21. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  22. {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
  23. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/parking.py +0 -0
  24. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  25. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
  26. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  27. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/state.py +0 -0
  28. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/trust.py +0 -0
  29. {alissa_tools_github_revloop-0.31.24 → alissa_tools_github_revloop-0.31.25}/src/main/alissa/tools/github/revloop/version.py +0 -0
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.24
3
+ Version: 0.31.25
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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: "dict[Path, tuple[float, int]] | None" = None,
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. `sizes`
563
- (path -> (mtime, bytes)), when given, caches those walks across sweeps
564
- and is re-keyed to this sweep's held folders; sizing stops at the time
565
- budget too, and what was left unsized is counted in `held_unsized`."""
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
- seen_sizes: "dict[Path, tuple[float, int]]" = {}
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
- key = f"{candidate.hub_slug}: {why}"
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
- if sizes is not None:
654
- sizes.clear()
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
- # (issue #185): the held-bytes summary re-walks only what changed.
2852
- self._janitor_sizes: "dict[Path, tuple[float, int]]" = {}
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.24
3
+ Version: 0.31.25
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera