gitgrip 1.5.0__py3-none-any.whl

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 (80) hide show
  1. gitgrip-1.5.0.dist-info/METADATA +13 -0
  2. gitgrip-1.5.0.dist-info/RECORD +80 -0
  3. gitgrip-1.5.0.dist-info/WHEEL +5 -0
  4. gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
  5. gitgrip-1.5.0.dist-info/top_level.txt +2 -0
  6. gr2/__init__.py +0 -0
  7. gr2/overlay/__init__.py +6 -0
  8. gr2/overlay/activate.py +196 -0
  9. gr2/overlay/agent_manifest.py +138 -0
  10. gr2/overlay/cli.py +181 -0
  11. gr2/overlay/cross_repo.py +124 -0
  12. gr2/overlay/drivers.py +113 -0
  13. gr2/overlay/introspection.py +155 -0
  14. gr2/overlay/language_drivers.py +115 -0
  15. gr2/overlay/objects.py +412 -0
  16. gr2/overlay/perf.py +251 -0
  17. gr2/overlay/refs.py +36 -0
  18. gr2/overlay/trust.py +150 -0
  19. gr2/overlay/types.py +69 -0
  20. gr2/overlay/units.py +313 -0
  21. gr2/overlay/workspace_spec.py +59 -0
  22. gr2/prototypes/__init__.py +0 -0
  23. gr2/prototypes/cache_materialization_probe.py +190 -0
  24. gr2/prototypes/concurrent_event_stress.py +199 -0
  25. gr2/prototypes/concurrent_lease_stress.py +240 -0
  26. gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
  27. gr2/prototypes/contribution_protocol.py +665 -0
  28. gr2/prototypes/cross_mode_lane_stress.py +986 -0
  29. gr2/prototypes/jsonl_store.py +158 -0
  30. gr2/prototypes/lane_workspace_prototype.py +2088 -0
  31. gr2/prototypes/layout_model_probe.py +139 -0
  32. gr2/prototypes/propagation_daemon.py +546 -0
  33. gr2/prototypes/propagation_state_machine.py +1478 -0
  34. gr2/prototypes/python_exec_playground.py +194 -0
  35. gr2/prototypes/python_hook_runtime_playground.py +240 -0
  36. gr2/prototypes/python_migration_playground.py +144 -0
  37. gr2/prototypes/python_review_checkout_playground.py +242 -0
  38. gr2/prototypes/python_spec_apply_playground.py +282 -0
  39. gr2/prototypes/real_git_lane_materialization.py +248 -0
  40. gr2/prototypes/real_git_playground.py +334 -0
  41. gr2/prototypes/recall_lane_history.py +274 -0
  42. gr2/prototypes/repo_maintenance_prototype.py +659 -0
  43. gr2/prototypes/repo_transport_probe.py +147 -0
  44. gr2/python_cli/__init__.py +2 -0
  45. gr2/python_cli/__main__.py +6 -0
  46. gr2/python_cli/add.py +51 -0
  47. gr2/python_cli/app.py +2516 -0
  48. gr2/python_cli/branch.py +67 -0
  49. gr2/python_cli/channel_bridge.py +131 -0
  50. gr2/python_cli/clone_exec.py +1019 -0
  51. gr2/python_cli/commit.py +199 -0
  52. gr2/python_cli/config.py +291 -0
  53. gr2/python_cli/env_exec.py +419 -0
  54. gr2/python_cli/events.py +529 -0
  55. gr2/python_cli/execops.py +372 -0
  56. gr2/python_cli/failures.py +98 -0
  57. gr2/python_cli/file_exec.py +256 -0
  58. gr2/python_cli/gitops.py +226 -0
  59. gr2/python_cli/grip.py +1337 -0
  60. gr2/python_cli/grip_cli.py +493 -0
  61. gr2/python_cli/hooks.py +450 -0
  62. gr2/python_cli/launch_exec.py +786 -0
  63. gr2/python_cli/merge_verification.py +274 -0
  64. gr2/python_cli/migration.py +985 -0
  65. gr2/python_cli/open_gr_review.py +699 -0
  66. gr2/python_cli/platform.py +441 -0
  67. gr2/python_cli/pr.py +487 -0
  68. gr2/python_cli/project_review.py +314 -0
  69. gr2/python_cli/prune.py +365 -0
  70. gr2/python_cli/push.py +172 -0
  71. gr2/python_cli/review.py +462 -0
  72. gr2/python_cli/review_ephemeral.py +143 -0
  73. gr2/python_cli/review_run.py +621 -0
  74. gr2/python_cli/spec_apply.py +1285 -0
  75. gr2/python_cli/staging_cleanup.py +205 -0
  76. gr2/python_cli/syncops.py +920 -0
  77. gr2/python_cli/target.py +100 -0
  78. gr2/python_cli/workspace_snapshot.py +105 -0
  79. gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
  80. gr2_overlay/__init__.py +37 -0
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ """Prototype cache-backed materialization for gr2 working clones.
3
+
4
+ This is not a final implementation. It exists to answer a narrower question:
5
+
6
+ - if `apply` seeds working clones from a local mirror/reference cache, does that
7
+ materially improve materialization speed while keeping the user-facing model
8
+ unchanged?
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import time
21
+ from pathlib import Path
22
+
23
+
24
+ PLAYGROUND_REPOS = {
25
+ "app": {
26
+ "ssh": "git@github.com:synapt-dev/gr2-playground-app.git",
27
+ "https": "https://github.com/synapt-dev/gr2-playground-app.git",
28
+ },
29
+ "api": {
30
+ "ssh": "git@github.com:synapt-dev/gr2-playground-api.git",
31
+ "https": "https://github.com/synapt-dev/gr2-playground-api.git",
32
+ },
33
+ "web": {
34
+ "ssh": "git@github.com:synapt-dev/gr2-playground-web.git",
35
+ "https": "https://github.com/synapt-dev/gr2-playground-web.git",
36
+ },
37
+ }
38
+
39
+
40
+ def parse_args() -> argparse.Namespace:
41
+ parser = argparse.ArgumentParser(
42
+ description="Measure direct clone vs cache-backed clone materialization"
43
+ )
44
+ parser.add_argument(
45
+ "--transport",
46
+ choices=["ssh", "https"],
47
+ default="ssh",
48
+ help="remote transport to test",
49
+ )
50
+ parser.add_argument(
51
+ "--repo",
52
+ action="append",
53
+ dest="repos",
54
+ help="repo(s) to test; defaults to all playground repos",
55
+ )
56
+ parser.add_argument(
57
+ "--workspace-root",
58
+ type=Path,
59
+ help="optional persistent temp root; defaults to a temporary directory",
60
+ )
61
+ parser.add_argument("--json", action="store_true")
62
+ return parser.parse_args()
63
+
64
+
65
+ def repo_url(repo_name: str, transport: str) -> str:
66
+ return PLAYGROUND_REPOS[repo_name][transport]
67
+
68
+
69
+ def git_env() -> dict[str, str]:
70
+ env = dict(os.environ)
71
+ env.setdefault(
72
+ "GIT_SSH_COMMAND",
73
+ "ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new",
74
+ )
75
+ return env
76
+
77
+
78
+ def run(argv: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
79
+ return subprocess.run(
80
+ argv,
81
+ cwd=cwd,
82
+ text=True,
83
+ capture_output=True,
84
+ check=True,
85
+ env=git_env(),
86
+ )
87
+
88
+
89
+ def timed_clone(argv: list[str], *, cwd: Path | None = None) -> tuple[float, subprocess.CompletedProcess[str]]:
90
+ start = time.perf_counter()
91
+ result = run(argv, cwd=cwd)
92
+ elapsed = time.perf_counter() - start
93
+ return (elapsed, result)
94
+
95
+
96
+ def verify_working_clone(path: Path) -> dict[str, object]:
97
+ head = run(["git", "-C", str(path), "rev-parse", "--abbrev-ref", "HEAD"]).stdout.strip()
98
+ status = run(["git", "-C", str(path), "status", "--short"]).stdout.strip()
99
+ alternates = path / ".git" / "objects" / "info" / "alternates"
100
+ return {
101
+ "head": head,
102
+ "clean": status == "",
103
+ "uses_alternates": alternates.exists(),
104
+ "alternates_path": str(alternates) if alternates.exists() else None,
105
+ }
106
+
107
+
108
+ def probe_repo(root: Path, repo_name: str, transport: str) -> dict[str, object]:
109
+ url = repo_url(repo_name, transport)
110
+ repo_root = root / repo_name
111
+ direct_root = repo_root / "direct"
112
+ cached_root = repo_root / "cached"
113
+ cache_root = repo_root / "cache"
114
+ direct_root.mkdir(parents=True, exist_ok=True)
115
+ cached_root.mkdir(parents=True, exist_ok=True)
116
+ cache_root.mkdir(parents=True, exist_ok=True)
117
+
118
+ direct_target = direct_root / repo_name
119
+ cache_mirror = cache_root / f"{repo_name}.git"
120
+ cached_target = cached_root / repo_name
121
+
122
+ direct_seconds, _ = timed_clone(["git", "clone", url, str(direct_target)])
123
+ mirror_seconds, _ = timed_clone(["git", "clone", "--mirror", url, str(cache_mirror)])
124
+ cached_seconds, _ = timed_clone(
125
+ [
126
+ "git",
127
+ "clone",
128
+ "--reference-if-able",
129
+ str(cache_mirror),
130
+ url,
131
+ str(cached_target),
132
+ ]
133
+ )
134
+
135
+ direct_info = verify_working_clone(direct_target)
136
+ cached_info = verify_working_clone(cached_target)
137
+
138
+ return {
139
+ "repo": repo_name,
140
+ "transport": transport,
141
+ "direct_clone_seconds": round(direct_seconds, 3),
142
+ "mirror_seed_seconds": round(mirror_seconds, 3),
143
+ "cached_clone_seconds": round(cached_seconds, 3),
144
+ "delta_seconds": round(direct_seconds - cached_seconds, 3),
145
+ "direct_clone": direct_info,
146
+ "cached_clone": cached_info,
147
+ "mirror_path": str(cache_mirror),
148
+ }
149
+
150
+
151
+ def main() -> int:
152
+ args = parse_args()
153
+ repos = args.repos or list(PLAYGROUND_REPOS.keys())
154
+ unknown = [repo for repo in repos if repo not in PLAYGROUND_REPOS]
155
+ if unknown:
156
+ raise SystemExit("unknown repos: " + ", ".join(unknown))
157
+
158
+ if args.workspace_root:
159
+ root = args.workspace_root.resolve()
160
+ root.mkdir(parents=True, exist_ok=True)
161
+ cleanup = False
162
+ else:
163
+ root = Path(tempfile.mkdtemp(prefix="gr2-cache-probe."))
164
+ cleanup = True
165
+
166
+ try:
167
+ rows = [probe_repo(root, repo, args.transport) for repo in repos]
168
+ if args.json:
169
+ print(json.dumps({"root": str(root), "results": rows}, indent=2))
170
+ else:
171
+ print(f"root: {root}")
172
+ print("REPO\tTRANSPORT\tDIRECT_S\tMIRROR_S\tCACHED_S\tDELTA_S\tALTERNATES")
173
+ for row in rows:
174
+ print(
175
+ f"{row['repo']}\t{row['transport']}\t{row['direct_clone_seconds']}\t"
176
+ f"{row['mirror_seed_seconds']}\t{row['cached_clone_seconds']}\t"
177
+ f"{row['delta_seconds']}\t{str(row['cached_clone']['uses_alternates']).lower()}"
178
+ )
179
+ print("notes:")
180
+ print("- direct clone is a normal working clone from remote")
181
+ print("- mirror seed is the one-time cache population cost")
182
+ print("- cached clone is a working clone using --reference-if-able from the local mirror")
183
+ return 0
184
+ finally:
185
+ if cleanup:
186
+ shutil.rmtree(root)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ sys.exit(main())
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """Repeated before/after stress harness for event-log sequence integrity."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import multiprocessing
9
+ import os
10
+ import sys
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ SOURCE_ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(SOURCE_ROOT))
16
+
17
+ from python_cli.events import EventType, emit # noqa: E402
18
+
19
+
20
+ def parse_args() -> argparse.Namespace:
21
+ parser = argparse.ArgumentParser(description=__doc__)
22
+ parser.add_argument("--rounds", type=int, default=30)
23
+ parser.add_argument("--writers", type=int, default=2)
24
+ parser.add_argument("--json", action="store_true")
25
+ return parser.parse_args()
26
+
27
+
28
+ def _worker(
29
+ workspace: str,
30
+ actor: str,
31
+ start: object,
32
+ queue: object,
33
+ unlocked: bool,
34
+ expected_source_root: str,
35
+ ) -> None:
36
+ try:
37
+ import gr2
38
+
39
+ module_file = Path(gr2.__file__).resolve() if gr2.__file__ is not None else None
40
+ source_root = Path(expected_source_root).resolve()
41
+ if module_file is None or not module_file.is_relative_to(source_root):
42
+ raise RuntimeError(
43
+ f"spawned worker imported gr2 outside expected worktree: "
44
+ f"module={module_file}, expected_root={source_root}"
45
+ )
46
+ if unlocked:
47
+ os.environ["GR2_DISABLE_EVENT_LOCKING"] = "1"
48
+ os.environ["GR2_EVENT_TEST_DELAY"] = "0.03"
49
+ if not start.wait(timeout=10):
50
+ raise TimeoutError("start gate timed out")
51
+ emit(
52
+ event_type=EventType.LANE_ENTERED,
53
+ workspace_root=Path(workspace),
54
+ actor=actor,
55
+ owner_unit="event-stress",
56
+ payload={"round_actor": actor},
57
+ )
58
+ except Exception as exc:
59
+ queue.put({"ok": False, "error": repr(exc)})
60
+ else:
61
+ queue.put({"ok": True, "gr2_module": str(module_file)})
62
+
63
+
64
+ def _read_rows(outbox: Path) -> tuple[list[dict[str, object]], int]:
65
+ rows: list[dict[str, object]] = []
66
+ corruption_count = 0
67
+ for line in outbox.read_text().splitlines() if outbox.exists() else []:
68
+ try:
69
+ row = json.loads(line)
70
+ except json.JSONDecodeError:
71
+ corruption_count += 1
72
+ continue
73
+ if not isinstance(row, dict):
74
+ corruption_count += 1
75
+ continue
76
+ rows.append(row)
77
+ return rows, corruption_count
78
+
79
+
80
+ def prove_corruption_detector() -> dict[str, object]:
81
+ """Prove the reported zero can distinguish a known malformed event."""
82
+ with tempfile.TemporaryDirectory(prefix="gr2-event-corruption-control-") as tmp:
83
+ outbox = Path(tmp) / "outbox.jsonl"
84
+ outbox.write_text("{malformed-json\n")
85
+ _, corruption_count = _read_rows(outbox)
86
+ return {
87
+ "proven": corruption_count == 1,
88
+ "fixture": "malformed-json",
89
+ "detected_count": corruption_count,
90
+ }
91
+
92
+
93
+ def _run_round(writers: int, unlocked: bool) -> dict[str, object]:
94
+ ctx = multiprocessing.get_context("spawn")
95
+ with tempfile.TemporaryDirectory(prefix="gr2-event-stress-") as tmp:
96
+ workspace = Path(tmp)
97
+ (workspace / ".grip").mkdir()
98
+ start = ctx.Event()
99
+ queue = ctx.Queue()
100
+ processes = [
101
+ ctx.Process(
102
+ target=_worker,
103
+ args=(
104
+ str(workspace),
105
+ f"writer:{index}",
106
+ start,
107
+ queue,
108
+ unlocked,
109
+ str(SOURCE_ROOT),
110
+ ),
111
+ )
112
+ for index in range(writers)
113
+ ]
114
+ for process in processes:
115
+ process.start()
116
+ start.set()
117
+ for process in processes:
118
+ process.join(timeout=15)
119
+ outcomes = [queue.get(timeout=2) for _ in processes]
120
+ rows, corruption_count = _read_rows(workspace / ".grip" / "events" / "outbox.jsonl")
121
+ seqs = [row.get("seq") for row in rows]
122
+ return {
123
+ "all_workers_succeeded": all(outcome["ok"] for outcome in outcomes),
124
+ "lost_events": len(rows) != writers,
125
+ "duplicate_seq": len(seqs) != len(set(seqs)),
126
+ "corruption_count": corruption_count,
127
+ }
128
+
129
+
130
+ def run_phase(*, rounds: int, writers: int, unlocked: bool) -> dict[str, object]:
131
+ results = [_run_round(writers, unlocked) for _ in range(rounds)]
132
+ return {
133
+ "locking": "disabled" if unlocked else "enabled",
134
+ "rounds": rounds,
135
+ "writers": writers,
136
+ "duplicate_seq_rounds": sum(bool(row["duplicate_seq"]) for row in results),
137
+ "lost_event_rounds": sum(bool(row["lost_events"]) for row in results),
138
+ "corruption_count": sum(int(row["corruption_count"]) for row in results),
139
+ "worker_failure_rounds": sum(not bool(row["all_workers_succeeded"]) for row in results),
140
+ }
141
+
142
+
143
+ def sequential_control(writes: int = 8) -> dict[str, object]:
144
+ with tempfile.TemporaryDirectory(prefix="gr2-event-sequential-control-") as tmp:
145
+ workspace = Path(tmp)
146
+ (workspace / ".grip").mkdir()
147
+ old_disabled = os.environ.get("GR2_DISABLE_EVENT_LOCKING")
148
+ old_delay = os.environ.get("GR2_EVENT_TEST_DELAY")
149
+ os.environ["GR2_DISABLE_EVENT_LOCKING"] = "1"
150
+ os.environ["GR2_EVENT_TEST_DELAY"] = "0"
151
+ try:
152
+ for index in range(writes):
153
+ emit(
154
+ event_type=EventType.LANE_ENTERED,
155
+ workspace_root=workspace,
156
+ actor="sequential-control",
157
+ owner_unit="event-stress",
158
+ payload={"index": index},
159
+ )
160
+ finally:
161
+ if old_disabled is None:
162
+ os.environ.pop("GR2_DISABLE_EVENT_LOCKING", None)
163
+ else:
164
+ os.environ["GR2_DISABLE_EVENT_LOCKING"] = old_disabled
165
+ if old_delay is None:
166
+ os.environ.pop("GR2_EVENT_TEST_DELAY", None)
167
+ else:
168
+ os.environ["GR2_EVENT_TEST_DELAY"] = old_delay
169
+ rows, corruption_count = _read_rows(workspace / ".grip" / "events" / "outbox.jsonl")
170
+ seqs = [row.get("seq") for row in rows]
171
+ return {
172
+ "writes": writes,
173
+ "strictly_monotonic": seqs == list(range(1, writes + 1)),
174
+ "corruption_count": corruption_count,
175
+ }
176
+
177
+
178
+ def main() -> int:
179
+ args = parse_args()
180
+ payload = {
181
+ "corruption_detector_control": prove_corruption_detector(),
182
+ "sequential_control": sequential_control(),
183
+ "before_locking": run_phase(rounds=args.rounds, writers=args.writers, unlocked=True),
184
+ "after_locking": run_phase(rounds=args.rounds, writers=args.writers, unlocked=False),
185
+ }
186
+ print(json.dumps(payload, indent=2))
187
+ after = payload["after_locking"]
188
+ return int(
189
+ not payload["corruption_detector_control"]["proven"]
190
+ or not payload["sequential_control"]["strictly_monotonic"]
191
+ or after["duplicate_seq_rounds"] != 0
192
+ or after["lost_event_rounds"] != 0
193
+ or after["corruption_count"] != 0
194
+ or after["worker_failure_rounds"] != 0
195
+ )
196
+
197
+
198
+ if __name__ == "__main__":
199
+ raise SystemExit(main())
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ """Concurrent lease stress harness with before/after locking results."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import multiprocessing
9
+ import os
10
+ import subprocess
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description="Run concurrent lease stress before and after file locking"
18
+ )
19
+ parser.add_argument("--rounds", type=int, default=50)
20
+ parser.add_argument("--json", action="store_true")
21
+ return parser.parse_args()
22
+
23
+
24
+ def repo_root() -> Path:
25
+ return Path(__file__).resolve().parents[2]
26
+
27
+
28
+ def lane_proto(root: Path) -> Path:
29
+ return root / "gr2" / "prototypes" / "lane_workspace_prototype.py"
30
+
31
+
32
+ def run(
33
+ argv: list[str],
34
+ *,
35
+ env: dict | None = None,
36
+ check: bool = True,
37
+ capture: bool = False,
38
+ ) -> subprocess.CompletedProcess[str]:
39
+ return subprocess.run(argv, check=check, text=True, capture_output=capture, env=env)
40
+
41
+
42
+ def init_workspace(workspace_root: Path) -> None:
43
+ (workspace_root / ".grip").mkdir(parents=True, exist_ok=True)
44
+ (workspace_root / "agents").mkdir(exist_ok=True)
45
+ spec = """schema_version = 1
46
+ workspace_name = "concurrent-lease-stress"
47
+
48
+ [cache]
49
+ root = ".grip/cache"
50
+
51
+ [[repos]]
52
+ name = "app"
53
+ path = "repos/app"
54
+ url = "https://example.invalid/app.git"
55
+
56
+ [[units]]
57
+ name = "atlas"
58
+ path = "agents/atlas"
59
+ agent_id = "atlas-agent"
60
+ repos = ["app"]
61
+ """
62
+ (workspace_root / ".grip" / "workspace_spec.toml").write_text(spec)
63
+
64
+
65
+ def create_lane(root: Path, workspace_root: Path) -> None:
66
+ run(
67
+ [
68
+ "python3",
69
+ str(lane_proto(root)),
70
+ "create-lane",
71
+ str(workspace_root),
72
+ "atlas",
73
+ "feat-race",
74
+ "--repos",
75
+ "app",
76
+ "--branch",
77
+ "feat/race",
78
+ ],
79
+ capture=True,
80
+ )
81
+
82
+
83
+ def worker(workspace_root: str, actor: str, queue, disable_locking: bool) -> None:
84
+ root = repo_root()
85
+ env = os.environ.copy()
86
+ if disable_locking:
87
+ env["GR2_DISABLE_LEASE_LOCKING"] = "1"
88
+ env["GR2_LEASE_TEST_DELAY"] = "0.02"
89
+ proc = run(
90
+ [
91
+ "python3",
92
+ str(lane_proto(root)),
93
+ "acquire-lane-lease",
94
+ workspace_root,
95
+ "atlas",
96
+ "feat-race",
97
+ "--actor",
98
+ actor,
99
+ "--mode",
100
+ "edit",
101
+ "--ttl-seconds",
102
+ "900",
103
+ ],
104
+ env=env,
105
+ check=False,
106
+ capture=True,
107
+ )
108
+ queue.put(
109
+ {
110
+ "actor": actor,
111
+ "returncode": proc.returncode,
112
+ "stdout": proc.stdout,
113
+ "stderr": proc.stderr,
114
+ }
115
+ )
116
+
117
+
118
+ def read_leases(workspace_root: Path) -> tuple[bool, list[dict] | None]:
119
+ path = workspace_root / "agents" / "atlas" / "lanes" / "feat-race" / "leases.json"
120
+ if not path.exists():
121
+ return True, []
122
+ try:
123
+ return True, json.loads(path.read_text())
124
+ except json.JSONDecodeError:
125
+ return False, None
126
+
127
+
128
+ def prove_corruption_detector() -> dict[str, object]:
129
+ """Prove the corruption counter's detector with a known-bad lease file."""
130
+ with tempfile.TemporaryDirectory(prefix="gr2-lease-corruption-control-") as tmp:
131
+ workspace_root = Path(tmp)
132
+ lease_path = (
133
+ workspace_root
134
+ / "agents"
135
+ / "atlas"
136
+ / "lanes"
137
+ / "feat-race"
138
+ / "leases.json"
139
+ )
140
+ lease_path.parent.mkdir(parents=True)
141
+ lease_path.write_text("{malformed-json")
142
+ valid_json, _ = read_leases(workspace_root)
143
+ return {
144
+ "proven": not valid_json,
145
+ "fixture": "malformed-json",
146
+ "detected_as_corrupt": not valid_json,
147
+ }
148
+
149
+
150
+ def release_all(root: Path, workspace_root: Path, disable_locking: bool) -> None:
151
+ env = os.environ.copy()
152
+ if disable_locking:
153
+ env["GR2_DISABLE_LEASE_LOCKING"] = "1"
154
+ for actor in ("worker:a", "worker:b"):
155
+ run(
156
+ [
157
+ "python3",
158
+ str(lane_proto(root)),
159
+ "release-lane-lease",
160
+ str(workspace_root),
161
+ "atlas",
162
+ "feat-race",
163
+ "--actor",
164
+ actor,
165
+ ],
166
+ env=env,
167
+ check=False,
168
+ capture=True,
169
+ )
170
+
171
+
172
+ def run_phase(disable_locking: bool, rounds: int) -> dict:
173
+ root = repo_root()
174
+ with tempfile.TemporaryDirectory(prefix="gr2-concurrent-lease-") as tmp:
175
+ workspace_root = Path(tmp)
176
+ init_workspace(workspace_root)
177
+ create_lane(root, workspace_root)
178
+
179
+ corruption_count = 0
180
+ both_succeeded_count = 0
181
+ unexpected_lease_count = 0
182
+
183
+ for _ in range(rounds):
184
+ queue = multiprocessing.Queue()
185
+ p1 = multiprocessing.Process(
186
+ target=worker,
187
+ args=(str(workspace_root), "worker:a", queue, disable_locking),
188
+ )
189
+ p2 = multiprocessing.Process(
190
+ target=worker,
191
+ args=(str(workspace_root), "worker:b", queue, disable_locking),
192
+ )
193
+ p1.start()
194
+ p2.start()
195
+ p1.join()
196
+ p2.join()
197
+
198
+ results = [queue.get(), queue.get()]
199
+ success_count = sum(1 for item in results if item["returncode"] == 0)
200
+ if success_count == 2:
201
+ both_succeeded_count += 1
202
+
203
+ valid_json, leases = read_leases(workspace_root)
204
+ if not valid_json:
205
+ corruption_count += 1
206
+ else:
207
+ expected = 1 if success_count >= 1 else 0
208
+ if len(leases or []) != expected:
209
+ unexpected_lease_count += 1
210
+
211
+ release_all(root, workspace_root, disable_locking)
212
+
213
+ return {
214
+ "locking": "disabled" if disable_locking else "enabled",
215
+ "rounds": rounds,
216
+ "corruption_count": corruption_count,
217
+ "both_succeeded_count": both_succeeded_count,
218
+ "unexpected_lease_count": unexpected_lease_count,
219
+ }
220
+
221
+
222
+ def main() -> int:
223
+ args = parse_args()
224
+ before = run_phase(disable_locking=True, rounds=args.rounds)
225
+ after = run_phase(disable_locking=False, rounds=args.rounds)
226
+ payload = {
227
+ "corruption_detector_control": prove_corruption_detector(),
228
+ "before_locking": before,
229
+ "after_locking": after,
230
+ }
231
+ if args.json:
232
+ print(json.dumps(payload, indent=2))
233
+ else:
234
+ print("gr2 concurrent lease stress")
235
+ print(json.dumps(payload, indent=2))
236
+ return 0 if payload["corruption_detector_control"]["proven"] else 1
237
+
238
+
239
+ if __name__ == "__main__":
240
+ raise SystemExit(main())