volcano-sdk 0.34.0 → 0.35.1
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/README.md +64 -8
- data/lib/volcano/client.rb +1 -1
- data/lib/volcano/durable.rb +11 -6
- data/lib/volcano/durable_approval_models.rb +1 -1
- data/lib/volcano/durable_message_arguments.rb +49 -0
- data/lib/volcano/durable_message_backlog.rb +38 -0
- data/lib/volcano/durable_message_catch_ups.rb +112 -0
- data/lib/volcano/durable_message_models.rb +62 -0
- data/lib/volcano/durable_message_responses.rb +107 -0
- data/lib/volcano/durable_message_session.rb +72 -0
- data/lib/volcano/durable_message_stream.rb +176 -0
- data/lib/volcano/durable_messages.rb +217 -0
- data/lib/volcano/durable_models.rb +32 -3
- data/lib/volcano/generated/lib/volcano-generated/api/auth_admin_api.rb +2 -2
- data/lib/volcano/generated/lib/volcano-generated/api/auth_configuration_api.rb +2 -2
- data/lib/volcano/generated/lib/volcano-generated/api/autofix_api.rb +222 -0
- data/lib/volcano/generated/lib/volcano-generated/api/durable_functions_api.rb +725 -0
- data/lib/volcano/generated/lib/volcano-generated/api/frontends_api.rb +2 -2
- data/lib/volcano/generated/lib/volcano-generated/api/issues_api.rb +254 -0
- data/lib/volcano/generated/lib/volcano-generated/api/sandboxes_api.rb +20 -20
- data/lib/volcano/generated/lib/volcano-generated/api/system_api.rb +57 -0
- data/lib/volcano/generated/lib/volcano-generated/api/variable_environments_api.rb +373 -0
- data/lib/volcano/generated/lib/volcano-generated/api/verified_domains_api.rb +2 -2
- data/lib/volcano/generated/lib/volcano-generated/models/auth_config.rb +46 -4
- data/lib/volcano/generated/lib/volcano-generated/models/auth_insights_series_point.rb +110 -2
- data/lib/volcano/generated/lib/volcano-generated/models/autofix_model.rb +45 -0
- data/lib/volcano/generated/lib/volcano-generated/models/autofix_provider.rb +40 -0
- data/lib/volcano/generated/lib/volcano-generated/models/autofix_settings.rb +385 -0
- data/lib/volcano/generated/lib/volcano-generated/models/autofix_stage.rb +40 -0
- data/lib/volcano/generated/lib/volcano-generated/models/capability.rb +214 -0
- data/lib/volcano/generated/lib/volcano-generated/models/capability_list.rb +166 -0
- data/lib/volcano/generated/lib/volcano-generated/models/capability_status.rb +40 -0
- data/lib/volcano/generated/lib/volcano-generated/models/create_sandbox_session_request.rb +16 -53
- data/lib/volcano/generated/lib/volcano-generated/models/create_variable_environment_request.rb +176 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_execution.rb +14 -4
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message.rb +396 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_direction.rb +40 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_list.rb +194 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_sender.rb +203 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_session_registration.rb +165 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_status.rb +43 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait.rb +278 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_list.rb +167 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_registration.rb +225 -0
- data/lib/volcano/generated/lib/volcano-generated/models/durable_message_wait_status.rb +43 -0
- data/lib/volcano/generated/lib/volcano-generated/models/frontend.rb +11 -1
- data/lib/volcano/generated/lib/volcano-generated/models/frontend_custom_domain_conflict_error.rb +1 -1
- data/lib/volcano/generated/lib/volcano-generated/models/frontend_custom_domain_response.rb +25 -2
- data/lib/volcano/generated/lib/volcano-generated/models/issue.rb +493 -0
- data/lib/volcano/generated/lib/volcano-generated/models/issue_status.rb +41 -0
- data/lib/volcano/generated/lib/volcano-generated/models/metric_usage_data.rb +1 -1
- data/lib/volcano/generated/lib/volcano-generated/models/open_durable_message_session_request.rb +228 -0
- data/lib/volcano/generated/lib/volcano-generated/models/paginated_issues.rb +230 -0
- data/lib/volcano/generated/lib/volcano-generated/models/project_config.rb +10 -1
- data/lib/volcano/generated/lib/volcano-generated/models/project_config_autofix.rb +206 -0
- data/lib/volcano/generated/lib/volcano-generated/models/project_config_custom_domain.rb +1 -1
- data/lib/volcano/generated/lib/volcano-generated/models/project_config_sandbox.rb +12 -50
- data/lib/volcano/generated/lib/volcano-generated/models/project_frontend_custom_domain.rb +25 -2
- data/lib/volcano/generated/lib/volcano-generated/models/publish_durable_message_request.rb +205 -0
- data/lib/volcano/generated/lib/volcano-generated/models/register_durable_message_wait_request.rb +284 -0
- data/lib/volcano/generated/lib/volcano-generated/models/rename_variable_environment_request.rb +176 -0
- data/lib/volcano/generated/lib/volcano-generated/models/request_durable_approval_request.rb +1 -1
- data/lib/volcano/generated/lib/volcano-generated/models/sandbox_command_request.rb +11 -12
- data/lib/volcano/generated/lib/volcano-generated/models/sandbox_execution_request.rb +51 -13
- data/lib/volcano/generated/lib/volcano-generated/models/sandbox_session.rb +4 -30
- data/lib/volcano/generated/lib/volcano-generated/models/send_durable_message_request.rb +205 -0
- data/lib/volcano/generated/lib/volcano-generated/models/update_auth_config_request.rb +79 -1
- data/lib/volcano/generated/lib/volcano-generated/models/update_autofix_settings_request.rb +273 -0
- data/lib/volcano/generated/lib/volcano-generated/models/update_issue_request.rb +186 -0
- data/lib/volcano/generated/lib/volcano-generated/models/variable_environment.rb +325 -0
- data/lib/volcano/generated/lib/volcano-generated/models/variable_environment_list.rb +175 -0
- data/lib/volcano/generated/lib/volcano-generated.rb +34 -0
- data/lib/volcano/generated_transport.rb +1 -0
- data/lib/volcano/generated_transport_durable_messages.rb +57 -0
- data/lib/volcano/realtime/channel_callbacks.rb +5 -1
- data/lib/volcano/realtime/channel_lifecycle.rb +4 -1
- data/lib/volcano/realtime/durable_channel.rb +47 -0
- data/lib/volcano/realtime/lifecycle.rb +37 -1
- data/lib/volcano/realtime/protocol.rb +7 -3
- data/lib/volcano/realtime/protocol_dispatch.rb +1 -1
- data/lib/volcano/realtime/protocol_io.rb +7 -4
- data/lib/volcano/realtime/protocol_lifecycle.rb +7 -0
- data/lib/volcano/realtime/protocol_recovery.rb +23 -12
- data/lib/volcano/realtime.rb +42 -8
- data/lib/volcano/redaction.rb +1 -1
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +8 -0
- data/sig/core_facades.rbs +15 -0
- data/sig/durable_message_records.rbs +68 -0
- data/sig/durable_records.rbs +17 -9
- metadata +68 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Delivers the messages a durable execution publishes in `seq` order, once
|
|
5
|
+
# each, from its realtime channel and from the messages list.
|
|
6
|
+
#
|
|
7
|
+
# A live message can only be delivered once everything before it has been
|
|
8
|
+
# listed, or the missed ones would fall below the last delivered `seq` and be
|
|
9
|
+
# skipped. So live messages are held until the first catch-up starts, while
|
|
10
|
+
# any catch-up is reading, and while any held message still follows a gap.
|
|
11
|
+
# A held message is released only once the message its broadcast names as
|
|
12
|
+
# the one before it has been delivered.
|
|
13
|
+
#
|
|
14
|
+
# After the first catch-up, every message is delivered from the stream's own
|
|
15
|
+
# task, one at a time. A live one is held and released there too, since a
|
|
16
|
+
# reconnect stops the connection's tasks midway and would leave the block's
|
|
17
|
+
# work on it unfinished.
|
|
18
|
+
#
|
|
19
|
+
# The channel itself drops publications while it is not subscribed, and
|
|
20
|
+
# reports each resubscription, which starts a catch-up of what it dropped.
|
|
21
|
+
# A broadcast naming a publish not yet delivered starts one too, since the
|
|
22
|
+
# broadcast before it was lost or has yet to arrive, and so does a
|
|
23
|
+
# publication the connection had no room for or that does not read as a
|
|
24
|
+
# message.
|
|
25
|
+
class DurableMessageStream
|
|
26
|
+
include DurableMessageResponses
|
|
27
|
+
|
|
28
|
+
Held = Data.define(:message, :prev_seq)
|
|
29
|
+
ENDED = 'the durable message subscription has ended'
|
|
30
|
+
private_constant :Held, :ENDED
|
|
31
|
+
|
|
32
|
+
# +list_after+ reads the page of published messages after a `seq`, and
|
|
33
|
+
# +on_failure+ hears about a catch-up that failed in the background or a
|
|
34
|
+
# handler that raised after the first catch-up.
|
|
35
|
+
def initialize(handler, list_after, on_failure)
|
|
36
|
+
require 'async/semaphore'
|
|
37
|
+
@handler = handler
|
|
38
|
+
@list_after = list_after
|
|
39
|
+
@on_failure = on_failure
|
|
40
|
+
@last_seq = @catch_ups = 0
|
|
41
|
+
@started = @closed = false
|
|
42
|
+
@held = DurableMessageBacklog.new
|
|
43
|
+
@reads = Async::Semaphore.new(1)
|
|
44
|
+
@background = DurableMessageCatchUps.new(
|
|
45
|
+
-> { read_missed }, -> { release_live }, -> { catch_up_in_background }, on_failure
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def live(data)
|
|
50
|
+
held = published(data) unless @closed
|
|
51
|
+
return unless held
|
|
52
|
+
|
|
53
|
+
waiting = behind?
|
|
54
|
+
@held.hold(held)
|
|
55
|
+
return if waiting
|
|
56
|
+
return catch_up_in_background if held.prev_seq > @last_seq
|
|
57
|
+
|
|
58
|
+
@background.release
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reads what was missed, then releases the held messages. Raises the
|
|
62
|
+
# list's or the handler's error, closing the stream before anything held
|
|
63
|
+
# is delivered, and raises Realtime::ClosedError if the stream was closed
|
|
64
|
+
# meanwhile. Call it from the task that subscribes, which the catch-ups
|
|
65
|
+
# the stream starts itself run under. Those start once it has read, so
|
|
66
|
+
# one asked for while subscribing cannot call the handler before the
|
|
67
|
+
# read whose errors the subscriber hears.
|
|
68
|
+
def catch_up
|
|
69
|
+
begin_catch_up
|
|
70
|
+
read_missed(first: true)
|
|
71
|
+
@background.start unless @closed
|
|
72
|
+
raise Realtime::ClosedError, ENDED if @closed
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reads back what the channel lost, as when it resubscribes or had no room
|
|
76
|
+
# for a publication. One catch-up waiting its turn reads whatever more are
|
|
77
|
+
# asked for meanwhile, so a burst of dropped publications reads once.
|
|
78
|
+
def catch_up_in_background
|
|
79
|
+
return if @background.pending?
|
|
80
|
+
|
|
81
|
+
begin_catch_up
|
|
82
|
+
@background.request
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Not stopped: the handler a catch-up is running may be what closed it.
|
|
86
|
+
def close
|
|
87
|
+
@closed = true
|
|
88
|
+
@held.clear
|
|
89
|
+
@background.close
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def behind? = !@started || @catch_ups.positive? || !@held.empty?
|
|
95
|
+
|
|
96
|
+
# Delivers the held live messages that follow on from the last one
|
|
97
|
+
# delivered, until a catch-up is asked for. One left behind a gap is read
|
|
98
|
+
# back.
|
|
99
|
+
def release_live
|
|
100
|
+
while @catch_ups.zero? && (held = @held.take_following(@last_seq))
|
|
101
|
+
deliver(held.message, report: true)
|
|
102
|
+
end
|
|
103
|
+
catch_up_in_background unless @held.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Counted before the read waits its turn, so nothing is delivered live
|
|
107
|
+
# meanwhile. A catch-up replaces the retry that was waiting.
|
|
108
|
+
def begin_catch_up
|
|
109
|
+
@started = true
|
|
110
|
+
@background.cancel_retry
|
|
111
|
+
@catch_ups += 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_missed(first: false)
|
|
115
|
+
read = false
|
|
116
|
+
@reads.acquire do
|
|
117
|
+
read_pages(report: !first)
|
|
118
|
+
read = true
|
|
119
|
+
ensure
|
|
120
|
+
close if first && !read
|
|
121
|
+
# Released by the last catch-up only, and while still counted, so a
|
|
122
|
+
# live message that arrives meanwhile is held behind the released ones.
|
|
123
|
+
release(read, report: !first) if @catch_ups == 1
|
|
124
|
+
end
|
|
125
|
+
ensure
|
|
126
|
+
@catch_ups -= 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def read_pages(report:)
|
|
130
|
+
until @closed
|
|
131
|
+
after = @last_seq
|
|
132
|
+
page = @list_after.call(after)
|
|
133
|
+
page.messages.each { |message| deliver(message, report:) }
|
|
134
|
+
# A page that delivered nothing would be asked for again unchanged.
|
|
135
|
+
return if !page.has_more || @last_seq == after
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Delivers the held messages that follow on from the last one delivered,
|
|
140
|
+
# and retries the read while any is left behind a gap or the read failed.
|
|
141
|
+
def release(read, report:)
|
|
142
|
+
while (held = @held.take_following(@last_seq))
|
|
143
|
+
deliver(held.message, report:)
|
|
144
|
+
end
|
|
145
|
+
ensure
|
|
146
|
+
read && @held.empty? ? @background.caught_up : @background.retry_later
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def deliver(message, report:)
|
|
150
|
+
call_handler(message, report) unless @closed || message.seq <= @last_seq
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Only the first catch-up raises what the handler raised, to the caller
|
|
154
|
+
# that subscribes. Later the stream's own task would lose it, so it is
|
|
155
|
+
# reported and delivery goes on.
|
|
156
|
+
def call_handler(message, report)
|
|
157
|
+
@last_seq = message.seq
|
|
158
|
+
@handler.call(message)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
raise unless report
|
|
161
|
+
|
|
162
|
+
@on_failure.call(e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# A publication that does not read as a message is still one the
|
|
166
|
+
# execution published, so the list is read for it as for a dropped one.
|
|
167
|
+
def published(data)
|
|
168
|
+
message = durable_message(data)
|
|
169
|
+
Held.new(message:, prev_seq: broadcast_prev_seq(data)) if message.direction == 'out'
|
|
170
|
+
rescue TypeError
|
|
171
|
+
catch_up_in_background
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
private_constant :DurableMessageStream
|
|
176
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Messages an execution publishes, delivered to the block #subscribe took
|
|
5
|
+
# until #unsubscribe.
|
|
6
|
+
class DurableMessageSubscription
|
|
7
|
+
# Subscribes +channel+ and delivers what was published so far. Leaves the
|
|
8
|
+
# channel if that fails, or the task running it is stopped. +session+ can
|
|
9
|
+
# end the subscription from the first read on.
|
|
10
|
+
def self.open(session, stream, channel, realtime)
|
|
11
|
+
subscription = session.end_with(new(stream, channel, realtime))
|
|
12
|
+
finished = false
|
|
13
|
+
begin
|
|
14
|
+
channel.subscribe
|
|
15
|
+
stream.catch_up
|
|
16
|
+
finished = true
|
|
17
|
+
ensure
|
|
18
|
+
subscription.unsubscribe unless finished
|
|
19
|
+
end
|
|
20
|
+
subscription
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(stream, channel, realtime)
|
|
24
|
+
@stream = stream
|
|
25
|
+
@channel = channel
|
|
26
|
+
@realtime = realtime
|
|
27
|
+
channel.on('message') { |data| stream.live(data) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Stops delivering messages and leaves the execution's channel. Safe to
|
|
31
|
+
# repeat, and to call from the block.
|
|
32
|
+
def unsubscribe
|
|
33
|
+
@stream.close
|
|
34
|
+
@realtime.__send__(:remove_durable_channel, @channel)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The channel holds the client token and the realtime client the session,
|
|
38
|
+
# so inspection shows neither.
|
|
39
|
+
def inspect = '#<Volcano::DurableMessageSubscription>'
|
|
40
|
+
alias to_s inspect
|
|
41
|
+
|
|
42
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Sends to, lists, and follows the messages of durable executions.
|
|
46
|
+
#
|
|
47
|
+
# Each method addresses an execution by its function's name or id and the
|
|
48
|
+
# execution's id. By default they use the application route with the
|
|
49
|
+
# client's credential, as Durable#start does; pass +client_token:+ where that
|
|
50
|
+
# credential needs it, as an anon key or a user who did not start the
|
|
51
|
+
# execution does. Pass +project_id:+ to use the owner's project route with
|
|
52
|
+
# the session's platform token instead.
|
|
53
|
+
class DurableMessages
|
|
54
|
+
include DurableArguments
|
|
55
|
+
include DurableMessageArguments
|
|
56
|
+
include DurableMessageResponses
|
|
57
|
+
|
|
58
|
+
Target = Data.define(:project_id, :function_id, :execution_id, :client_token)
|
|
59
|
+
|
|
60
|
+
# Reopens the generated record to mask its credential.
|
|
61
|
+
class Target
|
|
62
|
+
# @dynamic project_id, function_id, execution_id, client_token
|
|
63
|
+
|
|
64
|
+
# The client token is a bearer credential, so inspection masks it, as
|
|
65
|
+
# DurableExecution#inspect does.
|
|
66
|
+
def inspect
|
|
67
|
+
fields = to_h.map do |name, value|
|
|
68
|
+
"#{name}=#{name == :client_token && value ? Redaction::MARKER : value.inspect}"
|
|
69
|
+
end
|
|
70
|
+
"#<data #{self.class.name} #{fields.join(', ')}>"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s = inspect
|
|
74
|
+
|
|
75
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
76
|
+
end
|
|
77
|
+
private_constant :Target
|
|
78
|
+
|
|
79
|
+
SEND_OPTIONS = %i[idempotency_key client_token project_id].freeze
|
|
80
|
+
LIST_OPTIONS = %i[direction name after limit client_token project_id].freeze
|
|
81
|
+
private_constant :SEND_OPTIONS, :LIST_OPTIONS
|
|
82
|
+
|
|
83
|
+
def initialize(client, transport, realtime)
|
|
84
|
+
@client = client
|
|
85
|
+
@transport = transport
|
|
86
|
+
@realtime = realtime
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Sends a message to a running execution, queued until a wait for its
|
|
90
|
+
# +name+ takes it. +data+ is any JSON value. The returned message's
|
|
91
|
+
# +status+ is +delivered+ when a wait took it at once, +delivering+ while
|
|
92
|
+
# that handover is still being confirmed, and +queued+ otherwise. Sending
|
|
93
|
+
# again with an +idempotency_key:+ the same sender has used on the
|
|
94
|
+
# execution returns the message the first send stored, in its current
|
|
95
|
+
# status, and sends nothing. Sending to an execution that has ended raises
|
|
96
|
+
# Error::ConflictError, whose +code+ is +execution_not_running+, and to one
|
|
97
|
+
# holding 100 messages no wait has taken, +too_many_queued_messages+.
|
|
98
|
+
def send_message(function_name, execution_id, name, data = nil, **options)
|
|
99
|
+
target = message_target(function_name, execution_id, known_options(options, SEND_OPTIONS))
|
|
100
|
+
name = message_name(name)
|
|
101
|
+
key = bounded_text(options[:idempotency_key], 'idempotency_key', MAX_IDEMPOTENCY_KEY_LENGTH)
|
|
102
|
+
response = Transport.invoke do
|
|
103
|
+
@transport.send_durable_message(
|
|
104
|
+
authorization: authorization(target), target: target, name: name,
|
|
105
|
+
data: ImmutableRequestValue.capture(data), idempotency_key: key
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
durable_message(Transport.body(response, response.status == 200 ? 200 : 201))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Lists an execution's messages in both directions, oldest first, filtered
|
|
112
|
+
# by any of +direction+ (+in+ or +out+), +name+, +after+ (a +seq+), and
|
|
113
|
+
# +limit+ (1 to 500), given as keywords with +client_token+ and
|
|
114
|
+
# +project_id+. Read the next page with +after+ set to the last message's
|
|
115
|
+
# +seq+.
|
|
116
|
+
def list(function_name, execution_id, filters = {})
|
|
117
|
+
filters = list_filters(filters)
|
|
118
|
+
target = message_target(function_name, execution_id, filters)
|
|
119
|
+
durable_message_page(Transport.body(list_messages(target, list_query(filters)), 200))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Delivers every message the execution publishes to the block, in order,
|
|
123
|
+
# once each, and one call at a time. Messages it published before the
|
|
124
|
+
# call come first, and after a reconnect the ones published meanwhile are
|
|
125
|
+
# read back before live delivery resumes, over the owner's route when
|
|
126
|
+
# +project_id+ is given. Returns a
|
|
127
|
+
# DurableMessageSubscription once the messages published so far are
|
|
128
|
+
# delivered; a block that raises meanwhile ends the subscription and
|
|
129
|
+
# raises here, and a subscription ended meanwhile raises
|
|
130
|
+
# Realtime::ClosedError.
|
|
131
|
+
#
|
|
132
|
+
# Follows the execution on the client's realtime connection, so call it
|
|
133
|
+
# inside that connection's Async reactor, with a signed-in session. A
|
|
134
|
+
# connection an earlier session opened is closed, raising
|
|
135
|
+
# Error::SessionChangedError. A catch-up that fails in the background is
|
|
136
|
+
# reported to Realtime#on_error and retried, backing off to every 30
|
|
137
|
+
# seconds; a block that raises later is reported there too, and delivery
|
|
138
|
+
# goes on. Realtime#remove_all_channels and Realtime#disconnect end the
|
|
139
|
+
# subscription, and so does signing out, signing in again, or setting
|
|
140
|
+
# another session, though not a token refresh, and a reconnect the server
|
|
141
|
+
# refuses to restore the subscription on for good, which is reported to
|
|
142
|
+
# Realtime#on_error once. Raises
|
|
143
|
+
# Realtime::DuplicateSubscriptionError when this client already follows the
|
|
144
|
+
# execution.
|
|
145
|
+
def subscribe(function_name, execution_id, client_token: nil, project_id: nil, &handler)
|
|
146
|
+
raise ArgumentError, 'subscribe requires a block' unless block_given?
|
|
147
|
+
|
|
148
|
+
target = message_target(function_name, execution_id, { client_token: client_token, project_id: project_id })
|
|
149
|
+
session = DurableMessageSession.new(@realtime)
|
|
150
|
+
stream = session.stream(handler, reader(target), failure_reporter)
|
|
151
|
+
DurableMessageSubscription.open(session, stream, durable_channel(target, session), @realtime)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def durable_channel(target, session)
|
|
157
|
+
@realtime.__send__(:durable_channel, target.execution_id, target.client_token, session) ||
|
|
158
|
+
raise(Realtime::DuplicateSubscriptionError, 'already subscribed to this execution; unsubscribe first')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Reads off the reactor, which the transport would block along with the
|
|
162
|
+
# connection's reads and heartbeats.
|
|
163
|
+
def reader(target)
|
|
164
|
+
lambda do |after|
|
|
165
|
+
authorization = authorization(target)
|
|
166
|
+
response = @realtime.__send__(:blocking) do
|
|
167
|
+
list_messages(target, { direction: 'out', after: after }, authorization)
|
|
168
|
+
end
|
|
169
|
+
durable_message_page(Transport.body(response, 200))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def list_messages(target, query, authorization = authorization(target))
|
|
174
|
+
Transport.invoke do
|
|
175
|
+
@transport.list_durable_messages(authorization: authorization, target: target, query: query)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def failure_reporter
|
|
180
|
+
->(error) { @realtime.__send__(:report_channel_error, error) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def authorization(target)
|
|
184
|
+
target.project_id ? @client.session_token : @client.function_token
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def message_target(function_name, execution_id, options)
|
|
188
|
+
project_id = options[:project_id]
|
|
189
|
+
Target.new(
|
|
190
|
+
project_id: project_id.nil? ? nil : identifier(project_id, 'project_id'),
|
|
191
|
+
function_id: identifier(function_name, 'function_name'),
|
|
192
|
+
execution_id: identifier(execution_id, 'execution_id'),
|
|
193
|
+
client_token: bounded_text(options[:client_token], 'client_token', MAX_CLIENT_TOKEN_LENGTH)
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def list_filters(filters)
|
|
198
|
+
raise TypeError, 'filters must be a Hash' unless filters.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
known_options(filters, LIST_OPTIONS)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def list_query(filters)
|
|
204
|
+
name = filters[:name]
|
|
205
|
+
{
|
|
206
|
+
direction: direction_filter(filters[:direction]), name: name.nil? ? nil : message_name(name),
|
|
207
|
+
after: after_argument(filters[:after]), limit: limit_argument(filters[:limit])
|
|
208
|
+
}.compact
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def direction_filter(value)
|
|
212
|
+
return if value.nil?
|
|
213
|
+
|
|
214
|
+
DIRECTIONS.find { |direction| direction == value.to_s } || raise(ArgumentError, "direction must be 'in' or 'out'")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -5,7 +5,7 @@ require_relative 'immutable_request_value'
|
|
|
5
5
|
|
|
6
6
|
module Volcano
|
|
7
7
|
DURABLE_EXECUTION_ATTRIBUTES = %i[
|
|
8
|
-
id function_id name status region created_at result result_expired error completed_at
|
|
8
|
+
id function_id name status region created_at result result_expired error completed_at client_token
|
|
9
9
|
].freeze
|
|
10
10
|
private_constant :DURABLE_EXECUTION_ATTRIBUTES
|
|
11
11
|
|
|
@@ -35,6 +35,10 @@ module Volcano
|
|
|
35
35
|
# once its retention lapses. That is not the same as a function that returned
|
|
36
36
|
# nothing, so read `result_expired` before concluding anything from a missing
|
|
37
37
|
# result.
|
|
38
|
+
#
|
|
39
|
+
# `client_token` lets a browser, or a user who did not start the execution,
|
|
40
|
+
# reach its messages. Only the start that created the execution returns it;
|
|
41
|
+
# a repeat under the same name and every read leave it nil.
|
|
38
42
|
DurableExecution = Data.define(*DURABLE_EXECUTION_ATTRIBUTES)
|
|
39
43
|
|
|
40
44
|
# Reopens the generated record for checked initialization.
|
|
@@ -58,6 +62,17 @@ module Volcano
|
|
|
58
62
|
DURABLE_TERMINAL_STATUSES.include?(status)
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
# The client token is a bearer credential, so inspection masks it.
|
|
66
|
+
def inspect
|
|
67
|
+
fields = to_h.map do |name, value|
|
|
68
|
+
"#{name}=#{name == :client_token && value ? Redaction::MARKER : value.inspect}"
|
|
69
|
+
end
|
|
70
|
+
"#<data #{self.class.name} #{fields.join(', ')}>"
|
|
71
|
+
end
|
|
72
|
+
alias to_s inspect
|
|
73
|
+
|
|
74
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
75
|
+
|
|
61
76
|
private
|
|
62
77
|
|
|
63
78
|
# `result` is the function's own JSON, so it is frozen all the way down.
|
|
@@ -92,15 +107,29 @@ module Volcano
|
|
|
92
107
|
|
|
93
108
|
def durable_execution(payload)
|
|
94
109
|
values = execution_payload(payload)
|
|
110
|
+
result, result_expired, error, completed_at = execution_outcome(values)
|
|
95
111
|
DurableExecution.new(
|
|
96
112
|
id: execution_text(values, 'id'), function_id: execution_text(values, 'function_id'),
|
|
97
113
|
name: execution_text(values, 'name'), status: execution_text(values, 'status'),
|
|
98
114
|
region: execution_text(values, 'region'), created_at: execution_time(values),
|
|
99
|
-
result:
|
|
100
|
-
|
|
115
|
+
result: result, result_expired: result_expired, error: error, completed_at: completed_at,
|
|
116
|
+
client_token: client_token(values['client_token'])
|
|
101
117
|
)
|
|
102
118
|
end
|
|
103
119
|
|
|
120
|
+
def execution_outcome(values)
|
|
121
|
+
[
|
|
122
|
+
execution_result(values['result']), execution_expired(values['result_expired']),
|
|
123
|
+
execution_error(values['error']), parse_time(values['completed_at'])
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def client_token(value)
|
|
128
|
+
return value if value.nil? || (value.is_a?(String) && !value.empty?)
|
|
129
|
+
|
|
130
|
+
raise TypeError, INCOMPLETE_EXECUTION
|
|
131
|
+
end
|
|
132
|
+
|
|
104
133
|
def execution_payload(payload)
|
|
105
134
|
raise TypeError, INCOMPLETE_EXECUTION unless payload.is_a?(Hash)
|
|
106
135
|
|
|
@@ -303,7 +303,7 @@ module Volcano::Generated
|
|
|
303
303
|
end
|
|
304
304
|
|
|
305
305
|
# Get auth user insights
|
|
306
|
-
# Returns current and deleted auth-user totals, rolling 24-hour and 30-day active users, and zero-filled
|
|
306
|
+
# Returns current and deleted auth-user totals, rolling 24-hour and 30-day active users, and zero-filled daily activity and account-retention counts for an inclusive UTC date range. Weeks start on Monday. The existing deletions series counts accounts by deletion date; net_growth remains signups minus deletions and is not affected by bans. The banned_users, deleted_users, and available_users fields describe the current state of users registered in each bucket. Banned users exclude deleted users and expired bans. Available users are signups minus banned_users minus deleted_users. Unban and expiry restore availability in the original signup bucket. These current-state cohorts can change after the requested dates have passed, without rewriting activity counts. Sign-in counts remain grouped by session creation date. Deleted users contribute to active counts until their last activity leaves the rolling window. Token refreshes affect active users but not the sign-in series. Signup history is backfilled from accounts present when collection was deployed. Accounts hard-deleted before deletion-history collection cannot be recovered. Current bans require no separate event-history collection.
|
|
307
307
|
# @param id [String] Project ID
|
|
308
308
|
# @param [Hash] opts the optional parameters
|
|
309
309
|
# @option opts [Date] :from Inclusive UTC start date. Defaults to 29 days before `to`.
|
|
@@ -316,7 +316,7 @@ module Volcano::Generated
|
|
|
316
316
|
end
|
|
317
317
|
|
|
318
318
|
# Get auth user insights
|
|
319
|
-
# Returns current and deleted auth-user totals, rolling 24-hour and 30-day active users, and zero-filled
|
|
319
|
+
# Returns current and deleted auth-user totals, rolling 24-hour and 30-day active users, and zero-filled daily activity and account-retention counts for an inclusive UTC date range. Weeks start on Monday. The existing deletions series counts accounts by deletion date; net_growth remains signups minus deletions and is not affected by bans. The banned_users, deleted_users, and available_users fields describe the current state of users registered in each bucket. Banned users exclude deleted users and expired bans. Available users are signups minus banned_users minus deleted_users. Unban and expiry restore availability in the original signup bucket. These current-state cohorts can change after the requested dates have passed, without rewriting activity counts. Sign-in counts remain grouped by session creation date. Deleted users contribute to active counts until their last activity leaves the rolling window. Token refreshes affect active users but not the sign-in series. Signup history is backfilled from accounts present when collection was deployed. Accounts hard-deleted before deletion-history collection cannot be recovered. Current bans require no separate event-history collection.
|
|
320
320
|
# @param id [String] Project ID
|
|
321
321
|
# @param [Hash] opts the optional parameters
|
|
322
322
|
# @option opts [Date] :from Inclusive UTC start date. Defaults to 29 days before `to`.
|
|
@@ -1337,7 +1337,7 @@ module Volcano::Generated
|
|
|
1337
1337
|
end
|
|
1338
1338
|
|
|
1339
1339
|
# Send a test email using the project's saved SMTP config
|
|
1340
|
-
# Sends a diagnostic email to `to_email` using the project's persisted `auth_config` SMTP credentials. If `html_body` or `text_body` is supplied, the override path is taken: those values (plus optional `subject`) are rendered through html/text templates against the project's `Data` and used as the body — used by the template editor's \"Send Test\" affordance to preview an unsaved template. With both bodies omitted, a hardcoded diagnostic message is sent and any `subject` field is ignored. Sending `subject` alone (no bodies) is rejected with 400 to avoid a silently-dropped subject or a blank message. Also rejects with 400 if `email_enabled=false` or `
|
|
1340
|
+
# Sends a diagnostic email to `to_email` using the project's persisted `auth_config` SMTP credentials. If `html_body` or `text_body` is supplied, the override path is taken: those values (plus optional `subject`) are rendered through html/text templates against the project's `Data` and used as the body — used by the template editor's \"Send Test\" affordance to preview an unsaved template. With both bodies omitted, a hardcoded diagnostic message is sent and any `subject` field is ignored. Sending `subject` alone (no bodies) is rejected with 400 to avoid a silently-dropped subject or a blank message. Also rejects with 400 if `email_enabled=false`, `smtp_host` is empty, or the saved `smtp_password` can't be read and must be set again.
|
|
1341
1341
|
# @param id [String] Project ID
|
|
1342
1342
|
# @param test_email_request [TestEmailRequest]
|
|
1343
1343
|
# @param [Hash] opts the optional parameters
|
|
@@ -1348,7 +1348,7 @@ module Volcano::Generated
|
|
|
1348
1348
|
end
|
|
1349
1349
|
|
|
1350
1350
|
# Send a test email using the project's saved SMTP config
|
|
1351
|
-
# Sends a diagnostic email to `to_email` using the project's persisted `auth_config` SMTP credentials. If `html_body` or `text_body` is supplied, the override path is taken: those values (plus optional `subject`) are rendered through html/text templates against the project's `Data` and used as the body — used by the template editor's \"Send Test\" affordance to preview an unsaved template. With both bodies omitted, a hardcoded diagnostic message is sent and any `subject` field is ignored. Sending `subject` alone (no bodies) is rejected with 400 to avoid a silently-dropped subject or a blank message. Also rejects with 400 if `email_enabled=false` or `
|
|
1351
|
+
# Sends a diagnostic email to `to_email` using the project's persisted `auth_config` SMTP credentials. If `html_body` or `text_body` is supplied, the override path is taken: those values (plus optional `subject`) are rendered through html/text templates against the project's `Data` and used as the body — used by the template editor's \"Send Test\" affordance to preview an unsaved template. With both bodies omitted, a hardcoded diagnostic message is sent and any `subject` field is ignored. Sending `subject` alone (no bodies) is rejected with 400 to avoid a silently-dropped subject or a blank message. Also rejects with 400 if `email_enabled=false`, `smtp_host` is empty, or the saved `smtp_password` can't be read and must be set again.
|
|
1352
1352
|
# @param id [String] Project ID
|
|
1353
1353
|
# @param test_email_request [TestEmailRequest]
|
|
1354
1354
|
# @param [Hash] opts the optional parameters
|