temporal-ruby 0.0.0 → 0.0.1.pre.pre1
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/README.md +19 -42
- data/lib/gen/temporal/api/command/v1/message_pb.rb +146 -0
- data/lib/gen/temporal/api/common/v1/message_pb.rb +67 -0
- data/lib/gen/temporal/api/enums/v1/command_type_pb.rb +35 -0
- data/lib/gen/temporal/api/enums/v1/common_pb.rb +34 -0
- data/lib/gen/temporal/api/enums/v1/event_type_pb.rb +62 -0
- data/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb +60 -0
- data/lib/gen/temporal/api/enums/v1/namespace_pb.rb +31 -0
- data/lib/gen/temporal/api/enums/v1/query_pb.rb +31 -0
- data/lib/gen/temporal/api/enums/v1/task_queue_pb.rb +30 -0
- data/lib/gen/temporal/api/enums/v1/workflow_pb.rb +82 -0
- data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +55 -0
- data/lib/gen/temporal/api/failure/v1/message_pb.rb +81 -0
- data/lib/gen/temporal/api/filter/v1/message_pb.rb +38 -0
- data/lib/gen/temporal/api/history/v1/message_pb.rb +423 -0
- data/lib/gen/temporal/api/namespace/v1/message_pb.rb +55 -0
- data/lib/gen/temporal/api/query/v1/message_pb.rb +36 -0
- data/lib/gen/temporal/api/replication/v1/message_pb.rb +27 -0
- data/lib/gen/temporal/api/taskqueue/v1/message_pb.rb +60 -0
- data/lib/gen/temporal/api/version/v1/message_pb.rb +28 -0
- data/lib/gen/temporal/api/workflow/v1/message_pb.rb +83 -0
- data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +538 -0
- data/lib/gen/temporal/api/workflowservice/v1/service_pb.rb +19 -0
- data/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +223 -0
- data/lib/temporal-ruby.rb +1 -0
- data/lib/temporal.rb +137 -0
- data/lib/temporal/activity.rb +33 -0
- data/lib/temporal/activity/async_token.rb +34 -0
- data/lib/temporal/activity/context.rb +64 -0
- data/lib/temporal/activity/poller.rb +79 -0
- data/lib/temporal/activity/task_processor.rb +78 -0
- data/lib/temporal/activity/workflow_convenience_methods.rb +41 -0
- data/lib/temporal/client.rb +21 -0
- data/lib/temporal/client/errors.rb +8 -0
- data/lib/temporal/client/grpc_client.rb +345 -0
- data/lib/temporal/client/serializer.rb +31 -0
- data/lib/temporal/client/serializer/base.rb +23 -0
- data/lib/temporal/client/serializer/cancel_timer.rb +19 -0
- data/lib/temporal/client/serializer/complete_workflow.rb +20 -0
- data/lib/temporal/client/serializer/fail_workflow.rb +20 -0
- data/lib/temporal/client/serializer/failure.rb +29 -0
- data/lib/temporal/client/serializer/payload.rb +25 -0
- data/lib/temporal/client/serializer/record_marker.rb +23 -0
- data/lib/temporal/client/serializer/request_activity_cancellation.rb +19 -0
- data/lib/temporal/client/serializer/schedule_activity.rb +53 -0
- data/lib/temporal/client/serializer/start_child_workflow.rb +51 -0
- data/lib/temporal/client/serializer/start_timer.rb +20 -0
- data/lib/temporal/concerns/executable.rb +37 -0
- data/lib/temporal/concerns/typed.rb +40 -0
- data/lib/temporal/configuration.rb +44 -0
- data/lib/temporal/errors.rb +38 -0
- data/lib/temporal/executable_lookup.rb +25 -0
- data/lib/temporal/execution_options.rb +35 -0
- data/lib/temporal/json.rb +18 -0
- data/lib/temporal/metadata.rb +68 -0
- data/lib/temporal/metadata/activity.rb +27 -0
- data/lib/temporal/metadata/base.rb +17 -0
- data/lib/temporal/metadata/workflow.rb +22 -0
- data/lib/temporal/metadata/workflow_task.rb +25 -0
- data/lib/temporal/metrics.rb +37 -0
- data/lib/temporal/metrics_adapters/log.rb +33 -0
- data/lib/temporal/metrics_adapters/null.rb +9 -0
- data/lib/temporal/middleware/chain.rb +30 -0
- data/lib/temporal/middleware/entry.rb +9 -0
- data/lib/temporal/retry_policy.rb +27 -0
- data/lib/temporal/saga/concern.rb +23 -0
- data/lib/temporal/saga/result.rb +22 -0
- data/lib/temporal/saga/saga.rb +24 -0
- data/lib/temporal/testing.rb +50 -0
- data/lib/temporal/testing/future_registry.rb +27 -0
- data/lib/temporal/testing/local_activity_context.rb +17 -0
- data/lib/temporal/testing/local_workflow_context.rb +178 -0
- data/lib/temporal/testing/temporal_override.rb +121 -0
- data/lib/temporal/testing/workflow_execution.rb +44 -0
- data/lib/temporal/testing/workflow_override.rb +36 -0
- data/lib/temporal/thread_local_context.rb +14 -0
- data/lib/temporal/thread_pool.rb +63 -0
- data/lib/temporal/types.rb +7 -0
- data/lib/temporal/uuid.rb +19 -0
- data/lib/temporal/version.rb +1 -1
- data/lib/temporal/worker.rb +88 -0
- data/lib/temporal/workflow.rb +42 -0
- data/lib/temporal/workflow/command.rb +39 -0
- data/lib/temporal/workflow/command_state_machine.rb +48 -0
- data/lib/temporal/workflow/context.rb +243 -0
- data/lib/temporal/workflow/convenience_methods.rb +34 -0
- data/lib/temporal/workflow/dispatcher.rb +31 -0
- data/lib/temporal/workflow/execution_info.rb +51 -0
- data/lib/temporal/workflow/executor.rb +45 -0
- data/lib/temporal/workflow/future.rb +77 -0
- data/lib/temporal/workflow/history.rb +76 -0
- data/lib/temporal/workflow/history/event.rb +69 -0
- data/lib/temporal/workflow/history/event_target.rb +75 -0
- data/lib/temporal/workflow/history/window.rb +40 -0
- data/lib/temporal/workflow/poller.rb +67 -0
- data/lib/temporal/workflow/replay_aware_logger.rb +36 -0
- data/lib/temporal/workflow/state_manager.rb +342 -0
- data/lib/temporal/workflow/task_processor.rb +78 -0
- data/rbi/temporal-ruby.rbi +43 -0
- data/temporal.gemspec +10 -2
- metadata +186 -6
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'temporal/testing/local_workflow_context'
|
|
3
|
+
require 'temporal/testing/workflow_execution'
|
|
4
|
+
|
|
5
|
+
module Temporal
|
|
6
|
+
module Testing
|
|
7
|
+
module WorkflowOverride
|
|
8
|
+
def disabled_releases
|
|
9
|
+
@disabled_releases ||= Set.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def allow_all_releases
|
|
13
|
+
disabled_releases.clear
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def allow_release(release_name)
|
|
17
|
+
disabled_releases.delete(release_name.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def disable_release(release_name)
|
|
21
|
+
disabled_releases << release_name.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute_locally(*input)
|
|
25
|
+
workflow_id = SecureRandom.uuid
|
|
26
|
+
run_id = SecureRandom.uuid
|
|
27
|
+
execution = WorkflowExecution.new
|
|
28
|
+
context = Temporal::Testing::LocalWorkflowContext.new(
|
|
29
|
+
execution, workflow_id, run_id, disabled_releases
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
execute_in_context(context, input)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Provides context for Temporal::Activity::WorkflowConvenienceMethods
|
|
2
|
+
module Temporal
|
|
3
|
+
module ThreadLocalContext
|
|
4
|
+
WORKFLOW_CONTEXT_KEY = :temporal_workflow_context
|
|
5
|
+
|
|
6
|
+
def self.get
|
|
7
|
+
Thread.current[WORKFLOW_CONTEXT_KEY]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.set(context)
|
|
11
|
+
Thread.current[WORKFLOW_CONTEXT_KEY] = context
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# This class implements a very simple ThreadPool with the ability to
|
|
2
|
+
# block until at least one thread becomes available. This allows Pollers
|
|
3
|
+
# to only poll when there's an available thread in the pool.
|
|
4
|
+
#
|
|
5
|
+
# NOTE: There's a minor race condition that can occur between calling
|
|
6
|
+
# #wait_for_available_threads and #schedule, but should be rare
|
|
7
|
+
#
|
|
8
|
+
module Temporal
|
|
9
|
+
class ThreadPool
|
|
10
|
+
attr_reader :size
|
|
11
|
+
|
|
12
|
+
def initialize(size)
|
|
13
|
+
@size = size
|
|
14
|
+
@queue = Queue.new
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@availability = ConditionVariable.new
|
|
17
|
+
@available_threads = size
|
|
18
|
+
@pool = Array.new(size) do |i|
|
|
19
|
+
Thread.new { poll }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def wait_for_available_threads
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
while @available_threads <= 0
|
|
26
|
+
@availability.wait(@mutex)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schedule(&block)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@available_threads -= 1
|
|
34
|
+
@queue << block
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def shutdown
|
|
39
|
+
size.times do
|
|
40
|
+
schedule { throw EXIT_SYMBOL }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@pool.each(&:join)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
EXIT_SYMBOL = :exit
|
|
49
|
+
|
|
50
|
+
def poll
|
|
51
|
+
catch(EXIT_SYMBOL) do
|
|
52
|
+
loop do
|
|
53
|
+
job = @queue.pop
|
|
54
|
+
job.call
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
@available_threads += 1
|
|
57
|
+
@availability.signal
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# This is a simple UUIDv5 (SHA1) implementation adopted from:
|
|
2
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/digest/uuid.rb#L18
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Temporal
|
|
6
|
+
module UUID
|
|
7
|
+
def self.v5(uuid_namespace, name)
|
|
8
|
+
hash = Digest::SHA1.new
|
|
9
|
+
hash.update(uuid_namespace)
|
|
10
|
+
hash.update(name)
|
|
11
|
+
|
|
12
|
+
ary = hash.digest.unpack("NnnnnN")
|
|
13
|
+
ary[2] = (ary[2] & 0x0FFF) | (5 << 12)
|
|
14
|
+
ary[3] = (ary[3] & 0x3FFF) | 0x8000
|
|
15
|
+
|
|
16
|
+
"%08x-%04x-%04x-%04x-%04x%08x" % ary
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/temporal/version.rb
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'temporal/client'
|
|
2
|
+
require 'temporal/workflow/poller'
|
|
3
|
+
require 'temporal/activity/poller'
|
|
4
|
+
require 'temporal/execution_options'
|
|
5
|
+
require 'temporal/executable_lookup'
|
|
6
|
+
require 'temporal/middleware/entry'
|
|
7
|
+
|
|
8
|
+
module Temporal
|
|
9
|
+
class Worker
|
|
10
|
+
def initialize
|
|
11
|
+
@workflows = Hash.new { |hash, key| hash[key] = ExecutableLookup.new }
|
|
12
|
+
@activities = Hash.new { |hash, key| hash[key] = ExecutableLookup.new }
|
|
13
|
+
@pollers = []
|
|
14
|
+
@workflow_task_middleware = []
|
|
15
|
+
@activity_middleware = []
|
|
16
|
+
@shutting_down = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register_workflow(workflow_class, options = {})
|
|
20
|
+
execution_options = ExecutionOptions.new(workflow_class, options)
|
|
21
|
+
key = [execution_options.namespace, execution_options.task_queue]
|
|
22
|
+
|
|
23
|
+
@workflows[key].add(execution_options.name, workflow_class)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register_activity(activity_class, options = {})
|
|
27
|
+
execution_options = ExecutionOptions.new(activity_class, options)
|
|
28
|
+
key = [execution_options.namespace, execution_options.task_queue]
|
|
29
|
+
|
|
30
|
+
@activities[key].add(execution_options.name, activity_class)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_workflow_task_middleware(middleware_class, *args)
|
|
34
|
+
@workflow_task_middleware << Middleware::Entry.new(middleware_class, args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_activity_middleware(middleware_class, *args)
|
|
38
|
+
@activity_middleware << Middleware::Entry.new(middleware_class, args)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def start
|
|
42
|
+
workflows.each_pair do |(namespace, task_queue), lookup|
|
|
43
|
+
pollers << workflow_poller_for(namespace, task_queue, lookup)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
activities.each_pair do |(namespace, task_queue), lookup|
|
|
47
|
+
pollers << activity_poller_for(namespace, task_queue, lookup)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
trap_signals
|
|
51
|
+
|
|
52
|
+
pollers.each(&:start)
|
|
53
|
+
|
|
54
|
+
# wait until instructed to shut down
|
|
55
|
+
while !shutting_down? do
|
|
56
|
+
sleep 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stop
|
|
61
|
+
@shutting_down = true
|
|
62
|
+
pollers.each(&:stop)
|
|
63
|
+
pollers.each(&:wait)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :activities, :workflows, :pollers, :workflow_task_middleware, :activity_middleware
|
|
69
|
+
|
|
70
|
+
def shutting_down?
|
|
71
|
+
@shutting_down
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def workflow_poller_for(namespace, task_queue, lookup)
|
|
75
|
+
Workflow::Poller.new(namespace, task_queue, lookup.freeze, workflow_task_middleware)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def activity_poller_for(namespace, task_queue, lookup)
|
|
79
|
+
Activity::Poller.new(namespace, task_queue, lookup.freeze, activity_middleware)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def trap_signals
|
|
83
|
+
%w[TERM INT].each do |signal|
|
|
84
|
+
Signal.trap(signal) { stop }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'temporal/concerns/executable'
|
|
2
|
+
require 'temporal/workflow/convenience_methods'
|
|
3
|
+
require 'temporal/thread_local_context'
|
|
4
|
+
|
|
5
|
+
module Temporal
|
|
6
|
+
class Workflow
|
|
7
|
+
extend Concerns::Executable
|
|
8
|
+
extend ConvenienceMethods
|
|
9
|
+
|
|
10
|
+
def self.execute_in_context(context, input)
|
|
11
|
+
Temporal::ThreadLocalContext.set(context)
|
|
12
|
+
|
|
13
|
+
workflow = new(context)
|
|
14
|
+
result = workflow.execute(*input)
|
|
15
|
+
|
|
16
|
+
context.complete(result)
|
|
17
|
+
rescue StandardError, ScriptError => error
|
|
18
|
+
Temporal.logger.error("Workflow execution failed with: #{error.inspect}")
|
|
19
|
+
Temporal.logger.debug(error.backtrace.join("\n"))
|
|
20
|
+
|
|
21
|
+
context.fail(error)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(context)
|
|
25
|
+
@context = context
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute
|
|
29
|
+
raise NotImplementedError, '#execute method must be implemented by a subclass'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def workflow
|
|
35
|
+
@context
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def logger
|
|
39
|
+
workflow.logger
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Temporal
|
|
2
|
+
class Workflow
|
|
3
|
+
module Command
|
|
4
|
+
# TODO: Move these classes into their own directories under workflow/command/*
|
|
5
|
+
ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true)
|
|
6
|
+
StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true)
|
|
7
|
+
RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true)
|
|
8
|
+
RecordMarker = Struct.new(:name, :details, keyword_init: true)
|
|
9
|
+
StartTimer = Struct.new(:timeout, :timer_id, keyword_init: true)
|
|
10
|
+
CancelTimer = Struct.new(:timer_id, keyword_init: true)
|
|
11
|
+
CompleteWorkflow = Struct.new(:result, keyword_init: true)
|
|
12
|
+
FailWorkflow = Struct.new(:exception, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
# only these commands are supported right now
|
|
15
|
+
SCHEDULE_ACTIVITY_TYPE = :schedule_activity
|
|
16
|
+
START_CHILD_WORKFLOW_TYPE = :start_child_workflow
|
|
17
|
+
RECORD_MARKER_TYPE = :record_marker
|
|
18
|
+
START_TIMER_TYPE = :start_timer
|
|
19
|
+
CANCEL_TIMER_TYPE = :cancel_timer
|
|
20
|
+
COMPLETE_WORKFLOW_TYPE = :complete_workflow
|
|
21
|
+
FAIL_WORKFLOW_TYPE = :fail_workflow
|
|
22
|
+
|
|
23
|
+
COMMAND_CLASS_MAP = {
|
|
24
|
+
SCHEDULE_ACTIVITY_TYPE => ScheduleActivity,
|
|
25
|
+
START_CHILD_WORKFLOW_TYPE => StartChildWorkflow,
|
|
26
|
+
RECORD_MARKER_TYPE => RecordMarker,
|
|
27
|
+
START_TIMER_TYPE => StartTimer,
|
|
28
|
+
CANCEL_TIMER_TYPE => CancelTimer,
|
|
29
|
+
COMPLETE_WORKFLOW_TYPE => CompleteWorkflow,
|
|
30
|
+
FAIL_WORKFLOW_TYPE => FailWorkflow
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def self.generate(type, **args)
|
|
34
|
+
command_class = COMMAND_CLASS_MAP[type]
|
|
35
|
+
command_class.new(**args)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Temporal
|
|
2
|
+
class Workflow
|
|
3
|
+
class CommandStateMachine
|
|
4
|
+
NEW_STATE = :new
|
|
5
|
+
REQUESTED_STATE = :requested
|
|
6
|
+
SCHEDULED_STATE = :scheduled
|
|
7
|
+
STARTED_STATE = :started
|
|
8
|
+
COMPLETED_STATE = :completed
|
|
9
|
+
CANCELED_STATE = :canceled
|
|
10
|
+
FAILED_STATE = :failed
|
|
11
|
+
TIMED_OUT_STATE = :timed_out
|
|
12
|
+
|
|
13
|
+
attr_reader :state
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@state = NEW_STATE
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def requested
|
|
20
|
+
@state = REQUESTED_STATE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def schedule
|
|
24
|
+
@state = SCHEDULED_STATE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
@state = STARTED_STATE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def complete
|
|
32
|
+
@state = COMPLETED_STATE
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cancel
|
|
36
|
+
@state = CANCELED_STATE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fail
|
|
40
|
+
@state = FAILED_STATE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def time_out
|
|
44
|
+
@state = TIMED_OUT_STATE
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
require 'temporal/execution_options'
|
|
4
|
+
require 'temporal/errors'
|
|
5
|
+
require 'temporal/thread_local_context'
|
|
6
|
+
require 'temporal/workflow/history/event_target'
|
|
7
|
+
require 'temporal/workflow/command'
|
|
8
|
+
require 'temporal/workflow/future'
|
|
9
|
+
require 'temporal/workflow/replay_aware_logger'
|
|
10
|
+
require 'temporal/workflow/state_manager'
|
|
11
|
+
|
|
12
|
+
# This context class is available in the workflow implementation
|
|
13
|
+
# and provides context and methods for interacting with Temporal
|
|
14
|
+
#
|
|
15
|
+
module Temporal
|
|
16
|
+
class Workflow
|
|
17
|
+
class Context
|
|
18
|
+
def initialize(state_manager, dispatcher, metadata)
|
|
19
|
+
@state_manager = state_manager
|
|
20
|
+
@dispatcher = dispatcher
|
|
21
|
+
@metadata = metadata
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def logger
|
|
25
|
+
@logger ||= ReplayAwareLogger.new(Temporal.logger)
|
|
26
|
+
@logger.replay = state_manager.replay?
|
|
27
|
+
@logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def headers
|
|
31
|
+
metadata.headers
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def has_release?(release_name)
|
|
35
|
+
state_manager.release?(release_name.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def execute_activity(activity_class, *input, **args)
|
|
39
|
+
options = args.delete(:options) || {}
|
|
40
|
+
input << args unless args.empty?
|
|
41
|
+
|
|
42
|
+
execution_options = ExecutionOptions.new(activity_class, options)
|
|
43
|
+
|
|
44
|
+
command = Command::ScheduleActivity.new(
|
|
45
|
+
activity_id: options[:activity_id],
|
|
46
|
+
activity_type: execution_options.name,
|
|
47
|
+
input: input,
|
|
48
|
+
namespace: execution_options.namespace,
|
|
49
|
+
task_queue: execution_options.task_queue,
|
|
50
|
+
retry_policy: execution_options.retry_policy,
|
|
51
|
+
timeouts: execution_options.timeouts,
|
|
52
|
+
headers: execution_options.headers
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
target, cancelation_id = schedule_command(command)
|
|
56
|
+
future = Future.new(target, self, cancelation_id: cancelation_id)
|
|
57
|
+
|
|
58
|
+
dispatcher.register_handler(target, 'completed') do |result|
|
|
59
|
+
future.set(result)
|
|
60
|
+
future.callbacks.each { |callback| call_in_fiber(callback, result) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
dispatcher.register_handler(target, 'failed') do |exception|
|
|
64
|
+
future.fail(exception)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
future
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_activity!(activity_class, *input, **args)
|
|
71
|
+
future = execute_activity(activity_class, *input, **args)
|
|
72
|
+
result = future.get
|
|
73
|
+
|
|
74
|
+
raise result if future.failed?
|
|
75
|
+
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# TODO: how to handle failures?
|
|
80
|
+
def execute_local_activity(activity_class, *input, **args)
|
|
81
|
+
input << args unless args.empty?
|
|
82
|
+
|
|
83
|
+
side_effect do
|
|
84
|
+
# TODO: this probably requires a local context implementation
|
|
85
|
+
context = Activity::Context.new(nil, nil)
|
|
86
|
+
activity_class.execute_in_context(context, input)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def execute_workflow(workflow_class, *input, **args)
|
|
91
|
+
options = args.delete(:options) || {}
|
|
92
|
+
input << args unless args.empty?
|
|
93
|
+
|
|
94
|
+
execution_options = ExecutionOptions.new(workflow_class, options)
|
|
95
|
+
|
|
96
|
+
command = Command::StartChildWorkflow.new(
|
|
97
|
+
workflow_id: options[:workflow_id] || SecureRandom.uuid,
|
|
98
|
+
workflow_type: execution_options.name,
|
|
99
|
+
input: input,
|
|
100
|
+
namespace: execution_options.namespace,
|
|
101
|
+
task_queue: execution_options.task_queue,
|
|
102
|
+
retry_policy: execution_options.retry_policy,
|
|
103
|
+
timeouts: execution_options.timeouts,
|
|
104
|
+
headers: execution_options.headers
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
target, cancelation_id = schedule_command(command)
|
|
108
|
+
future = Future.new(target, self, cancelation_id: cancelation_id)
|
|
109
|
+
|
|
110
|
+
dispatcher.register_handler(target, 'completed') do |result|
|
|
111
|
+
future.set(result)
|
|
112
|
+
future.callbacks.each { |callback| call_in_fiber(callback, result) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
dispatcher.register_handler(target, 'failed') do |exception|
|
|
116
|
+
future.fail(exception)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
future
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def execute_workflow!(workflow_class, *input, **args)
|
|
123
|
+
future = execute_workflow(workflow_class, *input, **args)
|
|
124
|
+
result = future.get
|
|
125
|
+
|
|
126
|
+
raise result if future.failed?
|
|
127
|
+
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def side_effect(&block)
|
|
132
|
+
marker = state_manager.next_side_effect
|
|
133
|
+
return marker.last if marker
|
|
134
|
+
|
|
135
|
+
result = block.call
|
|
136
|
+
command = Command::RecordMarker.new(name: StateManager::SIDE_EFFECT_MARKER, details: result)
|
|
137
|
+
schedule_command(command)
|
|
138
|
+
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def sleep(timeout)
|
|
143
|
+
start_timer(timeout).wait
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def start_timer(timeout, timer_id = nil)
|
|
147
|
+
command = Command::StartTimer.new(timeout: timeout, timer_id: timer_id)
|
|
148
|
+
target, cancelation_id = schedule_command(command)
|
|
149
|
+
future = Future.new(target, self, cancelation_id: cancelation_id)
|
|
150
|
+
|
|
151
|
+
dispatcher.register_handler(target, 'fired') do |result|
|
|
152
|
+
future.set(result)
|
|
153
|
+
future.callbacks.each { |callback| call_in_fiber(callback, result) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
dispatcher.register_handler(target, 'canceled') do |exception|
|
|
157
|
+
future.fail(exception)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
future
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def cancel_timer(timer_id)
|
|
164
|
+
command = Command::CancelTimer.new(timer_id: timer_id)
|
|
165
|
+
schedule_command(command)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# TODO: check if workflow can be completed
|
|
169
|
+
def complete(result = nil)
|
|
170
|
+
command = Command::CompleteWorkflow.new(result: result)
|
|
171
|
+
schedule_command(command)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# TODO: check if workflow can be failed
|
|
175
|
+
def fail(exception)
|
|
176
|
+
command = Command::FailWorkflow.new(exception: exception)
|
|
177
|
+
schedule_command(command)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def wait_for_all(*futures)
|
|
181
|
+
futures.each(&:wait)
|
|
182
|
+
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def wait_for(future)
|
|
187
|
+
fiber = Fiber.current
|
|
188
|
+
|
|
189
|
+
dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do
|
|
190
|
+
fiber.resume if future.finished?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
Fiber.yield
|
|
194
|
+
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def now
|
|
199
|
+
state_manager.local_time
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def on_signal(&block)
|
|
203
|
+
target = History::EventTarget.workflow
|
|
204
|
+
|
|
205
|
+
dispatcher.register_handler(target, 'signaled') do |signal, input|
|
|
206
|
+
call_in_fiber(block, signal, input)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def cancel_activity(activity_id)
|
|
211
|
+
command = Command::RequestActivityCancellation.new(activity_id: activity_id)
|
|
212
|
+
|
|
213
|
+
schedule_command(command)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def cancel(target, cancelation_id)
|
|
217
|
+
case target.type
|
|
218
|
+
when History::EventTarget::ACTIVITY_TYPE
|
|
219
|
+
cancel_activity(cancelation_id)
|
|
220
|
+
when History::EventTarget::TIMER_TYPE
|
|
221
|
+
cancel_timer(cancelation_id)
|
|
222
|
+
else
|
|
223
|
+
raise "#{target} can not be canceled"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
attr_reader :state_manager, :dispatcher, :metadata
|
|
230
|
+
|
|
231
|
+
def schedule_command(command)
|
|
232
|
+
state_manager.schedule(command)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def call_in_fiber(block, *args)
|
|
236
|
+
Fiber.new do
|
|
237
|
+
Temporal::ThreadLocalContext.set(self)
|
|
238
|
+
block.call(*args)
|
|
239
|
+
end.resume
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|