hatchet-sdk 0.2.0 → 0.3.0
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/.rubocop.yml +5 -1
- data/CHANGELOG.md +24 -0
- data/lib/hatchet/clients/grpc/admin.rb +6 -6
- data/lib/hatchet/clients/grpc/dispatcher.rb +33 -8
- data/lib/hatchet/condition_converter.rb +20 -12
- data/lib/hatchet/contracts/dispatcher/dispatcher_pb.rb +3 -1
- data/lib/hatchet/contracts/dispatcher/dispatcher_services_pb.rb +1 -0
- data/lib/hatchet/contracts/v1/dispatcher_pb.rb +23 -1
- data/lib/hatchet/contracts/v1/dispatcher_services_pb.rb +2 -0
- data/lib/hatchet/contracts/v1/shared/condition_pb.rb +3 -1
- data/lib/hatchet/contracts/v1/shared/trigger_pb.rb +17 -0
- data/lib/hatchet/contracts/v1/workflows_pb.rb +4 -3
- data/lib/hatchet/contracts/v1/workflows_services_pb.rb +1 -0
- data/lib/hatchet/contracts/workflows/workflows_pb.rb +2 -4
- data/lib/hatchet/contracts/workflows/workflows_services_pb.rb +1 -1
- data/lib/hatchet/durable_context.rb +102 -33
- data/lib/hatchet/engine_version.rb +50 -0
- data/lib/hatchet/eviction_policy.rb +60 -0
- data/lib/hatchet/exceptions.rb +26 -0
- data/lib/hatchet/task.rb +7 -0
- data/lib/hatchet/version.rb +1 -1
- data/lib/hatchet/worker/durable_event_listener.rb +735 -0
- data/lib/hatchet/worker/durable_eviction/cache.rb +205 -0
- data/lib/hatchet/worker/durable_eviction/manager.rb +233 -0
- data/lib/hatchet/worker/runner.rb +278 -53
- data/lib/hatchet/worker_obj.rb +59 -3
- data/lib/hatchet/workflow.rb +8 -4
- data/lib/hatchet-sdk.rb +13 -3
- data/sig/hatchet/clients/grpc/dispatcher.rbs +2 -0
- data/sig/hatchet/durable_context.rbs +8 -2
- data/sig/hatchet/engine_version.rbs +12 -0
- data/sig/hatchet/eviction_policy.rbs +14 -0
- data/sig/hatchet/exceptions.rbs +12 -0
- data/sig/hatchet/task.rbs +2 -0
- data/sig/hatchet/worker/durable_event_listener.rbs +31 -0
- data/sig/hatchet/worker/durable_eviction/cache.rbs +41 -0
- data/sig/hatchet/worker/durable_eviction/manager.rbs +37 -0
- data/sig/hatchet/worker/runner.rbs +7 -1
- data/sig/hatchet/worker_obj.rbs +3 -0
- data/sig/hatchet/workflow.rbs +1 -1
- data/sig/hatchet-sdk.rbs +1 -1
- metadata +15 -4
|
@@ -23,53 +23,123 @@ module Hatchet
|
|
|
23
23
|
# result = ctx.wait_for("event", Hatchet::UserEventCondition.new(event_key: "user:update"))
|
|
24
24
|
# end
|
|
25
25
|
class DurableContext < Context
|
|
26
|
+
# @return [Hatchet::WorkerRuntime::DurableEviction::DurableEvictionManager, nil]
|
|
27
|
+
attr_accessor :eviction_manager
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] The action key used by the eviction manager to
|
|
30
|
+
# identify this run invocation.
|
|
31
|
+
attr_accessor :action_key
|
|
32
|
+
|
|
33
|
+
# @return [Hatchet::WorkerRuntime::DurableEventListener, nil] New-style bidi
|
|
34
|
+
# listener. When set the context delegates through it instead of the
|
|
35
|
+
# legacy RegisterDurableEvent/ListenForDurableEvent path.
|
|
36
|
+
attr_accessor :durable_event_listener
|
|
37
|
+
|
|
38
|
+
# @return [Integer] Durable-task invocation count (>= 1).
|
|
39
|
+
attr_accessor :invocation_count
|
|
40
|
+
|
|
41
|
+
# @return [String, nil] Engine version string advertised via GetVersion.
|
|
42
|
+
attr_accessor :engine_version
|
|
43
|
+
|
|
26
44
|
# Sleep for a specified duration. The task is suspended and resumed
|
|
27
45
|
# by the engine after the duration expires.
|
|
28
46
|
#
|
|
47
|
+
# Delegates to {#wait_for} with a {Hatchet::SleepCondition} so that both
|
|
48
|
+
# sleeps and event waits share a single registration / eviction path.
|
|
49
|
+
#
|
|
29
50
|
# @param duration [Integer, String] Duration in seconds, or a duration string (e.g. "60s")
|
|
51
|
+
# @param label [String, nil] Optional wait label shown in durable event logs.
|
|
30
52
|
# @return [Hash, nil] Result from the sleep event
|
|
31
|
-
def sleep_for(duration:)
|
|
32
|
-
signal_key = "sleep_#{duration}"
|
|
33
|
-
|
|
53
|
+
def sleep_for(duration:, label: nil)
|
|
34
54
|
duration_str = duration.is_a?(String) ? duration : "#{duration}s"
|
|
55
|
+
duration_value = duration.is_a?(String) ? duration : duration.to_i
|
|
56
|
+
wait_index = increment_wait_index
|
|
57
|
+
signal_key = "sleep:#{duration_str}-#{wait_index}"
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
sleep_condition = ::V1::SleepMatchCondition.new(
|
|
38
|
-
base: ::V1::BaseMatchCondition.new(
|
|
39
|
-
readable_data_key: signal_key,
|
|
40
|
-
action: :QUEUE,
|
|
41
|
-
or_group_id: SecureRandom.uuid,
|
|
42
|
-
),
|
|
43
|
-
sleep_for: duration_str,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
conditions = ::V1::DurableEventListenerConditions.new(
|
|
47
|
-
sleep_conditions: [sleep_condition],
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Register the durable event
|
|
51
|
-
register_request = ::V1::RegisterDurableEventRequest.new(
|
|
52
|
-
task_id: @step_run_id,
|
|
53
|
-
signal_key: signal_key,
|
|
54
|
-
conditions: conditions,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
v1_dispatcher_stub.register_durable_event(register_request, metadata: @client.config.auth_metadata)
|
|
58
|
-
|
|
59
|
-
# Listen for the durable event via bidi stream
|
|
60
|
-
listen_for_event(signal_key)
|
|
59
|
+
wait_for(signal_key, Hatchet::SleepCondition.new(duration_value), label: label)
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
# Wait for a condition to be met (event or sleep).
|
|
64
63
|
# The task is suspended and resumed when the condition is satisfied.
|
|
65
64
|
#
|
|
65
|
+
# Register the durable wait with ``send_event`` first, then start eviction
|
|
66
|
+
# tracking only while blocked on ``wait_for_callback``.
|
|
67
|
+
#
|
|
66
68
|
# @param key [String] A unique key for this wait operation
|
|
67
69
|
# @param condition [Object] The condition to wait for (UserEventCondition, SleepCondition, Hash, etc.)
|
|
70
|
+
# @param label [String, nil] Optional wait label shown in durable event logs.
|
|
68
71
|
# @return [Hash] Result from the wait, including which condition was satisfied
|
|
69
|
-
def wait_for(key, condition)
|
|
72
|
+
def wait_for(key, condition, label: nil)
|
|
70
73
|
conditions = build_durable_conditions(key, condition)
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
if supports_durable_eviction?
|
|
76
|
+
invocation = @invocation_count || 1
|
|
77
|
+
|
|
78
|
+
event = Hatchet::WorkerRuntime::DurableEventListener::WaitForEvent.new(
|
|
79
|
+
wait_for_conditions: conditions,
|
|
80
|
+
label: label,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
ack = @durable_event_listener.send_event(@step_run_id, invocation, event)
|
|
84
|
+
|
|
85
|
+
with_eviction_wait(wait_kind: "wait_for", resource_id: key) do
|
|
86
|
+
result = @durable_event_listener.wait_for_callback(
|
|
87
|
+
@step_run_id,
|
|
88
|
+
invocation,
|
|
89
|
+
ack[:branch_id],
|
|
90
|
+
ack[:node_id],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
result[:payload] || {}
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
with_eviction_wait(wait_kind: "wait_for", resource_id: key) do
|
|
97
|
+
legacy_wait_for(key, conditions)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Monotonically increasing per-context wait index, used to disambiguate
|
|
105
|
+
# multiple sleep/wait calls with otherwise identical resource ids.
|
|
106
|
+
def increment_wait_index
|
|
107
|
+
@wait_index ||= 0
|
|
108
|
+
index = @wait_index
|
|
109
|
+
@wait_index += 1
|
|
110
|
+
index
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Wrap a block in mark_waiting/mark_active calls on the eviction manager
|
|
114
|
+
# when one is attached. Safe no-op when not.
|
|
115
|
+
def with_eviction_wait(wait_kind:, resource_id:)
|
|
116
|
+
mgr = @eviction_manager
|
|
117
|
+
key = @action_key
|
|
118
|
+
|
|
119
|
+
mgr.mark_waiting(key, wait_kind: wait_kind, resource_id: resource_id) if mgr && key
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
yield
|
|
123
|
+
ensure
|
|
124
|
+
mgr.mark_active(key) if mgr && key
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# True when this context can use the durable-eviction bidi protocol.
|
|
129
|
+
#
|
|
130
|
+
# Matches the Python SDK's feature-gate naming more closely than
|
|
131
|
+
# ``use_new_listener?`` while preserving the same behavior here.
|
|
132
|
+
def supports_durable_eviction?
|
|
133
|
+
return false unless @durable_event_listener
|
|
134
|
+
return false unless @engine_version
|
|
135
|
+
|
|
136
|
+
!Hatchet::EngineVersion.semver_less_than?(
|
|
137
|
+
@engine_version,
|
|
138
|
+
Hatchet::MinEngineVersion::DURABLE_EVICTION,
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def legacy_wait_for(key, conditions)
|
|
73
143
|
register_request = ::V1::RegisterDurableEventRequest.new(
|
|
74
144
|
task_id: @step_run_id,
|
|
75
145
|
signal_key: key,
|
|
@@ -93,8 +163,6 @@ module Hatchet
|
|
|
93
163
|
end
|
|
94
164
|
end
|
|
95
165
|
|
|
96
|
-
private
|
|
97
|
-
|
|
98
166
|
# Get or create the V1::V1Dispatcher::Stub for durable events.
|
|
99
167
|
def v1_dispatcher_stub
|
|
100
168
|
@v1_dispatcher_stub ||= ::V1::V1Dispatcher::Stub.new(
|
|
@@ -194,7 +262,8 @@ module Hatchet
|
|
|
194
262
|
def process_durable_condition(key, condition, or_group_id, sleep_conditions, user_event_conditions)
|
|
195
263
|
ConditionConverter.convert_condition(
|
|
196
264
|
condition,
|
|
197
|
-
action
|
|
265
|
+
# Do not force base.action. Leaving it unset keeps protobuf default semantics on the server path.
|
|
266
|
+
action: nil,
|
|
198
267
|
sleep_conditions: sleep_conditions,
|
|
199
268
|
user_event_conditions: user_event_conditions,
|
|
200
269
|
or_group_id: or_group_id,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hatchet
|
|
4
|
+
# Minimum engine versions required for specific SDK features.
|
|
5
|
+
#
|
|
6
|
+
# Mirrors :class:`hatchet_sdk.engine_version.MinEngineVersion` in the
|
|
7
|
+
# Python SDK.
|
|
8
|
+
module MinEngineVersion
|
|
9
|
+
SLOT_CONFIG = "v0.78.23"
|
|
10
|
+
DURABLE_EVICTION = "v0.80.0"
|
|
11
|
+
OBSERVABILITY = "v0.82.0"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Semver parsing + comparison helpers used to gate features by engine version.
|
|
15
|
+
module EngineVersion
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Parse a semver string like ``"v0.78.23"`` into ``[major, minor, patch]``.
|
|
19
|
+
#
|
|
20
|
+
# Returns ``[0, 0, 0]`` if parsing fails, matching the Python helper.
|
|
21
|
+
#
|
|
22
|
+
# @param version [String, nil] The version string (with or without a ``v`` prefix, optional ``-pre`` suffix)
|
|
23
|
+
# @return [Array(Integer, Integer, Integer)]
|
|
24
|
+
def parse_semver(version)
|
|
25
|
+
return [0, 0, 0] if version.nil?
|
|
26
|
+
|
|
27
|
+
v = version.to_s
|
|
28
|
+
v = v.sub(/\Av/, "")
|
|
29
|
+
v = v.split("-", 2).first || ""
|
|
30
|
+
|
|
31
|
+
parts = v.split(".")
|
|
32
|
+
return [0, 0, 0] if parts.length != 3
|
|
33
|
+
|
|
34
|
+
[Integer(parts[0]), Integer(parts[1]), Integer(parts[2])]
|
|
35
|
+
rescue ArgumentError, TypeError
|
|
36
|
+
[0, 0, 0]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param left_version [String, nil]
|
|
40
|
+
# @param right_version [String, nil]
|
|
41
|
+
# @return [Boolean] true if semver ``left_version`` is strictly less than ``right_version``
|
|
42
|
+
def semver_less_than?(left_version, right_version)
|
|
43
|
+
(parse_semver(left_version) <=> parse_semver(right_version)).negative?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
alias semver_less_than semver_less_than?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hatchet
|
|
4
|
+
# Task-scoped eviction parameters for *durable* tasks.
|
|
5
|
+
#
|
|
6
|
+
# Setting the durable task's eviction policy to ``nil`` means the task run is
|
|
7
|
+
# never eligible for eviction.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Hatchet::EvictionPolicy.new(
|
|
11
|
+
# ttl: 600, # 10 minutes, in seconds
|
|
12
|
+
# allow_capacity_eviction: true,
|
|
13
|
+
# priority: 0,
|
|
14
|
+
# )
|
|
15
|
+
class EvictionPolicy
|
|
16
|
+
# @return [Numeric, nil] Maximum continuous waiting duration in seconds before
|
|
17
|
+
# TTL-eligible eviction. Applies to time spent in SDK-instrumented
|
|
18
|
+
# "waiting" states (e.g. :meth:`DurableContext#sleep_for`,
|
|
19
|
+
# :meth:`DurableContext#wait_for`). ``nil`` disables TTL eviction.
|
|
20
|
+
attr_reader :ttl
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] Whether this task may be evicted under durable-slot pressure.
|
|
23
|
+
attr_reader :allow_capacity_eviction
|
|
24
|
+
|
|
25
|
+
# @return [Integer] Lower values are evicted first when multiple candidates exist.
|
|
26
|
+
attr_reader :priority
|
|
27
|
+
|
|
28
|
+
# @param ttl [Numeric, nil] TTL in seconds (or nil to disable TTL-based eviction)
|
|
29
|
+
# @param allow_capacity_eviction [Boolean]
|
|
30
|
+
# @param priority [Integer]
|
|
31
|
+
def initialize(ttl:, allow_capacity_eviction: true, priority: 0)
|
|
32
|
+
@ttl = ttl
|
|
33
|
+
@allow_capacity_eviction = allow_capacity_eviction
|
|
34
|
+
@priority = priority
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ==(other)
|
|
39
|
+
other.is_a?(EvictionPolicy) &&
|
|
40
|
+
other.ttl == ttl &&
|
|
41
|
+
other.allow_capacity_eviction == allow_capacity_eviction &&
|
|
42
|
+
other.priority == priority
|
|
43
|
+
end
|
|
44
|
+
alias eql? ==
|
|
45
|
+
|
|
46
|
+
def hash
|
|
47
|
+
[self.class, ttl, allow_capacity_eviction, priority].hash
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Shared sensible defaults.
|
|
52
|
+
#
|
|
53
|
+
# NOTE: When changing these values, update the :param eviction_policy: docstrings
|
|
54
|
+
# in :meth:`Workflow#durable_task` and :meth:`Client#durable_task` to match.
|
|
55
|
+
DEFAULT_DURABLE_TASK_EVICTION_POLICY = EvictionPolicy.new(
|
|
56
|
+
ttl: 15 * 60, # 15 minutes
|
|
57
|
+
allow_capacity_eviction: true,
|
|
58
|
+
priority: 0,
|
|
59
|
+
)
|
|
60
|
+
end
|
data/lib/hatchet/exceptions.rb
CHANGED
|
@@ -41,6 +41,32 @@ module Hatchet
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Raised by the engine when durable-task execution detects a non-deterministic
|
|
45
|
+
# replay (the workflow did something different compared to the recorded log).
|
|
46
|
+
class NonDeterminismError < Error
|
|
47
|
+
# @return [String, nil]
|
|
48
|
+
attr_reader :task_external_id
|
|
49
|
+
# @return [Integer, nil]
|
|
50
|
+
attr_reader :invocation_count
|
|
51
|
+
# @return [Integer, nil]
|
|
52
|
+
attr_reader :node_id
|
|
53
|
+
|
|
54
|
+
def initialize(message, task_external_id: nil, invocation_count: nil, node_id: nil)
|
|
55
|
+
@task_external_id = task_external_id
|
|
56
|
+
@invocation_count = invocation_count
|
|
57
|
+
@node_id = node_id
|
|
58
|
+
super(message)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Raised inside a durable task thread when the eviction manager decides to
|
|
63
|
+
# evict that invocation (e.g. TTL expired, capacity pressure, worker shutdown).
|
|
64
|
+
class DurableTaskEvictedError < Error
|
|
65
|
+
def initialize(message = "Durable task evicted")
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
44
70
|
# Raised when a workflow run fails with one or more task errors
|
|
45
71
|
class FailedRunError < Error
|
|
46
72
|
# @return [Array<TaskRunError>] The individual task run errors
|
data/lib/hatchet/task.rb
CHANGED
|
@@ -65,6 +65,9 @@ module Hatchet
|
|
|
65
65
|
# @return [Boolean] Whether this is a durable task
|
|
66
66
|
attr_reader :durable
|
|
67
67
|
|
|
68
|
+
# @return [Hatchet::EvictionPolicy, nil] Eviction policy for durable tasks
|
|
69
|
+
attr_reader :eviction_policy
|
|
70
|
+
|
|
68
71
|
# @return [Proc, nil] The task execution block
|
|
69
72
|
attr_reader :fn
|
|
70
73
|
|
|
@@ -108,6 +111,7 @@ module Hatchet
|
|
|
108
111
|
wait_for: [],
|
|
109
112
|
skip_if: [],
|
|
110
113
|
durable: false,
|
|
114
|
+
eviction_policy: nil,
|
|
111
115
|
workflow: nil,
|
|
112
116
|
client: nil,
|
|
113
117
|
deps: nil,
|
|
@@ -126,6 +130,7 @@ module Hatchet
|
|
|
126
130
|
@wait_for = wait_for
|
|
127
131
|
@skip_if = skip_if
|
|
128
132
|
@durable = durable
|
|
133
|
+
@eviction_policy = eviction_policy
|
|
129
134
|
@workflow = workflow
|
|
130
135
|
@client = client
|
|
131
136
|
@deps = deps
|
|
@@ -184,6 +189,8 @@ module Hatchet
|
|
|
184
189
|
conditions_proto = conditions_to_proto(config)
|
|
185
190
|
opts[:conditions] = conditions_proto if conditions_proto
|
|
186
191
|
|
|
192
|
+
opts[:is_durable] = @durable
|
|
193
|
+
|
|
187
194
|
::V1::CreateTaskOpts.new(**opts)
|
|
188
195
|
end
|
|
189
196
|
|
data/lib/hatchet/version.rb
CHANGED