hatchet-sdk 0.3.0 → 0.4.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 +0 -3
- data/CHANGELOG.md +12 -0
- data/lib/hatchet/clients/grpc/admin.rb +107 -0
- data/lib/hatchet/contracts/v1/dispatcher_pb.rb +1 -1
- data/lib/hatchet/contracts/v1/workflows_pb.rb +3 -1
- data/lib/hatchet/durable_context.rb +2 -0
- data/lib/hatchet/exceptions.rb +28 -0
- data/lib/hatchet/idempotency.rb +27 -0
- data/lib/hatchet/version.rb +1 -1
- data/lib/hatchet/worker/durable_event_listener.rb +3 -1
- data/lib/hatchet/workflow.rb +8 -0
- data/lib/hatchet-sdk.rb +66 -74
- data/sig/hatchet/idempotency.rbs +9 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4ab895cedb8b0d897c4c9001a9aae71b2502ca0c11a8f56bcf8f177648236dd
|
|
4
|
+
data.tar.gz: b7ddc732b63d5465fb37aba0cdd8899294a1fcf4dbdd83e99a69aeca3875cde0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db979be9acf23c7d580260972f1a35b9757e420b8a05053ab35f10c094510bafebaa4c64c69aff6e6580f681a0ca86ba03ac108ac1e3e7fae10f370ed7015dff
|
|
7
|
+
data.tar.gz: a7e3ba74d34509e2854d2a98e12c6f5419652217266db4eab276c9065e74f0338e585e6b647649a6e82b171da8321e29c40b28146a052522339399673076dc45
|
data/.rubocop.yml
CHANGED
|
@@ -28,9 +28,6 @@ Style/FrozenStringLiteralComment:
|
|
|
28
28
|
Style/Documentation:
|
|
29
29
|
Enabled: false
|
|
30
30
|
|
|
31
|
-
# TODO(gregfurman): Requires refactoring classes in lib/hatchet-sdk.rb:73:1
|
|
32
|
-
Style/OneClassPerFile:
|
|
33
|
-
Enabled: false
|
|
34
31
|
|
|
35
32
|
Style/TrailingCommaInArguments:
|
|
36
33
|
EnforcedStyleForMultiline: consistent_comma
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to Hatchet's Ruby SDK will be documented in this changelog.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-06-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Adds support for defining **idempotency keys** on workflows and standalone tasks via an `idempotency` option, which ensures that they're only run once in a provided time window, based on a CEL expression. Triggers that collide with an existing run raise an `IdempotencyCollisionError` containing the existing run's ID.
|
|
13
|
+
|
|
14
|
+
## [0.3.1] - 2026-06-12
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed an issue where errors raised by child tasks spawned inside a durable parent task were not propagated back to the parent. The parent can now catch the child's error and handle it gracefully.
|
|
19
|
+
|
|
8
20
|
## [0.3.0] - 2026-04-28
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "google/protobuf/timestamp_pb"
|
|
5
|
+
require "google/rpc/status_pb"
|
|
5
6
|
|
|
6
7
|
module Hatchet
|
|
7
8
|
module Clients
|
|
@@ -93,6 +94,9 @@ module Hatchet
|
|
|
93
94
|
response = @v0_stub.trigger_workflow(request, metadata: @config.auth_metadata)
|
|
94
95
|
response.workflow_run_id
|
|
95
96
|
rescue ::GRPC::AlreadyExists => e
|
|
97
|
+
run_id = extract_idempotency_run_id(e)
|
|
98
|
+
raise(IdempotencyCollisionError, run_id) if run_id
|
|
99
|
+
|
|
96
100
|
raise DedupeViolationError, "Deduplication violation: #{e.message}"
|
|
97
101
|
rescue ::GRPC::ResourceExhausted => e
|
|
98
102
|
raise ResourceExhaustedError, e.message
|
|
@@ -108,6 +112,7 @@ module Hatchet
|
|
|
108
112
|
# @param workflow_name [String] The workflow name (will be namespaced)
|
|
109
113
|
# @param items [Array<Hash>] Array of { input:, options: } items
|
|
110
114
|
# @return [Array<String>] Array of workflow run IDs
|
|
115
|
+
# @raise [BulkTriggerIdempotencyCollisionError] If any runs collide on idempotency keys
|
|
111
116
|
def bulk_trigger_workflow(workflow_name, items)
|
|
112
117
|
ensure_connected!
|
|
113
118
|
|
|
@@ -150,6 +155,12 @@ module Hatchet
|
|
|
150
155
|
response = @v0_stub.bulk_trigger_workflow(bulk_request, metadata: @config.auth_metadata)
|
|
151
156
|
all_run_ids.concat(response.workflow_run_ids.to_a)
|
|
152
157
|
end
|
|
158
|
+
rescue ::GRPC::AlreadyExists => e
|
|
159
|
+
collision = extract_bulk_trigger_idempotency_collision(e)
|
|
160
|
+
|
|
161
|
+
raise collision if collision
|
|
162
|
+
|
|
163
|
+
raise Error, "gRPC error triggering bulk workflow: #{e.class}: #{e.message}"
|
|
153
164
|
rescue ::GRPC::ResourceExhausted => e
|
|
154
165
|
raise ResourceExhaustedError, e.message
|
|
155
166
|
rescue ::GRPC::BadStatus => e
|
|
@@ -284,6 +295,102 @@ module Hatchet
|
|
|
284
295
|
end
|
|
285
296
|
end
|
|
286
297
|
|
|
298
|
+
def extract_idempotency_run_id(grpc_error)
|
|
299
|
+
status_bin = grpc_error.to_status.metadata&.[]("grpc-status-details-bin")
|
|
300
|
+
return nil unless status_bin
|
|
301
|
+
|
|
302
|
+
rpc_status = Google::Rpc::Status.decode(status_bin.b)
|
|
303
|
+
rpc_status.details.each do |any|
|
|
304
|
+
next unless any.type_url.include?("IdempotencyCollisionError") &&
|
|
305
|
+
!any.type_url.include?("BulkTriggerIdempotencyCollisionError")
|
|
306
|
+
|
|
307
|
+
return ::V1::IdempotencyCollisionError.decode(any.value).existing_run_external_id
|
|
308
|
+
end
|
|
309
|
+
nil
|
|
310
|
+
rescue StandardError
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def extract_bulk_trigger_idempotency_collision(grpc_error)
|
|
315
|
+
status_bin = grpc_error.to_status.metadata&.[]("grpc-status-details-bin")
|
|
316
|
+
return nil unless status_bin
|
|
317
|
+
|
|
318
|
+
rpc_status = Google::Rpc::Status.decode(status_bin.b)
|
|
319
|
+
rpc_status.details.each do |any|
|
|
320
|
+
next unless any.type_url.include?("BulkTriggerIdempotencyCollisionError")
|
|
321
|
+
|
|
322
|
+
successful_ids, collisions = decode_bulk_trigger_collision_bytes(any.value)
|
|
323
|
+
return BulkTriggerIdempotencyCollisionError.new(
|
|
324
|
+
successful_workflow_run_external_ids: successful_ids,
|
|
325
|
+
collisions: collisions,
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
nil
|
|
329
|
+
rescue StandardError
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Manually decode a BulkTriggerIdempotencyCollisionError proto message.
|
|
334
|
+
# Field 1 (repeated string): successful_workflow_run_external_ids
|
|
335
|
+
# Field 2 (repeated message): collisions (each is an IdempotencyCollisionError)
|
|
336
|
+
def decode_bulk_trigger_collision_bytes(bytes)
|
|
337
|
+
successful_ids = []
|
|
338
|
+
collisions = []
|
|
339
|
+
data = bytes.b
|
|
340
|
+
pos = 0
|
|
341
|
+
|
|
342
|
+
while pos < data.bytesize
|
|
343
|
+
tag, bytes_read = decode_proto_varint(data, pos)
|
|
344
|
+
pos += bytes_read
|
|
345
|
+
field_number = tag >> 3
|
|
346
|
+
wire_type = tag & 0x7
|
|
347
|
+
|
|
348
|
+
case wire_type
|
|
349
|
+
when 2
|
|
350
|
+
len, bytes_read = decode_proto_varint(data, pos)
|
|
351
|
+
pos += bytes_read
|
|
352
|
+
field_bytes = data.byteslice(pos, len)
|
|
353
|
+
pos += len
|
|
354
|
+
|
|
355
|
+
if field_number == 1
|
|
356
|
+
successful_ids << field_bytes.force_encoding("UTF-8")
|
|
357
|
+
elsif field_number == 2
|
|
358
|
+
proto = ::V1::IdempotencyCollisionError.decode(field_bytes)
|
|
359
|
+
collisions << IdempotencyCollisionError.new(proto.existing_run_external_id)
|
|
360
|
+
end
|
|
361
|
+
when 0
|
|
362
|
+
_, bytes_read = decode_proto_varint(data, pos)
|
|
363
|
+
pos += bytes_read
|
|
364
|
+
when 5
|
|
365
|
+
pos += 4
|
|
366
|
+
when 1
|
|
367
|
+
pos += 8
|
|
368
|
+
else
|
|
369
|
+
break
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
[successful_ids, collisions]
|
|
374
|
+
rescue StandardError
|
|
375
|
+
[[], []]
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def decode_proto_varint(data, pos)
|
|
379
|
+
result = 0
|
|
380
|
+
shift = 0
|
|
381
|
+
bytes_read = 0
|
|
382
|
+
loop do
|
|
383
|
+
byte = data.getbyte(pos + bytes_read)
|
|
384
|
+
return [result, bytes_read] if byte.nil?
|
|
385
|
+
|
|
386
|
+
bytes_read += 1
|
|
387
|
+
result |= (byte & 0x7f) << shift
|
|
388
|
+
shift += 7
|
|
389
|
+
break if byte.nobits?(0x80)
|
|
390
|
+
end
|
|
391
|
+
[result, bytes_read]
|
|
392
|
+
end
|
|
393
|
+
|
|
287
394
|
def ensure_connected!
|
|
288
395
|
return if @v0_stub && @v1_stub
|
|
289
396
|
|
|
@@ -8,7 +8,7 @@ require 'v1/shared/condition_pb'
|
|
|
8
8
|
require 'v1/shared/trigger_pb'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
descriptor_data = "\n\x13v1/dispatcher.proto\x12\x02v1\x1a\x19v1/shared/condition.proto\x1a\x17v1/shared/trigger.proto\"5\n DurableTaskRequestRegisterWorker\x12\x11\n\tworker_id\x18\x01 \x01(\t\"6\n!DurableTaskResponseRegisterWorker\x12\x11\n\tworker_id\x18\x01 \x01(\t\"y\n\x17\x44urableEventLogEntryRef\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x18\n\x10invocation_count\x18\x02 \x01(\x05\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\x12\x0f\n\x07node_id\x18\x04 \x01(\x03\"^\n\x16\x44urableTaskRunAckEntry\x12\x0f\n\x07node_id\x18\x01 \x01(\x03\x12\x11\n\tbranch_id\x18\x02 \x01(\x03\x12 \n\x18workflow_run_external_id\x18\x03 \x01(\t\"\xa3\x01\n\x1f\x44urableTaskEventMemoAckResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\x12\x1c\n\x14memo_already_existed\x18\x02 \x01(\x08\x12 \n\x13memo_result_payload\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_memo_result_payload\"\x95\x01\n&DurableTaskEventTriggerRunsAckResponse\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x18\n\x10invocation_count\x18\x02 \x01(\x05\x12/\n\x0brun_entries\x18\x03 \x03(\x0b\x32\x1a.v1.DurableTaskRunAckEntry\"N\n\"DurableTaskEventWaitForAckResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\"
|
|
11
|
+
descriptor_data = "\n\x13v1/dispatcher.proto\x12\x02v1\x1a\x19v1/shared/condition.proto\x1a\x17v1/shared/trigger.proto\"5\n DurableTaskRequestRegisterWorker\x12\x11\n\tworker_id\x18\x01 \x01(\t\"6\n!DurableTaskResponseRegisterWorker\x12\x11\n\tworker_id\x18\x01 \x01(\t\"y\n\x17\x44urableEventLogEntryRef\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x18\n\x10invocation_count\x18\x02 \x01(\x05\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\x12\x0f\n\x07node_id\x18\x04 \x01(\x03\"^\n\x16\x44urableTaskRunAckEntry\x12\x0f\n\x07node_id\x18\x01 \x01(\x03\x12\x11\n\tbranch_id\x18\x02 \x01(\x03\x12 \n\x18workflow_run_external_id\x18\x03 \x01(\t\"\xa3\x01\n\x1f\x44urableTaskEventMemoAckResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\x12\x1c\n\x14memo_already_existed\x18\x02 \x01(\x08\x12 \n\x13memo_result_payload\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_memo_result_payload\"\x95\x01\n&DurableTaskEventTriggerRunsAckResponse\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x18\n\x10invocation_count\x18\x02 \x01(\x05\x12/\n\x0brun_entries\x18\x03 \x03(\x0b\x32\x1a.v1.DurableTaskRunAckEntry\"N\n\"DurableTaskEventWaitForAckResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\"\xa8\x01\n)DurableTaskEventLogEntryCompletedResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x12\n\nis_failure\x18\x03 \x01(\x08\x12\x1a\n\rerror_message\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x10\n\x0e_error_message\"\x7f\n!DurableTaskEvictInvocationRequest\x12\x18\n\x10invocation_count\x18\x01 \x01(\x05\x12 \n\x18\x64urable_task_external_id\x18\x02 \x01(\t\x12\x13\n\x06reason\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_reason\"\\\n\x1e\x44urableTaskEvictionAckResponse\x12\x18\n\x10invocation_count\x18\x01 \x01(\x05\x12 \n\x18\x64urable_task_external_id\x18\x02 \x01(\t\"\x82\x01\n DurableTaskAwaitedCompletedEntry\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x11\n\tbranch_id\x18\x02 \x01(\x03\x12\x0f\n\x07node_id\x18\x03 \x01(\x03\x12\x18\n\x10invocation_count\x18\x04 \x01(\x05\"j\n\x1c\x44urableTaskServerEvictNotice\x12 \n\x18\x64urable_task_external_id\x18\x01 \x01(\t\x12\x18\n\x10invocation_count\x18\x02 \x01(\x05\x12\x0e\n\x06reason\x18\x03 \x01(\t\"r\n\x1e\x44urableTaskWorkerStatusRequest\x12\x11\n\tworker_id\x18\x01 \x01(\t\x12=\n\x0fwaiting_entries\x18\x02 \x03(\x0b\x32$.v1.DurableTaskAwaitedCompletedEntry\"m\n\x1e\x44urableTaskCompleteMemoRequest\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x10\n\x08memo_key\x18\x03 \x01(\x0c\"\x83\x01\n\x16\x44urableTaskMemoRequest\x12\x18\n\x10invocation_count\x18\x01 \x01(\x05\x12 \n\x18\x64urable_task_external_id\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12\x14\n\x07payload\x18\x04 \x01(\x0cH\x00\x88\x01\x01\x42\n\n\x08_payload\"\x8d\x01\n\x1d\x44urableTaskTriggerRunsRequest\x12\x18\n\x10invocation_count\x18\x01 \x01(\x05\x12 \n\x18\x64urable_task_external_id\x18\x02 \x01(\t\x12\x30\n\x0ctrigger_opts\x18\x03 \x03(\x0b\x32\x1a.v1.TriggerWorkflowRequest\"\xd3\x01\n\x19\x44urableTaskWaitForRequest\x12\x18\n\x10invocation_count\x18\x01 \x01(\x05\x12 \n\x18\x64urable_task_external_id\x18\x02 \x01(\t\x12\x44\n\x13wait_for_conditions\x18\x03 \x01(\x0b\x32\".v1.DurableEventListenerConditionsH\x00\x88\x01\x01\x12\x12\n\x05label\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x16\n\x14_wait_for_conditionsB\x08\n\x06_label\"\xb7\x03\n\x12\x44urableTaskRequest\x12?\n\x0fregister_worker\x18\x01 \x01(\x0b\x32$.v1.DurableTaskRequestRegisterWorkerH\x00\x12*\n\x04memo\x18\x02 \x01(\x0b\x32\x1a.v1.DurableTaskMemoRequestH\x00\x12\x39\n\x0ctrigger_runs\x18\x03 \x01(\x0b\x32!.v1.DurableTaskTriggerRunsRequestH\x00\x12\x31\n\x08wait_for\x18\x04 \x01(\x0b\x32\x1d.v1.DurableTaskWaitForRequestH\x00\x12\x41\n\x10\x65vict_invocation\x18\x05 \x01(\x0b\x32%.v1.DurableTaskEvictInvocationRequestH\x00\x12;\n\rworker_status\x18\x06 \x01(\x0b\x32\".v1.DurableTaskWorkerStatusRequestH\x00\x12;\n\rcomplete_memo\x18\x07 \x01(\x0b\x32\".v1.DurableTaskCompleteMemoRequestH\x00\x42\t\n\x07message\"\x89\x01\n\x18\x44urableTaskErrorResponse\x12(\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.v1.DurableEventLogEntryRef\x12,\n\nerror_type\x18\x02 \x01(\x0e\x32\x18.v1.DurableTaskErrorType\x12\x15\n\rerror_message\x18\x03 \x01(\t\"\x92\x04\n\x13\x44urableTaskResponse\x12@\n\x0fregister_worker\x18\x01 \x01(\x0b\x32%.v1.DurableTaskResponseRegisterWorkerH\x00\x12\x37\n\x08memo_ack\x18\x02 \x01(\x0b\x32#.v1.DurableTaskEventMemoAckResponseH\x00\x12\x46\n\x10trigger_runs_ack\x18\x03 \x01(\x0b\x32*.v1.DurableTaskEventTriggerRunsAckResponseH\x00\x12>\n\x0cwait_for_ack\x18\x04 \x01(\x0b\x32&.v1.DurableTaskEventWaitForAckResponseH\x00\x12H\n\x0f\x65ntry_completed\x18\x05 \x01(\x0b\x32-.v1.DurableTaskEventLogEntryCompletedResponseH\x00\x12-\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x1c.v1.DurableTaskErrorResponseH\x00\x12:\n\x0c\x65viction_ack\x18\x07 \x01(\x0b\x32\".v1.DurableTaskEvictionAckResponseH\x00\x12\x38\n\x0cserver_evict\x18\x08 \x01(\x0b\x32 .v1.DurableTaskServerEvictNoticeH\x00\x42\t\n\x07message\"z\n\x1bRegisterDurableEventRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x12\n\nsignal_key\x18\x02 \x01(\t\x12\x36\n\nconditions\x18\x03 \x01(\x0b\x32\".v1.DurableEventListenerConditions\"\x1e\n\x1cRegisterDurableEventResponse\"C\n\x1cListenForDurableEventRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x12\n\nsignal_key\x18\x02 \x01(\t\"A\n\x0c\x44urableEvent\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x12\n\nsignal_key\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c*k\n\x14\x44urableTaskErrorType\x12\'\n#DURABLE_TASK_ERROR_TYPE_UNSPECIFIED\x10\x00\x12*\n&DURABLE_TASK_ERROR_TYPE_NONDETERMINISM\x10\x01\x32\x84\x02\n\x0cV1Dispatcher\x12\x44\n\x0b\x44urableTask\x12\x16.v1.DurableTaskRequest\x1a\x17.v1.DurableTaskResponse\"\x00(\x01\x30\x01\x12[\n\x14RegisterDurableEvent\x12\x1f.v1.RegisterDurableEventRequest\x1a .v1.RegisterDurableEventResponse\"\x00\x12Q\n\x15ListenForDurableEvent\x12 .v1.ListenForDurableEventRequest\x1a\x10.v1.DurableEvent\"\x00(\x01\x30\x01\x42\x42Z@github.com/hatchet-dev/hatchet/internal/services/shared/proto/v1b\x06proto3"
|
|
12
12
|
|
|
13
13
|
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
14
14
|
pool.add_serialized_file(descriptor_data)
|
|
@@ -9,7 +9,7 @@ require 'v1/shared/condition_pb'
|
|
|
9
9
|
require 'v1/shared/trigger_pb'
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
descriptor_data = "\n\x12v1/workflows.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19v1/shared/condition.proto\x1a\x17v1/shared/trigger.proto\"[\n\x12\x43\x61ncelTasksRequest\x12\x14\n\x0c\x65xternal_ids\x18\x01 \x03(\t\x12$\n\x06\x66ilter\x18\x02 \x01(\x0b\x32\x0f.v1.TasksFilterH\x00\x88\x01\x01\x42\t\n\x07_filter\"[\n\x12ReplayTasksRequest\x12\x14\n\x0c\x65xternal_ids\x18\x01 \x03(\t\x12$\n\x06\x66ilter\x18\x02 \x01(\x0b\x32\x0f.v1.TasksFilterH\x00\x88\x01\x01\x42\t\n\x07_filter\"\xb7\x01\n\x0bTasksFilter\x12\x10\n\x08statuses\x18\x01 \x03(\t\x12)\n\x05since\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\x05until\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x88\x01\x01\x12\x14\n\x0cworkflow_ids\x18\x04 \x03(\t\x12\x1b\n\x13\x61\x64\x64itional_metadata\x18\x05 \x03(\tB\x08\n\x06_until\".\n\x13\x43\x61ncelTasksResponse\x12\x17\n\x0f\x63\x61ncelled_tasks\x18\x01 \x03(\t\"-\n\x13ReplayTasksResponse\x12\x16\n\x0ereplayed_tasks\x18\x01 \x03(\t\"\xae\x02\n\x19TriggerWorkflowRunRequest\x12\x15\n\rworkflow_name\x18\x01 \x01(\t\x12\r\n\x05input\x18\x02 \x01(\x0c\x12\x1b\n\x13\x61\x64\x64itional_metadata\x18\x03 \x01(\x0c\x12\x15\n\x08priority\x18\x04 \x01(\x05H\x00\x88\x01\x01\x12U\n\x15\x64\x65sired_worker_labels\x18\x05 \x03(\x0b\x32\x36.v1.TriggerWorkflowRunRequest.DesiredWorkerLabelsEntry\x1aS\n\x18\x44\x65siredWorkerLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.v1.DesiredWorkerLabels:\x02\x38\x01\x42\x0b\n\t_priority\"1\n\x1aTriggerWorkflowRunResponse\x12\x13\n\x0b\x65xternal_id\x18\x01 \x01(\t\"X\n\x18\x42ranchDurableTaskRequest\x12\x18\n\x10task_external_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\x03\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\"Y\n\x19\x42ranchDurableTaskResponse\x12\x18\n\x10task_external_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\x03\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\"\
|
|
12
|
+
descriptor_data = "\n\x12v1/workflows.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19v1/shared/condition.proto\x1a\x17v1/shared/trigger.proto\"[\n\x12\x43\x61ncelTasksRequest\x12\x14\n\x0c\x65xternal_ids\x18\x01 \x03(\t\x12$\n\x06\x66ilter\x18\x02 \x01(\x0b\x32\x0f.v1.TasksFilterH\x00\x88\x01\x01\x42\t\n\x07_filter\"[\n\x12ReplayTasksRequest\x12\x14\n\x0c\x65xternal_ids\x18\x01 \x03(\t\x12$\n\x06\x66ilter\x18\x02 \x01(\x0b\x32\x0f.v1.TasksFilterH\x00\x88\x01\x01\x42\t\n\x07_filter\"\xb7\x01\n\x0bTasksFilter\x12\x10\n\x08statuses\x18\x01 \x03(\t\x12)\n\x05since\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\x05until\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x88\x01\x01\x12\x14\n\x0cworkflow_ids\x18\x04 \x03(\t\x12\x1b\n\x13\x61\x64\x64itional_metadata\x18\x05 \x03(\tB\x08\n\x06_until\".\n\x13\x43\x61ncelTasksResponse\x12\x17\n\x0f\x63\x61ncelled_tasks\x18\x01 \x03(\t\"-\n\x13ReplayTasksResponse\x12\x16\n\x0ereplayed_tasks\x18\x01 \x03(\t\"\xae\x02\n\x19TriggerWorkflowRunRequest\x12\x15\n\rworkflow_name\x18\x01 \x01(\t\x12\r\n\x05input\x18\x02 \x01(\x0c\x12\x1b\n\x13\x61\x64\x64itional_metadata\x18\x03 \x01(\x0c\x12\x15\n\x08priority\x18\x04 \x01(\x05H\x00\x88\x01\x01\x12U\n\x15\x64\x65sired_worker_labels\x18\x05 \x03(\x0b\x32\x36.v1.TriggerWorkflowRunRequest.DesiredWorkerLabelsEntry\x1aS\n\x18\x44\x65siredWorkerLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.v1.DesiredWorkerLabels:\x02\x38\x01\x42\x0b\n\t_priority\"1\n\x1aTriggerWorkflowRunResponse\x12\x13\n\x0b\x65xternal_id\x18\x01 \x01(\t\"X\n\x18\x42ranchDurableTaskRequest\x12\x18\n\x10task_external_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\x03\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\"Y\n\x19\x42ranchDurableTaskResponse\x12\x18\n\x10task_external_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\x03\x12\x11\n\tbranch_id\x18\x03 \x01(\x03\"\xed\x04\n\x1c\x43reateWorkflowVersionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x16\n\x0e\x65vent_triggers\x18\x04 \x03(\t\x12\x15\n\rcron_triggers\x18\x05 \x03(\t\x12!\n\x05tasks\x18\x06 \x03(\x0b\x32\x12.v1.CreateTaskOpts\x12$\n\x0b\x63oncurrency\x18\x07 \x01(\x0b\x32\x0f.v1.Concurrency\x12\x17\n\ncron_input\x18\x08 \x01(\tH\x00\x88\x01\x01\x12\x30\n\x0fon_failure_task\x18\t \x01(\x0b\x32\x12.v1.CreateTaskOptsH\x01\x88\x01\x01\x12\'\n\x06sticky\x18\n \x01(\x0e\x32\x12.v1.StickyStrategyH\x02\x88\x01\x01\x12\x1d\n\x10\x64\x65\x66\x61ult_priority\x18\x0b \x01(\x05H\x03\x88\x01\x01\x12(\n\x0f\x63oncurrency_arr\x18\x0c \x03(\x0b\x32\x0f.v1.Concurrency\x12*\n\x0f\x64\x65\x66\x61ult_filters\x18\r \x03(\x0b\x32\x11.v1.DefaultFilter\x12\x1e\n\x11input_json_schema\x18\x0e \x01(\x0cH\x04\x88\x01\x01\x12/\n\x0bidempotency\x18\x0f \x01(\x0b\x32\x15.v1.IdempotencyConfigH\x05\x88\x01\x01\x42\r\n\x0b_cron_inputB\x12\n\x10_on_failure_taskB\t\n\x07_stickyB\x13\n\x11_default_priorityB\x14\n\x12_input_json_schemaB\x0e\n\x0c_idempotency\"7\n\x11IdempotencyConfig\x12\x12\n\nexpression\x18\x01 \x01(\t\x12\x0e\n\x06ttl_ms\x18\x02 \x01(\x03\"=\n\x19IdempotencyCollisionError\x12 \n\x18\x65xisting_run_external_id\x18\x01 \x01(\t\"T\n\rDefaultFilter\x12\x12\n\nexpression\x18\x01 \x01(\t\x12\r\n\x05scope\x18\x02 \x01(\t\x12\x14\n\x07payload\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\n\n\x08_payload\"\x93\x01\n\x0b\x43oncurrency\x12\x12\n\nexpression\x18\x01 \x01(\t\x12\x15\n\x08max_runs\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12\x39\n\x0elimit_strategy\x18\x03 \x01(\x0e\x32\x1c.v1.ConcurrencyLimitStrategyH\x01\x88\x01\x01\x42\x0b\n\t_max_runsB\x11\n\x0f_limit_strategy\"\xb7\x05\n\x0e\x43reateTaskOpts\x12\x13\n\x0breadable_id\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x0f\n\x07timeout\x18\x03 \x01(\t\x12\x0e\n\x06inputs\x18\x04 \x01(\t\x12\x0f\n\x07parents\x18\x05 \x03(\t\x12\x0f\n\x07retries\x18\x06 \x01(\x05\x12,\n\x0brate_limits\x18\x07 \x03(\x0b\x32\x17.v1.CreateTaskRateLimit\x12;\n\rworker_labels\x18\x08 \x03(\x0b\x32$.v1.CreateTaskOpts.WorkerLabelsEntry\x12\x1b\n\x0e\x62\x61\x63koff_factor\x18\t \x01(\x02H\x00\x88\x01\x01\x12 \n\x13\x62\x61\x63koff_max_seconds\x18\n \x01(\x05H\x01\x88\x01\x01\x12$\n\x0b\x63oncurrency\x18\x0b \x03(\x0b\x32\x0f.v1.Concurrency\x12+\n\nconditions\x18\x0c \x01(\x0b\x32\x12.v1.TaskConditionsH\x02\x88\x01\x01\x12\x1d\n\x10schedule_timeout\x18\r \x01(\tH\x03\x88\x01\x01\x12\x12\n\nis_durable\x18\x0e \x01(\x08\x12;\n\rslot_requests\x18\x0f \x03(\x0b\x32$.v1.CreateTaskOpts.SlotRequestsEntry\x1aL\n\x11WorkerLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.v1.DesiredWorkerLabels:\x02\x38\x01\x1a\x33\n\x11SlotRequestsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x11\n\x0f_backoff_factorB\x16\n\x14_backoff_max_secondsB\r\n\x0b_conditionsB\x13\n\x11_schedule_timeout\"\xfd\x01\n\x13\x43reateTaskRateLimit\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x12\n\x05units\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12\x15\n\x08key_expr\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x17\n\nunits_expr\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x1e\n\x11limit_values_expr\x18\x05 \x01(\tH\x03\x88\x01\x01\x12,\n\x08\x64uration\x18\x06 \x01(\x0e\x32\x15.v1.RateLimitDurationH\x04\x88\x01\x01\x42\x08\n\x06_unitsB\x0b\n\t_key_exprB\r\n\x0b_units_exprB\x14\n\x12_limit_values_exprB\x0b\n\t_duration\"@\n\x1d\x43reateWorkflowVersionResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\"+\n\x14GetRunDetailsRequest\x12\x13\n\x0b\x65xternal_id\x18\x01 \x01(\t\"\xaa\x01\n\rTaskRunDetail\x12\x13\n\x0b\x65xternal_id\x18\x01 \x01(\t\x12\x1d\n\x06status\x18\x02 \x01(\x0e\x32\r.v1.RunStatus\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06output\x18\x04 \x01(\x0cH\x01\x88\x01\x01\x12\x13\n\x0breadable_id\x18\x05 \x01(\t\x12\x12\n\nis_evicted\x18\x06 \x01(\x08\x42\x08\n\x06_errorB\t\n\x07_output\"\x84\x02\n\x15GetRunDetailsResponse\x12\r\n\x05input\x18\x01 \x01(\x0c\x12\x1d\n\x06status\x18\x02 \x01(\x0e\x32\r.v1.RunStatus\x12:\n\ttask_runs\x18\x03 \x03(\x0b\x32\'.v1.GetRunDetailsResponse.TaskRunsEntry\x12\x0c\n\x04\x64one\x18\x04 \x01(\x08\x12\x1b\n\x13\x61\x64\x64itional_metadata\x18\x05 \x01(\x0c\x12\x12\n\nis_evicted\x18\x06 \x01(\x08\x1a\x42\n\rTaskRunsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.v1.TaskRunDetail:\x02\x38\x01*$\n\x0eStickyStrategy\x12\x08\n\x04SOFT\x10\x00\x12\x08\n\x04HARD\x10\x01*]\n\x11RateLimitDuration\x12\n\n\x06SECOND\x10\x00\x12\n\n\x06MINUTE\x10\x01\x12\x08\n\x04HOUR\x10\x02\x12\x07\n\x03\x44\x41Y\x10\x03\x12\x08\n\x04WEEK\x10\x04\x12\t\n\x05MONTH\x10\x05\x12\x08\n\x04YEAR\x10\x06*[\n\tRunStatus\x12\n\n\x06QUEUED\x10\x00\x12\x0b\n\x07RUNNING\x10\x01\x12\r\n\tCOMPLETED\x10\x02\x12\n\n\x06\x46\x41ILED\x10\x03\x12\r\n\tCANCELLED\x10\x04\x12\x0b\n\x07\x45VICTED\x10\x05*\x7f\n\x18\x43oncurrencyLimitStrategy\x12\x16\n\x12\x43\x41NCEL_IN_PROGRESS\x10\x00\x12\x0f\n\x0b\x44ROP_NEWEST\x10\x01\x12\x10\n\x0cQUEUE_NEWEST\x10\x02\x12\x15\n\x11GROUP_ROUND_ROBIN\x10\x03\x12\x11\n\rCANCEL_NEWEST\x10\x04\x32\xcf\x03\n\x0c\x41\x64minService\x12R\n\x0bPutWorkflow\x12 .v1.CreateWorkflowVersionRequest\x1a!.v1.CreateWorkflowVersionResponse\x12>\n\x0b\x43\x61ncelTasks\x12\x16.v1.CancelTasksRequest\x1a\x17.v1.CancelTasksResponse\x12>\n\x0bReplayTasks\x12\x16.v1.ReplayTasksRequest\x1a\x17.v1.ReplayTasksResponse\x12S\n\x12TriggerWorkflowRun\x12\x1d.v1.TriggerWorkflowRunRequest\x1a\x1e.v1.TriggerWorkflowRunResponse\x12\x44\n\rGetRunDetails\x12\x18.v1.GetRunDetailsRequest\x1a\x19.v1.GetRunDetailsResponse\x12P\n\x11\x42ranchDurableTask\x12\x1c.v1.BranchDurableTaskRequest\x1a\x1d.v1.BranchDurableTaskResponseBBZ@github.com/hatchet-dev/hatchet/internal/services/shared/proto/v1b\x06proto3"
|
|
13
13
|
|
|
14
14
|
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
15
15
|
pool.add_serialized_file(descriptor_data)
|
|
@@ -25,6 +25,8 @@ module V1
|
|
|
25
25
|
BranchDurableTaskRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.BranchDurableTaskRequest").msgclass
|
|
26
26
|
BranchDurableTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.BranchDurableTaskResponse").msgclass
|
|
27
27
|
CreateWorkflowVersionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.CreateWorkflowVersionRequest").msgclass
|
|
28
|
+
IdempotencyConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.IdempotencyConfig").msgclass
|
|
29
|
+
IdempotencyCollisionError = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.IdempotencyCollisionError").msgclass
|
|
28
30
|
DefaultFilter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.DefaultFilter").msgclass
|
|
29
31
|
Concurrency = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.Concurrency").msgclass
|
|
30
32
|
CreateTaskOpts = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("v1.CreateTaskOpts").msgclass
|
data/lib/hatchet/exceptions.rb
CHANGED
|
@@ -24,6 +24,34 @@ module Hatchet
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Raised when an idempotency key collision occurs.
|
|
28
|
+
# Contains the ID of the existing run that claimed the key.
|
|
29
|
+
class IdempotencyCollisionError < Error
|
|
30
|
+
# @return [String] The external ID of the existing workflow run
|
|
31
|
+
attr_reader :existing_run_external_id
|
|
32
|
+
|
|
33
|
+
def initialize(existing_run_external_id)
|
|
34
|
+
@existing_run_external_id = existing_run_external_id
|
|
35
|
+
super("idempotency key collision: existing run #{existing_run_external_id} already exists")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raised when one or more runs in a bulk trigger collide on idempotency keys.
|
|
40
|
+
# Contains the IDs of successfully triggered runs and the individual collision errors.
|
|
41
|
+
class BulkTriggerIdempotencyCollisionError < Error
|
|
42
|
+
# @return [Array<String>] External IDs of successfully triggered workflow runs
|
|
43
|
+
attr_reader :successful_workflow_run_external_ids
|
|
44
|
+
|
|
45
|
+
# @return [Array<IdempotencyCollisionError>] The individual collision errors
|
|
46
|
+
attr_reader :collisions
|
|
47
|
+
|
|
48
|
+
def initialize(successful_workflow_run_external_ids:, collisions:)
|
|
49
|
+
@successful_workflow_run_external_ids = successful_workflow_run_external_ids
|
|
50
|
+
@collisions = collisions
|
|
51
|
+
super("idempotency key collision in bulk trigger: #{collisions.length} collision(s)")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
27
55
|
# Represents an error from a failed task run
|
|
28
56
|
class TaskRunError < Error
|
|
29
57
|
# @return [String] The external ID of the failed task run
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hatchet
|
|
4
|
+
# TTL-based idempotency: prevents duplicate runs within a sliding time window.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# Hatchet::TTLBasedIdempotencyConfig.new(expression: "input.id", ttl_ms: 60_000)
|
|
8
|
+
class TTLBasedIdempotencyConfig
|
|
9
|
+
# @return [String] CEL expression evaluated against workflow input
|
|
10
|
+
attr_reader :expression
|
|
11
|
+
|
|
12
|
+
# @return [Integer] How long the idempotency key lives, in milliseconds
|
|
13
|
+
attr_reader :ttl_ms
|
|
14
|
+
|
|
15
|
+
# @param expression [String] CEL expression to derive the idempotency key
|
|
16
|
+
# @param ttl_ms [Integer] TTL for the idempotency key in milliseconds
|
|
17
|
+
def initialize(expression:, ttl_ms:)
|
|
18
|
+
@expression = expression
|
|
19
|
+
@ttl_ms = ttl_ms
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [V1::IdempotencyConfig]
|
|
23
|
+
def to_proto
|
|
24
|
+
::V1::IdempotencyConfig.new(expression: @expression, ttl_ms: @ttl_ms)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/hatchet/version.rb
CHANGED
|
@@ -689,7 +689,7 @@ module Hatchet
|
|
|
689
689
|
|
|
690
690
|
def parse_entry_completed(completed)
|
|
691
691
|
payload = nil
|
|
692
|
-
if completed.payload && !completed.payload.empty?
|
|
692
|
+
if !completed.is_failure && completed.payload && !completed.payload.empty?
|
|
693
693
|
begin
|
|
694
694
|
payload_json = completed.payload.dup.force_encoding("UTF-8")
|
|
695
695
|
payload = JSON.parse(payload_json)
|
|
@@ -702,6 +702,8 @@ module Hatchet
|
|
|
702
702
|
durable_task_external_id: completed.ref.durable_task_external_id,
|
|
703
703
|
node_id: completed.ref.node_id,
|
|
704
704
|
payload: payload,
|
|
705
|
+
is_failure: completed.is_failure,
|
|
706
|
+
error_message: completed.error_message,
|
|
705
707
|
}
|
|
706
708
|
end
|
|
707
709
|
|
data/lib/hatchet/workflow.rb
CHANGED
|
@@ -46,6 +46,9 @@ module Hatchet
|
|
|
46
46
|
# @return [Task, nil] The on_success task
|
|
47
47
|
attr_reader :on_success
|
|
48
48
|
|
|
49
|
+
# @return [Hatchet::TTLBasedIdempotencyConfig, nil] Idempotency configuration
|
|
50
|
+
attr_reader :idempotency
|
|
51
|
+
|
|
49
52
|
# @return [String, nil] The workflow ID writer (set after registration)
|
|
50
53
|
attr_writer :id
|
|
51
54
|
|
|
@@ -57,6 +60,7 @@ module Hatchet
|
|
|
57
60
|
# @param task_defaults [Hash, nil] Default task settings
|
|
58
61
|
# @param default_filters [Array<DefaultFilter>] Default filters
|
|
59
62
|
# @param sticky [Symbol, nil] Sticky strategy
|
|
63
|
+
# @param idempotency [Hatchet::TTLBasedIdempotencyConfig, nil] Idempotency configuration
|
|
60
64
|
# @param client [Hatchet::Client, nil] The client
|
|
61
65
|
def initialize(
|
|
62
66
|
name:,
|
|
@@ -67,6 +71,7 @@ module Hatchet
|
|
|
67
71
|
task_defaults: nil,
|
|
68
72
|
default_filters: [],
|
|
69
73
|
sticky: nil,
|
|
74
|
+
idempotency: nil,
|
|
70
75
|
client: nil
|
|
71
76
|
)
|
|
72
77
|
@name = name
|
|
@@ -78,6 +83,7 @@ module Hatchet
|
|
|
78
83
|
@task_defaults = task_defaults
|
|
79
84
|
@default_filters = default_filters
|
|
80
85
|
@sticky = sticky
|
|
86
|
+
@idempotency = idempotency
|
|
81
87
|
@client = client
|
|
82
88
|
@on_failure = nil
|
|
83
89
|
@on_success = nil
|
|
@@ -208,6 +214,8 @@ module Hatchet
|
|
|
208
214
|
args[:default_priority] = @default_priority if @default_priority
|
|
209
215
|
args[:default_filters] = filter_protos unless filter_protos.empty?
|
|
210
216
|
|
|
217
|
+
args[:idempotency] = @idempotency.to_proto if @idempotency
|
|
218
|
+
|
|
211
219
|
::V1::CreateWorkflowVersionRequest.new(**args)
|
|
212
220
|
end
|
|
213
221
|
|
data/lib/hatchet-sdk.rb
CHANGED
|
@@ -3,80 +3,72 @@
|
|
|
3
3
|
require_relative "hatchet/version"
|
|
4
4
|
require_relative "hatchet/config"
|
|
5
5
|
|
|
6
|
-
# Define base error class before loading submodules that depend on it
|
|
7
|
-
module Hatchet
|
|
8
|
-
# Base error class for all Hatchet-related errors
|
|
9
|
-
class Error < StandardError; end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
require_relative "hatchet/clients"
|
|
13
|
-
require_relative "hatchet/features/events"
|
|
14
|
-
require_relative "hatchet/features/runs"
|
|
15
|
-
require_relative "hatchet/features/tenant"
|
|
16
|
-
require_relative "hatchet/features/logs"
|
|
17
|
-
require_relative "hatchet/features/workers"
|
|
18
|
-
require_relative "hatchet/features/cel"
|
|
19
|
-
require_relative "hatchet/features/workflows"
|
|
20
|
-
require_relative "hatchet/features/filters"
|
|
21
|
-
require_relative "hatchet/features/metrics"
|
|
22
|
-
require_relative "hatchet/features/rate_limits"
|
|
23
|
-
require_relative "hatchet/features/cron"
|
|
24
|
-
require_relative "hatchet/features/scheduled"
|
|
25
|
-
|
|
26
|
-
# Core classes
|
|
27
|
-
require_relative "hatchet/exceptions"
|
|
28
|
-
require_relative "hatchet/engine_version"
|
|
29
|
-
require_relative "hatchet/eviction_policy"
|
|
30
|
-
require_relative "hatchet/concurrency"
|
|
31
|
-
require_relative "hatchet/conditions"
|
|
32
|
-
require_relative "hatchet/condition_converter"
|
|
33
|
-
require_relative "hatchet/rate_limit"
|
|
34
|
-
require_relative "hatchet/labels"
|
|
35
|
-
require_relative "hatchet/trigger_options"
|
|
36
|
-
require_relative "hatchet/default_filter"
|
|
37
|
-
require_relative "hatchet/workflow_run"
|
|
38
|
-
require_relative "hatchet/context"
|
|
39
|
-
require_relative "hatchet/durable_context"
|
|
40
|
-
require_relative "hatchet/task"
|
|
41
|
-
require_relative "hatchet/workflow"
|
|
42
|
-
require_relative "hatchet/context_vars"
|
|
43
|
-
require_relative "hatchet/worker_obj"
|
|
44
|
-
|
|
45
|
-
# gRPC connection and client infrastructure
|
|
46
|
-
require_relative "hatchet/connection"
|
|
47
|
-
|
|
48
|
-
# Generated protobuf contracts (add contracts directory to load path for internal requires)
|
|
49
|
-
$LOAD_PATH.unshift(File.join(__dir__, "hatchet", "contracts")) unless $LOAD_PATH.include?(File.join(__dir__, "hatchet", "contracts"))
|
|
50
|
-
require_relative "hatchet/contracts/dispatcher/dispatcher_pb"
|
|
51
|
-
require_relative "hatchet/contracts/dispatcher/dispatcher_services_pb"
|
|
52
|
-
require_relative "hatchet/contracts/events/events_pb"
|
|
53
|
-
require_relative "hatchet/contracts/events/events_services_pb"
|
|
54
|
-
require_relative "hatchet/contracts/workflows/workflows_pb"
|
|
55
|
-
require_relative "hatchet/contracts/workflows/workflows_services_pb"
|
|
56
|
-
require_relative "hatchet/contracts/v1/shared/condition_pb"
|
|
57
|
-
require_relative "hatchet/contracts/v1/shared/trigger_pb"
|
|
58
|
-
require_relative "hatchet/contracts/v1/dispatcher_pb"
|
|
59
|
-
require_relative "hatchet/contracts/v1/dispatcher_services_pb"
|
|
60
|
-
require_relative "hatchet/contracts/v1/workflows_pb"
|
|
61
|
-
require_relative "hatchet/contracts/v1/workflows_services_pb"
|
|
62
|
-
|
|
63
|
-
# gRPC client wrappers
|
|
64
|
-
require_relative "hatchet/clients/grpc/dispatcher"
|
|
65
|
-
require_relative "hatchet/clients/grpc/admin"
|
|
66
|
-
require_relative "hatchet/clients/grpc/event_client"
|
|
67
|
-
|
|
68
|
-
# Worker runtime
|
|
69
|
-
require_relative "hatchet/worker/action_listener"
|
|
70
|
-
require_relative "hatchet/worker/workflow_run_listener"
|
|
71
|
-
require_relative "hatchet/worker/durable_eviction/cache"
|
|
72
|
-
require_relative "hatchet/worker/durable_eviction/manager"
|
|
73
|
-
require_relative "hatchet/worker/durable_event_listener"
|
|
74
|
-
require_relative "hatchet/worker/runner"
|
|
75
|
-
|
|
76
6
|
# Ruby SDK for Hatchet workflow engine
|
|
77
7
|
#
|
|
78
8
|
# @see https://docs.hatchet.run for Hatchet documentation
|
|
79
9
|
module Hatchet
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
require_relative "hatchet/clients"
|
|
13
|
+
require_relative "hatchet/features/events"
|
|
14
|
+
require_relative "hatchet/features/runs"
|
|
15
|
+
require_relative "hatchet/features/tenant"
|
|
16
|
+
require_relative "hatchet/features/logs"
|
|
17
|
+
require_relative "hatchet/features/workers"
|
|
18
|
+
require_relative "hatchet/features/cel"
|
|
19
|
+
require_relative "hatchet/features/workflows"
|
|
20
|
+
require_relative "hatchet/features/filters"
|
|
21
|
+
require_relative "hatchet/features/metrics"
|
|
22
|
+
require_relative "hatchet/features/rate_limits"
|
|
23
|
+
require_relative "hatchet/features/cron"
|
|
24
|
+
require_relative "hatchet/features/scheduled"
|
|
25
|
+
|
|
26
|
+
require_relative "hatchet/exceptions"
|
|
27
|
+
require_relative "hatchet/engine_version"
|
|
28
|
+
require_relative "hatchet/eviction_policy"
|
|
29
|
+
require_relative "hatchet/concurrency"
|
|
30
|
+
require_relative "hatchet/idempotency"
|
|
31
|
+
require_relative "hatchet/conditions"
|
|
32
|
+
require_relative "hatchet/condition_converter"
|
|
33
|
+
require_relative "hatchet/rate_limit"
|
|
34
|
+
require_relative "hatchet/labels"
|
|
35
|
+
require_relative "hatchet/trigger_options"
|
|
36
|
+
require_relative "hatchet/default_filter"
|
|
37
|
+
require_relative "hatchet/workflow_run"
|
|
38
|
+
require_relative "hatchet/context"
|
|
39
|
+
require_relative "hatchet/durable_context"
|
|
40
|
+
require_relative "hatchet/task"
|
|
41
|
+
require_relative "hatchet/workflow"
|
|
42
|
+
require_relative "hatchet/context_vars"
|
|
43
|
+
require_relative "hatchet/worker_obj"
|
|
44
|
+
|
|
45
|
+
require_relative "hatchet/connection"
|
|
46
|
+
|
|
47
|
+
$LOAD_PATH.unshift(File.join(__dir__, "hatchet", "contracts")) unless $LOAD_PATH.include?(File.join(__dir__, "hatchet", "contracts"))
|
|
48
|
+
require_relative "hatchet/contracts/dispatcher/dispatcher_pb"
|
|
49
|
+
require_relative "hatchet/contracts/dispatcher/dispatcher_services_pb"
|
|
50
|
+
require_relative "hatchet/contracts/events/events_pb"
|
|
51
|
+
require_relative "hatchet/contracts/events/events_services_pb"
|
|
52
|
+
require_relative "hatchet/contracts/workflows/workflows_pb"
|
|
53
|
+
require_relative "hatchet/contracts/workflows/workflows_services_pb"
|
|
54
|
+
require_relative "hatchet/contracts/v1/shared/condition_pb"
|
|
55
|
+
require_relative "hatchet/contracts/v1/shared/trigger_pb"
|
|
56
|
+
require_relative "hatchet/contracts/v1/dispatcher_pb"
|
|
57
|
+
require_relative "hatchet/contracts/v1/dispatcher_services_pb"
|
|
58
|
+
require_relative "hatchet/contracts/v1/workflows_pb"
|
|
59
|
+
require_relative "hatchet/contracts/v1/workflows_services_pb"
|
|
60
|
+
|
|
61
|
+
require_relative "hatchet/clients/grpc/dispatcher"
|
|
62
|
+
require_relative "hatchet/clients/grpc/admin"
|
|
63
|
+
require_relative "hatchet/clients/grpc/event_client"
|
|
64
|
+
|
|
65
|
+
require_relative "hatchet/worker/action_listener"
|
|
66
|
+
require_relative "hatchet/worker/workflow_run_listener"
|
|
67
|
+
require_relative "hatchet/worker/durable_eviction/cache"
|
|
68
|
+
require_relative "hatchet/worker/durable_eviction/manager"
|
|
69
|
+
require_relative "hatchet/worker/durable_event_listener"
|
|
70
|
+
require_relative "hatchet/worker/runner"
|
|
71
|
+
|
|
80
72
|
# The main client for interacting with Hatchet services.
|
|
81
73
|
#
|
|
82
74
|
# @example Basic usage with API token
|
|
@@ -195,7 +187,7 @@ module Hatchet
|
|
|
195
187
|
# Create a new workflow definition
|
|
196
188
|
#
|
|
197
189
|
# @param name [String] Workflow name
|
|
198
|
-
# @param opts [Hash] Workflow options
|
|
190
|
+
# @param opts [Hash] Workflow options (on_events:, concurrency:, idempotency:, etc.)
|
|
199
191
|
# @return [Hatchet::Workflow]
|
|
200
192
|
#
|
|
201
193
|
# @example
|
|
@@ -208,17 +200,17 @@ module Hatchet
|
|
|
208
200
|
# Create a standalone task (auto-wraps in a single-task workflow)
|
|
209
201
|
#
|
|
210
202
|
# @param name [String] Task name
|
|
211
|
-
# @param opts [Hash] Task options
|
|
203
|
+
# @param opts [Hash] Task options (on_events:, idempotency:, retries:, etc.)
|
|
212
204
|
# @yield [input, ctx] The task execution block
|
|
213
205
|
# @return [Hatchet::Task]
|
|
214
206
|
#
|
|
215
207
|
# @example
|
|
216
208
|
# my_task = hatchet.task(name: "my_task") { |input, ctx| { "result" => "done" } }
|
|
217
209
|
def task(name:, **opts, &block)
|
|
218
|
-
# Create a workflow wrapper for standalone tasks
|
|
219
210
|
wf = Workflow.new(name: name, client: self,
|
|
220
211
|
on_events: opts.delete(:on_events) || [],
|
|
221
|
-
default_filters: opts.delete(:default_filters) || [],
|
|
212
|
+
default_filters: opts.delete(:default_filters) || [],
|
|
213
|
+
idempotency: opts.delete(:idempotency),)
|
|
222
214
|
wf.task(name, **opts, &block)
|
|
223
215
|
end
|
|
224
216
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hatchet-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabriel ruttner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '1.1'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: googleapis-common-protos-types
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.0'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.0'
|
|
83
97
|
- !ruby/object:Gem::Dependency
|
|
84
98
|
name: google-protobuf
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -472,6 +486,7 @@ files:
|
|
|
472
486
|
- lib/hatchet/features/tenant.rb
|
|
473
487
|
- lib/hatchet/features/workers.rb
|
|
474
488
|
- lib/hatchet/features/workflows.rb
|
|
489
|
+
- lib/hatchet/idempotency.rb
|
|
475
490
|
- lib/hatchet/labels.rb
|
|
476
491
|
- lib/hatchet/rate_limit.rb
|
|
477
492
|
- lib/hatchet/task.rb
|
|
@@ -519,6 +534,7 @@ files:
|
|
|
519
534
|
- sig/hatchet/features/tenant.rbs
|
|
520
535
|
- sig/hatchet/features/workers.rbs
|
|
521
536
|
- sig/hatchet/features/workflows.rbs
|
|
537
|
+
- sig/hatchet/idempotency.rbs
|
|
522
538
|
- sig/hatchet/labels.rbs
|
|
523
539
|
- sig/hatchet/rate_limit.rbs
|
|
524
540
|
- sig/hatchet/task.rbs
|