volcano-sdk 0.34.0 → 0.35.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -4
  3. data/lib/volcano/client.rb +1 -1
  4. data/lib/volcano/durable.rb +9 -4
  5. data/lib/volcano/durable_approval_models.rb +1 -1
  6. data/lib/volcano/durable_message_arguments.rb +49 -0
  7. data/lib/volcano/durable_message_backlog.rb +38 -0
  8. data/lib/volcano/durable_message_catch_ups.rb +112 -0
  9. data/lib/volcano/durable_message_models.rb +62 -0
  10. data/lib/volcano/durable_message_responses.rb +107 -0
  11. data/lib/volcano/durable_message_session.rb +72 -0
  12. data/lib/volcano/durable_message_stream.rb +176 -0
  13. data/lib/volcano/durable_messages.rb +217 -0
  14. data/lib/volcano/durable_models.rb +32 -3
  15. data/lib/volcano/generated/lib/volcano-generated/api/auth_admin_api.rb +2 -2
  16. data/lib/volcano/generated/lib/volcano-generated/api/auth_configuration_api.rb +2 -2
  17. data/lib/volcano/generated/lib/volcano-generated/api/autofix_api.rb +222 -0
  18. data/lib/volcano/generated/lib/volcano-generated/api/durable_functions_api.rb +725 -0
  19. data/lib/volcano/generated/lib/volcano-generated/api/frontends_api.rb +2 -2
  20. data/lib/volcano/generated/lib/volcano-generated/api/issues_api.rb +254 -0
  21. data/lib/volcano/generated/lib/volcano-generated/api/sandboxes_api.rb +20 -20
  22. data/lib/volcano/generated/lib/volcano-generated/api/system_api.rb +57 -0
  23. data/lib/volcano/generated/lib/volcano-generated/api/variable_environments_api.rb +373 -0
  24. data/lib/volcano/generated/lib/volcano-generated/api/verified_domains_api.rb +2 -2
  25. data/lib/volcano/generated/lib/volcano-generated/models/auth_config.rb +46 -4
  26. data/lib/volcano/generated/lib/volcano-generated/models/auth_insights_series_point.rb +110 -2
  27. data/lib/volcano/generated/lib/volcano-generated/models/autofix_model.rb +45 -0
  28. data/lib/volcano/generated/lib/volcano-generated/models/autofix_provider.rb +40 -0
  29. data/lib/volcano/generated/lib/volcano-generated/models/autofix_settings.rb +385 -0
  30. data/lib/volcano/generated/lib/volcano-generated/models/autofix_stage.rb +40 -0
  31. data/lib/volcano/generated/lib/volcano-generated/models/capability.rb +214 -0
  32. data/lib/volcano/generated/lib/volcano-generated/models/capability_list.rb +166 -0
  33. data/lib/volcano/generated/lib/volcano-generated/models/capability_status.rb +40 -0
  34. data/lib/volcano/generated/lib/volcano-generated/models/create_sandbox_session_request.rb +16 -53
  35. data/lib/volcano/generated/lib/volcano-generated/models/create_variable_environment_request.rb +176 -0
  36. data/lib/volcano/generated/lib/volcano-generated/models/durable_execution.rb +14 -4
  37. data/lib/volcano/generated/lib/volcano-generated/models/durable_message.rb +396 -0
  38. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_direction.rb +40 -0
  39. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_list.rb +194 -0
  40. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_sender.rb +203 -0
  41. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_session_registration.rb +165 -0
  42. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_status.rb +43 -0
  43. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait.rb +278 -0
  44. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_list.rb +167 -0
  45. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_registration.rb +225 -0
  46. data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_status.rb +43 -0
  47. data/lib/volcano/generated/lib/volcano-generated/models/frontend.rb +11 -1
  48. data/lib/volcano/generated/lib/volcano-generated/models/frontend_custom_domain_conflict_error.rb +1 -1
  49. data/lib/volcano/generated/lib/volcano-generated/models/frontend_custom_domain_response.rb +25 -2
  50. data/lib/volcano/generated/lib/volcano-generated/models/issue.rb +493 -0
  51. data/lib/volcano/generated/lib/volcano-generated/models/issue_status.rb +41 -0
  52. data/lib/volcano/generated/lib/volcano-generated/models/metric_usage_data.rb +1 -1
  53. data/lib/volcano/generated/lib/volcano-generated/models/open_durable_message_session_request.rb +228 -0
  54. data/lib/volcano/generated/lib/volcano-generated/models/paginated_issues.rb +230 -0
  55. data/lib/volcano/generated/lib/volcano-generated/models/project_config.rb +10 -1
  56. data/lib/volcano/generated/lib/volcano-generated/models/project_config_autofix.rb +206 -0
  57. data/lib/volcano/generated/lib/volcano-generated/models/project_config_custom_domain.rb +1 -1
  58. data/lib/volcano/generated/lib/volcano-generated/models/project_config_sandbox.rb +12 -50
  59. data/lib/volcano/generated/lib/volcano-generated/models/project_frontend_custom_domain.rb +25 -2
  60. data/lib/volcano/generated/lib/volcano-generated/models/publish_durable_message_request.rb +205 -0
  61. data/lib/volcano/generated/lib/volcano-generated/models/register_durable_message_wait_request.rb +284 -0
  62. data/lib/volcano/generated/lib/volcano-generated/models/rename_variable_environment_request.rb +176 -0
  63. data/lib/volcano/generated/lib/volcano-generated/models/request_durable_approval_request.rb +1 -1
  64. data/lib/volcano/generated/lib/volcano-generated/models/sandbox_command_request.rb +11 -12
  65. data/lib/volcano/generated/lib/volcano-generated/models/sandbox_execution_request.rb +51 -13
  66. data/lib/volcano/generated/lib/volcano-generated/models/sandbox_session.rb +4 -30
  67. data/lib/volcano/generated/lib/volcano-generated/models/send_durable_message_request.rb +205 -0
  68. data/lib/volcano/generated/lib/volcano-generated/models/update_auth_config_request.rb +79 -1
  69. data/lib/volcano/generated/lib/volcano-generated/models/update_autofix_settings_request.rb +273 -0
  70. data/lib/volcano/generated/lib/volcano-generated/models/update_issue_request.rb +186 -0
  71. data/lib/volcano/generated/lib/volcano-generated/models/variable_environment.rb +325 -0
  72. data/lib/volcano/generated/lib/volcano-generated/models/variable_environment_list.rb +175 -0
  73. data/lib/volcano/generated/lib/volcano-generated.rb +34 -0
  74. data/lib/volcano/generated_transport.rb +1 -0
  75. data/lib/volcano/generated_transport_durable_messages.rb +57 -0
  76. data/lib/volcano/realtime/channel_callbacks.rb +5 -1
  77. data/lib/volcano/realtime/channel_lifecycle.rb +4 -1
  78. data/lib/volcano/realtime/durable_channel.rb +47 -0
  79. data/lib/volcano/realtime/lifecycle.rb +37 -1
  80. data/lib/volcano/realtime/protocol.rb +7 -3
  81. data/lib/volcano/realtime/protocol_dispatch.rb +1 -1
  82. data/lib/volcano/realtime/protocol_io.rb +7 -4
  83. data/lib/volcano/realtime/protocol_lifecycle.rb +7 -0
  84. data/lib/volcano/realtime/protocol_recovery.rb +23 -12
  85. data/lib/volcano/realtime.rb +42 -8
  86. data/lib/volcano/redaction.rb +1 -1
  87. data/lib/volcano/version.rb +1 -1
  88. data/lib/volcano.rb +8 -0
  89. data/sig/core_facades.rbs +15 -0
  90. data/sig/durable_message_records.rbs +68 -0
  91. data/sig/durable_records.rbs +17 -9
  92. metadata +46 -1
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Durable message operations for the internal generated transport. A target
5
+ # with a project id takes the owner's project route; one without takes the
6
+ # application route, with the execution's client token when there is one.
7
+ class GeneratedTransport
8
+ def send_durable_message(authorization:, target:, name:, data:, idempotency_key:)
9
+ invoke do
10
+ durable = @api_factory.call(authorization).durable
11
+ body = Generated::SendDurableMessageRequest.new(name: name, data: data)
12
+ payload, status, headers = send_on_route(durable, target, body, idempotency_key)
13
+ response(payload, status, headers)
14
+ end
15
+ end
16
+
17
+ def list_durable_messages(authorization:, target:, query:)
18
+ invoke do
19
+ data, status, headers = list_on_route(@api_factory.call(authorization).durable, target, query)
20
+ response(data, status, headers)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def send_on_route(durable, target, body, idempotency_key)
27
+ if target.project_id
28
+ durable.send_durable_message_with_http_info(
29
+ target.project_id, target.function_id, target.execution_id, body,
30
+ durable_message_options(idempotency_key: idempotency_key)
31
+ )
32
+ else
33
+ durable.send_durable_message_from_application_with_http_info(
34
+ target.function_id, target.execution_id, body,
35
+ durable_message_options(idempotency_key: idempotency_key, x_volcano_client_token: target.client_token)
36
+ )
37
+ end
38
+ end
39
+
40
+ def list_on_route(durable, target, query)
41
+ if target.project_id
42
+ durable.list_durable_messages_with_http_info(
43
+ target.project_id, target.function_id, target.execution_id, durable_message_options(**query)
44
+ )
45
+ else
46
+ durable.list_durable_messages_from_application_with_http_info(
47
+ target.function_id, target.execution_id,
48
+ durable_message_options(**query, x_volcano_client_token: target.client_token)
49
+ )
50
+ end
51
+ end
52
+
53
+ def durable_message_options(**options)
54
+ options.compact.merge(debug_return_type: 'Object')
55
+ end
56
+ end
57
+ end
@@ -61,14 +61,18 @@ module Volcano
61
61
  end
