Files
node/docker-compose.yml
T
beardthelion 803d83efdb fix(node): reap leaked git child processes (#53) (#61)
* fix(docker): reap orphan processes via tini + init, cap pids

The node runs as PID 1 in the container and never reaps reparented
orphans, so git grandchildren (e.g. pack-objects left behind when a
served clone disconnects) accumulate as zombies until fork() fails with
EAGAIN. Run under tini and set compose init: true so reparented orphans
are reaped, and add pids_limit as a blast-radius cap.

Refs #53

* fix(node): reap leaked git child processes (#53)

Serving git over smart-HTTP could leak zombie processes that, over weeks,
exhaust the PID table until fork() fails with EAGAIN and all git
operations break until restart.

The serve path (run_git_service) now runs git in its own process group and
SIGKILLs that group when the request future is dropped (client disconnect)
or returns early, so git and its pack-objects child are torn down together
instead of orphaning the grandchild to PID 1 as a zombie. A drop guard,
disarmed once the request completes cleanly, owns this teardown.

create_issue and build_filtered_pack spawned a std Child and dropped it
unwaited if the stdin write failed (and build_filtered_pack could panic on
a missing stdin handle). Both now always wait the child before surfacing
any error, so the failure path cannot leak.

Adds libc for the group-kill syscall and three regression tests covering
direct-child kill, grandchild reaping via the group, and the disarmed
no-kill path.

Refs #53

* fix(review): reap-then-disarm on serve-path errors; tini/comment nits

Address code-review findings on the #53 fix:

- run_git_service no longer early-returns on a stdin-write error. It captures
  the write result, always reaps the child, then disarms the group guard
  before surfacing the error. Previously a write/wait error path could drop
  the still-armed guard after the child was already reaped, so the group
  SIGKILL could in theory land on a reused pgid. (correctness, reliability,
  adversarial all flagged this region.)
- Drop tini's -g flag: the node manages its own children and graceful
  shutdown, so group-wide signalling would only disturb in-flight git helpers
  on shutdown.
- Correct the SAFETY comment (kill(2) is not relevant to async-signal-safety
  here) and the alive() test-helper comment (EPERM does not read as alive).

* fix(node): SIGTERM (not SIGKILL) the served-git group on disconnect

SIGKILL gave git no chance to run its signal cleanup, so an aborted
receive-pack could leave a stale .git/*.lock that blocks the next push.
SIGTERM lets git remove its lock files before exiting. Signalling the
whole process group still reaches pack-objects directly, so the grandchild
is terminated rather than orphaned, and a single immediate signal avoids
any delayed-kill pid-reuse window.

Refs #53

* docs(node): correct stale SIGKILL comments to SIGTERM in served-git teardown
2026-06-23 10:10:17 +08:00

68 lines
2.8 KiB
YAML

# Run a gitlawb node + Postgres locally. For on-chain PoS operation, supply
# the operator key + contract addresses via a .env file or shell environment.
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: gitlawb
POSTGRES_USER: gitlawb
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitlawb"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
node:
build: .
# Reap reparented orphan processes so git's grandchildren (e.g. pack-objects
# left behind when a served clone disconnects) can't accumulate as zombies
# and exhaust the PID table (#53). The image also runs under tini, so this is
# belt-and-suspenders for the quickstart.
init: true
# Cap the process table so a regression fails loudly instead of taking the
# host down. Sits well above normal concurrent git fan-out.
pids_limit: 1024
depends_on:
postgres:
condition: service_healthy
ports:
- "${GITLAWB_PORT:-7545}:${GITLAWB_PORT:-7545}" # HTTP API + git smart-HTTP
- "${GITLAWB_P2P_PORT:-7546}:${GITLAWB_P2P_PORT:-7546}/udp" # libp2p QUIC
volumes:
- gitlawb-data:/data
environment:
DATABASE_URL: postgresql://gitlawb:${POSTGRES_PASSWORD:-changeme}@postgres:5432/gitlawb
GITLAWB_HOST: 0.0.0.0
GITLAWB_PORT: ${GITLAWB_PORT:-7545}
GITLAWB_PUBLIC_URL: ${GITLAWB_PUBLIC_URL:-http://localhost:7545}
GITLAWB_P2P_PORT: ${GITLAWB_P2P_PORT:-7546}
GITLAWB_BOOTSTRAP_PEERS: ${GITLAWB_BOOTSTRAP_PEERS:-}
GITLAWB_BOOTSTRAP_DISABLE_SEEDS: ${GITLAWB_BOOTSTRAP_DISABLE_SEEDS:-false}
GITLAWB_REQUIRE_SIGNED_PEER_WRITES: ${GITLAWB_REQUIRE_SIGNED_PEER_WRITES:-false}
GITLAWB_P2P_BOOTSTRAP: ${GITLAWB_P2P_BOOTSTRAP:-}
GITLAWB_PUBLIC_READ: ${GITLAWB_PUBLIC_READ:-true}
GITLAWB_MAX_PACK_BYTES: ${GITLAWB_MAX_PACK_BYTES:-2147483648}
# Sync
GITLAWB_AUTO_SYNC: ${GITLAWB_AUTO_SYNC:-false}
# On-chain PoS (optional — leave unset for local/dev)
GITLAWB_CHAIN_RPC_URL: ${GITLAWB_CHAIN_RPC_URL:-}
GITLAWB_CONTRACT_NODE_STAKING: ${GITLAWB_CONTRACT_NODE_STAKING:-}
GITLAWB_OPERATOR_PRIVATE_KEY: ${GITLAWB_OPERATOR_PRIVATE_KEY:-}
GITLAWB_OPERATOR_STRICT_MODE: ${GITLAWB_OPERATOR_STRICT_MODE:-false}
GITLAWB_HEARTBEAT_INTERVAL_HOURS: ${GITLAWB_HEARTBEAT_INTERVAL_HOURS:-20}
# Optional shared storage (leave unset to use local disk only)
GITLAWB_TIGRIS_BUCKET: ${GITLAWB_TIGRIS_BUCKET:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
AWS_ENDPOINT_URL_S3: ${AWS_ENDPOINT_URL_S3:-}
restart: unless-stopped
volumes:
pg-data:
gitlawb-data: