Files
tasks/service/frontend/admin_handler.go
Alan WuandGitHub cf7c62c8cc Add batch workflow refresh tasks (#8793)
## What changed?
Add batch workflow refresh tasks Admin API.

Add tdbg commands to invoke `StartAdminBatchOperation`.

## Details

AdminBatchOperations are executed in the Batched System Worker, similar
to existing Frontend BatchOperations. The worker activity will fetch
pages of workflow executions to perform operations on, and periodically
heartbeat its results. For RefreshWorkflowTasks, each individual Refresh
call will go through the admin handler.

Refreshing Workflow Tasks regenerates all pending tasks of an execution
given its mutable state.

## CLI usage

```
tdbg workflow batch-refresh-tasks \
  --namespace <ns> \
  --query "WorkflowType='MyType'" \
  --reason "fixing stuck workflows" \
  [--job-id <optional-job-id>] \
  [--archetype <optional-archetype>]
```

## Why?
Unblock new Matcher migration and allow for general use case batch Admin
calls. Currently, only supports `BatchOperationRefreshWorkflowTasks`.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)
2026-01-06 19:21:57 -05:00

2402 lines
79 KiB
Go

package frontend
import (
"context"
"errors"
"fmt"
"io"
"maps"
"math"
"net"
"strings"
"sync/atomic"
"time"
"github.com/google/uuid"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
namespacepb "go.temporal.io/api/namespace/v1"
replicationpb "go.temporal.io/api/replication/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
workflowpb "go.temporal.io/api/workflow/v1"
"go.temporal.io/api/workflowservice/v1"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/server/api/adminservice/v1"
batchspb "go.temporal.io/server/api/batch/v1"
clusterspb "go.temporal.io/server/api/cluster/v1"
commonspb "go.temporal.io/server/api/common/v1"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/api/matchingservice/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
replicationspb "go.temporal.io/server/api/replication/v1"
"go.temporal.io/server/chasm"
serverClient "go.temporal.io/server/client"
"go.temporal.io/server/client/admin"
"go.temporal.io/server/client/frontend"
"go.temporal.io/server/client/history"
"go.temporal.io/server/common"
"go.temporal.io/server/common/channel"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/config"
"go.temporal.io/server/common/convert"
"go.temporal.io/server/common/headers"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/membership"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/namespace/nsreplication"
"go.temporal.io/server/common/payload"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/common/persistence/visibility"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/common/persistence/visibility/store/elasticsearch"
"go.temporal.io/server/common/primitives"
"go.temporal.io/server/common/sdk"
"go.temporal.io/server/common/searchattribute"
"go.temporal.io/server/common/searchattribute/sadefs"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/util"
"go.temporal.io/server/service/history/tasks"
"go.temporal.io/server/service/worker/addsearchattributes"
"go.temporal.io/server/service/worker/batcher"
"go.temporal.io/server/service/worker/dlq"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
getNamespaceReplicationMessageBatchSize = 100
defaultLastMessageID = -1
listClustersPageSize = 100
)
type (
// AdminHandler - gRPC handler interface for adminservice
AdminHandler struct {
adminservice.UnimplementedAdminServiceServer
status int32
logger log.Logger
numberOfHistoryShards int32
config *Config
namespaceDLQHandler nsreplication.DLQMessageHandler
eventSerializer serialization.Serializer
visibilityMgr manager.VisibilityManager
persistenceExecutionName string
namespaceReplicationQueue persistence.NamespaceReplicationQueue
taskManager persistence.TaskManager
fairTaskManager persistence.FairTaskManager
clusterMetadataManager persistence.ClusterMetadataManager
persistenceMetadataManager persistence.MetadataManager
clientFactory serverClient.Factory
clientBean serverClient.Bean
historyClient historyservice.HistoryServiceClient
sdkClientFactory sdk.ClientFactory
membershipMonitor membership.Monitor
hostInfoProvider membership.HostInfoProvider
metricsHandler metrics.Handler
namespaceRegistry namespace.Registry
saProvider searchattribute.Provider
saManager searchattribute.Manager
saMapperProvider searchattribute.MapperProvider
saValidator *searchattribute.Validator
clusterMetadata cluster.Metadata
healthServer *health.Server
historyHealthChecker HealthChecker
chasmRegistry *chasm.Registry
// DEPRECATED: only history service on server side is supposed to
// use the following components.
taskCategoryRegistry tasks.TaskCategoryRegistry
matchingClient matchingservice.MatchingServiceClient
}
NewAdminHandlerArgs struct {
PersistenceConfig *config.Persistence
Config *Config
NamespaceReplicationQueue persistence.NamespaceReplicationQueue
ReplicatorNamespaceReplicationQueue persistence.NamespaceReplicationQueue
visibilityMgr manager.VisibilityManager
Logger log.Logger
TaskManager persistence.TaskManager
FairTaskManager persistence.FairTaskManager
PersistenceExecutionManager persistence.ExecutionManager
ClusterMetadataManager persistence.ClusterMetadataManager
PersistenceMetadataManager persistence.MetadataManager
ClientFactory serverClient.Factory
ClientBean serverClient.Bean
HistoryClient historyservice.HistoryServiceClient
sdkClientFactory sdk.ClientFactory
MembershipMonitor membership.Monitor
HostInfoProvider membership.HostInfoProvider
MetricsHandler metrics.Handler
NamespaceRegistry namespace.Registry
SaProvider searchattribute.Provider
SaManager searchattribute.Manager
SaMapperProvider searchattribute.MapperProvider
ClusterMetadata cluster.Metadata
HealthServer *health.Server
EventSerializer serialization.Serializer
TimeSource clock.TimeSource
ChasmRegistry *chasm.Registry
// DEPRECATED: only history service on server side is supposed to
// use the following components.
CategoryRegistry tasks.TaskCategoryRegistry
matchingClient matchingservice.MatchingServiceClient
}
)
var (
_ adminservice.AdminServiceServer = (*AdminHandler)(nil)
)
// NewAdminHandler creates a gRPC handler for the adminservice
func NewAdminHandler(
args NewAdminHandlerArgs,
) *AdminHandler {
namespaceReplicationTaskExecutor := nsreplication.NewTaskExecutor(
args.ClusterMetadata.GetCurrentClusterName(),
args.PersistenceMetadataManager,
args.Logger,
)
historyHealthChecker := NewHealthChecker(
primitives.HistoryService,
args.MembershipMonitor,
args.Config.HistoryHostErrorPercentage,
args.Config.HistoryHostSelfErrorProportion,
func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) {
resp, err := args.HistoryClient.DeepHealthCheck(ctx, &historyservice.DeepHealthCheckRequest{HostAddress: hostAddress})
if err != nil {
return enumsspb.HEALTH_STATE_NOT_SERVING, err
}
return resp.GetState(), nil
},
args.Logger,
)
return &AdminHandler{
logger: args.Logger,
status: common.DaemonStatusInitialized,
numberOfHistoryShards: args.PersistenceConfig.NumHistoryShards,
config: args.Config,
namespaceDLQHandler: nsreplication.NewDLQMessageHandler(
namespaceReplicationTaskExecutor,
args.NamespaceReplicationQueue,
args.Logger,
),
eventSerializer: args.EventSerializer,
visibilityMgr: args.visibilityMgr,
persistenceExecutionName: args.PersistenceExecutionManager.GetName(),
namespaceReplicationQueue: args.NamespaceReplicationQueue,
taskManager: args.TaskManager,
fairTaskManager: args.FairTaskManager,
clusterMetadataManager: args.ClusterMetadataManager,
persistenceMetadataManager: args.PersistenceMetadataManager,
clientFactory: args.ClientFactory,
clientBean: args.ClientBean,
historyClient: args.HistoryClient,
sdkClientFactory: args.sdkClientFactory,
membershipMonitor: args.MembershipMonitor,
hostInfoProvider: args.HostInfoProvider,
metricsHandler: args.MetricsHandler,
namespaceRegistry: args.NamespaceRegistry,
saProvider: args.SaProvider,
saManager: args.SaManager,
saMapperProvider: args.SaMapperProvider,
saValidator: searchattribute.NewValidator(
args.SaProvider,
args.SaMapperProvider,
args.Config.SearchAttributesNumberOfKeysLimit,
args.Config.SearchAttributesSizeOfValueLimit,
args.Config.SearchAttributesTotalSizeLimit,
args.visibilityMgr,
visibility.AllowListForValidation(
args.visibilityMgr.GetStoreNames(),
args.Config.VisibilityAllowList,
),
args.Config.SuppressErrorSetSystemSearchAttribute,
),
clusterMetadata: args.ClusterMetadata,
healthServer: args.HealthServer,
historyHealthChecker: historyHealthChecker,
taskCategoryRegistry: args.CategoryRegistry,
matchingClient: args.matchingClient,
chasmRegistry: args.ChasmRegistry,
}
}
// Start starts the handler
func (adh *AdminHandler) Start() {
if atomic.CompareAndSwapInt32(
&adh.status,
common.DaemonStatusInitialized,
common.DaemonStatusStarted,
) {
adh.healthServer.SetServingStatus(AdminServiceName, healthpb.HealthCheckResponse_SERVING)
}
}
// Stop stops the handler
func (adh *AdminHandler) Stop() {
if atomic.CompareAndSwapInt32(
&adh.status,
common.DaemonStatusStarted,
common.DaemonStatusStopped,
) {
adh.healthServer.SetServingStatus(AdminServiceName, healthpb.HealthCheckResponse_NOT_SERVING)
}
}
func (adh *AdminHandler) DeepHealthCheck(
ctx context.Context,
_ *adminservice.DeepHealthCheckRequest,
) (_ *adminservice.DeepHealthCheckResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
healthStatus, err := adh.historyHealthChecker.Check(ctx)
if err != nil {
return nil, err
}
return &adminservice.DeepHealthCheckResponse{State: healthStatus}, nil
}
// AddSearchAttributes add search attribute to the cluster.
func (adh *AdminHandler) AddSearchAttributes(
ctx context.Context,
request *adminservice.AddSearchAttributesRequest,
) (_ *adminservice.AddSearchAttributesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
// validate request
if request == nil {
return nil, errRequestNotSet
}
if len(request.GetSearchAttributes()) == 0 {
return nil, errSearchAttributesNotSet
}
indexName := request.GetIndexName()
if indexName == "" {
indexName = adh.visibilityMgr.GetIndexName()
}
currentSearchAttributes, err := adh.saProvider.GetSearchAttributes(indexName, true)
if err != nil {
return nil, serviceerror.NewUnavailablef(errUnableToGetSearchAttributesMessage, err)
}
for saName, saType := range request.GetSearchAttributes() {
if sadefs.IsReserved(saName) {
return nil, serviceerror.NewInvalidArgumentf(errSearchAttributeIsReservedMessage, saName)
}
if currentSearchAttributes.IsDefined(saName) {
return nil, serviceerror.NewInvalidArgumentf(errSearchAttributeAlreadyExistsMessage, saName)
}
if _, ok := enumspb.IndexedValueType_name[int32(saType)]; !ok {
return nil, serviceerror.NewInvalidArgumentf(errUnknownSearchAttributeTypeMessage, saType)
}
}
// TODO (rodrigozhou): Remove condition `indexName == ""`.
// If indexName == "", then calling addSearchAttributesElasticsearch will
// register the search attributes in the cluster metadata if ES is up or if
// `skip-schema-update` is set. This is for backward compatibility using
// standard visibility.
if adh.visibilityMgr.HasStoreName(elasticsearch.PersistenceName) || indexName == "" {
err = adh.addSearchAttributesElasticsearch(ctx, request, indexName)
} else {
err = adh.addSearchAttributesSQL(ctx, request, currentSearchAttributes)
}
if err != nil {
return nil, err
}
return &adminservice.AddSearchAttributesResponse{}, nil
}
func (adh *AdminHandler) addSearchAttributesElasticsearch(
ctx context.Context,
request *adminservice.AddSearchAttributesRequest,
indexName string,
) error {
// Execute workflow.
wfParams := addsearchattributes.WorkflowParams{
CustomAttributesToAdd: request.GetSearchAttributes(),
IndexName: indexName,
SkipSchemaUpdate: request.GetSkipSchemaUpdate(),
}
sdkClient := adh.sdkClientFactory.GetSystemClient()
run, err := sdkClient.ExecuteWorkflow(
ctx,
sdkclient.StartWorkflowOptions{
TaskQueue: primitives.DefaultWorkerTaskQueue,
ID: addsearchattributes.WorkflowName,
},
addsearchattributes.WorkflowName,
wfParams,
)
if err != nil {
return serviceerror.NewUnavailablef(
errUnableToStartWorkflowMessage, addsearchattributes.WorkflowName, err,
)
}
// Wait for workflow to complete.
err = run.Get(ctx, nil)
if err != nil {
return serviceerror.NewUnavailablef(
errWorkflowReturnedErrorMessage, addsearchattributes.WorkflowName, err,
)
}
return nil
}
func (adh *AdminHandler) addSearchAttributesSQL(
ctx context.Context,
request *adminservice.AddSearchAttributesRequest,
currentSearchAttributes searchattribute.NameTypeMap,
) error {
_, client, err := adh.clientFactory.NewLocalFrontendClientWithTimeout(
frontend.DefaultTimeout,
frontend.DefaultLongPollTimeout,
)
if err != nil {
return serviceerror.NewUnavailablef(errUnableToCreateFrontendClientMessage, err)
}
nsName := request.GetNamespace()
if nsName == "" {
return errNamespaceNotSet
}
resp, err := client.DescribeNamespace(
ctx,
&workflowservice.DescribeNamespaceRequest{Namespace: nsName},
)
if err != nil {
return serviceerror.NewUnavailablef(errUnableToGetNamespaceInfoMessage, nsName, err)
}
cmCustomSearchAttributes := currentSearchAttributes.Custom()
upsertFieldToAliasMap := make(map[string]string)
fieldToAliasMap := resp.Config.CustomSearchAttributeAliases
aliasToFieldMap := util.InverseMap(fieldToAliasMap)
for saName, saType := range request.GetSearchAttributes() {
// check if alias is already in use
if _, ok := aliasToFieldMap[saName]; ok {
return serviceerror.NewAlreadyExistsf(
errSearchAttributeAlreadyExistsMessage, saName,
)
}
// find the first available field for the given type
targetFieldName := ""
cntUsed := 0
for fieldName, fieldType := range cmCustomSearchAttributes {
if fieldType != saType || !sadefs.IsPreallocatedCSAFieldName(fieldName, fieldType) {
continue
}
if _, ok := fieldToAliasMap[fieldName]; ok {
cntUsed++
} else if _, ok := upsertFieldToAliasMap[fieldName]; ok {
cntUsed++
} else {
targetFieldName = fieldName
break
}
}
if targetFieldName == "" {
return serviceerror.NewInvalidArgumentf(
errTooManySearchAttributesMessage, cntUsed, saType.String(),
)
}
upsertFieldToAliasMap[targetFieldName] = saName
}
_, err = client.UpdateNamespace(ctx, &workflowservice.UpdateNamespaceRequest{
Namespace: nsName,
Config: &namespacepb.NamespaceConfig{
CustomSearchAttributeAliases: upsertFieldToAliasMap,
},
})
if err != nil && err.Error() == errCustomSearchAttributeFieldAlreadyAllocated.Error() {
return errRaceConditionAddingSearchAttributes
}
return err
}
// RemoveSearchAttributes remove search attribute from the cluster.
func (adh *AdminHandler) RemoveSearchAttributes(
ctx context.Context,
request *adminservice.RemoveSearchAttributesRequest,
) (_ *adminservice.RemoveSearchAttributesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
// validate request
if request == nil {
return nil, errRequestNotSet
}
if len(request.GetSearchAttributes()) == 0 {
return nil, errSearchAttributesNotSet
}
indexName := request.GetIndexName()
if indexName == "" {
indexName = adh.visibilityMgr.GetIndexName()
}
currentSearchAttributes, err := adh.saProvider.GetSearchAttributes(indexName, true)
if err != nil {
return nil, serviceerror.NewUnavailablef(errUnableToGetSearchAttributesMessage, err)
}
// TODO (rodrigozhou): Remove condition `indexName == ""`.
// If indexName == "", then calling addSearchAttributesElasticsearch will
// register the search attributes in the cluster metadata if ES is up or if
// `skip-schema-update` is set. This is for backward compatibility using
// standard visibility.
if adh.visibilityMgr.HasStoreName(elasticsearch.PersistenceName) || indexName == "" {
err = adh.removeSearchAttributesElasticsearch(ctx, request, indexName, currentSearchAttributes)
} else {
err = adh.removeSearchAttributesSQL(ctx, request, currentSearchAttributes)
}
if err != nil {
return nil, err
}
return &adminservice.RemoveSearchAttributesResponse{}, nil
}
func (adh *AdminHandler) removeSearchAttributesElasticsearch(
ctx context.Context,
request *adminservice.RemoveSearchAttributesRequest,
indexName string,
currentSearchAttributes searchattribute.NameTypeMap,
) error {
newCustomSearchAttributes := maps.Clone(currentSearchAttributes.Custom())
for _, saName := range request.GetSearchAttributes() {
if !currentSearchAttributes.IsDefined(saName) {
return serviceerror.NewInvalidArgumentf(errSearchAttributeDoesntExistMessage, saName)
}
if _, ok := newCustomSearchAttributes[saName]; !ok {
return serviceerror.NewInvalidArgumentf(errUnableToRemoveNonCustomSearchAttributesMessage, saName)
}
delete(newCustomSearchAttributes, saName)
}
err := adh.saManager.SaveSearchAttributes(ctx, indexName, newCustomSearchAttributes)
if err != nil {
return serviceerror.NewUnavailablef(errUnableToSaveSearchAttributesMessage, err)
}
return nil
}
func (adh *AdminHandler) removeSearchAttributesSQL(
ctx context.Context,
request *adminservice.RemoveSearchAttributesRequest,
currentSearchAttributes searchattribute.NameTypeMap,
) error {
_, client, err := adh.clientFactory.NewLocalFrontendClientWithTimeout(
frontend.DefaultTimeout,
frontend.DefaultLongPollTimeout,
)
if err != nil {
return serviceerror.NewUnavailablef(errUnableToCreateFrontendClientMessage, err)
}
nsName := request.GetNamespace()
if nsName == "" {
return errNamespaceNotSet
}
resp, err := client.DescribeNamespace(
ctx,
&workflowservice.DescribeNamespaceRequest{Namespace: nsName},
)
if err != nil {
return serviceerror.NewUnavailablef(errUnableToGetNamespaceInfoMessage, nsName, err)
}
upsertFieldToAliasMap := make(map[string]string)
aliasToFieldMap := util.InverseMap(resp.Config.CustomSearchAttributeAliases)
for _, saName := range request.GetSearchAttributes() {
if fieldName, ok := aliasToFieldMap[saName]; ok {
upsertFieldToAliasMap[fieldName] = ""
continue
}
if currentSearchAttributes.IsDefined(saName) {
return serviceerror.NewInvalidArgumentf(
errUnableToRemoveNonCustomSearchAttributesMessage, saName,
)
}
return serviceerror.NewNotFoundf(errSearchAttributeDoesntExistMessage, saName)
}
_, err = client.UpdateNamespace(ctx, &workflowservice.UpdateNamespaceRequest{
Namespace: nsName,
Config: &namespacepb.NamespaceConfig{
CustomSearchAttributeAliases: upsertFieldToAliasMap,
},
})
return err
}
func (adh *AdminHandler) GetSearchAttributes(
ctx context.Context,
request *adminservice.GetSearchAttributesRequest,
) (_ *adminservice.GetSearchAttributesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
indexName := request.GetIndexName()
if indexName == "" {
indexName = adh.visibilityMgr.GetIndexName()
}
searchAttributes, err := adh.saProvider.GetSearchAttributes(indexName, true)
if err != nil {
adh.logger.Error("getSearchAttributes error", tag.Error(err))
return nil, serviceerror.NewUnavailablef(errUnableToGetSearchAttributesMessage, err)
}
// TODO (rodrigozhou): Remove condition `indexName == ""`.
// If indexName == "", then calling addSearchAttributesElasticsearch will
// register the search attributes in the cluster metadata if ES is up or if
// `skip-schema-update` is set. This is for backward compatibility using
// standard visibility.
if adh.visibilityMgr.HasStoreName(elasticsearch.PersistenceName) || indexName == "" {
return adh.getSearchAttributesElasticsearch(ctx, indexName, searchAttributes)
}
return adh.getSearchAttributesSQL(ctx, request, searchAttributes)
}
func (adh *AdminHandler) getSearchAttributesElasticsearch(
ctx context.Context,
indexName string,
searchAttributes searchattribute.NameTypeMap,
) (*adminservice.GetSearchAttributesResponse, error) {
sdkClient := adh.sdkClientFactory.GetSystemClient()
descResp, err := sdkClient.DescribeWorkflowExecution(ctx, addsearchattributes.WorkflowName, "")
var wfInfo *workflowpb.WorkflowExecutionInfo
if err != nil {
// NotFound can happen when no search attributes were added and the workflow has never been executed.
if _, isNotFound := err.(*serviceerror.NotFound); !isNotFound {
err = serviceerror.NewUnavailablef("unable to get %s workflow state: %v", addsearchattributes.WorkflowName, err)
adh.logger.Error("getSearchAttributes error", tag.Error(err))
return nil, err
}
} else {
wfInfo = descResp.GetWorkflowExecutionInfo()
}
return &adminservice.GetSearchAttributesResponse{
CustomAttributes: searchAttributes.Custom(),
SystemAttributes: searchAttributes.System(),
AddWorkflowExecutionInfo: wfInfo,
}, nil
}
func (adh *AdminHandler) getSearchAttributesSQL(
ctx context.Context,
request *adminservice.GetSearchAttributesRequest,
searchAttributes searchattribute.NameTypeMap,
) (*adminservice.GetSearchAttributesResponse, error) {
_, client, err := adh.clientFactory.NewLocalFrontendClientWithTimeout(
frontend.DefaultTimeout,
frontend.DefaultLongPollTimeout,
)
if err != nil {
return nil, serviceerror.NewUnavailablef(errUnableToCreateFrontendClientMessage, err)
}
nsName := request.GetNamespace()
if nsName == "" {
return nil, errNamespaceNotSet
}
resp, err := client.DescribeNamespace(
ctx,
&workflowservice.DescribeNamespaceRequest{Namespace: nsName},
)
if err != nil {
return nil, serviceerror.NewUnavailablef(
errUnableToGetNamespaceInfoMessage, nsName, err,
)
}
fieldToAliasMap := resp.Config.CustomSearchAttributeAliases
customSearchAttributes := make(map[string]enumspb.IndexedValueType)
for field, tp := range searchAttributes.Custom() {
if alias, ok := fieldToAliasMap[field]; ok {
customSearchAttributes[alias] = tp
}
}
return &adminservice.GetSearchAttributesResponse{
CustomAttributes: customSearchAttributes,
SystemAttributes: searchAttributes.System(),
}, nil
}
func (adh *AdminHandler) RebuildMutableState(
ctx context.Context,
request *adminservice.RebuildMutableStateRequest,
) (_ *adminservice.RebuildMutableStateResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
if _, err := adh.historyClient.RebuildMutableState(ctx, &historyservice.RebuildMutableStateRequest{
NamespaceId: namespaceID.String(),
Execution: request.Execution,
}); err != nil {
return nil, err
}
return &adminservice.RebuildMutableStateResponse{}, nil
}
func (adh *AdminHandler) ImportWorkflowExecution(
ctx context.Context,
request *adminservice.ImportWorkflowExecutionRequest,
) (_ *adminservice.ImportWorkflowExecutionResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
unaliasedBatches, err := adh.unaliasAndValidateSearchAttributes(request.HistoryBatches, namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
resp, err := adh.historyClient.ImportWorkflowExecution(ctx, &historyservice.ImportWorkflowExecutionRequest{
NamespaceId: namespaceID.String(),
Execution: request.Execution,
HistoryBatches: unaliasedBatches,
VersionHistory: request.VersionHistory,
Token: request.Token,
})
if err != nil {
return nil, err
}
return &adminservice.ImportWorkflowExecutionResponse{
Token: resp.Token,
}, nil
}
func (adh *AdminHandler) unaliasAndValidateSearchAttributes(historyBatches []*commonpb.DataBlob, nsName namespace.Name) ([]*commonpb.DataBlob, error) {
var unaliasedBatches []*commonpb.DataBlob
for _, historyBatch := range historyBatches {
events, err := adh.eventSerializer.DeserializeEvents(historyBatch)
if err != nil {
return nil, serviceerror.NewInvalidArgument(err.Error())
}
hasSas := false
for _, event := range events {
sas, _ := searchattribute.GetFromEvent(event)
if sas == nil {
continue
}
hasSas = true
unaliasedSas, err := searchattribute.UnaliasFields(adh.saMapperProvider, sas, nsName.String())
if err != nil {
var invArgErr *serviceerror.InvalidArgument
if !errors.As(err, &invArgErr) {
return nil, err
}
// Mapper returns InvalidArgument if alias is not found. It means that history has field names, not aliases.
// Ignore the error and proceed with the original search attributes.
unaliasedSas = sas
}
// Now validate that search attributes are valid.
err = adh.saValidator.Validate(unaliasedSas, nsName.String())
if err != nil {
return nil, err
}
_ = searchattribute.SetToEvent(event, unaliasedSas)
}
// If blob doesn't have search attributes, it can be used as is w/o serialization.
if !hasSas {
unaliasedBatches = append(unaliasedBatches, historyBatch)
continue
}
unaliasedBatch, err := adh.eventSerializer.SerializeEvents(events)
if err != nil {
return nil, serviceerror.NewInvalidArgument(err.Error())
}
unaliasedBatches = append(unaliasedBatches, unaliasedBatch)
}
return unaliasedBatches, nil
}
// DescribeMutableState returns information about the specified workflow execution.
func (adh *AdminHandler) DescribeMutableState(ctx context.Context, request *adminservice.DescribeMutableStateRequest) (_ *adminservice.DescribeMutableStateResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
archetypeID, err := adh.archetypeNameToID(request.GetArchetype())
if err != nil {
return nil, err
}
shardID := common.WorkflowIDToHistoryShard(namespaceID.String(), request.Execution.WorkflowId, adh.numberOfHistoryShards)
shardIDStr := convert.Int32ToString(shardID)
resolver, err := adh.membershipMonitor.GetResolver(primitives.HistoryService)
if err != nil {
return nil, err
}
historyHost, err := resolver.Lookup(shardIDStr)
if err != nil {
return nil, err
}
historyAddr := historyHost.GetAddress()
historyResponse, err := adh.historyClient.DescribeMutableState(ctx, &historyservice.DescribeMutableStateRequest{
NamespaceId: namespaceID.String(),
Execution: request.Execution,
SkipForceReload: request.GetSkipForceReload(),
ArchetypeId: archetypeID,
})
if err != nil {
return nil, err
}
return &adminservice.DescribeMutableStateResponse{
ShardId: shardIDStr,
HistoryAddr: historyAddr,
DatabaseMutableState: historyResponse.GetDatabaseMutableState(),
CacheMutableState: historyResponse.GetCacheMutableState(),
}, nil
}
// RemoveTask returns information about the internal states of a history host
func (adh *AdminHandler) RemoveTask(ctx context.Context, request *adminservice.RemoveTaskRequest) (_ *adminservice.RemoveTaskResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
_, err := adh.historyClient.RemoveTask(ctx, &historyservice.RemoveTaskRequest{
ShardId: request.GetShardId(),
Category: request.GetCategory(),
TaskId: request.GetTaskId(),
VisibilityTime: request.GetVisibilityTime(),
})
return &adminservice.RemoveTaskResponse{}, err
}
// GetShard returns information about the internal states of a shard
func (adh *AdminHandler) GetShard(ctx context.Context, request *adminservice.GetShardRequest) (_ *adminservice.GetShardResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
resp, err := adh.historyClient.GetShard(ctx, &historyservice.GetShardRequest{ShardId: request.GetShardId()})
if err != nil {
return nil, err
}
return &adminservice.GetShardResponse{ShardInfo: resp.ShardInfo}, nil
}
// CloseShard returns information about the internal states of a history host
func (adh *AdminHandler) CloseShard(ctx context.Context, request *adminservice.CloseShardRequest) (_ *adminservice.CloseShardResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
_, err := adh.historyClient.CloseShard(ctx, &historyservice.CloseShardRequest{ShardId: request.GetShardId()})
return &adminservice.CloseShardResponse{}, err
}
func (adh *AdminHandler) ListHistoryTasks(
ctx context.Context,
request *adminservice.ListHistoryTasksRequest,
) (_ *adminservice.ListHistoryTasksResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if request.GetTaskRange() == nil {
return nil, errTaskRangeNotSet
}
if !adh.config.AdminEnableListHistoryTasks() {
return nil, errListHistoryTasksNotAllowed
}
resp, err := adh.historyClient.ListTasks(
ctx, &historyservice.ListTasksRequest{
Request: request,
},
)
if err != nil {
return nil, err
}
return resp.Response, nil
}
// DescribeHistoryHost returns information about the internal states of a history host
func (adh *AdminHandler) DescribeHistoryHost(ctx context.Context, request *adminservice.DescribeHistoryHostRequest) (_ *adminservice.DescribeHistoryHostResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
flagsCount := 0
if request.ShardId != 0 {
flagsCount++
}
if len(request.Namespace) != 0 && request.WorkflowExecution != nil {
flagsCount++
}
if len(request.GetHostAddress()) > 0 {
flagsCount++
}
if flagsCount != 1 {
return nil, serviceerror.NewInvalidArgument("must provide one and only one: shard id or namespace & workflow id or host address")
}
var err error
var namespaceID namespace.ID
if request.WorkflowExecution != nil {
namespaceID, err = adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.Namespace))
if err != nil {
return nil, err
}
if err := validateExecution(request.WorkflowExecution); err != nil {
return nil, err
}
}
resp, err := adh.historyClient.DescribeHistoryHost(ctx, &historyservice.DescribeHistoryHostRequest{
HostAddress: request.GetHostAddress(),
ShardId: request.GetShardId(),
NamespaceId: namespaceID.String(),
WorkflowExecution: request.GetWorkflowExecution(),
})
if resp == nil {
return nil, err
}
return &adminservice.DescribeHistoryHostResponse{
ShardsNumber: resp.GetShardsNumber(),
ShardIds: resp.GetShardIds(),
NamespaceCache: resp.GetNamespaceCache(),
Address: resp.GetAddress(),
}, err
}
func (adh *AdminHandler) GetWorkflowExecutionRawHistory(
ctx context.Context,
request *adminservice.GetWorkflowExecutionRawHistoryRequest,
) (_ *adminservice.GetWorkflowExecutionRawHistoryResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
response, err := adh.historyClient.GetWorkflowExecutionRawHistory(ctx,
&historyservice.GetWorkflowExecutionRawHistoryRequest{
NamespaceId: request.NamespaceId,
Request: request,
})
if err != nil {
return nil, err
}
return response.Response, nil
}
// GetWorkflowExecutionRawHistoryV2 - retrieves the history of workflow execution
func (adh *AdminHandler) GetWorkflowExecutionRawHistoryV2(ctx context.Context, request *adminservice.GetWorkflowExecutionRawHistoryV2Request) (_ *adminservice.GetWorkflowExecutionRawHistoryV2Response, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if err := adh.validateGetWorkflowExecutionRawHistoryV2Request(
request,
); err != nil {
return nil, err
}
response, err := adh.historyClient.GetWorkflowExecutionRawHistoryV2(ctx,
&historyservice.GetWorkflowExecutionRawHistoryV2Request{
NamespaceId: request.NamespaceId,
Request: request,
})
if err != nil {
return nil, err
}
return response.Response, nil
}
func (adh *AdminHandler) validateGetWorkflowExecutionRawHistoryV2Request(
request *adminservice.GetWorkflowExecutionRawHistoryV2Request,
) error {
execution := request.Execution
if execution.GetWorkflowId() == "" {
return errWorkflowIDNotSet
}
// TODO currently, this API is only going to be used by re-send history events
// to remote cluster if kafka is lossy again, in the future, this API can be used
// by CLI and client, then empty runID (meaning the current workflow) should be allowed
if execution.GetRunId() == "" || uuid.Validate(execution.GetRunId()) != nil {
return errInvalidRunID
}
pageSize := int(request.GetMaximumPageSize())
if pageSize <= 0 {
return errInvalidPageSize
}
if request.GetStartEventId() == common.EmptyEventID &&
request.GetStartEventVersion() == common.EmptyVersion &&
request.GetEndEventId() == common.EmptyEventID &&
request.GetEndEventVersion() == common.EmptyVersion {
return errInvalidEventQueryRange
}
return nil
}
// DescribeCluster return information about a temporal cluster
func (adh *AdminHandler) DescribeCluster(
ctx context.Context,
request *adminservice.DescribeClusterRequest,
) (_ *adminservice.DescribeClusterResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
membershipInfo := &clusterspb.MembershipInfo{}
if monitor := adh.membershipMonitor; monitor != nil {
membershipInfo.CurrentHost = &clusterspb.HostInfo{
Identity: adh.hostInfoProvider.HostInfo().Identity(),
}
members, err := monitor.GetReachableMembers()
if err != nil {
return nil, err
}
membershipInfo.ReachableMembers = members
var rings []*clusterspb.RingInfo
for _, role := range []primitives.ServiceName{
primitives.FrontendService,
primitives.InternalFrontendService,
primitives.HistoryService,
primitives.MatchingService,
primitives.WorkerService,
} {
resolver, err := monitor.GetResolver(role)
if err != nil {
if role == primitives.InternalFrontendService {
continue // this one is optional
}
return nil, err
}
var servers []*clusterspb.HostInfo
for _, server := range resolver.Members() {
servers = append(servers, &clusterspb.HostInfo{
Identity: server.Identity(),
})
}
rings = append(rings, &clusterspb.RingInfo{
Role: string(role),
MemberCount: int32(resolver.MemberCount()),
Members: servers,
})
}
membershipInfo.Rings = rings
}
if len(request.ClusterName) == 0 {
request.ClusterName = adh.clusterMetadata.GetCurrentClusterName()
}
metadata, err := adh.clusterMetadataManager.GetClusterMetadata(
ctx,
&persistence.GetClusterMetadataRequest{ClusterName: request.GetClusterName()},
)
if err != nil {
return nil, err
}
return &adminservice.DescribeClusterResponse{
SupportedClients: headers.SupportedClients,
ServerVersion: headers.ServerVersion,
MembershipInfo: membershipInfo,
ClusterId: metadata.GetClusterId(),
ClusterName: metadata.GetClusterName(),
HistoryShardCount: metadata.GetHistoryShardCount(),
PersistenceStore: adh.persistenceExecutionName,
VisibilityStore: strings.Join(adh.visibilityMgr.GetStoreNames(), ","),
VersionInfo: metadata.GetVersionInfo(),
FailoverVersionIncrement: metadata.GetFailoverVersionIncrement(),
InitialFailoverVersion: metadata.GetInitialFailoverVersion(),
IsGlobalNamespaceEnabled: metadata.GetIsGlobalNamespaceEnabled(),
Tags: metadata.GetTags(),
HttpAddress: metadata.GetHttpAddress(),
}, nil
}
// ListClusters return information about temporal clusters
// TODO: Remove this API after migrate tctl to use operator handler
func (adh *AdminHandler) ListClusters(
ctx context.Context,
request *adminservice.ListClustersRequest,
) (_ *adminservice.ListClustersResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if request.GetPageSize() <= 0 {
request.PageSize = listClustersPageSize
}
resp, err := adh.clusterMetadataManager.ListClusterMetadata(ctx, &persistence.ListClusterMetadataRequest{
PageSize: int(request.GetPageSize()),
NextPageToken: request.GetNextPageToken(),
})
if err != nil {
return nil, err
}
var clusterMetadataList []*persistencespb.ClusterMetadata
for _, clusterResp := range resp.ClusterMetadata {
clusterMetadataList = append(clusterMetadataList, clusterResp.ClusterMetadata)
}
return &adminservice.ListClustersResponse{
Clusters: clusterMetadataList,
NextPageToken: resp.NextPageToken,
}, nil
}
// ListClusterMembers
// TODO: Remove this API after migrate tctl to use operator handler
func (adh *AdminHandler) ListClusterMembers(
ctx context.Context,
request *adminservice.ListClusterMembersRequest,
) (_ *adminservice.ListClusterMembersResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
metadataMgr := adh.clusterMetadataManager
heartbitRef := request.GetLastHeartbeatWithin()
var heartbit time.Duration
if heartbitRef != nil {
heartbit = heartbitRef.AsDuration()
}
startedTimeRef := request.GetSessionStartedAfterTime()
var startedTime time.Time
if startedTimeRef != nil {
startedTime = startedTimeRef.AsTime()
}
hostIDEqual, err := uuid.Parse(request.GetHostId())
if err != nil {
return nil, serviceerror.NewInvalidArgumentf("host ID %q is not a valid UUID: %v", request.GetHostId(), err)
}
hostIDEqualBytes, err := hostIDEqual.MarshalBinary()
if err != nil {
return nil, serviceerror.NewInternalf("unable to marshal host ID %q to bytes: %v", request.GetHostId(), err)
}
resp, err := metadataMgr.GetClusterMembers(ctx, &persistence.GetClusterMembersRequest{
LastHeartbeatWithin: heartbit,
RPCAddressEquals: net.ParseIP(request.GetRpcAddress()),
HostIDEquals: hostIDEqualBytes,
RoleEquals: persistence.ServiceType(request.GetRole()),
SessionStartedAfter: startedTime,
PageSize: int(request.GetPageSize()),
NextPageToken: request.GetNextPageToken(),
})
if err != nil {
return nil, err
}
var activeMembers []*clusterspb.ClusterMember
for _, member := range resp.ActiveMembers {
u, err := uuid.FromBytes(member.HostID)
if err != nil {
return nil, serviceerror.NewInternalf("unable to parse host ID bytes to UUID: %v", err)
}
activeMembers = append(activeMembers, &clusterspb.ClusterMember{
Role: enumsspb.ClusterMemberRole(member.Role),
HostId: u.String(),
RpcAddress: member.RPCAddress.String(),
RpcPort: int32(member.RPCPort),
SessionStartTime: timestamppb.New(member.SessionStart),
LastHeartbitTime: timestamppb.New(member.LastHeartbeat),
RecordExpiryTime: timestamppb.New(member.RecordExpiry),
})
}
return &adminservice.ListClusterMembersResponse{
ActiveMembers: activeMembers,
NextPageToken: resp.NextPageToken,
}, nil
}
// AddOrUpdateRemoteCluster
// TODO: Remove this API after migrate tctl to use operator handler
func (adh *AdminHandler) AddOrUpdateRemoteCluster(
ctx context.Context,
request *adminservice.AddOrUpdateRemoteClusterRequest,
) (_ *adminservice.AddOrUpdateRemoteClusterResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
adminClient := adh.clientFactory.NewRemoteAdminClientWithTimeout(
request.GetFrontendAddress(),
admin.DefaultTimeout,
admin.DefaultLargeTimeout,
)
// Fetch cluster metadata from remote cluster
resp, err := adminClient.DescribeCluster(ctx, &adminservice.DescribeClusterRequest{})
if err != nil {
return nil, err
}
err = adh.validateRemoteClusterMetadata(resp)
if err != nil {
return nil, err
}
var updateRequestVersion int64 = 0
clusterMetadataMrg := adh.clusterMetadataManager
clusterData, err := clusterMetadataMrg.GetClusterMetadata(
ctx,
&persistence.GetClusterMetadataRequest{ClusterName: resp.GetClusterName()},
)
switch err.(type) {
case nil:
updateRequestVersion = clusterData.Version
case *serviceerror.NotFound:
updateRequestVersion = 0
default:
return nil, err
}
applied, err := clusterMetadataMrg.SaveClusterMetadata(ctx, &persistence.SaveClusterMetadataRequest{
ClusterMetadata: &persistencespb.ClusterMetadata{
ClusterName: resp.GetClusterName(),
HistoryShardCount: resp.GetHistoryShardCount(),
ClusterId: resp.GetClusterId(),
ClusterAddress: request.GetFrontendAddress(),
HttpAddress: resp.GetHttpAddress(),
FailoverVersionIncrement: resp.GetFailoverVersionIncrement(),
InitialFailoverVersion: resp.GetInitialFailoverVersion(),
IsGlobalNamespaceEnabled: resp.GetIsGlobalNamespaceEnabled(),
IsConnectionEnabled: request.GetEnableRemoteClusterConnection(),
IsReplicationEnabled: request.GetEnableReplication(),
Tags: resp.GetTags(),
},
Version: updateRequestVersion,
})
if err != nil {
return nil, err
}
if !applied {
return nil, serviceerror.NewInvalidArgument(
"Cannot update remote cluster due to update immutable fields")
}
return &adminservice.AddOrUpdateRemoteClusterResponse{}, nil
}
// RemoveRemoteCluster
// TODO: Remove this API after migrate tctl to use operator handler
func (adh *AdminHandler) RemoveRemoteCluster(
ctx context.Context,
request *adminservice.RemoveRemoteClusterRequest,
) (_ *adminservice.RemoveRemoteClusterResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if err := adh.clusterMetadataManager.DeleteClusterMetadata(
ctx,
&persistence.DeleteClusterMetadataRequest{ClusterName: request.GetClusterName()},
); err != nil {
return nil, err
}
return &adminservice.RemoveRemoteClusterResponse{}, nil
}
// GetReplicationMessages returns new replication tasks since the read level provided in the token.
func (adh *AdminHandler) GetReplicationMessages(ctx context.Context, request *adminservice.GetReplicationMessagesRequest) (_ *adminservice.GetReplicationMessagesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if request.GetClusterName() == "" {
return nil, errClusterNameNotSet
}
resp, err := adh.historyClient.GetReplicationMessages(ctx, &historyservice.GetReplicationMessagesRequest{
Tokens: request.GetTokens(),
ClusterName: request.GetClusterName(),
})
if err != nil {
return nil, err
}
return &adminservice.GetReplicationMessagesResponse{ShardMessages: resp.GetShardMessages()}, nil
}
// GetNamespaceReplicationMessages returns new namespace replication tasks since last retrieved task ID.
func (adh *AdminHandler) GetNamespaceReplicationMessages(ctx context.Context, request *adminservice.GetNamespaceReplicationMessagesRequest) (_ *adminservice.GetNamespaceReplicationMessagesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if adh.namespaceReplicationQueue == nil {
return nil, errors.New("namespace replication queue not enabled for cluster")
}
lastMessageID := request.GetLastRetrievedMessageId()
if request.GetLastRetrievedMessageId() == defaultLastMessageID {
if clusterAckLevels, err := adh.namespaceReplicationQueue.GetAckLevels(ctx); err == nil {
if ackLevel, ok := clusterAckLevels[request.GetClusterName()]; ok {
lastMessageID = ackLevel
}
}
}
replicationTasks, lastMessageID, err := adh.namespaceReplicationQueue.GetReplicationMessages(
ctx,
lastMessageID,
getNamespaceReplicationMessageBatchSize,
)
if err != nil {
return nil, err
}
if request.GetLastProcessedMessageId() != defaultLastMessageID {
if err := adh.namespaceReplicationQueue.UpdateAckLevel(
ctx,
request.GetLastProcessedMessageId(),
request.GetClusterName(),
); err != nil {
adh.logger.Warn("Failed to update namespace replication queue ack level",
tag.TaskID(request.GetLastProcessedMessageId()),
tag.ClusterName(request.GetClusterName()))
}
}
return &adminservice.GetNamespaceReplicationMessagesResponse{
Messages: &replicationspb.ReplicationMessages{
ReplicationTasks: replicationTasks,
LastRetrievedMessageId: lastMessageID,
},
}, nil
}
// GetDLQReplicationMessages returns new replication tasks based on the dlq info.
func (adh *AdminHandler) GetDLQReplicationMessages(ctx context.Context, request *adminservice.GetDLQReplicationMessagesRequest) (_ *adminservice.GetDLQReplicationMessagesResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if len(request.GetTaskInfos()) == 0 {
return nil, errEmptyReplicationInfo
}
resp, err := adh.historyClient.GetDLQReplicationMessages(ctx, &historyservice.GetDLQReplicationMessagesRequest{TaskInfos: request.GetTaskInfos()})
if err != nil {
return nil, err
}
return &adminservice.GetDLQReplicationMessagesResponse{ReplicationTasks: resp.GetReplicationTasks()}, nil
}
// ReapplyEvents applies stale events to the current workflow and the current run
func (adh *AdminHandler) ReapplyEvents(ctx context.Context, request *adminservice.ReapplyEventsRequest) (_ *adminservice.ReapplyEventsResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if request.WorkflowExecution == nil {
return nil, errExecutionNotSet
}
if request.GetWorkflowExecution().GetWorkflowId() == "" {
return nil, errWorkflowIDNotSet
}
if request.GetEvents() == nil {
return nil, errWorkflowIDNotSet
}
namespaceEntry, err := adh.namespaceRegistry.GetNamespaceByID(namespace.ID(request.GetNamespaceId()))
if err != nil {
return nil, err
}
_, err = adh.historyClient.ReapplyEvents(ctx, &historyservice.ReapplyEventsRequest{
NamespaceId: namespaceEntry.ID().String(),
Request: request,
})
if err != nil {
return nil, err
}
return &adminservice.ReapplyEventsResponse{}, nil
}
// GetDLQMessages reads messages from DLQ
func (adh *AdminHandler) GetDLQMessages(
ctx context.Context,
request *adminservice.GetDLQMessagesRequest,
) (resp *adminservice.GetDLQMessagesResponse, retErr error) {
defer log.CapturePanic(adh.logger, &retErr)
if request == nil {
return nil, errRequestNotSet
}
if request.GetMaximumPageSize() <= 0 {
request.MaximumPageSize = primitives.ReadDLQMessagesPageSize
}
if request.GetInclusiveEndMessageId() <= 0 {
request.InclusiveEndMessageId = common.EndMessageID
}
switch request.GetType() {
case enumsspb.DEAD_LETTER_QUEUE_TYPE_REPLICATION:
resp, err := adh.historyClient.GetDLQMessages(ctx, &historyservice.GetDLQMessagesRequest{
Type: request.GetType(),
ShardId: request.GetShardId(),
SourceCluster: request.GetSourceCluster(),
InclusiveEndMessageId: request.GetInclusiveEndMessageId(),
MaximumPageSize: request.GetMaximumPageSize(),
NextPageToken: request.GetNextPageToken(),
})
if resp == nil {
return nil, err
}
return &adminservice.GetDLQMessagesResponse{
Type: resp.GetType(),
ReplicationTasks: resp.GetReplicationTasks(),
ReplicationTasksInfo: resp.GetReplicationTasksInfo(),
NextPageToken: resp.GetNextPageToken(),
}, err
case enumsspb.DEAD_LETTER_QUEUE_TYPE_NAMESPACE:
tasks, token, err := adh.namespaceDLQHandler.Read(
ctx,
request.GetInclusiveEndMessageId(),
int(request.GetMaximumPageSize()),
request.GetNextPageToken())
if err != nil {
return nil, err
}
return &adminservice.GetDLQMessagesResponse{
ReplicationTasks: tasks,
NextPageToken: token,
}, nil
default:
return nil, errDLQTypeIsNotSupported
}
}
// PurgeDLQMessages purge messages from DLQ
func (adh *AdminHandler) PurgeDLQMessages(
ctx context.Context,
request *adminservice.PurgeDLQMessagesRequest,
) (_ *adminservice.PurgeDLQMessagesResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil {
return nil, errRequestNotSet
}
if request.GetInclusiveEndMessageId() <= 0 {
request.InclusiveEndMessageId = common.EndMessageID
}
switch request.GetType() {
case enumsspb.DEAD_LETTER_QUEUE_TYPE_REPLICATION:
resp, err := adh.historyClient.PurgeDLQMessages(ctx, &historyservice.PurgeDLQMessagesRequest{
Type: request.GetType(),
ShardId: request.GetShardId(),
SourceCluster: request.GetSourceCluster(),
InclusiveEndMessageId: request.GetInclusiveEndMessageId(),
})
if resp == nil {
return nil, err
}
return &adminservice.PurgeDLQMessagesResponse{}, err
case enumsspb.DEAD_LETTER_QUEUE_TYPE_NAMESPACE:
err := adh.namespaceDLQHandler.Purge(ctx, request.GetInclusiveEndMessageId())
if err != nil {
return nil, err
}
return &adminservice.PurgeDLQMessagesResponse{}, err
default:
return nil, errDLQTypeIsNotSupported
}
}
// MergeDLQMessages merges DLQ messages
func (adh *AdminHandler) MergeDLQMessages(
ctx context.Context,
request *adminservice.MergeDLQMessagesRequest,
) (resp *adminservice.MergeDLQMessagesResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil {
return nil, errRequestNotSet
}
if request.GetInclusiveEndMessageId() <= 0 {
request.InclusiveEndMessageId = common.EndMessageID
}
switch request.GetType() {
case enumsspb.DEAD_LETTER_QUEUE_TYPE_REPLICATION:
resp, err := adh.historyClient.MergeDLQMessages(ctx, &historyservice.MergeDLQMessagesRequest{
Type: request.GetType(),
ShardId: request.GetShardId(),
SourceCluster: request.GetSourceCluster(),
InclusiveEndMessageId: request.GetInclusiveEndMessageId(),
MaximumPageSize: request.GetMaximumPageSize(),
NextPageToken: request.GetNextPageToken(),
})
if resp == nil {
return nil, err
}
return &adminservice.MergeDLQMessagesResponse{
NextPageToken: request.GetNextPageToken(),
}, nil
case enumsspb.DEAD_LETTER_QUEUE_TYPE_NAMESPACE:
token, err := adh.namespaceDLQHandler.Merge(
ctx,
request.GetInclusiveEndMessageId(),
int(request.GetMaximumPageSize()),
request.GetNextPageToken(),
)
if err != nil {
return nil, err
}
return &adminservice.MergeDLQMessagesResponse{
NextPageToken: token,
}, nil
default:
return nil, errDLQTypeIsNotSupported
}
}
// RefreshWorkflowTasks re-generates the workflow tasks
func (adh *AdminHandler) RefreshWorkflowTasks(
ctx context.Context,
request *adminservice.RefreshWorkflowTasksRequest,
) (_ *adminservice.RefreshWorkflowTasksResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceEntry, err := adh.namespaceRegistry.GetNamespaceByID(namespace.ID(request.GetNamespaceId()))
if err != nil {
return nil, err
}
archetypeID, err := adh.archetypeNameToID(request.GetArchetype())
if err != nil {
return nil, err
}
_, err = adh.historyClient.RefreshWorkflowTasks(ctx, &historyservice.RefreshWorkflowTasksRequest{
NamespaceId: namespaceEntry.ID().String(),
ArchetypeId: archetypeID,
Request: request,
})
if err != nil {
return nil, err
}
return &adminservice.RefreshWorkflowTasksResponse{}, nil
}
// StartAdminBatchOperation starts an admin batch operation.
func (adh *AdminHandler) StartAdminBatchOperation(
ctx context.Context,
request *adminservice.StartAdminBatchOperationRequest,
) (_ *adminservice.StartAdminBatchOperationResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateAdminBatchOperation(request); err != nil {
return nil, err
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
// Validate concurrent batch operation
maxConcurrentBatchOperation := adh.config.MaxConcurrentAdminBatchOperation(request.GetNamespace())
countResp, err := adh.visibilityMgr.CountWorkflowExecutions(ctx, &manager.CountWorkflowExecutionsRequest{
NamespaceID: namespaceID,
Namespace: namespace.Name(request.GetNamespace()),
Query: batcher.OpenAdminBatchOperationQuery,
})
if err != nil {
return nil, err
}
openAdminBatchOperationCount := int(countResp.Count)
if openAdminBatchOperationCount >= maxConcurrentBatchOperation {
return nil, &serviceerror.ResourceExhausted{
Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT,
Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE,
Message: "Max concurrent admin batch operations is reached",
}
}
input := &batchspb.BatchOperationInput{
AdminRequest: request,
NamespaceId: namespaceID.String(),
}
identity := request.GetIdentity()
var batchTypeMemo string
switch op := request.Operation.(type) {
case *adminservice.StartAdminBatchOperationRequest_RefreshTasksOperation:
batchTypeMemo = "refresh_tasks"
default:
return nil, serviceerror.NewInvalidArgumentf("The operation type %T is not supported", op)
}
inputPayload, err := payloads.Encode(input)
if err != nil {
return nil, err
}
memo := &commonpb.Memo{
Fields: map[string]*commonpb.Payload{
batcher.BatchOperationTypeMemo: payload.EncodeString(batchTypeMemo),
batcher.BatchReasonMemo: payload.EncodeString(request.GetReason()),
},
}
var searchAttributes *commonpb.SearchAttributes
searchattribute.AddSearchAttribute(&searchAttributes, sadefs.BatcherUser, payload.EncodeString(identity))
searchattribute.AddSearchAttribute(&searchAttributes, sadefs.TemporalNamespaceDivision, payload.EncodeString(batcher.AdminNamespaceDivision))
startReq := &workflowservice.StartWorkflowExecutionRequest{
Namespace: request.Namespace,
WorkflowId: request.GetJobId(),
WorkflowType: &commonpb.WorkflowType{Name: batcher.BatchWFTypeProtobufName},
TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue},
Input: inputPayload,
Identity: identity,
RequestId: uuid.NewString(),
WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL,
WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE,
Memo: memo,
SearchAttributes: searchAttributes,
}
_, err = adh.historyClient.StartWorkflowExecution(
ctx,
common.CreateHistoryStartWorkflowRequest(
namespaceID.String(),
startReq,
nil,
nil,
time.Now().UTC(),
),
)
if err != nil {
return nil, err
}
return &adminservice.StartAdminBatchOperationResponse{}, nil
}
func validateAdminBatchOperation(params *adminservice.StartAdminBatchOperationRequest) error {
if params.GetOperation() == nil ||
params.GetReason() == "" ||
params.GetNamespace() == "" ||
(params.GetVisibilityQuery() == "" && len(params.GetExecutions()) == 0) {
return serviceerror.NewInvalidArgument("must provide required parameters: Operation/Reason/Namespace/Query or Executions")
}
if len(params.GetJobId()) == 0 {
return serviceerror.NewInvalidArgument("JobId is not set on request.")
}
if len(params.GetVisibilityQuery()) != 0 && len(params.GetExecutions()) != 0 {
return serviceerror.NewInvalidArgument("batch query and executions are mutually exclusive")
}
switch op := params.GetOperation().(type) {
case *adminservice.StartAdminBatchOperationRequest_RefreshTasksOperation:
// No additional validation needed
return nil
default:
return serviceerror.NewInvalidArgumentf("not supported admin batch type: %T", op)
}
}
// ResendReplicationTasks requests replication task from remote cluster
func (adh *AdminHandler) ResendReplicationTasks(
ctx context.Context,
request *adminservice.ResendReplicationTasksRequest,
) (_ *adminservice.ResendReplicationTasksResponse, err error) {
return nil, serviceerror.NewUnimplemented("ResendReplicationTasks is not implemented in AdminHandler")
}
// GetTaskQueueTasks returns tasks from task queue
func (adh *AdminHandler) GetTaskQueueTasks(
ctx context.Context,
request *adminservice.GetTaskQueueTasksRequest,
) (_ *adminservice.GetTaskQueueTasksResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil {
return nil, errRequestNotSet
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
var taskManager persistence.TaskManager
if request.GetMinPass() != 0 {
if adh.fairTaskManager == nil {
return nil, serviceerror.NewInvalidArgument("Fairness table is not available on this cluster")
}
taskManager = adh.fairTaskManager
request.MaxTaskId = math.MaxInt64 // required for fairness GetTasks call
} else {
taskManager = adh.taskManager
}
resp, err := taskManager.GetTasks(ctx, &persistence.GetTasksRequest{
NamespaceID: namespaceID.String(),
TaskQueue: request.GetTaskQueue(),
TaskType: request.GetTaskQueueType(),
InclusiveMinTaskID: request.GetMinTaskId(),
ExclusiveMaxTaskID: request.GetMaxTaskId(),
InclusiveMinPass: request.GetMinPass(),
Subqueue: int(request.GetSubqueue()),
PageSize: int(request.GetBatchSize()),
NextPageToken: request.NextPageToken,
})
if err != nil {
return nil, err
}
return &adminservice.GetTaskQueueTasksResponse{
Tasks: resp.Tasks,
NextPageToken: resp.NextPageToken,
}, nil
}
// DescribeTaskQueuePartition returns information for a given task queue partition of the task queue
func (adh *AdminHandler) DescribeTaskQueuePartition(
ctx context.Context,
request *adminservice.DescribeTaskQueuePartitionRequest,
) (_ *adminservice.DescribeTaskQueuePartitionResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
// validate request
if request == nil {
return nil, errRequestNotSet
}
if len(request.Namespace) == 0 {
return nil, errNamespaceNotSet
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
resp, err := adh.matchingClient.DescribeTaskQueuePartition(ctx, &matchingservice.DescribeTaskQueuePartitionRequest{
NamespaceId: namespaceID.String(),
TaskQueuePartition: request.GetTaskQueuePartition(),
Versions: request.GetBuildIds(),
ReportStats: true,
ReportPollers: true,
ReportInternalTaskQueueStatus: true,
})
if err != nil {
return nil, err
}
return &adminservice.DescribeTaskQueuePartitionResponse{
VersionsInfoInternal: resp.VersionsInfoInternal,
}, nil
}
// ForceUnloadTaskQueuePartition forcefully unloads a given task queue partition
func (adh *AdminHandler) ForceUnloadTaskQueuePartition(
ctx context.Context,
request *adminservice.ForceUnloadTaskQueuePartitionRequest,
) (_ *adminservice.ForceUnloadTaskQueuePartitionResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
// validate request
if request == nil {
return nil, errRequestNotSet
}
if len(request.Namespace) == 0 {
return nil, errNamespaceNotSet
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
resp, err := adh.matchingClient.ForceUnloadTaskQueuePartition(ctx, &matchingservice.ForceUnloadTaskQueuePartitionRequest{
NamespaceId: namespaceID.String(),
TaskQueuePartition: request.GetTaskQueuePartition(),
})
if err != nil {
return nil, err
}
// The response returned is for multiple build Id's
return &adminservice.ForceUnloadTaskQueuePartitionResponse{
WasLoaded: resp.WasLoaded,
}, nil
}
func (adh *AdminHandler) DeleteWorkflowExecution(
ctx context.Context,
request *adminservice.DeleteWorkflowExecutionRequest,
) (_ *adminservice.DeleteWorkflowExecutionResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
archetypeID, err := adh.archetypeNameToID(request.GetArchetype())
if err != nil {
return nil, err
}
response, err := adh.historyClient.ForceDeleteWorkflowExecution(ctx,
&historyservice.ForceDeleteWorkflowExecutionRequest{
NamespaceId: namespaceID.String(),
ArchetypeId: archetypeID,
Request: request,
})
if err != nil {
return nil, err
}
return response.Response, nil
}
func (adh *AdminHandler) validateRemoteClusterMetadata(metadata *adminservice.DescribeClusterResponse) error {
// Verify remote cluster config
currentClusterInfo := adh.clusterMetadata
if metadata.GetClusterName() == currentClusterInfo.GetCurrentClusterName() {
// cluster name conflict
return serviceerror.NewInvalidArgument("Cannot update current cluster metadata from rpc calls")
}
if metadata.GetFailoverVersionIncrement() != currentClusterInfo.GetFailoverVersionIncrement() {
// failover version increment is mismatch with current cluster config
return serviceerror.NewInvalidArgument("Cannot add remote cluster due to failover version increment mismatch")
}
if metadata.GetHistoryShardCount() != adh.config.NumHistoryShards {
remoteShardCount := metadata.GetHistoryShardCount()
large := remoteShardCount
small := adh.config.NumHistoryShards
if large < small {
small, large = large, small
}
if large%small != 0 {
return serviceerror.NewInvalidArgument("Remote cluster shard number and local cluster shard number are not multiples.")
}
}
if !metadata.IsGlobalNamespaceEnabled {
// remote cluster doesn't support global namespace
return serviceerror.NewInvalidArgument("Cannot add remote cluster as global namespace is not supported")
}
for clusterName, cluster := range currentClusterInfo.GetAllClusterInfo() {
if clusterName != metadata.ClusterName && cluster.InitialFailoverVersion == metadata.GetInitialFailoverVersion() {
// initial failover version conflict
// best effort: race condition if a concurrent write to db with the same version.
return serviceerror.NewInvalidArgument("Cannot add remote cluster due to initial failover version conflict")
}
}
return nil
}
func (adh *AdminHandler) StreamWorkflowReplicationMessages(
clientCluster adminservice.AdminService_StreamWorkflowReplicationMessagesServer,
) (retError error) {
defer log.CapturePanic(adh.logger, &retError)
_, serverClusterShardID, err := history.DecodeClusterShardMD(headers.NewGRPCHeaderGetter(clientCluster.Context()))
if err != nil {
return err
}
logger := log.With(adh.logger, tag.ShardID(serverClusterShardID.ShardID))
logger.Info("AdminStreamReplicationMessages started.")
defer logger.Info("AdminStreamReplicationMessages stopped.")
historyStreamCtx, cancel := context.WithCancel(clientCluster.Context())
defer cancel()
serverCluster, err := adh.historyClient.StreamWorkflowReplicationMessages(historyStreamCtx)
if err != nil {
return err
}
shutdownChan := channel.NewShutdownOnce()
go func() {
defer func() {
shutdownChan.Shutdown()
err = serverCluster.CloseSend()
if err != nil {
logger.Error("Failed to close AdminStreamReplicationMessages server", tag.Error(err))
}
}()
for !shutdownChan.IsShutdown() {
req, err := clientCluster.Recv()
if err != nil {
logger.Info("AdminStreamReplicationMessages client -> server encountered error", tag.Error(err))
return
}
switch attr := req.GetAttributes().(type) {
case *adminservice.StreamWorkflowReplicationMessagesRequest_SyncReplicationState:
if err = serverCluster.Send(&historyservice.StreamWorkflowReplicationMessagesRequest{
Attributes: &historyservice.StreamWorkflowReplicationMessagesRequest_SyncReplicationState{
SyncReplicationState: attr.SyncReplicationState,
},
}); err != nil {
logger.Info("AdminStreamReplicationMessages client -> server encountered error", tag.Error(err))
return
}
default:
logger.Info("AdminStreamReplicationMessages client -> server encountered error", tag.Error(serviceerror.NewInternalf(
"StreamWorkflowReplicationMessages encountered unknown type: %T %v", attr, attr,
)))
return
}
}
}()
go func() {
defer shutdownChan.Shutdown()
for !shutdownChan.IsShutdown() {
resp, err := serverCluster.Recv()
if err != nil {
logger.Info("AdminStreamReplicationMessages server -> client encountered error", tag.Error(err))
var solErr *serviceerrors.ShardOwnershipLost
var suErr *serviceerror.Unavailable
if errors.As(err, &solErr) || errors.As(err, &suErr) {
ctx, cl := context.WithTimeout(context.Background(), 2*time.Second)
// getShard here to make sure we will talk to correct host when stream is retrying
_, err := adh.historyClient.DescribeHistoryHost(ctx, &historyservice.DescribeHistoryHostRequest{ShardId: serverClusterShardID.ShardID})
if err != nil {
logger.Error("failed to get shard", tag.Error(err))
}
cl()
}
return
}
switch attr := resp.GetAttributes().(type) {
case *historyservice.StreamWorkflowReplicationMessagesResponse_Messages:
if err = clientCluster.Send(&adminservice.StreamWorkflowReplicationMessagesResponse{
Attributes: &adminservice.StreamWorkflowReplicationMessagesResponse_Messages{
Messages: attr.Messages,
},
}); err != nil {
if err != io.EOF {
logger.Info("AdminStreamReplicationMessages server -> client encountered error", tag.Error(err))
}
return
}
default:
logger.Info("AdminStreamReplicationMessages server -> client encountered error", tag.Error(serviceerror.NewInternalf(
"StreamWorkflowReplicationMessages encountered unknown type: %T %v", attr, attr,
)))
return
}
}
}()
<-shutdownChan.Channel()
return nil
}
func (adh *AdminHandler) GetNamespace(ctx context.Context, request *adminservice.GetNamespaceRequest) (_ *adminservice.GetNamespaceResponse, err error) {
defer log.CapturePanic(adh.logger, &err)
if request == nil || (len(request.GetId()) == 0 && len(request.GetNamespace()) == 0) {
return nil, errRequestNotSet
}
req := &persistence.GetNamespaceRequest{
Name: request.GetNamespace(),
ID: request.GetId(),
}
resp, err := adh.persistenceMetadataManager.GetNamespace(ctx, req)
if err != nil {
return nil, err
}
info := resp.Namespace.GetInfo()
nsConfig := resp.Namespace.GetConfig()
replicationConfig := resp.Namespace.GetReplicationConfig()
nsResponse := &adminservice.GetNamespaceResponse{
Info: &namespacepb.NamespaceInfo{
Name: info.Name,
State: info.State,
Description: info.Description,
OwnerEmail: info.Owner,
Data: info.Data,
Id: info.Id,
},
Config: &namespacepb.NamespaceConfig{
WorkflowExecutionRetentionTtl: nsConfig.Retention,
HistoryArchivalState: nsConfig.HistoryArchivalState,
HistoryArchivalUri: nsConfig.HistoryArchivalUri,
VisibilityArchivalState: nsConfig.VisibilityArchivalState,
VisibilityArchivalUri: nsConfig.VisibilityArchivalUri,
BadBinaries: nsConfig.BadBinaries,
CustomSearchAttributeAliases: nsConfig.CustomSearchAttributeAliases,
},
ReplicationConfig: &replicationpb.NamespaceReplicationConfig{
ActiveClusterName: replicationConfig.ActiveClusterName,
Clusters: convertClusterReplicationConfigToProto(replicationConfig.Clusters),
State: replicationConfig.GetState(),
},
ConfigVersion: resp.Namespace.GetConfigVersion(),
FailoverVersion: resp.Namespace.GetFailoverVersion(),
IsGlobalNamespace: resp.IsGlobalNamespace,
FailoverHistory: convertFailoverHistoryToReplicationProto(resp.Namespace.GetReplicationConfig().GetFailoverHistory()),
}
return nsResponse, nil
}
func (adh *AdminHandler) GetDLQTasks(
ctx context.Context,
request *adminservice.GetDLQTasksRequest,
) (*adminservice.GetDLQTasksResponse, error) {
response, err := adh.historyClient.GetDLQTasks(ctx, &historyservice.GetDLQTasksRequest{
DlqKey: request.DlqKey,
PageSize: request.PageSize,
NextPageToken: request.NextPageToken,
})
if err != nil {
return nil, err
}
return &adminservice.GetDLQTasksResponse{
DlqTasks: response.DlqTasks,
NextPageToken: response.NextPageToken,
}, nil
}
func (adh *AdminHandler) PurgeDLQTasks(
ctx context.Context,
request *adminservice.PurgeDLQTasksRequest,
) (*adminservice.PurgeDLQTasksResponse, error) {
if err := validateHistoryDLQKey(request.DlqKey); err != nil {
return nil, err
}
workflowID := adh.getDLQWorkflowID(request.DlqKey)
client := adh.sdkClientFactory.GetSystemClient()
run, err := client.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: workflowID,
TaskQueue: primitives.DefaultWorkerTaskQueue,
}, dlq.WorkflowName, dlq.WorkflowParams{
WorkflowType: dlq.WorkflowTypeDelete,
DeleteParams: dlq.DeleteParams{
Key: dlq.Key{
TaskCategoryID: int(request.DlqKey.TaskCategory),
SourceCluster: request.DlqKey.SourceCluster,
TargetCluster: request.DlqKey.TargetCluster,
},
MaxMessageID: request.InclusiveMaxTaskMetadata.MessageId,
},
})
if err != nil {
return nil, err
}
runID := run.GetRunID()
jobToken := adminservice.DLQJobToken{
WorkflowId: workflowID,
RunId: runID,
}
jobTokenBytes, _ := jobToken.Marshal()
return &adminservice.PurgeDLQTasksResponse{
JobToken: jobTokenBytes,
}, nil
}
func (adh *AdminHandler) MergeDLQTasks(ctx context.Context, request *adminservice.MergeDLQTasksRequest) (*adminservice.MergeDLQTasksResponse, error) {
if err := validateHistoryDLQKey(request.DlqKey); err != nil {
return nil, err
}
workflowID := adh.getDLQWorkflowID(request.DlqKey)
client := adh.sdkClientFactory.GetSystemClient()
run, err := client.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
ID: workflowID,
TaskQueue: primitives.DefaultWorkerTaskQueue,
}, dlq.WorkflowName, dlq.WorkflowParams{
WorkflowType: dlq.WorkflowTypeMerge,
MergeParams: dlq.MergeParams{
Key: dlq.Key{
TaskCategoryID: int(request.DlqKey.TaskCategory),
SourceCluster: request.DlqKey.SourceCluster,
TargetCluster: request.DlqKey.TargetCluster,
},
MaxMessageID: request.InclusiveMaxTaskMetadata.MessageId,
BatchSize: int(request.BatchSize), // Let the workflow code validate and set the default value if needed.
},
})
if err != nil {
return nil, err
}
runID := run.GetRunID()
jobToken := adminservice.DLQJobToken{
WorkflowId: workflowID,
RunId: runID,
}
jobTokenBytes, _ := jobToken.Marshal()
return &adminservice.MergeDLQTasksResponse{
JobToken: jobTokenBytes,
}, nil
}
func (adh *AdminHandler) DescribeDLQJob(ctx context.Context, request *adminservice.DescribeDLQJobRequest) (*adminservice.DescribeDLQJobResponse, error) {
jt := adminservice.DLQJobToken{}
err := jt.Unmarshal([]byte(request.JobToken))
if err != nil {
return nil, fmt.Errorf("%w: %v", errInvalidDLQJobToken, err)
}
client := adh.sdkClientFactory.GetSystemClient()
execution, err := client.DescribeWorkflowExecution(ctx, jt.WorkflowId, jt.RunId)
if err != nil {
return nil, err
}
response, err := client.QueryWorkflow(ctx, jt.WorkflowId, jt.RunId, dlq.QueryTypeProgress)
if err != nil {
return nil, err
}
var queryResponse dlq.ProgressQueryResponse
if err = response.Get(&queryResponse); err != nil {
return nil, err
}
var state enumsspb.DLQOperationState
switch execution.WorkflowExecutionInfo.Status {
case enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING:
state = enumsspb.DLQ_OPERATION_STATE_RUNNING
case enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED:
state = enumsspb.DLQ_OPERATION_STATE_COMPLETED
default:
state = enumsspb.DLQ_OPERATION_STATE_FAILED
}
var opType enumsspb.DLQOperationType
switch queryResponse.WorkflowType {
case dlq.WorkflowTypeDelete:
opType = enumsspb.DLQ_OPERATION_TYPE_PURGE
case dlq.WorkflowTypeMerge:
opType = enumsspb.DLQ_OPERATION_TYPE_MERGE
default:
return nil, serviceerror.NewInternalf("Invalid DLQ workflow type: %v", opType)
}
return &adminservice.DescribeDLQJobResponse{
DlqKey: &commonspb.HistoryDLQKey{
TaskCategory: int32(queryResponse.DlqKey.TaskCategoryID),
SourceCluster: queryResponse.DlqKey.SourceCluster,
TargetCluster: queryResponse.DlqKey.TargetCluster,
},
OperationType: opType,
OperationState: state,
MaxMessageId: queryResponse.MaxMessageIDToProcess,
LastProcessedMessageId: queryResponse.LastProcessedMessageID,
MessagesProcessed: queryResponse.NumberOfMessagesProcessed,
StartTime: execution.WorkflowExecutionInfo.StartTime,
EndTime: execution.WorkflowExecutionInfo.CloseTime,
}, nil
}
func (adh *AdminHandler) CancelDLQJob(ctx context.Context, request *adminservice.CancelDLQJobRequest) (*adminservice.CancelDLQJobResponse, error) {
jt := adminservice.DLQJobToken{}
err := jt.Unmarshal([]byte(request.JobToken))
if err != nil {
return nil, fmt.Errorf("%w: %v", errInvalidDLQJobToken, err)
}
client := adh.sdkClientFactory.GetSystemClient()
execution, err := client.DescribeWorkflowExecution(ctx, jt.WorkflowId, jt.RunId)
if err != nil {
return nil, err
}
if execution.WorkflowExecutionInfo.Status != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
return &adminservice.CancelDLQJobResponse{Canceled: false}, nil
}
err = client.TerminateWorkflow(ctx, jt.WorkflowId, jt.RunId, request.Reason)
if err != nil {
return nil, err
}
return &adminservice.CancelDLQJobResponse{Canceled: true}, nil
}
// AddTasks just translates the admin service's request proto into a history service request proto and then sends it.
func (adh *AdminHandler) AddTasks(
ctx context.Context,
request *adminservice.AddTasksRequest,
) (*adminservice.AddTasksResponse, error) {
historyTasks := make([]*historyservice.AddTasksRequest_Task, len(request.Tasks))
for i, task := range request.Tasks {
historyTasks[i] = &historyservice.AddTasksRequest_Task{
CategoryId: task.CategoryId,
Blob: task.Blob,
}
}
historyServiceRequest := &historyservice.AddTasksRequest{
ShardId: request.ShardId,
Tasks: historyTasks,
}
_, err := adh.historyClient.AddTasks(ctx, historyServiceRequest)
if err != nil {
return nil, err
}
return &adminservice.AddTasksResponse{}, nil
}
func (adh *AdminHandler) ListQueues(
ctx context.Context,
request *adminservice.ListQueuesRequest,
) (*adminservice.ListQueuesResponse, error) {
historyServiceRequest := &historyservice.ListQueuesRequest{
QueueType: request.QueueType,
PageSize: request.PageSize,
NextPageToken: request.NextPageToken,
}
resp, err := adh.historyClient.ListQueues(ctx, historyServiceRequest)
if err != nil {
return nil, err
}
queues := make([]*adminservice.ListQueuesResponse_QueueInfo, len(resp.Queues))
for i, queue := range resp.Queues {
queues[i] = &adminservice.ListQueuesResponse_QueueInfo{
QueueName: queue.QueueName,
MessageCount: queue.MessageCount,
LastMessageId: queue.LastMessageId,
}
}
return &adminservice.ListQueuesResponse{
Queues: queues,
NextPageToken: resp.NextPageToken,
}, nil
}
func (adh *AdminHandler) SyncWorkflowState(ctx context.Context, request *adminservice.SyncWorkflowStateRequest) (_ *adminservice.SyncWorkflowStateResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
res, err := adh.historyClient.SyncWorkflowState(ctx, &historyservice.SyncWorkflowStateRequest{
NamespaceId: request.NamespaceId,
Execution: request.Execution,
VersionHistories: request.VersionHistories,
VersionedTransition: request.VersionedTransition,
TargetClusterId: request.TargetClusterId,
ArchetypeId: request.ArchetypeId,
})
if err != nil {
return nil, err
}
return &adminservice.SyncWorkflowStateResponse{
VersionedTransitionArtifact: res.VersionedTransitionArtifact,
}, nil
}
func (adh *AdminHandler) GenerateLastHistoryReplicationTasks(
ctx context.Context,
request *adminservice.GenerateLastHistoryReplicationTasksRequest,
) (_ *adminservice.GenerateLastHistoryReplicationTasksResponse, retError error) {
defer log.CapturePanic(adh.logger, &retError)
if request == nil {
return nil, errRequestNotSet
}
if err := validateExecution(request.Execution); err != nil {
return nil, err
}
namespaceEntry, err := adh.namespaceRegistry.GetNamespace(namespace.Name(request.GetNamespace()))
if err != nil {
return nil, err
}
archetypeID, err := adh.archetypeNameToID(request.GetArchetype())
if err != nil {
return nil, err
}
resp, err := adh.historyClient.GenerateLastHistoryReplicationTasks(
ctx,
&historyservice.GenerateLastHistoryReplicationTasksRequest{
NamespaceId: namespaceEntry.ID().String(),
Execution: request.Execution,
TargetClusters: request.TargetClusters,
ArchetypeId: archetypeID,
},
)
if err != nil {
return nil, err
}
return &adminservice.GenerateLastHistoryReplicationTasksResponse{
StateTransitionCount: resp.StateTransitionCount,
HistoryLength: resp.HistoryLength,
}, nil
}
func (adh *AdminHandler) getDLQWorkflowID(
key *commonspb.HistoryDLQKey,
) string {
return fmt.Sprintf(
"manage-dlq-tasks-%s",
persistence.GetHistoryTaskQueueName(
int(key.TaskCategory),
key.SourceCluster,
key.TargetCluster,
),
)
}
func (adh *AdminHandler) archetypeNameToID(archetype chasm.Archetype) (chasm.ArchetypeID, error) {
if len(archetype) == 0 {
// For backwards compatibility, default to Workflow
return chasm.WorkflowArchetypeID, nil
}
archetypeID, ok := adh.chasmRegistry.ComponentIDByFqn(archetype)
if !ok {
return chasm.UnspecifiedArchetypeID, serviceerror.NewInvalidArgumentf("unknown archetype: %s", archetype)
}
return archetypeID, nil
}
func validateHistoryDLQKey(
key *commonspb.HistoryDLQKey,
) error {
if len(key.SourceCluster) == 0 {
return errSourceClusterNotSet
}
if len(key.TargetCluster) == 0 {
return errTargetClusterNotSet
}
// history service is responsible for validating
// categoryID using task category registry
return nil
}
func convertClusterReplicationConfigToProto(
input []string,
) []*replicationpb.ClusterReplicationConfig {
output := make([]*replicationpb.ClusterReplicationConfig, 0, len(input))
for _, clusterName := range input {
output = append(output, &replicationpb.ClusterReplicationConfig{ClusterName: clusterName})
}
return output
}
func convertFailoverHistoryToReplicationProto(
failoverHistoy []*persistencespb.FailoverStatus,
) []*replicationpb.FailoverStatus {
var replicationProto []*replicationpb.FailoverStatus
for _, failoverStatus := range failoverHistoy {
replicationProto = append(replicationProto, &replicationpb.FailoverStatus{
FailoverTime: failoverStatus.GetFailoverTime(),
FailoverVersion: failoverStatus.GetFailoverVersion(),
})
}
return replicationProto
}