324 lines
8.3 KiB
Go
324 lines
8.3 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"proxy-pool/internal/adapters/postgresadmin"
|
|
"proxy-pool/internal/adapters/redisactivity"
|
|
"proxy-pool/internal/adapters/redisprovider"
|
|
"proxy-pool/internal/config"
|
|
controllerProvider "proxy-pool/internal/controller/provider"
|
|
"proxy-pool/internal/platform/credentials"
|
|
platformMetrics "proxy-pool/internal/platform/metrics"
|
|
)
|
|
|
|
const (
|
|
redisNamespace = "controller"
|
|
redisOperationTTL = 30 * time.Second
|
|
redisMinimumScan = 4_096
|
|
redisMaximumScan = config.MaximumPoolSize
|
|
redisCleanupLimit = 1_024
|
|
providerLeaseTTL = 15 * time.Second
|
|
providerRenewEvery = 3 * time.Second
|
|
providerRetryInterval = 100 * time.Millisecond
|
|
providerPermitGrace = 5 * time.Second
|
|
)
|
|
|
|
var (
|
|
ErrPostgresConfiguration = errors.New("invalid PostgreSQL configuration")
|
|
ErrPostgresUnavailable = errors.New("PostgreSQL unavailable")
|
|
ErrRedisConfiguration = errors.New("invalid Redis configuration")
|
|
ErrRedisUnavailable = errors.New("Redis unavailable")
|
|
)
|
|
|
|
type productionInfrastructure struct {
|
|
holderID string
|
|
namespace string
|
|
}
|
|
|
|
func (infrastructure *productionInfrastructure) Open(
|
|
ctx context.Context,
|
|
configuration *config.Config,
|
|
) (_ ports, resultErr error) {
|
|
if ctx == nil || configuration == nil {
|
|
return ports{}, ErrInvalidOptions
|
|
}
|
|
namespace, err := resolveRedisNamespace(infrastructure.namespace)
|
|
if err != nil {
|
|
return ports{}, err
|
|
}
|
|
var postgresPool *pgxpool.Pool
|
|
var redisClient *redis.Client
|
|
closeResources := func() error {
|
|
var closeErr error
|
|
if redisClient != nil {
|
|
closeErr = redisClient.Close()
|
|
}
|
|
if postgresPool != nil {
|
|
postgresPool.Close()
|
|
}
|
|
return closeErr
|
|
}
|
|
defer func() {
|
|
if resultErr != nil {
|
|
_ = closeResources()
|
|
}
|
|
}()
|
|
|
|
opened := ports{close: closeResources}
|
|
if configuration.Admin.Enabled {
|
|
if strings.TrimSpace(configuration.Storage.PostgresURL) == "" {
|
|
return ports{}, ErrPostgresConfiguration
|
|
}
|
|
poolConfig, err := pgxpool.ParseConfig(configuration.Storage.PostgresURL)
|
|
if err != nil {
|
|
return ports{}, ErrPostgresConfiguration
|
|
}
|
|
poolConfig.ConnConfig.RuntimeParams["application_name"] = "proxy-controller"
|
|
postgresPool, err = pgxpool.NewWithConfig(ctx, poolConfig)
|
|
if err != nil {
|
|
return ports{}, ErrPostgresUnavailable
|
|
}
|
|
if err = postgresPool.Ping(ctx); err != nil {
|
|
return ports{}, contextOr(ctx, ErrPostgresUnavailable)
|
|
}
|
|
if err = postgresadmin.ApplyMigrations(ctx, postgresPool); err != nil {
|
|
return ports{}, err
|
|
}
|
|
opened.state, err = postgresadmin.New(postgresPool)
|
|
if err != nil {
|
|
return ports{}, err
|
|
}
|
|
}
|
|
|
|
providersEnabled := hasEnabledUpstream(configuration)
|
|
if configuration.Distribution.Enabled || configuration.Admin.Enabled || providersEnabled {
|
|
if strings.TrimSpace(configuration.Storage.RedisURL) == "" {
|
|
return ports{}, ErrRedisConfiguration
|
|
}
|
|
redisOptions, err := redis.ParseURL(configuration.Storage.RedisURL)
|
|
if err != nil {
|
|
return ports{}, ErrRedisConfiguration
|
|
}
|
|
redisClient = redis.NewClient(redisOptions)
|
|
if err = redisClient.Ping(ctx).Err(); err != nil {
|
|
return ports{}, contextOr(ctx, ErrRedisUnavailable)
|
|
}
|
|
credentialStore, err := credentials.NewMemoryStore(providerCredentialCapacity(configuration))
|
|
if err != nil {
|
|
return ports{}, err
|
|
}
|
|
adapter, err := redisactivity.New(redisClient, redisactivity.Options{
|
|
Namespace: namespace,
|
|
Credentials: credentialStore,
|
|
OperationTTL: redisOperationTTL,
|
|
MaxCandidateScan: candidateScan(configuration),
|
|
MaxRuntimeCounters: credentialCapacity(configuration),
|
|
MaxInventoryScan: maxInventoryScan(configuration),
|
|
CleanupLimit: redisCleanupLimit,
|
|
})
|
|
if err != nil {
|
|
return ports{}, err
|
|
}
|
|
opened.activity = adapter
|
|
opened.readiness = redisReadiness{client: redisClient}
|
|
opened.credentials = credentialStore
|
|
if providersEnabled {
|
|
stats, statsErr := controllerProvider.NewStatsRecorder(config.MaximumUpstreams)
|
|
if statsErr != nil {
|
|
return ports{}, statsErr
|
|
}
|
|
opened.providerResults = stats
|
|
holderID, holderErr := resolveHolderID(infrastructure.holderID)
|
|
if holderErr != nil {
|
|
return ports{}, holderErr
|
|
}
|
|
opened.coordinator, err = redisprovider.New(redisClient, redisprovider.Options{
|
|
Namespace: namespace, HolderID: holderID,
|
|
LeaseTTL: providerLeaseTTL, RenewEvery: providerRenewEvery,
|
|
RetryInterval: providerRetryInterval, PermitGrace: providerPermitGrace,
|
|
})
|
|
if err != nil {
|
|
return ports{}, err
|
|
}
|
|
}
|
|
}
|
|
if configuration.Metrics.Enabled {
|
|
opened.metricsReadiness = selectMetricsReadiness(
|
|
configuration,
|
|
storeReadiness{postgres: postgresPool, redis: redisClient},
|
|
redisReadiness{client: redisClient},
|
|
)
|
|
}
|
|
return opened, nil
|
|
}
|
|
|
|
func resolveRedisNamespace(configured string) (string, error) {
|
|
if strings.TrimSpace(configured) != configured {
|
|
return "", ErrInvalidOptions
|
|
}
|
|
if configured == "" {
|
|
return redisNamespace, nil
|
|
}
|
|
return configured, nil
|
|
}
|
|
|
|
func selectMetricsReadiness(
|
|
configuration *config.Config,
|
|
admin, activity platformMetrics.ReadinessChecker,
|
|
) platformMetrics.ReadinessChecker {
|
|
if configuration.Distribution.Enabled || hasEnabledUpstream(configuration) {
|
|
return activity
|
|
}
|
|
if configuration.Admin.Enabled {
|
|
return admin
|
|
}
|
|
return alwaysReady{}
|
|
}
|
|
|
|
type alwaysReady struct{}
|
|
|
|
func (alwaysReady) Ready(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return ErrInvalidOptions
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
type storeReadiness struct {
|
|
postgres *pgxpool.Pool
|
|
redis *redis.Client
|
|
}
|
|
|
|
func (readiness storeReadiness) Ready(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return ErrInvalidOptions
|
|
}
|
|
if readiness.postgres != nil {
|
|
if err := readiness.postgres.Ping(ctx); err != nil {
|
|
return contextOr(ctx, ErrPostgresUnavailable)
|
|
}
|
|
}
|
|
if readiness.redis != nil {
|
|
if err := readiness.redis.Ping(ctx).Err(); err != nil {
|
|
return contextOr(ctx, ErrRedisUnavailable)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type redisReadiness struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
func (readiness redisReadiness) Ready(ctx context.Context) error {
|
|
if ctx == nil || readiness.client == nil {
|
|
return ErrRedisUnavailable
|
|
}
|
|
if err := readiness.client.Ping(ctx).Err(); err != nil {
|
|
return contextOr(ctx, ErrRedisUnavailable)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func credentialCapacity(configuration *config.Config) int {
|
|
capacity := 0
|
|
maximum := int(^uint(0) >> 1)
|
|
for _, upstream := range configuration.Upstreams {
|
|
if !upstream.Enabled || upstream.Pool.MaxSize <= 0 {
|
|
continue
|
|
}
|
|
if capacity > maximum-upstream.Pool.MaxSize {
|
|
return maximum
|
|
}
|
|
capacity += upstream.Pool.MaxSize
|
|
}
|
|
if capacity == 0 {
|
|
return 1
|
|
}
|
|
return capacity
|
|
}
|
|
|
|
func providerCredentialCapacity(configuration *config.Config) int {
|
|
capacity := 0
|
|
maximum := int(^uint(0) >> 1)
|
|
for _, upstream := range configuration.Upstreams {
|
|
if upstream.Pool.MaxSize <= 0 {
|
|
continue
|
|
}
|
|
maxInFlight := upstream.Fetch.MaxInFlight
|
|
if maxInFlight <= 0 {
|
|
maxInFlight = 1
|
|
}
|
|
if upstream.Pool.MaxSize > maximum/maxInFlight {
|
|
return maximum
|
|
}
|
|
leases := upstream.Pool.MaxSize * maxInFlight
|
|
if capacity > maximum-leases {
|
|
return maximum
|
|
}
|
|
capacity += leases
|
|
}
|
|
if capacity == 0 {
|
|
return 1
|
|
}
|
|
return capacity
|
|
}
|
|
|
|
func maxInventoryScan(_ *config.Config) int {
|
|
return config.MaximumPoolSize
|
|
}
|
|
|
|
func hasEnabledUpstream(configuration *config.Config) bool {
|
|
if configuration == nil {
|
|
return false
|
|
}
|
|
for _, upstream := range configuration.Upstreams {
|
|
if upstream.Enabled {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func resolveHolderID(configured string) (string, error) {
|
|
if strings.TrimSpace(configured) != configured {
|
|
return "", ErrInvalidOptions
|
|
}
|
|
if configured != "" {
|
|
return configured, nil
|
|
}
|
|
var entropy [16]byte
|
|
if _, err := rand.Read(entropy[:]); err != nil {
|
|
return "", errors.Join(ErrStartup, err)
|
|
}
|
|
return "controller-" + hex.EncodeToString(entropy[:]), nil
|
|
}
|
|
|
|
func candidateScan(configuration *config.Config) int {
|
|
configured := configuration.Distribution.Extraction
|
|
if configured.MaxCountPerRequest > int(^uint(0)>>1)-configured.ReserveForGateway {
|
|
return int(^uint(0) >> 1)
|
|
}
|
|
value := configured.MaxCountPerRequest + configured.ReserveForGateway
|
|
if value < redisMinimumScan {
|
|
return redisMinimumScan
|
|
}
|
|
return value
|
|
}
|
|
|
|
func contextOr(ctx context.Context, fallback error) error {
|
|
if ctx != nil && ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return fallback
|
|
}
|