62
62
  end
63
63
 
64
+ # A durable message has no event field; everything the server publishes
65
+ # on its channel is one.
64
66
  def deliver_publication(event, data)
65
67
  if postgres?
66
68
  dispatch_postgres_change(data)
67
- elsif event == 'message'
69
+ elsif event == 'message' || durable?
68
70
  emit('message', data)
69
71
  end
70
72
  end
71
73
 
74
+ def durable? = @type == :durable
75
+
72
76
  def emit(event, data, before_delivery: nil)
73
77
  callbacks = @callbacks[event].dup
74
78
  return if callbacks.empty?
@@ -10,6 +10,7 @@ module Volcano
10
10
  def mark_closed
11
11
  @closed = true
12
12
  @subscription_desired = @subscribed = false
13
+ @follower&.close
13
14
  end_postgres_delivery
14
15
  reset_presence
15
16
  end
@@ -81,7 +82,9 @@ module Volcano
81
82
  detach_publication_handler(protocol)
82
83
  end
83
84
 
84
- def subscription_options(binding) = { recovery: binding&.last, recoverable: presence?, join_leave: presence? }
85
+ def subscription_options(binding)
86
+ { recovery: binding&.last, recoverable: presence?, join_leave: presence?, data: @subscribe_data }
87
+ end
85
88
 
