315 lines
8.9 KiB
Go
315 lines
8.9 KiB
Go
package node_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"iop/apps/node/internal/node"
|
|
"iop/apps/node/internal/store"
|
|
runtime "iop/packages/go/execution"
|
|
)
|
|
|
|
// fixedRouter dispatches to a pre-built adapter map. It satisfies runtime.Router.
|
|
type fixedRouter struct {
|
|
adapterName string
|
|
adapter runtime.Provider
|
|
adapters map[string]runtime.Provider
|
|
lookupErrors map[string]error // optional per-adapter errors for LookupAdapter
|
|
}
|
|
|
|
func (r *fixedRouter) Resolve(_ context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, error) {
|
|
return runtime.ExecutionSpec{
|
|
RunID: req.RunID,
|
|
Adapter: r.adapterName,
|
|
Target: req.Target,
|
|
SessionID: req.SessionID,
|
|
Background: req.Background,
|
|
Policy: req.Policy,
|
|
Input: req.Input,
|
|
TimeoutSec: req.TimeoutSec,
|
|
Metadata: req.Metadata,
|
|
}, nil
|
|
}
|
|
|
|
func (r *fixedRouter) ResolveAdapter(ctx context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, runtime.Provider, error) {
|
|
spec, err := r.Resolve(ctx, req)
|
|
if err != nil {
|
|
return runtime.ExecutionSpec{}, nil, err
|
|
}
|
|
a, ok := r.adapters[spec.Adapter]
|
|
if !ok {
|
|
return runtime.ExecutionSpec{}, nil, fmt.Errorf("router: adapter %q not found", spec.Adapter)
|
|
}
|
|
return spec, a, nil
|
|
}
|
|
|
|
func (r *fixedRouter) LookupAdapter(adapterName string) (runtime.Provider, error) {
|
|
if r.lookupErrors != nil {
|
|
if err, ok := r.lookupErrors[adapterName]; ok {
|
|
return nil, err
|
|
}
|
|
}
|
|
a, ok := r.adapters[adapterName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("adapter %q not found", adapterName)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (r *fixedRouter) GetAdapter(adapterName string) (runtime.Provider, bool) {
|
|
a, ok := r.adapters[adapterName]
|
|
return a, ok
|
|
}
|
|
|
|
// errorRouter always returns the configured error.
|
|
type errorRouter struct{ err error }
|
|
|
|
func (r *errorRouter) Resolve(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, error) {
|
|
return runtime.ExecutionSpec{}, r.err
|
|
}
|
|
|
|
func (r *errorRouter) ResolveAdapter(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, runtime.Provider, error) {
|
|
return runtime.ExecutionSpec{}, nil, r.err
|
|
}
|
|
|
|
func (r *errorRouter) LookupAdapter(_ string) (runtime.Provider, error) {
|
|
return nil, r.err
|
|
}
|
|
|
|
func (r *errorRouter) GetAdapter(_ string) (runtime.Provider, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
// makeNode builds a node with a default store (in-memory) and no concurrency gate.
|
|
func makeNode(t *testing.T, rtr runtime.Router) (*node.Node, *store.Store) {
|
|
t.Helper()
|
|
return makeNodeWithConcurrency(t, rtr, 0)
|
|
}
|
|
|
|
// makeNodeWithConcurrency builds a node with a default store (in-memory) and a
|
|
// global concurrency limit (0 = disabled).
|
|
func makeNodeWithConcurrency(t *testing.T, rtr runtime.Router, concurrency int) (*node.Node, *store.Store) {
|
|
t.Helper()
|
|
st, err := store.New(":memory:", zap.NewNop())
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
return node.New("test-node", rtr, st, concurrency, io.Discard, zap.NewNop(), nil), st
|
|
}
|
|
|
|
// --- shared concurrency helpers (slow adapter + synchronization) ---
|
|
|
|
// slowAdapterUnlimited reports MaxConcurrency=0 (unlimited) and blocks until
|
|
// released, exposing a start channel so tests can synchronize.
|
|
type slowAdapterUnlimited struct {
|
|
started chan struct{}
|
|
release chan struct{}
|
|
}
|
|
|
|
func newSlowAdapterUnlimited() *slowAdapterUnlimited {
|
|
return &slowAdapterUnlimited{started: make(chan struct{}, 1), release: make(chan struct{})}
|
|
}
|
|
|
|
func (a *slowAdapterUnlimited) Name() string { return "slow" }
|
|
func (a *slowAdapterUnlimited) Capabilities(_ context.Context) (runtime.Capabilities, error) {
|
|
return runtime.Capabilities{AdapterName: "slow", MaxConcurrency: 0}, nil
|
|
}
|
|
func (a *slowAdapterUnlimited) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
|
|
select {
|
|
case a.started <- struct{}{}:
|
|
default:
|
|
}
|
|
select {
|
|
case <-a.release:
|
|
case <-ctx.Done():
|
|
return runtime.ErrRunCancelled
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// queuedSlowAdapter is a configurable adapter used by concurrency safety tests.
|
|
// It advertises MaxConcurrency / MaxQueue / QueueTimeout via Capabilities, and
|
|
// gives per-run control: each Execute blocks until the test releases (or fails)
|
|
// that specific run id, so tests can observe execution ordering deterministically.
|
|
type queuedSlowAdapter struct {
|
|
name string
|
|
maxConcurrency int
|
|
maxQueue int
|
|
queueTimeout time.Duration
|
|
|
|
mu sync.Mutex
|
|
gates map[string]chan error // run_id → release signal carrying exec result
|
|
startSeq chan string // run_id pushed when Execute begins
|
|
}
|
|
|
|
func newQueuedSlowAdapter(name string, maxConcurrency, maxQueue int, queueTimeout time.Duration) *queuedSlowAdapter {
|
|
return &queuedSlowAdapter{
|
|
name: name,
|
|
maxConcurrency: maxConcurrency,
|
|
maxQueue: maxQueue,
|
|
queueTimeout: queueTimeout,
|
|
gates: make(map[string]chan error),
|
|
startSeq: make(chan string, 64),
|
|
}
|
|
}
|
|
|
|
func (a *queuedSlowAdapter) Name() string { return a.name }
|
|
|
|
func (a *queuedSlowAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
|
|
return runtime.Capabilities{
|
|
AdapterName: a.name,
|
|
MaxConcurrency: a.maxConcurrency,
|
|
MaxQueue: a.maxQueue,
|
|
QueueTimeoutMS: int(a.queueTimeout / time.Millisecond),
|
|
}, nil
|
|
}
|
|
|
|
func (a *queuedSlowAdapter) gateFor(runID string) chan error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
ch, ok := a.gates[runID]
|
|
if !ok {
|
|
ch = make(chan error, 1)
|
|
a.gates[runID] = ch
|
|
}
|
|
return ch
|
|
}
|
|
|
|
func (a *queuedSlowAdapter) Execute(ctx context.Context, spec runtime.ExecutionSpec, _ runtime.EventSink) error {
|
|
gate := a.gateFor(spec.RunID)
|
|
a.startSeq <- spec.RunID
|
|
select {
|
|
case res := <-gate:
|
|
return res
|
|
case <-ctx.Done():
|
|
return runtime.ErrRunCancelled
|
|
}
|
|
}
|
|
|
|
// releaseRun lets the given run complete successfully.
|
|
func (a *queuedSlowAdapter) releaseRun(runID string) { a.gateFor(runID) <- nil }
|
|
|
|
// failRun lets the given run terminate with a non-cancel error.
|
|
func (a *queuedSlowAdapter) failRun(runID string) { a.gateFor(runID) <- errBoom }
|
|
|
|
// preRelease pre-arms a run so it completes the instant it starts executing.
|
|
func (a *queuedSlowAdapter) preRelease(runID string) { a.gateFor(runID) <- nil }
|
|
|
|
var errBoom = errors.New("boom")
|
|
|
|
// waitStarted blocks until the adapter reports the given run id started, or
|
|
// fails the test after a timeout.
|
|
func waitStarted(t *testing.T, a *queuedSlowAdapter, runID string) {
|
|
t.Helper()
|
|
deadline := time.After(2 * time.Second)
|
|
for {
|
|
select {
|
|
case got := <-a.startSeq:
|
|
if got == runID {
|
|
return
|
|
}
|
|
a.startSeq <- got
|
|
time.Sleep(2 * time.Millisecond)
|
|
case <-deadline:
|
|
t.Fatalf("run %s never started", runID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// requireStatus asserts the run is stored with exactly the given status now.
|
|
func requireStatus(t *testing.T, st *store.Store, runID, status string) *store.RunRecord {
|
|
t.Helper()
|
|
return requireStatusEventually(t, st, runID, status)
|
|
}
|
|
|
|
// requireStatusEventually polls until the run reaches the expected status.
|
|
func requireStatusEventually(t *testing.T, st *store.Store, runID, status string) *store.RunRecord {
|
|
t.Helper()
|
|
deadline := time.After(2 * time.Second)
|
|
for {
|
|
run, err := st.GetRun(context.Background(), runID)
|
|
if err != nil {
|
|
t.Fatalf("GetRun(%s): %v", runID, err)
|
|
}
|
|
if run != nil && run.Status == status {
|
|
return run
|
|
}
|
|
select {
|
|
case <-deadline:
|
|
got := "<nil>"
|
|
if run != nil {
|
|
got = run.Status
|
|
}
|
|
t.Fatalf("run %s: expected status %q, got %q", runID, status, got)
|
|
case <-time.After(5 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- lifecycle test doubles (registry-refresh tests) ---
|
|
|
|
type lifecycleTestAdapter struct {
|
|
name string
|
|
stopCalls int32
|
|
started chan struct{}
|
|
blockChan chan struct{}
|
|
}
|
|
|
|
func (a *lifecycleTestAdapter) Name() string { return a.name }
|
|
|
|
func (a *lifecycleTestAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
|
|
return runtime.Capabilities{
|
|
AdapterName: a.name,
|
|
MaxConcurrency: 1,
|
|
}, nil
|
|
}
|
|
|
|
func (a *lifecycleTestAdapter) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
|
|
close(a.started)
|
|
select {
|
|
case <-a.blockChan:
|
|
case <-ctx.Done():
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *lifecycleTestAdapter) Start(_ context.Context) error { return nil }
|
|
|
|
func (a *lifecycleTestAdapter) Stop(_ context.Context) error {
|
|
atomic.AddInt32(&a.stopCalls, 1)
|
|
return nil
|
|
}
|
|
|
|
type blockingCapsAdapter struct {
|
|
name string
|
|
capsStarted chan struct{}
|
|
capsBlock chan struct{}
|
|
}
|
|
|
|
func (a *blockingCapsAdapter) Name() string { return a.name }
|
|
|
|
func (a *blockingCapsAdapter) Capabilities(ctx context.Context) (runtime.Capabilities, error) {
|
|
select {
|
|
case a.capsStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
<-a.capsBlock
|
|
return runtime.Capabilities{
|
|
AdapterName: a.name,
|
|
MaxConcurrency: 1,
|
|
}, nil
|
|
}
|
|
|
|
func (a *blockingCapsAdapter) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
|
|
return nil
|
|
}
|
|
func (a *blockingCapsAdapter) Start(_ context.Context) error { return nil }
|
|
func (a *blockingCapsAdapter) Stop(_ context.Context) error { return nil }
|