This document details how the Go context package is utilized throughout the Synapse Go application for dependency injection, cancellation signaling, and value propagation.
Note: This document focuses on specific implementation details and code examples of context usage in various components. For a higher-level architectural overview of how context flows through the application, see Context Flow in Architecture.
The context package in Go is a powerful tool for carrying request-scoped values, cancellation signals, and deadlines across API boundaries and between processes. Synapse Go makes extensive use of contexts for multiple purposes:
flowchart TD A[Application Context] -->|Contains| B1[ConfigContext] A -->|Contains| B2[WaitGroup] A -->|Derives| C1[API Context] A -->|Derives| C2[Inbound Context] B1 -->|Stores| D1[APIs] B1 -->|Stores| D2[Endpoints] B1 -->|Stores| D3[Sequences] B1 -->|Stores| D4[Inbounds] B1 -->|Stores| D5[Deployment Config] C1 -->|Processes| E1[API Requests] C2 -->|Handles| E2[File Events] C2 -->|Handles| E3[HTTP Requests] classDef contextStyle fill:#e1f5fe,stroke:#01579b,stroke-width:1px classDef configStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px classDef componentStyle fill:#fff8e1,stroke:#ff8f00,stroke-width:1px classDef operationStyle fill:#fce4ec,stroke:#c2185b,stroke-width:1px class A contextStyle class B1,B2 configStyle class C1,C2 componentStyle class D1,D2,D3,D4,D5,E1,E2,E3 operationStyle
Synapse Go defines context keys in internal/pkg/core/utils/context_types.go:
type ContextKey string
type WGKey string
const ConfigContextKey ContextKey = "configContext"
const WaitGroupKey WGKey = "waitGroup"
These typed keys ensure type safety when accessing values stored in the context.
The main context is created at the application entry point in cmd/synapse/main.go:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
synapse.Run(ctx)
}
This context is:
In internal/app/synapse/synapse.go, the context is enhanced with application-wide values:
func Run(ctx context.Context) error { // Add WaitGroup to context var wg sync.WaitGroup ctx, cancel := context.WithCancel(ctx) ctx = context.WithValue(ctx, utils.WaitGroupKey, &wg) defer cancel() // Add ConfigContext to context conCtx := artifacts.GetConfigContext() ctx = context.WithValue(ctx, utils.ConfigContextKey, conCtx) // ...rest of initialization code... }
This enhancement:
The configuration context is accessed throughout the application to retrieve configuration values and artifacts:
// Example from deployer func (d *Deployer) DeployAPIs(ctx context.Context, fileName string, xmlData string) { // ... configContext := ctx.Value(utils.ConfigContextKey).(*artifacts.ConfigContext) configContext.AddAPI(newApi) // ... } // Example from HTTP inbound func (h *HTTPInbound) handleRequest(w http.ResponseWriter, r *http.Request, mediator ports.InboundMessageMediator) { // ... ctx := r.Context() configContext := ctx.Value(utils.ConfigContextKey).(*artifacts.ConfigContext) sequence := configContext.SequenceMap[h.config.SequenceName] // ... }
The WaitGroup stored in the context is used to track goroutines and ensure graceful shutdown:
// Example from router service func (r *RouterService) StartServer(ctx context.Context) { wg := ctx.Value(utils.WaitGroupKey).(*sync.WaitGroup) wg.Add(1) go func() { defer wg.Done() // Server code }() // ... } // Example from inbound deployer func (d *Deployer) DeployInbounds(ctx context.Context, fileName string, xmlData string) { // ... wg := ctx.Value(utils.WaitGroupKey).(*sync.WaitGroup) wg.Add(1) go func(endpoint ports.InboundEndpoint) { defer wg.Done() // Inbound endpoint code }(inboundEndpoint) }
Context cancellation is used to signal shutdown to all components:
// In synapse.go (main function) <-ctx.Done() wg.Wait() routerService.StopServer() // In HTTP server code go func() { <-ctx.Done() // Shutdown server }() // In file inbound code go func() { <-f.ctx.Done() f.logger.Info("File inbound shutting down") f.wg.Wait() }()
This pattern ensures that:
For operations that should not block indefinitely, timeout contexts are used:
// In router service for HTTP server shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := r.server.Shutdown(shutdownCtx); err != nil { r.logger.Error("HTTP server shutdown error", "error", err) } // In file processing for limiting operation time ctx, cancel := context.WithTimeout(f.ctx, 30*time.Second) defer cancel() // Process file with timeout result, err := mediator.Mediate(ctx, message)
For HTTP requests, the request context is used to carry request-specific information:
func (h *HTTPInbound) handleRequest(w http.ResponseWriter, r *http.Request, mediator ports.InboundMessageMediator) { // Use the request's context ctx := r.Context() // Add request information to the context if needed ctx = context.WithValue(ctx, "requestID", uuid.New().String()) // Use the enhanced context for mediation result, err := sequence.Mediate(ctx, message, mediator) // ... }
Mediators can access and add values to the context during message processing:
func (m *LogMediator) Mediate(ctx context.Context, message *domain.Message) (*domain.Message, error) { // Get information from context requestID := ctx.Value("requestID") // Log with context information m.logger.Info("Processing message", "requestID", requestID, "message", m.Message) return message, nil }
The context is often derived to create child contexts with specific characteristics:
// Creating a cancellable context from a parent context f.ctx, f.cancelFunc = context.WithCancel(ctx) // Creating a timeout context for an operation opCtx, cancel := context.WithTimeout(parentCtx, timeout) defer cancel() // Creating a context with additional values enrichedCtx := context.WithValue(ctx, keyName, value)
The File Inbound endpoint makes particularly extensive use of context:
func (f *FileInbound) processFile(filePath string, fileName string, mediator ports.InboundMessageMediator) { defer f.wg.Done() defer f.syncMap.Delete(fileName) // Create a context with timeout for file processing ctx, cancel := context.WithTimeout(f.ctx, 30*time.Second) defer cancel() // Create a file handle file, err := f.vfs.Open(filePath) if err != nil { f.logger.Error("Error opening file", "file", filePath, "error", err) return } defer file.Close() // Read file content content, err := io.ReadAll(file) if err != nil { f.logger.Error("Error reading file", "file", filePath, "error", err) return } // Create a message with the file content message := &domain.Message{ Payload: content, } // Add file information to context ctx = context.WithValue(ctx, "filename", fileName) ctx = context.WithValue(ctx, "filepath", filePath) // Process the file using the mediation engine result, err := mediator.Mediate(ctx, message) // ...handling result... }
In this example:
The HTTP server implementation uses context for graceful shutdown:
func (r *RouterService) StartServer(ctx context.Context) { // ... go func() { <-ctx.Done() r.logger.Info("Shutting down HTTP server...") // Create a timeout context for graceful shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := r.server.Shutdown(shutdownCtx); err != nil { r.logger.Error("HTTP server shutdown error", "error", err) } }() }
This pattern:
Deployers use context to access configuration and create inbound endpoints:
func (d *Deployer) DeployInbounds(ctx context.Context, fileName string, xmlData string) { // ... configContext := ctx.Value(utils.ConfigContextKey).(*artifacts.ConfigContext) configContext.AddInbound(newInbound) // Start the inbound endpoint with the context wg := ctx.Value(utils.WaitGroupKey).(*sync.WaitGroup) wg.Add(1) go func(endpoint ports.InboundEndpoint) { defer wg.Done() if err := endpoint.Start(ctx, d.inboundMediator); err != nil { d.logger.Error("Error starting inbound endpoint:", "error", err) } }(inboundEndpoint) }
The flow of context through the application forms a chain:
Background Context
│
▼
Signal-aware Context
│
▼
Application Context
│
┌─────────────┬─────────────┐
│ │ │
▼ ▼ ▼
Component 1 Component 2 Component 3
Context Context Context
│ │ │
▼ ▼ ▼
Operation Operation Operation
Context Context Context
The Synapse Go codebase demonstrates several context best practices:
The codebase avoids common context pitfalls:
The context usage in Synapse Go demonstrates a comprehensive approach to:
This approach allows Synapse Go to maintain a clean architecture while providing effective coordination between components during the application lifecycle.