139 lines
4.0 KiB
Go
139 lines
4.0 KiB
Go
package backendclient
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config describes the backend endpoint and gateway client identity used
|
|
// to construct a Client. All fields are required when the gateway is
|
|
// expected to talk to a real backend; the empty value yields an
|
|
// always-unavailable client.
|
|
type Config struct {
|
|
// HTTPBaseURL is the absolute base URL of the backend HTTP listener
|
|
// (`/api/v1/{public,user,internal}/*`). Required.
|
|
HTTPBaseURL string
|
|
|
|
// GRPCPushURL is the dial target of the backend `Push.SubscribePush`
|
|
// listener (`host:port`). Required.
|
|
GRPCPushURL string
|
|
|
|
// GatewayClientID is the durable identifier this gateway instance
|
|
// presents to backend in `GatewaySubscribeRequest.gateway_client_id`.
|
|
// Required.
|
|
GatewayClientID string
|
|
|
|
// HTTPTimeout bounds individual REST calls. Must be positive.
|
|
HTTPTimeout time.Duration
|
|
|
|
// PushReconnectBaseBackoff is the starting delay between reconnect
|
|
// attempts of `Push.SubscribePush`. Must be positive.
|
|
PushReconnectBaseBackoff time.Duration
|
|
|
|
// PushReconnectMaxBackoff is the upper bound for exponential
|
|
// reconnect delays. Must be greater than or equal to
|
|
// PushReconnectBaseBackoff.
|
|
PushReconnectMaxBackoff time.Duration
|
|
}
|
|
|
|
// Validate reports a formatted error when cfg is missing required
|
|
// values. The empty value is invalid; callers that intentionally omit
|
|
// the backend may bypass this check by skipping NewClient entirely.
|
|
func (cfg Config) Validate() error {
|
|
trimmed := strings.TrimSpace(cfg.HTTPBaseURL)
|
|
if trimmed == "" {
|
|
return errors.New("backendclient: HTTPBaseURL must not be empty")
|
|
}
|
|
parsed, err := url.Parse(strings.TrimRight(trimmed, "/"))
|
|
if err != nil {
|
|
return fmt.Errorf("backendclient: parse HTTPBaseURL: %w", err)
|
|
}
|
|
if parsed.Scheme == "" || parsed.Host == "" {
|
|
return errors.New("backendclient: HTTPBaseURL must be absolute")
|
|
}
|
|
if strings.TrimSpace(cfg.GRPCPushURL) == "" {
|
|
return errors.New("backendclient: GRPCPushURL must not be empty")
|
|
}
|
|
if strings.TrimSpace(cfg.GatewayClientID) == "" {
|
|
return errors.New("backendclient: GatewayClientID must not be empty")
|
|
}
|
|
if cfg.HTTPTimeout <= 0 {
|
|
return errors.New("backendclient: HTTPTimeout must be positive")
|
|
}
|
|
if cfg.PushReconnectBaseBackoff <= 0 {
|
|
return errors.New("backendclient: PushReconnectBaseBackoff must be positive")
|
|
}
|
|
if cfg.PushReconnectMaxBackoff < cfg.PushReconnectBaseBackoff {
|
|
return errors.New("backendclient: PushReconnectMaxBackoff must be >= PushReconnectBaseBackoff")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Client aggregates the REST and gRPC adapters that talk to backend.
|
|
// One value is shared across the gateway process; all methods are safe
|
|
// for concurrent use.
|
|
type Client struct {
|
|
rest *RESTClient
|
|
push *PushClient
|
|
}
|
|
|
|
// NewClient constructs a Client that targets the configured backend.
|
|
// REST adapter is always built. The gRPC push adapter is built lazily
|
|
// when StartPush is called so unit tests can construct a Client with a
|
|
// stubbed push transport.
|
|
func NewClient(cfg Config) (*Client, error) {
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
rest, err := NewRESTClient(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
push, err := NewPushClient(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Client{rest: rest, push: push}, nil
|
|
}
|
|
|
|
// REST returns the REST adapter. The returned value is nil when the
|
|
// Client was constructed without a backend; callers must guard.
|
|
func (c *Client) REST() *RESTClient {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return c.rest
|
|
}
|
|
|
|
// Push returns the gRPC push adapter. The returned value is nil when
|
|
// the Client was constructed without a backend.
|
|
func (c *Client) Push() *PushClient {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return c.push
|
|
}
|
|
|
|
// Close releases idle HTTP connections and closes the gRPC push
|
|
// connection. Safe to call multiple times.
|
|
func (c *Client) Close() error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
var firstErr error
|
|
if c.rest != nil {
|
|
if err := c.rest.Close(); err != nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
if c.push != nil {
|
|
if err := c.push.Close(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|