Skip to content

Commit 11775af

Browse files
gruttmnafees
andauthored
feat: durable ruby (#3699)
* feat: durable ruby * chore: generate * fix: tests * fix: test * fix CI * version and changelog * fix gemlock * fix gemlock --------- Co-authored-by: Mohammed Nafees <hello@mnafees.me>
1 parent 0b54ad8 commit 11775af

57 files changed

Lines changed: 3225 additions & 143 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,5 @@ frontend/docs/lib/generated/
104104
hack/dev/psql-connect.sh
105105
CLAUDE.md
106106

107-
.cache/
107+
.cache
108+
.rspec_status
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
require_relative "worker"
6+
7+
def parse_payload(argv)
8+
return({ "ok" => true }) if argv.empty?
9+
10+
JSON.parse(argv.first)
11+
rescue JSON::ParserError => e
12+
warn "Invalid JSON payload: #{e.message}"
13+
warn %(Usage: bundle exec ruby push_event.rb '{"ok":true}')
14+
exit 1
15+
end
16+
17+
def main
18+
payload = parse_payload(ARGV)
19+
response = HATCHET.events.push(EVENT_KEY, payload)
20+
21+
puts "Pushed event #{EVENT_KEY}"
22+
puts response.inspect
23+
end
24+
25+
main if __FILE__ == $PROGRAM_NAME
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# frozen_string_literal: true
2+
3+
require "hatchet-sdk"
4+
5+
HATCHET = Hatchet::Client.new(debug: true) unless defined?(HATCHET)
6+
7+
EVICTION_TTL_SECONDS = 5
8+
LONG_SLEEP_SECONDS = 15
9+
CAPACITY_SLEEP_SECONDS = 20
10+
EVENT_KEY = "durable-eviction:event"
11+
12+
EVICTION_POLICY = Hatchet::EvictionPolicy.new(
13+
ttl: EVICTION_TTL_SECONDS,
14+
allow_capacity_eviction: true,
15+
priority: 0,
16+
)
17+
18+
CAPACITY_EVICTION_POLICY = Hatchet::EvictionPolicy.new(
19+
ttl: nil,
20+
allow_capacity_eviction: true,
21+
priority: 0,
22+
)
23+
24+
NON_EVICTABLE_POLICY = Hatchet::EvictionPolicy.new(
25+
ttl: nil,
26+
allow_capacity_eviction: false,
27+
priority: 0,
28+
)
29+
30+
CHILD_TASK = HATCHET.task(name: "child_task", execution_timeout: 60) do |_input, _ctx|
31+
sleep LONG_SLEEP_SECONDS
32+
{ "child_status" => "completed" }
33+
end
34+
35+
BULK_CHILD_TASK = HATCHET.task(name: "bulk_child_task", execution_timeout: 60) do |input, _ctx|
36+
sleep_for = (input["sleep_for"] || 0).to_i
37+
sleep sleep_for
38+
{ "sleep_for" => sleep_for, "status" => "completed" }
39+
end
40+
41+
EVICTABLE_SLEEP = HATCHET.durable_task(
42+
name: "evictable_sleep",
43+
execution_timeout: 300,
44+
eviction_policy: EVICTION_POLICY,
45+
) do |_input, ctx|
46+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
47+
{ "status" => "completed" }
48+
end
49+
50+
EVICTABLE_WAIT_FOR_EVENT = HATCHET.durable_task(
51+
name: "evictable_wait_for_event",
52+
execution_timeout: 300,
53+
eviction_policy: EVICTION_POLICY,
54+
) do |_input, ctx|
55+
ctx.wait_for(
56+
EVENT_KEY,
57+
Hatchet::UserEventCondition.new(event_key: EVENT_KEY, expression: "true"),
58+
)
59+
{ "status" => "completed" }
60+
end
61+
62+
EVICTABLE_CHILD_SPAWN = HATCHET.durable_task(
63+
name: "evictable_child_spawn",
64+
execution_timeout: 300,
65+
eviction_policy: EVICTION_POLICY,
66+
) do |_input, _ctx|
67+
child_result = CHILD_TASK.run
68+
{ "child" => child_result, "status" => "completed" }
69+
end
70+
71+
EVICTABLE_CHILD_BULK_SPAWN = HATCHET.durable_task(
72+
name: "evictable_child_bulk_spawn",
73+
execution_timeout: 300,
74+
eviction_policy: EVICTION_POLICY,
75+
) do |_input, _ctx|
76+
items = Array.new(3) do |i|
77+
BULK_CHILD_TASK.create_bulk_run_item(
78+
input: { "sleep_for" => (EVICTION_TTL_SECONDS + 5) * (i + 1) },
79+
key: "child#{i}",
80+
)
81+
end
82+
child_results = BULK_CHILD_TASK.run_many(items)
83+
{ "child_results" => child_results }
84+
end
85+
86+
MULTIPLE_EVICTION = HATCHET.durable_task(
87+
name: "multiple_eviction",
88+
execution_timeout: 300,
89+
eviction_policy: EVICTION_POLICY,
90+
) do |_input, ctx|
91+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
92+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
93+
{ "status" => "completed" }
94+
end
95+
96+
CAPACITY_EVICTABLE_SLEEP = HATCHET.durable_task(
97+
name: "capacity_evictable_sleep",
98+
execution_timeout: 300,
99+
eviction_policy: CAPACITY_EVICTION_POLICY,
100+
) do |_input, ctx|
101+
ctx.sleep_for(duration: CAPACITY_SLEEP_SECONDS)
102+
{ "status" => "completed" }
103+
end
104+
105+
NON_EVICTABLE_SLEEP = HATCHET.durable_task(
106+
name: "non_evictable_sleep",
107+
execution_timeout: 300,
108+
eviction_policy: NON_EVICTABLE_POLICY,
109+
) do |_input, ctx|
110+
ctx.sleep_for(duration: 10)
111+
{ "status" => "completed" }
112+
end
113+
114+
def main
115+
worker = HATCHET.worker(
116+
"eviction-worker",
117+
workflows: [
118+
EVICTABLE_SLEEP,
119+
EVICTABLE_WAIT_FOR_EVENT,
120+
EVICTABLE_CHILD_SPAWN,
121+
EVICTABLE_CHILD_BULK_SPAWN,
122+
MULTIPLE_EVICTION,
123+
CAPACITY_EVICTABLE_SLEEP,
124+
NON_EVICTABLE_SLEEP,
125+
CHILD_TASK,
126+
BULK_CHILD_TASK,
127+
],
128+
)
129+
worker.start
130+
end
131+
132+
main if __FILE__ == $PROGRAM_NAME

sdks/ruby/examples/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: ../src
33
specs:
4-
hatchet-sdk (0.2.0)
4+
hatchet-sdk (0.3.0)
55
concurrent-ruby (>= 1.1)
66
faraday (~> 2.0)
77
faraday-multipart
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
require_relative "worker"
6+
7+
def parse_payload(argv)
8+
return({ "ok" => true }) if argv.empty?
9+
10+
JSON.parse(argv.first)
11+
rescue JSON::ParserError => e
12+
warn "Invalid JSON payload: #{e.message}"
13+
warn %(Usage: bundle exec ruby push_event.rb '{"ok":true}')
14+
exit 1
15+
end
16+
17+
def main
18+
payload = parse_payload(ARGV)
19+
response = HATCHET.events.push(EVENT_KEY, payload)
20+
21+
puts "Pushed event #{EVENT_KEY}"
22+
puts response.inspect
23+
end
24+
25+
main if __FILE__ == $PROGRAM_NAME
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# frozen_string_literal: true
2+
3+
require "hatchet-sdk"
4+
5+
HATCHET = Hatchet::Client.new(debug: true) unless defined?(HATCHET)
6+
7+
EVICTION_TTL_SECONDS = 5
8+
LONG_SLEEP_SECONDS = 15
9+
CAPACITY_SLEEP_SECONDS = 20
10+
EVENT_KEY = "durable-eviction:event"
11+
12+
EVICTION_POLICY = Hatchet::EvictionPolicy.new(
13+
ttl: EVICTION_TTL_SECONDS,
14+
allow_capacity_eviction: true,
15+
priority: 0,
16+
)
17+
18+
CAPACITY_EVICTION_POLICY = Hatchet::EvictionPolicy.new(
19+
ttl: nil,
20+
allow_capacity_eviction: true,
21+
priority: 0,
22+
)
23+
24+
NON_EVICTABLE_POLICY = Hatchet::EvictionPolicy.new(
25+
ttl: nil,
26+
allow_capacity_eviction: false,
27+
priority: 0,
28+
)
29+
30+
CHILD_TASK = HATCHET.task(name: "child_task", execution_timeout: 60) do |_input, _ctx|
31+
sleep LONG_SLEEP_SECONDS
32+
{ "child_status" => "completed" }
33+
end
34+
35+
BULK_CHILD_TASK = HATCHET.task(name: "bulk_child_task", execution_timeout: 60) do |input, _ctx|
36+
sleep_for = (input["sleep_for"] || 0).to_i
37+
sleep sleep_for
38+
{ "sleep_for" => sleep_for, "status" => "completed" }
39+
end
40+
41+
EVICTABLE_SLEEP = HATCHET.durable_task(
42+
name: "evictable_sleep",
43+
execution_timeout: 300,
44+
eviction_policy: EVICTION_POLICY,
45+
) do |_input, ctx|
46+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
47+
{ "status" => "completed" }
48+
end
49+
50+
EVICTABLE_WAIT_FOR_EVENT = HATCHET.durable_task(
51+
name: "evictable_wait_for_event",
52+
execution_timeout: 300,
53+
eviction_policy: EVICTION_POLICY,
54+
) do |_input, ctx|
55+
ctx.wait_for(
56+
EVENT_KEY,
57+
Hatchet::UserEventCondition.new(event_key: EVENT_KEY, expression: "true"),
58+
)
59+
{ "status" => "completed" }
60+
end
61+
62+
EVICTABLE_CHILD_SPAWN = HATCHET.durable_task(
63+
name: "evictable_child_spawn",
64+
execution_timeout: 300,
65+
eviction_policy: EVICTION_POLICY,
66+
) do |_input, _ctx|
67+
child_result = CHILD_TASK.run
68+
{ "child" => child_result, "status" => "completed" }
69+
end
70+
71+
EVICTABLE_CHILD_BULK_SPAWN = HATCHET.durable_task(
72+
name: "evictable_child_bulk_spawn",
73+
execution_timeout: 300,
74+
eviction_policy: EVICTION_POLICY,
75+
) do |_input, _ctx|
76+
items = Array.new(3) do |i|
77+
BULK_CHILD_TASK.create_bulk_run_item(
78+
input: { "sleep_for" => (EVICTION_TTL_SECONDS + 5) * (i + 1) },
79+
key: "child#{i}",
80+
)
81+
end
82+
child_results = BULK_CHILD_TASK.run_many(items)
83+
{ "child_results" => child_results }
84+
end
85+
86+
MULTIPLE_EVICTION = HATCHET.durable_task(
87+
name: "multiple_eviction",
88+
execution_timeout: 300,
89+
eviction_policy: EVICTION_POLICY,
90+
) do |_input, ctx|
91+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
92+
ctx.sleep_for(duration: LONG_SLEEP_SECONDS)
93+
{ "status" => "completed" }
94+
end
95+
96+
CAPACITY_EVICTABLE_SLEEP = HATCHET.durable_task(
97+
name: "capacity_evictable_sleep",
98+
execution_timeout: 300,
99+
eviction_policy: CAPACITY_EVICTION_POLICY,
100+
) do |_input, ctx|
101+
ctx.sleep_for(duration: CAPACITY_SLEEP_SECONDS)
102+
{ "status" => "completed" }
103+
end
104+
105+
NON_EVICTABLE_SLEEP = HATCHET.durable_task(
106+
name: "non_evictable_sleep",
107+
execution_timeout: 300,
108+
eviction_policy: NON_EVICTABLE_POLICY,
109+
) do |_input, ctx|
110+
ctx.sleep_for(duration: 10)
111+
{ "status" => "completed" }
112+
end
113+
114+
def main
115+
worker = HATCHET.worker(
116+
"eviction-worker",
117+
workflows: [
118+
EVICTABLE_SLEEP,
119+
EVICTABLE_WAIT_FOR_EVENT,
120+
EVICTABLE_CHILD_SPAWN,
121+
EVICTABLE_CHILD_BULK_SPAWN,
122+
MULTIPLE_EVICTION,
123+
CAPACITY_EVICTABLE_SLEEP,
124+
NON_EVICTABLE_SLEEP,
125+
CHILD_TASK,
126+
BULK_CHILD_TASK,
127+
],
128+
)
129+
worker.start
130+
end
131+
132+
main if __FILE__ == $PROGRAM_NAME

sdks/ruby/generate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ generate_proto() {
3838
"events/events.proto"
3939
"workflows/workflows.proto"
4040
"v1/shared/condition.proto"
41+
"v1/shared/trigger.proto"
4142
"v1/dispatcher.proto"
4243
"v1/workflows.proto"
4344
)

sdks/ruby/src/.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Metrics/MethodLength:
5252
- method_call
5353

5454
Metrics/ClassLength:
55-
Max: 300
55+
Max: 450
5656
CountAsOne:
5757
- array
5858
- hash

sdks/ruby/src/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to Hatchet's Ruby SDK will be documented in this changelog.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0] - 2026-04-28
9+
10+
### Added
11+
12+
- Durable execution primitives for Ruby workers, including `Hatchet::DurableContext`.
13+
- Durable eviction support via `Hatchet::EvictionPolicy` and worker-side eviction management/cache.
14+
- Engine-version gating helpers (`Hatchet::MinEngineVersion`, semver parsing/comparison utilities).
15+
- Durable eviction examples for Ruby (`worker` and `push_event`) in both SDK and top-level examples.
16+
- New exception and type-surface additions for durable features.
17+
18+
### Changed
19+
20+
- Worker runtime and runner internals to support durable replay, event waits, and eviction lifecycle behavior.
21+
- gRPC dispatcher/admin clients and generated contracts to align with durable execution and eviction flows.
22+
- Task/workflow definitions and worker object wiring to expose durable/eviction configuration in the public API.
23+
- RBS signatures expanded across durable context, eviction policy/manager/cache, worker runner, task/workflow, and gRPC clients.
24+
- Test coverage expanded with focused specs for durable context, eviction manager/cache, listener behavior, runner integration, and engine version helpers.
25+
826
## [0.2.0] - 2026-03-03
927

1028
### Added

sdks/ruby/src/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
hatchet-sdk (0.2.0)
4+
hatchet-sdk (0.3.0)
55
concurrent-ruby (>= 1.1)
66
faraday (~> 2.0)
77
faraday-multipart

0 commit comments

Comments
 (0)