temporal-ruby 0.0.1.pre.pre1 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +23 -3
- data/lib/gen/temporal/api/command/v1/message_pb.rb +1 -1
- data/lib/gen/temporal/api/enums/v1/common_pb.rb +7 -0
- data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +5 -6
- data/lib/gen/temporal/api/version/v1/message_pb.rb +19 -8
- data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +19 -8
- data/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +0 -3
- data/lib/temporal.rb +104 -0
- data/lib/temporal/activity/context.rb +5 -1
- data/lib/temporal/activity/poller.rb +26 -9
- data/lib/temporal/activity/task_processor.rb +33 -20
- data/lib/temporal/client/converter/base.rb +35 -0
- data/lib/temporal/client/converter/composite.rb +49 -0
- data/lib/temporal/client/converter/payload/bytes.rb +30 -0
- data/lib/temporal/client/converter/payload/json.rb +28 -0
- data/lib/temporal/client/converter/payload/nil.rb +27 -0
- data/lib/temporal/client/grpc_client.rb +102 -27
- data/lib/temporal/client/retryer.rb +49 -0
- data/lib/temporal/client/serializer.rb +2 -0
- data/lib/temporal/client/serializer/cancel_timer.rb +2 -2
- data/lib/temporal/client/serializer/complete_workflow.rb +6 -4
- data/lib/temporal/client/serializer/continue_as_new.rb +37 -0
- data/lib/temporal/client/serializer/fail_workflow.rb +2 -2
- data/lib/temporal/client/serializer/failure.rb +4 -2
- data/lib/temporal/client/serializer/record_marker.rb +6 -4
- data/lib/temporal/client/serializer/request_activity_cancellation.rb +2 -2
- data/lib/temporal/client/serializer/retry_policy.rb +24 -0
- data/lib/temporal/client/serializer/schedule_activity.rb +8 -20
- data/lib/temporal/client/serializer/start_child_workflow.rb +9 -20
- data/lib/temporal/client/serializer/start_timer.rb +2 -2
- data/lib/temporal/concerns/payloads.rb +51 -0
- data/lib/temporal/configuration.rb +31 -4
- data/lib/temporal/error_handler.rb +11 -0
- data/lib/temporal/errors.rb +24 -0
- data/lib/temporal/execution_options.rb +9 -1
- data/lib/temporal/json.rb +3 -1
- data/lib/temporal/logger.rb +17 -0
- data/lib/temporal/metadata.rb +11 -3
- data/lib/temporal/metadata/activity.rb +15 -2
- data/lib/temporal/metadata/workflow.rb +8 -0
- data/lib/temporal/metadata/workflow_task.rb +11 -0
- data/lib/temporal/retry_policy.rb +6 -9
- data/lib/temporal/saga/concern.rb +1 -1
- data/lib/temporal/testing.rb +1 -0
- data/lib/temporal/testing/future_registry.rb +1 -1
- data/lib/temporal/testing/local_activity_context.rb +1 -1
- data/lib/temporal/testing/local_workflow_context.rb +38 -14
- data/lib/temporal/testing/scheduled_workflows.rb +75 -0
- data/lib/temporal/testing/temporal_override.rb +35 -7
- data/lib/temporal/testing/workflow_override.rb +6 -1
- data/lib/temporal/version.rb +1 -1
- data/lib/temporal/worker.rb +28 -10
- data/lib/temporal/workflow.rb +8 -2
- data/lib/temporal/workflow/command.rb +3 -0
- data/lib/temporal/workflow/context.rb +40 -5
- data/lib/temporal/workflow/errors.rb +39 -0
- data/lib/temporal/workflow/executor.rb +1 -1
- data/lib/temporal/workflow/future.rb +18 -6
- data/lib/temporal/workflow/history/event.rb +1 -3
- data/lib/temporal/workflow/history/event_target.rb +4 -0
- data/lib/temporal/workflow/history/window.rb +1 -1
- data/lib/temporal/workflow/poller.rb +41 -13
- data/lib/temporal/workflow/replay_aware_logger.rb +4 -4
- data/lib/temporal/workflow/state_manager.rb +33 -52
- data/lib/temporal/workflow/task_processor.rb +41 -11
- metadata +21 -9
- data/lib/temporal/client/serializer/payload.rb +0 -25
@@ -0,0 +1,11 @@
|
|
1
|
+
module Temporal
|
2
|
+
module ErrorHandler
|
3
|
+
def self.handle(error, metadata: nil)
|
4
|
+
Temporal.configuration.error_handlers.each do |handler|
|
5
|
+
handler.call(error, metadata: metadata)
|
6
|
+
rescue StandardError => e
|
7
|
+
Temporal.logger.error("Error handler failed", { error: e.inspect })
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/temporal/errors.rb
CHANGED
@@ -5,6 +5,10 @@ module Temporal
|
|
5
5
|
# Superclass for errors specific to Temporal worker itself
|
6
6
|
class InternalError < Error; end
|
7
7
|
|
8
|
+
# Indicates a non-deterministic workflow execution, might be due to
|
9
|
+
# a non-deterministic workflow implementation or the gem's bug
|
10
|
+
class NonDeterministicWorkflowError < InternalError; end
|
11
|
+
|
8
12
|
# Superclass for misconfiguration/misuse on the client (user) side
|
9
13
|
class ClientError < Error; end
|
10
14
|
|
@@ -21,6 +25,25 @@ module Temporal
|
|
21
25
|
class ApiError < Error; end
|
22
26
|
|
23
27
|
class NotFoundFailure < ApiError; end
|
28
|
+
|
29
|
+
# Superclass for system errors raised when retrieving a workflow result on the
|
30
|
+
# client, but the workflow failed remotely.
|
31
|
+
class WorkflowError < Error; end
|
32
|
+
|
33
|
+
class WorkflowTimedOut < WorkflowError; end
|
34
|
+
class WorkflowTerminated < WorkflowError; end
|
35
|
+
class WorkflowCanceled < WorkflowError; end
|
36
|
+
|
37
|
+
# Errors where the workflow run didn't complete but not an error for the whole workflow.
|
38
|
+
class WorkflowRunError < Error; end
|
39
|
+
class WorkflowRunContinuedAsNew < WorkflowRunError
|
40
|
+
attr_reader :new_run_id
|
41
|
+
def initialize(new_run_id:)
|
42
|
+
super
|
43
|
+
@new_run_id = new_run_id
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
24
47
|
class WorkflowExecutionAlreadyStartedFailure < ApiError
|
25
48
|
attr_reader :run_id
|
26
49
|
|
@@ -35,4 +58,5 @@ module Temporal
|
|
35
58
|
class NamespaceAlreadyExistsFailure < ApiError; end
|
36
59
|
class CancellationAlreadyRequestedFailure < ApiError; end
|
37
60
|
class QueryFailedFailure < ApiError; end
|
61
|
+
|
38
62
|
end
|
@@ -12,7 +12,7 @@ module Temporal
|
|
12
12
|
@timeouts = options[:timeouts] || {}
|
13
13
|
@headers = options[:headers] || {}
|
14
14
|
|
15
|
-
if
|
15
|
+
if has_executable_concern?(object)
|
16
16
|
@namespace ||= object.namespace
|
17
17
|
@task_queue ||= object.task_queue
|
18
18
|
@retry_policy ||= object.retry_policy
|
@@ -31,5 +31,13 @@ module Temporal
|
|
31
31
|
def task_list
|
32
32
|
@task_queue
|
33
33
|
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def has_executable_concern?(object)
|
38
|
+
object.singleton_class.included_modules.include?(Concerns::Executable)
|
39
|
+
rescue TypeError
|
40
|
+
false
|
41
|
+
end
|
34
42
|
end
|
35
43
|
end
|
data/lib/temporal/json.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Temporal
|
4
|
+
class Logger < ::Logger
|
5
|
+
SEVERITIES = %i[debug info warn error fatal unknown].freeze
|
6
|
+
|
7
|
+
SEVERITIES.each do |severity|
|
8
|
+
define_method severity do |message, data = {}|
|
9
|
+
super(message.to_s + ' ' + Oj.dump(data, mode: :strict))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(severity, message, data = {})
|
14
|
+
add(severity, message.to_s + ' ' + Oj.dump(data, mode: :strict))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/temporal/metadata.rb
CHANGED
@@ -2,6 +2,7 @@ require 'temporal/errors'
|
|
2
2
|
require 'temporal/metadata/activity'
|
3
3
|
require 'temporal/metadata/workflow'
|
4
4
|
require 'temporal/metadata/workflow_task'
|
5
|
+
require 'temporal/concerns/payloads'
|
5
6
|
|
6
7
|
module Temporal
|
7
8
|
module Metadata
|
@@ -10,6 +11,8 @@ module Temporal
|
|
10
11
|
WORKFLOW_TYPE = :workflow
|
11
12
|
|
12
13
|
class << self
|
14
|
+
include Concerns::Payloads
|
15
|
+
|
13
16
|
def generate(type, data, namespace = nil)
|
14
17
|
case type
|
15
18
|
when ACTIVITY_TYPE
|
@@ -26,7 +29,11 @@ module Temporal
|
|
26
29
|
private
|
27
30
|
|
28
31
|
def headers(fields)
|
29
|
-
|
32
|
+
result = {}
|
33
|
+
fields.each do |field, payload|
|
34
|
+
result[field] = from_payload(payload)
|
35
|
+
end
|
36
|
+
result
|
30
37
|
end
|
31
38
|
|
32
39
|
def activity_metadata_from(task, namespace)
|
@@ -39,7 +46,8 @@ module Temporal
|
|
39
46
|
workflow_run_id: task.workflow_execution.run_id,
|
40
47
|
workflow_id: task.workflow_execution.workflow_id,
|
41
48
|
workflow_name: task.workflow_type.name,
|
42
|
-
headers: headers(task.header&.fields
|
49
|
+
headers: headers(task.header&.fields),
|
50
|
+
heartbeat_details: from_details_payloads(task.heartbeat_details)
|
43
51
|
)
|
44
52
|
end
|
45
53
|
|
@@ -60,7 +68,7 @@ module Temporal
|
|
60
68
|
name: event.workflow_type.name,
|
61
69
|
run_id: event.original_execution_run_id,
|
62
70
|
attempt: event.attempt,
|
63
|
-
headers: headers(event.header&.fields
|
71
|
+
headers: headers(event.header&.fields)
|
64
72
|
)
|
65
73
|
end
|
66
74
|
end
|
@@ -3,9 +3,9 @@ require 'temporal/metadata/base'
|
|
3
3
|
module Temporal
|
4
4
|
module Metadata
|
5
5
|
class Activity < Base
|
6
|
-
attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers
|
6
|
+
attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers, :heartbeat_details
|
7
7
|
|
8
|
-
def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {})
|
8
|
+
def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {}, heartbeat_details:)
|
9
9
|
@namespace = namespace
|
10
10
|
@id = id
|
11
11
|
@name = name
|
@@ -15,6 +15,7 @@ module Temporal
|
|
15
15
|
@workflow_id = workflow_id
|
16
16
|
@workflow_name = workflow_name
|
17
17
|
@headers = headers
|
18
|
+
@heartbeat_details = heartbeat_details
|
18
19
|
|
19
20
|
freeze
|
20
21
|
end
|
@@ -22,6 +23,18 @@ module Temporal
|
|
22
23
|
def activity?
|
23
24
|
true
|
24
25
|
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
{
|
29
|
+
'namespace' => namespace,
|
30
|
+
'workflow_id' => workflow_id,
|
31
|
+
'workflow_name' => workflow_name,
|
32
|
+
'workflow_run_id' => workflow_run_id,
|
33
|
+
'activity_id' => id,
|
34
|
+
'activity_name' => name,
|
35
|
+
'attempt' => attempt
|
36
|
+
}
|
37
|
+
end
|
25
38
|
end
|
26
39
|
end
|
27
40
|
end
|
@@ -20,6 +20,17 @@ module Temporal
|
|
20
20
|
def workflow_task?
|
21
21
|
true
|
22
22
|
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
{
|
26
|
+
'namespace' => namespace,
|
27
|
+
'workflow_task_id' => id,
|
28
|
+
'workflow_name' => workflow_name,
|
29
|
+
'workflow_id' => workflow_id,
|
30
|
+
'workflow_run_id' => workflow_run_id,
|
31
|
+
'attempt' => attempt
|
32
|
+
}
|
33
|
+
end
|
23
34
|
end
|
24
35
|
end
|
25
36
|
end
|
@@ -1,25 +1,22 @@
|
|
1
1
|
require 'temporal/errors'
|
2
2
|
|
3
3
|
module Temporal
|
4
|
+
# See https://docs.temporal.io/docs/go/retries/ for go documentation of equivalent concepts.
|
4
5
|
class RetryPolicy < Struct.new(:interval, :backoff, :max_interval, :max_attempts,
|
5
|
-
:
|
6
|
+
:non_retriable_errors, keyword_init: true)
|
6
7
|
|
7
8
|
class InvalidRetryPolicy < ClientError; end
|
8
9
|
|
9
10
|
def validate!
|
10
|
-
unless interval && backoff
|
11
|
-
raise InvalidRetryPolicy, 'interval and backoff must be set'
|
11
|
+
unless max_attempts == 1 || (interval && backoff)
|
12
|
+
raise InvalidRetryPolicy, 'interval and backoff must be set if max_attempts != 1'
|
12
13
|
end
|
13
14
|
|
14
|
-
unless
|
15
|
-
raise InvalidRetryPolicy, 'max_attempts or expiration_interval must be set'
|
16
|
-
end
|
17
|
-
|
18
|
-
unless [interval, max_interval, expiration_interval].compact.all? { |arg| arg.is_a?(Integer) }
|
15
|
+
unless [interval, max_interval].compact.all? { |arg| arg.is_a?(Integer) }
|
19
16
|
raise InvalidRetryPolicy, 'All intervals must be specified in whole seconds'
|
20
17
|
end
|
21
18
|
|
22
|
-
unless [interval, max_interval
|
19
|
+
unless [interval, max_interval].compact.all? { |arg| arg > 0 }
|
23
20
|
raise InvalidRetryPolicy, 'All intervals must be greater than 0'
|
24
21
|
end
|
25
22
|
end
|
@@ -11,7 +11,7 @@ module Temporal
|
|
11
11
|
|
12
12
|
Result.new(true)
|
13
13
|
rescue StandardError => error # TODO: is there a need for a specialized error here?
|
14
|
-
logger.error("Saga execution aborted
|
14
|
+
logger.error("Saga execution aborted", { error: error.inspect })
|
15
15
|
logger.debug(error.backtrace.join("\n"))
|
16
16
|
|
17
17
|
saga.compensate
|
data/lib/temporal/testing.rb
CHANGED
@@ -9,21 +9,30 @@ require 'temporal/workflow/history/event_target'
|
|
9
9
|
module Temporal
|
10
10
|
module Testing
|
11
11
|
class LocalWorkflowContext
|
12
|
-
attr_reader :
|
12
|
+
attr_reader :metadata
|
13
13
|
|
14
|
-
def initialize(execution, workflow_id, run_id, disabled_releases,
|
14
|
+
def initialize(execution, workflow_id, run_id, disabled_releases, metadata)
|
15
15
|
@last_event_id = 0
|
16
16
|
@execution = execution
|
17
17
|
@run_id = run_id
|
18
18
|
@workflow_id = workflow_id
|
19
19
|
@disabled_releases = disabled_releases
|
20
|
-
@
|
20
|
+
@metadata = metadata
|
21
|
+
@completed = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def completed?
|
25
|
+
@completed
|
21
26
|
end
|
22
27
|
|
23
28
|
def logger
|
24
29
|
Temporal.logger
|
25
30
|
end
|
26
31
|
|
32
|
+
def headers
|
33
|
+
metadata.headers
|
34
|
+
end
|
35
|
+
|
27
36
|
def has_release?(change_name)
|
28
37
|
!disabled_releases.include?(change_name.to_s)
|
29
38
|
end
|
@@ -48,17 +57,25 @@ module Temporal
|
|
48
57
|
workflow_run_id: run_id,
|
49
58
|
workflow_id: workflow_id,
|
50
59
|
workflow_name: nil, # not yet used, but will be in the future
|
51
|
-
headers: execution_options.headers
|
60
|
+
headers: execution_options.headers,
|
61
|
+
heartbeat_details: nil
|
52
62
|
)
|
53
63
|
context = LocalActivityContext.new(metadata)
|
54
64
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
65
|
+
begin
|
66
|
+
result = activity_class.execute_in_context(context, input)
|
67
|
+
rescue StandardError => e
|
68
|
+
# Capture any failure from running the activity into the future
|
69
|
+
# instead of raising immediately in order to match the behavior of
|
70
|
+
# running against a Temporal server.
|
71
|
+
future.fail(e)
|
59
72
|
else
|
60
|
-
|
61
|
-
|
73
|
+
if context.async?
|
74
|
+
execution.register_future(context.async_token, future)
|
75
|
+
else
|
76
|
+
# Fulfill the future straight away for non-async activities
|
77
|
+
future.set(result)
|
78
|
+
end
|
62
79
|
end
|
63
80
|
|
64
81
|
future
|
@@ -66,11 +83,11 @@ module Temporal
|
|
66
83
|
|
67
84
|
def execute_activity!(activity_class, *input, **args)
|
68
85
|
future = execute_activity(activity_class, *input, **args)
|
69
|
-
|
86
|
+
result_or_exception = future.get
|
70
87
|
|
71
|
-
raise
|
88
|
+
raise result_or_exception if future.failed?
|
72
89
|
|
73
|
-
|
90
|
+
result_or_exception
|
74
91
|
end
|
75
92
|
|
76
93
|
def execute_local_activity(activity_class, *input, **args)
|
@@ -88,7 +105,8 @@ module Temporal
|
|
88
105
|
workflow_run_id: run_id,
|
89
106
|
workflow_id: workflow_id,
|
90
107
|
workflow_name: nil, # not yet used, but will be in the future
|
91
|
-
headers: execution_options.headers
|
108
|
+
headers: execution_options.headers,
|
109
|
+
heartbeat_details: nil
|
92
110
|
)
|
93
111
|
context = LocalActivityContext.new(metadata)
|
94
112
|
|
@@ -131,10 +149,12 @@ module Temporal
|
|
131
149
|
end
|
132
150
|
|
133
151
|
def complete(result = nil)
|
152
|
+
completed!
|
134
153
|
result
|
135
154
|
end
|
136
155
|
|
137
156
|
def fail(exception)
|
157
|
+
completed!
|
138
158
|
raise exception
|
139
159
|
end
|
140
160
|
|
@@ -169,6 +189,10 @@ module Temporal
|
|
169
189
|
|
170
190
|
attr_reader :execution, :run_id, :workflow_id, :disabled_releases
|
171
191
|
|
192
|
+
def completed!
|
193
|
+
@completed = true
|
194
|
+
end
|
195
|
+
|
172
196
|
def next_event_id
|
173
197
|
@last_event_id += 1
|
174
198
|
@last_event_id
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Temporal
|
2
|
+
module Testing
|
3
|
+
class WorkflowIDNotScheduled < ClientError; end
|
4
|
+
|
5
|
+
# When Temporal.schedule_workflow is called in a test in local mode, we defer the execution and do
|
6
|
+
# not do it automatically.
|
7
|
+
# You can execute them or inspect their cron schedules using this module.
|
8
|
+
module ScheduledWorkflows
|
9
|
+
def self.execute(workflow_id:)
|
10
|
+
Private::Store.execute(workflow_id: workflow_id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.execute_all
|
14
|
+
Private::Store.execute_all
|
15
|
+
end
|
16
|
+
|
17
|
+
# For someone who wants to assert that the schedule is what they expect.
|
18
|
+
# Populated by Temporal.schedule_workflow
|
19
|
+
# format: { <workflow_id>: <cron schedule string>, ... }
|
20
|
+
def self.cron_schedules
|
21
|
+
Private::Store.schedules
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.clear_all
|
25
|
+
Private::Store.clear_all
|
26
|
+
end
|
27
|
+
|
28
|
+
module Private
|
29
|
+
module Store
|
30
|
+
class << self
|
31
|
+
|
32
|
+
def schedules
|
33
|
+
@schedules ||= {}.freeze
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(workflow_id:, cron_schedule:, executor_lambda:)
|
37
|
+
new_schedules = schedules.dup
|
38
|
+
new_schedules[workflow_id] = cron_schedule
|
39
|
+
@schedules = new_schedules.freeze
|
40
|
+
|
41
|
+
new_scheduled_executions = scheduled_executions.dup
|
42
|
+
new_scheduled_executions[workflow_id] = executor_lambda
|
43
|
+
@scheduled_executions = new_scheduled_executions.freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
def clear_all
|
47
|
+
@scheduled_executions = {}.freeze
|
48
|
+
@schedules = {}.freeze
|
49
|
+
end
|
50
|
+
|
51
|
+
def execute(workflow_id:)
|
52
|
+
unless scheduled_executions.key?(workflow_id)
|
53
|
+
raise Temporal::Testing::WorkflowIDNotScheduled,
|
54
|
+
"There is no workflow with id #{workflow_id} that was scheduled with Temporal.schedule_workflow.\n"\
|
55
|
+
"Options: #{scheduled_executions.keys}"
|
56
|
+
end
|
57
|
+
|
58
|
+
scheduled_executions[workflow_id].call
|
59
|
+
end
|
60
|
+
|
61
|
+
def execute_all
|
62
|
+
scheduled_executions.values.each(&:call)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def scheduled_executions
|
68
|
+
@scheduled_executions ||= {}.freeze
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|