chore: 初始开源发布

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),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
MiFaZhan
2026-10-22 16:37:56 +08:00
commit 2bec257289
361 changed files with 71524 additions and 0 deletions
+516
View File
@@ -0,0 +1,516 @@
package batch
import (
"context"
"encoding/json"
"errors"
"io"
"reflect"
"strings"
"testing"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
)
type executorStub struct {
calls int
batchNo string
err error
}
func (s *executorStub) Execute(_ context.Context, batchNo string) error {
s.calls++
s.batchNo = batchNo
return s.err
}
type processingExecutorStub struct {
executorStub
canCalls int
main *entity.BatchMain
can bool
canErr error
selfClaim bool
}
type blockingExecutorStub struct {
calls int
}
func (s *blockingExecutorStub) Execute(ctx context.Context, _ string) error {
s.calls++
<-ctx.Done()
return ctx.Err()
}
func (s *processingExecutorStub) CanExecuteProcessing(_ context.Context, main *entity.BatchMain) (bool, error) {
s.canCalls++
s.main = main
return s.can, s.canErr
}
func (s *processingExecutorStub) ClaimsProcessingReentry() bool {
return s.selfClaim
}
type repositoryStub struct {
mains []*entity.BatchMain
findErr error
markErr error
claimed bool
findCalls int
markCalls int
markBatchNo string
markType int
failErr error
failCalls int
failBatchNo string
failType int
failReason string
successErr error
successCalls int
successBatchNo string
successType int
successCount int
successFails int
successReason string
reclaimErr error
reclaimSet bool
reclaimed bool
reclaimCalls int
reclaimBatchNo string
reclaimType int
}
func (s *repositoryStub) FindMain(context.Context, string) (*entity.BatchMain, error) {
s.findCalls++
if s.findErr != nil {
return nil, s.findErr
}
if len(s.mains) == 0 {
return nil, nil
}
index := s.findCalls - 1
if index >= len(s.mains) {
index = len(s.mains) - 1
}
return s.mains[index], nil
}
func (s *repositoryStub) MarkProcessing(_ context.Context, batchNo string, batchType int) (bool, error) {
s.markCalls++
s.markBatchNo = batchNo
s.markType = batchType
return s.claimed, s.markErr
}
func (s *repositoryStub) MarkFailed(_ context.Context, batchNo string, batchType int, reason string) (bool, error) {
s.failCalls++
s.failBatchNo = batchNo
s.failType = batchType
s.failReason = reason
return s.failErr == nil, s.failErr
}
func (s *repositoryStub) MarkSuccess(_ context.Context, batchNo string, batchType, success, fail int, reason string) (bool, error) {
s.successCalls++
s.successBatchNo = batchNo
s.successType = batchType
s.successCount = success
s.successFails = fail
s.successReason = reason
return s.successErr == nil, s.successErr
}
func (s *repositoryStub) ReclaimProcessing(_ context.Context, batchNo string, batchType int, _ *gtime.Time) (bool, error) {
s.reclaimCalls++
s.reclaimBatchNo = batchNo
s.reclaimType = batchType
if !s.reclaimSet {
return s.reclaimErr == nil, s.reclaimErr
}
return s.reclaimed, s.reclaimErr
}
func TestMessageJSONContract(t *testing.T) {
body, err := json.Marshal(Message{BatchNo: "BATCH-1", BatchType: Type103})
if err != nil {
t.Fatalf("marshal message: %v", err)
}
const expected = `{"batchNo":"BATCH-1","batchType":103}`
if string(body) != expected {
t.Fatalf("message JSON = %s, want %s", body, expected)
}
}
func TestDispatcherSupportsLifecycleTypesAndExplicitRegistration(t *testing.T) {
executor := &executorStub{}
dispatcher := NewDispatcher(executor)
for _, batchType := range []int{Type103, Type104} {
actual, ok := dispatcher.Executor(batchType)
if !ok || actual != executor {
t.Fatalf("batch type %d was not registered with shared executor", batchType)
}
}
if _, ok := dispatcher.Executor(102); ok {
t.Fatal("unexpected executor for unsupported batch type")
}
inbound := &executorStub{}
dispatcher.Register(1, inbound)
actual, ok := dispatcher.Executor(1)
if !ok || actual != inbound {
t.Fatal("registered inbound executor was not returned")
}
dataExecutor := &executorStub{}
dispatcher.Register(Type107, dataExecutor)
dispatcher.Register(Type109, dataExecutor)
dispatcher.Register(Type110, dataExecutor)
for _, batchType := range []int{Type107, Type109, Type110} {
actual, ok = dispatcher.Executor(batchType)
if !ok || actual != dataExecutor {
t.Fatalf("batch type %d was not registered with the data service executor", batchType)
}
}
}
func TestRuntimeAcknowledgesInvalidAndUnclaimableMessages(t *testing.T) {
g.Log().SetWriter(io.Discard)
tests := []struct {
name string
message Message
mains []*entity.BatchMain
claimed bool
wantFind int
wantMarked int
}{
{name: "empty batch number", message: Message{BatchType: Type103}},
{name: "blank batch number", message: Message{BatchNo: " ", BatchType: Type103}},
{name: "unsupported type", message: Message{BatchNo: "B-1", BatchType: 102}},
{name: "not found", message: Message{BatchNo: "B-1", BatchType: Type103}, wantFind: 1},
{name: "type mismatch", message: Message{BatchNo: "B-1", BatchType: Type103}, mains: []*entity.BatchMain{{BatchNo: "B-1", BatchType: Type104, BatchAsyncStatus: AsyncStatusPending}}, wantFind: 1},
{name: "processing", message: Message{BatchNo: "B-1", BatchType: Type103}, mains: []*entity.BatchMain{{BatchNo: "B-1", BatchType: Type103, BatchAsyncStatus: AsyncStatusProcessing}}, wantFind: 1},
{name: "terminal", message: Message{BatchNo: "B-1", BatchType: Type103}, mains: []*entity.BatchMain{{BatchNo: "B-1", BatchType: Type103, BatchAsyncStatus: 3}}, wantFind: 1},
{name: "CAS lost", message: Message{BatchNo: "B-1", BatchType: Type103}, mains: []*entity.BatchMain{{BatchNo: "B-1", BatchType: Type103, BatchAsyncStatus: AsyncStatusPending}, {BatchNo: "B-1", BatchType: Type103, BatchAsyncStatus: AsyncStatusProcessing}}, wantFind: 2, wantMarked: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repository := &repositoryStub{mains: tt.mains, claimed: tt.claimed}
executor := &executorStub{}
runtime := NewRuntime(repository, NewDispatcher(executor))
if err := runtime.Handle(context.Background(), tt.message); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if repository.findCalls != tt.wantFind || repository.markCalls != tt.wantMarked {
t.Fatalf("repository calls: find=%d mark=%d, want find=%d mark=%d", repository.findCalls, repository.markCalls, tt.wantFind, tt.wantMarked)
}
if executor.calls != 0 {
t.Fatalf("executor calls = %d, want 0", executor.calls)
}
})
}
}
func TestRuntimeExecutesRegisteredDataServiceBatch(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-109", BatchType: Type109, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
}
dataExecutor := &executorStub{}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type109, dataExecutor)
dispatcher.Register(Type110, dataExecutor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-109", BatchType: Type109}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if dataExecutor.calls != 1 || dataExecutor.batchNo != "B-109" {
t.Fatalf("data executor was not invoked: %+v", dataExecutor)
}
}
func TestRuntimeExecutesTrafficCutoffConfigWithDedicatedExecutor(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-41", BatchType: Type41, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
}
dataExecutor := &executorStub{err: errors.New("data executor must not be called")}
trafficCutoffConfigExecutor := &executorStub{}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type107, dataExecutor)
dispatcher.Register(Type41, trafficCutoffConfigExecutor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-41", BatchType: Type41}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if trafficCutoffConfigExecutor.calls != 1 || trafficCutoffConfigExecutor.batchNo != "B-41" {
t.Fatalf("traffic cutoff config executor was not invoked: %+v", trafficCutoffConfigExecutor)
}
if dataExecutor.calls != 0 {
t.Fatalf("data executor calls = %d, want 0", dataExecutor.calls)
}
}
func TestRuntimeIgnoresFinalizedTrafficCutoffConfigDuplicateMessage(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-41-DONE", BatchType: Type41, BatchAsyncStatus: AsyncStatusSuccess}},
}
trafficCutoffConfigExecutor := &executorStub{err: errors.New("traffic cutoff config executor must not be called")}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type41, trafficCutoffConfigExecutor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-41-DONE", BatchType: Type41}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if trafficCutoffConfigExecutor.calls != 0 {
t.Fatalf("traffic cutoff config executor calls = %d, want 0", trafficCutoffConfigExecutor.calls)
}
}
func TestRuntimeMarksExecutorFailureAsFailed(t *testing.T) {
expectedErr := errors.New("execute failed")
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-104", BatchType: Type104, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
}
executor := &executorStub{err: expectedErr}
runtime := NewRuntime(repository, NewDispatcher(executor))
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-104", BatchType: Type104}); err != nil {
t.Fatalf("Handle() error = %v, want nil", err)
}
if repository.markCalls != 1 || repository.markBatchNo != "B-104" || repository.markType != Type104 {
t.Fatalf("unexpected claim call: %+v", repository)
}
if executor.calls != 1 || executor.batchNo != "B-104" {
t.Fatalf("unexpected executor call: %+v", executor)
}
if repository.failCalls != 1 || repository.failBatchNo != "B-104" || repository.failType != Type104 || repository.failReason != expectedErr.Error() {
t.Fatalf("unexpected failure mark call: %+v", repository)
}
}
func TestRuntimeReturnsRetryableExecutorFailureWithoutFinalizing(t *testing.T) {
sourceErr := errors.New("source file is temporarily unavailable")
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "INB-RETRY", BatchType: 1, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
}
executor := &executorStub{err: Retryable(sourceErr)}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(1, executor)
runtime := NewRuntime(repository, dispatcher)
err := runtime.Handle(context.Background(), Message{BatchNo: "INB-RETRY", BatchType: 1})
if !errors.Is(err, sourceErr) || !IsRetryable(err) {
t.Fatalf("Handle() error = %v, want retryable wrapper of %v", err, sourceErr)
}
if repository.failCalls != 0 {
t.Fatalf("MarkFailed calls = %d, want 0", repository.failCalls)
}
}
func TestRuntimeReclaimsRetryableProcessingBatch(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "INB-RETRY", BatchType: 1, BatchAsyncStatus: AsyncStatusProcessing}},
reclaimSet: true,
reclaimed: true,
}
executor := &executorStub{}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(1, executor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "INB-RETRY", BatchType: 1, Retryable: true}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if repository.reclaimCalls != 1 || repository.reclaimBatchNo != "INB-RETRY" || repository.reclaimType != 1 {
t.Fatalf("unexpected reclaim call: %+v", repository)
}
if executor.calls != 1 {
t.Fatalf("executor calls = %d, want 1", executor.calls)
}
}
func TestRuntimeMarksTimeoutAsFailed(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-TIMEOUT", BatchType: Type104, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
}
executor := &blockingExecutorStub{}
runtime := NewRuntime(
repository,
NewDispatcher(executor),
WithExecutionTimeout(time.Millisecond),
WithFinalizationTimeout(time.Second),
)
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-TIMEOUT", BatchType: Type104}); err != nil {
t.Fatalf("Handle() error = %v, want nil", err)
}
if executor.calls != 1 {
t.Fatalf("executor calls = %d, want 1", executor.calls)
}
if repository.failCalls != 1 || !strings.Contains(repository.failReason, "timed out after 1ms") {
t.Fatalf("unexpected timeout failure mark: %+v", repository)
}
}
func TestRuntimeReturnsFailureMarkErrors(t *testing.T) {
executeErr := errors.New("execute failed")
markErr := errors.New("mark failed")
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-104", BatchType: Type104, BatchAsyncStatus: AsyncStatusPending}},
claimed: true,
failErr: markErr,
}
runtime := NewRuntime(repository, NewDispatcher(&executorStub{err: executeErr}))
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-104", BatchType: Type104}); !errors.Is(err, markErr) {
t.Fatalf("Handle() error = %v, want %v", err, markErr)
}
}
func TestRuntimeCompensatesFailureMarkWithoutReexecutingBatch(t *testing.T) {
markErr := errors.New("mark failed")
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-RETRY", BatchType: Type104, BatchAsyncStatus: AsyncStatusProcessing}},
failErr: markErr,
}
executor := &executorStub{err: errors.New("must not execute again")}
runtime := NewRuntime(repository, NewDispatcher(executor))
err := runtime.Handle(context.Background(), Message{
BatchNo: "B-RETRY",
BatchType: Type104,
FailureReason: "original executor failure",
})
if !errors.Is(err, markErr) {
t.Fatalf("Handle() error = %v, want %v", err, markErr)
}
if executor.calls != 0 {
t.Fatalf("executor calls = %d, want 0 for failure finalization retry", executor.calls)
}
if repository.markCalls != 0 {
t.Fatalf("MarkProcessing calls = %d, want 0 for failure finalization retry", repository.markCalls)
}
if repository.failCalls != 1 || repository.failReason != "original executor failure" {
t.Fatalf("unexpected failure finalization call: %+v", repository)
}
}
func TestRuntimeAllowsSelfClaimingProcessingReEntryWithoutRefreshingStartTime(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "RST-1", BatchType: Type105, BatchAsyncStatus: AsyncStatusProcessing}},
}
executor := &processingExecutorStub{can: true, selfClaim: true}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type105, executor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "RST-1", BatchType: Type105}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if executor.canCalls != 1 || executor.main == nil || executor.main.BatchNo != "RST-1" {
t.Fatalf("processing check not called with main: %+v", executor)
}
if repository.markCalls != 0 {
t.Fatalf("MarkProcessing calls = %d, want 0", repository.markCalls)
}
if repository.reclaimCalls != 0 {
t.Fatalf("ReclaimProcessing calls = %d, want 0 for self-claiming executor", repository.reclaimCalls)
}
if executor.calls != 1 || executor.batchNo != "RST-1" {
t.Fatalf("executor calls = %+v", executor.executorStub)
}
}
func TestRuntimeMarksProcessingReEntryFailureAsFailed(t *testing.T) {
expectedErr := errors.New("resume failed")
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "RST-1", BatchType: Type105, BatchAsyncStatus: AsyncStatusProcessing}},
}
executor := &processingExecutorStub{can: true}
executor.err = expectedErr
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type105, executor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "RST-1", BatchType: Type105}); err != nil {
t.Fatalf("Handle() error = %v, want nil", err)
}
if repository.markCalls != 0 {
t.Fatalf("MarkProcessing calls = %d, want 0", repository.markCalls)
}
if repository.failCalls != 1 || repository.failBatchNo != "RST-1" || repository.failType != Type105 || repository.failReason != expectedErr.Error() {
t.Fatalf("unexpected failure mark call: %+v", repository)
}
}
func TestRuntimeIgnoresProcessingWhenOptInExecutorDeclines(t *testing.T) {
repository := &repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "RST-2", BatchType: Type105, BatchAsyncStatus: AsyncStatusProcessing}},
}
executor := &processingExecutorStub{can: false}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type105, executor)
runtime := NewRuntime(repository, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "RST-2", BatchType: Type105}); err != nil {
t.Fatalf("Handle() error = %v", err)
}
if executor.canCalls != 1 {
t.Fatalf("processing check calls = %d, want 1", executor.canCalls)
}
if executor.calls != 0 {
t.Fatalf("executor calls = %d, want 0", executor.calls)
}
}
func TestRuntimeReturnsDatabaseErrors(t *testing.T) {
findErr := errors.New("find failed")
runtime := NewRuntime(&repositoryStub{findErr: findErr}, NewDispatcher(&executorStub{}))
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-1", BatchType: Type103}); !errors.Is(err, findErr) {
t.Fatalf("find error = %v, want %v", err, findErr)
}
markErr := errors.New("mark failed")
runtime = NewRuntime(&repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "B-1", BatchType: Type103, BatchAsyncStatus: AsyncStatusPending}},
markErr: markErr,
}, NewDispatcher(&executorStub{}))
if err := runtime.Handle(context.Background(), Message{BatchNo: "B-1", BatchType: Type103}); !errors.Is(err, markErr) {
t.Fatalf("mark error = %v, want %v", err, markErr)
}
checkErr := errors.New("check failed")
processing := &processingExecutorStub{canErr: checkErr}
dispatcher := NewDispatcher(&executorStub{})
dispatcher.Register(Type105, processing)
runtime = NewRuntime(&repositoryStub{
mains: []*entity.BatchMain{{BatchNo: "RST-3", BatchType: Type105, BatchAsyncStatus: AsyncStatusProcessing}},
}, dispatcher)
if err := runtime.Handle(context.Background(), Message{BatchNo: "RST-3", BatchType: Type105}); !errors.Is(err, checkErr) {
t.Fatalf("processing check error = %v, want %v", err, checkErr)
}
}
func TestMessageHasOnlyContractFields(t *testing.T) {
typeOfMessage := reflect.TypeOf(Message{})
if typeOfMessage.NumField() != 4 {
t.Fatalf("Message has %d fields, want 4", typeOfMessage.NumField())
}
if field, ok := typeOfMessage.FieldByName("FailureReason"); !ok || field.Tag.Get("json") != "failureReason,omitempty" {
t.Fatalf("FailureReason JSON contract is missing or incorrect")
}
if field, ok := typeOfMessage.FieldByName("Retryable"); !ok || field.Tag.Get("json") != "retryable,omitempty" {
t.Fatalf("Retryable JSON contract is missing or incorrect")
}
}