package extraction import ( "context" "errors" "fmt" "time" domain "proxy-pool/internal/domain/extraction" ) var ( ErrInvalidRequest = errors.New("invalid extraction request") ErrCountExceeded = errors.New("extraction count exceeds policy") ErrInvalidFulfillment = errors.New("invalid extraction fulfillment") ErrInvalidServicePolicy = errors.New("invalid extraction service policy") ErrAdmissionRejected = errors.New("extraction admission rejected") ErrUnavailable = errors.New("extraction service unavailable") ) type Policy struct { MaxCountPerRequest int DefaultFulfillment domain.Fulfillment MinRemainingTTL time.Duration MaxHealthCheckAge time.Duration ReserveForGateway int IdempotencyTTL time.Duration } type Filters struct { Protocols []string Regions []string Carriers []string Upstreams []string } type Request struct { RequestID string ClientID string SourceIP string IdempotencyKey string Count int Fulfillment domain.Fulfillment Filters Filters } type ExtractedProxy struct { ID string Protocol string Host string Port uint16 Username string Password string URL string Region string Carrier string Upstream string ExpiresAt time.Time RemainingTTLSeconds int64 ExtractedAt time.Time } type Response struct { RequestID string Requested int Returned int Proxies []ExtractedProxy } type Service struct { store domain.Store policy Policy admission Admission now func() time.Time } type Admission interface { Admit(context.Context, string) error } func NewService(store domain.Store, policy Policy, admission Admission, now func() time.Time) (*Service, error) { if admission == nil { return nil, fmt.Errorf("%w: admission is required", ErrInvalidServicePolicy) } if store == nil { return nil, fmt.Errorf("%w: store is required", ErrInvalidServicePolicy) } if policy.MaxCountPerRequest <= 0 || policy.MinRemainingTTL < 0 || policy.MaxHealthCheckAge < 0 || policy.ReserveForGateway < 0 || policy.IdempotencyTTL < 0 { return nil, ErrInvalidServicePolicy } if policy.DefaultFulfillment != domain.Partial && policy.DefaultFulfillment != domain.AllOrNothing { return nil, ErrInvalidServicePolicy } if now == nil { now = time.Now } return &Service{store: store, policy: policy, admission: admission, now: now}, nil } func (s *Service) Extract(ctx context.Context, request Request) (Response, error) { response := Response{RequestID: request.RequestID, Requested: request.Count} if request.RequestID == "" || (request.ClientID == "" && request.SourceIP == "") || request.Count <= 0 { return response, ErrInvalidRequest } if request.Count > s.policy.MaxCountPerRequest { return response, ErrCountExceeded } fulfillment := request.Fulfillment if fulfillment == "" { fulfillment = s.policy.DefaultFulfillment } if fulfillment != domain.Partial && fulfillment != domain.AllOrNothing { return response, ErrInvalidFulfillment } if err := s.admission.Admit(ctx, admissionKey(request)); err != nil { return response, errors.Join(ErrAdmissionRejected, err) } now := s.now().UTC() clientID := request.ClientID if clientID == "" { clientID = request.SourceIP } result, err := s.store.Extract(ctx, domain.Command{ RequestID: request.RequestID, ClientID: clientID, SourceIP: request.SourceIP, IdempotencyKey: request.IdempotencyKey, IdempotencyTTL: s.policy.IdempotencyTTL, Requested: request.Count, Fulfillment: fulfillment, Now: now, MinRemainingTTL: s.policy.MinRemainingTTL, MaxHealthCheckAge: s.policy.MaxHealthCheckAge, ReserveForGateway: s.policy.ReserveForGateway, Protocols: append([]string(nil), request.Filters.Protocols...), Regions: append([]string(nil), request.Filters.Regions...), Carriers: append([]string(nil), request.Filters.Carriers...), Upstreams: append([]string(nil), request.Filters.Upstreams...), }) if err != nil { return response, err } response.Proxies = make([]ExtractedProxy, 0, len(result.Items)) extractedAt := result.ExtractedAt if extractedAt.IsZero() { extractedAt = now } for _, candidate := range result.Items { remaining := int64(0) if !candidate.ExpiresAt.IsZero() && candidate.ExpiresAt.After(extractedAt) { remaining = int64(candidate.ExpiresAt.Sub(extractedAt) / time.Second) } response.Proxies = append(response.Proxies, ExtractedProxy{ ID: candidate.ID, Protocol: candidate.Protocol, Host: candidate.Host, Port: candidate.Port, Username: candidate.Username, Password: candidate.Password, URL: candidate.URL, Region: candidate.Region, Carrier: candidate.Carrier, Upstream: candidate.Upstream, ExpiresAt: candidate.ExpiresAt, RemainingTTLSeconds: remaining, ExtractedAt: extractedAt, }) } response.Returned = len(response.Proxies) return response, nil } func admissionKey(request Request) string { if request.ClientID != "" { return "client:" + request.ClientID } return "source:" + request.SourceIP }