86
89
  def recovery_binding
87
90
  return unless broadcast?
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ class Realtime
5
+ # Follows a durable execution's messages on a channel.
6
+ module DurableChannel
7
+ # @dynamic durable?
8
+
9
+ private
10
+
11
+ # +data+ goes with every subscribe, including the ones a reconnect
12
+ # restores. +follower+ is asked before each of those restores, hears each
13
+ # one and each publication the connection dropped, and is closed with the
14
+ # channel.
15
+ def follow_durable(data, follower)
16
+ @subscribe_data = data
17
+ @follower = follower
18
+ end
19
+
20
+ # A durable channel's follower reads back what the connection dropped.
21
+ def publication_rejection(protocol, generation)
22
+ follower = @follower
23
+ follower ? ->(_publication) { follower.dropped } : super
24
+ end
25
+
26
+ # The server refuses a durable subscribe for good once the execution is
27
+ # gone or hidden from the session, so the follower ends the subscription
28
+ # instead of the reconnect retrying it, and the refusal says so.
29
+ def restore_refused(error)
30
+ follower = @follower
31
+ return error unless follower
32
+ return error if error.temporary?
33
+
34
+ follower.refused
35
+ ServerError.new("#{error.message}; the durable message subscription has ended", code: error.code)
36
+ end
37
+
38
+ # The server authorizes a durable subscribe by the socket's session, so
39
+ # the socket must belong to the client's current one.
40
+ def prepare_protocol_subscription(protocol, epoch)
41
+ @realtime.__send__(:capture_protocol_session) if durable?
42
+ super
43
+ end
44
+ end
45
+ private_constant :DurableChannel
46
+ end
47
+ end
@@ -4,7 +4,7 @@ module Volcano
4
4
  class Realtime
5
5
  # Channel collection and connection-state lifecycle operations.
6
6
  module Lifecycle
7
- # @dynamic channel_lock, normalize_channel_type
7
+ # @dynamic channel_lock, normalize_channel_type, report_channel_error, protocol
8
8
  def connected? = !@closed && (@protocol&.connected? || false)
9
9
 
10
10
  def remove_channel(name, type: :broadcast)
@@ -34,6 +34,42 @@ module Volcano
34
34
  @channels.delete(name)
35
35
  end
36
36
 
