temporal-ruby 0.0.1.pre.pre1 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|