refactor: share health task contracts
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

This commit is contained in:
youfak 2026-07-31 21:37:41 +08:00
parent 9934c659ab
commit 0f029c6127
3 changed files with 130 additions and 106 deletions

View File

@ -28,36 +28,19 @@ type SchedulePolicy struct {
MaxAttempts int MaxAttempts int
} }
// Candidate is a bounded record supplied by a due-index query. The Planner // Task contracts live in the health domain so every shared store can use the
// never scans proxies and never starts a goroutine for a candidate. // same boundary without importing Controller orchestration code.
type Candidate struct { type Candidate = healthDomain.Candidate
ProxyID string type Priority = healthDomain.Priority
State proxyDomain.State type PlannedTask = healthDomain.PlannedTask
Level healthDomain.Level
RoutingName string
TargetURL string
DueAt time.Time
}
type Priority uint8
const ( const (
PriorityFetched Priority = iota + 1 PriorityFetched = healthDomain.PriorityFetched
PrioritySuspect PrioritySuspect = healthDomain.PrioritySuspect
PriorityUnhealthy PriorityUnhealthy = healthDomain.PriorityUnhealthy
PriorityAvailable PriorityAvailable = healthDomain.PriorityAvailable
) )
// PlannedTask is transport-neutral work ready for a leased broker to assign
// to a Checker. The task ID and proxy credential material are added only by
// the Controller's broker after it atomically claims the task.
type PlannedTask struct {
Candidate Candidate
Priority Priority
Deadline time.Time
Attempts int
}
// Planner is stateless and safe for concurrent callers. Its lack of internal // Planner is stateless and safe for concurrent callers. Its lack of internal
// queues makes maxInFlight and batch bounds explicit at the storage boundary. // queues makes maxInFlight and batch bounds explicit at the storage boundary.
type Planner struct { type Planner struct {
@ -129,7 +112,7 @@ func (planner *Planner) Plan(now time.Time, inFlight, maxTasks int, candidates [
if !eligible[left].candidate.DueAt.Equal(eligible[right].candidate.DueAt) { if !eligible[left].candidate.DueAt.Equal(eligible[right].candidate.DueAt) {
return eligible[left].candidate.DueAt.Before(eligible[right].candidate.DueAt) return eligible[left].candidate.DueAt.Before(eligible[right].candidate.DueAt)
} }
return candidateIdentity(eligible[left].candidate) < candidateIdentity(eligible[right].candidate) return healthDomain.CandidateIdentity(eligible[left].candidate) < healthDomain.CandidateIdentity(eligible[right].candidate)
}) })
if len(eligible) > limit { if len(eligible) > limit {
eligible = eligible[:limit] eligible = eligible[:limit]
@ -180,7 +163,3 @@ func (planner *Planner) validateCandidate(candidate Candidate) (Priority, bool,
return 0, false, nil return 0, false, nil
} }
} }
func candidateIdentity(candidate Candidate) string {
return candidate.ProxyID + "\x00" + string(candidate.Level) + "\x00" + candidate.RoutingName + "\x00" + candidate.TargetURL
}

View File

@ -6,7 +6,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@ -30,79 +29,12 @@ var (
const defaultMaxTasksPerClaim = 128 const defaultMaxTasksPerClaim = 128
// TaskMaterial is the short-lived proxy connection material needed to execute type TaskMaterial = healthDomain.TaskMaterial
// one probe. It crosses only the authenticated Checker control stream and is type TaskMaterialResolver = healthDomain.TaskMaterialResolver
// deliberately redacted from formatted values and persistence APIs. type TaskMaterialResolverFunc = healthDomain.TaskMaterialResolverFunc
type TaskMaterial struct { type TaskClaim = healthDomain.TaskClaim
Protocol proxyDomain.Scheme type LeasedTask = healthDomain.LeasedTask
Host string type TaskBroker = healthDomain.TaskBroker
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 a task's endpoint and credential material at
// claim time. A production implementation reads the Controller-local
// credential store; Checkers never read 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. MaxInFlight
// applies across all streams for the checker ID, so reconnecting cannot grow
// its local work window.
type TaskClaim struct {
CheckerID string
InstanceID string
MaxInFlight int
SupportedLevels []healthDomain.Level
}
// LeasedTask is a Controller-assigned task. It holds material only in memory
// for the duration of a task lease and must not be logged.
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 healthDomain.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. The Scheduler uses Offer,
// Checkers use Claim, and ReportObservations fences facts against the lease.
// The production Redis implementation will use this exact contract.
type TaskBroker interface {
TaskSink
Claim(context.Context, TaskClaim) ([]LeasedTask, error)
AuthorizeObservation(context.Context, string, string, healthDomain.Observation, time.Time) error
CompleteObservation(context.Context, string, string, healthDomain.Observation, time.Time) error
}
type MemoryTaskBrokerOptions struct { type MemoryTaskBrokerOptions struct {
LeaseTTL time.Duration LeaseTTL time.Duration
@ -453,7 +385,7 @@ func validateTaskMaterial(material TaskMaterial) error {
} }
func deterministicTaskID(task PlannedTask) string { func deterministicTaskID(task PlannedTask) string {
payload := candidateIdentity(task.Candidate) + "\x00" + task.Deadline.UTC().Format(time.RFC3339Nano) + "\x00" + payload := healthDomain.CandidateIdentity(task.Candidate) + "\x00" + task.Deadline.UTC().Format(time.RFC3339Nano) + "\x00" +
strconv.Itoa(task.Attempts) strconv.Itoa(task.Attempts)
digest := sha256.Sum256([]byte(payload)) digest := sha256.Sum256([]byte(payload))
return "check_" + hex.EncodeToString(digest[:]) return "check_" + hex.EncodeToString(digest[:])

View File

@ -0,0 +1,113 @@
package health
import (
"context"
"fmt"
"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
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
Attempts int
}
// 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
}