clientx

package module
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2025 License: MIT Imports: 11 Imported by: 0

README

egg/clientx

Overview

clientx provides Connect HTTP client factory with production-ready features including retry logic, circuit breaker, timeouts, and idempotency support. It simplifies creating resilient Connect-RPC clients.

Key Features

  • Automatic retry with exponential backoff
  • Circuit breaker to prevent cascade failures
  • Configurable request timeouts
  • Idempotency key support
  • Connection pooling
  • Clean transport abstraction

Dependencies

Layer: L3 (Runtime Communication Layer)
Depends on: connectrpc.com/connect, github.com/sony/gobreaker

Installation

go get go.eggybyte.com/egg/clientx@latest

Basic Usage

import (
    "go.eggybyte.com/egg/clientx"
    userv1connect "myapp/gen/go/user/v1/userv1connect"
)

func main() {
    // Create HTTP client with resilience features
    httpClient := clientx.NewHTTPClient("https://api.example.com",
        clientx.WithTimeout(5*time.Second),
        clientx.WithRetry(3),
        clientx.WithCircuitBreaker(true),
    )
    
    // Create Connect client
    client := userv1connect.NewUserServiceClient(
        httpClient,
        "https://api.example.com",
    )
    
    // Make requests
    resp, err := client.GetUser(ctx, connect.NewRequest(&userv1.GetUserRequest{
        UserId: "u-123",
    }))
}

Configuration Options

Option Type Description
WithTimeout(d) time.Duration Request timeout (default: 30s)
WithRetry(n) int Maximum retry attempts (default: 3)
WithCircuitBreaker(bool) bool Enable circuit breaker (default: true)
WithIdempotencyKey(key) string Custom idempotency header name

API Reference

Options
type Options struct {
    Timeout          time.Duration // Request timeout
    MaxRetries       int           // Maximum retry attempts
    RetryBackoff     time.Duration // Initial backoff duration
    EnableCircuit    bool          // Enable circuit breaker
    CircuitThreshold uint32        // Circuit breaker failure threshold
    IdempotencyKey   string        // Idempotency key header name
}
Functions
// NewHTTPClient creates a new HTTP client with Connect interceptors
func NewHTTPClient(baseURL string, opts ...Option) *http.Client

// NewConnectClient creates a Connect client with interceptors
func NewConnectClient[T any](
    baseURL, serviceName string,
    newClient func(connect.HTTPClient, string, ...connect.ClientOption) T,
    opts ...Option,
) T

Architecture

The clientx module provides resilient HTTP transport:

clientx/
├── clientx.go           # Public API (~114 lines)
│   ├── Options          # Configuration
│   ├── NewHTTPClient()  # HTTP client factory
│   └── NewConnectClient()  # Connect client helper
└── internal/
    └── retry.go         # Retry transport implementation
        ├── RoundTrip()      # HTTP transport with retry
        ├── shouldRetry()    # Retry decision logic
        └── backoff()        # Exponential backoff

Design Highlights:

  • Wraps standard http.Transport with retry logic
  • Circuit breaker prevents repeated failures
  • Configurable backoff strategy
  • Idempotent request detection

Example: Basic Client

func createUserClient() userv1connect.UserServiceClient {
    httpClient := clientx.NewHTTPClient("https://api.example.com",
        clientx.WithTimeout(10*time.Second),
        clientx.WithRetry(3),
    )
    
    return userv1connect.NewUserServiceClient(
        httpClient,
        "https://api.example.com",
    )
}

func main() {
    client := createUserClient()
    
    resp, err := client.GetUser(context.Background(), connect.NewRequest(&userv1.GetUserRequest{
        UserId: "u-123",
    }))
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("User: %s\n", resp.Msg.User.Name)
}

Example: With Circuit Breaker

// Circuit breaker opens after 5 consecutive failures
httpClient := clientx.NewHTTPClient("https://api.example.com",
    clientx.WithTimeout(5*time.Second),
    clientx.WithRetry(3),
    clientx.WithCircuitBreaker(true),
)

client := userv1connect.NewUserServiceClient(httpClient, "https://api.example.com")

// Make requests
for i := 0; i < 10; i++ {
    resp, err := client.GetUser(ctx, connect.NewRequest(&userv1.GetUserRequest{
        UserId: fmt.Sprintf("u-%d", i),
    }))
    
    if err != nil {
        if errors.Is(err, gobreaker.ErrOpenState) {
            log.Println("Circuit breaker is open, skipping requests")
            time.Sleep(60 * time.Second)  // Wait for circuit to close
            continue
        }
        log.Printf("Request failed: %v\n", err)
        continue
    }
    
    log.Printf("User: %s\n", resp.Msg.User.Name)
}

