proxy-pool/internal/domain/health/task.go
youfak 8efabda84b
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: schedule shared health tasks per upstream
2026-07-31 22:07:06 +08:00

130 lines
4.0 KiB
Go

package health
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"time"
proxyDomain "proxy-pool/internal/domain/proxy"
)
// Candidate is one bounded due-index item. It identifies a check without
// carrying endpoint credentials or any persistence-specific representation.
type Candidate struct {
ProxyID string
UpstreamID string
State proxyDomain.State
Level Level
RoutingName string
TargetURL string
DueAt time.Time
}
// CandidateIdentity is stable across Controller instances and is suitable for
// deterministic task identity and jitter derivation.
func CandidateIdentity(candidate Candidate) string {
return candidate.ProxyID + "\x00" + string(candidate.Level) + "\x00" + candidate.RoutingName + "\x00" + candidate.TargetURL
}
type Priority uint8
const (
PriorityFetched Priority = iota + 1
PrioritySuspect
PriorityUnhealthy
PriorityAvailable
)
// PlannedTask is transport-neutral work ready for a shared leased task store.
type PlannedTask struct {
Candidate Candidate
Priority Priority
Deadline time.Time
NextDue time.Time
Attempts int
MaxInFlight int
}
// TaskIDFor derives an idempotent task identity from all execution-relevant
// plan fields. Multiple Controller replicas can therefore offer the same plan
// without creating duplicate check work.
func TaskIDFor(task PlannedTask) string {
payload := CandidateIdentity(task.Candidate) + "\x00" + task.Deadline.UTC().Format(time.RFC3339Nano) + "\x00" +
task.NextDue.UTC().Format(time.RFC3339Nano) + "\x00" + strconv.Itoa(task.Attempts)
digest := sha256.Sum256([]byte(payload))
return "check_" + hex.EncodeToString(digest[:])
}
// TaskMaterial is short-lived proxy connection material. It crosses only the
// authenticated Checker control stream and must never be logged or persisted
// by a task queue.
type TaskMaterial struct {
Protocol proxyDomain.Scheme
Host string
Port uint16
SecretRef string
CredentialVersion string
Username string
Password string
}
func (TaskMaterial) Format(state fmt.State, _ rune) {
_, _ = state.Write([]byte("health.TaskMaterial{Credentials:<redacted>}"))
}
// TaskMaterialResolver resolves endpoint and credential material only when a
// task lease is granted. Checkers do not directly access Redis or PostgreSQL.
type TaskMaterialResolver interface {
ResolveCheckTask(context.Context, Candidate) (TaskMaterial, error)
}
type TaskMaterialResolverFunc func(context.Context, Candidate) (TaskMaterial, error)
func (resolver TaskMaterialResolverFunc) ResolveCheckTask(ctx context.Context, candidate Candidate) (TaskMaterial, error) {
return resolver(ctx, candidate)
}
// TaskClaim is one bounded pull request from a Checker process.
type TaskClaim struct {
CheckerID string
InstanceID string
MaxInFlight int
SupportedLevels []Level
}
// LeasedTask is a Controller-assigned task. Its material is kept in memory
// only for the task lease duration and is redacted from formatted values.
type LeasedTask struct {
TaskID string
LeaseToken string
ProxyID string
Protocol proxyDomain.Scheme
Host string
Port uint16
SecretRef string
CredentialVersion string
Username string
Password string
Level Level
RoutingName string
TargetURL string
Deadline time.Time
Attempts int
}
func (LeasedTask) Format(state fmt.State, _ rune) {
_, _ = state.Write([]byte("health.LeasedTask{Credentials:<redacted>}"))
}
// TaskBroker is the shared task lease boundary. Schedulers offer bounded
// work, Checkers claim work, and observation commits are fenced by a lease.
type TaskBroker interface {
Offer(context.Context, []PlannedTask) (int, error)
Claim(context.Context, TaskClaim) ([]LeasedTask, error)
AuthorizeObservation(context.Context, string, string, Observation, time.Time) error
CompleteObservation(context.Context, string, string, Observation, time.Time) error
}