myxce/.kiro/steering/tech.md
Rongsong Shen 6dd70f1091
feat(build): no fork in multi-threaded XCE processes, enforced by check-no-fork
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>
2026-10-10 13:35:52 +00:00

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.sh requires XCE + URL env vars.

Code Formatting

  • Go: gofmt / goimports (post-edit hooks configured)
  • C: clang-format using project config at xce/.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's uv_spawn() (a fork()+exec on 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 reaches exec.
  • Threaded processes: the controller, the scheduler, the xcecd daemon (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 with CLONE_VM|CLONE_VFORK, no copied user code), or a systemd unit over D-Bus as xcecd does for xcecd_worker. Setup that posix_spawn() cannot do (setuid, rlimits, cgroup join) belongs in a single-threaded process such as xcecd_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 from main(), callbacks included, reaches no pthread_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; its plug_mgr.c fork is tracked as a bug and must be fixed before it ships), and the Go frontend (os/exec is safe).
  • Enforcement: scripts/check-no-fork.py runs as the meson test no-fork rule (suites lib, controller, scheduler, worker, so /xce-build runs 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). Only config_file and pam (Linux + CGO) are functional; ldap and oauth2 are documented as future/stub providers whose Authenticate() always returns a "not yet implemented" error.
  • Permissions are a plain action string (e.g. "job:view", "queue:submit") paired with a separate structured domain list (e.g. {type: self}, {type: all}) in rbac.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 by controllers.RequirePermission() middleware.

Key Technical Decisions

  1. No Go workspace — apiserver and xcecli are independent modules; changes to shared types require updating both.
  2. 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() in cmd/main.go). Only RBAC (config/rbac.yaml) and auth (config/auth.yaml) are actually loaded from YAML at startup.
  3. CGO bridge — backend/ package wraps C library calls; unit tests run CGO_ENABLED=0, backend tests need CGO_ENABLED=1 inside nix dev shell.
  4. Swagger-first API docs — swaggo/swag annotations on handlers; docs served at runtime by Gin.
  5. Structured logging — every log entry carries context via zap.String(), zap.Error(). Never use fmt.Println for operational output.
  6. 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, AppendEntries request/reply, per-peer next_index[]/match_index[] progress vectors, (term,index) log matching/truncation, commit-index advance) implemented in the src/lib/raft/ shared module (raft.c/raft.h) and validated by the raft-log-replication, raft-commit-safety, raft-ha-core-safety, and ha-term-safety specs.