37
+ # Leaves a durable channel unless a later subscription replaced it. Once
38
+ # the connection is closed there is nothing left to leave.
39
+ def remove_durable_channel(channel)
40
+ channel_lock.acquire do
41
+ leave_durable_channel(channel) if forget_durable_channel(channel) && !@closed
42
+ end
43
+ nil
44
+ rescue Async::Cancel
45
+ # Stopped waiting for the lock, so the channel is still subscribed.
46
+ abandon_durable_leave(channel) { protocol.drop } if forget_durable_channel(channel)
47
+ raise
48
+ end
49
+
50
+ def forget_durable_channel(channel)
51
+ @channels.delete(channel.name) if @channels[channel.name].equal?(channel)
52
+ end
53
+
54
+ # A leave the server did not confirm may have left the subscription in
55
+ # place, so the connection is dropped, which ends it. The reconnect
56
+ # restores only the channels still in use.
57
+ def leave_durable_channel(channel)
58
+ channel.__send__(:remove)
59
+ rescue StandardError => e
60
+ abandon_durable_leave(channel) { protocol.close }
61
+ report_channel_error(e)
62
+ rescue Async::Cancel
63
+ # Closing here would start the reconnect in the task being stopped.
64
+ abandon_durable_leave(channel) { protocol.drop }
65
+ raise
66
+ end
67
+
68
+ def abandon_durable_leave(channel)
69
+ channel.mark_closed
70
+ yield if connected?
71
+ end
72
+
37
73
  def remove_channel_entry(name, channel)
38
74
  channel.__send__(:remove)
39
75
  @channels.delete(name)
@@ -16,10 +16,14 @@ module Volcano
16
16
 
17
17
  # @dynamic code
18
18
 
19
- def initialize(message, code: nil)
19
+ def initialize(message, code: nil, temporary: false)
20
20
  super(message)
21
21
  @code = code.is_a?(String) ? code.dup.freeze : code
22
+ @temporary = temporary == true
22
23
  end
24
+
25
+ # Whether the server expects the same command to succeed later.
26
+ def temporary? = @temporary
23
27
  end
24
28
 
25
29
  class ClosedError < StandardError; end
@@ -93,12 +97,12 @@ module Volcano
93
97
  result.tap { @connected = true }
94
98
  end
95
99
 
96
- def subscribe(channel:, recoverable: false, join_leave: false, recovery: nil)
100
+ def subscribe(channel:, recoverable: false, join_leave: false, recovery: nil, data: nil)
97
101
  @subscription_lock.acquire do
98
102
  ensure_open!
99
103
  raise DuplicateSubscriptionError, "already subscribed to #{channel}" if @subscriptions.include?(channel)
100
104
 
101
- result = subscribe_request(channel:, recoverable:, join_leave:, recovery:)
105
+ result = subscribe_request(channel:, recoverable:, join_leave:, recovery:, data:)
102
106
  ensure_open!
103
107
  @subscriptions.add(channel)
104
108
  result
@@ -59,7 +59,7 @@ module Volcano
59
59
  raise TypeError, 'realtime error must be an object' unless error.is_a?(Hash)
60
60
 
61
61
  message = error['message'] || 'realtime command failed'
62
- Protocol::Failure.new(error: ServerError.new(message, code: error['code']))
62
+ Protocol::Failure.new(error: ServerError.new(message, code: error['code'], temporary: error['temporary']))
63
63
  end
64
64
 
65
65
  def successful_reply(frame, reply_key)
@@ -10,11 +10,13 @@ module Volcano
10
10
 
11
11
  private
12
12
 
13
- def request(reply_key, on_reply: nil)
13
+ # on_write runs once the frame holds the write lock, when the server may
14
+ # start to receive it.
15
+ def request(reply_key, on_reply: nil, on_write: nil)
14
16
  # @type var registered_id: Integer?
15
17
  ensure_open!
16
18
  registered_id, reply_queue = register_request(reply_key, on_reply)
17
- write_frame(yield(registered_id || raise(TypeError, 'invalid realtime request id')))
19
+ write_frame(yield(registered_id || raise(TypeError, 'invalid realtime request id')), on_write)
18
20
  reply = await_reply(reply_queue)
19
21
  raise reply.error if reply.is_a?(Failure)
20
22
 
@@ -36,10 +38,11 @@ module Volcano
36
38
  [@next_id, queue]
37
39
  end
38
40
 
39
- def write_frame(frame) = write_serialized_frame("#{JSON.generate(frame)}\n")
41
+ def write_frame(frame, on_write = nil) = write_serialized_frame("#{JSON.generate(frame)}\n", on_write)
40
42
 
41
- def write_serialized_frame(frame)
43
+ def write_serialized_frame(frame, on_write = nil)
42
44
  @write_lock.acquire do
45
+ on_write&.call
43
46
  @socket.write(frame)
44
47
  @socket.flush
45
48
  end
@@ -9,6 +9,13 @@ module Volcano
9
9
  # @dynamic dispatch_publication_batches, read_loop
10
10
  def connected? = @connected && !@closed
11
11
 
12
+ # Ends the connection from the reader, as a loss would, so the reconnect
13
+ # runs in the protocol's tasks rather than the caller's.
14
+ def drop
15
+ close_socket
16
+ nil
17
+ end
18
+
12
19
  def on_publication(channel, on_rejection: nil, &handler)
13
20
  ensure_open!
