mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Add last message id in the DLQ ListQueues response. Use this last message id in merge DLQ command if it is not provided by the user. ## Why? tdbg dlq merge command has an optional argument --last-message-id. If this is not provided, the system workflow created for this task will merge/purge messages until the DLQ is empty. This can create a loop that will not end. If the underlying issue that pushed the messages to DLQ is still around, the newly merged tasks will again get added to DLQ. The system workflow will proceed to merge this task again. This can go in an infinite loop. ## How did you test it? - [x] built - [ ] run locally and tested manually - [x] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s)
691 lines
21 KiB
Go
691 lines
21 KiB
Go
package tests
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pborman/uuid"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/urfave/cli/v2"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
sdkclient "go.temporal.io/sdk/client"
|
|
"go.temporal.io/sdk/workflow"
|
|
"go.temporal.io/server/api/adminservice/v1"
|
|
enumsspb "go.temporal.io/server/api/enums/v1"
|
|
persistencespb "go.temporal.io/server/api/persistence/v1"
|
|
"go.temporal.io/server/common/codec"
|
|
"go.temporal.io/server/common/debug"
|
|
"go.temporal.io/server/common/definition"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/persistence/serialization"
|
|
"go.temporal.io/server/common/primitives"
|
|
"go.temporal.io/server/common/sdk"
|
|
"go.temporal.io/server/service/history/queues"
|
|
"go.temporal.io/server/service/history/tasks"
|
|
"go.temporal.io/server/tests/testcore"
|
|
"go.temporal.io/server/tests/testutils"
|
|
"go.temporal.io/server/tools/tdbg"
|
|
"go.temporal.io/server/tools/tdbg/tdbgtest"
|
|
"go.uber.org/fx"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type (
|
|
DLQSuite struct {
|
|
testcore.FunctionalTestBase
|
|
|
|
dlq persistence.HistoryTaskQueueManager
|
|
dlqTasks chan tasks.Task
|
|
writer bytes.Buffer
|
|
sdkClientFactory sdk.ClientFactory
|
|
tdbgApp *cli.App
|
|
deleteBlockCh chan interface{}
|
|
|
|
failingWorkflowIDPrefix atomic.Pointer[string]
|
|
}
|
|
dlqTestCase struct {
|
|
name string
|
|
dlqTestParams
|
|
configure func(*dlqTestParams)
|
|
}
|
|
dlqTestParams struct {
|
|
maxMessageCount string
|
|
lastMessageID string
|
|
targetCluster string
|
|
expectedNumMessages int
|
|
}
|
|
testExecutorWrapper struct {
|
|
suite *DLQSuite
|
|
}
|
|
testExecutor struct {
|
|
base queues.Executor
|
|
suite *DLQSuite
|
|
}
|
|
testDLQWriter struct {
|
|
suite *DLQSuite
|
|
queues.QueueWriter
|
|
}
|
|
testTaskQueueManager struct {
|
|
suite *DLQSuite
|
|
persistence.HistoryTaskQueueManager
|
|
}
|
|
)
|
|
|
|
const (
|
|
dlqTestTimeout = 10 * time.Second * debug.TimeoutMultiplier
|
|
)
|
|
|
|
func TestDLQSuite(t *testing.T) {
|
|
t.Parallel()
|
|
suite.Run(t, new(DLQSuite))
|
|
}
|
|
|
|
func (s *DLQSuite) SetupSuite() {
|
|
s.dlqTasks = make(chan tasks.Task)
|
|
testPrefix := "dlq-test-terminal-wfts-"
|
|
s.failingWorkflowIDPrefix.Store(&testPrefix)
|
|
s.FunctionalTestBase.SetupSuiteWithCluster(
|
|
testcore.WithFxOptionsForService(primitives.HistoryService,
|
|
fx.Populate(&s.dlq),
|
|
fx.Provide(
|
|
func() queues.ExecutorWrapper {
|
|
return &testExecutorWrapper{
|
|
suite: s,
|
|
}
|
|
},
|
|
),
|
|
fx.Decorate(
|
|
func(writer queues.QueueWriter) queues.QueueWriter {
|
|
return &testDLQWriter{
|
|
QueueWriter: writer,
|
|
suite: s,
|
|
}
|
|
},
|
|
),
|
|
fx.Decorate(
|
|
func(m persistence.HistoryTaskQueueManager) persistence.HistoryTaskQueueManager {
|
|
return &testTaskQueueManager{
|
|
suite: s,
|
|
HistoryTaskQueueManager: m,
|
|
}
|
|
},
|
|
),
|
|
),
|
|
testcore.WithFxOptionsForService(primitives.FrontendService,
|
|
fx.Populate(&s.sdkClientFactory),
|
|
),
|
|
)
|
|
s.tdbgApp = tdbgtest.NewCliApp(
|
|
func(params *tdbg.Params) {
|
|
params.ClientFactory = tdbg.NewClientFactory(tdbg.WithFrontendAddress(s.FrontendGRPCAddress()))
|
|
params.Writer = &s.writer
|
|
},
|
|
)
|
|
}
|
|
|
|
func myWorkflow(workflow.Context) (string, error) {
|
|
return "hello", nil
|
|
}
|
|
|
|
func (s *DLQSuite) SetupTest() {
|
|
s.FunctionalTestBase.SetupTest()
|
|
|
|
s.Worker().RegisterWorkflow(myWorkflow)
|
|
|
|
s.deleteBlockCh = make(chan interface{})
|
|
close(s.deleteBlockCh)
|
|
}
|
|
|
|
func (s *DLQSuite) TestReadArtificialDLQTasks() {
|
|
ctx := context.Background()
|
|
|
|
namespaceID := "test-namespace"
|
|
workflowID := "test-workflow-id"
|
|
workflowKey := definition.NewWorkflowKey(namespaceID, workflowID, "test-run-id")
|
|
|
|
category := tasks.CategoryTransfer
|
|
sourceCluster := "test-source-cluster-" + s.T().Name()
|
|
|
|
// Note: it's ok that this isn't unique across tests because the queue name will still be unique due to the source
|
|
// cluster name being included in the queue name. We use the current cluster name because that's what the default
|
|
// is if the target cluster flag isn't specified.
|
|
targetCluster := "active"
|
|
queueKey := persistence.QueueKey{
|
|
QueueType: persistence.QueueTypeHistoryDLQ,
|
|
Category: category,
|
|
SourceCluster: sourceCluster,
|
|
TargetCluster: targetCluster,
|
|
}
|
|
_, err := s.dlq.CreateQueue(ctx, &persistence.CreateQueueRequest{
|
|
QueueKey: queueKey,
|
|
})
|
|
s.NoError(err)
|
|
for i := 0; i < 4; i++ {
|
|
task := &tasks.WorkflowTask{
|
|
WorkflowKey: workflowKey,
|
|
TaskID: int64(42 + i),
|
|
}
|
|
_, err := s.dlq.EnqueueTask(ctx, &persistence.EnqueueTaskRequest{
|
|
QueueType: queueKey.QueueType,
|
|
SourceCluster: queueKey.SourceCluster,
|
|
TargetCluster: queueKey.TargetCluster,
|
|
Task: task,
|
|
SourceShardID: tasks.GetShardIDForTask(task, int(s.GetTestClusterConfig().HistoryConfig.NumHistoryShards)),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
file, err := os.CreateTemp("", "*")
|
|
s.NoError(err)
|
|
s.T().Cleanup(func() {
|
|
s.NoError(os.Remove(file.Name()))
|
|
})
|
|
for _, tc := range []dlqTestCase{
|
|
{
|
|
name: "max message count exceeded",
|
|
configure: func(params *dlqTestParams) {
|
|
params.maxMessageCount = "2"
|
|
params.lastMessageID = "999"
|
|
params.expectedNumMessages = 2
|
|
},
|
|
},
|
|
{
|
|
name: "last message ID exceeded",
|
|
configure: func(params *dlqTestParams) {
|
|
params.maxMessageCount = "999"
|
|
params.lastMessageID = "2" // first message is 0, so this should return 3 messages: 0, 1, 2
|
|
params.expectedNumMessages = 3
|
|
},
|
|
},
|
|
{
|
|
name: "target cluster specified",
|
|
configure: func(params *dlqTestParams) {
|
|
},
|
|
},
|
|
{
|
|
name: "target cluster not specified",
|
|
configure: func(params *dlqTestParams) {
|
|
params.targetCluster = ""
|
|
},
|
|
},
|
|
} {
|
|
s.Run(tc.name, func() {
|
|
tc.maxMessageCount = "999"
|
|
tc.lastMessageID = "999"
|
|
tc.expectedNumMessages = 4
|
|
tc.targetCluster = targetCluster
|
|
tc.configure(&tc.dlqTestParams)
|
|
args := []string{
|
|
"tdbg",
|
|
"--" + tdbg.FlagYes,
|
|
"dlq",
|
|
"read",
|
|
"--" + tdbg.FlagDLQType, strconv.Itoa(tasks.CategoryTransfer.ID()),
|
|
"--" + tdbg.FlagCluster, sourceCluster,
|
|
"--" + tdbg.FlagPageSize, "1",
|
|
"--" + tdbg.FlagMaxMessageCount, tc.maxMessageCount,
|
|
"--" + tdbg.FlagLastMessageID, tc.lastMessageID,
|
|
"--" + tdbg.FlagOutputFilename, file.Name(),
|
|
}
|
|
if tc.targetCluster != "" {
|
|
args = append(args, "--"+tdbg.FlagTargetCluster, tc.targetCluster)
|
|
}
|
|
cmdString := strings.Join(args, " ")
|
|
s.T().Log("TDBG command:", cmdString)
|
|
err = s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
|
|
s.T().Log("TDBG output:")
|
|
s.T().Log("========================================")
|
|
output, err := io.ReadAll(file)
|
|
s.NoError(err)
|
|
s.T().Log(string(output))
|
|
_, err = file.Seek(0, io.SeekStart)
|
|
s.NoError(err)
|
|
s.T().Log("========================================")
|
|
s.verifyNumTasks(file, tc.expectedNumMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
// This test executes an actual workflow for which we've set up an executor wrapper to return a terminal error. This
|
|
// causes the workflow task to be added to the DLQ. This tests the end-to-end functionality of the DLQ, whereas the
|
|
// above test is more for testing specific CLI flags when reading from the DLQ. After the workflow task is added to the
|
|
// DLQ, this test then purges the DLQ and verifies that the task was deleted.
|
|
// This test will then call DescribeDLQJob and CancelDLQJob api to verify.
|
|
func (s *DLQSuite) TestPurgeRealWorkflow() {
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, dlqTestTimeout)
|
|
defer cancel()
|
|
|
|
_, dlqMessageID := s.executeDoomedWorkflow(ctx)
|
|
|
|
// Delete the workflow task from the DLQ.
|
|
token := s.purgeMessages(ctx, dlqMessageID)
|
|
|
|
// Verify that the workflow task is no longer in the DLQ.
|
|
dlqTasks := s.readDLQTasks(ctx)
|
|
s.Empty(dlqTasks, "expected DLQ to be empty after purge")
|
|
|
|
// Run DescribeJob and validate
|
|
response := s.describeJob(ctx, token)
|
|
s.Equal(enumsspb.DLQ_OPERATION_TYPE_PURGE, response.OperationType)
|
|
s.Equal(enumsspb.DLQ_OPERATION_STATE_COMPLETED, response.OperationState)
|
|
s.Equal(dlqMessageID, response.MaxMessageId)
|
|
s.Equal(dlqMessageID, response.LastProcessedMessageId)
|
|
s.Equal(int64(1), response.MessagesProcessed)
|
|
|
|
// Try to cancel completed workflow
|
|
cancelResponse := s.cancelJob(ctx, token)
|
|
s.Equal(false, cancelResponse.Canceled)
|
|
}
|
|
|
|
// This test executes actual workflows for which we've set up an executor wrapper to return a terminal error. This
|
|
// causes the workflow tasks to be added to the DLQ. This tests the end-to-end functionality of the DLQ, whereas the
|
|
// above test is more for testing specific CLI flags when reading from the DLQ.
|
|
// This test will then call DescribeDLQJob and CancelDLQJob api to verify.
|
|
func (s *DLQSuite) TestMergeRealWorkflow() {
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, dlqTestTimeout)
|
|
defer cancel()
|
|
|
|
// Verify that we can execute a normal workflow.
|
|
run := s.executeWorkflow(ctx, "dlq-test-ok-workflow-id")
|
|
s.validateWorkflowRun(ctx, run)
|
|
|
|
// Execute several doomed workflows.
|
|
numWorkflows := 3
|
|
var dlqMessageID int64
|
|
var runs []sdkclient.WorkflowRun
|
|
for i := 0; i < numWorkflows; i++ {
|
|
run, dlqMessageID = s.executeDoomedWorkflow(ctx)
|
|
runs = append(runs, run)
|
|
}
|
|
|
|
// Re-enqueue the workflow tasks from the DLQ, but don't fail its WFTs this time.
|
|
nonExistantID := "some-workflow-id-that-wont-exist"
|
|
s.failingWorkflowIDPrefix.Store(&nonExistantID)
|
|
token := s.mergeMessages(ctx, dlqMessageID)
|
|
|
|
// Verify that the workflow task was deleted from the DLQ after merging.
|
|
dlqTasks := s.readDLQTasks(ctx)
|
|
s.Empty(dlqTasks)
|
|
|
|
// Verify that the workflows now eventually complete successfully.
|
|
for i := 0; i < numWorkflows; i++ {
|
|
s.validateWorkflowRun(ctx, runs[i])
|
|
}
|
|
|
|
// Run DescribeJob and validate
|
|
response := s.describeJob(ctx, token)
|
|
s.Equal(enumsspb.DLQ_OPERATION_TYPE_MERGE, response.OperationType)
|
|
s.Equal(enumsspb.DLQ_OPERATION_STATE_COMPLETED, response.OperationState)
|
|
s.Equal(dlqMessageID, response.MaxMessageId)
|
|
s.Equal(dlqMessageID, response.LastProcessedMessageId)
|
|
s.Equal(int64(numWorkflows), response.MessagesProcessed)
|
|
|
|
// Try to cancel completed workflow
|
|
cancelResponse := s.cancelJob(ctx, token)
|
|
s.Equal(false, cancelResponse.Canceled)
|
|
}
|
|
|
|
func (s *DLQSuite) TestCancelRunningMerge() {
|
|
s.deleteBlockCh = make(chan interface{})
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, dlqTestTimeout)
|
|
defer cancel()
|
|
|
|
// Execute several doomed workflows.
|
|
_, dlqMessageID := s.executeDoomedWorkflow(ctx)
|
|
|
|
token := s.mergeMessagesWithoutBlocking(ctx, dlqMessageID)
|
|
|
|
// Try to cancel running workflow
|
|
cancelResponse := s.cancelJob(ctx, token)
|
|
s.Equal(true, cancelResponse.Canceled)
|
|
// Unblock waiting tests on Delete
|
|
close(s.deleteBlockCh)
|
|
// Delete the workflow task from the DLQ.
|
|
s.purgeMessages(ctx, dlqMessageID)
|
|
}
|
|
|
|
func (s *DLQSuite) TestListQueues() {
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, dlqTestTimeout)
|
|
defer cancel()
|
|
targetCluster := "active"
|
|
category := tasks.CategoryTransfer
|
|
sourceCluster := "test-source-cluster-" + s.T().Name()
|
|
|
|
queueKey1 := persistence.QueueKey{
|
|
QueueType: persistence.QueueTypeHistoryDLQ,
|
|
Category: category,
|
|
SourceCluster: sourceCluster + "_1",
|
|
TargetCluster: targetCluster,
|
|
}
|
|
_, err := s.dlq.CreateQueue(ctx, &persistence.CreateQueueRequest{
|
|
QueueKey: queueKey1,
|
|
})
|
|
s.NoError(err)
|
|
|
|
queueKey2 := persistence.QueueKey{
|
|
QueueType: persistence.QueueTypeHistoryDLQ,
|
|
Category: category,
|
|
SourceCluster: sourceCluster + "_2",
|
|
TargetCluster: targetCluster,
|
|
}
|
|
_, err = s.dlq.CreateQueue(ctx, &persistence.CreateQueueRequest{
|
|
QueueKey: queueKey2,
|
|
})
|
|
s.NoError(err)
|
|
|
|
// Insert a message to second queue
|
|
_, err = s.dlq.EnqueueTask(ctx, &persistence.EnqueueTaskRequest{
|
|
QueueType: persistence.QueueTypeHistoryDLQ,
|
|
SourceCluster: sourceCluster + "_2",
|
|
TargetCluster: targetCluster,
|
|
Task: &tasks.WorkflowTask{},
|
|
SourceShardID: 1,
|
|
})
|
|
s.NoError(err)
|
|
|
|
queueInfos := s.listQueues(ctx)
|
|
qi0 := adminservice.ListQueuesResponse_QueueInfo{
|
|
QueueName: queueKey1.GetQueueName(),
|
|
MessageCount: 0,
|
|
LastMessageId: -1,
|
|
}
|
|
qi1 := adminservice.ListQueuesResponse_QueueInfo{
|
|
QueueName: queueKey2.GetQueueName(),
|
|
MessageCount: 1,
|
|
LastMessageId: 0,
|
|
}
|
|
var found0, found1 bool
|
|
for _, qi := range queueInfos {
|
|
found0 = found0 || proto.Equal(qi, &qi0)
|
|
found1 = found1 || proto.Equal(qi, &qi1)
|
|
|
|
}
|
|
s.True(found0, "unable to find %v in %v", &qi0, queueInfos)
|
|
s.True(found1, "unable to find %v in %v", &qi1, queueInfos)
|
|
}
|
|
|
|
func (s *DLQSuite) validateWorkflowRun(ctx context.Context, run sdkclient.WorkflowRun) {
|
|
var result string
|
|
err := run.Get(ctx, &result)
|
|
s.NoError(err)
|
|
s.Equal("hello", result)
|
|
}
|
|
|
|
// executeDoomedWorkflow runs a workflow that is guaranteed to produce a workflow task that will be added to the DLQ. It
|
|
// then returns the sdk workflow run and the message ID of the DLQ message for the failed workflow task.
|
|
func (s *DLQSuite) executeDoomedWorkflow(ctx context.Context) (sdkclient.WorkflowRun, int64) {
|
|
// Execute a workflow.
|
|
// Use a random workflow ID to ensure that we don't have any collisions with other runs.
|
|
run := s.executeWorkflow(ctx, *s.failingWorkflowIDPrefix.Load()+uuid.New())
|
|
|
|
// Wait for the workflow task to be added to the DLQ.
|
|
select {
|
|
case <-ctx.Done():
|
|
s.FailNow("timed out waiting for workflow to task to be DLQ'd")
|
|
case task := <-s.dlqTasks:
|
|
s.Equal(run.GetRunID(), task.GetRunID())
|
|
}
|
|
|
|
// Verify that the workflow task is in the DLQ.
|
|
task := s.verifyRunIsInDLQ(ctx, run)
|
|
dlqMessageID := task.MessageID
|
|
return run, dlqMessageID
|
|
}
|
|
|
|
func (s *DLQSuite) verifyRunIsInDLQ(
|
|
ctx context.Context,
|
|
run sdkclient.WorkflowRun,
|
|
) tdbgtest.DLQMessage[*persistencespb.TransferTaskInfo] {
|
|
dlqTasks := s.readDLQTasks(ctx)
|
|
for _, task := range dlqTasks {
|
|
if task.Payload.RunId == run.GetRunID() {
|
|
return task
|
|
}
|
|
}
|
|
s.Fail("workflow task not found in DLQ", run.GetRunID())
|
|
panic("unreachable")
|
|
}
|
|
|
|
// executeWorkflow just executes a simple no-op workflow that returns "hello" and returns the sdk workflow run.
|
|
func (s *DLQSuite) executeWorkflow(ctx context.Context, workflowID string) sdkclient.WorkflowRun {
|
|
run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
|
|
ID: workflowID,
|
|
TaskQueue: s.TaskQueue(),
|
|
}, myWorkflow)
|
|
s.NoError(err)
|
|
return run
|
|
}
|
|
|
|
// purgeMessages from the DLQ up to and including the specified message ID, blocking until the purge workflow completes.
|
|
func (s *DLQSuite) purgeMessages(ctx context.Context, maxMessageIDToDelete int64) string {
|
|
args := []string{
|
|
"tdbg",
|
|
"--" + tdbg.FlagYes,
|
|
"dlq",
|
|
"purge",
|
|
"--" + tdbg.FlagDLQType, strconv.Itoa(tasks.CategoryTransfer.ID()),
|
|
"--" + tdbg.FlagLastMessageID, strconv.FormatInt(maxMessageIDToDelete, 10),
|
|
}
|
|
err := s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
output := s.writer.Bytes()
|
|
s.writer.Truncate(0)
|
|
var data map[string]string
|
|
err = json.Unmarshal(output, &data)
|
|
s.NoError(err)
|
|
tokenString := data["jobToken"]
|
|
|
|
var response adminservice.PurgeDLQTasksResponse
|
|
s.NoError(protojson.Unmarshal(output, &response))
|
|
var token adminservice.DLQJobToken
|
|
s.NoError(proto.Unmarshal(response.GetJobToken(), &token))
|
|
|
|
systemSDKClient := s.sdkClientFactory.GetSystemClient()
|
|
run := systemSDKClient.GetWorkflow(ctx, token.WorkflowId, token.RunId)
|
|
s.NoError(run.Get(ctx, nil))
|
|
return tokenString
|
|
}
|
|
|
|
// mergeMessages from the DLQ up to and including the specified message ID, blocking until the merge workflow completes.
|
|
func (s *DLQSuite) mergeMessages(ctx context.Context, maxMessageID int64) string {
|
|
tokenString := s.mergeMessagesWithoutBlocking(ctx, maxMessageID)
|
|
tokenBytes, err := base64.StdEncoding.DecodeString(tokenString)
|
|
s.NoError(err)
|
|
var token adminservice.DLQJobToken
|
|
s.NoError(token.Unmarshal(tokenBytes))
|
|
systemSDKClient := s.sdkClientFactory.GetSystemClient()
|
|
run := systemSDKClient.GetWorkflow(ctx, token.WorkflowId, token.RunId)
|
|
s.NoError(run.Get(ctx, nil))
|
|
return tokenString
|
|
}
|
|
|
|
// mergeMessages from the DLQ up to and including the specified message ID, returns immediately after running tdbg command.
|
|
func (s *DLQSuite) mergeMessagesWithoutBlocking(ctx context.Context, maxMessageID int64) string {
|
|
args := []string{
|
|
"tdbg",
|
|
"--" + tdbg.FlagYes,
|
|
"dlq",
|
|
"merge",
|
|
"--" + tdbg.FlagDLQType, strconv.Itoa(tasks.CategoryTransfer.ID()),
|
|
"--" + tdbg.FlagLastMessageID, strconv.FormatInt(maxMessageID, 10),
|
|
"--" + tdbg.FlagPageSize, "1", // to ensure that we test pagination
|
|
}
|
|
err := s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
output := s.writer.Bytes()
|
|
s.writer.Truncate(0)
|
|
var data map[string]string
|
|
err = json.Unmarshal(output, &data)
|
|
s.NoError(err)
|
|
tokenString := data["jobToken"]
|
|
var response adminservice.MergeDLQTasksResponse
|
|
s.NoError(protojson.Unmarshal(output, &response))
|
|
return tokenString
|
|
}
|
|
|
|
// readDLQTasks from the transfer task DLQ for this cluster and return them.
|
|
func (s *DLQSuite) readDLQTasks(ctx context.Context) []tdbgtest.DLQMessage[*persistencespb.TransferTaskInfo] {
|
|
file := testutils.CreateTemp(s.T(), "", "*")
|
|
args := []string{
|
|
"tdbg",
|
|
"--" + tdbg.FlagYes,
|
|
"dlq",
|
|
"read",
|
|
"--" + tdbg.FlagDLQType, strconv.Itoa(tasks.CategoryTransfer.ID()),
|
|
"--" + tdbg.FlagOutputFilename, file.Name(),
|
|
}
|
|
s.NoError(s.tdbgApp.RunContext(ctx, args))
|
|
dlqTasks := s.readTransferTasks(file)
|
|
return dlqTasks
|
|
}
|
|
|
|
// Calls describe dlq job and verify the output
|
|
func (s *DLQSuite) describeJob(ctx context.Context, token string) *adminservice.DescribeDLQJobResponse {
|
|
args := []string{
|
|
"tdbg",
|
|
"dlq",
|
|
"job",
|
|
"describe",
|
|
"--" + tdbg.FlagJobToken, token,
|
|
}
|
|
err := s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
output := s.writer.Bytes()
|
|
s.T().Log(string(output))
|
|
s.writer.Truncate(0)
|
|
var response adminservice.DescribeDLQJobResponse
|
|
s.NoError(protojson.Unmarshal(output, &response))
|
|
return &response
|
|
}
|
|
|
|
// Calls delete dlq job and verify the output
|
|
func (s *DLQSuite) cancelJob(ctx context.Context, token string) *adminservice.CancelDLQJobResponse {
|
|
args := []string{
|
|
"tdbg",
|
|
"dlq",
|
|
"job",
|
|
"cancel",
|
|
"--" + tdbg.FlagJobToken, token,
|
|
"--" + tdbg.FlagReason, "testing cancel",
|
|
}
|
|
err := s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
output := s.writer.Bytes()
|
|
s.T().Log(string(output))
|
|
s.writer.Truncate(0)
|
|
var response adminservice.CancelDLQJobResponse
|
|
s.NoError(protojson.Unmarshal(output, &response))
|
|
return &response
|
|
}
|
|
|
|
// List all queues
|
|
func (s *DLQSuite) listQueues(ctx context.Context) []*adminservice.ListQueuesResponse_QueueInfo {
|
|
args := []string{
|
|
"tdbg",
|
|
"dlq",
|
|
"list",
|
|
"--" + tdbg.FlagPrintJSON,
|
|
}
|
|
|
|
err := s.tdbgApp.RunContext(ctx, args)
|
|
s.NoError(err)
|
|
b := s.writer.Bytes()
|
|
s.writer.Truncate(0)
|
|
var arr []*adminservice.ListQueuesResponse_QueueInfo
|
|
jsonpb := codec.NewJSONPBEncoder()
|
|
err = jsonpb.DecodeSlice(b, func() proto.Message {
|
|
resp := &adminservice.ListQueuesResponse_QueueInfo{}
|
|
arr = append(arr, resp)
|
|
return resp
|
|
})
|
|
s.NoError(err)
|
|
return arr
|
|
}
|
|
|
|
// verifyNumTasks verifies that the specified file contains the expected number of DLQ tasks, and that each task has the
|
|
// expected metadata and payload.
|
|
func (s *DLQSuite) verifyNumTasks(file *os.File, expectedNumTasks int) {
|
|
dlqTasks := s.readTransferTasks(file)
|
|
s.Len(dlqTasks, expectedNumTasks)
|
|
|
|
for i, task := range dlqTasks {
|
|
s.Equal(int64(persistence.FirstQueueMessageID+i), task.MessageID)
|
|
taskInfo := task.Payload
|
|
s.Equal(enumsspb.TASK_TYPE_TRANSFER_WORKFLOW_TASK, taskInfo.TaskType)
|
|
s.Equal("test-namespace", taskInfo.NamespaceId)
|
|
s.Equal("test-workflow-id", taskInfo.WorkflowId)
|
|
s.Equal("test-run-id", taskInfo.RunId)
|
|
s.Equal(int64(42+i), taskInfo.TaskId)
|
|
}
|
|
}
|
|
|
|
func (s *DLQSuite) readTransferTasks(file *os.File) []tdbgtest.DLQMessage[*persistencespb.TransferTaskInfo] {
|
|
dlqTasks, err := tdbgtest.ParseDLQMessages(file, func() *persistencespb.TransferTaskInfo {
|
|
return new(persistencespb.TransferTaskInfo)
|
|
})
|
|
s.NoError(err)
|
|
return dlqTasks
|
|
}
|
|
|
|
// EnqueueTask is used to intercept writes to the DLQ, so that we can unblock the test upon completion.
|
|
func (t *testDLQWriter) EnqueueTask(
|
|
ctx context.Context,
|
|
request *persistence.EnqueueTaskRequest,
|
|
) (*persistence.EnqueueTaskResponse, error) {
|
|
res, err := t.QueueWriter.EnqueueTask(ctx, request)
|
|
select {
|
|
case t.suite.dlqTasks <- request.Task:
|
|
case <-ctx.Done():
|
|
return res, fmt.Errorf("interrupted while trying to observe DLQ write: %w", ctx.Err())
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// Wrap is used to wrap the executor with our own faulty one.
|
|
func (t testExecutorWrapper) Wrap(delegate queues.Executor) queues.Executor {
|
|
return &testExecutor{
|
|
base: delegate,
|
|
suite: t.suite,
|
|
}
|
|
}
|
|
|
|
// Execute is used to wrap the executor so that we can intercept the workflow task and ensure it fails with a terminal
|
|
// error.
|
|
//
|
|
//nolint:err113
|
|
func (t testExecutor) Execute(ctx context.Context, e queues.Executable) queues.ExecuteResponse {
|
|
if strings.HasPrefix(e.GetWorkflowID(), *t.suite.failingWorkflowIDPrefix.Load()) && e.GetCategory() == tasks.CategoryTransfer {
|
|
// Return a terminal error that will cause this task to be added to the DLQ.
|
|
return queues.ExecuteResponse{
|
|
ExecutionErr: serialization.NewDeserializationError(enumspb.ENCODING_TYPE_PROTO3, errors.New("test error")),
|
|
}
|
|
}
|
|
return t.base.Execute(ctx, e)
|
|
}
|
|
|
|
// ReadTasks is used to block the dlq job workflow until one of them is cancelled in TestCancelRunningMerge.
|
|
func (m *testTaskQueueManager) DeleteTasks(
|
|
ctx context.Context,
|
|
request *persistence.DeleteTasksRequest,
|
|
) (*persistence.DeleteTasksResponse, error) {
|
|
<-m.suite.deleteBlockCh
|
|
return m.HistoryTaskQueueManager.DeleteTasks(ctx, request)
|
|
}
|