diff --git a/.agents/rules/code-style.md b/.agents/rules/code-style.md new file mode 100644 index 0000000..ea09168 --- /dev/null +++ b/.agents/rules/code-style.md @@ -0,0 +1,80 @@ +# Code Style Rules + +## General Principles + +- Follow standard Go conventions and idioms +- Keep code simple, readable, and maintainable +- Prefer composition over inheritance +- Use short, descriptive variable names in small scopes + +## Formatting + +- Use `go fmt` for all code formatting (enforced via `make fmt`) +- Use tabs for indentation (Go standard) +- Keep line length reasonable (no hard limit, but use good judgment) + +## Naming Conventions + +### Variables +- Use short names in small scopes: `ctx`, `g`, `fn`, `err` +- Use descriptive names for package-level variables and types +- Boolean variables should read naturally: `started()`, `setup()` + +### Functions +- Private helper functions: lowercase (e.g., `signalCtx`, `block`) +- Public API functions: PascalCase (e.g., `NewSignals`, `Liveness`) +- Function types: suffix with `Fn` (e.g., `runFn`) + +### Types +- Exported types: PascalCase (e.g., `Group`) +- Private types: camelCase (rare in this codebase) + +## Package Organization + +- Keep related functionality in the same file +- File naming should reflect the main type or concept: + - `group.go` - Group type and lifecycle + - `probes.go` - Health check handlers + - `signal.go` - Signal handling utilities +- Test files mirror source files: `group_test.go`, `probes_test.go` + +## Error Handling + +- Return errors, don't panic (unless truly exceptional) +- Don't ignore errors in production code +- In tests, use `t.Error()` or `t.Fatal()` appropriately +- Error returns should be the last return value + +## Context Usage + +- Always accept `context.Context` as the first parameter +- Use context for cancellation and deadline propagation +- Don't store contexts in structs (store cancel functions if needed) +- Check `ctx.Done()` in long-running operations + +## Concurrency + +- Use channels for coordination: `startedCh`, `setupCh` +- Use `sync.WaitGroup` or `errgroup.Group` for goroutine management +- Document goroutine lifecycle and ownership +- Prefer `errgroup.WithContext` for concurrent error handling + +## Comments + +- Document all exported functions, types, and methods +- Keep comments concise and up-to-date +- Use godoc conventions (package comment, function comments) +- Don't comment obvious code + +## Testing Conventions + +See `.agents/rules/testing.md` for detailed testing guidelines. + +## Linting + +All code must pass: +- `go vet ./...` - Standard Go static analysis +- `golangci-lint run` - Additional linters +- `go mod tidy` - Module dependencies are clean + +Run `make lint` to check, `make fmt` to auto-fix issues. diff --git a/.agents/rules/security.md b/.agents/rules/security.md new file mode 100644 index 0000000..c74c5d0 --- /dev/null +++ b/.agents/rules/security.md @@ -0,0 +1,176 @@ +# Security Rules + +## General Security Principles + +- This is a library package with no network exposure or secrets management +- Security concerns focus on safe API design and dependency management +- Follow secure Go coding practices + +## Context Security + +### Proper Context Cancellation +- Always respect context cancellation to prevent resource leaks +- Check `ctx.Done()` in long-running operations +- Never ignore context cancellation signals + +```go +// Good: Respects cancellation +func process(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil // Clean shutdown + default: + // Do work + } + } +} +``` + +### Context Storage +- Never store contexts in structs (Go best practice) +- Pass contexts as function parameters +- Store cancel functions if needed, not contexts themselves + +## Goroutine Safety + +### Prevent Goroutine Leaks +- Every goroutine must have a clear termination path +- Use contexts for goroutine lifecycle management +- Always provide cleanup mechanisms + +```go +// Good: Goroutine respects context +go func() { + <-ctx.Done() + _ = srv.Close() // Cleanup +}() +``` + +### Race Conditions +- Run tests with `-race` flag (enforced in Makefile) +- Protect shared state with proper synchronization +- Use channels for goroutine communication + +## Signal Handling + +### Safe Signal Processing +- Signal handlers must not panic +- Clean up resources before exit +- Use `signal.Stop()` to prevent leaks + +```go +defer signal.Stop(c) // Always cleanup signal notifications +``` + +### Signal Channel Buffering +```go +c := make(chan os.Signal, len(sig)) // Buffered to prevent blocking +signal.Notify(c, sig...) +``` + +## Error Handling Security + +### Don't Leak Sensitive Information +- Error messages should be descriptive but not expose internals +- In this library, errors are returned as-is (no sensitive data) +- Downstream users should wrap errors appropriately + +### Fail Safely +- Default to stopping the service group if any component fails +- Don't continue processing if setup fails +- Ensure cleanup happens even on error paths + +## Dependency Security + +### Minimal Dependencies +- Only one external dependency: `golang.org/x/sync/errgroup` +- Review dependency updates carefully +- Keep `go.mod` and `go.sum` in sync + +### Dependency Updates +```bash +# Check for updates +go list -u -m all + +# Update dependencies +go get -u ./... +go mod tidy + +# Verify go.sum +go mod verify +``` + +## HTTP Probe Security + +### Probe Endpoints +- Liveness and readiness probes expose minimal information +- Only return HTTP status codes (200 or 503) +- No request body parsing or authentication needed +- Suitable for exposure to orchestration systems + +### No Sensitive Data in Probes +```go +// Good: Only status code, no details +func (g Group) Liveness() http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if !g.started() { + w.WriteHeader(http.StatusServiceUnavailable) + } + } +} +``` + +## Testing Security + +### Cleanup in Tests +- Always cleanup resources (contexts, goroutines, processes) +- Use `defer` for cleanup to ensure it happens +- Kill test processes properly to avoid zombies + +```go +defer func() { _ = cmd.Process.Kill() }() +``` + +### Test Isolation +- Each test should be independent +- Don't rely on test execution order +- Clean up global state (signal handlers, etc.) + +## Production Usage Security + +### Library Consumer Responsibilities +When using this library, consumers should: +- Not expose probe endpoints publicly without authentication +- Handle secrets and configuration outside this library +- Implement proper logging without leaking sensitive data +- Use TLS for production HTTP servers + +### Example Server Security +The `examples/server/main.go` is for demonstration only: +- No authentication or authorization +- No TLS +- Listens on all interfaces by default +- **Not production-ready without hardening** + +## Resource Exhaustion Prevention + +### Bounded Operations +- Use contexts with timeouts for bounded operations +- Don't create unbounded goroutines +- errgroup limits concurrent operations naturally + +### Graceful Degradation +- Service group stops all components if one fails +- Prevents partial operation states +- Clean shutdown on errors + +## Code Review Checklist + +When reviewing changes: +- [ ] Are contexts propagated correctly? +- [ ] Are goroutines guaranteed to terminate? +- [ ] Is the race detector passing? +- [ ] Are resources cleaned up on all paths? +- [ ] Do error messages avoid leaking sensitive data? +- [ ] Are dependencies necessary and up-to-date? diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md new file mode 100644 index 0000000..406dac4 --- /dev/null +++ b/.agents/rules/testing.md @@ -0,0 +1,207 @@ +# Testing Rules + +## Test Philosophy + +- Tests should be fast, deterministic, and isolated +- Use table-driven tests for multiple similar cases +- Test both success and failure paths +- Integration tests verify real binary behavior + +## Test Organization + +### Unit Tests +- Located alongside source: `group_test.go`, `probes_test.go` +- Test package naming: `package service_test` (black-box testing) +- Each test file tests its corresponding source file + +### System Tests +- Located in `system/` directory: `system/system_test.go` +- Test compiled binary behavior (integration tests) +- Verify signal handling and HTTP endpoints work end-to-end + +## Running Tests + +```bash +# All tests (lint + unit + system) +make test + +# Unit tests only +make test-unit + +# System tests only +make test-system +``` + +All tests run with: +- Race detector enabled (`-race`) +- 1-minute timeout (`-timeout 1m`) + +## Test Naming + +### Test Functions +- Start with `Test`: `TestGroup`, `TestProbes`, `TestBinary` +- Use subtests for variations: `t.Run("success", func(t *testing.T) {...})` +- Describe what is being tested, not implementation details + +### Helper Functions +- Use descriptive names: `block`, `verifyStatusCode`, `mustGetStatus` +- Mark test helpers with `t.Helper()` +- Keep helpers in the same file or `_test.go` file + +## Test Structure + +### Subtests Pattern +```go +func TestGroup(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Test happy path + }) + + t.Run("setup before register", func(t *testing.T) { + // Test specific behavior + }) +} +``` + +### Cleanup Pattern +```go +ctx, cancel := context.WithCancel(context.Background()) +defer cancel() // Clean up resources + +// Or use Close() method +defer g.Close() +``` + +## Assertions + +- Use clear, descriptive error messages +- Include expected vs actual values: `"expected %d, got: %d"` +- Use `t.Helper()` in assertion functions +- Prefer `t.Error()` for non-fatal assertions +- Use `t.Fatal()` for setup failures that prevent test continuation + +## Testing Concurrency + +### Channels for Synchronization +```go +done := make(chan struct{}) +go func() { + defer close(done) + // Async work +}() + +select { +case <-done: + // Success +case <-time.After(timeout): + t.Fatal("timeout") +} +``` + +### Testing Blocking Behavior +```go +select { +case <-done: + t.Error("expected not done") +default: + // Still blocking, as expected +} +``` + +### Eventually Pattern +For asynchronous operations that need time to settle: +```go +func eventually(t *testing.T, attempts int, f func() bool) { + t.Helper() + for i := 0; i < attempts; i++ { + if f() { + return + } + <-time.After(500 * time.Millisecond * time.Duration(i+1)) + } + t.Fatal("expected success") +} +``` + +## System Tests + +### Binary Compilation +- System tests require building `examples/server` first +- Build target: `build/server` +- Command: `make build-test` + +### Testing Binaries +```go +cmd := exec.Command("../build/server") +cmd.Env = []string{"EXAMPLE_HTTP_ADDR=:4444"} +if err := cmd.Start(); err != nil { + t.Fatalf("starting command: %v", err) +} +defer cmd.Process.Kill() +``` + +### HTTP Endpoint Testing +Use retry logic for services that need startup time: +```go +func mustGetStatus(t *testing.T, url string, attempts int, status int) { + t.Helper() + var got int + for i := 0; i < attempts; i++ { + resp, err := http.Get(url) + if err == nil && resp.StatusCode == status { + return + } + if err == nil { + got = resp.StatusCode + } + <-time.After(time.Duration(500*(i+1)) * time.Millisecond) + } + t.Errorf("url: %q, expected: %d, got: %d", url, status, got) +} +``` + +## Race Detector + +- Always enabled via `-race` flag +- Catches data races in concurrent code +- May slow tests significantly (acceptable trade-off) +- Fix all race conditions immediately + +## Test Coverage + +- Aim for high coverage of public API +- Test error paths and edge cases +- Don't obsess over 100% coverage +- Focus on testing behavior, not implementation + +## Common Patterns + +### Testing Probes +```go +// Verify status code changes over time +verifyStatusCode(t, "live", g.Liveness(), http.StatusServiceUnavailable) +go func() { _ = g.Start() }() +verifyStatusCode(t, "live", g.Liveness(), http.StatusOK) +``` + +### Testing Context Cancellation +```go +ctx, cancel := context.WithCancel(context.Background()) +g := service.NewCtx(ctx) +g.Register(func(ctx context.Context) error { + <-ctx.Done() + return nil +}) + +cancel() // Trigger shutdown +if err := g.Start(); err != nil { + t.Error(err) +} +``` + +## Avoiding Flaky Tests + +- Use proper synchronization (channels, contexts) +- Avoid hardcoded sleeps (use retry loops instead) +- Make timeouts generous enough for CI environments +- Test cleanup should always happen (use defer) diff --git a/.gitignore b/.gitignore index 567609b..e2a330a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ build/ +CLAUDE.local.md +.claude/settings.local.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ce121e3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,142 @@ +# AGENTS.md + +Single source of truth for AI agents working on this repository. + +## Project Summary + +`service` is a lightweight Go library that simplifies running and managing Go services. It provides a group-based lifecycle management system with built-in signal handling, graceful shutdown, and health check probes (liveness and readiness). + +The library enables developers to: +- Register multiple service components that run concurrently +- Define setup functions that run before service components start +- Handle OS signals (SIGINT, SIGKILL) for graceful shutdown +- Expose HTTP liveness and readiness probes for orchestration systems +- Automatically stop all components if any one fails + +## Tech Stack + +| Category | Technology | +|----------|-----------| +| Language | Go 1.13+ | +| Concurrency | `golang.org/x/sync/errgroup` | +| Testing | Go standard testing, race detector | +| Build | Make, Go toolchain | +| Linting | `golangci-lint`, `go vet` | + +## Repository Layout + +``` +service/ +├── group.go # Core Group type and lifecycle management +├── signal.go # OS signal handling utilities +├── probes.go # HTTP liveness and readiness handlers +├── group_test.go # Unit tests for Group +├── probes_test.go # Unit tests for probes +├── examples/ +│ └── server/ +│ └── main.go # Example HTTP server implementation +├── system/ +│ └── system_test.go # Integration tests against compiled binary +├── go.mod # Go module definition +├── Makefile # Build and test commands +├── README.md # User-facing documentation +└── .gitignore # Git ignore rules +``` + +## Key Entry Points + +### Public API (`service` package) + +**Core Types:** +- `Group` - Main service orchestration type +- `NewSignals(sig ...os.Signal) *Group` - Create group with signal handling +- `NewCtx(ctx context.Context) *Group` - Create group with custom context + +**Lifecycle Methods:** +- `Setup(fn func(ctx context.Context) error)` - Register setup function (runs before processes) +- `Register(fn func(ctx context.Context) error)` - Register a service process +- `Start() error` - Start all setup functions, then all registered processes +- `Close() error` - Cancel context to trigger graceful shutdown + +**Health Probes:** +- `Liveness() http.HandlerFunc` - Returns 503 until group started +- `Readiness() http.HandlerFunc` - Returns 503 until setup complete and group started + +### Example Usage + +See `examples/server/main.go` for a complete working example of an HTTP server with liveness/readiness probes. + +## Development Workflow + +### Setup +```bash +# Install dependencies +go mod download + +# Install golangci-lint (required for linting) +# See: https://golangci-lint.run/usage/install/ +``` + +### Testing +```bash +# Run all tests (lint + unit + system tests) +make test + +# Run only unit tests +make test-unit + +# Run only system tests (requires building example binary first) +make test-system + +# Build example server binary +make build-test +``` + +### Linting and Formatting +```bash +# Run linters (go vet + golangci-lint + go mod tidy) +make lint + +# Auto-fix linting issues and format code +make fmt +``` + +### Running Tests Manually +```bash +# Run tests with race detector +go test -race -timeout 1m ./... + +# Run tests excluding system tests +go test -race -timeout 1m $(go list ./... | grep -v /system) + +# Run only system tests +go test -race -timeout 1m ./system +``` + +## Agent Rules Reference + +This repository follows specific coding conventions, testing practices, and security guidelines: + +- **Code Style**: See `.agents/rules/code-style.md` +- **Testing**: See `.agents/rules/testing.md` +- **Security**: See `.agents/rules/security.md` + +## Architecture + +For detailed architecture documentation including sequence diagrams and component relationships, see `docs/ARCHITECTURE.md`. + +## Updating This File + +When making changes that affect repository structure, workflows, or core concepts: + +1. Update the relevant section in this file +2. If adding new agent rules, create a new file in `.agents/rules/` +3. Update `CLAUDE.md` if new rule files are added +4. Keep this file as the single source of truth - avoid duplicating information in README.md + +This file should be updated when: +- New major features are added +- Directory structure changes +- Development workflow changes +- Dependencies are added or significantly updated +- New conventions or rules are established diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dcdfe7a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,12 @@ +@AGENTS.md +@.agents/rules/code-style.md +@.agents/rules/testing.md +@.agents/rules/security.md +@docs/ARCHITECTURE.md + + +### Response Preferences +- Be concise. Prefer code over prose. +- When uncertain about architecture, read `docs/ARCHITECTURE.md` before assuming. +- When a change would invalidate any section of `AGENTS.md` or `README.md`, + flag it and offer to update them per the instructions in AGENTS.md. diff --git a/README.md b/README.md index db46a03..180cf23 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,181 @@ -## service +# service -_make it easy to run go services_ +A lightweight Go library for managing service lifecycles with built-in signal handling, graceful shutdown, and health check probes. -### How it works +## Features -The service groups allows registration of the different parts of a service. -These components are run in a group, if any one them fails, the group is stopped. +- **Simple API**: Register setup and service functions with minimal boilerplate +- **Graceful Shutdown**: Automatic context cancellation on OS signals or errors +- **Health Probes**: Built-in HTTP liveness and readiness handlers for orchestration systems +- **Concurrent Execution**: Run multiple service components concurrently with `errgroup` +- **Two-Phase Initialization**: Setup phase runs before service components start + +## Installation + +```bash +go get github.com/Attest/service +``` + +## Quick Start ```go -group := service.NewSignals(os.Interrupt) +package main + +import ( + "context" + "log" + "net/http" + "os" + + "github.com/Attest/service" +) + +func main() { + // Create a service group that handles SIGINT + group := service.NewSignals(os.Interrupt) + + // Register setup functions (run sequentially before processes start) + group.Setup(func(ctx context.Context) error { + log.Println("Initializing database connection...") + // Setup code here + return nil + }) + + // Register service processes (run concurrently) + group.Register(func(ctx context.Context) error { + srv := &http.Server{Addr: ":8080"} + + // Graceful shutdown on context cancellation + go func() { + <-ctx.Done() + srv.Close() + }() + + return srv.ListenAndServe() + }) + + // Start the service group + log.Fatal(group.Start()) +} +``` + +## Health Check Probes + +The service group exposes HTTP handlers for liveness and readiness probes: + +```go +http.HandleFunc("/live", group.Liveness()) +http.HandleFunc("/ready", group.Readiness()) +``` + +### Probe Behavior + +**Liveness Probe** (`/live`): +- Returns `503 Service Unavailable` until the group has started +- Returns `200 OK` once `Start()` is called +- Indicates the service process is alive + +**Readiness Probe** (`/ready`): +- Returns `503 Service Unavailable` until setup is complete +- Returns `200 OK` once all setup functions have succeeded and processes are running +- Indicates the service is ready to accept traffic + +## API Reference + +### Creating a Group + +```go +// Create group with OS signal handling +group := service.NewSignals(os.Interrupt, os.Kill) + +// Create group with custom context +ctx := context.WithTimeout(context.Background(), 30*time.Second) +group := service.NewCtx(ctx) +``` -group.Setup(func(ctx context.Context) error) { - // register a setup function. - // all setup functions are run before registered functions. - // if it returns an error, the group is stopped. +### Registering Functions + +```go +// Setup: runs sequentially before processes start +group.Setup(func(ctx context.Context) error { + // Initialization logic + return nil }) +// Register: runs concurrently after setup completes group.Register(func(ctx context.Context) error { - // register a function that accepts a context - // if it returns an error, the group is stopped + // Service logic + <-ctx.Done() // Wait for cancellation + return nil }) +``` -// the group exposes two http probes -// liveness reports service unavailable until all the setup functions have completed -// readiness reports service unavailable until all the registered functions have started -group.Liveness() -group.Readiness() +### Starting and Stopping -group.Register(httpServer()) +```go +// Start the group (blocks until all processes complete) +if err := group.Start(); err != nil { + log.Fatal(err) +} -// run the service -log.Fatal(group.Start()) +// Manually trigger shutdown +if err := group.Close(); err != nil { + log.Fatal(err) +} ``` + +## How It Works + +1. **Setup Phase**: All registered setup functions run sequentially. If any fails, the group stops immediately. +2. **Process Phase**: All registered processes run concurrently using `errgroup`. If any process returns an error, the context is canceled and all processes are stopped. +3. **Shutdown**: When the context is canceled (via signal, error, or manual close), all processes receive the cancellation signal and should clean up gracefully. + +## Examples + +See the [examples/server](examples/server/main.go) directory for a complete working example of an HTTP server with health check probes. + +## Development + +### Requirements + +- Go 1.13 or later +- `golangci-lint` for linting + +### Running Tests + +```bash +# Run all tests (lint + unit + system) +make test + +# Run only unit tests +make test-unit + +# Run only system tests +make test-system +``` + +### Code Formatting + +```bash +# Check linting +make lint + +# Auto-fix and format code +make fmt +``` + +## Architecture + +For detailed architecture documentation including sequence diagrams and design decisions, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). + +## Contributing + +This repository is maintained by [@Attest/audience-accounts](CODEOWNERS). + +For AI agents and automated tools: +- See [AGENTS.md](AGENTS.md) for comprehensive repository documentation +- See [CLAUDE.md](CLAUDE.md) for Claude-specific instructions + +## License + +Copyright © Attest diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c56e33e --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,357 @@ +# Architecture + +## Overview + +The `service` package provides a lightweight orchestration layer for Go services. It manages the lifecycle of multiple concurrent components with support for graceful shutdown, setup phases, and health check probes. + +## Core Concepts + +### Service Group +The `Group` is the central orchestration type that: +- Manages setup and runtime phases +- Coordinates multiple concurrent processes +- Handles context cancellation and signal propagation +- Provides health check endpoints + +### Lifecycle Phases + +1. **Created**: Group initialized, functions registered +2. **Started**: `Start()` called, setup functions begin +3. **Setup Complete**: All setup functions finished successfully +4. **Running**: All registered processes running concurrently +5. **Shutting Down**: Context canceled (signal or error) +6. **Stopped**: All processes terminated + +## Component Diagram + +```mermaid +graph TB + subgraph "service Package" + Group[Group
Lifecycle Manager] + Signal[signalCtx
Signal Handler] + Probes[Health Probes
Liveness/Readiness] + end + + subgraph "Dependencies" + ErrGroup[errgroup.Group
golang.org/x/sync] + Context[context.Context
Go stdlib] + HTTP[net/http
Go stdlib] + end + + subgraph "User Code" + Setup[Setup Functions] + Processes[Service Processes] + Server[HTTP Server] + end + + Signal --> Context + Group --> Context + Group --> ErrGroup + Group --> Signal + Probes --> Group + Server --> Probes + Setup --> Group + Processes --> Group + + style Group fill:#4a90e2 + style Signal fill:#7ed321 + style Probes fill:#f5a623 +``` + +## Data Flow + +### Group Structure + +```mermaid +classDiagram + class Group { + -context.Context rootCtx + -func() rootCancel + -chan struct{} startedCh + -chan struct{} setupCh + -[]runFn setups + -[]runFn processes + +Setup(fn) + +Register(fn) + +Start() error + +Close() error + +Liveness() HandlerFunc + +Readiness() HandlerFunc + -started() bool + -setup() bool + } + + class runFn { + <> + +func(context.Context) error + } + + Group --> runFn : contains +``` + +### State Machine + +```mermaid +stateDiagram-v2 + [*] --> Created: NewSignals() or NewCtx() + Created --> Created: Setup() / Register() + Created --> Started: Start() + Started --> SetupRunning: Run setup functions + SetupRunning --> SetupComplete: All setups succeed + SetupRunning --> Error: Setup fails + SetupComplete --> Running: Start all processes + Running --> ShuttingDown: Context canceled or process error + ShuttingDown --> Stopped: All processes terminated + Error --> [*] + Stopped --> [*] + + note right of Created + Liveness: 503 + Readiness: 503 + end note + + note right of Started + Liveness: 200 + Readiness: 503 + end note + + note right of SetupComplete + Liveness: 200 + Readiness: 200 + end note +``` + +## Sequence Diagrams + +### Service Startup + +```mermaid +sequenceDiagram + participant U as User Code + participant G as Group + participant S as Setup Functions + participant P as Processes + participant E as errgroup + + U->>G: NewSignals(os.Interrupt) + U->>G: Setup(fn1) + U->>G: Setup(fn2) + U->>G: Register(process1) + U->>G: Register(process2) + + U->>G: Start() + G->>G: close(startedCh) + Note over G: Liveness = 200 + + loop Each setup + G->>S: fn(ctx) + S-->>G: nil + end + + G->>G: close(setupCh) + Note over G: Readiness = 200 + + G->>E: WithContext(rootCtx) + + par Concurrent Processes + G->>P: process1(ctx) + and + G->>P: process2(ctx) + end + + E-->>G: Wait() returns error + G-->>U: error or nil +``` + +### Signal Handling + +```mermaid +sequenceDiagram + participant OS as Operating System + participant SH as signalCtx + participant Ctx as Context + participant G as Group + participant P as Processes + + OS->>SH: SIGINT + SH->>Ctx: cancel() + Ctx->>G: ctx.Done() closed + G->>P: ctx.Done() closed + + par Process Cleanup + P->>P: Detect ctx.Done() + P->>P: Cleanup resources + P-->>G: return nil/error + end + + G->>G: errgroup.Wait() + G-->>G: All processes stopped +``` + +### Health Check Probes + +```mermaid +sequenceDiagram + participant K8s as Orchestrator + participant L as /live endpoint + participant R as /ready endpoint + participant G as Group + + Note over G: State: Created + K8s->>L: GET /live + L->>G: started()? + G-->>L: false + L-->>K8s: 503 Service Unavailable + + K8s->>R: GET /ready + R->>G: started() && setup()? + G-->>R: false + R-->>K8s: 503 Service Unavailable + + Note over G: Start() called + Note over G: State: Started + + K8s->>L: GET /live + L->>G: started()? + G-->>L: true + L-->>K8s: 200 OK + + K8s->>R: GET /ready + R->>G: started() && setup()? + G-->>R: false (setup not done) + R-->>K8s: 503 Service Unavailable + + Note over G: State: Setup Complete + + K8s->>R: GET /ready + R->>G: started() && setup()? + G-->>R: true + R-->>K8s: 200 OK +``` + +## Key Design Decisions + +### 1. Errgroup for Process Management +**Decision**: Use `golang.org/x/sync/errgroup` for concurrent process execution. + +**Rationale**: +- Automatic context propagation on first error +- Simplified error collection from multiple goroutines +- Well-tested, official Go extension library + +### 2. Two-Phase Initialization +**Decision**: Separate setup phase from process execution phase. + +**Rationale**: +- Allows initialization work before service starts accepting traffic +- Setup functions run sequentially, reducing race conditions +- Processes run concurrently only after setup succeeds +- Enables accurate readiness probes + +### 3. Channel-Based State Tracking +**Decision**: Use closed channels for state signaling (`startedCh`, `setupCh`). + +**Rationale**: +- Non-blocking state checks via `select` +- Multiple readers can check state simultaneously +- Channel closure is a one-time, thread-safe signal +- No need for mutexes + +### 4. No Restart Logic +**Decision**: Group stops permanently if any process errors or context cancels. + +**Rationale**: +- Simplicity: restart logic belongs at orchestration layer (systemd, k8s) +- Predictability: clear failure semantics +- Safety: prevents cascading failures or partial states + +### 5. Context-First Design +**Decision**: All functions accept `context.Context` as first parameter. + +**Rationale**: +- Standard Go idiom +- Enables cancellation, deadlines, and value propagation +- Works naturally with errgroup +- Testable with custom contexts + +## Error Handling Strategy + +### Setup Phase +- Errors stop execution immediately +- No processes start if setup fails +- Error returned directly to caller + +### Process Phase +- First error cancels context for all processes +- errgroup waits for all goroutines to complete +- First error is returned (others are discarded) + +### Signal Handling +- Signal causes context cancellation +- Processes should return nil on graceful shutdown +- User code determines error handling for abrupt termination + +## Testing Architecture + +### Unit Tests +- Test each component in isolation +- Mock contexts and channels for state verification +- Test concurrency with goroutines and channels + +### System Tests +- Build real binary (`examples/server`) +- Test actual signal handling (SIGINT) +- Verify HTTP probe endpoints work correctly +- Integration test of full lifecycle + +## Performance Considerations + +### Goroutine Usage +- One goroutine per registered process +- One goroutine for signal handling (if using NewSignals) +- No unbounded goroutine creation + +### Memory +- Fixed allocations after Start() +- Channel cleanup via garbage collection after closure +- No internal buffering or queuing + +### Latency +- Health probes: O(1) channel select +- Start(): Sequential setup then concurrent processes +- Shutdown: Bounded by slowest process cleanup + +## Extension Points + +### Custom Context Creation +```go +ctx := context.WithTimeout(context.Background(), 30*time.Second) +g := service.NewCtx(ctx) +``` + +### Custom Signal Handling +```go +g := service.NewSignals(os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) +``` + +### Wrapping Existing Servers +```go +g.Register(func(ctx context.Context) error { + srv := &http.Server{Addr: ":8080"} + go func() { + <-ctx.Done() + srv.Close() + }() + return srv.ListenAndServe() +}) +``` + +## Limitations + +- No built-in retry logic +- No process dependencies (all start concurrently) +- No partial failure handling (one fails, all stop) +- No built-in metrics or logging +- No configuration management + +These are intentional design choices to keep the library focused and composable.