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.
- gitgrip-1.5.0.dist-info/METADATA +13 -0
- gitgrip-1.5.0.dist-info/RECORD +80 -0
- gitgrip-1.5.0.dist-info/WHEEL +5 -0
- gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
- gitgrip-1.5.0.dist-info/top_level.txt +2 -0
- gr2/__init__.py +0 -0
- gr2/overlay/__init__.py +6 -0
- gr2/overlay/activate.py +196 -0
- gr2/overlay/agent_manifest.py +138 -0
- gr2/overlay/cli.py +181 -0
- gr2/overlay/cross_repo.py +124 -0
- gr2/overlay/drivers.py +113 -0
- gr2/overlay/introspection.py +155 -0
- gr2/overlay/language_drivers.py +115 -0
- gr2/overlay/objects.py +412 -0
- gr2/overlay/perf.py +251 -0
- gr2/overlay/refs.py +36 -0
- gr2/overlay/trust.py +150 -0
- gr2/overlay/types.py +69 -0
- gr2/overlay/units.py +313 -0
- gr2/overlay/workspace_spec.py +59 -0
- gr2/prototypes/__init__.py +0 -0
- gr2/prototypes/cache_materialization_probe.py +190 -0
- gr2/prototypes/concurrent_event_stress.py +199 -0
- gr2/prototypes/concurrent_lease_stress.py +240 -0
- gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
- gr2/prototypes/contribution_protocol.py +665 -0
- gr2/prototypes/cross_mode_lane_stress.py +986 -0
- gr2/prototypes/jsonl_store.py +158 -0
- gr2/prototypes/lane_workspace_prototype.py +2088 -0
- gr2/prototypes/layout_model_probe.py +139 -0
- gr2/prototypes/propagation_daemon.py +546 -0
- gr2/prototypes/propagation_state_machine.py +1478 -0
- gr2/prototypes/python_exec_playground.py +194 -0
- gr2/prototypes/python_hook_runtime_playground.py +240 -0
- gr2/prototypes/python_migration_playground.py +144 -0
- gr2/prototypes/python_review_checkout_playground.py +242 -0
- gr2/prototypes/python_spec_apply_playground.py +282 -0
- gr2/prototypes/real_git_lane_materialization.py +248 -0
- gr2/prototypes/real_git_playground.py +334 -0
- gr2/prototypes/recall_lane_history.py +274 -0
- gr2/prototypes/repo_maintenance_prototype.py +659 -0
- gr2/prototypes/repo_transport_probe.py +147 -0
- gr2/python_cli/__init__.py +2 -0
- gr2/python_cli/__main__.py +6 -0
- gr2/python_cli/add.py +51 -0
- gr2/python_cli/app.py +2516 -0
- gr2/python_cli/branch.py +67 -0
- gr2/python_cli/channel_bridge.py +131 -0
- gr2/python_cli/clone_exec.py +1019 -0
- gr2/python_cli/commit.py +199 -0
- gr2/python_cli/config.py +291 -0
- gr2/python_cli/env_exec.py +419 -0
- gr2/python_cli/events.py +529 -0
- gr2/python_cli/execops.py +372 -0
- gr2/python_cli/failures.py +98 -0
- gr2/python_cli/file_exec.py +256 -0
- gr2/python_cli/gitops.py +226 -0
- gr2/python_cli/grip.py +1337 -0
- gr2/python_cli/grip_cli.py +493 -0
- gr2/python_cli/hooks.py +450 -0
- gr2/python_cli/launch_exec.py +786 -0
- gr2/python_cli/merge_verification.py +274 -0
- gr2/python_cli/migration.py +985 -0
- gr2/python_cli/open_gr_review.py +699 -0
- gr2/python_cli/platform.py +441 -0
- gr2/python_cli/pr.py +487 -0
- gr2/python_cli/project_review.py +314 -0
- gr2/python_cli/prune.py +365 -0
- gr2/python_cli/push.py +172 -0
- gr2/python_cli/review.py +462 -0
- gr2/python_cli/review_ephemeral.py +143 -0
- gr2/python_cli/review_run.py +621 -0
- gr2/python_cli/spec_apply.py +1285 -0
- gr2/python_cli/staging_cleanup.py +205 -0
- gr2/python_cli/syncops.py +920 -0
- gr2/python_cli/target.py +100 -0
- gr2/python_cli/workspace_snapshot.py +105 -0
- gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
- 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())
|