tungsten-runtime 0.1.2
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 +7 -0
- data/LICENSE +55 -0
- data/README.md +23 -0
- data/lib/tungsten/runtime/auth.rb +258 -0
- data/lib/tungsten/runtime/classify.rb +248 -0
- data/lib/tungsten/runtime/client_core.rb +1147 -0
- data/lib/tungsten/runtime/descriptors.rb +173 -0
- data/lib/tungsten/runtime/dispatcher.rb +83 -0
- data/lib/tungsten/runtime/envelope.rb +230 -0
- data/lib/tungsten/runtime/expr.rb +186 -0
- data/lib/tungsten/runtime/helpers.rb +185 -0
- data/lib/tungsten/runtime/idempotency.rb +110 -0
- data/lib/tungsten/runtime/json.rb +346 -0
- data/lib/tungsten/runtime/macros.rb +343 -0
- data/lib/tungsten/runtime/oauth.rb +329 -0
- data/lib/tungsten/runtime/options.rb +142 -0
- data/lib/tungsten/runtime/pages.rb +173 -0
- data/lib/tungsten/runtime/schema.rb +320 -0
- data/lib/tungsten/runtime/serialize.rb +353 -0
- data/lib/tungsten/runtime/sse.rb +148 -0
- data/lib/tungsten/runtime/stream.rb +500 -0
- data/lib/tungsten/runtime/transport.rb +386 -0
- data/lib/tungsten/runtime/typed.rb +107 -0
- data/lib/tungsten/runtime/util.rb +254 -0
- data/lib/tungsten/runtime/version.rb +9 -0
- data/lib/tungsten/runtime.rb +49 -0
- metadata +82 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
module Tungsten
|
|
5
|
+
module Runtime
|
|
6
|
+
# What the consuming helpers of a stream return: the result, or when
|
|
7
|
+
# the stream failed, what was gathered before the failure with the
|
|
8
|
+
# failure as `error` (an Err); `reconnects` of the last event delivered.
|
|
9
|
+
Folded = Data.define(:value, :error, :reconnects) do
|
|
10
|
+
def ok? = error.nil?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The server-sent events of an operation (`client.<resource>.<op>_stream`).
|
|
14
|
+
# The request is sent when the enumeration starts; an error ends it
|
|
15
|
+
# (`error` is then the Err). A dropped stream is reconnected with
|
|
16
|
+
# `Last-Event-ID` when the call is safe to repeat, and an event whose id
|
|
17
|
+
# was delivered is never delivered twice. `cancel` ends it for good,
|
|
18
|
+
# without an error, from any thread.
|
|
19
|
+
class EventStream
|
|
20
|
+
include Enumerable
|
|
21
|
+
|
|
22
|
+
# Like the transport deadline, this must cross the call pipeline's
|
|
23
|
+
# StandardError handlers before collect turns it into a diagnostic.
|
|
24
|
+
class CollectionDeadline < Exception; end # rubocop:disable Lint/InheritException
|
|
25
|
+
|
|
26
|
+
# The connection being read and where the stream is.
|
|
27
|
+
Open = Struct.new(:body, :parser, :decoder, :queue, :ended, :oversize, :meta, :delivered_count, :key, :key_header, :check,
|
|
28
|
+
:secrets, :timeout_ms, :repeatable, :reconnects, :last_id, :last_retry, :delivered, :replaying,
|
|
29
|
+
keyword_init: true)
|
|
30
|
+
|
|
31
|
+
attr_reader :error
|
|
32
|
+
|
|
33
|
+
def initialize(core, op, args, opts, decode)
|
|
34
|
+
@core = core
|
|
35
|
+
@op = op
|
|
36
|
+
@args = args
|
|
37
|
+
@opts = opts
|
|
38
|
+
@decode = decode
|
|
39
|
+
@state = :idle
|
|
40
|
+
@error = nil
|
|
41
|
+
@cancelled = false
|
|
42
|
+
@lock = Mutex.new
|
|
43
|
+
@open = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The operation's descriptor.
|
|
47
|
+
def operation = @op
|
|
48
|
+
|
|
49
|
+
# End the stream for good, without an error and without a reconnect.
|
|
50
|
+
def cancel
|
|
51
|
+
body = @lock.synchronize do
|
|
52
|
+
@cancelled = true
|
|
53
|
+
@open&.body
|
|
54
|
+
end
|
|
55
|
+
body&.close
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cancelled? = @lock.synchronize { @cancelled }
|
|
60
|
+
|
|
61
|
+
# Close the connection.
|
|
62
|
+
def close
|
|
63
|
+
@open&.body&.close
|
|
64
|
+
set_open(nil)
|
|
65
|
+
@state = :done
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Every event, in order; stops at the end, at a failure (`error`) or
|
|
70
|
+
# after `cancel`.
|
|
71
|
+
def each
|
|
72
|
+
return enum_for(:each) unless block_given?
|
|
73
|
+
|
|
74
|
+
while (event = next_event)
|
|
75
|
+
yield event
|
|
76
|
+
end
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Read the stream to its end, calling the handler named like each
|
|
81
|
+
# event (`"message"` for one without a name), else the one named `"*"`;
|
|
82
|
+
# `value` is the number of events delivered.
|
|
83
|
+
def on(handlers)
|
|
84
|
+
count = 0
|
|
85
|
+
reconnects = 0
|
|
86
|
+
handlers = handlers.transform_keys(&:to_s)
|
|
87
|
+
each do |event|
|
|
88
|
+
reconnects = event.reconnects
|
|
89
|
+
count += 1
|
|
90
|
+
(handlers[event.event] || handlers["*"])&.call(event)
|
|
91
|
+
end
|
|
92
|
+
Folded.new(value: count, error: @error, reconnects: reconnects)
|
|
93
|
+
ensure
|
|
94
|
+
close
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Read the stream to its end and fold its events into one value.
|
|
98
|
+
def reduce(initial)
|
|
99
|
+
acc = initial
|
|
100
|
+
reconnects = 0
|
|
101
|
+
each do |event|
|
|
102
|
+
reconnects = event.reconnects
|
|
103
|
+
acc = yield(acc, event)
|
|
104
|
+
end
|
|
105
|
+
Folded.new(value: acc, error: @error, reconnects: reconnects)
|
|
106
|
+
ensure
|
|
107
|
+
close
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The first event for which the block holds (any event without a
|
|
111
|
+
# block), then close the stream; `value` is nil when none did.
|
|
112
|
+
def first(*)
|
|
113
|
+
reconnects = 0
|
|
114
|
+
each do |event|
|
|
115
|
+
reconnects = event.reconnects
|
|
116
|
+
return Folded.new(value: event, error: nil, reconnects: reconnects) if !block_given? || yield(event)
|
|
117
|
+
end
|
|
118
|
+
Folded.new(value: nil, error: @error, reconnects: reconnects)
|
|
119
|
+
ensure
|
|
120
|
+
close
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Every event in order and the final failure, if any: [events, Err or
|
|
124
|
+
# nil]. The collection is bounded: it ends with UNEXPECTED_RESPONSE
|
|
125
|
+
# once the kept events hold more than ClientOptions `max_collect_bytes`
|
|
126
|
+
# of JSON or it ran longer than `max_collect_ms`.
|
|
127
|
+
def collect
|
|
128
|
+
return [[], @error] unless @core
|
|
129
|
+
|
|
130
|
+
events = []
|
|
131
|
+
size = 0
|
|
132
|
+
Timeout.timeout(@core.max_collect_ms / 1000.0, CollectionDeadline) do
|
|
133
|
+
each do |event|
|
|
134
|
+
size += Json.text(event.raw).bytesize
|
|
135
|
+
if size > @core.max_collect_bytes
|
|
136
|
+
return [events, @core.send(:over_budget, @op, true, events.length)]
|
|
137
|
+
end
|
|
138
|
+
events << event
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
[events, @error]
|
|
142
|
+
rescue CollectionDeadline
|
|
143
|
+
@error = @core.send(:over_budget, @op, false, events.length)
|
|
144
|
+
[events, @error]
|
|
145
|
+
ensure
|
|
146
|
+
close
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def set_open(open)
|
|
152
|
+
cancelled = @lock.synchronize do
|
|
153
|
+
@open = open
|
|
154
|
+
@cancelled
|
|
155
|
+
end
|
|
156
|
+
open.body.close if cancelled && open
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fail_with(err)
|
|
160
|
+
@error = err
|
|
161
|
+
close
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The next event, or nil at the end.
|
|
166
|
+
def next_event
|
|
167
|
+
loop do
|
|
168
|
+
case @state
|
|
169
|
+
when :done then return nil
|
|
170
|
+
when :idle
|
|
171
|
+
if cancelled?
|
|
172
|
+
@state = :done
|
|
173
|
+
return nil
|
|
174
|
+
end
|
|
175
|
+
open = @core.send(:open_stream, @op, @args, @opts, nil)
|
|
176
|
+
return fail_with(open) if open.is_a?(Err)
|
|
177
|
+
|
|
178
|
+
set_open(open)
|
|
179
|
+
@state = :open
|
|
180
|
+
if cancelled?
|
|
181
|
+
close
|
|
182
|
+
return nil
|
|
183
|
+
end
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
o = @open
|
|
187
|
+
if cancelled?
|
|
188
|
+
close
|
|
189
|
+
return nil
|
|
190
|
+
end
|
|
191
|
+
unless o.queue.empty?
|
|
192
|
+
ev = o.queue.shift
|
|
193
|
+
item = @core.send(:accept_event, @op, o, ev, @decode)
|
|
194
|
+
return fail_with(item) if item.is_a?(Err)
|
|
195
|
+
if item == :done
|
|
196
|
+
close
|
|
197
|
+
return nil
|
|
198
|
+
end
|
|
199
|
+
next if item.nil?
|
|
200
|
+
|
|
201
|
+
return item
|
|
202
|
+
end
|
|
203
|
+
return fail_with(@core.send(:oversize, @op, o)) if o.oversize
|
|
204
|
+
|
|
205
|
+
why = nil
|
|
206
|
+
if o.ended
|
|
207
|
+
# A stream that names its terminal event has not finished without it.
|
|
208
|
+
if @op.stream["done"].nil?
|
|
209
|
+
close
|
|
210
|
+
return nil
|
|
211
|
+
end
|
|
212
|
+
why = :early_end
|
|
213
|
+
else
|
|
214
|
+
kind, chunk = o.body.read
|
|
215
|
+
if cancelled?
|
|
216
|
+
close
|
|
217
|
+
return nil
|
|
218
|
+
end
|
|
219
|
+
case kind
|
|
220
|
+
when :chunk
|
|
221
|
+
o.queue.concat(o.parser.push(o.decoder.decode(chunk)))
|
|
222
|
+
o.oversize = o.parser.exceeded?
|
|
223
|
+
next
|
|
224
|
+
when :end
|
|
225
|
+
o.queue.concat(o.parser.push(o.decoder.finish))
|
|
226
|
+
o.queue.concat(o.parser.finish)
|
|
227
|
+
o.oversize = o.parser.exceeded?
|
|
228
|
+
o.ended = true
|
|
229
|
+
next
|
|
230
|
+
when :timeout
|
|
231
|
+
return fail_with(@core.send(:interrupted, @op, o, nil)) unless @core.idle_timeout_ms
|
|
232
|
+
|
|
233
|
+
why = :idle
|
|
234
|
+
else
|
|
235
|
+
why = :lost
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
unless @core.send(:may_reconnect?, o)
|
|
239
|
+
# Without reconnects a clean end stays a clean end.
|
|
240
|
+
if why == :early_end && o.reconnects.zero?
|
|
241
|
+
close
|
|
242
|
+
return nil
|
|
243
|
+
end
|
|
244
|
+
return fail_with(@core.send(:interrupted, @op, o, why))
|
|
245
|
+
end
|
|
246
|
+
fresh = @core.send(:reconnect, @op, @args, @opts, o)
|
|
247
|
+
if cancelled?
|
|
248
|
+
fresh.body.close if fresh.is_a?(Open)
|
|
249
|
+
close
|
|
250
|
+
return nil
|
|
251
|
+
end
|
|
252
|
+
return fail_with(fresh) if fresh.is_a?(Err)
|
|
253
|
+
|
|
254
|
+
set_open(fresh)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
class ClientCore
|
|
260
|
+
# The wait before a reconnect when the server sent no retry value.
|
|
261
|
+
DEFAULT_RECONNECT_MS = 250
|
|
262
|
+
|
|
263
|
+
# The server-sent events of an operation (an id or a descriptor).
|
|
264
|
+
# `decode` turns each event's JSON data into the SDK's type.
|
|
265
|
+
#
|
|
266
|
+
# @return [EventStream]
|
|
267
|
+
def stream(op, args = {}, opts = nil, decode: nil)
|
|
268
|
+
d = op.is_a?(OperationDescriptor) ? op : operation(op.to_s)
|
|
269
|
+
unless d&.stream
|
|
270
|
+
id = op.is_a?(OperationDescriptor) ? op.id : op.to_s
|
|
271
|
+
err = Runtime.diag(id, "VALIDATION_FAILED").param("operation").expected("an operation with an event stream")
|
|
272
|
+
.remedy("#{id} has no event stream (its success response is not text/event-stream); use call instead. Nothing was sent.").err
|
|
273
|
+
return FailedStream.new(err)
|
|
274
|
+
end
|
|
275
|
+
o = begin
|
|
276
|
+
CallOptions.from(opts)
|
|
277
|
+
rescue ArgumentError => e
|
|
278
|
+
return FailedStream.new(Runtime.diag(d.id, "VALIDATION_FAILED").param("opts").expected("call options")
|
|
279
|
+
.remedy("Pass the call options as a Hash of idempotency_key, confirm, timeout_ms, verify and headers (#{e.message}).").err)
|
|
280
|
+
end
|
|
281
|
+
EventStream.new(self, d, args, o, decode)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A stream that fails before it starts.
|
|
285
|
+
class FailedStream < EventStream
|
|
286
|
+
def initialize(err)
|
|
287
|
+
super(nil, nil, nil, nil, nil)
|
|
288
|
+
@error = err
|
|
289
|
+
@state = :done
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
# The stream's request flag set to true, in a merged body field or
|
|
296
|
+
# inside an object body argument.
|
|
297
|
+
def with_stream_flag(op, args)
|
|
298
|
+
flag = op.stream["flag"]
|
|
299
|
+
return args if flag.nil? || op.body.nil? || !args.is_a?(Hash)
|
|
300
|
+
|
|
301
|
+
out = args.dup
|
|
302
|
+
if op.body.arg.nil?
|
|
303
|
+
field = op.body.fields.find { |_, wire| wire == flag }
|
|
304
|
+
out[field[0]] = true if field
|
|
305
|
+
elsif args[op.body.arg].is_a?(Hash)
|
|
306
|
+
out[op.body.arg] = args[op.body.arg].merge(flag => true)
|
|
307
|
+
end
|
|
308
|
+
out
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def open_stream(op, args, opts, resume_from)
|
|
312
|
+
headers = {}
|
|
313
|
+
accept = ""
|
|
314
|
+
[[false, @headers], [true, (opts.headers || {}).to_h.transform_keys(&:to_s)]].each do |own, layer|
|
|
315
|
+
layer.each do |name, value|
|
|
316
|
+
if name.casecmp?("accept")
|
|
317
|
+
accept = value.to_s
|
|
318
|
+
elsif own
|
|
319
|
+
headers[name] = value
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
headers["Accept"] = Util.merge_accept(accept)
|
|
324
|
+
if resume_from
|
|
325
|
+
headers.delete_if { |name, _| name.casecmp?("last-event-id") }
|
|
326
|
+
headers["Last-Event-ID"] = resume_from
|
|
327
|
+
end
|
|
328
|
+
stream_opts = opts.with(headers: headers)
|
|
329
|
+
converted =
|
|
330
|
+
begin
|
|
331
|
+
Json.from_ruby(args.nil? ? {} : args)
|
|
332
|
+
rescue ArgumentError
|
|
333
|
+
args
|
|
334
|
+
end
|
|
335
|
+
p = prepare(op, with_stream_flag(op, converted), stream_opts, :call, nil, nil)
|
|
336
|
+
return p if p.is_a?(Err)
|
|
337
|
+
|
|
338
|
+
result = send_with(p, stream_opts, true)
|
|
339
|
+
return result if result.is_a?(Err)
|
|
340
|
+
|
|
341
|
+
if result.is_a?(Ok)
|
|
342
|
+
type = result.meta.headers["content-type"]
|
|
343
|
+
after = op.mutation? ? " The call took effect; do not repeat it." : ""
|
|
344
|
+
return Runtime.diag(op.id, "UNEXPECTED_RESPONSE").status(result.meta.status).request_id(result.meta.request_id)
|
|
345
|
+
.param("response").received(type ? Util.envelope_value(type, false) : nil)
|
|
346
|
+
.expected("a text/event-stream body")
|
|
347
|
+
.remedy("The success response is not an event stream (Content-Type #{type || 'missing'}).#{after}")
|
|
348
|
+
.retrying("never").attempts(result.meta.attempts).err
|
|
349
|
+
end
|
|
350
|
+
_, meta, body = result
|
|
351
|
+
check = op.mutation? && !Classify.replay_protection?(op, !p.key.nil?) ? outcome_check(op, p.args) : nil
|
|
352
|
+
EventStream::Open.new(body: body, parser: SseParser.new(@max_event_bytes), decoder: Utf8Decoder.new, queue: [],
|
|
353
|
+
ended: false, oversize: false, meta: meta, delivered_count: 0, key: p.key, key_header: p.key_header,
|
|
354
|
+
check: check, secrets: p.secrets.dup, timeout_ms: attempt_timeout(stream_opts),
|
|
355
|
+
repeatable: !op.mutation? || Classify.replay_protection?(op, !p.key.nil?), reconnects: 0,
|
|
356
|
+
last_id: nil, last_retry: nil, delivered: {}, replaying: false)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# A dropped stream may be reconnected when the call is safe to repeat,
|
|
360
|
+
# reconnects are left, and an event id to resume from is known (or no
|
|
361
|
+
# event was delivered yet).
|
|
362
|
+
def may_reconnect?(o)
|
|
363
|
+
o.repeatable && o.reconnects < @max_reconnects &&
|
|
364
|
+
(o.delivered_count.zero? || (!o.last_id.nil? && Serialize.valid_header_value?(o.last_id)))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Wait as the server asked (capped, shortened by up to 25 % at random),
|
|
368
|
+
# then send the request again with Last-Event-ID.
|
|
369
|
+
def reconnect(op, args, opts, old)
|
|
370
|
+
old.body.close
|
|
371
|
+
ceiling = [old.last_retry || DEFAULT_RECONNECT_MS, @reconnect_max_ms].min.to_f
|
|
372
|
+
sleep([ceiling - (random * 0.25 * ceiling), 0].max / 1000.0)
|
|
373
|
+
fresh = open_stream(op, args, opts, old.last_id)
|
|
374
|
+
return fresh if fresh.is_a?(Err)
|
|
375
|
+
|
|
376
|
+
fresh.delivered_count = old.delivered_count
|
|
377
|
+
fresh.reconnects = old.reconnects + 1
|
|
378
|
+
fresh.last_id = old.last_id
|
|
379
|
+
fresh.last_retry = old.last_retry
|
|
380
|
+
fresh.delivered = old.delivered
|
|
381
|
+
fresh.replaying = true
|
|
382
|
+
fresh
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def plural(n, one, many) = n == 1 ? one : many
|
|
386
|
+
|
|
387
|
+
# The final error of a stream that broke after `o.delivered_count` events; `why`
|
|
388
|
+
# is :lost, :idle, :early_end, or nil for a silence past the attempt
|
|
389
|
+
# timeout.
|
|
390
|
+
def interrupted(op, o, why)
|
|
391
|
+
after = "after #{o.delivered_count} #{plural(o.delivered_count, 'event', 'events')}"
|
|
392
|
+
retried =
|
|
393
|
+
case o.reconnects
|
|
394
|
+
when 0 then ""
|
|
395
|
+
when 1 then " (1 reconnect made)"
|
|
396
|
+
else " (#{o.reconnects} reconnects made)"
|
|
397
|
+
end
|
|
398
|
+
idle = why == :idle
|
|
399
|
+
code = idle ? "STREAM_IDLE" : nil
|
|
400
|
+
status = o.meta.status
|
|
401
|
+
ctx = Classify::CallContext.new(api: @api, op: op, key: o.key, key_header: o.key_header, attempts: o.meta.attempts, check: o.check)
|
|
402
|
+
d =
|
|
403
|
+
if why.nil? || idle
|
|
404
|
+
silence = "No event arrived within #{o.timeout_ms} ms #{after}"
|
|
405
|
+
option = "timeout_ms"
|
|
406
|
+
if idle
|
|
407
|
+
silence = "No bytes arrived within #{@idle_timeout_ms} ms #{after}#{retried}"
|
|
408
|
+
option = "idle_timeout_ms"
|
|
409
|
+
end
|
|
410
|
+
cause = "#{silence}; the stream was abandoned."
|
|
411
|
+
if op.mutation?
|
|
412
|
+
Classify.outcome_unknown(ctx, cause, http_status: status, code: code, request_id: o.meta.request_id)
|
|
413
|
+
else
|
|
414
|
+
Runtime.diag(op.id, "UPSTREAM_UNAVAILABLE").status(status).code(code).request_id(o.meta.request_id)
|
|
415
|
+
.remedy("#{cause} This read has no side effects; call again later or with a larger #{option}.")
|
|
416
|
+
.attempts(o.meta.attempts).build
|
|
417
|
+
end
|
|
418
|
+
else
|
|
419
|
+
cause = "The event stream was cut off #{after}#{retried}: the connection failed before it ended."
|
|
420
|
+
if op.mutation?
|
|
421
|
+
Classify.outcome_unknown(ctx, cause, http_status: status, request_id: o.meta.request_id)
|
|
422
|
+
else
|
|
423
|
+
Runtime.diag(op.id, "TRANSPORT_FAILED").status(status).request_id(o.meta.request_id)
|
|
424
|
+
.remedy("#{cause} This read has no side effects; call again.").attempts(o.meta.attempts).build
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
Err.new(error: Util.scrub_diagnostic(d, o.secrets))
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def over_budget(op, bytes, kept)
|
|
431
|
+
what = bytes ? "at most #{@max_collect_bytes} bytes of events" : "at most #{@max_collect_ms} ms to collect the events"
|
|
432
|
+
after = op.mutation? ? " The call took effect; do not repeat it." : ""
|
|
433
|
+
Runtime.diag(op.id, "UNEXPECTED_RESPONSE").param("events").expected(what)
|
|
434
|
+
.remedy("The stream was abandoned after #{kept} #{plural(kept, 'event', 'events')} (#{what}; max_collect_bytes and max_collect_ms).#{after} " \
|
|
435
|
+
"The events collected so far precede this error.")
|
|
436
|
+
.retrying("never").err
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def oversize(op, o)
|
|
440
|
+
limit = @max_event_bytes
|
|
441
|
+
after = op.mutation? ? " The call took effect; do not repeat it." : ""
|
|
442
|
+
d = Runtime.diag(op.id, "UNEXPECTED_RESPONSE").status(o.meta.status).request_id(o.meta.request_id)
|
|
443
|
+
.param("events[#{o.delivered_count}]").expected("an event of at most #{limit} bytes")
|
|
444
|
+
.remedy("Event #{o.delivered_count} of the stream is larger than the limit of #{limit} bytes (max_event_bytes); the stream was abandoned.#{after} " \
|
|
445
|
+
"Events before it were delivered. Raise max_event_bytes if the server sends events this large on purpose.")
|
|
446
|
+
.retrying("never").attempts(o.meta.attempts).build
|
|
447
|
+
Err.new(error: Util.scrub_diagnostic(d, o.secrets))
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Decode, check and number one event: a StreamEvent, :done for the done
|
|
451
|
+
# sentinel, nil for a replayed event, or an Err.
|
|
452
|
+
def accept_event(op, o, ev, decode)
|
|
453
|
+
return :done if !op.stream["done"].nil? && op.stream["done"] == ev.data
|
|
454
|
+
|
|
455
|
+
if o.replaying
|
|
456
|
+
return nil if !ev.id.nil? && o.delivered[ev.id]
|
|
457
|
+
|
|
458
|
+
o.replaying = false
|
|
459
|
+
end
|
|
460
|
+
index = o.delivered_count
|
|
461
|
+
after_effect = op.mutation? ? " The call took effect; do not repeat it." : ""
|
|
462
|
+
failure = lambda do |b|
|
|
463
|
+
Err.new(error: Util.scrub_diagnostic(b.status(o.meta.status).request_id(o.meta.request_id).retrying("never")
|
|
464
|
+
.attempts(o.meta.attempts).build, o.secrets))
|
|
465
|
+
end
|
|
466
|
+
value = Json.try_parse(ev.data)
|
|
467
|
+
if UNSET.equal?(value)
|
|
468
|
+
return failure.call(Runtime.diag(op.id, "UNEXPECTED_RESPONSE").param("events[#{index}]")
|
|
469
|
+
.received(Util.envelope_value(ev.data, false)).expected("JSON in the data of every event")
|
|
470
|
+
.remedy("Event #{index} of the stream (event \"#{ev.event}\") does not carry JSON in its data.#{after_effect} Events before it were delivered."))
|
|
471
|
+
end
|
|
472
|
+
if @validate_responses != "off" && op.stream["event"]
|
|
473
|
+
issues = judge(op.stream["event"], value)
|
|
474
|
+
unless issues.empty?
|
|
475
|
+
issue = issues.first
|
|
476
|
+
path_text = Runtime.issue_path(issue.path)
|
|
477
|
+
redacted = Util.redact_paths(value, op.agent["sensitive_response_fields"])
|
|
478
|
+
received = Util.get_path(redacted, issue.path)
|
|
479
|
+
sensitive = issue.path.any? { |s| s.is_a?(String) && Util.looks_sensitive?(s) }
|
|
480
|
+
e = failure.call(Runtime.diag(op.id, "UNEXPECTED_RESPONSE").param("events[#{index}]#{path_text}")
|
|
481
|
+
.received(Util.envelope_value(received, sensitive)).expected(issue.message)
|
|
482
|
+
.remedy("Event #{index} of the stream (event \"#{ev.event}\") does not match the API description at events[#{index}]#{path_text} (#{issue.message}).#{after_effect}"))
|
|
483
|
+
return e if @validate_responses == "strict"
|
|
484
|
+
|
|
485
|
+
emit(e.error)
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
o.delivered_count += 1
|
|
489
|
+
unless ev.id.nil?
|
|
490
|
+
o.delivered[ev.id] = true
|
|
491
|
+
o.last_id = ev.id
|
|
492
|
+
end
|
|
493
|
+
o.last_retry = ev.retry_ms unless ev.retry_ms.nil?
|
|
494
|
+
typed = decode ? decode.call(value) : value
|
|
495
|
+
StreamEvent.new(value: typed, raw: value, event: ev.event, id: o.last_id, retry_ms: o.last_retry, meta: o.meta,
|
|
496
|
+
reconnects: o.reconnects)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|