14
21
  @publication_handlers[channel] << ProtocolPublicationDispatch::Registration.new(
@@ -6,27 +6,38 @@ module Volcano
6
6
  module ProtocolRecovery
7
7
  # @dynamic class, request, dispatch_recovered_publications, recovery_position
8
8
  # @dynamic delete_recovery_state, set_recovery_position, mark_unknown_gap
9
- # @dynamic mark_publication_gap, same_epoch?
9
+ # @dynamic mark_publication_gap, same_epoch?, close_socket
10
10
 
11
11
  private
12
12
 
13
- def subscribe_request(channel:, recoverable:, join_leave:, recovery:)
14
- on_reply = nil
15
- if recovery
16
- on_reply = lambda do |reply|
17
- publications = parse_recovery_result(channel: channel, recovery: recovery, result: reply)
18
- dispatch_recovered_publications(channel, publications)
19
- end
20
- end
21
-
22
- request('subscribe', on_reply:) do |id|
23
- self.class.subscribe(
13
+ def subscribe_request(channel:, recoverable:, join_leave:, recovery:, data:)
14
+ writing = false
15
+ request('subscribe', on_reply: recovery_reply(channel, recovery), on_write: -> { writing = true }) do |id|
16
+ command = self.class.subscribe(
24
17
  id: id,
25
18
  channel: channel,
26
19
  recoverable: recoverable,
27
20
  join_leave: join_leave,
28
21
  recovery: recovery
29
22
  )
23
+ command['subscribe']['data'] = data if data
24
+ command
25
+ end
26
+ rescue Async::Cancel
27
+ # Once the frame is being written the server may subscribe, and nothing
28
+ # would leave the channel. Closing the socket has the reader end the
29
+ # connection, as on a loss, so the reconnect is not cancelled along with
30
+ # this task. Stopped before that, there is nothing to leave.
31
+ close_socket if writing
32
+ raise
33
+ end
34
+
35
+ def recovery_reply(channel, recovery)
36
+ return unless recovery
37
+
38
+ lambda do |reply|
39
+ publications = parse_recovery_result(channel: channel, recovery: recovery, result: reply)
40
+ dispatch_recovered_publications(channel, publications)
30
41
  end
31
42
  end
32
43
 
@@ -16,6 +16,7 @@ require_relative 'realtime/postgres_batch'
16
16
  require_relative 'realtime/postgres_delivery'
17
17
  require_relative 'realtime/broadcast_delivery'
18
18
  require_relative 'realtime/channel_callbacks'
19
+ require_relative 'realtime/durable_channel'
19
20
 
20
21
  module Volcano
21
22
  # Manages a project's realtime connection and broadcast channels.
@@ -68,6 +69,9 @@ module Volcano
68
69
  end
69
70
 
70
71
  CHANNEL_TYPES = %i[broadcast presence postgres].freeze
72
+ # A channel that fetches no rows still takes a batch configuration.
73
+ UNBATCHED = PostgresBatchConfig.new(auto_fetch: false, batch_window_ms: 1, max_batch_size: 1)
74
+ private_constant :UNBATCHED
71
75
 
72
76
  # Owns the public realtime facade and its channel collection.
73
77
  module Core
@@ -152,6 +156,8 @@ module Volcano
152
156
 
153
157
  def report_channel_error(error) = protocol_error(public_error(error))
154
158
 
159
+ def blocking(&) = BlockingCall.call(&)
160
+
155
161
  def cached_channel(channel, batch_config)
156
162
  channel.__send__(:ensure_fetch_config!, batch_config)
157
163
  channel
@@ -161,6 +167,20 @@ module Volcano
161
167
  Channel.new(self, -> { protocol }, name, type, batch_config:)
162
168
  end
163
169
 
170
+ # Opens the channel an execution publishes its messages on, or answers
171
+ # nil when this client already follows the execution.
172
+ def durable_channel(execution_id, client_token, follower)
173
+ channel_lock.acquire do
174
+ ensure_open!
175
+ name = "durable:#{execution_id}"
176
+ next if @channels.key?(name)
177
+
178
+ channel = build_channel(name, :durable, UNBATCHED)
179
+ channel.__send__(:follow_durable, client_token && { 'client_token' => client_token }, follower)
180
+ @channels[name] = channel
181
+ end
182
+ end
183
+
164
184
  def protocol_lock
165
185
  require 'async/semaphore'
166
186
  @protocol_lock ||= Async::Semaphore.new(1)
@@ -186,6 +206,7 @@ module Volcano
186
206
  include PostgresDelivery
187
207
  include BroadcastDelivery
188
208
  include ChannelCallbacks
209
+ include DurableChannel
189
210
 
190
211
  attr_reader :name
191
212
 
@@ -194,7 +215,7 @@ module Volcano
194
215
  @protocol_provider = protocol_provider
195
216
  @name = name.freeze
196
217
  @handler_registered = @subscribed = @subscription_desired = @closed = false
197
- @lifecycle_lock = nil
218
+ @lifecycle_lock = @subscribe_data = @follower = nil
198
219
  initialize_recovery_state
199
220
  initialize_callback_dispatch
200
221
  initialize_presence(type)
@@ -230,13 +251,13 @@ module Volcano
230
251
  def restoration_needed? = subscription_desired? && !@subscribed
231
252
 
232
253
  def restore_subscription(protocol)
233
- state = with_lifecycle_lock do
234
- restoration_needed? ? subscribe_protocol(protocol) : nil
235
- end
236
- if state
237
- subscribed_protocol, epoch = state
238
- sync_presence(subscribed_protocol, epoch)
239
- end
254
+ ask_follower_to_restore
255
+ state = resubscribe(protocol)
256
+ return unless state
257
+
258
+ subscribed_protocol, epoch = state
259
+ sync_presence(subscribed_protocol, epoch)
260
+ @follower&.resubscribed
240
261
  nil
241
262
  end
242
263
 
@@ -247,6 +268,7 @@ module Volcano
247
268
  clear_broadcast_recovery_state
248
269
  clear_channel_recovery_state
249
270
  mark_removed
271
+ @follower&.close
250
272
  end
251
273
  nil
252
274
  end
@@ -273,6 +295,18 @@ module Volcano
273
295
  raise
274
296
  end
275
297
 
298
+ # Outside the lifecycle lock: a follower that refuses may leave the channel.
299
+ def ask_follower_to_restore
300
+ @follower&.restoring if restoration_needed?
301
+ end
302
+
303
+ # The refusal is handled outside the lifecycle lock too, for the same reason.
304
+ def resubscribe(protocol)
305
+ with_lifecycle_lock { restoration_needed? ? subscribe_protocol(protocol) : nil }
306
+ rescue ServerError => e
307
+ raise restore_refused(e)
308
+ end
309
+
276
310
  def with_lifecycle_lock(&)
277
311
  require 'async/semaphore'
278
312
  lock = @lifecycle_lock ||= Async::Semaphore.new(1)
@@ -25,7 +25,7 @@ module Volcano
25
25
  return copy_volcano_error(error, redacted_message)
26
26
  end
27
27
  if defined?(Realtime::ServerError) && error.is_a?(Realtime::ServerError)
28
- return error.class.new(redacted_message, code: error.code)
28
+ return error.class.new(redacted_message, code: error.code, temporary: error.temporary?)
29
29
  end
30
30
 
31
31
  error.exception(redacted_message)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.34.0'
4
+ VERSION = '0.35.0'
5
5
  end
data/lib/volcano.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'volcano/session_credentials'
9
9
  require_relative 'volcano/function_response'
10
10
  require_relative 'volcano/durable_models'
11
11
  require_relative 'volcano/durable_approval_models'
12
+ require_relative 'volcano/durable_message_models'
12
13
  require_relative 'volcano/log_responses'
13
14
  require_relative 'volcano/storage_models'
14
15
  require_relative 'volcano/storage_upload_models'
@@ -48,6 +49,13 @@ require_relative 'volcano/functions'
48
49
  require_relative 'volcano/durable_arguments'
49
50
  require_relative 'volcano/durable_approval_responses'
50
51
  require_relative 'volcano/durable_approvals'
52
+ require_relative 'volcano/durable_message_arguments'
53
+ require_relative 'volcano/durable_message_responses'
54
+ require_relative 'volcano/durable_message_catch_ups'
55
+ require_relative 'volcano/durable_message_backlog'
56
+ require_relative 'volcano/durable_message_stream'
57
+ require_relative 'volcano/durable_message_session'
58
+ require_relative 'volcano/durable_messages'
51
59
  require_relative 'volcano/durable'
52
60
  require_relative 'volcano/logs'
53
61
  require_relative 'volcano/storage_session_adapter'
data/sig/core_facades.rbs CHANGED
@@ -1,6 +1,7 @@
1
1
  module Volcano
2
2
  type sdk_json = nil | bool | Integer | Float | String | Time | Array[sdk_json] | Hash[String | Symbol, sdk_json]
3
3
  type sdk_payload = Hash[String | Symbol, sdk_json]
4
+ type durable_message_filters = { ?direction: (String | Symbol)?, ?name: String?, ?after: Integer?, ?limit: Integer?, ?client_token: String?, ?project_id: String? }
4
5
  type durable_approval_filters = { ?status: String?, ?function: String?, ?execution_id: String?, ?from: (Time | String)?, ?to: (Time | String)?, ?page: Integer?, ?limit: Integer? }
5
6
  type database_rows = Array[Hash[String, json_value]]
6
7
 
@@ -79,8 +80,22 @@ module Volcano
79
80
  def deny: (String project_id, String approval_id, ?comment: String?) -> DurableApproval
80
81
  end
81
82
 
83
+ class DurableMessageSubscription
84
+ def unsubscribe: () -> nil
85
+ def inspect: () -> String
86
+ def to_s: () -> String
87
+ def pretty_print: (DurableExecution::_PrettyPrinter printer) -> void
88
+ end
89
+
90
+ class DurableMessages
91
+ def send_message: (String function_name, String execution_id, String name, ?sdk_json data, **String? options) -> DurableMessage
92
+ def list: (String function_name, String execution_id, ?durable_message_filters filters) -> DurableMessagePage
93
+ def subscribe: (String function_name, String execution_id, ?client_token: String?, ?project_id: String?) { (DurableMessage) -> void } -> DurableMessageSubscription
94
+ end
95
+
82
96
  class Durable
83
97
  attr_reader approvals: DurableApprovals
98
+ attr_reader messages: DurableMessages
84
99
 
85
100
  def start: (String function_name, ?sdk_payload payload, ?execution_name: String?) -> DurableExecution
86
101
  def get: (String project_id, String function_name, String execution_id) -> DurableExecution
@@ -0,0 +1,68 @@
1
+ module Volcano
2
+ class DurableMessageSender < Data
3
+ attr_reader type: String
4
+ attr_reader user_id: String?
5
+
6
+ def self.new: (String type, ?String? user_id) -> DurableMessageSender
7
+ | (type: String, ?user_id: String?) -> DurableMessageSender
8
+ def self.[]: (String type, ?String? user_id) -> DurableMessageSender
9
+ | (type: String, ?user_id: String?) -> DurableMessageSender
10
+ def self.members: () -> [:type, :user_id]
11
+
12
+ def initialize: (type: String, ?user_id: String?) -> void
13
+ def members: () -> [:type, :user_id]
14
+ def with: (?type: String, ?user_id: String?) -> DurableMessageSender
15
+ def to_h: () -> { type: String, user_id: String? }
16
+ | [K, V] () { (Symbol, String?) -> [K, V] } -> Hash[K, V]
17
+ def deconstruct: () -> [String, String?]
18
+ def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, String?]
19
+ end
20
+
21
+ class DurableMessage < Data
22
+ type field = String | Integer | json_value | DurableMessageSender | Time | nil
23
+
24
+ attr_reader id: String
25
+ attr_reader execution_id: String
26
+ attr_reader direction: String
27
+ attr_reader seq: Integer
28
+ attr_reader name: String
29
+ attr_reader data: json_value
30
+ attr_reader sender: DurableMessageSender
31
+ attr_reader status: String
32
+ attr_reader sent_at: Time
33
+ attr_reader delivered_at: Time?
34
+
35
+ def self.new: (String id, String execution_id, String direction, Integer seq, String name, json_value data, DurableMessageSender sender, String status, Time sent_at, ?Time? delivered_at) -> DurableMessage
36
+ | (id: String, execution_id: String, direction: String, seq: Integer, name: String, ?data: json_value, sender: DurableMessageSender, status: String, sent_at: Time, ?delivered_at: Time?) -> DurableMessage
37
+ def self.[]: (String id, String execution_id, String direction, Integer seq, String name, json_value data, DurableMessageSender sender, String status, Time sent_at, ?Time? delivered_at) -> DurableMessage
38
+ | (id: String, execution_id: String, direction: String, seq: Integer, name: String, ?data: json_value, sender: DurableMessageSender, status: String, sent_at: Time, ?delivered_at: Time?) -> DurableMessage
39
+ def self.members: () -> [:id, :execution_id, :direction, :seq, :name, :data, :sender, :status, :sent_at, :delivered_at]
40
+
41
+ private def initialize: (**field attributes) -> void
42
+ def members: () -> [:id, :execution_id, :direction, :seq, :name, :data, :sender, :status, :sent_at, :delivered_at]
43
+ def with: (?id: String, ?execution_id: String, ?direction: String, ?seq: Integer, ?name: String, ?data: json_value, ?sender: DurableMessageSender, ?status: String, ?sent_at: Time, ?delivered_at: Time?) -> DurableMessage
44
+ def to_h: () -> { id: String, execution_id: String, direction: String, seq: Integer, name: String, data: json_value, sender: DurableMessageSender, status: String, sent_at: Time, delivered_at: Time? }
45
+ | [K, V] () { (Symbol, field) -> [K, V] } -> Hash[K, V]
46
+ def deconstruct: () -> [String, String, String, Integer, String, json_value, DurableMessageSender, String, Time, Time?]
47
+ def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, field]
48
+ end
49
+
50
+ class DurableMessagePage < Data
51
+ attr_reader messages: Array[DurableMessage]
52
+ attr_reader has_more: bool
53
+
54
+ def self.new: (Enumerable[DurableMessage] messages, bool has_more) -> DurableMessagePage
55
+ | (messages: Enumerable[DurableMessage], has_more: bool) -> DurableMessagePage
56
+ def self.[]: (Enumerable[DurableMessage] messages, bool has_more) -> DurableMessagePage
57
+ | (messages: Enumerable[DurableMessage], has_more: bool) -> DurableMessagePage
58
+ def self.members: () -> [:messages, :has_more]
59
+
60
+ def initialize: (messages: Enumerable[DurableMessage], has_more: bool) -> void
61
+ def members: () -> [:messages, :has_more]
62
+ def with: (?messages: Enumerable[DurableMessage], ?has_more: bool) -> DurableMessagePage
63
+ def to_h: () -> { messages: Array[DurableMessage], has_more: bool }
64
+ | [K, V] () { (Symbol, Array[DurableMessage] | bool) -> [K, V] } -> Hash[K, V]
65
+ def deconstruct: () -> [Array[DurableMessage], bool]
66
+ def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, Array[DurableMessage] | bool]
67
+ end
68
+ end
@@ -29,21 +29,29 @@ module Volcano
29
29
  attr_reader result_expired: bool?
