Pre-existing uncommitted work: ha-repl-hardening payload data plane (7.x), heartbeat liveness fix (Req 1.3), connection-reuse ASAN fixes, doc cleanup. New: ha-consistency-invariants umbrella (requirements/design/research) and its four implementation sub-specs (specs generated via batch, cross-spec review applied), steering roadmap + product.md Raft+CP decision.
5.1 KiB
API Standards
Philosophy
Two API surfaces: Go REST API (apiserver) and C daemon messaging (Cap'n Proto over TCP). Both follow a consistent pattern: validate at the boundary, delegate business logic, return structured responses. Non-blocking I/O throughout the daemon layer.
Go REST API (apiserver)
Handler Pattern
Every resource follows a single consistent pattern:
// {Resource}Controller struct
type JobController struct{}
// {Action}{Resource} handler method
func (j JobController) SubmitJob(ctx *gin.Context) {
// 1. Validate input at the boundary
req, err := preCheckSubmitJobRequest(ctx)
if err != nil {
ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil)
return
}
// 2. Delegate to service layer
result, err := j.svc.Submit(req)
if err != nil {
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
return
}
// 3. Return structured success
ReturnSuccess(ctx, common.SUCCESS_CODE, "submitted", result)
}
Rules:
- Early return on every error —
ReturnError(...); return - Keep
preCheck*functions in the same file as the handler - Controllers delegate to services — business logic never lives in handlers
- Dependencies flow inward:
controllers → services → store/backend
Response Format
Every HTTP response uses a consistent envelope via two helpers in controllers/response.go:
// Success (always HTTP 200)
ReturnSuccess(ctx, code, msg, data)
// → {code: 0, msg: "success", data: {...}}
// Error (HTTP status == code parameter)
ReturnError(ctx, code, msg, details)
// → {code: 401, msg: "access forbidden", details: nil}
Standard Error Codes
All codes defined in common/common.go:
| Code | Constant | Usage |
|---|---|---|
| 0 | SUCCESS_CODE |
All success responses |
| 400 | INVALID_PARAMS_CODE |
Malformed/missing input |
| 401 | ERROR_AUTH_CHECK_TOKEN_CODE |
Missing/invalid JWT |
| 403 | ERROR_AUTH_CODE |
Valid JWT, insufficient permission |
| 404 | ERROR_NOT_FOUND_CODE |
Resource not found |
| 429 | ERROR_TOO_MANY_REQUESTS_CODE |
Rate limit exceeded |
| 500 | ERROR_CODE |
Unexpected server error |
Middleware Chain
Order is defined in router/routers.go and is non-negotiable:
WhitelistURL → WhitelistIP → RateLimit → ValidateHandler (JWT) → CORS → RBAC
- WhitelistURL: filter by allowed paths (applied first for efficiency)
- WhitelistIP: abort 403 if client IP not in whitelist
- RateLimit: 10 req/sec, burst 20 (global config)
- ValidateHandler: parse and validate JWT from
Authorization: Bearerheader; validate body content-type - CORS: origin checks via config
- RBAC:
RequirePermission("action:scope")per-route, enforced bycontrollers/rbac_check.go
Permission Format
"action:scope" — scopes: all, self, cluster, hostgroup, self_ug, self_hg.
Defined in config/rbac.yaml. Roles support inheritance via includes:.
C Daemon Messaging
Cap'n Proto Wire Format
All inter-daemon messages are Cap'n Proto serialized. Schema: src/schema/controller.capnp.
Request (xce_request_t):
opcode— OPCode enum (38 values)seq— request sequence numberfrom— sender peer info (host/key)body— union of 27 message types
Reply (xce_reply_t):
opcode— alwaysOPCode_replyorOPCode_redirectstatus— error code (0 = success)seq— echoes request seqdata— union of reply payloads (clusterconf, jobid, stateReport, walackreplymsg, etc.)
Key OPCodes
| Group | OPCodes | Purpose |
|---|---|---|
| Control | jobsubmit (11), jobdispatch (8), jobcontrol (15) |
Job lifecycle |
| Cluster | hello (2), join (3), clusterconf (17), heartbeat (25) |
Membership and liveness |
| Replication | replConfig (29), replConfigPush (30), walCatchupEntry (37), snapshotTransfer (36) |
Config + WAL replication |
| Vote | requestVote, reply |
Raft election |
| Data | push (6), get (5), notify (10) |
Data exchange |
Daemon I/O Functions
// Enqueue request to endpoint send buffer (non-blocking, always)
int xce_request(endpoint_t *ep, xce_request_t *req);
// Enqueue reply to inbound request sender (non-blocking)
int xce_reply(endpoint_t *ep, xce_peerinfo_t *host, xce_reply_t *reply);
// Cap'n Proto serialize request to packet
int xce_encode(xce_request_t *req, io_packet_t *pkt);
// Cap'n Proto deserialize packet to request
int xce_decode(io_packet_t *pkt, xce_request_t *req);
msg_handler callback: Each endpoint has ep->msg_handler — a function pointer int (*)(void*) called by iomgr when a packet arrives. Handlers must return quickly (vtask context). Registered per-endpoint during connection setup.
Adding a New Opcode
- Add the opcode constant to
src/include/request.h(OPCode enum) - Add the message struct to
src/schema/controller.capnp(Request/Reply union arms) - Rebuild capnp schemas (automatic via meson)
- Wire the handler in the dispatch gate (
ha_dispatch_gateinha.c) - Add encode/decode paths in
src/lib/request.cif using raw payloads