fix: materialize egress checks before basic tasks
This commit is contained in:
parent
b9b323af9b
commit
6f5a2faea6
@ -295,7 +295,10 @@ func (supervisor *ConfiguredSchedulerSupervisor) upstreamDueGroups(
|
||||
egressURLs []string,
|
||||
targets []configuredTargetProfile,
|
||||
) ([]DueSource, error) {
|
||||
groups := []DueSource{basic}
|
||||
// EGRESS and TARGET due reads lazily materialize references from BASIC due
|
||||
// entries. Run them before BASIC so a small batch cannot consume the only
|
||||
// source reference before these independent checks are established.
|
||||
groups := make([]DueSource, 0, 1+len(egressURLs)+len(targets))
|
||||
if egress, supported := supervisor.source.(EgressUpstreamTaskSource); supported {
|
||||
for _, targetURL := range egressURLs {
|
||||
source, err := newEgressUpstreamDueSource(egress, upstreamID, targetURL)
|
||||
@ -314,6 +317,7 @@ func (supervisor *ConfiguredSchedulerSupervisor) upstreamDueGroups(
|
||||
groups = append(groups, source)
|
||||
}
|
||||
}
|
||||
groups = append(groups, basic)
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
|
||||
@ -188,9 +188,50 @@ func TestConfiguredSchedulerSupervisorSchedulesBoundedEgressGroups(t *testing.T)
|
||||
t.Fatalf("NewConfiguredSchedulerSupervisor() = %v", err)
|
||||
}
|
||||
result, err := supervisor.Tick(context.Background())
|
||||
if err != nil || result != (TickResult{Planned: 3, Offered: 3}) || len(sink.tasks) != 1 ||
|
||||
sink.tasks[0].Candidate.Level != healthDomain.LevelEgress || source.sources["provider-a"].egressCalls != 2 {
|
||||
t.Fatalf("Tick() = (%+v, %v); source=%+v sink=%+v", result, err, source, sink.tasks)
|
||||
if err != nil || result != (TickResult{Planned: 3, Offered: 3}) || !sink.offeredLevel(healthDomain.LevelEgress) ||
|
||||
source.sources["provider-a"].egressCalls != 2 {
|
||||
t.Fatalf("Tick() = (%+v, %v); source=%+v sink=%+v", result, err, source, sink.batches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredSchedulerSupervisorMaterializesEveryEgressGroupBeforeBasic(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 11, 15, 0, 0, time.UTC)
|
||||
firstURL := "https://egress-one.example/identity"
|
||||
secondURL := "https://egress-two.example/identity"
|
||||
configuration := &config.Config{
|
||||
Defaults: config.Defaults{Check: config.Check{
|
||||
Interval: config.Duration(time.Minute), MaxInFlight: 1, Timeout: config.Duration(time.Second), MaxAttempts: 1,
|
||||
URLs: []string{firstURL, secondURL},
|
||||
}},
|
||||
Upstreams: map[string]config.Upstream{"provider-a": {Enabled: true}},
|
||||
}
|
||||
source := &upstreamTaskSourceStub{sources: map[string]*dueSourceStub{
|
||||
"provider-a": {candidates: []Candidate{{
|
||||
ProxyID: "basic", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now,
|
||||
}}, egressCandidates: map[string][]Candidate{
|
||||
firstURL: {{ProxyID: "egress-one", State: proxyDomain.StateFetched, Level: healthDomain.LevelEgress, TargetURL: firstURL, DueAt: now}},
|
||||
secondURL: {{ProxyID: "egress-two", State: proxyDomain.StateFetched, Level: healthDomain.LevelEgress, TargetURL: secondURL, DueAt: now}},
|
||||
}},
|
||||
}}
|
||||
sink := &taskSinkStub{}
|
||||
supervisor, err := NewConfiguredSchedulerSupervisor(&configurationSourceStub{configuration: configuration}, source, sink,
|
||||
SchedulerRunnerOptions{PollInterval: time.Second, BatchSize: 1, Now: func() time.Time { return now }})
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfiguredSchedulerSupervisor() = %v", err)
|
||||
}
|
||||
for tick := 0; tick < 2; tick++ {
|
||||
if result, err := supervisor.Tick(context.Background()); err != nil || result != (TickResult{Planned: 1, Offered: 1}) {
|
||||
t.Fatalf("Tick(%d) = (%+v, %v)", tick, result, err)
|
||||
}
|
||||
}
|
||||
item := source.sources["provider-a"]
|
||||
if item.egressCalls != 2 || item.dueCalls != 0 || len(sink.batches) != 2 ||
|
||||
sink.batches[0][0].Candidate.Level != healthDomain.LevelEgress || sink.batches[1][0].Candidate.Level != healthDomain.LevelEgress {
|
||||
t.Fatalf("first two ticks did not establish egress groups: source=%+v batches=%+v", item, sink.batches)
|
||||
}
|
||||
if result, err := supervisor.Tick(context.Background()); err != nil || result != (TickResult{Planned: 1, Offered: 1}) ||
|
||||
item.dueCalls != 1 || len(sink.batches) != 3 || sink.batches[2][0].Candidate.Level != healthDomain.LevelBasic {
|
||||
t.Fatalf("basic Tick() = (%+v, %v), source=%+v batches=%+v", result, err, item, sink.batches)
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,10 +266,9 @@ func TestConfiguredSchedulerSupervisorSchedulesRoutingTargetProfiles(t *testing.
|
||||
t.Fatalf("NewConfiguredSchedulerSupervisor() = %v", err)
|
||||
}
|
||||
result, err := supervisor.Tick(context.Background())
|
||||
if err != nil || result != (TickResult{Planned: 2, Offered: 2}) || len(sink.tasks) != 1 ||
|
||||
sink.tasks[0].Candidate.Level != healthDomain.LevelTarget || sink.tasks[0].Candidate.RoutingName != routingName ||
|
||||
if err != nil || result != (TickResult{Planned: 2, Offered: 2}) || !sink.offeredTarget(routingName, targetURL) ||
|
||||
source.sources["provider-a"].targetCalls != 1 {
|
||||
t.Fatalf("Tick() = (%+v, %v); source=%+v sink=%+v", result, err, source, sink.tasks)
|
||||
t.Fatalf("Tick() = (%+v, %v); source=%+v sink=%+v", result, err, source, sink.batches)
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,13 +400,38 @@ func targetCandidateKey(routingName, targetURL string) string {
|
||||
|
||||
type taskSinkStub struct {
|
||||
tasks []PlannedTask
|
||||
batches [][]PlannedTask
|
||||
offer int
|
||||
}
|
||||
|
||||
func (sink *taskSinkStub) Offer(_ context.Context, tasks []PlannedTask) (int, error) {
|
||||
sink.tasks = append([]PlannedTask(nil), tasks...)
|
||||
sink.batches = append(sink.batches, append([]PlannedTask(nil), tasks...))
|
||||
if sink.offer != 0 {
|
||||
return sink.offer, nil
|
||||
}
|
||||
return len(tasks), nil
|
||||
}
|
||||
|
||||
func (sink *taskSinkStub) offeredLevel(level healthDomain.Level) bool {
|
||||
for _, batch := range sink.batches {
|
||||
for _, task := range batch {
|
||||
if task.Candidate.Level == level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sink *taskSinkStub) offeredTarget(routingName, targetURL string) bool {
|
||||
for _, batch := range sink.batches {
|
||||
for _, task := range batch {
|
||||
if task.Candidate.Level == healthDomain.LevelTarget && task.Candidate.RoutingName == routingName &&
|
||||
task.Candidate.TargetURL == targetURL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
前重建并下发递增版本,Gateway 在同一长连接内原子替换视图;构建失败或流中断时才由
|
||||
SessionSupervisor 按原有退避重连。该机制复用 `RefreshingSnapshotSource`,不降低
|
||||
Delta 的 fail-closed 校验,也不把自动 Drain/ACK 记为已完成。
|
||||
- Health Scheduler 现在先建立 EGRESS 与 Routing TARGET 的独立 due 引用,再投递
|
||||
BASIC 任务;在单项小批次下,短 TTL Proxy 不会因 BASIC 先出队而错过首次出口/目标探测。
|
||||
|
||||
## 2026-07-30
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user