30
30
  attr_reader error: DurableExecutionError?
31
31
  attr_reader completed_at: Time?
32
+ attr_reader client_token: String?
32
33
 
33
- def self.new: (String id, String function_id, String name, String status, String region, Time created_at, ?json_value result, ?bool? result_expired, ?DurableExecutionError? error, ?Time? completed_at) -> DurableExecution
34
- | (id: String, function_id: String, name: String, status: String, region: String, created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?) -> DurableExecution
35
- def self.[]: (String id, String function_id, String name, String status, String region, Time created_at, ?json_value result, ?bool? result_expired, ?DurableExecutionError? error, ?Time? completed_at) -> DurableExecution
36
- | (id: String, function_id: String, name: String, status: String, region: String, created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?) -> DurableExecution
37
- def self.members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at]
34
+ def self.new: (String id, String function_id, String name, String status, String region, Time created_at, ?json_value result, ?bool? result_expired, ?DurableExecutionError? error, ?Time? completed_at, ?String? client_token) -> DurableExecution
35
+ | (id: String, function_id: String, name: String, status: String, region: String, created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?, ?client_token: String?) -> DurableExecution
36
+ def self.[]: (String id, String function_id, String name, String status, String region, Time created_at, ?json_value result, ?bool? result_expired, ?DurableExecutionError? error, ?Time? completed_at, ?String? client_token) -> DurableExecution
37
+ | (id: String, function_id: String, name: String, status: String, region: String, created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?, ?client_token: String?) -> DurableExecution
38
+ def self.members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at, :client_token]
38
39
 
