Files
tasks/tools/tdbg/dlq_v2_service.go
Jacob BarzeeandGitHub 06171037f0 Import alignment (#6426)
## What changed?
Make import formatting a part of the build, and test validity with
"ensure-no-changes" target

## Why?
So we never discuss import order again (hopefully)

## How did you test it?
its a build change, so the build will test it!

## Potential risks
As long as build works, the only thing I risk is the ire of coworker who
want imports to be different ;)

## Documentation
N/A

## Is hotfix candidate?
No
2024-08-23 14:48:10 -06:00

432 lines
12 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package tdbg
import (
"bytes"
"encoding/json"
"fmt"
"io"
"slices"
"sort"
"strconv"
"strings"
"github.com/urfave/cli/v2"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/server/api/adminservice/v1"
commonspb "go.temporal.io/server/api/common/v1"
"go.temporal.io/server/common/collection"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/service/history/tasks"
"go.uber.org/multierr"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type (
// DLQV2Service implements DLQService for [persistence.QueueV2].
DLQV2Service struct {
category tasks.Category
sourceCluster string
targetCluster string
clientFactory ClientFactory
writer io.Writer
prompter *Prompter
taskBlobEncoder TaskBlobEncoder
}
// DLQMessage is used primarily to form the JSON output of the `read` command. It's only used for v2.
DLQMessage struct {
// MessageID is the ID of the message within the DLQ. You can use this ID as an input to the `--last_message_id`
// flag for the `purge` and `merge` commands.
MessageID int64 `json:"message_id"`
// ShardID is only used for non-namespace replication tasks.
ShardID int32 `json:"shard_id"`
// Payload contains the parsed task metadata from the server.
Payload *TaskPayload `json:"payload"`
}
// TaskPayload implements both [json.Marshaler] and [json.Unmarshaler]. This allows us to pretty-print tasks using
// jsonpb when serializing and then store the raw bytes of the task payload for later use when deserializing. We
// need to store the raw bytes instead of immediately decoding to a concrete type because that logic is dynamic and
// can't depend solely on the task category ID in case there are additional task categories in use.
TaskPayload struct {
taskBlobEncoder TaskBlobEncoder
taskCategoryID int
blob *commonpb.DataBlob
bytes []byte
}
)
var (
_ json.Marshaler = (*TaskPayload)(nil)
_ json.Unmarshaler = (*TaskPayload)(nil)
)
func (p *TaskPayload) MarshalJSON() ([]byte, error) {
var b bytes.Buffer
err := p.taskBlobEncoder.Encode(&b, p.taskCategoryID, p.blob)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
func (p *TaskPayload) UnmarshalJSON(b []byte) error {
p.bytes = b
return nil
}
// Bytes returns the raw bytes of the deserialized TaskPayload. This will return nil if the payload has not been
// deserialized yet.
func (p *TaskPayload) Bytes() []byte {
return p.bytes
}
const dlqV2DefaultMaxMessageCount = 100
func NewDLQV2Service(
category tasks.Category,
sourceCluster string,
targetCluster string,
clientFactory ClientFactory,
writer io.Writer,
prompter *Prompter,
taskBlobEncoder TaskBlobEncoder,
) *DLQV2Service {
return &DLQV2Service{
category: category,
sourceCluster: sourceCluster,
targetCluster: targetCluster,
clientFactory: clientFactory,
writer: writer,
prompter: prompter,
taskBlobEncoder: taskBlobEncoder,
}
}
type jsonEncoder struct {
writer io.Writer
protojson protojson.MarshalOptions
encoder *json.Encoder
}
func (j jsonEncoder) Encode(v any) error {
if pb, ok := v.(proto.Message); ok {
bs, err := j.protojson.Marshal(pb)
if err != nil {
return err
}
_, err = j.writer.Write(bs)
return err
}
return j.encoder.Encode(v)
}
func newEncoder(writer io.Writer) jsonEncoder {
enc := jsonEncoder{
writer: writer,
protojson: protojson.MarshalOptions{Indent: " "},
encoder: json.NewEncoder(writer),
}
enc.encoder.SetIndent("", " ")
return enc
}
func (ac *DLQV2Service) ReadMessages(c *cli.Context) (err error) {
ctx, cancel := newContext(c)
defer cancel()
remainingMessageCount := dlqV2DefaultMaxMessageCount
if c.IsSet(FlagMaxMessageCount) {
remainingMessageCount = c.Int(FlagMaxMessageCount)
if remainingMessageCount <= 0 {
return fmt.Errorf("--%s must be positive but was %d", FlagMaxMessageCount, remainingMessageCount)
}
}
maxMessageID, err := ac.getLastMessageID(c, "read")
if err != nil {
return err
}
outputFile, err := getOutputFile(c.String(FlagOutputFilename), ac.writer)
if err != nil {
return err
}
defer func() {
err = multierr.Append(err, outputFile.Close())
}()
adminClient := ac.clientFactory.AdminClient(c)
pageSize := c.Int(FlagPageSize)
iterator := collection.NewPagingIterator[*commonspb.HistoryDLQTask](
func(paginationToken []byte) ([]*commonspb.HistoryDLQTask, []byte, error) {
request := &adminservice.GetDLQTasksRequest{
DlqKey: &commonspb.HistoryDLQKey{
TaskCategory: int32(ac.category.ID()),
SourceCluster: ac.sourceCluster,
TargetCluster: ac.targetCluster,
},
PageSize: int32(pageSize),
NextPageToken: paginationToken,
}
res, err := adminClient.GetDLQTasks(ctx, request)
if err != nil {
return nil, nil, fmt.Errorf("call to GetDLQTasks from ReadMessages failed: %w", err)
}
return res.DlqTasks, res.NextPageToken, nil
},
)
for iterator.HasNext() && remainingMessageCount > 0 {
dlqTask, err := iterator.Next()
if err != nil {
return fmt.Errorf("DLQ task iterator returned error: %w", err)
}
if dlqTask.Metadata.MessageId > maxMessageID {
break
}
blob := dlqTask.Payload.Blob
if blob == nil {
return fmt.Errorf("DLQ task payload blob is nil: %+v", dlqTask)
}
payload := &TaskPayload{
taskBlobEncoder: ac.taskBlobEncoder,
blob: blob,
taskCategoryID: ac.category.ID(),
}
message := DLQMessage{
MessageID: dlqTask.Metadata.MessageId,
ShardID: dlqTask.Payload.ShardId,
Payload: payload,
}
err = newEncoder(outputFile).Encode(message)
if err != nil {
return err
}
remainingMessageCount--
_, err = outputFile.Write([]byte("\n"))
if err != nil {
return fmt.Errorf("fail to print dlq messages: %s", err)
}
}
return nil
}
func (ac *DLQV2Service) PurgeMessages(c *cli.Context) error {
adminClient := ac.clientFactory.AdminClient(c)
lastMessageID, err := ac.getLastMessageID(c, "purge")
if err != nil {
return err
}
ctx, cancel := newContext(c)
defer cancel()
response, err := adminClient.PurgeDLQTasks(ctx, &adminservice.PurgeDLQTasksRequest{
DlqKey: ac.getDLQKey(),
InclusiveMaxTaskMetadata: &commonspb.HistoryDLQTaskMetadata{
MessageId: lastMessageID,
},
})
if err != nil {
return fmt.Errorf("call to PurgeDLQTasks failed: %w", err)
}
err = newEncoder(ac.writer).Encode(response)
if err != nil {
return fmt.Errorf("unable to encode PurgeDLQTasks response: %w", err)
}
return nil
}
func (ac *DLQV2Service) MergeMessages(c *cli.Context) error {
adminClient := ac.clientFactory.AdminClient(c)
lastMessageID, err := ac.getLastMessageID(c, "merge")
if err != nil {
return err
}
ctx, cancel := newContext(c)
defer cancel()
response, err := adminClient.MergeDLQTasks(ctx, &adminservice.MergeDLQTasksRequest{
DlqKey: ac.getDLQKey(),
InclusiveMaxTaskMetadata: &commonspb.HistoryDLQTaskMetadata{
MessageId: lastMessageID,
},
BatchSize: int32(c.Int(FlagPageSize)), // let the server handle validation and defaulting of batch size.
})
if err != nil {
return fmt.Errorf("call to MergeDLQTasks failed: %w", err)
}
err = newEncoder(ac.writer).Encode(response)
if err != nil {
return fmt.Errorf("unable to encode MergeDLQTasks response: %w", err)
}
return nil
}
func (ac *DLQV2Service) getDLQKey() *commonspb.HistoryDLQKey {
return &commonspb.HistoryDLQKey{
TaskCategory: int32(ac.category.ID()),
SourceCluster: ac.sourceCluster,
TargetCluster: ac.targetCluster,
}
}
func (ac *DLQV2Service) getLastMessageID(c *cli.Context, action string) (int64, error) {
if !c.IsSet(FlagLastMessageID) {
msg := fmt.Sprintf(
"You did not set --%s. Are you sure you want to %s all messages without an upper bound?",
FlagLastMessageID,
action,
)
ac.prompter.Prompt(msg)
return persistence.MaxQueueMessageID, nil
}
lastMessageID := c.Int64(FlagLastMessageID)
if lastMessageID < persistence.FirstQueueMessageID {
return 0, fmt.Errorf(
"--%s must be at least %d but was %d",
FlagLastMessageID,
persistence.FirstQueueMessageID,
lastMessageID,
)
}
return lastMessageID, nil
}
func getSupportedDLQTaskCategories(taskCategoryRegistry tasks.TaskCategoryRegistry) []tasks.Category {
categories := make([]tasks.Category, 0, len(taskCategoryRegistry.GetCategories())-1)
for _, c := range taskCategoryRegistry.GetCategories() {
if c != tasks.CategoryMemoryTimer {
categories = append(categories, c)
}
}
slices.SortFunc(categories, func(a, b tasks.Category) int {
return a.ID() - b.ID()
})
return categories
}
func getCategoriesList(taskCategoryRegistry tasks.TaskCategoryRegistry) string {
var categoryString strings.Builder
categories := getSupportedDLQTaskCategories(taskCategoryRegistry)
for i, c := range categories {
if i == len(categories)-1 {
categoryString.WriteString(" and ")
} else if i > 0 {
categoryString.WriteString(", ")
}
_, _ = fmt.Fprintf(&categoryString, "%d (%s)", c.ID(), c.Name())
}
return categoryString.String()
}
func getCategoryByID(
c *cli.Context,
taskCategoryRegistry tasks.TaskCategoryRegistry,
categoryIDString string,
) (tasks.Category, bool, error) {
if c.Command.Name == "list" {
return tasks.Category{}, true, nil
}
if categoryIDString == "" {
return tasks.Category{}, false, fmt.Errorf("--%s is required", FlagDLQType)
}
id, err := strconv.Atoi(categoryIDString)
if err != nil {
return tasks.Category{}, false, fmt.Errorf(
"%w: unable to parse category ID as an integer: %s", err, categoryIDString,
)
}
for _, c := range getSupportedDLQTaskCategories(taskCategoryRegistry) {
if c.ID() == id {
return c, true, nil
}
}
return tasks.Category{}, false, nil
}
func (ac *DLQV2Service) ListQueues(c *cli.Context) (err error) {
ctx, cancel := newContext(c)
defer cancel()
if err != nil {
return err
}
outputFile, err := getOutputFile(c.String(FlagOutputFilename), ac.writer)
if err != nil {
return err
}
defer func() {
err = multierr.Append(err, outputFile.Close())
}()
adminClient := ac.clientFactory.AdminClient(c)
pageSize := c.Int(FlagPageSize)
iterator := collection.NewPagingIterator[*adminservice.ListQueuesResponse_QueueInfo](
func(paginationToken []byte) ([]*adminservice.ListQueuesResponse_QueueInfo, []byte, error) {
request := &adminservice.ListQueuesRequest{
QueueType: int32(persistence.QueueTypeHistoryDLQ),
PageSize: int32(pageSize),
NextPageToken: paginationToken,
}
res, err := adminClient.ListQueues(ctx, request)
if err != nil {
return nil, nil, fmt.Errorf("call to ListQueues failed: %w", err)
}
return res.Queues, res.NextPageToken, nil
},
)
var queues []adminservice.ListQueuesResponse_QueueInfo
for iterator.HasNext() {
queue, err := iterator.Next()
if err != nil {
return fmt.Errorf("ListQueues task iterator returned error: %w", err)
}
queues = append(queues, *queue)
}
// Sort the list of queues in decreasing order of MessageCount.
sort.Slice(queues, func(i, j int) bool {
return queues[i].MessageCount > queues[j].MessageCount
})
items := make([]interface{}, len(queues))
for i, queue := range queues {
items[i] = queue
}
printJson := c.Bool(FlagPrintJSON)
if printJson {
err = newEncoder(outputFile).Encode(items)
} else {
err = printTable(items, outputFile)
}
if err != nil {
return fmt.Errorf("failed to print dlq messages: %w", err)
}
return nil
}