Iterated design review until clean (GO): - Controller main() calls mylog_fini() before every early return, so log lines written by tlog's background thread survive the exit. - cluster_init_phase1() keeps its candidates_count > 0 guard around the new xce_peers_find_identity() call (standalone stays my_priority 0). - xceadmin adds the "(from $VAR)" source note only to network errors, not to local cluster-bin file errors (OSError with filename set). - Regression script: scratch --workdir/--certdir/--logfile per daemon, refuses to run if a local cluster is up (port 9990, worker socket), --sim case asserts the sim branch ran, 2.3 patches follow_redirects, 2.8 malformed-entry cases use a readable cluster-bin file, wrapper test expects PEERS_STR form. - Two existing scripts that start xce_scheduler without --peers will unset XCE_CONTROLLER_PEERS; --interactive wording corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
Research & Design Decisions
Summary
- Feature:
controller-peers-env-fallback - Discovery Scope: Extension (existing CLI argument-parsing across 5 already-shipped binaries)
- Key Findings:
- Finding 1: harness/
xce_testdoes not take a--peersCLI flag at all — it reads amasterssetting (a{key, value, defval}table entry, set only via the interactive/scriptedset masters ...command or rc-file loading). This is architecturally different from controller/scheduler/worker and requires a different insertion point. - Finding 2: click's
envvar=parameter already accepts an ordered list of names and natively returns "the first non-empty value," treating an empty string exactly like unset — verified directly in the installed click library'sParameter.resolve_envvar_value(). This eliminates the need for any custom default-resolution code on the xceadmin side. - Finding 3:
xce_parse_peers()(src/lib/cluster.c) already strips an optionalname=prefix per comma-separated entry before parsinghost:port—xceadmin.discovery.resolve_controllers()does not. Porting the same, already-proven convention intoresolve_controllers()is a minimal, consistent, backward-compatible extension rather than a new parsing scheme.
- Finding 1: harness/
Research Log
Does --peers-omitted behavior differ by component, and is it safe to default it via an env var?
- Context: Requirement 1.3 requires that omitting
--peerswith no env var set behaves exactly as today; needed to confirm this "today" behavior is genuinely a supported mode (not an error) for all four components, and that an env-var-supplied default cannot unsafely interact with an unrelated mode. - Sources Consulted:
src/controller/main.c(~L403-429),src/scheduler/src/main.c(~L380-429),src/worker/src/main.c(~L389-475), buglog.json bug-1457. - Findings:
- controller:
if (arguments.peers_str) {...}— omitting it is controller's own supported single-node/standalone primary path (bug-1457 fixed a startup-ordering defect in this exact path in 2026-07; the path itself is intentional, not an error). - scheduler:
if (arguments.peers) {...} else { INFO("No --peers flag; using single-controller path, ...") }— same shape, explicitly logged as intentional. Critically, scheduler's--simmodereturns before this block is ever reached (main.c~L380-400) — an env-var-supplied peers default therefore cannot affect--simreplay mode (with or without--interactive) at all, by construction, with no special-casing needed. - worker:
if (peers_str) {...}— identical shape. - All three: an invalid
--peersvalue is a hard parse-error (xce_parse_peers() != 0→fprintf(stderr, ...); return 1;), never a silent fallback.
- controller:
- Implications: The env-var fallback only needs to be consulted at the single point each component already gates on
peers_str/peersbeing non-NULL — no new mode-interaction risk exists, and--simis unaffected without any extra guard.
harness/xce_test's actual connection-target mechanism
- Context: Requirements assumed harness took a
--peers-shaped CLI argument like the other three; needed to verify before designing its integration point. - Sources Consulted:
src/harness/test.c(settings[]~L265-273,get_setting()~L991-1005,enter_ctx()~L1007-1035),src/harness/CLAUDE.md. - Findings: harness has no
--peers/--masterstop-level CLI flag.settings[]is a static{key, value, defval}array; the"masters"entry shipsvalue=NULL, defval="localhost:9990".get_setting()returnsvalueif the operator has runset masters ...(interactively or via an rc-file, at any point beforeenter_ctx()'s connection attempt) — otherwise it falls through todefval.enter_ctx()parses whateverget_setting("masters")returns through the samexce_parse_peers()used everywhere else, and hard-errors on an invalid value. - Implications: harness's integration point is computing
defvalonce at process startup (before the REPL loop and rc-file loading) from the env var, not gating on a CLI argument. This preserves the existingvalue-before-defvalprecedence untouched — an explicitset masters ...(interactive or rc-file) still always wins over the env-var-derived default, satisfying the same "explicit override always wins" property the other three components have, just through a different existing seam.
click's multi-envvar precedence (xceadmin side)
- Context: Needed to confirm whether xceadmin's
--controlleroption can prioritizeXCE_CONTROLLER_PEERSover the existingXCEADMIN_CONTROLLERwithout custom callback code. - Sources Consulted: Installed click library,
click/core.py,Parameter.resolve_envvar_value()(read directly, not from memory). - Findings:
envvaracceptsstr | Sequence[str]. When given a sequence, click iterates in order and "return[s] the first non-empty value of the list" — an empty string is treated identically to the variable being absent. This is already exactly the "new var first, legacy var second, empty-or-unset falls through" precedence Requirement 2 (2.4-2.6) specifies. - Implications: No custom
default=/callback=wiring is needed. Each--controlleroption's existingenvvar="XCEADMIN_CONTROLLER"becomesenvvar=["XCE_CONTROLLER_PEERS", "XCEADMIN_CONTROLLER"]. This is strictly simpler than the two-separate-variable design considered during discovery.
Where to strip a name= prefix for xceadmin
- Context:
XCE_CONTROLLER_PEERSmay carryname=host:portentries (the daemons' native format);xceadmin.discovery.resolve_controllers()(bug-1821) does a plainpart.rsplit(":", 1)with no such handling, so a named entry would mis-parse as a literal unresolvable hostname. - Sources Consulted:
src/xceadmin/src/xceadmin/discovery.py(resolve_controllers(),follow_redirects_any()),src/lib/cluster.c(xce_parse_peers()'s ownname=stripping, for convention parity). - Findings:
xce_parse_peers()already strips everything before the first=in each entry, unconditionally, before its ownhost:portparse. No production hostname can legally contain=(not permitted in DNS names or IP literals), so this is unambiguous and safe. - Implications: Extend
resolve_controllers()itself with the same one-line convention, rather than adding a separate conversion function ahead of it. This keeps thename=-tolerant behavior available to any caller ofresolve_controllers()(not just the new env-var path) and mirrors an already-proven pattern instead of inventing a second one.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
Single shared C helper (xce_controller_peers_env()) in libxce, called individually by each daemon's existing --peers-gating if |
One pure function (read env var, treat empty as unset); each call site keeps its own existing control flow | Minimal surface, trivially unit-testable, no new abstraction | None identified — all 4 C targets already link libxce |
Selected |
Per-component duplicate getenv() calls, no shared helper |
Simplest possible per-file change | No shared bug surface to fix once | Duplicates the exact same 2-line logic 4 times; harder to keep the "empty string == unset" rule consistent if one copy drifts | Rejected — violates Simplification/DRY with zero offsetting benefit given the shared helper is trivial |
Custom click default=/callback= wrapper for xceadmin's --controller |
Full control over resolution order and transformation | None over the adopted option | Reinvents what click's own multi-envvar list already does natively |
Rejected once click's native behavior was confirmed (see Research Log) |
Extend resolve_controllers() to strip name= prefixes |
One-line change to an existing, already-tested function | Reuses existing, well-tested follow_redirects_any failover; no new function; benefits any future caller |
None identified | Selected |
Design Decisions
Decision: One shared C primitive, not a per-component copy or a --peers-value-transforming helper
- Context: controller/scheduler/worker each already gate on
peers_str/peersbeing non-NULL at one call site; harness has no such call site (see settings-table finding above). - Alternatives Considered:
- A helper taking
(cli_value)and returning the resolved value, changing each call site'sif (arguments.peers)toif ((resolved = xce_resolve_peers(arguments.peers))). - A bare
xce_controller_peers_env(void)returning the raw env value (or NULL), left for each call site to combine with its own local variable using its own existingif.
- A helper taking
- Selected Approach: Option 2 —
const char *xce_controller_peers_env(void)insrc/lib/cluster.c, declared insrc/include/xce/cluster.halongsidexce_parse_peers(). - Rationale: harness has no
cli_valueto pass in at all (its "explicit override" is a settings-table write, not a function argument), so a helper shaped around "given a CLI value, return the resolved one" doesn't fit uniformly across all 4 components. A bare env-reader fits every call site's own existing control flow without forcing a shape mismatch. - Trade-offs: Each of the 4 call sites writes its own 1-3 line combination (
arguments.peers_str ? arguments.peers_str : xce_controller_peers_env(), or, for harness, a startup-timedefvalcomputation) instead of one identical call everywhere — a small amount of repetition, accepted because the alternative (option 1) does not actually fit harness's shape.
Decision: Reuse click's native multi-envvar list instead of a custom default resolver
- Context: See Research Log.
- Alternatives Considered:
- A new
controller_peers_from_env()Python helper performingname=stripping, wired in via a customdefault=callable ahead of click's ownenvvar=chain. - Extend
resolve_controllers()to toleratename=prefixes directly, and add"XCE_CONTROLLER_PEERS"to each option's existingenvvar=list.
- A new
- Selected Approach: Option 2.
- Rationale: Click already implements exactly the required precedence (first non-empty of an ordered list, empty treated as unset) — reinventing it in a custom
default=callable would duplicate already-correct, already-tested library behavior for no benefit. - Trade-offs: None identified; strictly less code than the alternative.
Decision: Controller refuses an env-supplied peers list that lacks its own name (design review, 2026-10-02)
- Context: For the controller,
--peersis the Raft membership.src/controller/cluster.c~L1780-1791 findsmy_priority(which is also Raftself_idx) by matching the controller's name against each candidate'sname(orhostnamewhen unnamed), and falls back to the last index when nothing matches.cluster_open_listener()(~L1902) already refuses that no-match case before any network I/O, so Raft safety is not at risk, but the refusal blames "--peers", goes only to the log file, and the process exits with status 0 (main()ignoresmyapp_run()'s result). An inheritedXCE_CONTROLLER_PEERSmakes the case far more likely: a controller started without--myname(name = hostname), or one meant to run standalone. - Alternatives Considered:
- The controller ignores
XCE_CONTROLLER_PEERS; only scheduler, worker, harness and xceadmin read it. - The controller reads it, but refuses to start when its own name is not a candidate.
- Make the no-match case an error for explicit
--peerstoo.
- The controller ignores
- Selected Approach: Option 2, with the matching rule moved into a shared
xce_peers_find_identity()that both existingcluster.clookups also call, so the three cannot drift. - Rationale: Keeps one variable for every component (the brief's goal) and turns the most likely new misconfiguration into an immediate, clear, non-zero-exit error instead of a log-only refusal that blames the wrong source. Option 3 changes shipped explicit-flag behavior, which the requirements rule out; it is recorded as a follow-up in the design's Revalidation Triggers.
- Opt-out for one invocation (the brief's open question): unset the variable or set it empty for that run; no separate flag.
xce_controller_peers_env()already treats empty as unset.
Decision: Generated scripts/xceadmin wrapper exports XCE_CONTROLLER_PEERS (design review, 2026-10-02)
- Context:
deploy-ha.sh~L692 points the wrapper at its own deployment only throughXCEADMIN_CONTROLLER. Because the new variable outranks it, an operator withXCE_CONTROLLER_PEERSset for another cluster would see the wrapper reach the wrong controller (bug-1566's symptom), while the generated daemon scripts stay safe because they pass--peersexplicitly. - Alternatives Considered:
- Keep
deploy-ha.shuntouched and document the shadowing. - Add
export XCE_CONTROLLER_PEERS="$PEERS_STR"to the wrapper.
- Keep
- Selected Approach: Option 2. One line, and the wrapper gains failover across the deployment's full candidate list instead of only the first candidate.
- Trade-offs: Narrows the brief's "deploy-ha unchanged" scope decision to "generated daemon scripts unchanged".
Risks & Mitigations
- Risk: A future
--peers/--controller/mastersvalue could legitimately contain=in a way that collides with thename=stripping convention. Mitigation: Not possible in practice — DNS hostnames and IP literals cannot contain=; bothxce_parse_peers()(existing, proven) and the extendedresolve_controllers()share the identical, narrow "split on the first=" rule. - Risk: harness's
defvalmutation could be perceived as "changing settings without the operator asking," surprising asetcommand's own listing output. Mitigation:defvalhas always been the table's documented fallback slot (get_setting()already falls through to it); computing it from an explicit, documented environment variable at startup is more transparent than silently keeping a stale hardcoded literal while an env var the operator deliberately set is ignored.
References
src/lib/cluster.c—xce_parse_peers()(existingname=convention, reused).src/xceadmin/src/xceadmin/discovery.py—resolve_controllers()/follow_redirects_any()(bug-1821).src/xceadmin/src/xceadmin/cli.py— existing--controlleroption definitions (bug-1566).- click
core.py,Parameter.resolve_envvar_value()— multi-envvarprecedence, read directly from the installed library.