restate-sdk 0.10.0 → 0.12.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.
@@ -1,652 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- require 'async'
5
- require 'async/queue'
6
- require 'logger'
7
-
8
- module Restate
9
- # The core execution context for a Restate handler invocation.
10
- # Implements the progress loop and all context API methods (state, run, sleep, call, send).
11
- #
12
- # Concurrency model:
13
- # - The handler runs inside a Fiber managed by Falcon/Async.
14
- # - `run` blocks spawn child Async tasks.
15
- # - When the progress loop needs input, it dequeues from @input_queue, yielding the Fiber.
16
- # - The HTTP input reader (a separate Async task) feeds chunks into @input_queue.
17
- # - Output chunks are written directly to the streaming response body.
18
- class ServerContext
19
- include WorkflowContext
20
- include WorkflowSharedContext
21
-
22
- LOGGER = Logger.new($stdout, progname: 'Restate::ServerContext')
23
-
24
- attr_reader :vm, :invocation
25
-
26
- def initialize(vm:, handler:, invocation:, send_output:, input_queue:, middleware: [],
27
- outbound_middleware: [])
28
- @vm = vm
29
- @handler = handler
30
- @invocation = invocation
31
- @send_output = send_output
32
- @input_queue = input_queue
33
- @run_coros_to_execute = {}
34
- @attempt_finished_event = AttemptFinishedEvent.new
35
- @middleware = middleware
36
- @outbound_middleware = outbound_middleware
37
- end
38
-
39
- # ── Main entry point ──
40
-
41
- # Runs the handler to completion, writing the output (or failure) to the journal.
42
- def enter
43
- Thread.current[:restate_context] = self
44
- Thread.current[:restate_service_kind] = @handler.service_tag.kind
45
- Thread.current[:restate_handler_kind] = @handler.kind
46
- in_buffer = @invocation.input_buffer
47
- out_buffer = Restate.invoke_handler(handler: @handler, ctx: self, in_buffer: in_buffer,
48
- middleware: @middleware)
49
- @vm.sys_write_output_success(out_buffer)
50
- @vm.sys_end
51
- rescue TerminalError => e
52
- failure = Failure.new(code: e.status_code, message: e.message)
53
- @vm.sys_write_output_failure(failure)
54
- @vm.sys_end
55
- rescue SuspendedError, InternalError
56
- # These are expected internal control flow exceptions; do nothing.
57
- rescue DisconnectedError
58
- raise
59
- rescue StandardError => e
60
- # Walk the cause chain for TerminalError or internal exceptions
61
- cause = e
62
- handled = false
63
- while cause
64
- if cause.is_a?(TerminalError)
65
- f = Failure.new(code: cause.status_code, message: cause.message)
66
- @vm.sys_write_output_failure(f)
67
- @vm.sys_end
68
- handled = true
69
- break
70
- elsif cause.is_a?(SuspendedError) || cause.is_a?(InternalError)
71
- handled = true
72
- break
73
- end
74
- cause = cause.cause
75
- end
76
- unless handled
77
- @vm.notify_error(e.inspect, e.backtrace&.join("\n"))
78
- raise
79
- end
80
- ensure
81
- @run_coros_to_execute.clear
82
- Thread.current[:restate_context] = nil
83
- Thread.current[:restate_service_kind] = nil
84
- Thread.current[:restate_handler_kind] = nil
85
- end
86
-
87
- # Called by the server when the attempt ends (handler completed, disconnected,
88
- # or transient error). Signals the attempt_finished_event so that user code
89
- # and background pool jobs can clean up.
90
- def on_attempt_finished
91
- @attempt_finished_event.set!
92
- end
93
-
94
- # ── State operations ──
95
-
96
- # Durably retrieves a state entry by name. Returns nil if unset.
97
- def get(name, serde: JsonSerde)
98
- get_async(name, serde: serde).await
99
- end
100
-
101
- # Returns a DurableFuture for a state entry. Resolves to nil if unset.
102
- def get_async(name, serde: JsonSerde)
103
- handle = @vm.sys_get_state(name)
104
- DurableFuture.new(self, handle, serde: serde)
105
- end
106
-
107
- # Durably sets a state entry. The value is serialized via +serde+.
108
- def set(name, value, serde: JsonSerde)
109
- @vm.sys_set_state(name, serde.serialize(value))
110
- end
111
-
112
- # Durably removes a single state entry by name.
113
- def clear(name)
114
- @vm.sys_clear_state(name)
115
- end
116
-
117
- # Durably removes all state entries for this virtual object or workflow.
118
- def clear_all
119
- @vm.sys_clear_all_state
120
- end
121
-
122
- # Returns the list of all state entry names for this virtual object or workflow.
123
- def state_keys
124
- state_keys_async.await
125
- end
126
-
127
- # Returns a DurableFuture for the list of all state entry names.
128
- def state_keys_async
129
- handle = @vm.sys_get_state_keys
130
- DurableFuture.new(self, handle)
131
- end
132
-
133
- # ── Sleep ──
134
-
135
- # Returns a durable future that completes after the given duration.
136
- # The timer survives handler restarts.
137
- def sleep(seconds)
138
- millis = (seconds * 1000).to_i
139
- handle = @vm.sys_sleep(millis)
140
- DurableFuture.new(self, handle)
141
- end
142
-
143
- # Block until a previously created handle completes. Returns the value.
144
- def resolve_handle(handle)
145
- poll_and_take(handle)
146
- end
147
-
148
- # Wait until any of the given handles completes. Does not take notifications.
149
- def wait_any_handle(handles)
150
- poll_or_cancel(handles) unless handles.any? { |h| @vm.is_completed(h) }
151
- end
152
-
153
- # Check if a handle is completed (non-blocking).
154
- def completed?(handle)
155
- @vm.is_completed(handle)
156
- end
157
-
158
- # Take a completed handle's notification, returning the value.
159
- # Raises TerminalError if the handle resolved to a failure.
160
- def take_completed(handle)
161
- must_take_notification(handle)
162
- end
163
-
164
- # Wait until any of the given futures completes. Returns [completed, remaining].
165
- def wait_any(*futures)
166
- handles = futures.map(&:handle)
167
- wait_any_handle(handles)
168
- futures.partition(&:completed?)
169
- end
170
-
171
- # ── Durable run (side effect) ──
172
-
173
- # Executes a durable side effect. The block runs at most once; its result is
174
- # journaled and replayed on retries. Returns a DurableFuture for the result.
175
- #
176
- # Pass +background: true+ to run the block in a real OS Thread, keeping the
177
- # fiber event loop responsive for other concurrent handlers. Use this for
178
- # CPU-intensive work.
179
- def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
180
- handle = @vm.sys_run(name)
181
-
182
- @run_coros_to_execute[handle] =
183
- if background
184
- -> { execute_run_threaded(handle, action, serde, retry_policy) }
185
- else
186
- -> { execute_run(handle, action, serde, retry_policy) }
187
- end
188
-
189
- DurableFuture.new(self, handle, serde: serde)
190
- end
191
-
192
- # Convenience shortcut for +run(...).await+ — executes the durable side effect
193
- # and returns the result directly.
194
- #
195
- # Accepts all the same options as +run+, including +background: true+.
196
- def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
197
- run(name, serde: serde, retry_policy: retry_policy, background: background, &action).await
198
- end
199
-
200
- # ── Service calls ──
201
-
202
- # Durably calls a handler on a Restate service and returns a future for its result.
203
- def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
204
- input_serde: NOT_SET, output_serde: NOT_SET)
205
- svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
206
- in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
207
- out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
208
- parameter = in_serde.serialize(arg)
209
- with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
210
- call_handle = @vm.sys_call(
211
- service: svc_name, handler: handler_name, parameter: parameter,
212
- key: key, idempotency_key: idempotency_key, headers: hdrs
213
- )
214
- DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
215
- output_serde: out_serde)
216
- end
217
- end
218
-
219
- # Sends a one-way invocation to a Restate service handler (fire-and-forget).
220
- def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil,
221
- input_serde: NOT_SET)
222
- svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
223
- in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
224
- parameter = in_serde.serialize(arg)
225
- delay_ms = delay ? (delay * 1000).to_i : nil
226
- with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
227
- invocation_id_handle = @vm.sys_send(
228
- service: svc_name, handler: handler_name, parameter: parameter,
229
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
230
- )
231
- SendHandle.new(self, invocation_id_handle)
232
- end
233
- end
234
-
235
- # Durably calls a handler on a Restate virtual object, keyed by +key+.
236
- def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
237
- input_serde: NOT_SET, output_serde: NOT_SET)
238
- svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
239
- in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
240
- out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
241
- parameter = in_serde.serialize(arg)
242
- with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
243
- call_handle = @vm.sys_call(
244
- service: svc_name, handler: handler_name, parameter: parameter,
245
- key: key, idempotency_key: idempotency_key, headers: hdrs
246
- )
247
- DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
248
- output_serde: out_serde)
249
- end
250
- end
251
-
252
- # Sends a one-way invocation to a Restate virtual object handler (fire-and-forget).
253
- def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil,
254
- input_serde: NOT_SET)
255
- svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
256
- in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
257
- parameter = in_serde.serialize(arg)
258
- delay_ms = delay ? (delay * 1000).to_i : nil
259
- with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
260
- invocation_id_handle = @vm.sys_send(
261
- service: svc_name, handler: handler_name, parameter: parameter,
262
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
263
- )
264
- SendHandle.new(self, invocation_id_handle)
265
- end
266
- end
267
-
268
- # Durably calls a handler on a Restate workflow, keyed by +key+.
269
- def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
270
- input_serde: NOT_SET, output_serde: NOT_SET)
271
- object_call(service, handler, key, arg, idempotency_key: idempotency_key, headers: headers,
272
- input_serde: input_serde, output_serde: output_serde) # rubocop:disable Layout/HashAlignment
273
- end
274
-
275
- # Sends a one-way invocation to a Restate workflow handler (fire-and-forget).
276
- def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil,
277
- input_serde: NOT_SET)
278
- object_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key, headers: headers,
279
- input_serde: input_serde) # rubocop:disable Layout/HashAlignment
280
- end
281
-
282
- # ── Awakeables ──
283
-
284
- # Creates an awakeable and returns [awakeable_id, DurableFuture].
285
- def awakeable(serde: JsonSerde)
286
- id, handle = @vm.sys_awakeable
287
- [id, DurableFuture.new(self, handle, serde: serde)]
288
- end
289
-
290
- # Resolves an awakeable with a success value.
291
- def resolve_awakeable(awakeable_id, payload, serde: JsonSerde)
292
- @vm.sys_complete_awakeable_success(awakeable_id, serde.serialize(payload))
293
- end
294
-
295
- # Rejects an awakeable with a terminal failure.
296
- def reject_awakeable(awakeable_id, message, code: 500)
297
- failure = Failure.new(code: code, message: message)
298
- @vm.sys_complete_awakeable_failure(awakeable_id, failure)
299
- end
300
-
301
- # ── Promises (Workflow API) ──
302
-
303
- # Gets a durable promise value, blocking until resolved.
304
- def promise(name, serde: JsonSerde)
305
- handle = @vm.sys_get_promise(name)
306
- poll_and_take(handle) do |raw|
307
- raw.nil? ? nil : serde.deserialize(raw)
308
- end
309
- end
310
-
311
- # Peeks at a durable promise value without blocking. Returns nil if not yet resolved.
312
- def peek_promise(name, serde: JsonSerde)
313
- handle = @vm.sys_peek_promise(name)
314
- poll_and_take(handle) do |raw|
315
- raw.nil? ? nil : serde.deserialize(raw)
316
- end
317
- end
318
-
319
- # Resolves a durable promise with a success value.
320
- def resolve_promise(name, payload, serde: JsonSerde)
321
- handle = @vm.sys_complete_promise_success(name, serde.serialize(payload))
322
- poll_and_take(handle)
323
- nil
324
- end
325
-
326
- # Rejects a durable promise with a terminal failure.
327
- def reject_promise(name, message, code: 500)
328
- failure = Failure.new(code: code, message: message)
329
- handle = @vm.sys_complete_promise_failure(name, failure)
330
- poll_and_take(handle)
331
- nil
332
- end
333
-
334
- # ── Cancel invocation ──
335
-
336
- # Requests cancellation of another invocation by its id.
337
- def cancel_invocation(invocation_id)
338
- @vm.sys_cancel_invocation(invocation_id)
339
- end
340
-
341
- # ── Generic calls (raw bytes, no serde) ──
342
-
343
- # Durably calls a handler using raw bytes (no serialization). Useful for proxying.
344
- def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil)
345
- with_outbound_middleware(service, handler, headers) do |hdrs|
346
- call_handle = @vm.sys_call(
347
- service: service, handler: handler, parameter: arg,
348
- key: key, idempotency_key: idempotency_key, headers: hdrs
349
- )
350
- DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
351
- output_serde: nil)
352
- end
353
- end
354
-
355
- # Sends a one-way invocation using raw bytes (no serialization). Useful for proxying.
356
- def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil)
357
- delay_ms = delay ? (delay * 1000).to_i : nil
358
- with_outbound_middleware(service, handler, headers) do |hdrs|
359
- invocation_id_handle = @vm.sys_send(
360
- service: service, handler: handler, parameter: arg,
361
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
362
- )
363
- SendHandle.new(self, invocation_id_handle)
364
- end
365
- end
366
-
367
- # ── Request metadata ──
368
-
369
- # Returns metadata about the current invocation (id, headers, raw body).
370
- def request
371
- @request ||= Request.new(
372
- id: @invocation.invocation_id,
373
- headers: @invocation.headers.to_h,
374
- body: @invocation.input_buffer,
375
- attempt_finished_event: @attempt_finished_event
376
- )
377
- end
378
-
379
- # Returns the key for this virtual object or workflow invocation.
380
- def key
381
- @invocation.key
382
- end
383
-
384
- private
385
-
386
- # ── Progress loop ──
387
-
388
- # Polls until the given handle(s) complete, then takes the notification.
389
- def poll_and_take(handle, &)
390
- poll_or_cancel([handle]) unless @vm.is_completed(handle)
391
- must_take_notification(handle, &)
392
- end
393
-
394
- def poll_or_cancel(handles)
395
- loop do
396
- flush_output
397
- response = @vm.do_progress(handles)
398
-
399
- if response.is_a?(Exception)
400
- LOGGER.error("Exception in do_progress: #{response}")
401
- raise InternalError
402
- end
403
-
404
- case response
405
- when Suspended
406
- raise SuspendedError
407
- when DoProgressAnyCompleted
408
- return
409
- when DoProgressCancelSignalReceived
410
- raise TerminalError.new('cancelled', status_code: 409)
411
- when DoProgressExecuteRun
412
- fn = @run_coros_to_execute.delete(response.handle)
413
- raise "Missing run coroutine for handle #{response.handle}" unless fn
414
-
415
- # Spawn child task for the run action
416
- Async do
417
- fn.call
418
- ensure
419
- @input_queue.enqueue(:run_completed)
420
- end
421
- when DoWaitPendingRun, DoProgressReadFromInput
422
- # Wait for input from the HTTP body reader or a run completion signal
423
- event = @input_queue.dequeue
424
-
425
- case event
426
- when :run_completed
427
- next
428
- when :eof
429
- @vm.notify_input_closed
430
- when :disconnected
431
- raise DisconnectedError
432
- when String
433
- @vm.notify_input(event)
434
- end
435
- end
436
- end
437
- end
438
-
439
- def must_take_notification(handle, &block)
440
- result = @vm.take_notification(handle)
441
-
442
- if result.is_a?(Exception)
443
- flush_output
444
- LOGGER.error("Exception in take_notification: #{result}")
445
- raise InternalError
446
- end
447
-
448
- case result
449
- when Suspended
450
- flush_output
451
- raise SuspendedError
452
- when NotReady
453
- raise "Unexpected NotReady for handle #{handle}"
454
- when Failure
455
- raise TerminalError.new(result.message, status_code: result.code)
456
- else
457
- block ? yield(result) : result
458
- end
459
- end
460
-
461
- def flush_output
462
- loop do
463
- output = @vm.take_output
464
- break if output.nil? || output.empty?
465
-
466
- @send_output.call(output)
467
- end
468
- end
469
-
470
- # ── Outbound middleware ──
471
-
472
- # Runs outbound middleware chain (Sidekiq client middleware pattern).
473
- # Each middleware gets +call(service, handler, headers)+ and must +yield+
474
- # to continue the chain. The block at the end performs the actual VM call.
475
- def with_outbound_middleware(service, handler, headers, &action)
476
- if @outbound_middleware.empty?
477
- action.call(headers)
478
- else
479
- h = headers || {}
480
- chain = ->(hdrs) { action.call(hdrs) }
481
- @outbound_middleware.reverse_each do |mw|
482
- prev = chain
483
- chain = ->(hdrs) { mw.call(service, handler, hdrs) { prev.call(hdrs) } }
484
- end
485
- chain.call(h)
486
- end
487
- end
488
-
489
- # ── Call target resolution ──
490
-
491
- # Resolves a service+handler pair from class/symbol or string/string.
492
- # Returns [service_name, handler_name, handler_metadata_or_nil].
493
- def resolve_call_target(service, handler)
494
- handler_name = handler.is_a?(Symbol) ? handler.name : handler.to_s
495
- if service.is_a?(Class) && service.respond_to?(:service_name)
496
- svc_name = service.service_name
497
- handler_meta = service.respond_to?(:handlers) ? service.handlers[handler_name] : nil
498
- [svc_name, handler_name, handler_meta]
499
- else
500
- [service.to_s, handler_name, nil]
501
- end
502
- end
503
-
504
- # Resolves a serde value: if the caller passed NOT_SET, fall back to handler metadata, then JsonSerde.
505
- def resolve_serde(caller_serde, handler_meta, field)
506
- return caller_serde unless caller_serde.equal?(NOT_SET)
507
- return JsonSerde unless handler_meta
508
-
509
- io = handler_meta.handler_io
510
- case field
511
- when :input_serde then io.input_serde
512
- when :output_serde then io.output_serde
513
- else JsonSerde
514
- end
515
- end
516
-
517
- # ── Run execution ──
518
-
519
- def execute_run(handle, action, serde, retry_policy)
520
- propose_run_result(handle, action, serde, retry_policy)
521
- end
522
-
523
- # Like execute_run, but offloads the action to a real OS Thread.
524
- # The fiber yields (via IO.pipe) while the thread runs, keeping the event loop responsive.
525
- def execute_run_threaded(handle, action, serde, retry_policy)
526
- propose_run_result(handle, -> { offload_to_thread(action) }, serde, retry_policy)
527
- end
528
-
529
- # Runs the action and proposes the result (success/failure/transient) to the VM.
530
- def propose_run_result(handle, action, serde, retry_policy)
531
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
532
- begin
533
- result = action.call
534
- buffer = serde.serialize(result)
535
- @vm.propose_run_completion_success(handle, buffer)
536
- rescue TerminalError => e
537
- failure = Failure.new(code: e.status_code, message: e.message)
538
- @vm.propose_run_completion_failure(handle, failure)
539
- rescue SuspendedError, InternalError
540
- raise
541
- rescue StandardError => e
542
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
543
- attempt_duration_ms = (elapsed * 1000).to_i
544
- failure = Failure.new(
545
- code: 500,
546
- message: e.inspect,
547
- stacktrace: e.backtrace&.join("\n")
548
- )
549
- config = RunRetryConfig.new(
550
- initial_interval: retry_policy&.initial_interval,
551
- max_attempts: retry_policy&.max_attempts,
552
- max_duration: retry_policy&.max_duration,
553
- max_interval: retry_policy&.max_interval,
554
- interval_factor: retry_policy&.interval_factor
555
- )
556
- @vm.propose_run_completion_transient(
557
- handle,
558
- failure: failure,
559
- attempt_duration_ms: attempt_duration_ms,
560
- config: config
561
- )
562
- end
563
- end
564
-
565
- # Run a block in an OS thread from a shared pool, yielding the current fiber
566
- # until it completes. Uses IO.pipe to yield the fiber to the Async event loop
567
- # while the thread does work.
568
- #
569
- # The action is wrapped with a cancellation flag so that if the invocation
570
- # finishes (e.g., suspended, terminal error) before the pool picks up the job,
571
- # the action is skipped.
572
- #
573
- # Note: With Async 2.x and Ruby 3.1+, the Fiber Scheduler already intercepts
574
- # most blocking I/O (Net::HTTP, TCPSocket, etc.) and yields the fiber
575
- # automatically. +background: true+ is only needed for CPU-heavy native
576
- # extensions that release the GVL (e.g., image processing, crypto).
577
- def offload_to_thread(action)
578
- read_io, write_io = IO.pipe
579
- result = nil
580
- error = nil
581
- event = @attempt_finished_event
582
-
583
- begin
584
- BackgroundPool.submit do
585
- if event.set?
586
- # Attempt already finished before pool picked up the job — skip.
587
- next
588
- end
589
-
590
- result = action.call
591
- rescue Exception => e # rubocop:disable Lint/RescueException
592
- error = e
593
- ensure
594
- write_io.close unless write_io.closed?
595
- end
596
-
597
- # Yields the fiber in Async context; resumes when the worker closes write_io.
598
- read_io.read(1)
599
- read_io.close
600
-
601
- raise error if error
602
-
603
- result
604
- ensure
605
- read_io.close unless read_io.closed?
606
- write_io.close unless write_io.closed?
607
- end
608
- end
609
-
610
- # A simple fixed-size thread pool for background: true runs.
611
- # Avoids creating a new Thread per call (~1ms + ~1MB stack each).
612
- # Workers are daemon threads that do not prevent process exit.
613
- module BackgroundPool
614
- @queue = Queue.new
615
- @workers = []
616
- @mutex = Mutex.new
617
- @size = 0
618
-
619
- POOL_SIZE = Integer(ENV.fetch('RESTATE_BACKGROUND_POOL_SIZE', 8))
620
-
621
- module_function
622
-
623
- # Submit a block to be executed by a pool worker.
624
- def submit(&block)
625
- ensure_started
626
- @queue.push(block)
627
- end
628
-
629
- def ensure_started
630
- return if @size >= POOL_SIZE
631
-
632
- @mutex.synchronize do
633
- while @size < POOL_SIZE
634
- @size += 1
635
- worker = Thread.new do
636
- Kernel.loop do
637
- job = @queue.pop
638
- break if job == :shutdown
639
-
640
- job.call
641
- end
642
- end
643
- worker.name = "restate-bg-#{@size}"
644
- # Daemon thread: does not prevent the process from exiting.
645
- worker.report_on_exception = false
646
- @workers << worker
647
- end
648
- end
649
- end
650
- end
651
- end
652
- end