diff --git a/temporal/api/enums/v1/event_type.proto b/temporal/api/enums/v1/event_type.proto index 26a512992..028282ab2 100644 --- a/temporal/api/enums/v1/event_type.proto +++ b/temporal/api/enums/v1/event_type.proto @@ -173,4 +173,7 @@ enum EventType { EVENT_TYPE_WORKFLOW_EXECUTION_PAUSED = 58; // An event that indicates that the previously paused workflow execution has been unpaused. EVENT_TYPE_WORKFLOW_EXECUTION_UNPAUSED = 59; + // A time point was advanced for this workflow execution via AdvanceWorkflowExecutionTimePoint + // or auto-skip. + EVENT_TYPE_WORKFLOW_EXECUTION_TIME_POINT_ADVANCED = 60; } diff --git a/temporal/api/history/v1/message.proto b/temporal/api/history/v1/message.proto index 2fb99f7e9..1f6615807 100644 --- a/temporal/api/history/v1/message.proto +++ b/temporal/api/history/v1/message.proto @@ -947,6 +947,18 @@ message WorkflowExecutionUnpausedEventAttributes { string request_id = 3; } +// Attributes for an event marking that a time point was advanced for a workflow execution, +// either via an explicit AdvanceWorkflowExecutionTimePoint call or auto-skip. +message WorkflowExecutionTimePointAdvancedEventAttributes { + // The amount of virtual time advanced by this operation. This is the delta + // added to the workflow's virtual time offset. + google.protobuf.Duration duration_advanced = 1; + // The identity of the client who initiated the advance. Empty for auto-skip. + string identity = 2; + // The request ID of the advance request. Empty for auto-skip. + string request_id = 3; +} + // Event marking that an operation was scheduled by a workflow via the ScheduleNexusOperation command. message NexusOperationScheduledEventAttributes { // Endpoint name, must exist in the endpoint registry. @@ -1179,6 +1191,7 @@ message HistoryEvent { NexusOperationCancelRequestFailedEventAttributes nexus_operation_cancel_request_failed_event_attributes = 62; WorkflowExecutionPausedEventAttributes workflow_execution_paused_event_attributes = 63; WorkflowExecutionUnpausedEventAttributes workflow_execution_unpaused_event_attributes = 64; + WorkflowExecutionTimePointAdvancedEventAttributes workflow_execution_time_point_advanced_event_attributes = 65; } } diff --git a/temporal/api/workflow/v1/message.proto b/temporal/api/workflow/v1/message.proto index 40753b2ce..b2c0cd938 100644 --- a/temporal/api/workflow/v1/message.proto +++ b/temporal/api/workflow/v1/message.proto @@ -566,6 +566,177 @@ message WorkflowExecutionOptions { // If set, overrides the workflow's priority sent by the SDK. temporal.api.common.v1.Priority priority = 2; + + // Time skipping configuration for this workflow execution. + // Once enabled, cannot be disabled. See `TimeSkippingConfig` for details. + TimeSkippingConfig time_skipping_config = 3; +} + +// Configuration for time skipping on a workflow execution. +// Time skipping allows a workflow's virtual time to diverge from wall-clock time, +// enabling fast-forwarding through timers and timeouts for testing purposes. +// Once enabled, cannot be disabled (the workflow's virtual time has diverged). +message TimeSkippingConfig { + // Enables time skipping for this workflow execution. Once set to true, cannot be + // set back to false. When enabled, timers and timeouts will not fire based on + // wall-clock time; they only fire via explicit SkipWorkflowExecutionTime calls + // or auto-skip (if configured). + bool enabled = 1; + + // If set, enables automatic time skipping. Clear this field (set to null via + // FieldMask) to disable auto-skip and return to manual-only time skipping. + // + // Auto-skip is automatically paused (inactive) while there are any pending + // activities, child workflows, or Nexus operations, to avoid unexpectedly + // firing their timeouts. It resumes once the workflow becomes idle and a + // workflow task completes. Users who need to advance time while external + // work is pending should use the AdvanceWorkflowExecutionTimePoint API + // directly. + AutoSkipConfig auto_skip = 2; + + // If true, newly started child workflows will automatically inherit this + // time-skipping configuration. Default false. The inherited config includes + // this flag, so grandchildren will also inherit (transitive). + bool propagate_to_new_children = 3; + + // If true, continue-as-new will inherit this time-skipping configuration + // into the new run. The proto default is false, but SDKs should default + // this to true when constructing a TimeSkippingConfig, since losing + // time-skipping on continue-as-new is rarely desired. + bool propagate_on_continue_as_new = 4; + + // Configuration for automatic time skipping. + // Auto-skip advances the workflow's virtual time to the next upcoming time + // point whenever the workflow is idle (no pending activities, child + // workflows, or Nexus operations). Auto-skip halts when either the time + // bound or the firings limit is reached, at which point the workflow + // behaves as manual-skip until the config is updated. + message AutoSkipConfig { + // The time bound for auto-skip. Auto-skip will not advance the workflow's + // virtual time past this point. Exactly one of `until_time` or `until_duration` + // must be set. + oneof bound { + // Absolute virtual timestamp to auto-skip until. + google.protobuf.Timestamp until_time = 1; + // Duration from the workflow's current virtual time (at the time the config + // is applied) to auto-skip for. The server resolves this to an absolute + // timestamp stored in TimeSkippingInfo. + google.protobuf.Duration until_duration = 2; + } + + // Maximum number of time-point firings (timer fires, timeout expirations) + // auto-skip will trigger before halting. Resets each time auto-skip config + // is updated. Defaults to 10 if not set or set to 0. + int32 max_firings = 3; + } +} + +// Runtime information about time skipping for a workflow execution. +// Returned by DescribeWorkflowExecution when time skipping is enabled. +message TimeSkippingInfo { + // The current time skipping configuration. + TimeSkippingConfig config = 1; + + // The accumulated virtual time offset from time-skipping advances. + google.protobuf.Duration virtual_time_offset = 2; + + // The workflow's current virtual time (server wall clock + virtual_time_offset). + google.protobuf.Timestamp virtual_time = 3; + + // Present when auto-skip is configured. Null when auto-skip is not active. + AutoSkipInfo auto_skip = 4; + + // Runtime information about automatic time skipping. + message AutoSkipInfo { + // Whether auto-skip is currently advancing time. False when paused due to + // pending activities, child workflows, or Nexus operations, or when a + // limit has been reached. + bool active = 1; + + // The resolved auto-skip deadline as an absolute virtual timestamp + // (regardless of whether the config was set via `until_time` or + // `until_duration`). + google.protobuf.Timestamp deadline = 2; + + // The remaining virtual time until the auto-skip deadline is reached. + google.protobuf.Duration deadline_remaining = 3; + + // Number of time-point firings triggered by auto-skip in the current window. + int32 firings_used = 4; + + // Number of firings remaining before the max_firings limit is reached. + int32 firings_remaining = 5; + } +} + +// Describes an upcoming time point for a workflow execution: a future instant at +// which something will happen (a timer fires, a timeout expires, etc.). +// Returned by DescribeWorkflowExecution. +// +// The following are intentionally NOT considered time points: +// +// - Workflow task timeout: Workflow task processing still occurs during time +// skipping and needs to be timed out if the worker is unresponsive. The server +// retries the task automatically. +// - Workflow retry backoff: Whole-workflow retries are not modeled as time +// points at this time. +// - Activity heartbeat timeout: Heartbeats are a liveness check for running +// activities and are still needed during time skipping. May be added in the +// future. +// - Activity retry backoff: Activity retries can be managed through other +// mechanisms such as activity reset. +message UpcomingTimePointInfo { + // The absolute virtual time at which this time point will fire. + google.protobuf.Timestamp fire_time = 1; + + // The remaining virtual time until this time point fires + // (fire_time minus current virtual time at the time of the response). + google.protobuf.Duration fire_time_remaining = 2; + + // The source of this upcoming time point. + oneof source { + TimerTimePoint timer = 3; + ActivityTimeoutTimePoint activity_timeout = 4; + WorkflowTimeoutTimePoint workflow_timeout = 5; + ChildWorkflowTimePoint child_workflow = 6; + NexusOperationTimeoutTimePoint nexus_operation_timeout = 7; + } + + // A workflow timer (e.g., from workflow.sleep() or workflow.newTimer()). + message TimerTimePoint { + string timer_id = 1; + int64 started_event_id = 2; + } + + // An activity timeout about to expire. + message ActivityTimeoutTimePoint { + string activity_id = 1; + temporal.api.enums.v1.TimeoutType timeout_type = 2; + } + + // A workflow-level timeout about to expire. + // SCHEDULE_TO_CLOSE = execution timeout (total including retries/CaN). + // START_TO_CLOSE = run timeout (single run). + message WorkflowTimeoutTimePoint { + temporal.api.enums.v1.TimeoutType timeout_type = 1; + } + + // An upcoming time point originating from a child workflow. + // TODO: Decide whether to include details of the child's time point source. + message ChildWorkflowTimePoint { + string workflow_id = 1; + string run_id = 2; + int64 initiated_event_id = 3; + } + + // A Nexus operation timeout about to expire. + message NexusOperationTimeoutTimePoint { + string endpoint = 1; + string service = 2; + string operation = 3; + int64 scheduled_event_id = 4; + temporal.api.enums.v1.TimeoutType timeout_type = 5; + } } // Used to override the versioning behavior (and pinned deployment version, if applicable) of a diff --git a/temporal/api/workflowservice/v1/request_response.proto b/temporal/api/workflowservice/v1/request_response.proto index 4bf3272c6..e3ffdfc62 100644 --- a/temporal/api/workflowservice/v1/request_response.proto +++ b/temporal/api/workflowservice/v1/request_response.proto @@ -197,6 +197,8 @@ message StartWorkflowExecutionRequest { temporal.api.common.v1.Priority priority = 27; // Deployment Options of the worker who will process the eager task. Passed when `request_eager_execution=true`. temporal.api.deployment.v1.WorkerDeploymentOptions eager_worker_deployment_options = 28; + // Time skipping configuration. If set, enables time skipping for this workflow from the start. + temporal.api.workflow.v1.TimeSkippingConfig time_skipping_config = 29; } message StartWorkflowExecutionResponse { @@ -816,6 +818,8 @@ message SignalWithStartWorkflowExecutionRequest { temporal.api.workflow.v1.VersioningOverride versioning_override = 25; // Priority metadata temporal.api.common.v1.Priority priority = 26; + // Time skipping configuration. If set, enables time skipping for this workflow from the start. + temporal.api.workflow.v1.TimeSkippingConfig time_skipping_config = 27; } message SignalWithStartWorkflowExecutionResponse { @@ -1074,6 +1078,14 @@ message DescribeWorkflowExecutionResponse { repeated temporal.api.workflow.v1.CallbackInfo callbacks = 6; repeated temporal.api.workflow.v1.PendingNexusOperationInfo pending_nexus_operations = 7; temporal.api.workflow.v1.WorkflowExecutionExtendedInfo workflow_extended_info = 8; + // Upcoming time points for this workflow execution. Each unique combination of + // source and timeout type is returned at most once with its nearest fire time. + // For example, an activity with both a heartbeat timeout and a start-to-close + // timeout will appear as two separate entries. A timer always appears once. + // TODO: Determine how to represent child workflow time points with different sources. + repeated temporal.api.workflow.v1.UpcomingTimePointInfo upcoming_time_points = 9; + // Time skipping information. Present when time skipping is enabled for this workflow. + temporal.api.workflow.v1.TimeSkippingInfo time_skipping_info = 10; } // (-- api-linter: core::0203::optional=disabled @@ -2718,6 +2730,40 @@ message UnpauseWorkflowExecutionRequest { // Response to a successful UnpauseWorkflowExecution request. message UnpauseWorkflowExecutionResponse { } +message AdvanceWorkflowExecutionTimePointRequest { + string namespace = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + // Optionally enable or update time skipping configuration in the same call. + // If time skipping is not already enabled, this must be provided with enabled=true. + temporal.api.workflow.v1.TimeSkippingConfig time_skipping_config = 3; + // The identity of the client who initiated this request. + string identity = 4; + // A unique identifier for this request, used for idempotency. + string request_id = 5; + // Optional upper bound for the advance. Virtual time will advance to the + // earlier of the next time point or this value. If no time point exists + // before this value, virtual time advances to this value without firing + // any time points. + oneof up_to { + // Absolute virtual timestamp to advance up to. + google.protobuf.Timestamp up_to_time = 6; + // Duration from the workflow's current virtual time to advance up to. + google.protobuf.Duration up_to_duration = 7; + } +} + +message AdvanceWorkflowExecutionTimePointResponse { + // Time skipping state after the advance (config, virtual time offset, auto-skip info). + temporal.api.workflow.v1.TimeSkippingInfo time_skipping_info = 1; + // The time points that were fired at the advanced-to time. + // Empty when there was no upcoming time point to advance to. + // Currently all entries will share the same fire_time, since each advance + // moves to a single point in time and fires everything scheduled there. + repeated temporal.api.workflow.v1.UpcomingTimePointInfo advanced_time_points = 2; + // Remaining upcoming time points after the advance. + repeated temporal.api.workflow.v1.UpcomingTimePointInfo upcoming_time_points = 3; +} + message StartActivityExecutionRequest { string namespace = 1; // The identity of the client who initiated this request diff --git a/temporal/api/workflowservice/v1/service.proto b/temporal/api/workflowservice/v1/service.proto index c67a2717b..3d0f9ce59 100644 --- a/temporal/api/workflowservice/v1/service.proto +++ b/temporal/api/workflowservice/v1/service.proto @@ -1358,6 +1358,25 @@ service WorkflowService { }; } + // AdvanceWorkflowExecutionTimePoint advances a workflow's virtual time to the + // next upcoming time point (the nearest pending timer fire or timeout expiration), + // causing it to trigger. If there is no upcoming time point, returns successfully + // with no_upcoming_time_point=true. + // Requires time skipping to be enabled on the workflow (either previously via + // UpdateWorkflowExecutionOptions or inline via the time_skipping_config field). + // Records a WORKFLOW_EXECUTION_TIME_POINT_ADVANCED event in the workflow history. + // The workflow must be in RUNNING status and not PAUSED. + rpc AdvanceWorkflowExecutionTimePoint (AdvanceWorkflowExecutionTimePointRequest) returns (AdvanceWorkflowExecutionTimePointResponse) { + option (google.api.http) = { + post: "/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/advance-time-point" + body: "*" + additional_bindings { + post: "/api/v1/namespaces/{namespace}/workflows/{workflow_execution.workflow_id}/advance-time-point" + body: "*" + } + }; + } + // StartActivityExecution starts a new activity execution. // // Returns an `ActivityExecutionAlreadyStarted` error if an instance already exists with same activity ID in this namespace