2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
523 lines
18 KiB
Go
523 lines
18 KiB
Go
package monthlyusage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/testutil"
|
|
"github.com/gogf/gf/v2/container/gvar"
|
|
)
|
|
|
|
// datasetScanner applies the SQL eligibility predicate before LIMIT; metadata
|
|
// eligibility is applied by the producer. Optional hooks simulate SQL latency,
|
|
// live additions and failures without accessing an application database.
|
|
type datasetScanner struct {
|
|
cards []Card
|
|
channels []channelMetadata
|
|
upper int64
|
|
hook func(context.Context, string, int64) error
|
|
queries atomic.Int32
|
|
lists atomic.Int32
|
|
inflight atomic.Int32
|
|
peak atomic.Int32
|
|
}
|
|
|
|
func (s *datasetScanner) SnapshotMaxID(context.Context) (int64, error) { return s.upper, nil }
|
|
func (s *datasetScanner) ListChannels(context.Context) ([]channelMetadata, error) {
|
|
s.lists.Add(1)
|
|
return s.channels, nil
|
|
}
|
|
func (s *datasetScanner) Scan(ctx context.Context, last, upper int64, size int) ([]Card, error) {
|
|
var rows []Card
|
|
for _, card := range s.cards {
|
|
if card.ID > last && card.ID <= upper {
|
|
for _, channel := range s.channels {
|
|
if channel.ChanKey == card.ChanKey {
|
|
card.TempKey = channel.TempKey
|
|
card.SyncEnabled = channel.SyncEnabled
|
|
card.Operator = channel.Operator
|
|
}
|
|
}
|
|
rows = append(rows, card)
|
|
if len(rows) == size {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return rows, nil
|
|
}
|
|
func (s *datasetScanner) ScanChannel(ctx context.Context, key string, last int64, size int) ([]Card, error) {
|
|
s.queries.Add(1)
|
|
running := s.inflight.Add(1)
|
|
defer s.inflight.Add(-1)
|
|
for {
|
|
old := s.peak.Load()
|
|
if running <= old || s.peak.CompareAndSwap(old, running) {
|
|
break
|
|
}
|
|
}
|
|
if s.hook != nil {
|
|
if err := s.hook(ctx, key, last); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var rows []Card
|
|
for _, card := range s.cards {
|
|
if card.ChanKey == key && card.ID > last {
|
|
rows = append(rows, card)
|
|
if len(rows) == size {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return rows, nil
|
|
}
|
|
func scannerFor(cards []Card, upper int64) *datasetScanner {
|
|
keys := map[string]bool{}
|
|
s := &datasetScanner{cards: cards, upper: upper}
|
|
for _, card := range cards {
|
|
if card.ChanKey != "" && !keys[card.ChanKey] {
|
|
keys[card.ChanKey] = true
|
|
s.channels = append(s.channels, channelMetadata{ChanKey: card.ChanKey, TempKey: "sim_yd_v5", Operator: 1})
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
func testStore(t *testing.T) *zset.Store {
|
|
t.Helper()
|
|
store, err := zset.New(testutil.Redis(t), "flow:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return store
|
|
}
|
|
func testProducer(t *testing.T, scanner CardScanner, store *zset.Store, config Config) *Producer {
|
|
t.Helper()
|
|
config.RoundRetryEnabled = true
|
|
p, err := New(scanner, store, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
func contextForTest(t *testing.T) context.Context {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
t.Cleanup(cancel)
|
|
return ctx
|
|
}
|
|
func taskIDs(t *testing.T, store *zset.Store, run string) []string {
|
|
t.Helper()
|
|
key, _ := store.TaskStateKey(run)
|
|
result, err := store.RedisClient().Do(context.Background(), "HKEYS", key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids := result.Strings()
|
|
sort.Strings(ids)
|
|
return ids
|
|
}
|
|
func requireMeta(t *testing.T, store *zset.Store, run string, done bool, active int64) {
|
|
t.Helper()
|
|
meta, err := store.GetRunMeta(context.Background(), run)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if meta.ProducerDone != done || meta.ActiveCount != active {
|
|
t.Fatalf("meta=%+v, want done=%t active=%d", meta, done, active)
|
|
}
|
|
}
|
|
func waitUntil(t *testing.T, ctx context.Context, condition func() bool) {
|
|
t.Helper()
|
|
for !condition() {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
case <-time.After(time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelModeSettings(t *testing.T) {
|
|
store, _ := zset.New(&producerRedis{}, "flow:")
|
|
p, err := New(&fakeScanner{}, store, Config{})
|
|
if err != nil || p.config.ScanConcurrency != 4 {
|
|
t.Fatalf("defaults: p=%+v err=%v", p, err)
|
|
}
|
|
for _, config := range []Config{{ScanConcurrency: -1}} {
|
|
if _, err = New(&fakeScanner{}, store, config); err == nil {
|
|
t.Fatalf("accepted invalid config/capability %+v", config)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelScanFiniteMemoryBoundary(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
upper int64
|
|
cards []Card
|
|
ids []int64
|
|
queries int32
|
|
}{
|
|
{"zero upper", 0, []Card{{ID: 1, ChanKey: "a"}}, nil, 0},
|
|
{"exact page", 4, []Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "a"}, {ID: 3, ChanKey: "a"}, {ID: 4, ChanKey: "a"}}, []int64{1, 2, 3, 4}, 2},
|
|
{"tail then empty", 9, []Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "a"}, {ID: 3, ChanKey: "a"}}, []int64{1, 2, 3}, 3},
|
|
{"first outside", 3, []Card{{ID: 4, ChanKey: "a"}, {ID: 5, ChanKey: "a"}}, nil, 1},
|
|
{"mixed final page", 3, []Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "a"}, {ID: 3, ChanKey: "a"}, {ID: 4, ChanKey: "a"}, {ID: 5, ChanKey: "a"}}, []int64{1, 2, 3}, 2},
|
|
{"empty channel", 9, nil, nil, 1},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
store := testStore(t)
|
|
scanner := scannerFor(test.cards, test.upper)
|
|
if len(scanner.channels) == 0 {
|
|
scanner.channels = []channelMetadata{{ChanKey: "a", TempKey: "sim_yd_v5"}}
|
|
}
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2})
|
|
if err := p.Produce(contextForTest(t), scopedMessage("boundary")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var want []string
|
|
for _, id := range test.ids {
|
|
want = append(want, zset.StableTaskID("boundary", operation, id))
|
|
}
|
|
sort.Strings(want)
|
|
got := taskIDs(t, store, "boundary")
|
|
if len(got) != len(want) || (len(want) > 0 && !reflect.DeepEqual(got, want)) {
|
|
t.Fatalf("got=%v want=%v", got, want)
|
|
}
|
|
if scanner.queries.Load() != test.queries {
|
|
t.Fatalf("queries=%d want=%d", scanner.queries.Load(), test.queries)
|
|
}
|
|
requireMeta(t, store, "boundary", true, int64(len(want)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelScanLiveHigherIDsDoNotExtendRun(t *testing.T) {
|
|
store := testStore(t)
|
|
scanner := scannerFor([]Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "a"}, {ID: 3, ChanKey: "a"}}, 3)
|
|
scanner.hook = func(_ context.Context, _ string, last int64) error {
|
|
if last > 0 {
|
|
scanner.cards = append(scanner.cards, Card{ID: 100 + last, ChanKey: "a"})
|
|
}
|
|
return nil
|
|
}
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2})
|
|
if err := p.Produce(contextForTest(t), scopedMessage("live")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(taskIDs(t, store, "live")) != 3 || scanner.queries.Load() != 2 {
|
|
t.Fatal("higher IDs extended finite range")
|
|
}
|
|
}
|
|
|
|
func TestChannelScanSerialTaskIDSetParity(t *testing.T) {
|
|
for _, scope := range [][]string{nil, {"a", "b"}, {"disabled", "unknown"}} {
|
|
t.Run(fmt.Sprint(scope), func(t *testing.T) {
|
|
store := testStore(t)
|
|
cards := []Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "b"}, {ID: 3, ChanKey: "a"}, {ID: 4, ChanKey: "disabled"}, {ID: 5, ChanKey: "no-template"}, {ID: 6, ChanKey: "unknown"}, {ID: 7, ChanKey: "b"}, {ID: 8, ChanKey: "a"}}
|
|
var sets [][]string
|
|
for _, mode := range []string{"serial", "channel"} {
|
|
scanner := scannerFor(cards, 7)
|
|
scanner.channels = []channelMetadata{{ChanKey: "a", TempKey: "sim_yd_v5", Operator: 1}, {ChanKey: "b", TempKey: "sim_dx_v1", Operator: 2}, {ChanKey: "disabled", TempKey: "sim_yd_v5", SyncEnabled: true}, {ChanKey: "no-template"}}
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2})
|
|
run := "parity-" + mode
|
|
if err := p.Produce(contextForTest(t), scopedMessage(run, scope...)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids := taskIDs(t, store, run)
|
|
for i, id := range ids {
|
|
ids[i] = strings.TrimPrefix(id, run+":")
|
|
}
|
|
sets = append(sets, ids)
|
|
if mode == "channel" && scanner.lists.Load() != 1 {
|
|
t.Fatal("metadata not loaded once")
|
|
}
|
|
_, _ = store.RedisClient().Do(context.Background(), "SREM", store.ActiveRunsKey(), run)
|
|
}
|
|
if !reflect.DeepEqual(sets[0], sets[1]) {
|
|
t.Fatalf("serial=%v channel=%v", sets[0], sets[1])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelSQLSlotsReleasedWhileChannelsWait(t *testing.T) {
|
|
store := testStore(t)
|
|
ctx := contextForTest(t)
|
|
var cards []Card
|
|
for i := 0; i < 9; i++ {
|
|
cards = append(cards, Card{ID: int64(i + 1), ChanKey: fmt.Sprint(i)}, Card{ID: int64(i + 10), ChanKey: fmt.Sprint(i)})
|
|
}
|
|
sort.Slice(cards, func(i, j int) bool { return cards[i].ID < cards[j].ID })
|
|
scanner := scannerFor(cards, 100)
|
|
scanner.hook = func(ctx context.Context, _ string, _ int64) error { return sleepContext(ctx, 5*time.Millisecond) }
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2, ScanConcurrency: 4, ChannelHighWaterMark: 1, GlobalHighWaterMark: 100, PollInterval: time.Millisecond})
|
|
result := make(chan error, 1)
|
|
go func() { result <- p.Produce(ctx, scopedMessage("isolation")) }()
|
|
waitUntil(t, ctx, func() bool { return len(taskIDs(t, store, "isolation")) == 9 })
|
|
if peak := scanner.peak.Load(); peak > 4 || peak < 2 {
|
|
t.Fatalf("SQL peak=%d", peak)
|
|
}
|
|
// All nine channels got a first task despite the first four waiting at
|
|
// channel capacity. SQL slots are free while every channel is paused.
|
|
if scanner.inflight.Load() != 0 {
|
|
t.Fatal("SQL tokens held during channel backpressure")
|
|
}
|
|
for _, channel := range scanner.channels {
|
|
if _, err := store.BlockRunScoped(ctx, "isolation", channel.ChanKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := <-result; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta, err := store.GetRunMeta(ctx, "isolation")
|
|
if err != nil || !meta.ProducerDone {
|
|
t.Fatalf("meta=%+v err=%v", meta, err)
|
|
}
|
|
}
|
|
|
|
func TestChannelRemainingCapacityAndDuplicateAdmission(t *testing.T) {
|
|
store := testStore(t)
|
|
ctx := contextForTest(t)
|
|
scanner := scannerFor(nil, 0)
|
|
p := testProducer(t, scanner, store, Config{GlobalHighWaterMark: 3, ChannelHighWaterMark: 2, PollInterval: time.Millisecond})
|
|
if err := store.CreateRun(ctx, zset.RunMeta{RunID: "capacity", Status: "producing"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Existing success and waiting states still deduplicate and consume no new
|
|
// capacity. Active includes leases: inflight must not be added again.
|
|
stateKey, _ := store.TaskStateKey("capacity")
|
|
metaKey, _ := store.RunMetaKey("capacity")
|
|
activeKey, _ := store.RoundActiveKey("capacity", 1, "a")
|
|
_, _ = store.RedisClient().Do(ctx, "HSET", stateKey, "one", `{"status":"success"}`, "two", `{"status":"waiting"}`)
|
|
_, _ = store.RedisClient().Do(ctx, "HSET", metaKey, "activeCount", 2, "inflightCount", 2)
|
|
stats := &channelScanStats{chanKey: "a", lastSummary: time.Now()}
|
|
tasks := []zset.Task{{TaskID: "one", RunID: "capacity", ChanKey: "a"}, {TaskID: "two", RunID: "capacity", ChanKey: "a"}, {TaskID: "three", RunID: "capacity", ChanKey: "a"}, {TaskID: "four", RunID: "capacity", ChanKey: "a"}}
|
|
gate := make(chan struct{}, 1)
|
|
result := make(chan error, 1)
|
|
go func() { _, err := p.admitChannelPage(ctx, "capacity", tasks, time.Now(), gate, stats); result <- err }()
|
|
waitUntil(t, ctx, func() bool { meta, e := store.GetRunMeta(ctx, "capacity"); return e == nil && meta.ActiveCount == 3 })
|
|
if size, err := store.SizeKey(ctx, activeKey); err != nil || size != 1 {
|
|
t.Fatalf("initial slice size=%d err=%v", size, err)
|
|
}
|
|
_, _ = store.RedisClient().Do(ctx, "HSET", metaKey, "activeCount", 0, "inflightCount", 0)
|
|
if err := <-result; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stats.duplicates != 2 || stats.added != 2 || stats.enqueueBatches != 2 || stats.globalWait == 0 {
|
|
t.Fatalf("stats=%+v", stats)
|
|
}
|
|
}
|
|
|
|
// faultRedis forwards to a real isolated Redis, optionally returning a lost
|
|
// response after Lua succeeded. Never mocks the Lua semantics under test.
|
|
type faultRedis struct {
|
|
zset.RedisClient
|
|
failEnqueue atomic.Int32
|
|
failRead atomic.Bool
|
|
failRenew atomic.Bool
|
|
lostResponse bool
|
|
reject bool
|
|
}
|
|
|
|
func (r *faultRedis) Do(ctx context.Context, command string, args ...any) (*gvar.Var, error) {
|
|
if r.failRead.Load() && command == "HMGET" {
|
|
return nil, errors.New("Redis read failure")
|
|
}
|
|
return r.RedisClient.Do(ctx, command, args...)
|
|
}
|
|
func (r *faultRedis) Eval(ctx context.Context, script string, n int64, keys []string, args []any) (*gvar.Var, error) {
|
|
if r.failRenew.Load() && strings.Contains(script, "PEXPIRE") && strings.Contains(script, "owner ~=") {
|
|
return gvar.New(0), nil
|
|
}
|
|
if n == 4 && strings.Contains(script, "HEXISTS") {
|
|
if r.reject {
|
|
return gvar.New(0), nil
|
|
}
|
|
if r.failEnqueue.CompareAndSwap(1, 0) {
|
|
if r.lostResponse {
|
|
if _, err := r.RedisClient.Eval(ctx, script, n, keys, args); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, errors.New("enqueue response unavailable")
|
|
}
|
|
}
|
|
return r.RedisClient.Eval(ctx, script, n, keys, args)
|
|
}
|
|
|
|
func TestChannelRescanPartialAndUncertainWrites(t *testing.T) {
|
|
for _, uncertain := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(uncertain), func(t *testing.T) {
|
|
client := &faultRedis{RedisClient: testutil.Redis(t), lostResponse: uncertain}
|
|
store, _ := zset.New(client, "flow:")
|
|
scanner := scannerFor([]Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "a"}, {ID: 3, ChanKey: "a"}, {ID: 4, ChanKey: "a"}}, 4)
|
|
// Fail the second page: first page must survive; uncertain success is
|
|
// confirmed only on a same-Run rescan, with stable IDs and no double count.
|
|
scanner.hook = func(_ context.Context, _ string, last int64) error {
|
|
if last == 2 {
|
|
client.failEnqueue.Store(1)
|
|
}
|
|
return nil
|
|
}
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2})
|
|
if err := p.Produce(contextForTest(t), scopedMessage("rescan")); err == nil {
|
|
t.Fatal("expected enqueue failure")
|
|
}
|
|
active := int64(2)
|
|
if uncertain {
|
|
active = 4
|
|
}
|
|
requireMeta(t, store, "rescan", false, active)
|
|
scanner.hook = nil
|
|
if err := p.Produce(contextForTest(t), scopedMessage("rescan")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requireMeta(t, store, "rescan", true, 4)
|
|
if len(taskIDs(t, store, "rescan")) != 4 {
|
|
t.Fatal("wrong stable task set")
|
|
}
|
|
before := scanner.queries.Load()
|
|
if err := p.Produce(contextForTest(t), scopedMessage("rescan")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if before != scanner.queries.Load() {
|
|
t.Fatal("producerDone redelivery rescanned cards")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelFailureCancelsAllWorkersWithoutProducerDone(t *testing.T) {
|
|
for _, kind := range []string{"sql", "redis", "unexplained_rejection", "lease"} {
|
|
t.Run(kind, func(t *testing.T) {
|
|
client := &faultRedis{RedisClient: testutil.Redis(t)}
|
|
store, _ := zset.New(client, "flow:")
|
|
cards := []Card{{ID: 1, ChanKey: "a"}, {ID: 2, ChanKey: "b"}, {ID: 3, ChanKey: "c"}}
|
|
scanner := scannerFor(cards, 10)
|
|
allStarted := make(chan struct{})
|
|
var started atomic.Int32
|
|
var once sync.Once
|
|
scanner.hook = func(ctx context.Context, key string, _ int64) error {
|
|
if started.Add(1) >= 3 {
|
|
once.Do(func() { close(allStarted) })
|
|
}
|
|
select {
|
|
case <-allStarted:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
if kind == "lease" {
|
|
client.failRenew.Store(true)
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
if key == "a" {
|
|
switch kind {
|
|
case "sql":
|
|
return errors.New("SQL failed")
|
|
case "redis":
|
|
client.failRead.Store(true)
|
|
case "unexplained_rejection":
|
|
client.reject = true
|
|
}
|
|
return nil
|
|
}
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
p := testProducer(t, scanner, store, Config{PageSize: 2, RunLockTTL: 90 * time.Millisecond})
|
|
if err := p.Produce(contextForTest(t), scopedMessage("failure")); err == nil {
|
|
t.Fatal("expected fatal failure")
|
|
}
|
|
if scanner.inflight.Load() != 0 {
|
|
t.Fatal("scanner goroutines leaked")
|
|
}
|
|
meta, err := store.GetRunMeta(context.Background(), "failure")
|
|
if err != nil || meta.ProducerDone {
|
|
t.Fatalf("partial scan marked done: %+v err=%v", meta, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelScanStateStopsAndNoTarget(t *testing.T) {
|
|
for _, kind := range []string{"terminal", "producerDone", "blocked", "missing", "no-target", "no-cards"} {
|
|
t.Run(kind, func(t *testing.T) {
|
|
store := testStore(t)
|
|
scanner := scannerFor([]Card{{ID: 1, ChanKey: "a"}}, 2)
|
|
if kind == "no-target" {
|
|
scanner.channels[0].SyncEnabled = true
|
|
}
|
|
if kind == "no-cards" {
|
|
scanner.cards = nil
|
|
}
|
|
p := testProducer(t, scanner, store, Config{})
|
|
if kind != "no-target" && kind != "no-cards" {
|
|
if err := store.CreateRun(context.Background(), zset.RunMeta{RunID: "stop", Status: "producing"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := store.RunMetaKey("stop")
|
|
switch kind {
|
|
case "terminal":
|
|
_, _ = store.RedisClient().Do(context.Background(), "HSET", key, "status", "completed")
|
|
case "producerDone":
|
|
if err := store.MarkProducerDone(context.Background(), "stop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "blocked":
|
|
_, _ = store.BlockRunScoped(context.Background(), "stop", "a")
|
|
case "missing":
|
|
scanner.hook = func(ctx context.Context, _ string, _ int64) error {
|
|
_, err := store.RedisClient().Do(ctx, "DEL", key)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
err := p.Produce(contextForTest(t), scopedMessage("stop"))
|
|
if kind == "terminal" {
|
|
if !errors.Is(err, ErrRunFinished) {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
} else if kind == "missing" {
|
|
if !errors.Is(err, zset.ErrRunNotFound) {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(taskIDs(t, store, "stop")) != 0 {
|
|
t.Fatal("stopped/empty run enqueued tasks")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Omitted retry/scan-mode settings must still use the channel Run producer.
|
|
func TestDefaultProducerUsesChannelRun(t *testing.T) {
|
|
store := testStore(t)
|
|
scanner := scannerFor([]Card{{ID: 1, ChanKey: "a"}}, 1)
|
|
p, err := New(scanner, store, Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = p.Produce(contextForTest(t), scopedMessage("default-channel")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if scanner.queries.Load() == 0 || scanner.lists.Load() != 1 {
|
|
t.Fatal("default did not use channel scan")
|
|
}
|
|
requireMeta(t, store, "default-channel", true, 1)
|
|
}
|