129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package redisactivity
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
_ "embed"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
extractionDomain "proxy-pool/internal/domain/extraction"
|
|
)
|
|
|
|
type scriptStatus string
|
|
|
|
const (
|
|
scriptOK scriptStatus = "ok"
|
|
scriptInvalid scriptStatus = "invalid"
|
|
scriptNotFound scriptStatus = "not_found"
|
|
scriptConflict scriptStatus = "conflict"
|
|
scriptStale scriptStatus = "stale"
|
|
scriptUnavailable scriptStatus = "unavailable"
|
|
scriptInsufficient scriptStatus = "insufficient"
|
|
scriptAlreadyOwned scriptStatus = "already_owned"
|
|
scriptNotDraining scriptStatus = "not_draining"
|
|
scriptDrainNotReady scriptStatus = "drain_not_ready"
|
|
)
|
|
|
|
type upsertScriptReply struct {
|
|
Status scriptStatus `json:"status"`
|
|
Accepted int `json:"accepted"`
|
|
Inserted int `json:"inserted"`
|
|
Refreshed int `json:"refreshed"`
|
|
Dropped int `json:"dropped"`
|
|
}
|
|
|
|
type healthScriptReply struct {
|
|
Status scriptStatus `json:"status"`
|
|
Record string `json:"record,omitempty"`
|
|
}
|
|
|
|
type extractScriptReply struct {
|
|
Status scriptStatus `json:"status"`
|
|
RequestDigest string `json:"requestDigest"`
|
|
Record string `json:"record,omitempty"`
|
|
}
|
|
|
|
type ownershipScriptReply struct {
|
|
Status scriptStatus `json:"status"`
|
|
Record string `json:"record,omitempty"`
|
|
}
|
|
|
|
type maintenanceScriptReply struct {
|
|
Status scriptStatus `json:"status"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
//go:embed scripts/upsert.lua
|
|
var upsertSource string
|
|
|
|
//go:embed scripts/health.lua
|
|
var healthSource string
|
|
|
|
//go:embed scripts/extract.lua
|
|
var extractSource string
|
|
|
|
//go:embed scripts/ownership.lua
|
|
var ownershipSource string
|
|
|
|
//go:embed scripts/sweep.lua
|
|
var sweepSource string
|
|
|
|
var (
|
|
upsertScript = redis.NewScript(upsertSource)
|
|
healthScript = redis.NewScript(healthSource)
|
|
extractScript = redis.NewScript(extractSource)
|
|
ownershipScript = redis.NewScript(ownershipSource)
|
|
sweepScript = redis.NewScript(sweepSource)
|
|
)
|
|
|
|
func runScript(ctx context.Context, client redis.Scripter, script *redis.Script, keys []string, args ...any) (any, error) {
|
|
result, err := script.Run(ctx, client, keys, args...).Result()
|
|
if err != nil {
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return nil, err
|
|
}
|
|
return nil, errors.Join(
|
|
extractionDomain.ErrStoreUnavailable,
|
|
fmt.Errorf("run redis activity script: %w", err),
|
|
)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func newOperationID() (string, error) {
|
|
var value [16]byte
|
|
if _, err := rand.Read(value[:]); err != nil {
|
|
return "", fmt.Errorf("create redis activity operation ID: %w", err)
|
|
}
|
|
return hex.EncodeToString(value[:]), nil
|
|
}
|
|
|
|
func operationTTLMillis(ttl time.Duration) int64 {
|
|
milliseconds := ttl / time.Millisecond
|
|
if ttl%time.Millisecond != 0 {
|
|
milliseconds++
|
|
}
|
|
return int64(milliseconds)
|
|
}
|
|
|
|
func decodeScriptResult(result any, destination any) error {
|
|
var payload string
|
|
switch value := result.(type) {
|
|
case string:
|
|
payload = value
|
|
case []byte:
|
|
payload = string(value)
|
|
default:
|
|
return errors.Join(extractionDomain.ErrStoreUnavailable, errors.New("invalid Redis script reply type"))
|
|
}
|
|
if err := decodeJSON(payload, destination); err != nil {
|
|
return errors.Join(extractionDomain.ErrStoreUnavailable, fmt.Errorf("decode Redis script reply: %w", err))
|
|
}
|
|
return nil
|
|
}
|