Files
tasks/host/query_workflow_test.go
Yimin ChenandGitHub b5dee4f7c1 Fix query (#2826)
Fix query for 2 cases:
1) No workflow task has ever scheduled, usually due to firstWorkflowTaskBackoff. 
    In this case, return WorkflowNotReady error.
2) No workflow task has ever started, usually due to race condition or worker is down.
    In this case, do not dispatch query directly, instead buffer query and sent the query with next workflow task. 
    The query might timeout if no worker pick up the scheduled workflow task.
2022-05-10 14:51:44 -07:00

261 lines
7.6 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 host
import (
"context"
"sync"
"sync/atomic"
"time"
commonpb "go.temporal.io/api/common/v1"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
"go.temporal.io/server/service/history/consts"
"go.temporal.io/server/common/log/tag"
)
func (s *clientIntegrationSuite) TestQueryWorkflow_Sticky() {
var replayCount int32
workflowFn := func(ctx workflow.Context) (string, error) {
// every replay will start from here
atomic.AddInt32(&replayCount, 1)
workflow.SetQueryHandler(ctx, "test", func() (string, error) {
return "query works", nil
})
signalCh := workflow.GetSignalChannel(ctx, "test")
var msg string
signalCh.Receive(ctx, &msg)
return msg, nil
}
s.worker.RegisterWorkflow(workflowFn)
id := "test-query-sticky"
workflowOptions := sdkclient.StartWorkflowOptions{
ID: id,
TaskQueue: s.taskQueue,
WorkflowRunTimeout: 20 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowRun, err := s.sdkClient.ExecuteWorkflow(ctx, workflowOptions, workflowFn)
if err != nil {
s.Logger.Fatal("Start workflow failed with err", tag.Error(err))
}
s.NotNil(workflowRun)
s.True(workflowRun.GetRunID() != "")
queryResult, err := s.sdkClient.QueryWorkflow(ctx, id, "", "test", "test")
s.NoError(err)
var queryResultStr string
err = queryResult.Get(&queryResultStr)
s.NoError(err)
s.Equal("query works", queryResultStr)
// verify query is handed by sticky worker (no replay)
s.Equal(int32(1), replayCount)
}
func (s *clientIntegrationSuite) TestQueryWorkflow_Consistent_PiggybackQuery() {
workflowFn := func(ctx workflow.Context) (string, error) {
var receivedMsgs string
workflow.SetQueryHandler(ctx, "test", func() (string, error) {
return receivedMsgs, nil
})
signalCh := workflow.GetSignalChannel(ctx, "test")
for {
var msg string
signalCh.Receive(ctx, &msg)
receivedMsgs += msg
if msg == "pause" {
// block workflow task for 3s.
workflow.ExecuteLocalActivity(ctx, func() {
time.Sleep(time.Second * 3)
}).Get(ctx, nil)
}
}
return receivedMsgs, nil
}
s.worker.RegisterWorkflow(workflowFn)
id := "test-query-consistent"
workflowOptions := sdkclient.StartWorkflowOptions{
ID: id,
TaskQueue: s.taskQueue,
WorkflowRunTimeout: 20 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowRun, err := s.sdkClient.ExecuteWorkflow(ctx, workflowOptions, workflowFn)
if err != nil {
s.Logger.Fatal("Start workflow failed with err", tag.Error(err))
}
s.NotNil(workflowRun)
s.True(workflowRun.GetRunID() != "")
err = s.sdkClient.SignalWorkflow(ctx, id, "", "test", "pause")
s.NoError(err)
err = s.sdkClient.SignalWorkflow(ctx, id, "", "test", "abc")
s.NoError(err)
queryResult, err := s.sdkClient.QueryWorkflow(ctx, id, "", "test")
s.NoError(err)
var queryResultStr string
err = queryResult.Get(&queryResultStr)
s.NoError(err)
// verify query sees all signals before it
s.Equal("pauseabc", queryResultStr)
}
func (s *clientIntegrationSuite) TestQueryWorkflow_QueryWhileBackoff() {
workflowFn := func(ctx workflow.Context) (string, error) {
workflow.SetQueryHandler(ctx, "test", func() (string, error) {
return "should-reach-here", nil
})
return "", temporal.NewApplicationError("retry-me", "test-error")
}
s.worker.RegisterWorkflow(workflowFn)
id := "test-query-before-backoff"
workflowOptions := sdkclient.StartWorkflowOptions{
ID: id,
TaskQueue: s.taskQueue,
WorkflowRunTimeout: 20 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 10 * time.Second,
},
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowRun, err := s.sdkClient.ExecuteWorkflow(ctx, workflowOptions, workflowFn)
if err != nil {
s.Logger.Fatal("Start workflow failed with err", tag.Error(err))
}
s.NotNil(workflowRun)
s.True(workflowRun.GetRunID() != "")
// wait until retry with backoff is scheduled
findBackoffWorkflow := false
for i := 0; i < 5; i++ {
historyEvents := s.getHistory(s.namespace, &commonpb.WorkflowExecution{
WorkflowId: id,
})
s.True(len(historyEvents) > 0)
startEvent := historyEvents[0]
startAttributes := startEvent.GetWorkflowExecutionStartedEventAttributes()
s.NotNil(startAttributes)
if startAttributes.FirstWorkflowTaskBackoff != nil && *startAttributes.FirstWorkflowTaskBackoff > 0 {
findBackoffWorkflow = true
break
}
// wait for the retry, which will have backoff
time.Sleep(time.Second)
}
s.True(findBackoffWorkflow)
_, err = s.sdkClient.QueryWorkflow(ctx, id, "", "test")
s.Error(err)
s.ErrorContains(err, consts.ErrWorkflowTaskNotScheduled.Error())
}
func (s *clientIntegrationSuite) TestQueryWorkflow_QueryBeforeStart() {
// stop the worker, so the workflow won't be started before query
s.worker.Stop()
workflowFn := func(ctx workflow.Context) (string, error) {
status := "initialized"
workflow.SetQueryHandler(ctx, "test", func() (string, error) {
return status, nil
})
status = "started"
workflow.Sleep(ctx, time.Hour)
return "", nil
}
id := "test-query-before-start"
workflowOptions := sdkclient.StartWorkflowOptions{
ID: id,
TaskQueue: s.taskQueue,
WorkflowRunTimeout: 20 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workflowRun, err := s.sdkClient.ExecuteWorkflow(ctx, workflowOptions, workflowFn)
if err != nil {
s.Logger.Fatal("Start workflow failed with err", tag.Error(err))
}
s.NotNil(workflowRun)
s.True(workflowRun.GetRunID() != "")
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
startTime := time.Now()
queryResult, err := s.sdkClient.QueryWorkflow(ctx, id, "", "test")
endTime := time.Now()
s.NoError(err)
var queryResultStr string
err = queryResult.Get(&queryResultStr)
s.NoError(err)
// verify query sees all signals before it
s.Equal("started", queryResultStr)
s.True(endTime.Sub(startTime) > time.Second)
}()
// delay 2s to start worker, this will block query for 2s
time.Sleep(time.Second * 2)
s.worker = worker.New(s.sdkClient, s.taskQueue, worker.Options{})
s.worker.RegisterWorkflow(workflowFn)
if err := s.worker.Start(); err != nil {
s.Logger.Fatal("Error when start worker", tag.Error(err))
}
// wait query
wg.Wait()
}