Files
go-sip/internal/dispatcher/task_trunk_concurrency_test.go
T

165 lines
5.8 KiB
Go

package dispatcher
import (
"context"
"encoding/json"
"errors"
"testing"
"git.ipao.vip/rogee/go-sip/internal/configread"
)
func TestSelectTrunkHonorsTaskLineLimit(t *testing.T) {
for _, tc := range []struct {
name string
limit, task, total int64
wantErr error
}{
{"available", 1, 0, 0, nil},
{"other task occupancy", 1, 0, 1, nil},
{"disabled", 0, 0, 0, ErrRuleWait},
{"task line full", 1, 1, 1, ErrRuleWait},
{"unknown line occupied", 1, 2, 0, ErrRuleWait},
{"global line full", 5, 0, 2, ErrRuleWait},
{"negative limit", -1, 0, 0, ErrRuleInvalid},
} {
t.Run(tc.name, func(t *testing.T) {
snapshot := policySnapshot(t)
snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: tc.limit}}
choice, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), map[string]int64{"trunk-mock": tc.total}, map[string]int64{"trunk-mock": tc.task}, map[string]int64{"trunk-mock": 8})
if !errors.Is(err, tc.wantErr) {
t.Fatalf("selected=%q error=%v, want %v", choice.TrunkID, err, tc.wantErr)
}
})
}
}
func TestSelectTrunkSkipsDisabledAndSaturatedTaskLinesInConfiguredOrder(t *testing.T) {
for _, limit := range []int64{0, 1} {
snapshot := policySnapshot(t)
var trunks []trunkConfig
if err := json.Unmarshal(snapshot.SIP.Trunks, &trunks); err != nil {
t.Fatal(err)
}
second := trunks[0]
second.TrunkID = "trunk-second"
trunks = append(trunks, second)
var err error
snapshot.SIP.Trunks, err = json.Marshal(trunks)
if err != nil {
t.Fatal(err)
}
snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: limit}, {TrunkID: "trunk-second", Concurrency: 1}}
choice, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), map[string]int64{"trunk-mock": 1}, map[string]int64{"trunk-mock": 1}, map[string]int64{"trunk-mock": 8, "trunk-second": 8})
if err != nil || choice.TrunkID != "trunk-second" {
t.Fatalf("limit=%d selected=%q err=%v", limit, choice.TrunkID, err)
}
}
}
func TestSelectTrunkDoesNotHideMissingLineBehindZeroConcurrency(t *testing.T) {
snapshot := policySnapshot(t)
snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "missing", Concurrency: 0}}
if _, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), nil, nil, nil); !errors.Is(err, ErrRuleInvalid) {
t.Fatalf("disabled line hid a broken reference: %v", err)
}
}
func TestSelectTrunkRejectsDuplicateTaskLineIDs(t *testing.T) {
snapshot := policySnapshot(t)
snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: 1}, {TrunkID: "trunk-mock", Concurrency: 2}}
_, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), nil, nil, map[string]int64{"trunk-mock": 8})
if !errors.Is(err, ErrRuleInvalid) {
t.Fatalf("duplicate task line accepted: %v", err)
}
}
func editedTaskLine(t *testing.T, snapshot configread.Snapshot, revision, limit int64) configread.Snapshot {
t.Helper()
var task map[string]json.RawMessage
if err := json.Unmarshal(snapshot.Task.Raw, &task); err != nil {
t.Fatal(err)
}
for key, value := range map[string]any{
"task_revision": revision,
"allowed_trunk_ids": []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: limit}},
} {
raw, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
task[key] = raw
}
raw, err := json.Marshal(task)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(raw, &snapshot.Task); err != nil {
t.Fatal(err)
}
return snapshot
}
func TestExecuteControllerKeepsUnknownTaskLineSlotUntilConfirmedEnd(t *testing.T) {
c, origin, _, _ := newExecuteFixture(t)
snapshot := editedTaskLine(t, policySnapshot(t), 2, 1)
if state, err := c.Store.CompleteEdit(snapshot, "edit-line-limit"); err != nil || state != "applied" {
t.Fatalf("edit=%q err=%v", state, err)
}
ctx := context.Background()
lostReply := errors.New("simulated originate reply lost")
origin.err = lostReply
if err := c.ProcessExecute(ctx, executeBody(t, "call-first", "15003164745")); !errors.Is(err, lostReply) {
t.Fatalf("unknown originate outcome=%v", err)
}
origin.err = nil
if err := c.ProcessExecute(ctx, executeBody(t, "call-second", "15003164745")); err != nil {
t.Fatal(err)
}
if len(origin.calls) != 1 {
t.Fatalf("task-line limit exceeded: attempts=%d", len(origin.calls))
}
if err := c.ProcessPending(ctx); err != nil || len(origin.calls) != 1 {
t.Fatalf("unknown call was retried or lost its slot: calls=%d err=%v", len(origin.calls), err)
}
if err := c.Store.FinishExecute(c.DispatcherID, "call-first"); err != nil {
t.Fatal(err)
}
if err := c.ProcessPending(ctx); err != nil || len(origin.calls) != 2 {
t.Fatalf("confirmed end did not admit the pending call: calls=%d err=%v", len(origin.calls), err)
}
if origin.calls[1].Snapshot.Task.TaskRevision != 2 || origin.calls[1].Snapshot.Task.AllowedTrunks[0].Concurrency != 1 {
t.Fatal("new task-line limit was not frozen into the approved call")
}
}
type editingTaskOriginator struct {
*fakeOriginator
edit func()
}
func (o *editingTaskOriginator) LoadedTrunks(ctx context.Context) (map[string]int64, error) {
if o.edit != nil {
edit := o.edit
o.edit = nil
edit()
}
return o.fakeOriginator.LoadedTrunks(ctx)
}
func TestExecuteControllerRejectsSelectionIfTaskChangesBeforeReservation(t *testing.T) {
c, origin, _, _ := newExecuteFixture(t)
c.Originator = &editingTaskOriginator{fakeOriginator: origin, edit: func() {
snapshot := editedTaskLine(t, policySnapshot(t), 2, 0)
if state, err := c.Store.CompleteEdit(snapshot, "disable-before-reserve"); err != nil || state != "applied" {
t.Fatalf("edit=%q err=%v", state, err)
}
}}
if err := c.ProcessExecute(context.Background(), executeBody(t, "call-stale-choice", "15003164745")); err != nil {
t.Fatal(err)
}
if len(origin.calls) != 0 {
t.Fatal("stale selection originated after the task line was disabled")
}
}