diff --git a/go.mod b/go.mod index 75eb2b27f..f10008343 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,12 @@ module github.com/temporalio/cli -go 1.26.3 +go 1.26.4 + +replace ( + go.temporal.io/api => ../api-go-saa-batch-cmds + go.temporal.io/sdk => ../sdk-go + go.temporal.io/server => ../orig-temporal +) require ( github.com/BurntSushi/toml v1.4.0 @@ -17,13 +23,13 @@ require ( github.com/stretchr/testify v1.11.1 github.com/temporalio/cli/cliext v0.0.0 github.com/temporalio/ui-server/v2 v2.49.1 - go.temporal.io/api v1.62.13 + go.temporal.io/api v1.62.15-0.20260615235047-378792ab2240 go.temporal.io/sdk v1.44.1 go.temporal.io/sdk/contrib/envconfig v1.0.2 go.temporal.io/server v1.32.0-157.0 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mod v0.35.0 - golang.org/x/term v0.42.0 + golang.org/x/term v0.43.0 golang.org/x/tools v0.44.0 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 @@ -201,12 +207,12 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/api v0.276.0 // indirect google.golang.org/genproto v0.0.0-20260420184626-e10c466a9529 // indirect diff --git a/go.sum b/go.sum index 545168789..89a9d5d08 100644 --- a/go.sum +++ b/go.sum @@ -469,16 +469,10 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.temporal.io/api v1.62.13 h1:xMa8Nt5oAMX+LvlCJA44wjTCc1H09i2rG9poB1/xvH4= -go.temporal.io/api v1.62.13/go.mod h1:0k75tRljEuELWGeXjEZZO7zYqBln4+1FrG6+IMOMy7Q= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= -go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM= -go.temporal.io/sdk v1.44.1/go.mod h1:vkApR12F9/Y8OR+hkxe7WyXQFuCX6clhzqnAk6rzDAM= go.temporal.io/sdk/contrib/envconfig v1.0.2 h1:MGHfsuPUtsf7X9M6WYn3zYJj/mWsuYHnA1uuiL0KEuE= go.temporal.io/sdk/contrib/envconfig v1.0.2/go.mod h1:MuMiH7hksps2uXnmKuAWaP9P6WbkSDy62kl64t1VJVg= -go.temporal.io/server v1.32.0-157.0 h1:nzFqNwx+5lXsT0/DSiFyR5vHMnDcT3PVAvmRDqCUn38= -go.temporal.io/server v1.32.0-157.0/go.mod h1:a76wf30/s28JXh+3nDQtQi8KzOfRQEddpebvmr/oQL4= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -509,8 +503,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -543,8 +537,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -571,15 +565,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -587,8 +581,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/temporalcli/commands.activity.go b/internal/temporalcli/commands.activity.go index 582f5d622..328e05f39 100644 --- a/internal/temporalcli/commands.activity.go +++ b/internal/temporalcli/commands.activity.go @@ -8,6 +8,7 @@ import ( "time" "github.com/fatih/color" + "github.com/google/uuid" "github.com/temporalio/cli/internal/printer" activitypb "go.temporal.io/api/activity/v1" "go.temporal.io/api/batch/v1" @@ -40,6 +41,8 @@ type ( } ) +const activityDeleteWarning = "WARNING: Deleting Standalone Activity Executions in a global Namespace removes them from all replicas. Requests sent to a passive cluster are forwarded to the active cluster by default; to target the passive cluster directly, specify `--grpc-meta xdc-redirection=false`." + func (c *TemporalActivityStartCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { @@ -525,6 +528,65 @@ func (c *TemporalActivityCountCommand) run(cctx *CommandContext, args []string) return nil } +func (s *ActivityReferenceOrBatchOptions) activityExecOrBatch( + cctx *CommandContext, + namespace string, + cl client.Client, + yesFlag bool, + overrides singleOrBatchOverrides, +) (*client.GetActivityHandleOptions, *workflowservice.StartBatchOperationRequest, error) { + // If activity is set, we return activity handle options with activity ID and run ID + if s.ActivityId != "" { + if s.Query != "" { + return nil, nil, fmt.Errorf("cannot set query when activity ID is set") + } else if yesFlag && !overrides.AllowYesWithActivityID { + return nil, nil, fmt.Errorf("cannot set 'yes' when activity ID is set") + } else if s.Rps != 0 { + return nil, nil, fmt.Errorf("cannot set rps when activity ID is set") + } + return &client.GetActivityHandleOptions{ + ActivityID: s.ActivityId, + RunID: s.RunId, + }, nil, nil + } + + // Check query is set properly + if s.Query == "" { + return nil, nil, fmt.Errorf("must set either activity ID or query") + } else if s.ActivityId != "" { // This is redundant, but kept for completeness + return nil, nil, fmt.Errorf("cannot set activity ID when query is set") + } else if s.RunId != "" { + return nil, nil, fmt.Errorf("cannot set run ID when query is set") + } + + // The count is only used in the confirmation prompt; skip the request when --yes + // bypasses it, so batch jobs can still proceed if the visibility API is timing out. + var promptMessage string + if yesFlag { + promptMessage = fmt.Sprintf("Start batch against standalone activities matching query %q? y/N", s.Query) + } else { + count, err := cl.CountActivities(cctx, client.CountActivitiesOptions{Query: s.Query}) + if err != nil { + return nil, nil, fmt.Errorf("failed counting standalone activities from query: %w", err) + } + promptMessage = fmt.Sprintf("Start batch against approximately %v standalone activities(s)? y/N", count.Count) + } + isYes, err := cctx.promptYes(promptMessage, yesFlag) + if err != nil { + return nil, nil, err + } else if !isYes { + // We consider this a command failure + return nil, nil, fmt.Errorf("user denied confirmation") + } + + return nil, &workflowservice.StartBatchOperationRequest{ + MaxOperationsPerSecond: s.Rps, + Namespace: namespace, + JobId: uuid.NewString(), + VisibilityQuery: s.Query, + }, nil +} + func (c *TemporalActivityCancelCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { @@ -532,14 +594,48 @@ func (c *TemporalActivityCancelCommand) run(cctx *CommandContext, args []string) } defer cl.Close() - handle := cl.GetActivityHandle(client.GetActivityHandleOptions{ - ActivityID: c.ActivityId, - RunID: c.RunId, - }) - if err := handle.Cancel(cctx, client.CancelActivityOptions{Reason: c.Reason}); err != nil { - return fmt.Errorf("failed to request activity cancellation: %w", err) + opts := ActivityReferenceOrBatchOptions{ + ActivityId: c.ActivityId, + RunId: c.RunId, + Query: c.Query, + Rps: c.Rps, + } + + // TODO: should --yes be required if -o json or jsonl is used? + + activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{}) + if err != nil { + return err + } + + if activityOptions != nil { + handle := cl.GetActivityHandle(*activityOptions) + if err := handle.Cancel(cctx, client.CancelActivityOptions{Reason: c.Reason}); err != nil { + return fmt.Errorf("failed to request activity cancellation: %w", err) + } + cctx.Printer.Println("Cancellation requested") + } else { // batchReq != nil + cancelActivitiesOperation := &batch.BatchOperationCancelActivities{ + Identity: c.Parent.Identity, + // do not fallback to defaultReason, to be consistent with single activity cancel + Reason: c.Reason, + } + + batchReq.Operation = &workflowservice.StartBatchOperationRequest_CancelActivitiesOperation{ + CancelActivitiesOperation: cancelActivitiesOperation, + } + + // Reason in batch request falls back to defaultReason + if c.Reason != "" { + batchReq.Reason = c.Reason + } else { + batchReq.Reason = defaultReason() + } + + if err := startBatchJob(cctx, cl, batchReq); err != nil { + return err + } } - cctx.Printer.Println("Cancellation requested") return nil } @@ -550,24 +646,128 @@ func (c *TemporalActivityTerminateCommand) run(cctx *CommandContext, args []stri } defer cl.Close() - // The CLI adds a default for terminate but not cancel. - // This matches the behavior for workflows. + opts := ActivityReferenceOrBatchOptions{ + ActivityId: c.ActivityId, + RunId: c.RunId, + Query: c.Query, + Rps: c.Rps, + } + + activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{}) + if err != nil { + return err + } + + // Reason for single terminate or batch request falls back to defaultReason reason := c.Reason if reason == "" { reason = defaultReason() } - handle := cl.GetActivityHandle(client.GetActivityHandleOptions{ - ActivityID: c.ActivityId, - RunID: c.RunId, + + if activityOptions != nil { + // The CLI adds a default for terminate but not cancel. + // This matches the behavior for workflows. + handle := cl.GetActivityHandle(*activityOptions) + // Terminate may fail if the activity doesn't exist or has already completed. + if err := handle.Terminate(cctx, client.TerminateActivityOptions{Reason: reason}); err != nil { + return fmt.Errorf("failed to terminate activity: %w", err) + } + cctx.Printer.Println("Activity terminated") + } else { // batchReq != nil + terminateActivitiesOperation := &batch.BatchOperationTerminateActivities{ + Identity: c.Parent.Identity, + Reason: reason, + } + + batchReq.Reason = reason + + batchReq.Operation = &workflowservice.StartBatchOperationRequest_TerminateActivitiesOperation{ + TerminateActivitiesOperation: terminateActivitiesOperation, + } + + if err := startBatchJob(cctx, cl, batchReq); err != nil { + return err + } + } + + return nil +} + +func (c *TemporalActivityDeleteCommand) run(cctx *CommandContext, args []string) error { + cl, err := dialClient(cctx, &c.Parent.ClientOptions) + if err != nil { + return err + } + defer cl.Close() + + // TODO: do we need this warning, similar to workflow delete? + // Only warn when the namespace is global, or can't get the namespace info + nsResp, nsErr := cl.WorkflowService().DescribeNamespace(cctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: c.Parent.Namespace, }) - // Terminate may fail if the activity doesn't exist or has already completed. - if err := handle.Terminate(cctx, client.TerminateActivityOptions{Reason: reason}); err != nil { - return fmt.Errorf("failed to terminate activity: %w", err) + if nsErr != nil || nsResp.GetIsGlobalNamespace() { + fmt.Fprintln(cctx.Options.Stderr, activityDeleteWarning) } - cctx.Printer.Println("Activity terminated") + + opts := ActivityReferenceOrBatchOptions{ + ActivityId: c.ActivityId, + RunId: c.RunId, + Query: c.Query, + Rps: c.Rps, + } + + activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{ + AllowYesWithActivityID: true, + }) + if err != nil { + return err + } + + if activityOptions != nil { + yes, err := cctx.promptYes(activityDeleteSingleConfirmationMessage(activityOptions), c.Yes) + if err != nil { + return err + } else if !yes { + return fmt.Errorf("user denied confirmation") + } + _, err = cl.WorkflowService().DeleteActivityExecution(cctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: c.Parent.Namespace, + ActivityId: c.ActivityId, + RunId: c.RunId, + }) + if err != nil { + return fmt.Errorf("failed to delete standalone activity: %w", err) + } + cctx.Printer.Println("Delete activity succeeded") + } else { // batchReq != nil + deleteActivitiesOperation := &batch.BatchOperationDeleteActivities{} + + if c.Reason != "" { + batchReq.Reason = c.Reason + } else { + batchReq.Reason = defaultReason() + } + + batchReq.Operation = &workflowservice.StartBatchOperationRequest_DeleteActivitiesOperation{ + DeleteActivitiesOperation: deleteActivitiesOperation, + } + + if err := startBatchJob(cctx, cl, batchReq); err != nil { + return err + } + } + return nil } +func activityDeleteSingleConfirmationMessage(activityOptions *client.GetActivityHandleOptions) string { + action := fmt.Sprintf("Delete Standalone Activity %q", activityOptions.ActivityID) + if activityOptions.RunID != "" { + action += fmt.Sprintf(" with Run ID %q", activityOptions.RunID) + } + return fmt.Sprintf("%s? y/N", action) +} + func (c *TemporalActivityCompleteCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { diff --git a/internal/temporalcli/commands.activity_test.go b/internal/temporalcli/commands.activity_test.go index ec6b50a4f..13998169f 100644 --- a/internal/temporalcli/commands.activity_test.go +++ b/internal/temporalcli/commands.activity_test.go @@ -14,6 +14,7 @@ import ( "go.temporal.io/api/history/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/activity" "go.temporal.io/sdk/client" "go.temporal.io/sdk/converter" "go.temporal.io/sdk/temporal" @@ -1167,6 +1168,38 @@ func (s *SharedServerSuite) TestActivity_Terminate() { s.Contains(err.Error(), "terminated") } +func (s *SharedServerSuite) TestActivity_Delete_Success() { + activityStarted := make(chan struct{}) + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + close(activityStarted) + <-ctx.Done() + return nil, ctx.Err() + }) + + started := s.startActivity("delete-test") + runID := started["runId"].(string) + <-activityStarted + + res := s.Execute( + "activity", "delete", + "--activity-id", "delete-test", + "--run-id", runID, + "--address", s.Address(), + "-y", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Delete activity succeeded") + + s.Eventually(func() bool { + handle := s.Client.GetActivityHandle(client.GetActivityHandleOptions{ + ActivityID: "delete-test", + RunID: runID, + }) + err := handle.Get(s.Context, nil) + return err != nil && strings.Contains(err.Error(), "activity not found") + }, 5*time.Second, 200*time.Millisecond) +} + func (s *SharedServerSuite) TestActivity_SearchAttributes() { s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { return nil, nil @@ -1369,3 +1402,308 @@ func (s *SharedServerSuite) TestActivity_Terminate_DefaultReason_NoUnknownUser() s.Contains(failureMsg, "Requested from CLI by") s.NotContains(failureMsg, "") } + +// batch operators (cancel, terminate, delete) on standalone activities +func (s *SharedServerSuite) TestActivity_CancelTerminateDelete_BatchSuccess() { + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + // don't complete the activity + return nil, activity.ErrResultPending + }) + + for _, operator := range []string{"cancel", "terminate", "delete"} { + uniqueKW := operator + "-" + uuid.NewString()[:8] + activityIds := make([]string, 0, 5) + for i := 0; i < 5; i++ { + activityId := fmt.Sprintf("%s-test-%d", operator, i) + activityIds = append(activityIds, activityId) + s.startActivity(activityId, + "--search-attribute", fmt.Sprintf(`CustomKeywordField="%s"`, uniqueKW), + ) + } + + // Wait for all to be visible + s.Eventually(func() bool { + res := s.Execute( + "activity", "list", + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s" AND ExecutionStatus = "Running"`, uniqueKW), + ) + return res.Err == nil && strings.Count(res.Stdout.String(), operator+"-test-") >= 5 + }, 5*time.Second, 200*time.Millisecond) + + // Send cancel, terminate or delete + reason := "test batch " + operator + res := s.Execute( + "activity", operator, + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s"`, uniqueKW), + "--reason", reason, + "-y", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Started batch") + + // get job ID for later check. + lines := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n") + s.Equal(2, len(lines), "expected one success line") + parts := strings.Split(lines[1], ":") + s.Equal(2, len(parts), "expected success line to contain job ID") + jobId := strings.TrimSpace(parts[1]) + + switch operator { + case "cancel": + // Wait for all to be canceled + s.Eventually(func() bool { + count := 0 + for _, activityId := range activityIds { + handle := s.Client.GetActivityHandle(client.GetActivityHandleOptions{ + ActivityID: activityId, + }) + desc, err := handle.Describe(s.Context, client.DescribeActivityOptions{}) + if err == nil && desc.RunState.String() == "CancelRequested" { + count++ + } + } + return count >= 5 + }, 5*time.Second, 200*time.Millisecond) + + case "terminate": + // Wait for all to be terminated + s.Eventually(func() bool { + res := s.Execute( + "activity", "list", + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s" AND ExecutionStatus = "Terminated"`, uniqueKW), + ) + return res.Err == nil && strings.Count(res.Stdout.String(), operator+"-test-") >= 5 + }, 5*time.Second, 200*time.Millisecond) + + // //TODO: is it better to use list above or handle below? + // s.Eventually(func() bool { + // count := 0 + // for _, activityId := range activityIds { + // handle := s.Client.GetActivityHandle(client.GetActivityHandleOptions{ + // ActivityID: activityId, + // }) + // err := handle.Get(s.Context, nil) + // if err != nil && strings.Contains(err.Error(), "terminated") { + // count++ + // } + // } + // return count >= 5 + // }, 5*time.Second, 200*time.Millisecond) + + case "delete": + // Wait for all to be deleted + s.Eventually(func() bool { + res := s.Execute( + "activity", "list", + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s"`, uniqueKW), + ) + return res.Err == nil && strings.Count(res.Stdout.String(), operator+"-test-") == 0 + }, 5*time.Second, 200*time.Millisecond) + } + + // check batch job has no failure + res = s.Execute( + "batch", "describe", + "--address", s.Address(), + "--job-id", jobId, + ) + s.NoError(res.Err) + out := res.Stdout.String() + s.ContainsOnSameLine(out, "CompletedCount", "5/5") + s.ContainsOnSameLine(out, "FailureCount", "0/5") + + // check for reason + res = s.Execute( + "batch", "describe", + "--address", s.Address(), + "--job-id", jobId, + "-o", "json", + ) + s.NoError(res.Err) + var jsonOut map[string]any + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(reason, jsonOut["reason"]) + } +} + +func (s *SharedServerSuite) TestActivity_CancelTerminateDelete_BatchFailed() { + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + // don't complete the activity + return nil, activity.ErrResultPending + }) + + for _, operator := range []string{"cancel", "terminate", "delete"} { + uniqueKW := operator + "-test-fail-" + uuid.NewString()[:8] + activityId := operator + "-test-fail" + started := s.startActivity(activityId, + "--search-attribute", fmt.Sprintf(`CustomKeywordField="%s"`, uniqueKW), + ) + runId := started["runId"].(string) + + query := fmt.Sprintf(`CustomKeywordField = "%s"`, uniqueKW) + res := s.Execute( + "activity", operator, + "--address", s.Address(), + "--activity-id", activityId, + "--query", query, + "-y", + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "cannot set query when activity ID is set") + + res = s.Execute( + "activity", operator, + "--address", s.Address(), + "--run-id", runId, + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "must set either activity ID or query") + + res = s.Execute( + "activity", operator, + "--address", s.Address(), + "--activity-id", activityId, + "--run-id", runId, + "--query", query, + "-y", + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "cannot set query when activity ID is set") + + res = s.Execute( + "activity", operator, + "--address", s.Address(), + "--activity-id", activityId, + "--rps", "10", + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "cannot set rps when activity ID is set") + + res = s.Execute( + "activity", operator, + "--address", s.Address(), + "--run-id", runId, + "--query", query, + "-y", + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "cannot set run ID when query is set") + + if operator != "delete" { + res = s.Execute( + "activity", operator, + "--address", s.Address(), + "--activity-id", activityId, + "-y", + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "cannot set 'yes' when activity ID is set") + } + } +} + +func (s *SharedServerSuite) TestActivity_CancelTerminateDelete_BatchDefaultReason() { + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + // don't complete the activity + return nil, activity.ErrResultPending + }) + + for _, operator := range []string{"cancel", "terminate", "delete"} { + // Send cancel, terminate or delete with default reason + res := s.Execute( + "activity", operator, + "--address", s.Address(), + "--query", `CustomKeywordField = "UnknownValue"`, + "-y", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Started batch") + + // get job ID for later check. + lines := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n") + s.Equal(2, len(lines), "expected two lines, confirmation and success message") + parts := strings.Split(lines[1], ":") + s.Equal(2, len(parts), "expected success line to contain job ID") + jobId := strings.TrimSpace(parts[1]) + + // check for reason + res = s.Execute( + "batch", "describe", + "--address", s.Address(), + "--job-id", jobId, + "-o", "json", + ) + s.NoError(res.Err) + var jsonOut map[string]any + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Contains(jsonOut["reason"], "Requested from CLI by") + } +} + +func (s *SharedServerSuite) TestActivity_CancelTerminateDelete_BatchRateLimit() { + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + // don't complete the activity + return nil, activity.ErrResultPending + }) + + // following borrowed from testTerminateBatchWorkflow to intercept batch request + var lastRequestLock sync.Mutex + var startBatchRequest *workflowservice.StartBatchOperationRequest + s.CommandHarness.Options.AdditionalClientGRPCDialOptions = append( + s.CommandHarness.Options.AdditionalClientGRPCDialOptions, + grpc.WithChainUnaryInterceptor(func( + ctx context.Context, + method string, req, reply any, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, + ) error { + lastRequestLock.Lock() + if r, ok := req.(*workflowservice.StartBatchOperationRequest); ok { + startBatchRequest = r + } + lastRequestLock.Unlock() + return invoker(ctx, method, req, reply, cc, opts...) + }), + ) + + for _, operator := range []string{"cancel", "terminate", "delete"} { + uniqueKW := operator + "-rps-" + uuid.NewString()[:8] + iterations := 2 + activityIds := make([]string, 0, iterations) + for i := 0; i < iterations; i++ { + activityId := fmt.Sprintf("%s-test-rps-%d", operator, i) + activityIds = append(activityIds, activityId) + s.startActivity(activityId, + "--search-attribute", fmt.Sprintf(`CustomKeywordField="%s"`, uniqueKW), + ) + } + + // Wait for all to be visible + s.Eventually(func() bool { + res := s.Execute( + "activity", "list", + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s" AND ExecutionStatus = "Running"`, uniqueKW), + ) + return res.Err == nil && strings.Count(res.Stdout.String(), operator+"-test-") >= iterations + }, 5*time.Second, 200*time.Millisecond) + + // Send cancel, terminate or delete + var rps float32 = 1 + s.CommandHarness.Stdin.WriteString("y\n") + res := s.Execute( + "activity", operator, + "--address", s.Address(), + "--query", fmt.Sprintf(`CustomKeywordField = "%s"`, uniqueKW), + "--rps", fmt.Sprint(rps), + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Started batch") + + s.NotNil(startBatchRequest) + s.Equal(rps, startBatchRequest.MaxOperationsPerSecond) + } +} diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 42c17e7d4..ec8cd56c4 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -151,6 +151,22 @@ func (v *DeploymentReferenceOptions) BuildFlags(f *pflag.FlagSet) { _ = cobra.MarkFlagRequired(f, "build-id") } +type ActivityReferenceOrBatchOptions struct { + ActivityId string + RunId string + Query string + Rps float32 + FlagSet *pflag.FlagSet +} + +func (v *ActivityReferenceOrBatchOptions) BuildFlags(f *pflag.FlagSet) { + v.FlagSet = f + f.StringVarP(&v.ActivityId, "activity-id", "a", "", "Activity ID. You must set either --activity-id or --query.") + f.StringVarP(&v.RunId, "run-id", "r", "", "Activity Run ID. If not set, targets the latest run. Only use with --activity-id. Cannot use with --query.") + f.StringVarP(&v.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter. You must set either --activity-id or --query. Note: Using --query for batch activity operations is an experimental feature and may change in the future.") + f.Float32Var(&v.Rps, "rps", 0, "Limit batch's requests per second. Only allowed when --query is present.") +} + type SingleActivityOrBatchOptions struct { WorkflowId string Query string @@ -541,6 +557,7 @@ func NewTemporalActivityCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalActivityCancelCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityCompleteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityCountCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalActivityDeleteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityExecuteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityFailCommand(cctx, &s).Command) @@ -559,8 +576,9 @@ func NewTemporalActivityCommand(cctx *CommandContext, parent *TemporalCommand) * type TemporalActivityCancelCommand struct { Parent *TemporalActivityCommand Command cobra.Command - ActivityReferenceOptions + ActivityReferenceOrBatchOptions Reason string + Yes bool } func NewTemporalActivityCancelCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityCancelCommand { @@ -570,13 +588,14 @@ func NewTemporalActivityCancelCommand(cctx *CommandContext, parent *TemporalActi s.Command.Use = "cancel [flags]" s.Command.Short = "Request cancellation of a Standalone Activity (Experimental)" if hasHighlighting { - s.Command.Long = "Request cancellation of a Standalone Activity.\n\n\x1b[1mtemporal activity cancel \\\n --activity-id YourActivityId\x1b[0m\n\nRequesting cancellation transitions the Activity's run state\nto CancelRequested. If the Activity is heartbeating, a\ncancellation error will be raised when the next heartbeat\nresponse is received; if the Activity allows this error to\npropagate, the Activity transitions to canceled status." + s.Command.Long = "Request cancellation of a Standalone Activity.\n\n\x1b[1mtemporal activity cancel \\\n --activity-id YourActivityId\x1b[0m\n\nRequesting cancellation transitions the Activity's run state\nto CancelRequested. If the Activity is heartbeating, a\ncancellation error will be raised when the next heartbeat\nresponse is received; if the Activity allows this error to\npropagate, the Activity transitions to canceled status.\n\nA visibility Query lets you send bulk cancellations to Standalone Activity\nExecutions matching the results:\n\n\x1b[1mtemporal activity cancel \\\n --query YourQuery \\\n --reason YourReason\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference." } else { - s.Command.Long = "Request cancellation of a Standalone Activity.\n\n```\ntemporal activity cancel \\\n --activity-id YourActivityId\n```\n\nRequesting cancellation transitions the Activity's run state\nto CancelRequested. If the Activity is heartbeating, a\ncancellation error will be raised when the next heartbeat\nresponse is received; if the Activity allows this error to\npropagate, the Activity transitions to canceled status." + s.Command.Long = "Request cancellation of a Standalone Activity.\n\n```\ntemporal activity cancel \\\n --activity-id YourActivityId\n```\n\nRequesting cancellation transitions the Activity's run state\nto CancelRequested. If the Activity is heartbeating, a\ncancellation error will be raised when the next heartbeat\nresponse is received; if the Activity allows this error to\npropagate, the Activity transitions to canceled status.\n\nA visibility Query lets you send bulk cancellations to Standalone Activity\nExecutions matching the results:\n\n```\ntemporal activity cancel \\\n --query YourQuery \\\n --reason YourReason\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference." } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for cancellation.") - s.ActivityReferenceOptions.BuildFlags(s.Command.Flags()) + s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for cancellation. Also used as reason for batch operation with --query, which defaults to a message with the current user's name.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm. Only allowed when --query is present.") + s.ActivityReferenceOrBatchOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -647,6 +666,37 @@ func NewTemporalActivityCountCommand(cctx *CommandContext, parent *TemporalActiv return &s } +type TemporalActivityDeleteCommand struct { + Parent *TemporalActivityCommand + Command cobra.Command + ActivityReferenceOrBatchOptions + Reason string + Yes bool +} + +func NewTemporalActivityDeleteCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityDeleteCommand { + var s TemporalActivityDeleteCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "delete [flags]" + s.Command.Short = "Delete a Standalone Activity Execution (Experimental)" + if hasHighlighting { + s.Command.Long = "Delete a Standalone Activity Execution and its Event History.\n\n\x1b[1mtemporal activity delete \\\n --activity-id YourActivityId\x1b[0m\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nA visibility Query lets you send bulk delete to Standalone Activity\nExecutions matching the results:\n\n\x1b[1mtemporal activity delete \\\n --query YourQuery\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference." + } else { + s.Command.Long = "Delete a Standalone Activity Execution and its Event History.\n\n```\ntemporal activity delete \\\n --activity-id YourActivityId\n```\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nA visibility Query lets you send bulk delete to Standalone Activity\nExecutions matching the results:\n\n```\ntemporal activity delete \\\n --query YourQuery\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for batch operation. Only use with --query. Defaults to a message with the current user's name.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm.") + s.ActivityReferenceOrBatchOptions.BuildFlags(s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalActivityDescribeCommand struct { Parent *TemporalActivityCommand Command cobra.Command @@ -904,8 +954,9 @@ func NewTemporalActivityStartCommand(cctx *CommandContext, parent *TemporalActiv type TemporalActivityTerminateCommand struct { Parent *TemporalActivityCommand Command cobra.Command - ActivityReferenceOptions + ActivityReferenceOrBatchOptions Reason string + Yes bool } func NewTemporalActivityTerminateCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityTerminateCommand { @@ -915,13 +966,14 @@ func NewTemporalActivityTerminateCommand(cctx *CommandContext, parent *TemporalA s.Command.Use = "terminate [flags]" s.Command.Short = "Forcefully end a Standalone Activity (Experimental)" if hasHighlighting { - s.Command.Long = "Terminate a Standalone Activity.\n\n\x1b[1mtemporal activity terminate \\\n --activity-id YourActivityId \\\n --reason YourReason\x1b[0m\n\nActivity code cannot see or respond to terminations." + s.Command.Long = "Terminate a Standalone Activity.\n\n\x1b[1mtemporal activity terminate \\\n --activity-id YourActivityId \\\n --reason YourReason\x1b[0m\n\nActivity code cannot see or respond to terminations.\n\nA visibility Query lets you send bulk terminations to Standalone Activity\nExecutions matching the results:\n\n\x1b[1mtemporal activity terminate \\\n --query YourQuery \\\n --reason YourReason\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference." } else { - s.Command.Long = "Terminate a Standalone Activity.\n\n```\ntemporal activity terminate \\\n --activity-id YourActivityId \\\n --reason YourReason\n```\n\nActivity code cannot see or respond to terminations." + s.Command.Long = "Terminate a Standalone Activity.\n\n```\ntemporal activity terminate \\\n --activity-id YourActivityId \\\n --reason YourReason\n```\n\nActivity code cannot see or respond to terminations.\n\nA visibility Query lets you send bulk terminations to Standalone Activity\nExecutions matching the results:\n\n```\ntemporal activity terminate \\\n --query YourQuery \\\n --reason YourReason\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference." } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for termination. Defaults to a message with the current user's name.") - s.ActivityReferenceOptions.BuildFlags(s.Command.Flags()) + s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for termination. Defaults to a message with the current user's name. Also used as reason for batch operation with --query.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm. Only allowed when --query is present.") + s.ActivityReferenceOrBatchOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) diff --git a/internal/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go index e03047714..0927553f6 100644 --- a/internal/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -538,6 +538,7 @@ func defaultReason() string { type singleOrBatchOverrides struct { AllowReasonWithWorkflowID bool AllowYesWithWorkflowID bool + AllowYesWithActivityID bool } func (s *SingleWorkflowOrBatchOptions) workflowExecOrBatch( diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index bc655419a..ef560c7d1 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -193,12 +193,33 @@ commands: cancellation error will be raised when the next heartbeat response is received; if the Activity allows this error to propagate, the Activity transitions to canceled status. + + A visibility Query lets you send bulk cancellations to Standalone Activity + Executions matching the results: + + ``` + temporal activity cancel \ + --query YourQuery \ + --reason YourReason + ``` + + Visit https://docs.temporal.io/visibility to read more about Search Attributes + and Query creation. See `temporal batch --help` for a quick reference. option-sets: - - activity-reference + - activity-reference-or-batch options: - name: reason type: string - description: Reason for cancellation. + description: | + Reason for cancellation. + Also used as reason for batch operation with --query, + which defaults to a message with the current user's name. + - name: yes + type: bool + short: y + description: | + Don't prompt to confirm. + Only allowed when --query is present. - name: temporal activity complete summary: Mark an activity as completed successfully with a result @@ -671,14 +692,71 @@ commands: ``` Activity code cannot see or respond to terminations. + + A visibility Query lets you send bulk terminations to Standalone Activity + Executions matching the results: + + ``` + temporal activity terminate \ + --query YourQuery \ + --reason YourReason + ``` + + Visit https://docs.temporal.io/visibility to read more about Search Attributes + and Query creation. See `temporal batch --help` for a quick reference. option-sets: - - activity-reference + - activity-reference-or-batch options: - name: reason type: string description: | Reason for termination. Defaults to a message with the current user's name. + Also used as reason for batch operation with --query. + - name: yes + type: bool + short: y + description: | + Don't prompt to confirm. + Only allowed when --query is present. + + - name: temporal activity delete + summary: Delete a Standalone Activity Execution (Experimental) + description: | + Delete a Standalone Activity Execution and its Event History. + + ``` + temporal activity delete \ + --activity-id YourActivityId + ``` + + The removal executes asynchronously. If the Execution is Running, the Service + terminates it before deletion. + + A visibility Query lets you send bulk delete to Standalone Activity + Executions matching the results: + + ``` + temporal activity delete \ + --query YourQuery + ``` + + Visit https://docs.temporal.io/visibility to read more about Search Attributes + and Query creation. See `temporal batch --help` for a quick reference. + option-sets: + - activity-reference-or-batch + options: + - name: reason + type: string + description: | + Reason for batch operation. + Only use with --query. + Defaults to a message with the current user's name. + - name: yes + type: bool + short: y + description: | + Don't prompt to confirm. - name: temporal batch summary: Manage running batch jobs @@ -4932,6 +5010,36 @@ option-sets: description: Build ID for a Worker Deployment. required: true + # Combines activity-reference and --query, with an experimental note. + # Cannot extend activity-reference, because activity-id is not required. + - name: activity-reference-or-batch + options: + - name: activity-id + type: string + short: a + description: | + Activity ID. + You must set either --activity-id or --query. + - name: run-id + type: string + short: r + description: | + Activity Run ID. + If not set, targets the latest run. + Only use with --activity-id. Cannot use with --query. + - name: query + type: string + short: q + description: | + Content for an SQL-like `QUERY` List Filter. + You must set either --activity-id or --query. + Note: Using --query for batch activity operations is an experimental feature and may change in the future. + - name: rps + type: float + description: | + Limit batch's requests per second. + Only allowed when --query is present. + # Duplicate of single-workflow-or-batch with an experimental note on --query. # Cannot extend the shared option set because workflow commands that also use # it (workflow cancel, workflow count, etc.) are not experimental.