Example: Custom Retry Strategy

// Configure aggressive retry for critical operations
httpClient := clientx.NewHTTPClient("https://api.example.com",
    clientx.WithTimeout(30*time.Second),
    clientx.WithRetry(5),  // More retries
)

client := paymentv1connect.NewPaymentServiceClient(httpClient, "https://api.example.com")

// Make payment request (will retry up to 5 times on transient failures)
resp, err := client.ProcessPayment(ctx, connect.NewRequest(&paymentv1.ProcessPaymentRequest{
    Amount:   10000,
    Currency: "USD",
}))

Example: Disable Circuit Breaker

// For internal services where circuit breaker isn't needed
httpClient := clientx.NewHTTPClient("http://internal-service:8080",
    clientx.WithTimeout(10*time.Second),
    clientx.WithRetry(2),
    clientx.WithCircuitBreaker(false),  // Disable circuit breaker
)

Retry Logic

Retryable Conditions

Requests are retried when:

  1. Network errors (connection refused, timeout, etc.)
  2. HTTP 5xx server errors
  3. HTTP 429 (rate limit) errors
  4. Transient Connect errors
Non-Retryable Conditions

Requests are NOT retried for:

  1. HTTP 4xx client errors (except 429)
  2. Successful responses (2xx)
  3. Non-idempotent methods (POST without idempotency key)
Backoff Strategy

Exponential backoff with jitter:

Attempt 1: 100ms
Attempt 2: 200ms
Attempt 3: 400ms
Attempt 4: 800ms
...

Circuit Breaker

States
  1. Closed (Normal)

    • Requests pass through
    • Failures are counted
  2. Open (Failing)

    • Requests are immediately rejected
    • After timeout, transitions to Half-Open
  3. Half-Open (Testing)

    • Limited requests allowed
    • Success → Closed
    • Failure → Open
Configuration
// Circuit opens after 5 consecutive failures
CircuitThreshold: 5

// Circuit stays open for 60 seconds before testing
Timeout: 60 * time.Second

// In Half-Open state, allow 3 test requests
MaxRequests: 3

Idempotency Support

// Configure custom idempotency header
httpClient := clientx.NewHTTPClient("https://api.example.com",
    clientx.WithIdempotencyKey("X-Idempotency-Key"),
)

// Client automatically adds idempotency key for POST requests
// Header: X-Idempotency-Key: {generated-uuid}

Connection Pooling

The HTTP client uses Go's default connection pooling:

// Default pool settings (configurable via http.Transport):
MaxIdleConns:        100
MaxIdleConnsPerHost: 2
IdleConnTimeout:     90 * time.Second

For custom pooling:

transport := &http.Transport{
    MaxIdleConns:        200,
    MaxIdleConnsPerHost: 10,
    IdleConnTimeout:     120 * time.Second,
}

// Wrap with retry logic
retryTransport := internal.NewRetryTransport(transport, 3, 100*time.Millisecond, nil)

httpClient := &http.Client{
    Timeout:   30 * time.Second,
    Transport: retryTransport,
}

Testing

Mock clients for testing:

func TestUserService(t *testing.T) {
    // Create test server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(&userv1.GetUserResponse{
            User: &userv1.User{Id: "u-123", Name: "Test User"},
        })
    }))
    defer server.Close()
    
    // Create client pointing to test server
    httpClient := clientx.NewHTTPClient(server.URL,
        clientx.WithTimeout(1*time.Second),
        clientx.WithRetry(1),
    )
    
    client := userv1connect.NewUserServiceClient(httpClient, server.URL)
    
    // Test
    resp, err := client.GetUser(context.Background(), connect.NewRequest(&userv1.GetUserRequest{
        UserId: "u-123",
    }))
    
    require.NoError(t, err)
    assert.Equal(t, "Test User", resp.Msg.User.Name)
}

Best Practices

  1. Set reasonable timeouts - Prevent hanging requests
  2. Use circuit breaker for external services - Protect against cascading failures
  3. Configure appropriate retry counts - Balance reliability vs latency
  4. Enable idempotency for mutations - Safe retries for POST/PUT/DELETE
  5. Monitor circuit breaker state - Alert when circuits open frequently
  6. Test failure scenarios - Ensure retry logic works as expected

Performance Considerations

  • Retry Overhead: Each retry adds latency (100ms base + exponential backoff)
  • Circuit Breaker Overhead: Minimal (~microseconds per request)
  • Connection Pooling: Reuse connections for better performance
  • Timeout Configuration: Balance between resilience and latency

Stability

Status: Stable
Layer: L3 (Runtime Communication)
API Guarantees: Backward-compatible changes only

