Class: Shikibu::Workflow
- Inherits:
-
Object
- Object
- Shikibu::Workflow
- Defined in:
- lib/shikibu/workflow.rb
Overview
Base class for workflow definitions Subclass this to create durable workflows
Instance Attribute Summary collapse
-
#ctx ⇒ Object
readonly
Returns the value of attribute ctx.
-
#input ⇒ Object
readonly
Returns the value of attribute input.
Class Method Summary collapse
-
.deserialize_input(data) ⇒ Object
Deserialize input from storage.
-
.deserialize_output(data) ⇒ Object
Deserialize output from storage.
-
.event_handler(value = nil) ⇒ Object
Set whether this workflow handles events.
-
.input(type_class = nil) ⇒ Object
Set the input type for this workflow.
-
.lock_timeout(seconds = nil) ⇒ Object
Set the lock timeout for this workflow.
-
.output(type_class = nil) ⇒ Object
Set the output type for this workflow (optional).
-
.register! ⇒ Object
Register this workflow with Shikibu.
-
.run(**input) ⇒ Object
Run workflow using global Shikibu.app.
-
.serialize_input(input_value) ⇒ Hash
Serialize input for storage.
-
.serialize_output(output_value) ⇒ Hash, Object
Serialize output for storage.
-
.source_code ⇒ Object
Get the source code of this workflow.
-
.source_hash ⇒ Object
Get the source hash for this workflow.
-
.start(app, instance_id: nil, **input) ⇒ String
Create and start a new workflow instance.
-
.typed_input? ⇒ Boolean
Check if this workflow has typed input.
-
.typed_output? ⇒ Boolean
Check if this workflow has typed output.
-
.workflow_name(name = nil) ⇒ Object
Set the workflow name.
Instance Method Summary collapse
-
#activity(name, retry_policy: nil, returns: nil) ⇒ Object
Execute an activity with automatic retry and history tracking.
-
#context=(context) ⇒ Object
Set context (called by ReplayEngine).
-
#execute(**input) ⇒ Object
Override this method to implement workflow logic.
-
#initialize(context_or_input = nil, **input) ⇒ Workflow
constructor
Initialize with context (internal) or input (for Shikibu.run with instance).
- #instance_id ⇒ Object
-
#on_failure(name, **args) ⇒ Object
Register a compensation action to run on failure (Romancy/Edda compatible).
- #publish(channel, data, metadata: nil) ⇒ Object
- #receive(channel, timeout: nil, mode: ChannelMode::BROADCAST) ⇒ Object
- #recur(**new_input) ⇒ Object
-
#run_compensations ⇒ Object
Execute registered compensations in reverse order (LIFO) Called by ReplayEngine on workflow failure Implements Romancy/Edda compatible idempotency checking.
- #send_to(target_instance_id, channel, data) ⇒ Object
-
#sleep(seconds, timer_id: nil) ⇒ Object
Convenience methods delegated to context.
- #sleep_until(until_time, timer_id: nil) ⇒ Object
- #subscribe(channel, mode: ChannelMode::BROADCAST) ⇒ Object
- #try_receive(channel, mode: ChannelMode::BROADCAST) ⇒ Object
- #unsubscribe(channel) ⇒ Object
- #wait_event(event_type, timeout: nil) ⇒ Object
Constructor Details
#initialize(context_or_input = nil, **input) ⇒ Workflow
Initialize with context (internal) or input (for Shikibu.run with instance)
185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/shikibu/workflow.rb', line 185 def initialize(context_or_input = nil, **input) if context_or_input.is_a?(WorkflowContext) @ctx = context_or_input @input = {} else # Called as OrderSaga.new(order_id: '123') @ctx = nil @input = context_or_input.is_a?(Hash) ? context_or_input : input end @pending_compensations = [] end |
Instance Attribute Details
#ctx ⇒ Object (readonly)
Returns the value of attribute ctx.
182 183 184 |
# File 'lib/shikibu/workflow.rb', line 182 def ctx @ctx end |
#input ⇒ Object (readonly)
Returns the value of attribute input.
182 183 184 |
# File 'lib/shikibu/workflow.rb', line 182 def input @input end |
Class Method Details
.deserialize_input(data) ⇒ Object
Deserialize input from storage
117 118 119 120 121 122 |
# File 'lib/shikibu/workflow.rb', line 117 def deserialize_input(data) return data unless typed_input? return data unless data.is_a?(Hash) TypedPayload.from_h(data, @input_type) end |
.deserialize_output(data) ⇒ Object
Deserialize output from storage
134 135 136 137 138 139 |
# File 'lib/shikibu/workflow.rb', line 134 def deserialize_output(data) return data unless typed_output? return data unless data.is_a?(Hash) TypedPayload.from_h(data, @output_type) end |
.event_handler(value = nil) ⇒ Object
Set whether this workflow handles events
44 45 46 47 48 49 50 |
# File 'lib/shikibu/workflow.rb', line 44 def event_handler(value = nil) if value.nil? @event_handler || false else @event_handler = value end end |
.input(type_class = nil) ⇒ Object
Set the input type for this workflow
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/shikibu/workflow.rb', line 69 def input(type_class = nil) if type_class unless TypedPayload.typed_class?(type_class) raise ArgumentError, "#{type_class} must be a Dry::Struct, Data, or respond to .new and #to_h" end @input_type = type_class else @input_type end end |
.lock_timeout(seconds = nil) ⇒ Object
Set the lock timeout for this workflow
54 55 56 57 58 59 60 |
# File 'lib/shikibu/workflow.rb', line 54 def lock_timeout(seconds = nil) if seconds @lock_timeout = seconds else @lock_timeout || 300 end end |
.output(type_class = nil) ⇒ Object
Set the output type for this workflow (optional)
83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/shikibu/workflow.rb', line 83 def output(type_class = nil) if type_class unless TypedPayload.typed_class?(type_class) raise ArgumentError, "#{type_class} must be a Dry::Struct, Data, or respond to .new and #to_h" end @output_type = type_class else @output_type end end |
.register! ⇒ Object
Register this workflow with Shikibu
161 162 163 |
# File 'lib/shikibu/workflow.rb', line 161 def register! Shikibu.register_workflow(self) end |
.run(**input) ⇒ Object
Run workflow using global Shikibu.app
177 178 179 |
# File 'lib/shikibu/workflow.rb', line 177 def run(**input) Shikibu.run(self, **input) end |
.serialize_input(input_value) ⇒ Hash
Serialize input for storage
110 111 112 |
# File 'lib/shikibu/workflow.rb', line 110 def serialize_input(input_value) TypedPayload.to_h(input_value) end |
.serialize_output(output_value) ⇒ Hash, Object
Serialize output for storage
127 128 129 |
# File 'lib/shikibu/workflow.rb', line 127 def serialize_output(output_value) TypedPayload.to_h(output_value) end |
.source_code ⇒ Object
Get the source code of this workflow
150 151 152 153 154 155 156 157 158 |
# File 'lib/shikibu/workflow.rb', line 150 def source_code # Try to get source from file file, = instance_method(:execute).source_location return '' unless file && File.exist?(file) File.read(file) rescue StandardError '' end |
.source_hash ⇒ Object
Get the source hash for this workflow
142 143 144 145 146 147 |
# File 'lib/shikibu/workflow.rb', line 142 def source_hash @source_hash ||= begin source = source_code Digest::SHA256.hexdigest(source)[0, 16] end end |
.start(app, instance_id: nil, **input) ⇒ String
Create and start a new workflow instance
170 171 172 |
# File 'lib/shikibu/workflow.rb', line 170 def start(app, instance_id: nil, **input) app.start_workflow(self, instance_id: instance_id, **input) end |
.typed_input? ⇒ Boolean
Check if this workflow has typed input
97 98 99 |
# File 'lib/shikibu/workflow.rb', line 97 def typed_input? !@input_type.nil? end |
.typed_output? ⇒ Boolean
Check if this workflow has typed output
103 104 105 |
# File 'lib/shikibu/workflow.rb', line 103 def typed_output? !@output_type.nil? end |
.workflow_name(name = nil) ⇒ Object
Set the workflow name
34 35 36 37 38 39 40 |
# File 'lib/shikibu/workflow.rb', line 34 def workflow_name(name = nil) if name @workflow_name = name.to_s else @workflow_name || name.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase end end |
Instance Method Details
#activity(name, retry_policy: nil, returns: nil) ⇒ Object
Execute an activity with automatic retry and history tracking
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/shikibu/workflow.rb', line 216 def activity(name, retry_policy: nil, returns: nil, &) if returns && !TypedPayload.typed_class?(returns) raise ArgumentError, "returns: must be a typed class (Dry::Struct, Data, or duck-typed), got #{returns.inspect}" end activity_id = ctx.generate_activity_id(name.to_s) ctx.current_activity_id = activity_id # Check for cached result during replay if ctx. && ctx.cached_result?(activity_id) cached = ctx.get_cached_result(activity_id) handle_cached_result(activity_id, cached) ctx.record_last_activity_id(activity_id) if cached[:event_type] == EventType::ACTIVITY_COMPLETED result = cached[:result] # Restore type if specified if returns && TypedPayload.typed_class?(returns) && result.is_a?(Hash) result = TypedPayload.from_h(result, returns) end return result end # Re-raise cached error raise reconstruct_error(cached) end # Execute the activity result = execute_activity(activity_id, name.to_s, retry_policy || RetryPolicy.default, &) ctx.record_last_activity_id(activity_id) result ensure ctx.current_activity_id = nil end |
#context=(context) ⇒ Object
Set context (called by ReplayEngine)
198 199 200 |
# File 'lib/shikibu/workflow.rb', line 198 def context=(context) @ctx = context end |
#execute(**input) ⇒ Object
Override this method to implement workflow logic
205 206 207 |
# File 'lib/shikibu/workflow.rb', line 205 def execute(**input) raise NotImplementedError, 'Subclasses must implement #execute' end |
#instance_id ⇒ Object
334 335 336 |
# File 'lib/shikibu/workflow.rb', line 334 def instance_id ctx.instance_id end |
#on_failure(name, **args) ⇒ Object
Register a compensation action to run on failure (Romancy/Edda compatible)
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/shikibu/workflow.rb', line 269 def on_failure(name, **args) # Use last_activity_id since current_activity_id is cleared after activity completes activity_id = ctx.last_activity_id compensation_name = name.to_s # Get the compensation function from registry compensation_fn = Shikibu.get_compensation(compensation_name) raise ArgumentError, "Compensation '#{compensation_name}' not registered" if compensation_fn.nil? @pending_compensations << { activity_id: activity_id, compensation_name: compensation_name, args: args, block: proc { compensation_fn.call(ctx, **args) } } ctx.register_compensation( activity_id: activity_id, compensation_name: compensation_name, args: args ) end |
#publish(channel, data, metadata: nil) ⇒ Object
318 319 320 |
# File 'lib/shikibu/workflow.rb', line 318 def publish(channel, data, metadata: nil) ctx.publish(channel, data, metadata: ) end |
#receive(channel, timeout: nil, mode: ChannelMode::BROADCAST) ⇒ Object
310 311 312 |
# File 'lib/shikibu/workflow.rb', line 310 def receive(channel, timeout: nil, mode: ChannelMode::BROADCAST) ctx.receive(channel, timeout: timeout, mode: mode) end |
#recur(**new_input) ⇒ Object
326 327 328 |
# File 'lib/shikibu/workflow.rb', line 326 def recur(**new_input) ctx.recur(**new_input) end |
#run_compensations ⇒ Object
Execute registered compensations in reverse order (LIFO) Called by ReplayEngine on workflow failure Implements Romancy/Edda compatible idempotency checking
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/shikibu/workflow.rb', line 341 def run_compensations # Get compensations from DB to have table IDs (for Romancy/Edda compatibility) db_compensations = ctx.storage.get_compensations(ctx.instance_id) # Get already executed compensation IDs from history (idempotency check) executed_ids = executed_compensation_ids # Build a lookup from activity_id to DB compensation record db_lookup = db_compensations.each_with_object({}) do |comp, hash| hash[comp[:activity_id]] = comp end # Execute compensations in reverse order (LIFO) @pending_compensations.reverse.each do |comp| db_comp = db_lookup[comp[:activity_id]] compensation_id = db_comp&.dig(:id) # Skip if already executed (idempotency) next if compensation_id && executed_ids.include?(compensation_id) execute_single_compensation(comp, compensation_id) end end |
#send_to(target_instance_id, channel, data) ⇒ Object
322 323 324 |
# File 'lib/shikibu/workflow.rb', line 322 def send_to(target_instance_id, channel, data) ctx.send_to(target_instance_id, channel, data) end |
#sleep(seconds, timer_id: nil) ⇒ Object
Convenience methods delegated to context
294 295 296 |
# File 'lib/shikibu/workflow.rb', line 294 def sleep(seconds, timer_id: nil) ctx.sleep(seconds, timer_id: timer_id) end |
#sleep_until(until_time, timer_id: nil) ⇒ Object
298 299 300 |
# File 'lib/shikibu/workflow.rb', line 298 def sleep_until(until_time, timer_id: nil) ctx.sleep_until(until_time, timer_id: timer_id) end |
#subscribe(channel, mode: ChannelMode::BROADCAST) ⇒ Object
302 303 304 |
# File 'lib/shikibu/workflow.rb', line 302 def subscribe(channel, mode: ChannelMode::BROADCAST) ctx.subscribe(channel, mode: mode) end |
#try_receive(channel, mode: ChannelMode::BROADCAST) ⇒ Object
314 315 316 |
# File 'lib/shikibu/workflow.rb', line 314 def try_receive(channel, mode: ChannelMode::BROADCAST) ctx.try_receive(channel, mode: mode) end |
#unsubscribe(channel) ⇒ Object
306 307 308 |
# File 'lib/shikibu/workflow.rb', line 306 def unsubscribe(channel) ctx.unsubscribe(channel) end |
#wait_event(event_type, timeout: nil) ⇒ Object
330 331 332 |
# File 'lib/shikibu/workflow.rb', line 330 def wait_event(event_type, timeout: nil) receive(event_type, timeout: timeout, mode: ChannelMode::BROADCAST) end |