proxy-pool/internal/adapters/redisactivity/health.go
youfak 0a904a7b6b
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: add atomic global health reducer
2026-07-31 17:55:06 +08:00

130 lines
4.6 KiB
Go

package redisactivity
import (
"context"
"encoding/hex"
"proxy-pool/internal/domain/activitypool"
healthDomain "proxy-pool/internal/domain/health"
)
var _ activitypool.HealthStore = (*Adapter)(nil)
var _ activitypool.GlobalHealthStore = (*Adapter)(nil)
func (a *Adapter) ApplyHealth(ctx context.Context, update activitypool.HealthUpdate) (activitypool.Entry, error) {
if ctx == nil {
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
}
if err := ctx.Err(); err != nil {
return activitypool.Entry{}, err
}
if a == nil || update.ProxyID == "" || update.CheckedAt.IsZero() || update.Latency < 0 ||
!validProxyState(string(update.NextState)) {
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
}
operationID, err := newOperationID()
if err != nil {
return activitypool.Entry{}, err
}
result, err := runScript(ctx, a.client, healthScript, []string{
a.keys.records, a.keys.unique, a.keys.idkeys, a.keys.expiry, a.keys.available,
a.keys.inventory, a.keys.stateInventory, a.keys.owners, a.keys.ownerExpiry,
a.keys.operation(operationID),
}, update.CheckedAt.UnixMilli(), string(update.NextState), int64(update.Latency),
a.options.CleanupLimit, operationTTLMillis(a.options.OperationTTL), update.ProxyID)
if err != nil {
return activitypool.Entry{}, err
}
var reply healthScriptReply
if err := decodeScriptResult(result, &reply); err != nil {
return activitypool.Entry{}, err
}
switch reply.Status {
case scriptNotFound:
return activitypool.Entry{}, activitypool.ErrActivityNotFound
case scriptStale:
return activitypool.Entry{}, activitypool.ErrStaleHealthUpdate
case scriptInvalid:
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
case scriptOK:
if reply.Record == "" {
return activitypool.Entry{}, invalidScriptReply("health reply omitted record")
}
record, err := decodeProxyRecord(reply.Record)
if err != nil {
return activitypool.Entry{}, invalidScriptReply("health reply contained an invalid record")
}
return proxyRecordEntry(record), nil
default:
return activitypool.Entry{}, invalidScriptReply("unexpected health status")
}
}
// ApplyGlobalObservation reduces one BASIC or EGRESS observation inside the
// same Redis Lua transaction that updates state indexes and inventory.
func (a *Adapter) ApplyGlobalObservation(
ctx context.Context,
command activitypool.GlobalHealthCommand,
) (activitypool.Entry, error) {
if ctx == nil {
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
}
if err := ctx.Err(); err != nil {
return activitypool.Entry{}, err
}
normalized, err := healthDomain.NormalizeObservation(command.Observation)
if err != nil {
return activitypool.Entry{}, err
}
if a == nil || normalized.Level == healthDomain.LevelTarget || command.MaxConsecutiveFailures <= 0 {
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
}
digest, err := healthDomain.ObservationDigest(normalized)
if err != nil {
return activitypool.Entry{}, err
}
operationID, err := newOperationID()
if err != nil {
return activitypool.Entry{}, err
}
success := "0"
if normalized.Success {
success = "1"
}
result, err := runScript(ctx, a.client, healthScript, []string{
a.keys.records, a.keys.unique, a.keys.idkeys, a.keys.expiry, a.keys.available,
a.keys.inventory, a.keys.stateInventory, a.keys.owners, a.keys.ownerExpiry,
a.keys.operation(operationID),
}, normalized.ObservedAt.UnixMilli(), "", int64(normalized.Latency),
a.options.CleanupLimit, operationTTLMillis(a.options.OperationTTL), normalized.ProxyID,
"global", success, command.MaxConsecutiveFailures, normalized.TaskID, hex.EncodeToString(digest[:]))
if err != nil {
return activitypool.Entry{}, err
}
var reply healthScriptReply
if err := decodeScriptResult(result, &reply); err != nil {
return activitypool.Entry{}, err
}
switch reply.Status {
case scriptNotFound:
return activitypool.Entry{}, activitypool.ErrActivityNotFound
case scriptStale:
return activitypool.Entry{}, healthDomain.ErrStaleObservation
case scriptConflict:
return activitypool.Entry{}, healthDomain.ErrConflictingObservation
case scriptInvalid:
return activitypool.Entry{}, activitypool.ErrInvalidHealthUpdate
case scriptOK:
if reply.Record == "" {
return activitypool.Entry{}, invalidScriptReply("global health reply omitted record")
}
record, err := decodeProxyRecord(reply.Record)
if err != nil {
return activitypool.Entry{}, invalidScriptReply("global health reply contained an invalid record")
}
return proxyRecordEntry(record), nil
default:
return activitypool.Entry{}, invalidScriptReply("unexpected global health status")
}
}