proxy-pool/internal/controller/extraction/service.go

179 lines
5.2 KiB
Go

package extraction
import (
"context"
"errors"
"fmt"
"time"
domain "github.com/proxy-pool/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")
)
type Policy struct {
MaxCountPerRequest int
DefaultFulfillment domain.Fulfillment
MinRemainingTTL time.Duration
MaxHealthCheckAge time.Duration
ReserveForGateway int
}
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 {
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 = "anonymous"
}
result, err := s.store.Extract(ctx, domain.Command{
RequestID: request.RequestID,
ClientID: clientID,
SourceIP: request.SourceIP,
IdempotencyKey: request.IdempotencyKey,
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
}