Design constraint (tech.md "Process Creation"): the controller, scheduler,
xcecd and anything that starts libxce threads never call fork, vfork,
forkpty, daemon, system, popen or uv_spawn; children start with posix_spawn()
or a systemd unit. A process proven single-threaded may fork, marked
/* xce-fork-ok: <why> */. Tests, harness, autoscaler (not finalized; bug-1969)
and the Go frontend are out of scope.
- scripts/check-no-fork.py scans src/{controller,scheduler,lib,worker}
(comment/string/#if 0 aware); runs as meson test "no-fork rule" in the
lib/controller/scheduler/worker suites and in the pre-commit hook.
- worker: drop xcecd's unused fork-based worker start paths
(process_manager_start_process, process_manager_start_worker; xcecd
starts xcecd_worker over D-Bus) and a dead #if 0 daemonize fork.
- xcecd_worker's fork/uv_spawn/system calls are marked: its call graph
from main(), callbacks included, reaches no pthread_create or libuv
thread-pool call.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
8.1 KiB
8.1 KiB
Technology Stack
Architecture
xcecli (Go/Cobra) ──REST──► apiserver (Go/Gin) ──CGO──► C daemon layer
controller / scheduler / worker / autoscaler
↑↑↑ all linked against libxce (src/lib/)
Request path inside apiserver: Gin Router → Middleware (WhitelistURL → WhitelistIP → RateLimit → JWT → CORS) → RBAC Check → Controller → Services → In-Memory Store
All controller state is in-memory. No persistent database in the API tier; job/queue/host state is pushed live into the in-memory stores from the C controller backend over CGO (see Key Technical Decision #2), not loaded from a file. C daemons communicate via TCP using a custom messaging layer: iomgr (epoll-based I/O loop), vtaskmgr (thread pool for packet dispatch), and xce_request/xce_reply for non-blocking send/receive. WAL + checkpoint provide persistence at the controller level.
Core Technologies
- Go 1.23.4 — apiserver (REST API) and xcecli (CLI). Two independent Go modules, no
go.work. - C (gnu17) — controller, worker, scheduler, autoscaler, libxce. No C++.
- Gin v1.10.0 — HTTP router, middleware, binding
- Cobra v1.9.1 — CLI framework for xcecli
- Zap v1.27.0 — structured logging throughout (both Go modules)
- Viper v1.19.0 / v1.20.1 — YAML config management
- Cap'n Proto — wire serialization for WAL and daemon-to-daemon replication
- iomgr / vtaskmgr / delaymgr — libxce async I/O framework: single-threaded epoll loop (iomgr), thread-pool packet dispatch (vtaskmgr), timer thread for periodic/blocking tasks (delaymgr)
Build Systems
| System | Scope |
|---|---|
| Meson ≥ 1.5 | C daemons + Go modules via tools/go_build wrapper — sole build/test path for apiserver/xcecli |
| Nix flakes | Hermetic dev environment, AppImage packaging |
# Full build (C + Go, debug + ASAN)
/xce-build
# Go development loop (apiserver + xcecli)
/xce-build apiserver # builds both binaries + runs unit tests
meson test -C <build_dir> --suite apiserver # CGO-backed backend tests
XCE=<build_dir>/frontend/xcecli/xcecli URL=http://localhost:8080 \
src/frontend/tests/regression/run.sh # regression suite
# Nix package builds (from src/)
nix build .#xcecore # C daemons + apiserver
nix build .#xcecore_debug # debug + ASAN
nix build .#xcecli # CLI only
Development Standards
Controller Pattern (Go)
// Struct: {Resource}Controller (typically an empty struct{})
type JobController struct{}
// Handler: {Action}{Resource}
func (j JobController) CreateJob(ctx *gin.Context) {
// Validate input
var req models.JobSubmitRequest
if err := utils.BindJSONStrict(ctx, &req); err != nil { ReturnError(ctx, code, err.Error(), nil); return }
parsedIndices, err := preCheckJobRequest(&req)
if err != nil { ReturnError(ctx, code, err.Error(), nil); return }
// Business logic — package-level services function, not a controller field
err = services.SubmitJob(&newJob, arraySize)
if err != nil { ReturnError(ctx, code, err.Error(), nil); return }
ReturnSuccess(ctx, code, "submitted", result)
}
Early return on every error. Log with zap fields before returning.
Testing Requirements
- TDD mandatory: Write failing test first, then implementation.
- Go unit tests: Table-driven, no external dependencies,
t.TempDir()for fixtures. - C tests: Add test case in
testsuite/before modifying implementation. /xce-build apiserver's unit-test step must pass without a live server.tests/regression/run.shrequiresXCE+URLenv vars.
Code Formatting
- Go:
gofmt/goimports(post-edit hooks configured) - C:
clang-formatusing project config atxce/.clang-format
Process Creation (no fork in multi-threaded processes)
- Rule: a multi-threaded XCE process never calls
fork(),vfork(),forkpty(),daemon(),system(),popen()or libuv'suv_spawn()(afork()+execon Linux).fork()copies only the calling thread, so a lock another thread held (malloc arena, logger mutex, stdio, ASAN) stays locked in the child, which then deadlocks before it reachesexec. - Threaded processes: the controller, the scheduler, the
xcecddaemon (xcecd,xcecd_virtual: thread pool, network, resource-sync and connector threads), and any process that starts libxce's delaymgr/netio/vtaskmgr threads (xce_ctx_start()). - How to start a child instead:
posix_spawn()/posix_spawnp()(glibc runs it withCLONE_VM|CLONE_VFORK, no copied user code), or a systemd unit over D-Bus asxcecddoes forxcecd_worker. Setup thatposix_spawn()cannot do (setuid, rlimits, cgroup join) belongs in a single-threaded process such asxcecd_worker, never in the threaded daemon. - Allowed:
exec*()without a fork (process replacement, e.g. the controller's HA self-restart); and a fork-family call in a process proven single-threaded, marked on the call line or the line above with/* xce-fork-ok: <why> */.xcecd_worker(the per-job process) is single-threaded: its call graph frommain(), callbacks included, reaches nopthread_create()and no libuv thread-pool call (uv_queue_work,uv_fs_*,uv_getaddrinfo); re-prove it before adding a thread there. - Scope: C code under
src/controller,src/scheduler,src/lib,src/worker. Excluded (decision 2026-10-10): test code (testsuite/,test*files; forking to isolate a test case is fine), the harness (src/harness,xce_test), the autoscaler (not finalized; itsplug_mgr.cfork is tracked as a bug and must be fixed before it ships), and the Go frontend (os/execis safe). - Enforcement:
scripts/check-no-fork.pyruns as the meson testno-fork rule(suites lib, controller, scheduler, worker, so/xce-buildruns it) and in the pre-commit hook on staged C files.
Auth and RBAC
- Auth backend set in
config/auth.yaml(provider: config_file | pam | ldap | oauth2). Onlyconfig_fileandpam(Linux + CGO) are functional;ldapandoauth2are documented as future/stub providers whoseAuthenticate()always returns a "not yet implemented" error. - Permissions are a plain action string (e.g.
"job:view","queue:submit") paired with a separate structureddomainlist (e.g.{type: self},{type: all}) inrbac.yaml— not a combined"action:scope"string.controllers.RequirePermission()checks the action string only; domain/scope is evaluated separately. - RBAC loaded from
config/rbac.yaml, enforced bycontrollers.RequirePermission()middleware.
Key Technical Decisions
- No Go workspace —
apiserverandxcecliare independent modules; changes to shared types require updating both. - In-memory API state — no database in Go tier;
store.InitStores()allocates empty per-resource stores at startup, and job/queue/host/etc. state is then pushed in live from the C controller backend over CGO (backend.RegisterToBackend()+backend.StartNotificationListener()incmd/main.go). Only RBAC (config/rbac.yaml) and auth (config/auth.yaml) are actually loaded from YAML at startup. - CGO bridge —
backend/package wraps C library calls; unit tests runCGO_ENABLED=0, backend tests needCGO_ENABLED=1inside nix dev shell. - Swagger-first API docs —
swaggo/swagannotations on handlers; docs served at runtime by Gin. - Structured logging — every log entry carries context via
zap.String(),zap.Error(). Never usefmt.Printlnfor operational output. - Single-writer HA with full Raft log replication — controller is always one active primary; standby election and log replication use a Raft protocol (term-based voting, freshness gates,
AppendEntriesrequest/reply, per-peernext_index[]/match_index[]progress vectors,(term,index)log matching/truncation, commit-index advance) implemented in thesrc/lib/raft/shared module (raft.c/raft.h) and validated by theraft-log-replication,raft-commit-safety,raft-ha-core-safety, andha-term-safetyspecs.