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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/README.md +23 -3
  4. data/lib/gen/temporal/api/command/v1/message_pb.rb +1 -1
  5. data/lib/gen/temporal/api/enums/v1/common_pb.rb +7 -0
  6. data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +5 -6
  7. data/lib/gen/temporal/api/version/v1/message_pb.rb +19 -8
  8. data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +19 -8
  9. data/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +0 -3
  10. data/lib/temporal.rb +104 -0
  11. data/lib/temporal/activity/context.rb +5 -1
  12. data/lib/temporal/activity/poller.rb +26 -9
  13. data/lib/temporal/activity/task_processor.rb +33 -20
  14. data/lib/temporal/client/converter/base.rb +35 -0
  15. data/lib/temporal/client/converter/composite.rb +49 -0
  16. data/lib/temporal/client/converter/payload/bytes.rb +30 -0
  17. data/lib/temporal/client/converter/payload/json.rb +28 -0
  18. data/lib/temporal/client/converter/payload/nil.rb +27 -0
  19. data/lib/temporal/client/grpc_client.rb +102 -27
  20. data/lib/temporal/client/retryer.rb +49 -0
  21. data/lib/temporal/client/serializer.rb +2 -0
  22. data/lib/temporal/client/serializer/cancel_timer.rb +2 -2
  23. data/lib/temporal/client/serializer/complete_workflow.rb +6 -4
  24. data/lib/temporal/client/serializer/continue_as_new.rb +37 -0
  25. data/lib/temporal/client/serializer/fail_workflow.rb +2 -2
  26. data/lib/temporal/client/serializer/failure.rb +4 -2
  27. data/lib/temporal/client/serializer/record_marker.rb +6 -4
  28. data/lib/temporal/client/serializer/request_activity_cancellation.rb +2 -2
  29. data/lib/temporal/client/serializer/retry_policy.rb +24 -0
  30. data/lib/temporal/client/serializer/schedule_activity.rb +8 -20
  31. data/lib/temporal/client/serializer/start_child_workflow.rb +9 -20
  32. data/lib/temporal/client/serializer/start_timer.rb +2 -2
  33. data/lib/temporal/concerns/payloads.rb +51 -0
  34. data/lib/temporal/configuration.rb +31 -4
  35. data/lib/temporal/error_handler.rb +11 -0
  36. data/lib/temporal/errors.rb +24 -0
  37. data/lib/temporal/execution_options.rb +9 -1
  38. data/lib/temporal/json.rb +3 -1
  39. data/lib/temporal/logger.rb +17 -0
  40. data/lib/temporal/metadata.rb +11 -3
  41. data/lib/temporal/metadata/activity.rb +15 -2
  42. data/lib/temporal/metadata/workflow.rb +8 -0
  43. data/lib/temporal/metadata/workflow_task.rb +11 -0
  44. data/lib/temporal/retry_policy.rb +6 -9
  45. data/lib/temporal/saga/concern.rb +1 -1
  46. data/lib/temporal/testing.rb +1 -0
  47. data/lib/temporal/testing/future_registry.rb +1 -1
  48. data/lib/temporal/testing/local_activity_context.rb +1 -1
  49. data/lib/temporal/testing/local_workflow_context.rb +38 -14
  50. data/lib/temporal/testing/scheduled_workflows.rb +75 -0
  51. data/lib/temporal/testing/temporal_override.rb +35 -7
  52. data/lib/temporal/testing/workflow_override.rb +6 -1
  53. data/lib/temporal/version.rb +1 -1
  54. data/lib/temporal/worker.rb +28 -10
  55. data/lib/temporal/workflow.rb +8 -2
  56. data/lib/temporal/workflow/command.rb +3 -0
  57. data/lib/temporal/workflow/context.rb +40 -5
  58. data/lib/temporal/workflow/errors.rb +39 -0
  59. data/lib/temporal/workflow/executor.rb +1 -1
  60. data/lib/temporal/workflow/future.rb +18 -6
  61. data/lib/temporal/workflow/history/event.rb +1 -3
  62. data/lib/temporal/workflow/history/event_target.rb +4 -0
  63. data/lib/temporal/workflow/history/window.rb +1 -1
  64. data/lib/temporal/workflow/poller.rb +41 -13
  65. data/lib/temporal/workflow/replay_aware_logger.rb +4 -4
  66. data/lib/temporal/workflow/state_manager.rb +33 -52
  67. data/lib/temporal/workflow/task_processor.rb +41 -11
  68. metadata +21 -9
  69. 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
@@ -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 object.singleton_class.included_modules.include?(Concerns::Executable)
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
@@ -4,7 +4,9 @@ require 'oj'
4
4
  module Temporal
5
5
  module JSON
6
6
  OJ_OPTIONS = {
7
- mode: :object
7
+ mode: :object,
8
+ # use ruby's built-in serialization. If nil, OJ seems to default to ~15 decimal places of precision
9
+ float_precision: 0
8
10
  }.freeze
9
11
 
10
12
  def self.serialize(value)
@@ -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
@@ -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
- fields.transform_values { |v| v[:data] }
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.to_h)
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.to_h)
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
@@ -17,6 +17,14 @@ module Temporal
17
17
  def workflow?
18
18
  true
19
19
  end
20
+
21
+ def to_h
22
+ {
23
+ 'workflow_name' => name,
24
+ 'workflow_run_id' => run_id,
25
+ 'attempt' => attempt
26
+ }
27
+ end
20
28
  end
21
29
  end
22
30
  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
- :expiration_interval, :non_retriable_errors, keyword_init: true)
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 max_attempts || expiration_interval
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, expiration_interval].compact.all? { |arg| arg > 0 }
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: #{error.inspect}")
14
+ logger.error("Saga execution aborted", { error: error.inspect })
15
15
  logger.debug(error.backtrace.join("\n"))
16
16
 
17
17
  saga.compensate
@@ -1,5 +1,6 @@
1
1
  require 'temporal/testing/temporal_override'
2
2
  require 'temporal/testing/workflow_override'
3
+ require 'temporal/testing/scheduled_workflows'
3
4
 
4
5
  module Temporal
5
6
  module Testing
@@ -16,7 +16,7 @@ module Temporal
16
16
  end
17
17
 
18
18
  def fail(token, error)
19
- store[token].fail(error.class.name, error.message)
19
+ store[token].fail(error)
20
20
  end
21
21
 
22
22
  private
@@ -10,7 +10,7 @@ module Temporal
10
10
  end
11
11
 
12
12
  def heartbeat(details = nil)
13
- raise NotImplementedError, 'not yet available for testing'
13
+ # behavior is not yet testable in local mode
14
14
  end
15
15
  end
16
16
  end
@@ -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 :headers
12
+ attr_reader :metadata
13
13
 
14
- def initialize(execution, workflow_id, run_id, disabled_releases, headers = {})
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
- @headers = headers
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
- result = activity_class.execute_in_context(context, input)
56
-
57
- if context.async?
58
- execution.register_future(context.async_token, future)
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
- # Fulfil the future straigt away for non-async activities
61
- future.set(result)
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
- result = future.get
86
+ result_or_exception = future.get
70
87
 
71
- raise future.exception if future.failed?
88
+ raise result_or_exception if future.failed?
72
89
 
73
- result
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