-
Notifications
You must be signed in to change notification settings - Fork 288
fix(opal-server): git resilience — never stuck on an offline repo (PR3) #924
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
base: master
Are you sure you want to change the base?
Changes from all commits
245f80b
6abbc2a
ed86a9c
c043e88
5dd53a2
382cd83
f778edc
c353ceb
bad21c1
d31a0b6
f4c54f1
904bf7d
4e2cbfe
4c0dc7e
c2d8328
a583537
311e292
02d9465
00715dd
02b4bf2
08081ef
d5868d6
705cac1
4beb296
a15b1ef
43236aa
78975e4
83dacd2
3cf9d5e
569595f
86f97c3
84afe1d
ae8cd92
f9b170a
d304232
208a404
7a595d6
a6cc9d2
81f8507
d577103
85dc358
0c52dd8
2c02c53
c3eb19b
842a098
d610a60
8400a75
d2ad045
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 05 — OPAL Config Reference (private) | ||
|
|
||
| Private, internal supplement to the public operator docs under `documentation/docs/`. Tracks | ||
| `OPAL_*` env vars added or clarified by the OPAL Server Git Fixes work, with the declaring | ||
| `file:line` so contributors can jump straight to the `Confi` declaration. Each key maps to an | ||
| `OPAL_<NAME>` env var (the `OPAL_` prefix is added once by the component's `Confi(prefix="OPAL_")` | ||
| instantiation — the bare name is what appears in the table). | ||
|
|
||
| ## 4. opal-server keys | ||
|
|
||
| | Env var | Type | Default | Purpose | Declared at | | ||
| |---|---|---|---|---| | ||
| | `OPAL_SCOPES_GIT_FETCH_TIMEOUT` | float (seconds) | `120.0` | Hard timeout for a single scope git clone/fetch. On timeout the operation is logged and skipped (retried next cycle), so one unreachable repo can never block boot or other scopes *indefinitely*. `0` = no timeout. | `packages/opal-server/opal_server/config.py:214-221` | | ||
| | `OPAL_SCOPES_GIT_MAX_WORKERS` | int | `10` | Bounds how many scope git operations (clone/fetch) may be *LIVE* (not yet timed out) at once, via an `asyncio.Semaphore` — there is no shared thread pool. Each operation still gets its own single-use daemon-thread executor, so a timed-out operation releases its capacity slot immediately rather than waiting for its thread to die; also bounds how many scopes are synced concurrently. | `packages/opal-server/opal_server/config.py:222-226` | | ||
| | `OPAL_SCOPES_PURGE_CHANNEL` | str | `__opal_scope_purge__` | fleet-wide scope purge channel | `packages/opal-server/opal_server/config.py:311-318` | | ||
|
|
||
| > **Caveat (timeout is soft, not a hard kill).** `OPAL_SCOPES_GIT_FETCH_TIMEOUT` is enforced via | ||
| > `asyncio.wait`, which unblocks the event loop and the awaiting coroutine — but the underlying | ||
| > pygit2 call keeps running on its own private daemon-thread executor until the OS network timeout | ||
| > fires (a "zombie"). There is no shared pool for these zombies to exhaust: each git operation gets a | ||
| > single-use `_DaemonThreadPoolExecutor(max_workers=1)`, so a lingering op occupies only its own | ||
| > thread and never holds capacity another operation needs. A per-repo in-flight guard | ||
| > (`git_op_in_flight`) prevents a second git op from touching the same (non-thread-safe) pygit2 repo | ||
| > while the first is still lingering. Hard-kill via subprocess is out of scope. See spec §6. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LOW] Dangling spec reference and two off-by-one Problem: this line ends "See spec §6", but no such file exists on this branch: Separately, the doc's stated purpose is letting a contributor jump straight to the Suggestion: land or drop the referenced spec/deep-dive files, and cite symbol names rather than line ranges — the ranges go stale on the next edit to config.py. |
||
| > | ||
| > **Boot / concurrency.** Scope syncs run concurrently, bounded by `OPAL_SCOPES_GIT_MAX_WORKERS` via | ||
| > an asyncio semaphore that gates only LIVE (not-yet-timed-out) operations. On timeout, the awaiting | ||
| > coroutine unblocks *and* its semaphore slot is released immediately — before the zombie thread | ||
| > returns — so a queued op waits for a freed **slot**, not a freed thread. That makes | ||
| > `ceil(offline / workers) × timeout` the correct worst-case bound for how long a healthy scope waits | ||
| > before it can start; `OPAL_SCOPES_GIT_FETCH_TIMEOUT` should be set well below any deployment's | ||
| > acceptable serve latency. What the worker count does *not* bound is the zombie count: with `N` | ||
| > sources unreachable and `M` live slots, worst-case thread count during an outage is `M` (live) plus | ||
| > up to `N` (lingering zombies, each alive until its own OS/TCP network timeout — which can far exceed | ||
| > `OPAL_SCOPES_GIT_FETCH_TIMEOUT`). (This bears on the `app-tests/git-leak` offline test, which runs | ||
| > 40 offline repos against the default 10 workers.) | ||
| > | ||
| > **What this actually guarantees.** (1) **event-loop isolation** — the HTTP surface and bundle | ||
| > serving run on the loop's default executor, never a git operation's thread, so they never block on a | ||
| > hung repo; (2) a **bounded per-slot stall** — each sync's coroutine waits at most | ||
| > `OPAL_SCOPES_GIT_FETCH_TIMEOUT` before the event loop moves on and its capacity slot is freed for | ||
| > the next queued operation; and (3) **daemon threads** — every operation's private executor thread is | ||
| > a daemon thread, so a lingering zombie never blocks process shutdown, however many are outstanding. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -211,6 +211,20 @@ class OpalServerConfig(Confi): | |
| 0, | ||
| description="The timeout for cloning the policy repository (0 means wait forever)", | ||
| ) | ||
| SCOPES_GIT_FETCH_TIMEOUT = confi.float( | ||
| "SCOPES_GIT_FETCH_TIMEOUT", | ||
| 120.0, | ||
| description="Soft timeout in seconds for a single scope git clone/fetch " | ||
| "(the awaiting operation is abandoned, but the underlying git call keeps " | ||
| "running on its thread until the OS network timeout). On timeout the " | ||
| "operation is logged and skipped (retried next cycle), so one unreachable " | ||
| "repo can never block boot or other scopes indefinitely (0 = no timeout).", | ||
| ) | ||
| SCOPES_GIT_MAX_WORKERS = confi.int( | ||
| "SCOPES_GIT_MAX_WORKERS", | ||
| 10, | ||
| description="Maximum number of LIVE scope git operations (clone/fetch) running concurrently; also bounds how many scopes are synced at once. A timed-out operation stops counting against this limit (its lingering thread persists on its own until the OS network timeout), so capacity is never starved by hung remotes — but worst-case thread count during an outage is this limit plus the number of lingering timed-out operations.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LOW] Description is a single 438-character line Problem: Suggestion: wrap it the same way the adjacent key is wrapped, and move the multi-clause caveat about worst-case thread count into a code comment next to the declaration. |
||
| ) | ||
| LEADER_LOCK_FILE_PATH = confi.str( | ||
| "LEADER_LOCK_FILE_PATH", | ||
| "/tmp/opal_server_leader.lock", | ||
|
|
@@ -295,6 +309,14 @@ class OpalServerConfig(Confi): | |
| 20, | ||
| description="Timeout for forgetting a server from which a keep-alive haven't been seen (keep-alive frequency would be half of this value)", | ||
| ) | ||
| SCOPES_PURGE_CHANNEL = confi.str( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LOW] Problem: Setting Suggestion: add |
||
| "SCOPES_PURGE_CHANNEL", | ||
| "__opal_scope_purge__", | ||
| description="Pub/sub channel (worker-to-worker, over the broadcaster) used to " | ||
| "purge GitPolicyFetcher caches fleet-wide when a scope is deleted, repointed " | ||
| "to a new source, or its clone dir is reclaimed as an orphan. Every worker " | ||
| "subscribes; the leader additionally removes the clone dir.", | ||
| ) | ||
|
|
||
| # Data updates | ||
| ALL_DATA_TOPIC = confi.str( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM] A doc that declares itself private is committed to the OSS repo, and it is the only place the new keys are documented
Problem: this file's own opening lines call it a "private, internal supplement to the public operator docs".
.claude/has never been tracked in this repository —git ls-tree -r origin/mastermatches nothing under.claude, and the only commits touching.claude/plansare on this branch.At the same time it is the only documentation the three new keys get.
documentation/docs/getting-started/configuration.mdxis the public per-key reference (169#### OPAL_entries, includingOPAL_SCOPES_REPO_CLONES_SHARDSat :476 andOPAL_POLICY_REPO_CLONE_TIMEOUTat :720) and gains none ofOPAL_SCOPES_GIT_FETCH_TIMEOUT,OPAL_SCOPES_GIT_MAX_WORKERS,OPAL_SCOPES_PURGE_CHANNEL. Operators reading the published docs cannot discover any of the new knobs — including the one that changes worst-case boot behaviour.Suggestion: add the three keys to
configuration.mdxnext to the otherOPAL_SCOPES*entries, and either drop this file from the commit or keep.claude/untracked (moving any engineering rationale worth publishing into the public docs or code comments).