restate-sdk 0.5.1 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b23261e2b75927f25f70d83c93133191b894a2451c8a194e73a38e91b3f44d6
4
- data.tar.gz: 269ac3b7c8a496da0db88b53d5dedbd54afe64a6507c712c9d71c828811bfb55
3
+ metadata.gz: bc7f07e2b3bd62dbf82cb815a5b66443586e339bb3ae896e6b94594fdce9fab4
4
+ data.tar.gz: 9066dc59318b6e14f249c249c59b03b8e075df6986cf2b2d8a3d9b93ca56614b
5
5
  SHA512:
6
- metadata.gz: 3b66009141e9bfba8841a5f73c7a8bd4b808d36f1edd122f70e73b91f4718f5a0de1f8d653c55b1686839c66dbef802a1f71a13e7fba71fbb4d631b2c1721295
7
- data.tar.gz: 9caa27f5058be3d18cf9a06295dc828c5053cbb3d4622c6944ec6f27466b8b80e41b67e11ac244c3fb1847297e24b03997aec7fe602347bdabfa9377213b77d9
6
+ metadata.gz: 79b2cee29852e255f9745665f44b8d4450badc22319c59120a2988c9ccd9dfa88723529ffb5059dbf4c94d929722368e00a5a1887c0e32b48663196513768860
7
+ data.tar.gz: 8faf5c53ca4b7bc538ff348a3ccb99a0cdbac8a418d9263073f67a8476353d8c01fb714784c93237f644b35ea7f1ff08e89074a1e07a2761f9c0d59f892643d5
data/Cargo.lock CHANGED
@@ -569,7 +569,7 @@ dependencies = [
569
569
 
570
570
  [[package]]
571
571
  name = "restate_internal"
572
- version = "0.5.1"
572
+ version = "0.6.0"
573
573
  dependencies = [
574
574
  "magnus",
575
575
  "rb-sys",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "0.5.1"
3
+ version = "0.6.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -15,11 +15,15 @@ module Restate
15
15
  sig { returns(T.nilable(String)) }
16
16
  attr_accessor :protocol
17
17
 
18
+ sig { returns(T::Array[T.untyped]) }
19
+ attr_reader :middleware
20
+
18
21
  sig { void }
19
22
  def initialize
20
23
  @services = T.let({}, T::Hash[String, T.untyped])
21
24
  @protocol = T.let(nil, T.nilable(String))
22
25
  @identity_keys = T.let([], T::Array[String])
26
+ @middleware = T.let([], T::Array[T.untyped])
23
27
  end
24
28
 
25
29
  # Bind one or more services to this endpoint.
@@ -59,6 +63,70 @@ module Restate
59
63
  self
60
64
  end
61
65
 
66
+ # Add handler-level middleware.
67
+ #
68
+ # Middleware wraps every handler invocation with access to the handler metadata
69
+ # and context. Use it for tracing, metrics, logging, error reporting, etc.
70
+ #
71
+ # A middleware is a class whose instances respond to +call(handler, ctx)+.
72
+ # Use +yield+ inside +call+ to invoke the next middleware or the handler.
73
+ # The return value of +yield+ is the handler's return value.
74
+ #
75
+ # This follows the same pattern as {https://github.com/sidekiq/sidekiq/wiki/Middleware Sidekiq middleware}.
76
+ #
77
+ # @example OpenTelemetry tracing
78
+ # class OpenTelemetryMiddleware
79
+ # def call(handler, ctx)
80
+ # tracer.in_span(handler.name, attributes: {
81
+ # 'restate.service' => handler.service_tag.name,
82
+ # 'restate.invocation_id' => ctx.request.id
83
+ # }) do
84
+ # yield
85
+ # end
86
+ # end
87
+ # end
88
+ # endpoint.use(OpenTelemetryMiddleware)
89
+ #
90
+ # @example Metrics
91
+ # class MetricsMiddleware
92
+ # def call(handler, ctx)
93
+ # start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
94
+ # result = yield
95
+ # duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
96
+ # StatsD.timing("restate.handler.#{handler.name}", duration)
97
+ # result
98
+ # end
99
+ # end
100
+ # endpoint.use(MetricsMiddleware)
101
+ #
102
+ # @example Middleware with configuration
103
+ # class AuthMiddleware
104
+ # def initialize(api_key:)
105
+ # @api_key = api_key
106
+ # end
107
+ #
108
+ # def call(handler, ctx)
109
+ # raise Restate::TerminalError.new('unauthorized', status_code: 401) unless valid?(ctx)
110
+ # yield
111
+ # end
112
+ # end
113
+ # endpoint.use(AuthMiddleware, api_key: 'secret')
114
+ #
115
+ # @param klass [Class] middleware class (will be instantiated by the SDK)
116
+ # @param args [Array] positional arguments for the middleware constructor
117
+ # @param kwargs [Hash] keyword arguments for the middleware constructor
118
+ # @return [self]
119
+ sig { params(klass: T.untyped, args: T.untyped, kwargs: T.untyped).returns(T.self_type) }
120
+ def use(klass, *args, **kwargs)
121
+ instance = if kwargs.empty?
122
+ klass.new(*args)
123
+ else
124
+ klass.new(*args, **kwargs)
125
+ end
126
+ @middleware << instance
127
+ self
128
+ end
129
+
62
130
  # Build and return the Rack-compatible application.
63
131
  sig { returns(T.untyped) }
64
132
  def app
@@ -33,19 +33,32 @@ module Restate
33
33
 
34
34
  # Invoke a handler with the context and raw input bytes.
35
35
  # The context is passed as the first argument to every handler.
36
+ # Middleware (if any) wraps the handler call.
36
37
  # Returns raw output bytes.
37
- sig { params(handler: T.untyped, ctx: T.untyped, in_buffer: String).returns(String) }
38
- def invoke_handler(handler:, ctx:, in_buffer:)
39
- if handler.arity == 2
40
- begin
41
- in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
42
- rescue StandardError => e
43
- Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
38
+ sig do
39
+ params(handler: T.untyped, ctx: T.untyped, in_buffer: String,
40
+ middleware: T::Array[T.untyped]).returns(String)
41
+ end
42
+ def invoke_handler(handler:, ctx:, in_buffer:, middleware: []) # rubocop:disable Metrics/AbcSize
43
+ call_handler = Kernel.proc do
44
+ if handler.arity == 2
45
+ begin
46
+ in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
47
+ rescue StandardError => e
48
+ Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
49
+ end
50
+ handler.callable.call(ctx, in_arg)
51
+ else
52
+ handler.callable.call(ctx)
44
53
  end
45
- out_arg = handler.callable.call(ctx, in_arg)
46
- else
47
- out_arg = handler.callable.call(ctx)
48
54
  end
55
+
56
+ # Build the middleware chain so each middleware can use `yield` to call the next.
57
+ chain = middleware.reverse.reduce(call_handler) do |nxt, mw|
58
+ Kernel.proc { mw.call(handler, ctx, &nxt) }
59
+ end
60
+
61
+ out_arg = chain.call
49
62
  handler.handler_io.output_serde.serialize(out_arg)
50
63
  end
51
64
  end
@@ -204,7 +204,8 @@ module Restate
204
204
  handler: handler,
205
205
  invocation: invocation,
206
206
  send_output: send_output,
207
- input_queue: input_queue
207
+ input_queue: input_queue,
208
+ middleware: @endpoint.middleware
208
209
  )
209
210
 
210
211
  # Spawn the handler as an async task so the response body can stream
@@ -28,8 +28,11 @@ module Restate
28
28
  sig { returns(T.untyped) }
29
29
  attr_reader :invocation
30
30
 
31
- sig { params(vm: VMWrapper, handler: T.untyped, invocation: T.untyped, send_output: T.untyped, input_queue: Async::Queue).void }
32
- def initialize(vm:, handler:, invocation:, send_output:, input_queue:)
31
+ sig do
32
+ params(vm: VMWrapper, handler: T.untyped, invocation: T.untyped, send_output: T.untyped,
33
+ input_queue: Async::Queue, middleware: T::Array[T.untyped]).void
34
+ end
35
+ def initialize(vm:, handler:, invocation:, send_output:, input_queue:, middleware: [])
33
36
  @vm = T.let(vm, VMWrapper)
34
37
  @handler = T.let(handler, T.untyped)
35
38
  @invocation = T.let(invocation, T.untyped)
@@ -37,6 +40,7 @@ module Restate
37
40
  @input_queue = T.let(input_queue, Async::Queue)
38
41
  @run_coros_to_execute = T.let({}, T::Hash[Integer, T.untyped])
39
42
  @attempt_finished_event = T.let(AttemptFinishedEvent.new, AttemptFinishedEvent)
43
+ @middleware = T.let(middleware, T::Array[T.untyped])
40
44
  end
41
45
 
42
46
  # ── Main entry point ──
@@ -48,7 +52,8 @@ module Restate
48
52
  Thread.current[:restate_service_kind] = @handler.service_tag.kind
49
53
  Thread.current[:restate_handler_kind] = @handler.kind
50
54
  in_buffer = @invocation.input_buffer
51
- out_buffer = Restate.invoke_handler(handler: @handler, ctx: self, in_buffer: in_buffer)
55
+ out_buffer = Restate.invoke_handler(handler: @handler, ctx: self, in_buffer: in_buffer,
56
+ middleware: @middleware)
52
57
  @vm.sys_write_output_success(out_buffer.b)
53
58
  @vm.sys_end
54
59
  rescue TerminalError => e
@@ -52,14 +52,17 @@ module Restate
52
52
  # @param restate_image [String] Docker image for Restate server.
53
53
  # @param always_replay [Boolean] Force replay on every suspension point.
54
54
  # @param disable_retries [Boolean] Disable Restate retry policy.
55
+ # @yield [Endpoint] Optional block to configure the endpoint (e.g. add middleware).
55
56
  def initialize(*services,
56
57
  restate_image: 'docker.io/restatedev/restate:latest',
57
58
  always_replay: false,
58
- disable_retries: false)
59
+ disable_retries: false,
60
+ &configure)
59
61
  @services = services
60
62
  @restate_image = restate_image
61
63
  @always_replay = always_replay
62
64
  @disable_retries = disable_retries
65
+ @configure = configure
63
66
  @server_thread = nil
64
67
  @container = nil
65
68
  @port = nil
@@ -69,7 +72,9 @@ module Restate
69
72
 
70
73
  def start
71
74
  @port = find_free_port
72
- rack_app = Restate.endpoint(*@services).app
75
+ endpoint = Restate.endpoint(*@services)
76
+ @configure&.call(endpoint)
77
+ rack_app = endpoint.app
73
78
  start_sdk_server(rack_app)
74
79
  wait_for_tcp(@port)
75
80
  start_restate_container
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Restate
5
- VERSION = '0.5.1'
5
+ VERSION = '0.6.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restate-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Restate Developers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-18 00:00:00.000000000 Z
11
+ date: 2026-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async