iop/apps/edge/internal/openai/provider_test_support_test.go
toki e5869a53ae fix(edge): provider 호출 정규화 계층을 추가한다
caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
2026-08-13 23:06:50 +09:00

884 lines
28 KiB
Go

package openai
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
// providerFakeRunService extends the normalized fakeRunService with the
// provider tunnel/pool dispatch state used by provider scenario tests.
type providerFakeRunService struct {
fakeRunService
// tunnelProviderURL, when set, makes SubmitProviderTunnel relay the tunnel
// request to this httptest provider fixture and stream the raw provider
// response back as ordered frames (emulating the Node relay).
tunnelProviderURL string
// tunnelServedTarget emulates provider-pool admission: BuildBody is called
// with this target when non-empty.
tunnelServedTarget string
// tunnelFrames, when set, is handed to the tunnel handle as-is instead of
// contacting tunnelProviderURL.
tunnelFrames chan *iop.ProviderTunnelFrame
tunnelErr error
tunnelReqs []edgeservice.SubmitProviderTunnelRequest
tunnelBodies [][]byte
// tunnelWaitTimeout overrides the tunnel handle wait timeout for
// timeout-path fixtures; zero keeps the 5s default.
tunnelWaitTimeout time.Duration
// poolDispatchPath records which execution path was returned by
// SubmitProviderPool: "provider_tunnel" or "normalized".
poolDispatchPath string
// poolRunFrames is the frame source used by the normalized path when
// SubmitProviderPool is in use.
poolRunFrames chan *iop.RunEvent
// lastTunnelHandle stores the tunnel handle from the last SubmitProviderPool
// tunnel path call, so tests can check headers set by the handler.
lastTunnelHandle *fakeTunnelHandle
// poolSubmitResults provides custom results for successive SubmitProviderPool
// calls. When non-empty, results are consumed in order; when empty, the
// poolDispatchPath / tunnelErr fields are used instead.
poolSubmitResults []edgeservice.ProviderPoolDispatchResult
// poolSubmitCount tracks how many times SubmitProviderPool was called
// (including from the chat handler). Used by tests to assert dispatch
// surface fidelity.
poolSubmitCount int
// poolLastRun records the last SubmitRunRequest passed to SubmitProviderPool
// (pre-PrepareRun). Tests assert ModelGroupKey is non-empty to catch the
// "empty Run" regression.
poolLastRun edgeservice.SubmitRunRequest
// poolPrepareRunCalled tracks whether the optional PrepareRun hook was
// invoked by SubmitProviderPool on the normalized path. Only meaningful
// when poolDispatchPath == "normalized".
poolPrepareRunCalled bool
// poolSelectedCandidate is passed to PrepareProtocolTunnel. Zero value
// emulates a legacy candidate without a concrete profile.
poolSelectedCandidate edgeservice.ProviderPoolCandidate
}
type fakeTunnelHandle struct {
dispatch edgeservice.RunDispatch
headers map[string]string
frames chan *iop.ProviderTunnelFrame
waitTimeout time.Duration
}
func (h *fakeTunnelHandle) Headers() map[string]string { return h.headers }
func (h *fakeTunnelHandle) SetHeaders(hdrs map[string]string) { h.headers = hdrs }
func (h *fakeTunnelHandle) Dispatch() edgeservice.RunDispatch {
disp := h.dispatch
if strings.TrimSpace(disp.ProviderID) == "" {
disp.ProviderID = "test-provider"
}
if strings.TrimSpace(disp.NodeID) == "" {
disp.NodeID = "test-node"
}
return disp
}
func (h *fakeTunnelHandle) Stream() edgeservice.ProviderTunnelStream {
return edgeservice.ProviderTunnelStream{Frames: h.frames}
}
func (h *fakeTunnelHandle) Close() {}
func (h *fakeTunnelHandle) WaitTimeout() time.Duration {
if h.waitTimeout > 0 {
return h.waitTimeout
}
return 5 * time.Second
}
// fakeRunResult implements edgeservice.RunResult for tests.
type fakeRunResult struct {
dispatch edgeservice.RunDispatch
events chan *iop.RunEvent
}
func (r *fakeRunResult) Dispatch() edgeservice.RunDispatch {
disp := r.dispatch
if strings.TrimSpace(disp.ProviderID) == "" {
disp.ProviderID = "test-provider"
}
if strings.TrimSpace(disp.NodeID) == "" {
disp.NodeID = "test-node"
}
return disp
}
func (r *fakeRunResult) Stream() edgeservice.RunStream {
return edgeservice.RunStream{Events: r.events}
}
func (r *fakeRunResult) Close() {}
func (r *fakeRunResult) WaitTimeout() time.Duration {
return 5 * time.Second
}
func (s *providerFakeRunService) SubmitProviderTunnel(_ context.Context, req edgeservice.SubmitProviderTunnelRequest) (edgeservice.ProviderTunnelResult, error) {
s.submitMu.Lock()
s.tunnelReqs = append(s.tunnelReqs, req)
s.submitMu.Unlock()
if s.tunnelErr != nil {
return nil, s.tunnelErr
}
target := req.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
body := req.Body
if req.BuildBody != nil {
built, err := req.BuildBody(target)
if err != nil {
return nil, err
}
body = built
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
frames := s.tunnelFrames
if frames == nil && s.tunnelProviderURL != "" {
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Method, req.Path, body)
if err != nil {
return nil, err
}
frames = relayed
}
providerID := strings.TrimSpace(req.ProviderID)
if providerID == "" {
providerID = "test-provider"
}
nodeID := strings.TrimSpace(req.NodeRef)
if nodeID == "" {
nodeID = "node-1"
}
attribution := strings.TrimSpace(req.UsageAttribution)
if attribution == "" {
attribution = config.UsageAttributionProvider
}
return &fakeTunnelHandle{
dispatch: edgeservice.RunDispatch{
RunID: "run-tunnel",
NodeID: nodeID,
ProviderID: providerID,
UsageAttribution: attribution,
ModelGroupKey: req.ModelGroupKey,
Adapter: req.Adapter,
Target: target,
SessionID: req.SessionID,
TimeoutSec: 5,
},
frames: frames,
waitTimeout: s.tunnelWaitTimeout,
}, nil
}
func checkFakeCandidateAdmission(candidate edgeservice.ProviderPoolCandidate, req edgeservice.ProviderPoolDispatchRequest) error {
operation := strings.TrimSpace(req.Tunnel.Operation)
if operation != "" && candidate.ProtocolProfile != nil {
if _, supported := candidate.ProtocolProfile.Operations[operation]; !supported {
return &edgeservice.ProviderPoolOperationUnsupportedError{Operation: operation}
}
}
if req.AcceptCandidate != nil && !req.AcceptCandidate(candidate) {
return edgeservice.ErrProviderPoolCandidateRejected
}
return nil
}
// SubmitProviderPool is a minimal test double for the provider-pool one-shot
// dispatch. It records the selected path (tunnel vs normalized) and returns a
// stub result so the OpenAI handler can exercise the selection-first flow.
// It also tracks whether the optional PrepareRun hook was invoked, so tests
// can assert the handler only calls PrepareRun on the normalized path.
func (s *providerFakeRunService) SubmitProviderPool(_ context.Context, req edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error) {
s.submitMu.Lock()
s.poolSubmitCount++
s.poolLastRun = req.Run
s.poolPrepareRunCalled = false
s.submitMu.Unlock()
// Custom results for successive calls (e.g. tool-validation retry tests).
s.submitMu.Lock()
if len(s.poolSubmitResults) > 0 {
result := s.poolSubmitResults[0]
s.poolSubmitResults = s.poolSubmitResults[1:]
s.submitMu.Unlock()
if err := checkFakeCandidateAdmission(s.poolSelectedCandidate, req); err != nil {
return nil, err
}
// Apply pre-dispatch tunnel preparation for tunnel path when custom
// results are used.
if result.Path == edgeservice.ProviderPoolPathTunnel && (req.PrepareProtocolTunnel != nil || req.PrepareTunnel != nil) {
tunnelReqPrepared := req.Tunnel
var prepErr error
if req.PrepareProtocolTunnel != nil {
tunnelReqPrepared, prepErr = req.PrepareProtocolTunnel(tunnelReqPrepared, s.poolSelectedCandidate)
} else {
tunnelReqPrepared, prepErr = req.PrepareTunnel(tunnelReqPrepared)
}
if prepErr != nil {
return nil, prepErr
}
req.Tunnel = tunnelReqPrepared
}
return s.buildPoolResult(req, result)
}
err := s.tunnelErr
path := s.poolDispatchPath
s.submitMu.Unlock()
if err := checkFakeCandidateAdmission(s.poolSelectedCandidate, req); err != nil {
return nil, err
}
// Apply pre-dispatch tunnel preparation (mirrors real SubmitProviderPool
// behavior: PrepareTunnel runs BEFORE buildProviderTunnelRequest and the
// Node Send step). On failure, no tunnel request is recorded or sent.
tunnelReq := req.Tunnel
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
if selectedProvider := strings.TrimSpace(s.poolSelectedCandidate.ProviderID); selectedProvider != "" {
tunnelReq.ProviderID = selectedProvider
}
if selectedTarget := strings.TrimSpace(s.poolSelectedCandidate.ActualModel); selectedTarget != "" {
tunnelReq.Target = selectedTarget
}
if req.PrepareProtocolTunnel != nil {
tunnelReqPrepared, prepErr := req.PrepareProtocolTunnel(tunnelReq, s.poolSelectedCandidate)
if prepErr != nil {
return nil, prepErr
}
tunnelReq = tunnelReqPrepared
} else if req.PrepareTunnel != nil {
tunnelReqPrepared, prepErr := req.PrepareTunnel(tunnelReq)
if prepErr != nil {
return nil, prepErr
}
tunnelReq = tunnelReqPrepared
}
req.Tunnel = tunnelReq
}
// Record tunnel request for test assertions.
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
s.submitMu.Lock()
s.tunnelReqs = append(s.tunnelReqs, tunnelReq)
s.submitMu.Unlock()
}
if err != nil {
return nil, err
}
disp := edgeservice.RunDispatch{
RunID: "run-pool-tunnel",
NodeID: "node-pool",
ModelGroupKey: req.Run.ModelGroupKey,
Adapter: "selected",
Target: "served",
ProviderID: "prov-fake",
ProviderType: "vllm",
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel),
QueueReason: "dispatched",
}
if path == string(edgeservice.ProviderPoolPathNormalized) {
disp.RunID = "run-pool-normalized"
}
if selected := s.poolSelectedCandidate; selected.ProtocolProfile != nil {
disp.ProfileID = selected.ProfileID
disp.ProfileDriver = selected.ProfileDriver
disp.ProfileOperation = req.Tunnel.Operation
disp.ProfileCapabilities = append([]string(nil), selected.ProfileCapabilities...)
if selected.ProviderID != "" {
disp.ProviderID = selected.ProviderID
}
if selected.ActualModel != "" {
disp.Target = selected.ActualModel
}
}
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
frames := s.tunnelFrames
if frames == nil && s.tunnelProviderURL != "" {
body := req.Tunnel.Body
if req.Tunnel.BuildBody != nil {
target := req.Tunnel.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
built, err := req.Tunnel.BuildBody(target)
if err != nil {
return nil, err
}
body = built
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Tunnel.Method, req.Tunnel.Path, body)
if err != nil {
return nil, err
}
frames = relayed
} else if frames == nil {
frames = staticProviderTunnelFrames(`{"ok":true}`)
// Record the built tunnel body even when using a static frame source.
body := req.Tunnel.Body
if req.Tunnel.BuildBody != nil {
target := req.Tunnel.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
if built, buildErr := req.Tunnel.BuildBody(target); buildErr == nil {
body = built
}
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
} else {
body := req.Tunnel.Body
if req.Tunnel.BuildBody != nil {
target := req.Tunnel.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
if built, err := req.Tunnel.BuildBody(target); err == nil {
body = built
}
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
}
handle := &fakeTunnelHandle{
dispatch: edgeservice.RunDispatch{
RunID: "run-tunnel",
NodeID: "node-1",
ProviderID: disp.ProviderID,
UsageAttribution: disp.UsageAttribution,
ModelGroupKey: req.Tunnel.ModelGroupKey,
Adapter: req.Tunnel.Adapter,
Target: disp.Target,
SessionID: req.Tunnel.SessionID,
TimeoutSec: 5,
},
headers: req.Tunnel.Headers,
frames: frames,
}
// Store for later inspection by tests.
s.submitMu.Lock()
s.lastTunnelHandle = handle
s.submitMu.Unlock()
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathTunnel,
Tunnel: handle,
DispatchInfo: disp,
}, nil
}
// Normalized path (mimics Ollama/CLI behavior).
// Invoke the optional PrepareRun hook so tests can assert it only runs
// on the normalized path (mirrors real SubmitProviderPool behavior).
if req.PrepareRun != nil {
s.submitMu.Lock()
s.poolPrepareRunCalled = true
s.submitMu.Unlock()
prepared, prepErr := req.PrepareRun(req.Run)
if prepErr != nil {
return nil, prepErr
}
req.Run = prepared
}
s.submitMu.Lock()
s.reqs = append(s.reqs, req.Run)
s.submitMu.Unlock()
runChan := s.poolRunFrames
if runChan == nil {
runChan = make(chan *iop.RunEvent, 1)
runChan <- &iop.RunEvent{RunId: disp.RunID, Type: "complete"}
close(runChan)
}
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathNormalized,
Run: &fakeRunResult{
dispatch: disp,
events: runChan,
},
DispatchInfo: disp,
}, nil
}
// buildPoolResult assembles a ProviderPoolDispatchResult from a pre-configured
// custom result. It handles tunnel request recording, frame relay, and
// normalized path run construction.
func (s *providerFakeRunService) buildPoolResult(req edgeservice.ProviderPoolDispatchRequest, result edgeservice.ProviderPoolDispatchResult) (*edgeservice.ProviderPoolDispatchResult, error) {
if result.Path == edgeservice.ProviderPoolPathTunnel || result.Path == "" {
s.submitMu.Lock()
s.tunnelReqs = append(s.tunnelReqs, req.Tunnel)
s.submitMu.Unlock()
var frames chan *iop.ProviderTunnelFrame
if frames == nil && s.tunnelProviderURL != "" {
body := req.Tunnel.Body
if req.Tunnel.BuildBody != nil {
target := req.Tunnel.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
if built, err := req.Tunnel.BuildBody(target); err == nil {
body = built
}
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Tunnel.Method, req.Tunnel.Path, body)
if err != nil {
return nil, err
}
frames = relayed
} else if frames == nil {
frames = staticProviderTunnelFrames(`{"ok":true}`)
}
disp := result.DispatchInfo
if disp.RunID == "" {
disp.RunID = "run-tunnel"
}
if disp.NodeID == "" {
disp.NodeID = "node-1"
}
handle := &fakeTunnelHandle{
dispatch: disp,
headers: req.Tunnel.Headers,
frames: frames,
waitTimeout: s.tunnelWaitTimeout,
}
s.submitMu.Lock()
s.lastTunnelHandle = handle
s.submitMu.Unlock()
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathTunnel,
Tunnel: handle,
DispatchInfo: disp,
}, nil
}
// Normalized path.
disp := result.DispatchInfo
if disp.RunID == "" {
disp.RunID = "run-pool-normalized"
}
if disp.NodeID == "" {
disp.NodeID = "node-pool"
}
// Invoke the optional PrepareRun hook on the normalized path (mirrors
// real SubmitProviderPool behavior).
if req.PrepareRun != nil {
prepared, prepErr := req.PrepareRun(req.Run)
if prepErr != nil {
return nil, prepErr
}
req.Run = prepared
}
s.submitMu.Lock()
s.poolPrepareRunCalled = true
s.submitMu.Unlock()
s.submitMu.Lock()
s.reqs = append(s.reqs, req.Run)
s.submitMu.Unlock()
// Use the Run from the custom result if provided, otherwise create a
// default completing run.
if result.Run != nil {
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathNormalized,
Run: result.Run,
DispatchInfo: disp,
}, nil
}
runChan := make(chan *iop.RunEvent, 1)
runChan <- &iop.RunEvent{RunId: disp.RunID, Type: "complete"}
close(runChan)
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathNormalized,
Run: &fakeRunResult{
dispatch: disp,
events: runChan,
},
DispatchInfo: disp,
}, nil
}
// tunnelHandleSnapshot returns the tunnel handle from the last SubmitProviderPool
// call, allowing tests to check headers set by the handler.
type tunnelHandleSnapshot struct {
headers map[string]string
runID string
nodeID string
}
func (s *providerFakeRunService) tunnelHandleSnapshot() tunnelHandleSnapshot {
s.submitMu.Lock()
defer s.submitMu.Unlock()
h := s.lastTunnelHandle
if h == nil {
return tunnelHandleSnapshot{}
}
return tunnelHandleSnapshot{
headers: h.Headers(),
runID: h.Dispatch().RunID,
nodeID: h.Dispatch().NodeID,
}
}
// poolSubmitCountSnapshot returns the number of times SubmitProviderPool was
// called. Tests assert exactly-once dispatch from the handler.
func (s *providerFakeRunService) poolSubmitCountSnapshot() int {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return s.poolSubmitCount
}
// poolLastRunSnapshot returns a copy of the last SubmitRunRequest passed to
// SubmitProviderPool. Tests assert ModelGroupKey is non-empty to catch the
// "empty Run" regression.
func (s *providerFakeRunService) poolLastRunSnapshot() edgeservice.SubmitRunRequest {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return s.poolLastRun
}
// poolPrepareRunCalledSnapshot returns whether the PrepareRun hook was
// invoked by the last SubmitProviderPool call. Only meaningful when the last
// call was on the normalized path.
func (s *providerFakeRunService) poolPrepareRunCalledSnapshot() bool {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return s.poolPrepareRunCalled
}
func (s *providerFakeRunService) tunnelReqsSnapshot() []edgeservice.SubmitProviderTunnelRequest {
s.submitMu.Lock()
reqs := make([]edgeservice.SubmitProviderTunnelRequest, len(s.tunnelReqs))
copy(reqs, s.tunnelReqs)
s.submitMu.Unlock()
return reqs
}
func (s *providerFakeRunService) tunnelBodiesSnapshot() [][]byte {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return append([][]byte(nil), s.tunnelBodies...)
}
// --- scripted provider-pool admission double (S16 host fixtures) -------------
// scriptedPoolAttempt describes one SubmitProviderPool admission the scripted
// double must produce, in order. It carries the actual selection result the
// real pool would compute (path, provider, model), so a fixture can assert that
// one request runtime observed a real actual-path switch.
type scriptedPoolAttempt struct {
// path is the execution path this admission resolves to, as the
// service-layer string value ("normalized" or "provider_tunnel").
path string
runID string
provider string
target string
runEvents chan *iop.RunEvent
frames chan *iop.ProviderTunnelFrame
err error
// prepareErr, when set, is returned by the recorded PrepareRun hook so a
// fixture can exercise a normalized candidate whose validation fails.
prepareErr error
}
// scriptedPoolRunService is a thread-safe runService double whose provider-pool
// admissions are scripted in order. It records every dispatch, transport close,
// and cancel so an S16 fixture can assert single-dispatch, exactly-once
// cleanup, and no leak from a discarded attempt.
type scriptedPoolRunService struct {
mu sync.Mutex
attempts []scriptedPoolAttempt
next int
poolRequests []edgeservice.ProviderPoolDispatchRequest
runRequests []edgeservice.SubmitRunRequest
tunnelRequests []edgeservice.SubmitProviderTunnelRequest
cancels []edgeservice.CancelRunRequest
runCloses []string
tunnelCloses []string
prepareTunnel int
prepareRun int
}
func newScriptedPoolRunService(attempts ...scriptedPoolAttempt) *scriptedPoolRunService {
return &scriptedPoolRunService{attempts: attempts}
}
func (s *scriptedPoolRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) {
s.mu.Lock()
s.runRequests = append(s.runRequests, req)
s.mu.Unlock()
return nil, fmt.Errorf("scriptedPoolRunService: direct SubmitRun is not part of a provider-pool fixture")
}
func (s *scriptedPoolRunService) SubmitProviderTunnel(_ context.Context, req edgeservice.SubmitProviderTunnelRequest) (edgeservice.ProviderTunnelResult, error) {
s.mu.Lock()
s.tunnelRequests = append(s.tunnelRequests, req)
s.mu.Unlock()
return nil, fmt.Errorf("scriptedPoolRunService: direct SubmitProviderTunnel is not part of a provider-pool fixture")
}
func (s *scriptedPoolRunService) OllamaAPI(context.Context, edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) {
return edgeservice.OllamaAPIView{StatusCode: http.StatusOK}, nil
}
func (s *scriptedPoolRunService) CancelRun(_ context.Context, req edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) {
s.mu.Lock()
s.cancels = append(s.cancels, req)
s.mu.Unlock()
return edgeservice.CommandResult{NodeID: req.NodeRef}, nil
}
func (s *scriptedPoolRunService) SubmitProviderPool(_ context.Context, req edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error) {
s.mu.Lock()
s.poolRequests = append(s.poolRequests, req)
if s.next >= len(s.attempts) {
s.mu.Unlock()
return nil, fmt.Errorf("scriptedPoolRunService: no scripted attempt left")
}
attempt := s.attempts[s.next]
s.next++
s.mu.Unlock()
if attempt.err != nil {
return nil, attempt.err
}
dispatch := edgeservice.RunDispatch{
RunID: attempt.runID,
NodeID: "node-pool",
ModelGroupKey: req.Run.ModelGroupKey,
Adapter: "selected",
Target: attempt.target,
ProviderID: attempt.provider,
ProviderType: "scripted",
ExecutionPath: string(attempt.path),
SessionID: req.Run.SessionID,
TimeoutSec: 5,
}
if attempt.path == string(edgeservice.ProviderPoolPathTunnel) {
tunnelReq := req.Tunnel
if req.PrepareProtocolTunnel != nil {
s.mu.Lock()
s.prepareTunnel++
s.mu.Unlock()
prepared, err := req.PrepareProtocolTunnel(tunnelReq, edgeservice.ProviderPoolCandidate{
ActualModel: attempt.target, ProviderID: attempt.provider, ExecutionPath: string(attempt.path),
})
if err != nil {
return nil, err
}
tunnelReq = prepared
} else if req.PrepareTunnel != nil {
s.mu.Lock()
s.prepareTunnel++
s.mu.Unlock()
prepared, err := req.PrepareTunnel(tunnelReq)
if err != nil {
return nil, err
}
tunnelReq = prepared
}
if tunnelReq.BuildBody != nil {
if _, err := tunnelReq.BuildBody(attempt.target); err != nil {
return nil, err
}
}
s.mu.Lock()
s.tunnelRequests = append(s.tunnelRequests, tunnelReq)
s.mu.Unlock()
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathTunnel,
Tunnel: &countingTunnelHandle{service: s, dispatch: dispatch, frames: attempt.frames},
DispatchInfo: dispatch,
}, nil
}
runReq := req.Run
if req.PrepareRun != nil {
s.mu.Lock()
s.prepareRun++
s.mu.Unlock()
if attempt.prepareErr != nil {
return nil, attempt.prepareErr
}
prepared, err := req.PrepareRun(runReq)
if err != nil {
return nil, err
}
runReq = prepared
}
s.mu.Lock()
s.runRequests = append(s.runRequests, runReq)
s.mu.Unlock()
return &edgeservice.ProviderPoolDispatchResult{
Path: edgeservice.ProviderPoolPathNormalized,
Run: &countingRunHandle{service: s, dispatch: dispatch, events: attempt.runEvents},
DispatchInfo: dispatch,
}, nil
}
func (s *scriptedPoolRunService) snapshot() (pools int, cancels []string, runCloses, tunnelCloses []string, runReqs []edgeservice.SubmitRunRequest, tunnelReqs []edgeservice.SubmitProviderTunnelRequest) {
s.mu.Lock()
defer s.mu.Unlock()
ids := make([]string, 0, len(s.cancels))
for _, c := range s.cancels {
ids = append(ids, c.RunID)
}
return len(s.poolRequests),
ids,
append([]string(nil), s.runCloses...),
append([]string(nil), s.tunnelCloses...),
append([]edgeservice.SubmitRunRequest(nil), s.runRequests...),
append([]edgeservice.SubmitProviderTunnelRequest(nil), s.tunnelRequests...)
}
func (s *scriptedPoolRunService) poolSubmits() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.poolRequests)
}
type countingRunHandle struct {
service *scriptedPoolRunService
dispatch edgeservice.RunDispatch
events chan *iop.RunEvent
}
func (h *countingRunHandle) Dispatch() edgeservice.RunDispatch { return h.dispatch }
func (h *countingRunHandle) Stream() edgeservice.RunStream {
return edgeservice.RunStream{Events: h.events}
}
func (h *countingRunHandle) Close() {
h.service.mu.Lock()
h.service.runCloses = append(h.service.runCloses, h.dispatch.RunID)
h.service.mu.Unlock()
}
func (h *countingRunHandle) WaitTimeout() time.Duration { return 5 * time.Second }
type countingTunnelHandle struct {
service *scriptedPoolRunService
dispatch edgeservice.RunDispatch
headers map[string]string
frames chan *iop.ProviderTunnelFrame
}
func (h *countingTunnelHandle) Headers() map[string]string { return h.headers }
func (h *countingTunnelHandle) SetHeaders(hdrs map[string]string) { h.headers = hdrs }
func (h *countingTunnelHandle) Dispatch() edgeservice.RunDispatch { return h.dispatch }
func (h *countingTunnelHandle) Stream() edgeservice.ProviderTunnelStream {
return edgeservice.ProviderTunnelStream{Frames: h.frames}
}
func (h *countingTunnelHandle) Close() {
h.service.mu.Lock()
h.service.tunnelCloses = append(h.service.tunnelCloses, h.dispatch.RunID)
h.service.mu.Unlock()
}
func (h *countingTunnelHandle) WaitTimeout() time.Duration { return 5 * time.Second }
var (
_ runService = (*scriptedPoolRunService)(nil)
_ edgeservice.RunResult = (*countingRunHandle)(nil)
_ edgeservice.ProviderTunnelResult = (*countingTunnelHandle)(nil)
)
// bufferedTunnelFrames builds a closed, fully buffered tunnel frame channel.
func bufferedTunnelFrames(frames ...*iop.ProviderTunnelFrame) chan *iop.ProviderTunnelFrame {
ch := make(chan *iop.ProviderTunnelFrame, len(frames))
for _, f := range frames {
ch <- f
}
close(ch)
return ch
}
// relayProviderFixtureFrames performs the provider HTTP request the way the
// Node relay does and converts status/header/body bytes into ordered tunnel
// frames, chunking the body at odd boundaries to exercise ordered writes.
func relayProviderFixtureFrames(providerURL, method, path string, body []byte) (chan *iop.ProviderTunnelFrame, error) {
httpReq, err := http.NewRequest(method, providerURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
headers := make(map[string]string)
for k, values := range resp.Header {
if len(values) > 0 {
headers[k] = values[0]
}
}
const chunkSize = 7
frames := make(chan *iop.ProviderTunnelFrame, len(respBody)/chunkSize+3)
seq := int64(0)
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: int32(resp.StatusCode),
Headers: headers,
}
seq++
for off := 0; off < len(respBody); off += chunkSize {
end := off + chunkSize
if end > len(respBody) {
end = len(respBody)
}
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: append([]byte(nil), respBody[off:end]...),
}
seq++
}
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
End: true,
}
close(frames)
return frames, nil
}
func staticProviderTunnelFrames(body string) chan *iop.ProviderTunnelFrame {
frames := make(chan *iop.ProviderTunnelFrame, 3)
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(body)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
return frames
}