The clientx module is production-ready and follows semantic versioning.

License

This package is part of the egg framework and is licensed under the MIT License. See the root LICENSE file for details.

Documentation

Overview

Package clientx provides Connect client factory with retry, circuit breaker, and timeouts.

Overview:

  • Responsibility: Create Connect HTTP clients with production-ready interceptors
  • Key Types: Options for client configuration, interceptors for resilience
  • Concurrency Model: Clients are safe for concurrent use
  • Error Semantics: Retry only on transient/idempotent errors
  • Performance Notes: Circuit breaker prevents cascade failures

Usage:

client := clientx.NewHTTPClient("https://api.example.com",
  clientx.WithTimeout(5*time.Second),
  clientx.WithRetry(3),
)

Package clientx provides Connect HTTP client construction with retry, circuit breaker, idempotency headers, and timeouts.

Overview

clientx offers production-grade HTTP clients suitable for Connect-based services. It includes exponential backoff retry, optional circuit breaker, and request timeouts while keeping APIs minimal and composable.

Features

  • Exponential backoff retries for transient 5xx errors
  • Optional circuit breaker to prevent cascade failures
  • Request timeouts and idempotency key injection
  • Generic helper for constructing typed Connect clients

Usage

client := clientx.NewHTTPClient("https://api.example.com",
	clientx.WithTimeout(5*time.Second),
	clientx.WithRetry(3),
	clientx.WithCircuitBreaker(true),
)

Layer

clientx belongs to Layer 3 (L3) and depends on core/log, connectx (optionally).

Stability

Stable since v0.1.0. Minor versions may introduce backward-compatible improvements.

Package clientx provides client-side metrics collection.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientMetricsInterceptor

func ClientMetricsInterceptor(collector *ClientMetricsCollector) connect.UnaryInterceptorFunc

ClientMetricsInterceptor creates a Connect client interceptor that collects outbound RPC metrics. It records request count and duration for all outbound RPC calls.

Parameters:

  • collector: client metrics collector instance

Returns:

  • connect.UnaryInterceptorFunc: client interceptor function

Metrics collected:

  • rpc_client_requests_total: counter of outbound requests by service, method, code
  • rpc_client_request_duration_seconds: histogram of outbound request duration

Labels:

  • rpc_service: target service name
  • rpc_method: target method name
  • rpc_code: Connect error code

Concurrency:

  • Safe for concurrent use

func NewConnectClient

func NewConnectClient[T any](baseURL, serviceName string, newClient func(connect.HTTPClient, string, ...connect.ClientOption) T, opts ...Option) T

NewConnectClient creates a Connect client with interceptors. This is a convenience wrapper for creating Connect clients with standard interceptors.

func NewHTTPClient

func NewHTTPClient(baseURL string, opts ...Option) *http.Client

NewHTTPClient creates a new HTTP client with Connect interceptors.

Types

type ClientMetricsCollector

type ClientMetricsCollector struct {
	// contains filtered or unexported fields
}

ClientMetricsCollector holds OpenTelemetry metrics instruments for client-side RPC monitoring.

func NewClientMetricsCollector

func NewClientMetricsCollector(otelProvider *obsx.Provider) (*ClientMetricsCollector, error)

NewClientMetricsCollector creates a new metrics collector for client-side RPC monitoring. If otelProvider is nil, metrics collection is disabled.

Parameters:

  • otelProvider: OpenTelemetry provider (can be nil to disable metrics)

Returns:

  • *ClientMetricsCollector: metrics collector instance
  • error: initialization error if metrics setup fails

Concurrency:

  • Safe for concurrent use after initialization

type Option

type Option func(*Options)

Option is a functional option for configuring the client.

func WithCircuitBreaker

func WithCircuitBreaker(enabled bool) Option

WithCircuitBreaker enables or disables the circuit breaker.

func WithIdempotencyKey

func WithIdempotencyKey(key string) Option

WithIdempotencyKey sets the idempotency key header name.

func WithRetry

func WithRetry(maxRetries int) Option

WithRetry sets the maximum retry attempts.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the client timeout.

type Options

type Options struct {
	Timeout          time.Duration // Request timeout (default: 30s)
	MaxRetries       int           // Maximum retry attempts (default: 3)
	RetryBackoff     time.Duration // Initial backoff duration (default: 100ms)
	EnableCircuit    bool          // Enable circuit breaker (default: true)
	CircuitThreshold uint32        // Circuit breaker failure threshold (default: 5)
	IdempotencyKey   string        // Custom idempotency key header name
}

Options configures the HTTP client behavior.

Directories

Path Synopsis
Package internal provides internal implementation details for clientx.
Package internal provides internal implementation details for clientx.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL