iop/apps/edge/internal/service/run_dispatch_internal_test.go
toki 4dcf6f0cf9 feat(edge): provider 리소스 승인 소유권 정렬 구현
- 노드 연결 2단계 핸드셰이크 추가 (Register→NodeReady→DispatchReady)
- ConnectionGeneration 기반 연결 세대 관리로 stale 연결 차폐
- configured 노드 catalog 기반 snapshot rebuild (Connected 상태 분리)
- provider 리스 소유권 일원화: edge가 소유권 승인·반환 전까지 대기
- 모델 대기열 승인/해제/스냅샷 서비스 구현
- 재연결 준비도 통합 테스트, 아카이브된 하위태스크 8건 포함
2026-07-22 18:10:54 +09:00

2031 lines
73 KiB
Go

package service
import (
"context"
"net"
"strings"
"sync"
"testing"
"time"
toki "git.toki-labs.com/toki/proto-socket/go"
"google.golang.org/protobuf/proto"
edgeevents "iop/apps/edge/internal/events"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func TestRunHandleCloseIsIdempotent(t *testing.T) {
calls := 0
handle := &RunHandle{
close: func() {
calls++
},
}
handle.Close()
handle.Close()
if calls != 1 {
t.Fatalf("close called %d times, want 1", calls)
}
}
func waitForCondition(t *testing.T, cond func() bool, msg string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal(msg)
}
func inflightRunCount(q *modelQueueManager) int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.leaseByRun)
}
// TestRouteProviderTunnelFrameDoesNotPublishToBus verifies that tunnel frames
// reach only the request-bound tunnel subscriber and never the run event bus
// fanout (negative test for the passthrough/no-sideband boundary).
func TestRouteProviderTunnelFrameDoesNotPublishToBus(t *testing.T) {
bus := edgeevents.NewBus()
svc := New(edgenode.NewRegistry(), bus)
runCh, unsubRun := bus.SubscribeAllRuns(8)
defer unsubRun()
tunnelCh, unsubTunnel := svc.tunnels.subscribe("tunnel-neg-1", 8)
defer unsubTunnel()
frame := &iop.ProviderTunnelFrame{
RunId: "run-neg-1",
TunnelId: "tunnel-neg-1",
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: raw provider bytes\n\n"),
}
svc.RouteProviderTunnelFrame(frame)
select {
case got := <-tunnelCh:
if string(got.GetBody()) != string(frame.GetBody()) {
t.Fatalf("tunnel frame body mismatch: %q", got.GetBody())
}
case <-time.After(time.Second):
t.Fatal("tunnel subscriber did not receive the frame")
}
select {
case e := <-runCh:
t.Fatalf("tunnel frame leaked into run event bus: %+v", e)
case <-time.After(100 * time.Millisecond):
}
}
// TestRouteProviderTunnelFrameWithoutSubscriberIsDropped verifies routing a
// frame with no subscriber neither blocks nor panics.
func TestRouteProviderTunnelFrameWithoutSubscriberIsDropped(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{TunnelId: "no-sub"})
}
type providerTunnelTestEnv struct {
svc *Service
nodeClient *toki.TcpClient
capturedMu sync.Mutex
captured *iop.ProviderTunnelRequest
}
func (e *providerTunnelTestEnv) capturedRequest() *iop.ProviderTunnelRequest {
e.capturedMu.Lock()
defer e.capturedMu.Unlock()
return e.captured
}
// newProviderTunnelTestEnv wires a provider-pool service to a fake node over
// net.Pipe and captures the ProviderTunnelRequest the node receives.
func newProviderTunnelTestEnv(t *testing.T) *providerTunnelTestEnv {
t.Helper()
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
env := &providerTunnelTestEnv{nodeClient: nodeClient}
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(req *iop.ProviderTunnelRequest) {
env.capturedMu.Lock()
env.captured = req
env.capturedMu.Unlock()
})
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-pool",
Runtime: config.RuntimeConf{Concurrency: 4},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-vllm-01",
Adapter: "vllm-gpu",
Type: "vllm",
Models: []string{"served-qwen"},
Health: "available",
Capacity: 1,
},
},
})
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-pool",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
svc := New(reg, edgeevents.NewBus())
svc.SetNodeStore(store)
svc.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm-01": "served-qwen"}},
})
env.svc = svc
return env
}
// TestSubmitProviderTunnelProviderPoolSendsRequestAndReleasesSlotOnEnd covers
// the provider-pool tunnel path end to end: admission rewrites adapter/target,
// BuildBody receives the served target, ordered frames reach the handle, and
// the admission slot is released on the END frame.
func TestSubmitProviderTunnelProviderPoolSendsRequestAndReleasesSlotOnEnd(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
var builtTarget string
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-001",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
BuildBody: func(target string) ([]byte, error) {
builtTarget = target
return []byte(`{"model":"` + target + `","stream":true}`), nil
},
Stream: true,
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
defer handle.Close()
if builtTarget != "served-qwen" {
t.Errorf("BuildBody target: got %q, want served-qwen", builtTarget)
}
dispatch := handle.Dispatch()
if dispatch.Adapter != "vllm-gpu" || dispatch.Target != "served-qwen" {
t.Errorf("dispatch adapter/target: got %q/%q", dispatch.Adapter, dispatch.Target)
}
waitForCondition(t, func() bool { return env.capturedRequest() != nil },
"fake node did not receive ProviderTunnelRequest")
captured := env.capturedRequest()
if captured.GetAdapter() != "vllm-gpu" || captured.GetTarget() != "served-qwen" {
t.Errorf("wire adapter/target: got %q/%q", captured.GetAdapter(), captured.GetTarget())
}
if !strings.Contains(string(captured.GetBody()), `"model":"served-qwen"`) {
t.Errorf("wire body missing served model: %s", captured.GetBody())
}
if inflightRunCount(svc.queue) != 1 {
t.Fatalf("expected 1 inflight run after dispatch, got %d", inflightRunCount(svc.queue))
}
tunnelID := captured.GetTunnelId()
frames := []*iop.ProviderTunnelFrame{
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 0, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200},
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 1, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: chunk-1\n\n")},
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 2, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true},
}
for _, f := range frames {
svc.RouteProviderTunnelFrame(f)
}
stream := handle.Stream()
for i, want := range frames {
select {
case got := <-stream.Frames:
if got.GetSequence() != want.GetSequence() || got.GetKind() != want.GetKind() {
t.Fatalf("frame %d: got seq=%d kind=%v", i, got.GetSequence(), got.GetKind())
}
case <-time.After(time.Second):
t.Fatalf("frame %d not delivered", i)
}
}
select {
case _, open := <-stream.Frames:
if open {
t.Fatal("expected frame channel to close after END")
}
case <-time.After(time.Second):
t.Fatal("frame channel not closed after END")
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after END frame")
}
// TestSubmitProviderTunnelCloseReleasesSlot verifies the cancel path: closing
// the handle before a terminal frame releases the provider-pool slot.
func TestSubmitProviderTunnelCloseReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-cancel",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
if inflightRunCount(svc.queue) != 1 {
t.Fatalf("expected 1 inflight run after dispatch, got %d", inflightRunCount(svc.queue))
}
handle.Close()
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after Close")
}
// TestProviderTunnelRouterBackpressureBlocksWithoutDrop verifies the router
// blocks the producer while the subscriber buffer is full instead of dropping
// frames, and delivers every frame in order once the subscriber drains.
func TestProviderTunnelRouterBackpressureBlocksWithoutDrop(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
ch, unsub := svc.tunnels.subscribe("tunnel-bp", 2)
defer unsub()
const total = 6
routed := make(chan struct{})
go func() {
defer close(routed)
for i := 0; i < total; i++ {
svc.tunnels.route(&iop.ProviderTunnelFrame{
TunnelId: "tunnel-bp",
Sequence: int64(i),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
})
}
}()
// With buffer 2 the producer cannot finish all routes before the
// subscriber drains: full buffer must block, not drop.
select {
case <-routed:
t.Fatal("route calls finished while the subscriber buffer was full; expected blocking backpressure")
case <-time.After(100 * time.Millisecond):
}
for i := 0; i < total; i++ {
select {
case f := <-ch:
if f.GetSequence() != int64(i) {
t.Fatalf("frame %d: got sequence %d; frames must stay ordered and lossless under backpressure", i, f.GetSequence())
}
case <-time.After(time.Second):
t.Fatalf("frame %d not delivered after draining", i)
}
}
select {
case <-routed:
case <-time.After(time.Second):
t.Fatal("producer still blocked after all frames were drained")
}
}
// TestProviderTunnelRouterCloseUnblocksBlockedRoute verifies unsubscribe (the
// handle Close/cancel path) releases a route call blocked on a full buffer, so
// an abandoned tunnel can never wedge the transport frame reader.
func TestProviderTunnelRouterCloseUnblocksBlockedRoute(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
_, unsub := svc.tunnels.subscribe("tunnel-close", 1)
frame := func(seq int64) *iop.ProviderTunnelFrame {
return &iop.ProviderTunnelFrame{
TunnelId: "tunnel-close",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
}
}
blocked := make(chan struct{})
go func() {
defer close(blocked)
svc.tunnels.route(frame(0)) // fills the buffer
svc.tunnels.route(frame(1)) // blocks until close releases it
}()
select {
case <-blocked:
t.Fatal("second route must block on the full subscriber buffer")
case <-time.After(100 * time.Millisecond):
}
unsub()
select {
case <-blocked:
case <-time.After(time.Second):
t.Fatal("unsubscribe did not release the blocked route call")
}
// A late frame for the closed tunnel is dropped without blocking.
svc.tunnels.route(frame(2))
}
// TestProviderTunnelReleaseExactlyOnceAcrossEndAndClose verifies the tracked
// provider-pool slot (capacity 1) is released exactly once when both the
// terminal END frame and handle Close fire, and that the freed slot admits the
// next tunnel immediately instead of queueing.
func TestProviderTunnelReleaseExactlyOnceAcrossEndAndClose(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
submit := func(runID string) ProviderTunnelResult {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
handle, err := svc.SubmitProviderTunnel(ctx, SubmitProviderTunnelRequest{
RunID: runID,
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel(%s): %v", runID, err)
}
return handle
}
handle1 := submit("run-release-1")
waitForCondition(t, func() bool {
captured := env.capturedRequest()
return captured != nil && captured.GetRunId() == "run-release-1"
}, "fake node did not receive the first ProviderTunnelRequest")
captured := env.capturedRequest()
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{
RunId: captured.GetRunId(),
TunnelId: captured.GetTunnelId(),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
End: true,
})
stream := handle1.Stream()
for {
if _, open := <-stream.Frames; !open {
break
}
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after END frame")
if got := leaseCount(svc.queue); got != 0 {
t.Fatalf("live leases after END frame: got %d, want 0", got)
}
// Close after the terminal frame must stay a no-op release: the lease it
// would target is already gone, so there is nothing left to decrement.
handle1.Close()
if got := inflightRunCount(svc.queue); got != 0 {
t.Fatalf("inflight count after END+Close: got %d, want 0", got)
}
if got := leaseCount(svc.queue); got != 0 {
t.Fatalf("live leases after END+Close: got %d, want 0", got)
}
// The capacity-1 slot is free again: the next tunnel is admitted without
// waiting on the queue (a leaked slot would block this submit).
handle2 := submit("run-release-2")
defer handle2.Close()
waitForCondition(t, func() bool {
captured := env.capturedRequest()
return captured != nil && captured.GetRunId() == "run-release-2"
}, "fake node did not receive the second ProviderTunnelRequest after slot release")
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("inflight count after second dispatch: got %d, want 1", got)
}
// The second tunnel holds its own lease identity, so a late release carrying
// the first tunnel's id can never free it.
if got := leaseCount(svc.queue); got != 1 {
t.Fatalf("live leases after second dispatch: got %d, want 1", got)
}
}
// TestSubmitProviderTunnelErrorFrameReleasesSlot verifies the ERROR frame is
// terminal and releases the provider-pool slot like END.
func TestSubmitProviderTunnelErrorFrameReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-error",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
defer handle.Close()
waitForCondition(t, func() bool { return env.capturedRequest() != nil },
"fake node did not receive ProviderTunnelRequest")
captured := env.capturedRequest()
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{
RunId: captured.GetRunId(),
TunnelId: captured.GetTunnelId(),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR,
Error: "provider unavailable",
})
stream := handle.Stream()
select {
case got := <-stream.Frames:
if got.GetKind() != iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR {
t.Fatalf("expected ERROR frame, got %v", got.GetKind())
}
case <-time.After(time.Second):
t.Fatal("ERROR frame not delivered")
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after ERROR frame")
}
// TestSubmitProviderPoolSelectsTunnelProviderAndReleasesSlot verifies that
// SubmitProviderPool with an OpenAI-compatible provider (tunnel path) performs
// a single admission, sends a ProviderTunnelRequest, and releases the slot on
// tunnel END frame.
func TestSubmitProviderPoolSelectsTunnelProviderAndReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
// Catalog entry with a vLLM provider (tunnel path).
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm-01": "served-qwen"}},
}
svc.SetModelCatalog(catalog)
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "qwen3.6:35b",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != ProviderPoolPathTunnel {
t.Fatalf("expected path=tunnel, got %q", result.Path)
}
if result.Tunnel == nil {
t.Fatal("expected tunnel handle for tunnel path")
}
if result.Run != nil {
t.Fatal("expected nil run for tunnel path")
}
// Verify inflight after dispatch.
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight after dispatch, got %d", got)
}
}
// TestSubmitProviderPoolSelectsNormalizedProviderAndDoesNotOpenTunnel verifies
// that SubmitProviderPool with an Ollama provider (normalized path) performs
// a single admission, sends a RunRequest (not ProviderTunnelRequest), and
// the tunnel handle is nil.
func TestSubmitProviderPoolSelectsNormalizedProviderAndDoesNotOpenTunnel(t *testing.T) {
// Build a provider-pool service with an Ollama provider.
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
// Capture the RunRequest received by the fake node.
var capturedRunReq *iop.RunRequest
var capturedMu sync.Mutex
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
capturedMu.Lock()
capturedRunReq = req
capturedMu.Unlock()
})
// Build NodeStore with an Ollama provider (normalized path).
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-ollama",
Runtime: config.RuntimeConf{Concurrency: 2},
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-ollama-01",
Adapter: "ollama",
Type: "ollama",
Models: []string{"served-qwen"},
Health: "available",
Capacity: 2,
},
},
})
// Build catalog with Ollama provider.
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-ollama-01": "served-qwen"}},
}
// Build registry with the fake node.
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-ollama",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
// Create Service with queue and catalog.
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
// SubmitProviderPool with Ollama provider → normalized path.
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "qwen3.6:35b",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != ProviderPoolPathNormalized {
t.Fatalf("expected path=normalized, got %q", result.Path)
}
if result.Run == nil {
t.Fatal("expected run handle for normalized path")
}
if result.Tunnel != nil {
t.Fatal("expected nil tunnel for normalized path")
}
// Wait for the fake node to receive the RunRequest.
time.Sleep(50 * time.Millisecond)
capturedMu.Lock()
defer capturedMu.Unlock()
if capturedRunReq == nil {
t.Fatal("no RunRequest captured from fake node; SubmitProviderPool did not send")
}
// Verify that the adapter and target were rewritten from the provider-pool candidate.
if capturedRunReq.GetAdapter() != "ollama" {
t.Errorf("adapter: got %q, want %q", capturedRunReq.GetAdapter(), "ollama")
}
if capturedRunReq.GetTarget() != "served-qwen" {
t.Errorf("target: got %q, want %q", capturedRunReq.GetTarget(), "served-qwen")
}
// Verify inflight after dispatch.
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight after dispatch, got %d", got)
}
}
// TestSubmitProviderPoolUsesSingleAdmissionForMixedCandidates verifies that
// SubmitProviderPool performs exactly one queue admission when presented with
// mixed tunnel+normalized candidates (e.g., vLLM + Ollama). The selected
// candidate (by in-flight/priority rotation) determines the path.
func TestSubmitProviderPoolUsesSingleAdmissionForMixedCandidates(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
var capturedRunReq *iop.RunRequest
var capturedMu sync.Mutex
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
capturedMu.Lock()
capturedRunReq = req
capturedMu.Unlock()
})
// Build NodeStore with two providers: vLLM (tunnel) and Ollama (normalized).
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-mixed",
Runtime: config.RuntimeConf{Concurrency: 4},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-vllm",
Adapter: "vllm-gpu",
Type: "vllm",
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
{
ID: "prov-ollama",
Adapter: "ollama",
Type: "ollama",
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
},
})
// Catalog with both providers.
catalog := []config.ModelCatalogEntry{
{ID: "mixed-model", Providers: map[string]string{"prov-vllm": "served-model", "prov-ollama": "served-model"}},
}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-mixed",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
// SubmitProviderPool → should do exactly one admission.
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "mixed-model",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
// The selected provider depends on admission order (vLLM or Ollama by
// providerID alphabetical tie-break). Verify only one request was sent.
time.Sleep(50 * time.Millisecond)
capturedMu.Lock()
defer capturedMu.Unlock()
if capturedRunReq == nil {
t.Fatal("expected exactly one RunRequest for normalized path; admission must be single")
}
// Verify the request target matches the catalog served model.
if capturedRunReq.GetTarget() != "served-model" {
t.Errorf("target: got %q, want %q", capturedRunReq.GetTarget(), "served-model")
}
// Verify inflight is exactly 1 (single admission).
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight (single admission), got %d", got)
}
}
// TestDispatchRejectsStaleGenerationBeforeSend pins the pre-send generation
// fence on both execution paths: when the connection a lease was admitted under
// is superseded by a reconnect before dispatch, the normalized path releases the
// lease without ever sending a RunRequest, and the tunnel path releases it
// without ever sending a ProviderTunnelRequest, to the stale (dead) client. Both
// request parsers are wired to the stale connection so a leaked send of either
// type is caught directly.
func TestDispatchRejectsStaleGenerationBeforeSend(t *testing.T) {
t.Run("normalized", func(t *testing.T) {
staleGenerationFenceCase(t, providerExecutionPathNormalized)
})
t.Run("tunnel", func(t *testing.T) {
staleGenerationFenceCase(t, providerExecutionPathTunnel)
})
}
// staleGenerationFenceCase drives one execution path through its provider-pool
// dispatch entry point with a lease admitted under a connection a reconnect has
// already superseded, and asserts the fence releases the lease and fails with no
// request of any type reaching the stale connection.
func staleGenerationFenceCase(t *testing.T, path providerExecutionPath) {
t.Helper()
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
staleClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
var capturedMu sync.Mutex
captured := 0
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(*iop.RunRequest) {
capturedMu.Lock()
captured++
capturedMu.Unlock()
})
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(*iop.ProviderTunnelRequest) {
capturedMu.Lock()
captured++
capturedMu.Unlock()
})
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-stale",
Runtime: config.RuntimeConf{Concurrency: 1},
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{{Name: "ollama", Enabled: true}},
},
Providers: []config.NodeProviderConf{
{ID: "prov-stale", Adapter: "ollama", Type: "ollama", Models: []string{"served"}, Health: "available", Capacity: 1},
},
})
reg := edgenode.NewRegistry()
// entry1 is the connection admission selected; it holds the stale client.
entry1 := &edgenode.NodeEntry{NodeID: "node-stale", LifecycleState: edgenode.LifecycleConnected, Client: staleClient}
reg.Register(entry1)
staleGeneration := entry1.ConnectionGeneration
// A reconnect supersedes entry1: the registry's current owner is a strictly
// higher generation, so entry1's lease is now stale.
reg.Register(&edgenode.NodeEntry{NodeID: "node-stale", LifecycleState: edgenode.LifecycleConnected, Client: &toki.TcpClient{}})
if reg.IsCurrentOwnerGeneration("node-stale", staleGeneration) {
t.Fatal("reconnect should have superseded the stale generation")
}
svc := New(reg, edgeevents.NewBus())
svc.SetNodeStore(store)
svc.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "stale-model", Providers: map[string]string{"prov-stale": "served"}},
})
// Admit a lease bound to entry1's (now stale) generation on the path under test.
selectedCand := candidateNode{
entry: entry1,
providerID: "prov-stale",
servedTarget: "served",
generation: staleGeneration,
capacity: 1,
adapter: "ollama",
providerType: "ollama",
executionPath: path,
}
admitted, _, err := svc.queue.admitWithReason(context.Background(), "stale-model", "ollama", "served", []candidateNode{selectedCand}, groupPolicy{}, nil, false, false)
if err != nil || admitted == nil {
t.Fatalf("admit: %v", err)
}
if leaseCount(svc.queue) != 1 {
t.Fatalf("expected 1 live lease after admit, got %d", leaseCount(svc.queue))
}
reservation := newQueueReservation(svc.queue, admitted)
switch path {
case providerExecutionPathNormalized:
req := SubmitRunRequest{ModelGroupKey: "stale-model", ProviderPool: true, Background: true}
result, err := svc.dispatchProviderPoolRun(context.Background(), req, "ollama", "served", admitted, "dispatched", reservation)
if err == nil {
t.Fatal("expected stale-generation dispatch to be rejected before send")
}
if result != nil {
t.Fatal("expected nil result on a fenced dispatch")
}
if !strings.Contains(err.Error(), "connection changed before dispatch") {
t.Fatalf("unexpected error: %v", err)
}
case providerExecutionPathTunnel:
req := ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "stale-model", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{
ModelGroupKey: "stale-model",
ProviderPool: true,
Method: "POST",
Path: "/v1/chat/completions",
},
}
result, err := svc.dispatchProviderPoolTunnel(req, "ollama", "served", admitted, "dispatched", reservation)
if err == nil {
t.Fatal("expected stale-generation tunnel dispatch to be rejected before send")
}
if result != nil {
t.Fatal("expected nil result on a fenced tunnel dispatch")
}
if !strings.Contains(err.Error(), "connection changed before dispatch") {
t.Fatalf("unexpected error: %v", err)
}
default:
t.Fatalf("unhandled execution path %q", path)
}
// The fenced dispatch must have released its lease and never sent a request.
if got := leaseCount(svc.queue); got != 0 {
t.Fatalf("expected the fenced dispatch to release its lease, got %d live leases", got)
}
time.Sleep(50 * time.Millisecond)
capturedMu.Lock()
sent := captured
capturedMu.Unlock()
if sent != 0 {
t.Fatalf("a request reached the stale connection %d time(s); want none", sent)
}
}
// TestSubmitProviderPoolDispatchInfoObservation verifies that the provider-pool
// one-shot dispatch carries selected provider id, provider type, and execution
// path in RunDispatch on both tunnel and normalized paths (SURFACE_OBS-1).
func TestSubmitProviderPoolDispatchInfoObservation(t *testing.T) {
for _, tc := range []struct {
name string
providerType string
wantPath providerPoolPath
wantExecPath string
}{
{
name: "vllm_tunnel",
providerType: "vllm",
wantPath: ProviderPoolPathTunnel,
wantExecPath: "provider_tunnel",
},
{
name: "ollama_normalized",
providerType: "ollama",
wantPath: ProviderPoolPathNormalized,
wantExecPath: "normalized",
},
{
name: "cli_normalized",
providerType: "cli",
wantPath: ProviderPoolPathNormalized,
wantExecPath: "normalized",
},
} {
t.Run(tc.name, func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
_ = toki.NewTcpClient(nodeConn, 0, 0, parserMap)
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-obs",
Runtime: config.RuntimeConf{Concurrency: 2},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{{Name: "vllm-gpu", Enabled: true}},
OllamaInstances: []config.OllamaInstanceConf{{Name: "ollama", Enabled: true}},
OpenAICompatInstances: []config.OpenAICompatInstanceConf{{Name: "openai", Enabled: true}},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-obs",
Adapter: "vllm-gpu",
Type: tc.providerType,
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
},
})
catalog := []config.ModelCatalogEntry{
{ID: "obs-model", Providers: map[string]string{"prov-obs": "served-model"}},
}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-obs",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "obs-model",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != tc.wantPath {
t.Fatalf("path: got %q, want %q", result.Path, tc.wantPath)
}
disp := result.DispatchInfo
if disp.ProviderID != "prov-obs" {
t.Errorf("provider_id: got %q, want %q", disp.ProviderID, "prov-obs")
}
if disp.ProviderType != tc.providerType {
t.Errorf("provider_type: got %q, want %q", disp.ProviderType, tc.providerType)
}
if disp.ExecutionPath != tc.wantExecPath {
t.Errorf("execution_path: got %q, want %q", disp.ExecutionPath, tc.wantExecPath)
}
if disp.QueueReason != "dispatched" {
t.Errorf("queue_reason: got %q, want %q", disp.QueueReason, "dispatched")
}
if disp.Adapter != "vllm-gpu" {
t.Errorf("adapter: got %q, want %q", disp.Adapter, "vllm-gpu")
}
if disp.Target != "served-model" {
t.Errorf("target: got %q, want %q", disp.Target, "served-model")
}
// Also verify tunnel/normalized handle DispatchInfo matches.
switch result.Path {
case ProviderPoolPathTunnel:
if result.Tunnel == nil {
t.Fatal("expected tunnel handle for tunnel path")
}
hDisp := result.Tunnel.Dispatch()
if hDisp.ProviderID != "prov-obs" {
t.Errorf("tunnel handle provider_id: got %q, want %q", hDisp.ProviderID, "prov-obs")
}
if hDisp.ProviderType != tc.providerType {
t.Errorf("tunnel handle provider_type: got %q, want %q", hDisp.ProviderType, tc.providerType)
}
if hDisp.ExecutionPath != tc.wantExecPath {
t.Errorf("tunnel handle execution_path: got %q, want %q", hDisp.ExecutionPath, tc.wantExecPath)
}
// Close releases the inflight slot.
result.Tunnel.Close()
if got := inflightRunCount(svc.queue); got != 0 {
t.Fatalf("expected 0 inflight after tunnel close, got %d", got)
}
case ProviderPoolPathNormalized:
if result.Run == nil {
t.Fatal("expected run handle for normalized path")
}
hDisp := result.Run.Dispatch()
if hDisp.ProviderID != "prov-obs" {
t.Errorf("run handle provider_id: got %q, want %q", hDisp.ProviderID, "prov-obs")
}
if hDisp.ProviderType != tc.providerType {
t.Errorf("run handle provider_type: got %q, want %q", hDisp.ProviderType, tc.providerType)
}
if hDisp.ExecutionPath != tc.wantExecPath {
t.Errorf("run handle execution_path: got %q, want %q", hDisp.ExecutionPath, tc.wantExecPath)
}
// Inflight should be 1 for normalized (run hasn't completed yet).
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight for normalized, got %d", got)
}
result.Run.Close()
}
})
}
}
// TestPendingClientDirectSendFence asserts the pending→ready dispatch fence at
// the granularity of the exact protobuf each direct wire carries. While the
// connection is pending every direct send path fails and nothing reaches the
// node. After the ready transition each wire is delivered exactly once, and its
// identity fields — not merely its type — are asserted against a cloned copy of
// the received message, so a request that dispatched with an empty or wrong field
// can no longer pass by type count alone.
func TestPendingClientDirectSendFence(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.NodeCommandRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.NodeCommandResponse{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandResponse{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.CancelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.CancelRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
// Each listener clones the received protobuf so its exact identity fields can
// be asserted; fire-and-forget wires arrive on buffered channels, and the
// synchronous NodeCommandRequests accumulate under a lock keyed by type.
runReqs := make(chan *iop.RunRequest, 4)
tunnelReqs := make(chan *iop.ProviderTunnelRequest, 4)
cancelReqs := make(chan *iop.CancelRequest, 4)
var cmdMu sync.Mutex
cmdReqs := make(map[iop.NodeCommandType][]*iop.NodeCommandRequest)
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
runReqs <- proto.Clone(req).(*iop.RunRequest)
})
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(req *iop.ProviderTunnelRequest) {
tunnelReqs <- proto.Clone(req).(*iop.ProviderTunnelRequest)
})
toki.AddListenerTyped[*iop.CancelRequest](&nodeClient.Communicator, func(req *iop.CancelRequest) {
cancelReqs <- proto.Clone(req).(*iop.CancelRequest)
})
toki.AddRequestListenerTyped(&nodeClient.Communicator, func(req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) {
cmdMu.Lock()
cmdReqs[req.GetType()] = append(cmdReqs[req.GetType()], proto.Clone(req).(*iop.NodeCommandRequest))
cmdMu.Unlock()
return &iop.NodeCommandResponse{
RequestId: req.GetRequestId(),
Type: req.GetType(),
Result: map[string]string{"result": "ok", "status_code": "200"},
}, nil
})
reg := edgenode.NewRegistry()
entry := &edgenode.NodeEntry{
NodeID: "node-fence-1",
Alias: "fence-alias",
AgentKind: config.AgentKindGenericNode,
Client: edgeClient,
DispatchReady: false, // start pending
}
reg.RegisterIfAbsent(entry)
svc := New(reg, nil)
ctx := context.Background()
// 1. Observability lookups still see the pending connection.
if _, err := svc.ResolveNode("node-fence-1"); err != nil {
t.Errorf("ResolveNode failed for pending: %v", err)
}
snap, err := svc.ResolveNodeSnapshot("node-fence-1")
if err != nil {
t.Errorf("ResolveNodeSnapshot failed for pending: %v", err)
}
if !snap.Connected {
t.Errorf("expected Connected=true for pending snapshot, got %v", snap.Connected)
}
// 2. Every direct send path fails while pending and puts nothing on the wire.
if _, err := svc.SubmitRun(ctx, SubmitRunRequest{NodeRef: "node-fence-1", Adapter: "cli", Target: "codex"}); err == nil {
t.Error("expected SubmitRun on pending node to fail")
}
if _, err := svc.submitProviderTunnelDirect(SubmitProviderTunnelRequest{NodeRef: "node-fence-1", Adapter: "cli", Target: "codex"}); err == nil {
t.Error("expected submitProviderTunnelDirect on pending node to fail")
}
if _, err := svc.CancelRun(ctx, CancelRunRequest{NodeRef: "node-fence-1", RunID: "run-1"}); err == nil {
t.Error("expected CancelRun on pending node to fail")
}
if _, err := svc.TerminateSession(ctx, TerminateSessionRequest{NodeRef: "node-fence-1"}); err == nil {
t.Error("expected TerminateSession on pending node to fail")
}
if _, err := svc.UsageStatus(ctx, UsageStatusRequest{NodeRef: "node-fence-1"}); err == nil {
t.Error("expected UsageStatus on pending node to fail")
}
pendingResp, _ := svc.ExecuteCommand(ctx, &iop.EdgeCommandRequest{
CommandId: "cmd-pending",
Operation: "agent.command",
TargetSelector: "node-fence-1",
Parameters: map[string]string{"command": "capabilities"},
}, func(*iop.EdgeCommandEvent) {})
if pendingResp == nil || pendingResp.Status != "error" {
t.Errorf("expected ExecuteCommand on pending node to error, got %v", pendingResp.GetStatus())
}
if n := len(runReqs) + len(tunnelReqs) + len(cancelReqs); n != 0 {
t.Fatalf("pending node received %d fire-and-forget wire messages, want 0", n)
}
cmdMu.Lock()
pendingCmds := len(cmdReqs)
cmdMu.Unlock()
if pendingCmds != 0 {
t.Fatalf("pending node received %d command wire types, want 0", pendingCmds)
}
// 3. Ready opens dispatch.
if _, transitioned, ok := reg.MarkDispatchReadyIfClient("node-fence-1", edgeClient); !ok || !transitioned {
t.Fatalf("failed to transition node to ready: ok=%v transitioned=%v", ok, transitioned)
}
snap2, err := svc.ResolveNodeSnapshot("node-fence-1")
if err != nil {
t.Errorf("ResolveNodeSnapshot failed after ready: %v", err)
}
if !snap2.Connected {
t.Errorf("expected Connected=true after ready, got %v", snap2.Connected)
}
// 4. Each direct wire now dispatches exactly once with its exact payload.
if _, err := svc.SubmitRun(ctx, SubmitRunRequest{
NodeRef: "node-fence-1", RunID: "run-fence-run", Adapter: "cli", Target: "codex",
SessionID: "sess-run", Background: true, Prompt: "hello-fence",
Metadata: map[string]string{"source": "fence-test"},
}); err != nil {
t.Errorf("SubmitRun after ready: %v", err)
}
if _, err := svc.submitProviderTunnelDirect(SubmitProviderTunnelRequest{
NodeRef: "node-fence-1", RunID: "run-fence-tunnel", Adapter: "cli", Target: "codex",
SessionID: "sess-tunnel", Method: "POST", Path: "/v1/chat", Body: []byte("tunnel-body"),
}); err != nil {
t.Errorf("submitProviderTunnelDirect after ready: %v", err)
}
if _, err := svc.CancelRun(ctx, CancelRunRequest{
NodeRef: "node-fence-1", RunID: "run-fence-cancel", Adapter: "cli", Target: "codex", SessionID: "sess-cancel",
}); err != nil {
t.Errorf("CancelRun after ready: %v", err)
}
if _, err := svc.TerminateSession(ctx, TerminateSessionRequest{
NodeRef: "node-fence-1", Adapter: "cli", Target: "codex", SessionID: "sess-term",
}); err != nil {
t.Errorf("TerminateSession after ready: %v", err)
}
if _, err := svc.UsageStatus(ctx, UsageStatusRequest{
NodeRef: "node-fence-1", Adapter: "cli", Target: "codex", SessionID: "sess-usage",
}); err != nil {
t.Errorf("UsageStatus after ready: %v", err)
}
if _, err := svc.SessionList(ctx, NodeCommandRequestSpec{
NodeRef: "node-fence-1", Adapter: "cli", Target: "codex", SessionID: "sess-list",
}); err != nil {
t.Errorf("SessionList after ready: %v", err)
}
if _, err := svc.TransportStatus(ctx, NodeCommandRequestSpec{
NodeRef: "node-fence-1", Adapter: "cli", Target: "codex", SessionID: "sess-transport",
}); err != nil {
t.Errorf("TransportStatus after ready: %v", err)
}
if _, err := svc.OllamaAPI(ctx, OllamaAPIRequest{
NodeRef: "node-fence-1", Adapter: "ollama", Target: "llama", Method: "POST", Path: "/api/chat", Body: "ollama-body",
}); err != nil {
t.Errorf("OllamaAPI after ready: %v", err)
}
readyResp, _ := svc.ExecuteCommand(ctx, &iop.EdgeCommandRequest{
CommandId: "cmd-ready",
Operation: "agent.command",
TargetSelector: "node-fence-1",
Parameters: map[string]string{"command": "capabilities"},
}, func(*iop.EdgeCommandEvent) {})
if readyResp == nil || readyResp.Status == "error" {
t.Errorf("ExecuteCommand after ready failed: %v", readyResp.GetError())
}
// 4.1 RunRequest identity.
run := recvWire(t, runReqs, "RunRequest")
if got := run.GetRunId(); got != "run-fence-run" {
t.Errorf("RunRequest.run_id = %q, want run-fence-run", got)
}
if got := run.GetAdapter(); got != "cli" {
t.Errorf("RunRequest.adapter = %q, want cli", got)
}
if got := run.GetTarget(); got != "codex" {
t.Errorf("RunRequest.target = %q, want codex", got)
}
if !run.GetBackground() {
t.Errorf("RunRequest.background = false, want true")
}
if got := run.GetSessionId(); got != "sess-run" {
t.Errorf("RunRequest.session_id = %q, want sess-run", got)
}
if got := run.GetInput().GetFields()["prompt"].GetStringValue(); got != "hello-fence" {
t.Errorf("RunRequest.input.prompt = %q, want hello-fence", got)
}
if got := run.GetMetadata()["source"]; got != "fence-test" {
t.Errorf("RunRequest.metadata.source = %q, want fence-test", got)
}
if run.GetSessionMode() != iop.RunSessionMode_RUN_SESSION_MODE_CREATE_IF_MISSING {
t.Errorf("RunRequest.session_mode = %v, want CREATE_IF_MISSING", run.GetSessionMode())
}
assertNoExtra(t, runReqs, "RunRequest")
// 4.2 ProviderTunnelRequest identity.
tunnel := recvWire(t, tunnelReqs, "ProviderTunnelRequest")
if got := tunnel.GetRunId(); got != "run-fence-tunnel" {
t.Errorf("ProviderTunnelRequest.run_id = %q, want run-fence-tunnel", got)
}
if got := tunnel.GetTunnelId(); got != "run-fence-tunnel-tunnel" {
t.Errorf("ProviderTunnelRequest.tunnel_id = %q, want run-fence-tunnel-tunnel", got)
}
if got := tunnel.GetAdapter(); got != "cli" {
t.Errorf("ProviderTunnelRequest.adapter = %q, want cli", got)
}
if got := tunnel.GetTarget(); got != "codex" {
t.Errorf("ProviderTunnelRequest.target = %q, want codex", got)
}
if got := tunnel.GetMethod(); got != "POST" {
t.Errorf("ProviderTunnelRequest.method = %q, want POST", got)
}
if got := tunnel.GetPath(); got != "/v1/chat" {
t.Errorf("ProviderTunnelRequest.path = %q, want /v1/chat", got)
}
if got := string(tunnel.GetBody()); got != "tunnel-body" {
t.Errorf("ProviderTunnelRequest.body = %q, want tunnel-body", got)
}
if got := tunnel.GetSessionId(); got != "sess-tunnel" {
t.Errorf("ProviderTunnelRequest.session_id = %q, want sess-tunnel", got)
}
assertNoExtra(t, tunnelReqs, "ProviderTunnelRequest")
// 4.3 CancelRequest: one CANCEL_RUN and one TERMINATE_SESSION.
firstCancel := recvWire(t, cancelReqs, "CancelRequest")
secondCancel := recvWire(t, cancelReqs, "CancelRequest")
cancels := map[iop.CancelAction]*iop.CancelRequest{
firstCancel.GetAction(): firstCancel,
secondCancel.GetAction(): secondCancel,
}
if len(cancels) != 2 {
t.Fatalf("expected distinct CANCEL_RUN and TERMINATE_SESSION, got actions %v and %v", firstCancel.GetAction(), secondCancel.GetAction())
}
if cancelRun := cancels[iop.CancelAction_CANCEL_ACTION_CANCEL_RUN]; cancelRun == nil {
t.Error("missing CANCEL_RUN cancel request")
} else {
if got := cancelRun.GetRunId(); got != "run-fence-cancel" {
t.Errorf("CANCEL_RUN.run_id = %q, want run-fence-cancel", got)
}
if got := cancelRun.GetSessionId(); got != "sess-cancel" {
t.Errorf("CANCEL_RUN.session_id = %q, want sess-cancel", got)
}
if got := cancelRun.GetAdapter(); got != "cli" {
t.Errorf("CANCEL_RUN.adapter = %q, want cli", got)
}
if got := cancelRun.GetTarget(); got != "codex" {
t.Errorf("CANCEL_RUN.target = %q, want codex", got)
}
}
if term := cancels[iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION]; term == nil {
t.Error("missing TERMINATE_SESSION cancel request")
} else {
if got := term.GetSessionId(); got != "sess-term" {
t.Errorf("TERMINATE_SESSION.session_id = %q, want sess-term", got)
}
if got := term.GetRunId(); got != "" {
t.Errorf("TERMINATE_SESSION.run_id = %q, want empty", got)
}
}
assertNoExtra(t, cancelReqs, "CancelRequest")
// 4.4 NodeCommandRequests: each type exactly once with its exact fields.
cmdMu.Lock()
defer cmdMu.Unlock()
expectCommand := func(typ iop.NodeCommandType, wantAdapter, wantTarget, wantSession, wantIDPrefix string, extra func(*iop.NodeCommandRequest)) {
reqs := cmdReqs[typ]
if len(reqs) != 1 {
t.Errorf("command %v received %d times, want 1", typ, len(reqs))
return
}
req := reqs[0]
if got := req.GetAdapter(); got != wantAdapter {
t.Errorf("%v adapter = %q, want %q", typ, got, wantAdapter)
}
if got := req.GetTarget(); got != wantTarget {
t.Errorf("%v target = %q, want %q", typ, got, wantTarget)
}
if got := req.GetSessionId(); got != wantSession {
t.Errorf("%v session_id = %q, want %q", typ, got, wantSession)
}
if got := req.GetRequestId(); !strings.HasPrefix(got, wantIDPrefix) {
t.Errorf("%v request_id = %q, want prefix %q", typ, got, wantIDPrefix)
}
if extra != nil {
extra(req)
}
}
expectCommand(iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS, "cli", "codex", "sess-usage", "status-", nil)
expectCommand(iop.NodeCommandType_NODE_COMMAND_TYPE_SESSION_LIST, "cli", "codex", "sess-list", "sessions-", nil)
expectCommand(iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS, "cli", "codex", "sess-transport", "transport-", nil)
expectCommand(iop.NodeCommandType_NODE_COMMAND_TYPE_OLLAMA_API, "ollama", "llama", "default", "ollama-", func(req *iop.NodeCommandRequest) {
if got := req.GetMetadata()["ollama_method"]; got != "POST" {
t.Errorf("OLLAMA_API metadata.ollama_method = %q, want POST", got)
}
if got := req.GetMetadata()["ollama_path"]; got != "/api/chat" {
t.Errorf("OLLAMA_API metadata.ollama_path = %q, want /api/chat", got)
}
if got := req.GetMetadata()["ollama_body"]; got != "ollama-body" {
t.Errorf("OLLAMA_API metadata.ollama_body = %q, want ollama-body", got)
}
})
expectCommand(iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES, "", "", "default", "caps-", nil)
if len(cmdReqs) != 5 {
t.Errorf("received %d distinct command types, want 5", len(cmdReqs))
}
}
// recvWire returns the next captured wire of type T or fails after a timeout.
func recvWire[T proto.Message](t *testing.T, ch chan T, name string) T {
t.Helper()
select {
case v := <-ch:
return v
case <-time.After(2 * time.Second):
t.Fatalf("timed out waiting for %s wire", name)
var zero T
return zero
}
}
// assertNoExtra fails if any additional wire of type T is buffered. No further
// send was issued after the expected ones, so a non-blocking read is a
// deterministic exactly-once guard.
func assertNoExtra[T proto.Message](t *testing.T, ch chan T, name string) {
t.Helper()
select {
case v := <-ch:
t.Errorf("unexpected extra %s wire: %v", name, v)
default:
}
}
// gateConn blocks the first (and every subsequent) Write until release is
// closed, signalling entered once the write is in progress. The provider-pool
// dispatch performs its wire send inside WithCurrentDispatchOwner while holding
// the registry lock, so blocking the write here holds that lock and lets a test
// schedule a competing disconnect deterministically against it.
type gateConn struct {
net.Conn
entered chan struct{}
release chan struct{}
enterOnce sync.Once
}
func (c *gateConn) Write(b []byte) (int, error) {
c.enterOnce.Do(func() { close(c.entered) })
<-c.release
return c.Conn.Write(b)
}
// newProviderPoolRaceService builds a Service whose registry holds one ready
// node with a single capacity-1 provider serving model-1. provType selects the
// dispatch path: an OpenAI-compatible type (e.g. "vllm") routes to the tunnel
// path, everything else (e.g. "openai") to the normalized run path.
func newProviderPoolRaceService(t *testing.T, nodeID, provType string, edgeClient *toki.TcpClient) (*Service, *edgenode.Registry, *edgenode.NodeEntry) {
t.Helper()
reg := edgenode.NewRegistry()
entry := &edgenode.NodeEntry{
NodeID: nodeID,
Alias: nodeID + "-alias",
AgentKind: config.AgentKindGenericNode,
Client: edgeClient,
DispatchReady: true,
}
reg.Register(entry)
svc := New(reg, edgeevents.NewBus())
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
{ID: nodeID, Alias: nodeID + "-alias", Token: "token", AgentKind: config.AgentKindGenericNode, Providers: []config.NodeProviderConf{
{ID: "prov-1", Type: provType, Models: []string{"model-1"}, Health: "available", Capacity: 1},
}},
})
if err != nil {
t.Fatalf("load node store: %v", err)
}
svc.SetNodeStore(store)
svc.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-1", Providers: map[string]string{"prov-1": "model-1"}}})
return svc, reg, entry
}
// assertQueueSettled asserts every lease is released and every provider/group
// in-flight counter is back to zero, so a race can never leak a slot.
func assertQueueSettled(t *testing.T, q *modelQueueManager) {
t.Helper()
q.mu.Lock()
defer q.mu.Unlock()
if len(q.leases) != 0 {
t.Errorf("leases not settled: %d remain", len(q.leases))
}
if len(q.leaseByRun) != 0 {
t.Errorf("leaseByRun not settled: %d remain", len(q.leaseByRun))
}
for key, res := range q.resources {
if res.inFlight != 0 || res.longInFlight != 0 {
t.Errorf("provider resource %v not settled: inFlight=%d longInFlight=%d", key, res.inFlight, res.longInFlight)
}
}
for gk, g := range q.groups {
for slot, n := range g.inflight {
if n != 0 {
t.Errorf("group %q slot %q inflight=%d, want 0", gk, slot, n)
}
}
for slot, n := range g.longInflight {
if n != 0 {
t.Errorf("group %q slot %q longInflight=%d, want 0", gk, slot, n)
}
}
}
}
// assertTrackedLease proves the send-winning barrier was reached only after the
// exact run identity had been bound to one live lease for the expected owner
// generation. The later disconnect/terminal assertions then prove that same
// identity is removed without leaving a duplicate decrement behind.
func assertTrackedLease(t *testing.T, q *modelQueueManager, runID, nodeID string, generation uint64) {
t.Helper()
q.mu.Lock()
defer q.mu.Unlock()
leaseID, ok := q.leaseByRun[runID]
if !ok || leaseID == 0 {
t.Fatalf("run %q has no tracked lease", runID)
}
lease := q.leases[leaseID]
if lease == nil {
t.Fatalf("run %q maps to missing lease %d", runID, leaseID)
}
if lease.nodeID != nodeID || lease.generation != generation {
t.Fatalf("run %q lease owner=(node=%q generation=%d), want (%q,%d)",
runID, lease.nodeID, lease.generation, nodeID, generation)
}
}
func disconnectCurrentOwner(svc *Service, reg *edgenode.Registry, entry *edgenode.NodeEntry, reason string) (uint64, bool) {
generation, ok := reg.UnregisterIfClient(entry.NodeID, entry.Client)
if ok {
svc.HandleNodeDisconnect(entry.NodeID, generation, reason)
}
return generation, ok
}
// TestProviderPoolDispatchRunDisconnectRace pins the normalized send/disconnect
// race in two deterministic directions instead of inferring order from sleeps.
//
// - send-winning: the dispatch enters its wire send under the owner gate
// (registry lock held); a disconnect racing in blocks on that lock, the wire
// is written exactly once, and the authoritative disconnect then releases the
// handed-off lease exactly once, leaving every counter at zero.
// - disconnect-winning: the disconnect settles (unregister + fence) while the
// dispatch is parked at its post-admission PrepareRun barrier, before the
// owner gate. The gate then fences the stale generation, so no wire and no
// run handle escape and the reserved lease is released exactly once.
func TestProviderPoolDispatchRunDisconnectRace(t *testing.T) {
runParser := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
t.Run("send-winning", func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
gate := &gateConn{Conn: edgeConn, entered: make(chan struct{}), release: make(chan struct{})}
edgeClient := toki.NewTcpClient(gate, 0, 0, runParser)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, runParser)
runReqs := make(chan *iop.RunRequest, 4)
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
runReqs <- proto.Clone(req).(*iop.RunRequest)
})
svc, reg, entry := newProviderPoolRaceService(t, "node-race-run", "openai", edgeClient)
dispatchDone := make(chan struct{})
var result *ProviderPoolDispatchResult
var dispatchErr error
go func() {
result, dispatchErr = svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{NodeRef: "node-race-run", RunID: "run-send-win", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true, Background: true},
})
close(dispatchDone)
}()
select {
case <-gate.entered:
case <-time.After(3 * time.Second):
t.Fatal("dispatch did not reach the wire send")
}
assertTrackedLease(t, svc.queue, "run-send-win", "node-race-run", entry.ConnectionGeneration)
type disconnectOutcome struct {
generation uint64
ok bool
}
disconnectDone := make(chan disconnectOutcome, 1)
go func() {
generation, ok := disconnectCurrentOwner(svc, reg, entry, "test-disconnect")
disconnectDone <- disconnectOutcome{generation: generation, ok: ok}
}()
close(gate.release)
<-dispatchDone
disconnected := <-disconnectDone
if !disconnected.ok || disconnected.generation != entry.ConnectionGeneration {
t.Fatalf("send-winning disconnect=(generation=%d ok=%v), want (%d,true)",
disconnected.generation, disconnected.ok, entry.ConnectionGeneration)
}
if dispatchErr != nil {
t.Fatalf("send-winning dispatch must succeed, got %v", dispatchErr)
}
if result == nil || result.Run == nil {
t.Fatal("send-winning must return a run handle")
}
wire := recvWire(t, runReqs, "RunRequest")
if got := wire.GetRunId(); got != "run-send-win" {
t.Errorf("wire run_id = %q, want run-send-win", got)
}
assertNoExtra(t, runReqs, "RunRequest")
assertQueueSettled(t, svc.queue)
// A terminal callback losing to the authoritative disconnect is an
// idempotent no-op for the same tracked lease; closing the handle must not
// reopen or double-release it either.
svc.HandleRunLifecycleEvent(&iop.RunEvent{RunId: wire.GetRunId(), Type: "complete", NodeId: "node-race-run"})
result.Run.Close()
assertQueueSettled(t, svc.queue)
})
t.Run("disconnect-winning", func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, runParser)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, runParser)
runReqs := make(chan *iop.RunRequest, 4)
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
runReqs <- proto.Clone(req).(*iop.RunRequest)
})
svc, reg, entry := newProviderPoolRaceService(t, "node-race-run", "openai", edgeClient)
reachedPrepare := make(chan struct{})
releasePrepare := make(chan struct{})
dispatchDone := make(chan struct{})
var result *ProviderPoolDispatchResult
var dispatchErr error
go func() {
result, dispatchErr = svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{NodeRef: "node-race-run", RunID: "run-disc-win", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true, Background: true},
PrepareRun: func(req SubmitRunRequest) (SubmitRunRequest, error) {
close(reachedPrepare)
<-releasePrepare
return req, nil
},
})
close(dispatchDone)
}()
<-reachedPrepare
if got := leaseCount(svc.queue); got != 1 {
t.Fatalf("disconnect-winning reserved leases=%d before fence, want 1", got)
}
disconnectedGen, ok := disconnectCurrentOwner(svc, reg, entry, "test-disconnect")
if !ok || disconnectedGen != entry.ConnectionGeneration {
t.Fatalf("disconnect-winning disconnect=(generation=%d ok=%v), want (%d,true)",
disconnectedGen, ok, entry.ConnectionGeneration)
}
close(releasePrepare)
<-dispatchDone
if dispatchErr == nil {
t.Fatal("disconnect-winning dispatch must fail")
}
if result != nil {
t.Fatalf("disconnect-winning must not return a result, got %+v", result)
}
assertNoExtra(t, runReqs, "RunRequest")
assertQueueSettled(t, svc.queue)
})
}
// TestProviderPoolDispatchTunnelDisconnectRace mirrors the normalized race for
// the tunnel/passthrough path: a send-winning subtest that writes exactly one
// ProviderTunnelRequest then releases the lease on the authoritative disconnect,
// and a disconnect-winning subtest whose PrepareTunnel barrier lets the fence
// win so no tunnel frame and no handle escape.
func TestProviderPoolDispatchTunnelDisconnectRace(t *testing.T) {
tunnelParser := toki.ParserMap{
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
t.Run("send-winning", func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
gate := &gateConn{Conn: edgeConn, entered: make(chan struct{}), release: make(chan struct{})}
edgeClient := toki.NewTcpClient(gate, 0, 0, tunnelParser)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, tunnelParser)
tunnelReqs := make(chan *iop.ProviderTunnelRequest, 4)
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(req *iop.ProviderTunnelRequest) {
tunnelReqs <- proto.Clone(req).(*iop.ProviderTunnelRequest)
})
svc, reg, entry := newProviderPoolRaceService(t, "node-race-tunnel", "vllm", edgeClient)
dispatchDone := make(chan struct{})
var result *ProviderPoolDispatchResult
var dispatchErr error
go func() {
result, dispatchErr = svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{NodeRef: "node-race-tunnel", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{NodeRef: "node-race-tunnel", RunID: "run-tunnel-send-win", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true},
})
close(dispatchDone)
}()
select {
case <-gate.entered:
case <-time.After(3 * time.Second):
t.Fatal("tunnel dispatch did not reach the wire send")
}
assertTrackedLease(t, svc.queue, "run-tunnel-send-win", "node-race-tunnel", entry.ConnectionGeneration)
type disconnectOutcome struct {
generation uint64
ok bool
}
disconnectDone := make(chan disconnectOutcome, 1)
go func() {
generation, ok := disconnectCurrentOwner(svc, reg, entry, "test-disconnect")
disconnectDone <- disconnectOutcome{generation: generation, ok: ok}
}()
close(gate.release)
<-dispatchDone
disconnected := <-disconnectDone
if !disconnected.ok || disconnected.generation != entry.ConnectionGeneration {
t.Fatalf("send-winning tunnel disconnect=(generation=%d ok=%v), want (%d,true)",
disconnected.generation, disconnected.ok, entry.ConnectionGeneration)
}
if dispatchErr != nil {
t.Fatalf("send-winning tunnel dispatch must succeed, got %v", dispatchErr)
}
if result == nil || result.Tunnel == nil {
t.Fatal("send-winning must return a tunnel handle")
}
wire := recvWire(t, tunnelReqs, "ProviderTunnelRequest")
if got := wire.GetRunId(); got != "run-tunnel-send-win" {
t.Errorf("wire run_id = %q, want run-tunnel-send-win", got)
}
assertNoExtra(t, tunnelReqs, "ProviderTunnelRequest")
assertQueueSettled(t, svc.queue)
// Deliver the competing tunnel terminal after disconnect. The handle sees
// exactly one END and closes, while the already-settled lease/counters stay
// at zero when Close races afterward.
stream := result.Tunnel.Stream()
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{
RunId: wire.GetRunId(), TunnelId: wire.GetTunnelId(),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true,
})
select {
case terminal, ok := <-stream.Frames:
if !ok || terminal.GetKind() != iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END || !terminal.GetEnd() {
t.Fatalf("tunnel terminal=%v open=%v, want one END frame", terminal, ok)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for tunnel END frame")
}
select {
case extra, ok := <-stream.Frames:
if ok {
t.Fatalf("unexpected extra tunnel terminal/frame: %v", extra)
}
case <-time.After(2 * time.Second):
t.Fatal("tunnel stream did not close after END")
}
result.Tunnel.Close()
assertQueueSettled(t, svc.queue)
})
t.Run("disconnect-winning", func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, tunnelParser)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, tunnelParser)
tunnelReqs := make(chan *iop.ProviderTunnelRequest, 4)
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(req *iop.ProviderTunnelRequest) {
tunnelReqs <- proto.Clone(req).(*iop.ProviderTunnelRequest)
})
svc, reg, entry := newProviderPoolRaceService(t, "node-race-tunnel", "vllm", edgeClient)
reachedPrepare := make(chan struct{})
releasePrepare := make(chan struct{})
dispatchDone := make(chan struct{})
var result *ProviderPoolDispatchResult
var dispatchErr error
go func() {
result, dispatchErr = svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{NodeRef: "node-race-tunnel", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{NodeRef: "node-race-tunnel", RunID: "run-tunnel-disc-win", ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true},
PrepareTunnel: func(req SubmitProviderTunnelRequest) (SubmitProviderTunnelRequest, error) {
close(reachedPrepare)
<-releasePrepare
return req, nil
},
})
close(dispatchDone)
}()
<-reachedPrepare
if got := leaseCount(svc.queue); got != 1 {
t.Fatalf("disconnect-winning tunnel reserved leases=%d before fence, want 1", got)
}
disconnectedGen, ok := disconnectCurrentOwner(svc, reg, entry, "test-disconnect")
if !ok || disconnectedGen != entry.ConnectionGeneration {
t.Fatalf("disconnect-winning tunnel disconnect=(generation=%d ok=%v), want (%d,true)",
disconnectedGen, ok, entry.ConnectionGeneration)
}
close(releasePrepare)
<-dispatchDone
if dispatchErr == nil {
t.Fatal("disconnect-winning tunnel dispatch must fail")
}
if result != nil {
t.Fatalf("disconnect-winning must not return a result, got %+v", result)
}
assertNoExtra(t, tunnelReqs, "ProviderTunnelRequest")
assertQueueSettled(t, svc.queue)
})
}
// providerResourceState reads a provider resource's in-flight count and orphan
// flag under the manager lock for white-box assertions.
func providerResourceInflight(q *modelQueueManager, nodeID, providerID string) (inFlight int, orphan, ok bool) {
q.mu.Lock()
defer q.mu.Unlock()
res, exists := q.resources[providerResourceKey{nodeID: nodeID, providerID: providerID}]
if !exists {
return 0, false, false
}
return res.inFlight, res.orphan, true
}
func queuedItemCount(q *modelQueueManager) int {
q.mu.Lock()
defer q.mu.Unlock()
n := 0
for _, g := range q.groups {
n += len(g.queue)
}
return n
}
// TestReconnectActivationLinearizesAgainstDisconnect drives the real queue
// through an actual reconnect and proves the ownership linearization the fix
// establishes: a stranded provider-pool waiter is pumped exactly once, and only
// by the current reconnect generation. A stale (superseded) generation's
// HandleNodeConnect is a no-op — it neither re-activates the orphaned resource
// nor pumps the waiter — while the current generation activates and dispatches it
// exactly once, and all counters settle to zero afterward.
func TestReconnectActivationLinearizesAgainstDisconnect(t *testing.T) {
parser := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
altEdge, altNode := net.Pipe()
defer altEdge.Close()
defer altNode.Close()
reconEdge, reconNode := net.Pipe()
defer reconEdge.Close()
defer reconNode.Close()
altEdgeClient := toki.NewTcpClient(altEdge, 0, 0, parser)
altNodeClient := toki.NewTcpClient(altNode, 0, 0, parser)
reconEdgeClient := toki.NewTcpClient(reconEdge, 0, 0, parser)
reconNodeClient := toki.NewTcpClient(reconNode, 0, 0, parser)
altRuns := make(chan *iop.RunRequest, 4)
reconRuns := make(chan *iop.RunRequest, 4)
toki.AddListenerTyped[*iop.RunRequest](&altNodeClient.Communicator, func(req *iop.RunRequest) {
altRuns <- proto.Clone(req).(*iop.RunRequest)
})
toki.AddListenerTyped[*iop.RunRequest](&reconNodeClient.Communicator, func(req *iop.RunRequest) {
reconRuns <- proto.Clone(req).(*iop.RunRequest)
})
reg := edgenode.NewRegistry()
altEntry := &edgenode.NodeEntry{NodeID: "node-alt", Alias: "alt", AgentKind: config.AgentKindGenericNode, Client: altEdgeClient, DispatchReady: true}
reg.Register(altEntry)
reconEntry := &edgenode.NodeEntry{NodeID: "node-recon", Alias: "recon", AgentKind: config.AgentKindGenericNode, Client: reconEdgeClient, DispatchReady: true}
reg.Register(reconEntry)
gen1 := reconEntry.ConnectionGeneration
svc := New(reg, edgeevents.NewBus())
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
{ID: "node-alt", Alias: "alt", Token: "alt-token", AgentKind: config.AgentKindGenericNode, Providers: []config.NodeProviderConf{
{ID: "prov-alt", Type: "openai", Models: []string{"model-1"}, Health: "available", Capacity: 1},
}},
{ID: "node-recon", Alias: "recon", Token: "recon-token", AgentKind: config.AgentKindGenericNode, Providers: []config.NodeProviderConf{
{ID: "prov-recon", Type: "openai", Models: []string{"model-1"}, Health: "available", Capacity: 1},
}},
})
if err != nil {
t.Fatalf("load node store: %v", err)
}
svc.SetNodeStore(store)
svc.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-1", Providers: map[string]string{"prov-alt": "model-1", "prov-recon": "model-1"}}})
svc.HandleNodeConnect("node-alt", altEntry.ConnectionGeneration)
svc.HandleNodeConnect("node-recon", gen1)
ctx := context.Background()
// Occupy prov-alt with a background run. With both providers idle the tie-break
// (prov-alt < prov-recon) sends it to prov-alt, leaving prov-recon the only
// candidate a later waiter can reach.
if _, err := svc.SubmitRun(ctx, SubmitRunRequest{ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true, Background: true}); err != nil {
t.Fatalf("submit blocking run: %v", err)
}
occupy := recvWire(t, altRuns, "RunRequest(alt)")
if got, _, _ := providerResourceInflight(svc.queue, "node-alt", "prov-alt"); got != 1 {
t.Fatalf("prov-alt in-flight = %d after blocking run, want 1", got)
}
// Disconnect node-recon: its provider resource is orphaned.
reg.UnregisterIfClient("node-recon", reconEntry.Client)
svc.HandleNodeDisconnect("node-recon", gen1, "test-disconnect")
if _, orphan, ok := providerResourceInflight(svc.queue, "node-recon", "prov-recon"); !ok || !orphan {
t.Fatalf("prov-recon must be orphaned after disconnect (ok=%v orphan=%v)", ok, orphan)
}
// Submit the waiter: alt is full and recon is offline, so it strands in queue.
type outcome struct {
res RunResult
err error
}
waiterDone := make(chan outcome, 1)
go func() {
res, err := svc.SubmitRun(ctx, SubmitRunRequest{ModelGroupKey: "model-1", Adapter: "openai", Target: "model-1", ProviderPool: true, Background: true})
waiterDone <- outcome{res: res, err: err}
}()
waitForCondition(t, func() bool { return queuedItemCount(svc.queue) == 1 }, "waiter did not enqueue")
// Reconnect node-recon under a strictly higher generation (pending→ready in the
// real flow; Register here mints the new generation and makes it the owner).
reconEntry2 := &edgenode.NodeEntry{NodeID: "node-recon", Alias: "recon", AgentKind: config.AgentKindGenericNode, Client: reconEdgeClient, DispatchReady: true}
reg.Register(reconEntry2)
gen2 := reconEntry2.ConnectionGeneration
if gen2 <= gen1 {
t.Fatalf("reconnect generation %d must exceed %d", gen2, gen1)
}
// Stale activation: the superseded generation must be a no-op — no resource
// re-activation, no pump, waiter stays queued.
svc.HandleNodeConnect("node-recon", gen1)
if _, orphan, ok := providerResourceInflight(svc.queue, "node-recon", "prov-recon"); !ok || !orphan {
t.Fatalf("stale activation must leave prov-recon orphaned (ok=%v orphan=%v)", ok, orphan)
}
if n := queuedItemCount(svc.queue); n != 1 {
t.Fatalf("stale activation pumped the waiter: queued=%d, want 1", n)
}
select {
case o := <-waiterDone:
t.Fatalf("stale activation dispatched the waiter: err=%v", o.err)
default:
}
assertNoExtra(t, reconRuns, "RunRequest(recon)")
// Current activation: the reconnect generation activates the resource and
// pumps the stranded waiter to it exactly once.
svc.HandleNodeConnect("node-recon", gen2)
var waiter outcome
select {
case waiter = <-waiterDone:
case <-time.After(3 * time.Second):
t.Fatal("current activation did not dispatch the waiter")
}
if waiter.err != nil {
t.Fatalf("waiter dispatch failed: %v", waiter.err)
}
dispatch := waiter.res.Dispatch()
if dispatch.NodeID != "node-recon" || dispatch.ProviderID != "prov-recon" {
t.Fatalf("waiter dispatch=(node=%q provider=%q), want node-recon/prov-recon", dispatch.NodeID, dispatch.ProviderID)
}
reconWire := recvWire(t, reconRuns, "RunRequest(recon)")
if reconWire.GetRunId() != dispatch.RunID {
t.Fatalf("recon wire run_id=%q, want %q", reconWire.GetRunId(), dispatch.RunID)
}
assertNoExtra(t, reconRuns, "RunRequest(recon)")
if got, _, _ := providerResourceInflight(svc.queue, "node-recon", "prov-recon"); got != 1 {
t.Fatalf("prov-recon in-flight = %d after activation, want 1", got)
}
// Settle both runs and confirm no counter leaked.
svc.HandleRunLifecycleEvent(&iop.RunEvent{RunId: occupy.GetRunId(), Type: "complete"})
svc.HandleRunLifecycleEvent(&iop.RunEvent{RunId: dispatch.RunID, Type: "complete"})
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 }, "runs did not settle")
assertQueueSettled(t, svc.queue)
}