mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-26 04:00:55 +00:00
345 lines
15 KiB
Go
345 lines
15 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 internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
|
|
"go.temporal.io/sdk/converter"
|
|
"go.temporal.io/sdk/internal/common"
|
|
"go.temporal.io/sdk/internal/common/metrics"
|
|
"go.temporal.io/sdk/log"
|
|
)
|
|
|
|
type (
|
|
// ActivityType identifies a activity type.
|
|
ActivityType struct {
|
|
Name string
|
|
}
|
|
|
|
// ActivityInfo contains information about currently executing activity.
|
|
ActivityInfo struct {
|
|
TaskToken []byte
|
|
WorkflowType *WorkflowType
|
|
WorkflowNamespace string
|
|
WorkflowExecution WorkflowExecution
|
|
ActivityID string
|
|
ActivityType ActivityType
|
|
TaskQueue string
|
|
HeartbeatTimeout time.Duration // Maximum time between heartbeats. 0 means no heartbeat needed.
|
|
ScheduledTime time.Time // Time of activity scheduled by a workflow
|
|
StartedTime time.Time // Time of activity start
|
|
Deadline time.Time // Time of activity timeout
|
|
Attempt int32 // Attempt starts from 1, and increased by 1 for every retry if retry policy is specified.
|
|
}
|
|
|
|
// RegisterActivityOptions consists of options for registering an activity
|
|
RegisterActivityOptions struct {
|
|
// When an activity is a function the name is an actual activity type name.
|
|
// When an activity is part of a structure then each member of the structure becomes an activity with
|
|
// this Name as a prefix + activity function name.
|
|
Name string
|
|
DisableAlreadyRegisteredCheck bool
|
|
|
|
// When registering a struct with activities, skip functions that are not valid activities. If false,
|
|
// registration panics.
|
|
SkipInvalidStructFunctions bool
|
|
}
|
|
|
|
// ActivityOptions stores all activity-specific parameters that will be stored inside of a context.
|
|
// The current timeout resolution implementation is in seconds and uses math.Ceil(d.Seconds()) as the duration. But is
|
|
// subjected to change in the future.
|
|
ActivityOptions struct {
|
|
// TaskQueue that the activity needs to be scheduled on.
|
|
// optional: The default task queue with the same name as the workflow task queue.
|
|
TaskQueue string
|
|
|
|
// ScheduleToCloseTimeout - Total time that a workflow is willing to wait for Activity to complete.
|
|
// ScheduleToCloseTimeout limits the total time of an Activity's execution including retries
|
|
// (use StartToCloseTimeout to limit the time of a single attempt).
|
|
// The zero value of this uses default value.
|
|
// Either this option or StartToClose is required: Defaults to unlimited.
|
|
ScheduleToCloseTimeout time.Duration
|
|
|
|
// ScheduleToStartTimeout - Time that the Activity Task can stay in the Task Queue before it is picked up by
|
|
// a Worker. Do not specify this timeout unless using host specific Task Queues for Activity Tasks are being
|
|
// used for routing. In almost all situations that don't involve routing activities to specific hosts it is
|
|
// better to rely on the default value.
|
|
// ScheduleToStartTimeout is always non-retryable. Retrying after this timeout doesn't make sense as it would
|
|
// just put the Activity Task back into the same Task Queue.
|
|
// If ScheduleToClose is not provided then this timeout is required.
|
|
// Optional: Defaults to unlimited.
|
|
ScheduleToStartTimeout time.Duration
|
|
|
|
// StartToCloseTimeout - Maximum time of a single Activity execution attempt.
|
|
// Note that the Temporal Server doesn't detect Worker process failures directly. It relies on this timeout
|
|
// to detect that an Activity that didn't complete on time. So this timeout should be as short as the longest
|
|
// possible execution of the Activity body. Potentially long running Activities must specify HeartbeatTimeout
|
|
// and call Activity.RecordHeartbeat(ctx, "my-heartbeat") periodically for timely failure detection.
|
|
// If ScheduleToClose is not provided then this timeout is required: Defaults to the ScheduleToCloseTimeout value.
|
|
StartToCloseTimeout time.Duration
|
|
|
|
// HeartbeatTimeout - Heartbeat interval. Activity must call Activity.RecordHeartbeat(ctx, "my-heartbeat")
|
|
// before this interval passes after the last heartbeat or the Activity starts.
|
|
HeartbeatTimeout time.Duration
|
|
|
|
// WaitForCancellation - Whether to wait for canceled activity to be completed(
|
|
// activity can be failed, completed, cancel accepted)
|
|
// Optional: default false
|
|
WaitForCancellation bool
|
|
|
|
// ActivityID - Business level activity ID, this is not needed for most of the cases if you have
|
|
// to specify this then talk to temporal team. This is something will be done in future.
|
|
// Optional: default empty string
|
|
ActivityID string
|
|
|
|
// RetryPolicy specifies how to retry an Activity if an error occurs.
|
|
// More details are available at docs.temporal.io.
|
|
// RetryPolicy is optional. If one is not specified a default RetryPolicy is provided by the server.
|
|
// The default RetryPolicy provided by the server specifies:
|
|
// - InitialInterval of 1 second
|
|
// - BackoffCoefficient of 2.0
|
|
// - MaximumInterval of 100 x InitialInterval
|
|
// - MaximumAttempts of 0 (unlimited)
|
|
// To disable retries set MaximumAttempts to 1.
|
|
// The default RetryPolicy provided by the server can be overridden by the dynamic config.
|
|
RetryPolicy *RetryPolicy
|
|
}
|
|
|
|
// LocalActivityOptions stores local activity specific parameters that will be stored inside of a context.
|
|
LocalActivityOptions struct {
|
|
// ScheduleToCloseTimeout - The end to end timeout for the local activity including retries.
|
|
// This field is required.
|
|
ScheduleToCloseTimeout time.Duration
|
|
|
|
// StartToCloseTimeout - The timeout for a single execution of the local activity.
|
|
// Optional: defaults to ScheduleToClose
|
|
StartToCloseTimeout time.Duration
|
|
|
|
// RetryPolicy specify how to retry activity if error happens.
|
|
// Optional: default is to retry according to the default retry policy up to ScheduleToCloseTimeout
|
|
// with 1sec initial delay between retries and 2x backoff.
|
|
RetryPolicy *RetryPolicy
|
|
}
|
|
)
|
|
|
|
// GetActivityInfo returns information about currently executing activity.
|
|
func GetActivityInfo(ctx context.Context) ActivityInfo {
|
|
return getActivityOutboundInterceptor(ctx).GetInfo(ctx)
|
|
}
|
|
|
|
// HasHeartbeatDetails checks if there is heartbeat details from last attempt.
|
|
func HasHeartbeatDetails(ctx context.Context) bool {
|
|
return getActivityOutboundInterceptor(ctx).HasHeartbeatDetails(ctx)
|
|
}
|
|
|
|
// GetHeartbeatDetails extract heartbeat details from last failed attempt. This is used in combination with retry policy.
|
|
// An activity could be scheduled with an optional retry policy on ActivityOptions. If the activity failed then server
|
|
// would attempt to dispatch another activity task to retry according to the retry policy. If there was heartbeat
|
|
// details reported by activity from the failed attempt, the details would be delivered along with the activity task for
|
|
// retry attempt. Activity could extract the details by GetHeartbeatDetails() and resume from the progress.
|
|
func GetHeartbeatDetails(ctx context.Context, d ...interface{}) error {
|
|
return getActivityOutboundInterceptor(ctx).GetHeartbeatDetails(ctx, d...)
|
|
}
|
|
|
|
// GetActivityLogger returns a logger that can be used in activity
|
|
func GetActivityLogger(ctx context.Context) log.Logger {
|
|
return getActivityOutboundInterceptor(ctx).GetLogger(ctx)
|
|
}
|
|
|
|
// GetActivityMetricsHandler returns a metrics handler that can be used in activity
|
|
func GetActivityMetricsHandler(ctx context.Context) metrics.Handler {
|
|
return getActivityOutboundInterceptor(ctx).GetMetricsHandler(ctx)
|
|
}
|
|
|
|
// GetWorkerStopChannel returns a read-only channel. The closure of this channel indicates the activity worker is stopping.
|
|
// When the worker is stopping, it will close this channel and wait until the worker stop timeout finishes. After the timeout
|
|
// hit, the worker will cancel the activity context and then exit. The timeout can be defined by worker option: WorkerStopTimeout.
|
|
// Use this channel to handle activity graceful exit when the activity worker stops.
|
|
func GetWorkerStopChannel(ctx context.Context) <-chan struct{} {
|
|
return getActivityOutboundInterceptor(ctx).GetWorkerStopChannel(ctx)
|
|
}
|
|
|
|
// RecordActivityHeartbeat sends heartbeat for the currently executing activity
|
|
// If the activity is either canceled (or) workflow/activity doesn't exist then we would cancel
|
|
// the context with error context.Canceled.
|
|
// TODO: we don't have a way to distinguish between the two cases when context is canceled because
|
|
// context doesn't support overriding value of ctx.Error.
|
|
// TODO: Implement automatic heartbeating with cancellation through ctx.
|
|
// details - the details that you provided here can be seen in the workflow when it receives TimeoutError, you
|
|
// can check error TimeoutType()/Details().
|
|
func RecordActivityHeartbeat(ctx context.Context, details ...interface{}) {
|
|
getActivityOutboundInterceptor(ctx).RecordHeartbeat(ctx, details...)
|
|
}
|
|
|
|
// ServiceInvoker abstracts calls to the Temporal service from an activity implementation.
|
|
// Implement to unit test activities.
|
|
type ServiceInvoker interface {
|
|
// Returns ActivityTaskCanceledError if activity is canceled
|
|
Heartbeat(ctx context.Context, details *commonpb.Payloads, skipBatching bool) error
|
|
Close(ctx context.Context, flushBufferedHeartbeat bool)
|
|
GetClient(options ClientOptions) Client
|
|
}
|
|
|
|
// WithActivityTask adds activity specific information into context.
|
|
// Use this method to unit test activity implementations that use context extractor methodshared.
|
|
func WithActivityTask(
|
|
ctx context.Context,
|
|
task *workflowservice.PollActivityTaskQueueResponse,
|
|
taskQueue string,
|
|
invoker ServiceInvoker,
|
|
logger log.Logger,
|
|
metricsHandler metrics.Handler,
|
|
dataConverter converter.DataConverter,
|
|
workerStopChannel <-chan struct{},
|
|
contextPropagators []ContextPropagator,
|
|
interceptors []WorkerInterceptor,
|
|
) (context.Context, error) {
|
|
var deadline time.Time
|
|
scheduled := common.TimeValue(task.GetScheduledTime())
|
|
started := common.TimeValue(task.GetStartedTime())
|
|
scheduleToCloseTimeout := common.DurationValue(task.GetScheduleToCloseTimeout())
|
|
startToCloseTimeout := common.DurationValue(task.GetStartToCloseTimeout())
|
|
heartbeatTimeout := common.DurationValue(task.GetHeartbeatTimeout())
|
|
|
|
startToCloseDeadline := started.Add(startToCloseTimeout)
|
|
if scheduleToCloseTimeout > 0 {
|
|
scheduleToCloseDeadline := scheduled.Add(scheduleToCloseTimeout)
|
|
// Minimum of the two deadlines.
|
|
if scheduleToCloseDeadline.Before(startToCloseDeadline) {
|
|
deadline = scheduleToCloseDeadline
|
|
} else {
|
|
deadline = startToCloseDeadline
|
|
}
|
|
} else {
|
|
deadline = startToCloseDeadline
|
|
}
|
|
|
|
logger = log.With(logger,
|
|
tagActivityID, task.ActivityId,
|
|
tagActivityType, task.ActivityType.GetName(),
|
|
tagAttempt, task.Attempt,
|
|
tagWorkflowType, task.WorkflowType.GetName(),
|
|
tagWorkflowID, task.WorkflowExecution.WorkflowId,
|
|
tagRunID, task.WorkflowExecution.RunId,
|
|
)
|
|
|
|
return newActivityContext(ctx, interceptors, &activityEnvironment{
|
|
taskToken: task.TaskToken,
|
|
serviceInvoker: invoker,
|
|
activityType: ActivityType{Name: task.ActivityType.GetName()},
|
|
activityID: task.ActivityId,
|
|
workflowExecution: WorkflowExecution{
|
|
RunID: task.WorkflowExecution.RunId,
|
|
ID: task.WorkflowExecution.WorkflowId},
|
|
logger: logger,
|
|
metricsHandler: metricsHandler,
|
|
deadline: deadline,
|
|
heartbeatTimeout: heartbeatTimeout,
|
|
scheduledTime: scheduled,
|
|
startedTime: started,
|
|
taskQueue: taskQueue,
|
|
dataConverter: dataConverter,
|
|
attempt: task.GetAttempt(),
|
|
heartbeatDetails: task.HeartbeatDetails,
|
|
workflowType: &WorkflowType{
|
|
Name: task.WorkflowType.GetName(),
|
|
},
|
|
workflowNamespace: task.WorkflowNamespace,
|
|
workerStopChannel: workerStopChannel,
|
|
contextPropagators: contextPropagators,
|
|
})
|
|
}
|
|
|
|
// WithLocalActivityTask adds local activity specific information into context.
|
|
func WithLocalActivityTask(
|
|
ctx context.Context,
|
|
task *localActivityTask,
|
|
logger log.Logger,
|
|
metricsHandler metrics.Handler,
|
|
dataConverter converter.DataConverter,
|
|
interceptors []WorkerInterceptor,
|
|
) (context.Context, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
workflowTypeLocal := task.params.WorkflowInfo.WorkflowType
|
|
workflowType := task.params.WorkflowInfo.WorkflowType.Name
|
|
activityType := task.params.ActivityType
|
|
logger = log.With(logger,
|
|
tagActivityID, task.activityID,
|
|
tagActivityType, activityType,
|
|
tagAttempt, task.attempt,
|
|
tagWorkflowType, workflowType,
|
|
tagWorkflowID, task.params.WorkflowInfo.WorkflowExecution.ID,
|
|
tagRunID, task.params.WorkflowInfo.WorkflowExecution.RunID,
|
|
)
|
|
return newActivityContext(ctx, interceptors, &activityEnvironment{
|
|
workflowType: &workflowTypeLocal,
|
|
workflowNamespace: task.params.WorkflowInfo.Namespace,
|
|
taskQueue: task.params.WorkflowInfo.TaskQueueName,
|
|
activityType: ActivityType{Name: activityType},
|
|
activityID: fmt.Sprintf("%v", task.activityID),
|
|
workflowExecution: task.params.WorkflowInfo.WorkflowExecution,
|
|
logger: logger,
|
|
metricsHandler: metricsHandler,
|
|
isLocalActivity: true,
|
|
dataConverter: dataConverter,
|
|
attempt: task.attempt,
|
|
})
|
|
}
|
|
|
|
func newActivityContext(
|
|
ctx context.Context,
|
|
interceptors []WorkerInterceptor,
|
|
env *activityEnvironment,
|
|
) (context.Context, error) {
|
|
ctx = context.WithValue(ctx, activityEnvContextKey, env)
|
|
|
|
// Create interceptor with default inbound and outbound values and put on
|
|
// context
|
|
envInterceptor := &activityEnvironmentInterceptor{env: env}
|
|
envInterceptor.inboundInterceptor = envInterceptor
|
|
envInterceptor.outboundInterceptor = envInterceptor
|
|
ctx = context.WithValue(ctx, activityEnvInterceptorContextKey, envInterceptor)
|
|
ctx = context.WithValue(ctx, activityInterceptorContextKey, envInterceptor.outboundInterceptor)
|
|
|
|
// Intercept, run init, and put the new outbound interceptor on the context
|
|
for i := len(interceptors) - 1; i >= 0; i-- {
|
|
envInterceptor.inboundInterceptor = interceptors[i].InterceptActivity(ctx, envInterceptor.inboundInterceptor)
|
|
}
|
|
err := envInterceptor.inboundInterceptor.Init(envInterceptor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx = context.WithValue(ctx, activityInterceptorContextKey, envInterceptor.outboundInterceptor)
|
|
|
|
return ctx, nil
|
|
}
|