cctally 1.97.0 → 1.99.0

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.
@@ -888,6 +888,58 @@ def note_stats_maintenance_released() -> None:
888
888
  _STATS_MAINTENANCE_HELD.set(max(0, _STATS_MAINTENANCE_HELD.get() - 1))
889
889
 
890
890
 
891
+ # === #500 §8.1 ordered-partial-release apply lock set =====================
892
+ #
893
+ # `_cctally_rederive.codex_attribution_apply_locks` takes the two cache writer
894
+ # flocks INSIDE the stats maintenance and ingest locks, and the repository
895
+ # lock-order law requires every cache write to be committed and unlocked before
896
+ # the stats transaction opens. The ordered partial release is what satisfies
897
+ # that law; this counter is what MAKES it a law rather than a convention, by
898
+ # letting `_run_stats_ingest_once(locks_held=True)` refuse while they are still
899
+ # held instead of quietly running the stats transaction underneath them.
900
+ #
901
+ # A ContextVar like the two above, but NOT for their reason, and the difference
902
+ # is worth stating because copying their rationale here inverts it. Those two
903
+ # are AUTHORIZATION counters: a global would let one sanctioned context
904
+ # authorize an unsanctioned one, so per-context isolation is what fails safe.
905
+ # This one is a REFUSAL guard, and for a refusal per-context isolation fails
906
+ # OPEN — a context that cannot see the hold does not refuse.
907
+ #
908
+ # What the ContextVar buys is therefore narrower and still worth having: the
909
+ # guard reports the state of the context that is about to open the stats
910
+ # transaction, so it can never be silenced by an unrelated dashboard thread that
911
+ # happens to hold the cache flocks for its own read, and it can never be left
912
+ # armed by one. It is a guard against the apply sequence's own steps running out
913
+ # of order, not a process-wide interlock.
914
+ #
915
+ # The cost of that choice: the guard is ADVISORY across a context boundary. A
916
+ # bare `os.fork()` inherits the value by copy and a thread spawned inside the
917
+ # `with` block starts from a copy of the spawning context, so neither child sees
918
+ # a later release, and a thread created BEFORE the acquisition never sees the
919
+ # hold at all. `codex_attribution_apply_locks` therefore forbids both inside its
920
+ # block; the flocks themselves remain the real mutual exclusion.
921
+
922
+ _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD = contextvars.ContextVar(
923
+ "cctally_attribution_apply_cache_flocks_held", default=0
924
+ )
925
+
926
+
927
+ def holds_attribution_apply_cache_flocks() -> bool:
928
+ """True while this context still holds the #500 apply set's cache flocks."""
929
+ return _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD.get() > 0
930
+
931
+
932
+ def note_attribution_apply_cache_flocks_acquired() -> None:
933
+ _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD.set(
934
+ _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD.get() + 1)
935
+
936
+
937
+ def note_attribution_apply_cache_flocks_released() -> None:
938
+ """Clamped at zero, for the reason the maintenance twin above states."""
939
+ _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD.set(
940
+ max(0, _ATTRIBUTION_APPLY_CACHE_FLOCKS_HELD.get() - 1))
941
+
942
+
891
943
  # === Alerts validation cluster ======================================
892
944
 
893
945