feat: backend service
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user