Files
Dan DavisonandGitHub c1355e9165 Update APIs: server-imposed deadline expiry and non-blocking Update Poll (#5114)
## What changed?

Makes changes summarized in
https://github.com/temporalio/temporal/assets/52205/0660a021-588a-4573-94d3-301cae0c0040

### Empty responses on server-timeout
The behavior of `PollWorkflowExecutionUpdate` and
`UpdateWorkflowExecution` was changed as follows:
- They introduce a "server-imposed deadline" equal to current time plus
`LongPollExpirationInterval`
- If a timeout occurs due to the server-imposed deadline, then they do
not return an error, but return a response with an empty outcome.
- Otherwise, their behavior is unchanged (in particular, if a timeout
occurs due context deadline expiry, they return the error as before)

### Non-blocking poll
- Change the Update Poll endpoint so that omitting the `WaitPolicy` now
means "do not wait; respond with current Update status without
blocking". Previously omitting the `WaitPolicy` was interpreted to mean
"attempt to wait until Completed"
- Also return the `UpdateRef`

## Why?

### Empty responses on server-timeout
- This makes the behavior of the endoint consistent with
`GetWorkflowExecutionHistory`.
- This makes sense, because that endpoint is used in an analogous way
(e.g. `GetWorkflowExecutionHistory` is used for `wfHandle.result()` and
`PollWorkflowExecutionUpdateRequest` is used for
`updateHandle.result()`, and both involve polling for the result).
- In particular, a polling client interprets
timeout-on-server-imposed-deadline to mean that it should continue
polling, and we would prefer not to use an error to communicate this
non-exceptional situation.

### Non-blocking poll
SDK clients may use non-blocking poll to, for example
- Obtain current status of an Update (perhaps `updateHandle.describe()`
- Determine whether an Update with a given ID already exists / is
running

### How did you test it?
- Unit tests
- Modified Typescript SDK to not send `WaitPolicy`, ran a feature test
against the modified server and modified SDK, and confirmed API
communication as expected (this revealed that I needed to stop Frontend
mutating the request to set `WaitPolicy` to `Completed`).
- I changed the poll loop in sdk-typescript to no longer catch
`DeadlineExceeded`, ran the `update/basic_async` feature test against
the local SDK and against server binaries built prior to and with this
commit, and confirmed that the change in this PR fixed the poll loop.

### Potential risks
- Could break Update operations in SDKs

### Is hotfix candidate?
- No
2023-11-16 17:48:49 -05:00

120 lines
4.2 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package pollupdate
import (
"context"
"fmt"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
updatepb "go.temporal.io/api/update/v1"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/common/definition"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/service/history/api"
"go.temporal.io/server/service/history/shard"
"go.temporal.io/server/service/history/workflow"
"go.temporal.io/server/service/history/workflow/update"
)
func Invoke(
ctx context.Context,
req *historyservice.PollWorkflowExecutionUpdateRequest,
shardContext shard.Context,
ctxLookup api.WorkflowConsistencyChecker,
) (*historyservice.PollWorkflowExecutionUpdateResponse, error) {
waitStage := req.GetRequest().GetWaitPolicy().GetLifecycleStage()
updateRef := req.GetRequest().GetUpdateRef()
wfexec := updateRef.GetWorkflowExecution()
wfKey, upd, ok, err := func() (*definition.WorkflowKey, *update.Update, bool, error) {
wfctx, err := ctxLookup.GetWorkflowContext(
ctx,
nil,
api.BypassMutableStateConsistencyPredicate,
definition.NewWorkflowKey(
req.GetNamespaceId(),
wfexec.GetWorkflowId(),
wfexec.GetRunId(),
),
workflow.LockPriorityHigh,
)
if err != nil {
return nil, nil, false, err
}
release := wfctx.GetReleaseFn()
defer release(nil)
upd, found := wfctx.GetUpdateRegistry(ctx).Find(ctx, updateRef.UpdateId)
wfKey := wfctx.GetWorkflowKey()
return &wfKey, upd, found, nil
}()
if err != nil {
return nil, err
}
if !ok {
return nil, serviceerror.NewNotFound(fmt.Sprintf("update %q not found", updateRef.GetUpdateId()))
}
var status update.UpdateStatus
switch waitStage {
case enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED:
status, err = upd.Status()
if err != nil {
return nil, err
}
case enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED:
namespaceID := namespace.ID(req.GetNamespaceId())
ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
if err != nil {
return nil, err
}
serverTimeout := shardContext.GetConfig().LongPollExpirationInterval(ns.Name().String())
// If the long-poll times out due to serverTimeout then return a non-error empty response.
status, err = upd.WaitLifecycleStage(ctx, waitStage, serverTimeout)
if err != nil {
return nil, err
}
default:
return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("support for LifecycleStage=%v is not implemented", waitStage))
}
return &historyservice.PollWorkflowExecutionUpdateResponse{
Response: &workflowservice.PollWorkflowExecutionUpdateResponse{
Outcome: status.Outcome,
Stage: status.Stage,
UpdateRef: &updatepb.UpdateRef{
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: wfKey.WorkflowID,
RunId: wfKey.RunID,
},
UpdateId: updateRef.UpdateId,
},
},
}, nil
}