package provider import ( "context" "errors" "fmt" "hash/fnv" "reflect" "time" "proxy-pool/internal/controller/pool" "proxy-pool/internal/domain/activitypool" ) var ( ErrInvalidUpstreamRuntime = errors.New("invalid Provider upstream runtime") ErrUpstreamRuntimeStopped = errors.New("Provider upstream runtime stopped") ) type UpstreamRuntimeConfig struct { Provider Config ReconcilePolicy pool.ReconcilePolicy ReconcileInterval time.Duration } type UpstreamRuntimeDependencies struct { Coordinator Coordinator Inventory pool.InventoryReader Adapter ProviderAdapter Parser Parser Activity activitypool.Upserter Results ResultRecorder } // UpstreamRuntime owns every leader-scoped object for one Upstream. A fresh // local budget and coalescing signal are created for each leadership term. type UpstreamRuntime struct { config UpstreamRuntimeConfig dependencies UpstreamRuntimeDependencies sleeper Sleeper } func NewUpstreamRuntime( config UpstreamRuntimeConfig, dependencies UpstreamRuntimeDependencies, ) (*UpstreamRuntime, error) { if config.ReconcileInterval <= 0 || nilRuntimeDependency(dependencies.Coordinator) || nilRuntimeDependency(dependencies.Inventory) || nilRuntimeDependency(dependencies.Adapter) || nilRuntimeDependency(dependencies.Parser) || nilRuntimeDependency(dependencies.Activity) || nilRuntimeDependency(dependencies.Results) { return nil, ErrInvalidUpstreamRuntime } runtime := &UpstreamRuntime{ config: config, dependencies: dependencies, sleeper: timerSleeper{}, } if _, err := runtime.newLeaderTerm(); err != nil { return nil, errors.Join(ErrInvalidUpstreamRuntime, err) } return runtime, nil } func (runtime *UpstreamRuntime) Run(ctx context.Context) error { if runtime == nil || ctx == nil { return ErrInvalidUpstreamRuntime } limits := CoordinationLimits{ RequestInterval: runtime.config.Provider.RequestInterval, MaxInFlight: runtime.config.Provider.MaxInFlight, MaxAttemptDuration: runtime.config.Provider.Timeout, MaxTotal: runtime.config.Provider.MaxTotal, } err := runtime.dependencies.Coordinator.RunLeader( ctx, runtime.config.Provider.UpstreamID, limits, func(leaderCtx context.Context, session LeaderSession) error { term, buildErr := runtime.newLeaderTerm() if buildErr != nil { return buildErr } return term.run(leaderCtx, session) }, ) if ctx.Err() != nil { return ctx.Err() } if err == nil { return ErrUpstreamRuntimeStopped } return err } func (runtime *UpstreamRuntime) ID() string { if runtime == nil { return "" } return runtime.config.Provider.UpstreamID } func (runtime *UpstreamRuntime) newLeaderTerm() (*upstreamLeaderTerm, error) { budget, err := pool.NewFetchBudget(pool.FetchBudgetConfig{ UpstreamID: runtime.config.Provider.UpstreamID, MaxSize: runtime.config.Provider.MaxSize, ExpectedPerFetch: runtime.config.ReconcilePolicy.ExpectedPerFetch, }) if err != nil { return nil, err } providerConfig := runtime.config.Provider providerConfig.RequestInterval = 0 providerReconciler, err := NewReconciler(providerConfig, Ports{ Adapter: runtime.dependencies.Adapter, Parser: runtime.dependencies.Parser, Activity: runtime.dependencies.Activity, Results: runtime.dependencies.Results, Capacity: budget, }) if err != nil { return nil, err } poolReconciler, err := pool.NewReconciler( runtime.config.ReconcilePolicy, budget, providerReconciler, ) if err != nil { return nil, err } return &upstreamLeaderTerm{ upstreamID: runtime.config.Provider.UpstreamID, interval: runtime.config.ReconcileInterval, safetyMargin: runtime.config.ReconcilePolicy.SafetyMargin, inventory: runtime.dependencies.Inventory, pool: poolReconciler, provider: providerReconciler, sleeper: runtime.sleeper, }, nil } type upstreamLeaderTerm struct { upstreamID string interval time.Duration safetyMargin time.Duration inventory pool.InventoryReader pool *pool.Reconciler provider *Reconciler sleeper Sleeper } func (term *upstreamLeaderTerm) run(ctx context.Context, session LeaderSession) error { termCtx, cancel := context.WithCancel(ctx) defer cancel() done := make(chan error, 2) go func() { done <- term.provider.RunLeader(termCtx, session) }() go func() { done <- term.reconcileInventory(termCtx) }() first := <-done cancel() second := <-done if ctx.Err() != nil { return nil } if first == nil { first = ErrUpstreamRuntimeStopped } if second != nil && !errors.Is(second, context.Canceled) { return errors.Join(first, second) } return first } func (term *upstreamLeaderTerm) reconcileInventory(ctx context.Context) error { if delay := initialReconcileDelay(term.upstreamID, term.interval); delay > 0 { if err := term.sleeper.Sleep(ctx, delay); err != nil { if ctx.Err() != nil { return nil } return fmt.Errorf("wait for initial Provider inventory reconciliation: %w", err) } } for ctx.Err() == nil { inventory, err := term.inventory.ReadInventory( ctx, term.upstreamID, term.safetyMargin, ) if err == nil && inventory.Managed >= 0 && inventory.AvailableSlots >= 0 { term.pool.ReconcileSnapshot(inventory) } if err := term.sleeper.Sleep(ctx, term.interval); err != nil { if ctx.Err() != nil { return nil } return fmt.Errorf("wait for Provider inventory reconciliation: %w", err) } } return nil } func initialReconcileDelay(upstreamID string, interval time.Duration) time.Duration { window := min(interval/4, 250*time.Millisecond) if window <= 1 { return 0 } digest := fnv.New64a() _, _ = digest.Write([]byte(upstreamID)) return time.Duration(digest.Sum64() % uint64(window)) } func nilRuntimeDependency(value any) bool { if value == nil { return true } reflected := reflect.ValueOf(value) switch reflected.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return reflected.IsNil() default: return false } }