[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# Cancellation, Timeouts & Deadlines
|
||||
|
||||
## Cancellation
|
||||
|
||||
`context.WithCancel` returns a derived context and a `cancel` function. When `cancel()` is called, the context's `Done()` channel is closed, signaling all listeners to stop.
|
||||
|
||||
```go
|
||||
func processItems(ctx context.Context, items []Item) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel() // always defer cancel to free resources
|
||||
|
||||
errCh := make(chan error, len(items))
|
||||
for _, item := range items {
|
||||
go func(item Item) {
|
||||
errCh <- processOne(ctx, item)
|
||||
}(item)
|
||||
}
|
||||
|
||||
for range items {
|
||||
if err := <-errCh; err != nil {
|
||||
cancel() // cancel remaining goroutines on first error
|
||||
return fmt.Errorf("processing items: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Why `defer cancel()` matters
|
||||
|
||||
Every `WithCancel`, `WithTimeout`, and `WithDeadline` creates cancellation state; timeout/deadline contexts also use timer resources. `cancel()` MUST be called on all control-flow paths unless the function explicitly returns or transfers ownership of both the context and cancel function. In ordinary scoped work, defer cancel immediately.
|
||||
|
||||
```go
|
||||
// ✗ Bad — cancel is never called, resources leak
|
||||
func fetch(ctx context.Context) error {
|
||||
ctx, _ = context.WithTimeout(ctx, 5*time.Second)
|
||||
return doWork(ctx)
|
||||
}
|
||||
|
||||
// ✓ Good — scoped work, defer cancel immediately
|
||||
func fetch(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
return doWork(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
## Timeouts and Deadlines
|
||||
|
||||
### `context.WithTimeout` — relative duration
|
||||
|
||||
```go
|
||||
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
```
|
||||
|
||||
### `context.WithDeadline` — absolute point in time
|
||||
|
||||
```go
|
||||
func (s *BatchService) ProcessBatch(ctx context.Context, batch Batch) error {
|
||||
// The batch must complete by its SLA deadline
|
||||
ctx, cancel := context.WithDeadline(ctx, batch.SLADeadline)
|
||||
defer cancel()
|
||||
|
||||
for _, item := range batch.Items {
|
||||
if err := s.process(ctx, item); err != nil {
|
||||
return fmt.Errorf("processing batch item %s: %w", item.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Nested timeouts take the shorter deadline
|
||||
|
||||
If a parent context has a 5s timeout and you create a child with 10s, the child still expires at 5s. The shorter deadline always wins.
|
||||
|
||||
```go
|
||||
// Parent has 2s timeout — child's 10s is effectively ignored
|
||||
parentCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
childCtx, childCancel := context.WithTimeout(parentCtx, 10*time.Second)
|
||||
defer childCancel()
|
||||
// childCtx expires after 2s, not 10s
|
||||
```
|
||||
|
||||
## Listening for Cancellation
|
||||
|
||||
### The `select` pattern
|
||||
|
||||
Use `ctx.Done()` in a `select` statement to react to cancellation alongside other work:
|
||||
|
||||
```go
|
||||
func poll(ctx context.Context, interval time.Duration) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err() // context.Canceled or context.DeadlineExceeded
|
||||
case <-ticker.C:
|
||||
if err := doWork(ctx); err != nil {
|
||||
return fmt.Errorf("polling: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checking cancellation in loops
|
||||
|
||||
For CPU-bound work, periodically check `ctx.Err()`:
|
||||
|
||||
```go
|
||||
func processLargeDataset(ctx context.Context, items []Item) error {
|
||||
for i, item := range items {
|
||||
if ctx.Err() != nil {
|
||||
return fmt.Errorf("processing interrupted after %d/%d items: %w", i, len(items), ctx.Err())
|
||||
}
|
||||
process(item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## `context.AfterFunc` (Go 1.21+)
|
||||
|
||||
Registers a callback that runs in its own goroutine when the context is cancelled. Useful for cleanup without blocking the main flow.
|
||||
|
||||
```go
|
||||
func watchResource(ctx context.Context, res *Resource) {
|
||||
stop := context.AfterFunc(ctx, func() {
|
||||
// Runs in a new goroutine when ctx is cancelled
|
||||
res.Release()
|
||||
})
|
||||
|
||||
// If you no longer need the callback, cancel it:
|
||||
// stop() returns true if the callback was successfully cancelled
|
||||
_ = stop
|
||||
}
|
||||
```
|
||||
|
||||
## `context.WithoutCancel` (Go 1.21+)
|
||||
|
||||
Creates a child context that is not cancelled when the parent is. Use this for background work that must continue after the request completes — like async logging, audit trails, or enqueuing follow-up tasks.
|
||||
|
||||
```go
|
||||
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
order, err := h.orderService.Create(ctx, req)
|
||||
if err != nil {
|
||||
// handle error
|
||||
return
|
||||
}
|
||||
|
||||
// Audit log must complete even if the client disconnects.
|
||||
// WithoutCancel preserves context values (trace_id) but detaches cancellation.
|
||||
auditCtx := context.WithoutCancel(ctx)
|
||||
go h.auditService.LogOrderCreated(auditCtx, order)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
```
|
||||
|
||||
Without `WithoutCancel`, you'd have to choose between `ctx` (which gets cancelled when the handler returns, killing your background work) and `context.Background()` (which loses trace_id and other values). `WithoutCancel` gives you the best of both: values are preserved, but cancellation is detached.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Context in HTTP Servers & Service Calls
|
||||
|
||||
## Context in HTTP Servers
|
||||
|
||||
`http.Request` carries a context that is cancelled when the client disconnects or the request handler returns. MUST use `r.Context()` — NEVER create a new `context.Background()` inside a handler.
|
||||
|
||||
```go
|
||||
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context() // this context is cancelled if the client disconnects
|
||||
|
||||
order, err := h.orderService.Get(ctx, r.PathValue("id"))
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
// Client disconnected, no point writing a response
|
||||
return
|
||||
}
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware enriching context
|
||||
|
||||
Middleware injects request-scoped values before handlers run. Use unexported key types to prevent collisions:
|
||||
|
||||
```go
|
||||
// Helpers for trace propagation
|
||||
type contextKey string
|
||||
const (
|
||||
traceIDKey contextKey = "trace_id"
|
||||
spanIDKey contextKey = "span_id"
|
||||
)
|
||||
|
||||
func TracingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
traceID := r.Header.Get("X-Trace-ID")
|
||||
if traceID == "" {
|
||||
traceID = generateTraceID()
|
||||
}
|
||||
spanID := r.Header.Get("X-Span-ID")
|
||||
if spanID == "" {
|
||||
spanID = generateSpanID()
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), traceIDKey, traceID)
|
||||
ctx = context.WithValue(ctx, spanIDKey, spanID)
|
||||
|
||||
w.Header().Set("X-Trace-ID", traceID)
|
||||
w.Header().Set("X-Span-ID", spanID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// Propagate trace context to downstream services
|
||||
func (c *HTTPClient) Do(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
if traceID, ok := ctx.Value(traceIDKey).(string); ok {
|
||||
req.Header.Set("X-Trace-ID", traceID)
|
||||
}
|
||||
if spanID, ok := ctx.Value(spanIDKey).(string); ok {
|
||||
req.Header.Set("X-Span-ID", spanID)
|
||||
}
|
||||
return c.client.Do(req)
|
||||
}
|
||||
```
|
||||
|
||||
## Context in Calls to Other Services
|
||||
|
||||
Context MUST be propagated to all HTTP clients and databases using context-aware APIs: `http.NewRequestWithContext`, `QueryContext`, `ExecContext`, and `QueryRowContext`. This ensures that client disconnections cancel all downstream operations.
|
||||
|
||||
```go
|
||||
// ✗ Bad — downstream calls ignore the request context
|
||||
func (c *PaymentClient) Charge(ctx context.Context, amount int) error {
|
||||
req, _ := http.NewRequest("POST", c.url+"/charge", body)
|
||||
return c.client.Do(req) // not context-aware
|
||||
}
|
||||
|
||||
// ✓ Good — all downstream operations respect the context
|
||||
func (c *PaymentClient) Charge(ctx context.Context, amount int) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", c.url+"/charge", body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
return c.client.Do(req)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ✗ Bad — downstream calls ignore the request context
|
||||
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
|
||||
row := r.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✓ Good — all downstream operations respect the context
|
||||
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
|
||||
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
# Context Values & Cross-Service Tracing
|
||||
|
||||
## Using context values correctly
|
||||
|
||||
Context values carry request-scoped metadata that crosses API boundaries — not function parameters, configuration, or optional arguments. Good candidates: trace IDs, span IDs, request IDs, authenticated user info, correlation IDs.
|
||||
|
||||
Always use an unexported type as the key to prevent collisions between packages:
|
||||
|
||||
```go
|
||||
// ✓ Good — unexported key type prevents collisions
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
traceIDKey contextKey = "trace_id"
|
||||
requestIDKey contextKey = "request_id"
|
||||
)
|
||||
|
||||
func WithTraceID(ctx context.Context, traceID string) context.Context {
|
||||
return context.WithValue(ctx, traceIDKey, traceID)
|
||||
}
|
||||
|
||||
func TraceIDFromContext(ctx context.Context) (string, bool) {
|
||||
traceID, ok := ctx.Value(traceIDKey).(string)
|
||||
return traceID, ok
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ✗ Bad — string keys collide across packages
|
||||
ctx = context.WithValue(ctx, "trace_id", traceID) // another package could use the same key
|
||||
```
|
||||
|
||||
## What belongs in context values vs function parameters
|
||||
|
||||
| Data | Context value? | Why |
|
||||
| --- | --- | --- |
|
||||
| trace_id, span_id, request_id | Yes | Request-scoped metadata for observability |
|
||||
| Authenticated user/tenant | Yes | Request-scoped, crosses API boundaries |
|
||||
| Database connection | No | Infrastructure dependency, pass explicitly |
|
||||
| Feature flags | No | Configuration, pass explicitly or inject |
|
||||
| Function arguments (user ID, order data) | No | Business logic parameters, pass as arguments |
|
||||
| Logger | Depends | OK if enriched with request-scoped fields (trace_id); otherwise pass explicitly |
|
||||
|
||||
## Trace propagation between services
|
||||
|
||||
In a microservices architecture, `context.Context` is the vehicle for trace propagation. When Service A calls Service B, the trace_id and span_id travel through context values and are injected into outgoing HTTP headers (typically via OpenTelemetry). This creates a connected trace across the entire request path.
|
||||
|
||||
```go
|
||||
// Middleware injects trace_id from incoming request headers into context
|
||||
func TracingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
traceID := r.Header.Get("X-Trace-ID")
|
||||
if traceID == "" {
|
||||
traceID = generateTraceID()
|
||||
}
|
||||
|
||||
ctx := WithTraceID(r.Context(), traceID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// When making outbound HTTP calls, inject trace_id from context into headers
|
||||
func (c *HTTPClient) Do(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
// Propagate trace_id to downstream service
|
||||
if traceID, ok := TraceIDFromContext(ctx); ok {
|
||||
req.Header.Set("X-Trace-ID", traceID)
|
||||
}
|
||||
|
||||
return c.client.Do(req)
|
||||
}
|
||||
```
|
||||
|
||||
With OpenTelemetry, this propagation is handled automatically through the `otel` SDK and `propagation.TraceContext`, but the mechanism is the same: context carries the trace state, and it must be propagated through every layer.
|
||||
Reference in New Issue
Block a user