39
40
  private def initialize: (**(json_value | Time | DurableExecutionError) attributes) -> void
40
- def members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at]
41
- def with: (?id: String, ?function_id: String, ?name: String, ?status: String, ?region: String, ?created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?) -> DurableExecution
42
- def to_h: () -> { id: String, function_id: String, name: String, status: String, region: String, created_at: Time, result: json_value, result_expired: bool?, error: DurableExecutionError?, completed_at: Time? }
41
+ def members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at, :client_token]
42
+ def with: (?id: String, ?function_id: String, ?name: String, ?status: String, ?region: String, ?created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?, ?client_token: String?) -> DurableExecution
43
+ def to_h: () -> { id: String, function_id: String, name: String, status: String, region: String, created_at: Time, result: json_value, result_expired: bool?, error: DurableExecutionError?, completed_at: Time?, client_token: String? }
43
44
  | [K, V] () { (Symbol, String | Time | json_value | bool | DurableExecutionError | nil) -> [K, V] } -> Hash[K, V]
44
- def deconstruct: () -> [String, String, String, String, String, Time, json_value, bool?, DurableExecutionError?, Time?]
45
+ def deconstruct: () -> [String, String, String, String, String, Time, json_value, bool?, DurableExecutionError?, Time?, String?]
45
46
  def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, String | Time | json_value | bool | DurableExecutionError | nil]
46
47
  def terminal?: () -> bool
48
+ def inspect: () -> String
49
+ def to_s: () -> String
50
+ def pretty_print: (_PrettyPrinter printer) -> void
51
+
52
+ interface _PrettyPrinter
53
+ def text: (String text) -> void
54
+ end
47
55
  end
48
56
 
49
57
  class DurableExecutionPage < Data