Repository navigation
ci(cmux-next media): let tours run side by side on one Mac #22046
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3b6b740
ci(cmux-next media): tests for concurrent tours on one Mac (red)
teamleaderleo ff2f66f
ci(cmux-next media): let tours run side by side on one Mac
teamleaderleo f0fe23e
ci(cmux-next media): tours launch the app with CMUX_NEXT_NO_ACTIVATE=1
teamleaderleo 1286a4a
test: a prune keeps the DerivedData of a tag a tour holds
teamleaderleo 7468a34
ci: tours keep their held tag through a prune, and old heads still ho…
teamleaderleo 0c15ff8
Merge remote-tracking branch 'origin/feat-cmux-next' into rescue/ci-t…
teamleaderleo e7d0698
test(media prune): a tour asking for a tag mid-delete is refused
teamleaderleo 1b8a89f
fix(media prune): hold the tag lock until the dir is gone
teamleaderleo 41f9b19
test(media tour): a tour waits for its tag as long as the holder's jo…
teamleaderleo d013c33
fix(media tour): wait for a held tag as long as the holder's job can run
teamleaderleo a77a158
test(media prune): an old head's tour keeps a tag another tour holds
teamleaderleo c40e202
fix(media tour): an old head's tour prunes with the lock-aware script
teamleaderleo 82b9d03
Merge remote-tracking branch 'origin/feat-cmux-next' into ci/tour-con…
teamleaderleo 45331f8
ci(guards): run the media tag lock test
teamleaderleo b5a23d7
Merge remote-tracking branch 'origin/feat-cmux-next' into ci/tour-con…
teamleaderleo 071bcb0
test(media tour): a held-tag wait leaves the waiter time to build
teamleaderleo bd085a7
fix(media tour): wait an hour for a held tag, and keep the checkout full
teamleaderleo 5bfa69b
Merge branch 'feat-cmux-next' into ci/tour-concurrent
teamleaderleo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| #!/usr/bin/env python3 | ||
| """Hold a cmux-next media tour's tag for the rest of its job. | ||
|
|
||
| media_tag_lock.py hold --tag ci-media-pr-123 [--wait SECONDS] | ||
|
|
||
| A tour builds into per-tag DerivedData (cmux-<tag>-base, cmux-<tag>-head) | ||
| kept warm across runs of one pull request, and launches apps whose sockets | ||
| and daemon session come from the tag. Several tours run on one Mac at once, | ||
| each on its own runner; the workflow's concurrency group keeps two runs of a | ||
| pull request apart, but not a cancelled run that is still stopping, a | ||
| dispatch, or a merge tour beside a dispatch on the shared feat-cmux-next tag. | ||
| Two tours building into one DerivedData at once break its module cache | ||
| ("ghostty.h has been modified since the module file was built"). | ||
|
|
||
| `hold` waits for the tag's lock (one file per tag under --lock-dir), then | ||
| leaves a background process holding it and returns. The holder lets go when | ||
| the job's Runner.Worker exits (the runner also ends a job's leftover | ||
| processes) or after --max-hold seconds, so a lost job never keeps a tag. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import fcntl | ||
| import os | ||
| from pathlib import Path | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| import time | ||
|
|
||
| DEFAULT_LOCK_DIR = Path.home() / "Library/Caches/cmux-ci-media/locks" | ||
| WORKER = "Runner.Worker" | ||
|
|
||
|
|
||
| def worker_pid(start: int) -> int | None: | ||
| """The nearest Runner.Worker among start's ancestors, from ps.""" | ||
| pid = start | ||
| for _ in range(64): | ||
| if pid <= 1: | ||
| return None | ||
| done = subprocess.run(["ps", "-o", "ppid=,comm=", "-p", str(pid)], capture_output=True, text=True) | ||
| fields = done.stdout.strip().split(None, 1) | ||
| if len(fields) != 2: | ||
| return None | ||
| if os.path.basename(fields[1]) == WORKER: | ||
| return pid | ||
| pid = int(fields[0]) | ||
| return None | ||
|
|
||
|
|
||
| def alive(pid: int) -> bool: | ||
| try: | ||
| os.kill(pid, 0) | ||
| except ProcessLookupError: | ||
| return False | ||
| except PermissionError: | ||
| return True | ||
| return True | ||
|
|
||
|
|
||
| def acquire(path: Path, wait: float) -> int | None: | ||
| """An fd holding path's exclusive lock, or None after wait seconds.""" | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o644) | ||
| deadline = time.monotonic() + wait | ||
| said = False | ||
| while True: | ||
| try: | ||
| fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) | ||
| return fd | ||
| except BlockingIOError: | ||
| pass | ||
| if not said: | ||
| holder = path.read_text(errors="replace").strip() or "unknown" | ||
| print(f"waiting for {path.name}: held by {holder}", flush=True) | ||
| said = True | ||
| if time.monotonic() >= deadline: | ||
| os.close(fd) | ||
| return None | ||
| time.sleep(2) | ||
|
|
||
|
|
||
| def hold(fd: int, path: Path, worker: int | None, max_hold: float) -> None: | ||
| """Fork a holder that keeps fd until the worker exits or max_hold passes; the caller returns.""" | ||
| os.ftruncate(fd, 0) | ||
| os.pwrite(fd, f"run {os.environ.get('GITHUB_RUN_ID', '?')} on {os.environ.get('RUNNER_NAME', '?')}\n".encode(), 0) | ||
| if os.fork(): | ||
| os.close(fd) | ||
| return | ||
| os.setsid() | ||
| devnull = os.open(os.devnull, os.O_RDWR) | ||
| for target in (0, 1, 2): | ||
| os.dup2(devnull, target) | ||
| deadline = time.monotonic() + max_hold | ||
| while time.monotonic() < deadline and (worker is None or alive(worker)): | ||
| time.sleep(5) | ||
| os._exit(0) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | ||
| sub = parser.add_subparsers(dest="command", required=True) | ||
| held = sub.add_parser("hold") | ||
| held.add_argument("--tag", required=True) | ||
| held.add_argument("--lock-dir", type=Path, default=DEFAULT_LOCK_DIR) | ||
| held.add_argument("--wait", type=float, default=7200) | ||
| held.add_argument("--max-hold", type=float, default=3 * 3600) | ||
| args = parser.parse_args(argv) | ||
| if not re.fullmatch(r"[A-Za-z0-9._-]{1,100}", args.tag): | ||
| parser.error("--tag must be a simple tag name") | ||
| path = args.lock_dir / f"{args.tag}.lock" | ||
| fd = acquire(path, args.wait) | ||
| if fd is None: | ||
| print(f"::error::{path.name} was still held after {args.wait:.0f}s", flush=True) | ||
| return 1 | ||
| worker = worker_pid(os.getppid()) | ||
| if worker is None: | ||
| print(f"no {WORKER} above this step; the tag is held for at most {args.max_hold:.0f}s", flush=True) | ||
| hold(fd, path, worker, args.max_hold) | ||
| print(f"holding {path.name}", flush=True) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.