feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+138
View File
@@ -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
}