concurrent-ruby-edge 0.3.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +552 -0
  3. data/LICENSE.txt +18 -18
  4. data/README.md +261 -103
  5. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/abstract.rb +2 -0
  6. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/awaits.rb +2 -0
  7. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/buffer.rb +2 -0
  8. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/errors_on_unknown_message.rb +2 -0
  9. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/executes_context.rb +2 -0
  10. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/linking.rb +2 -0
  11. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/pausing.rb +2 -0
  12. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/removes_child.rb +2 -0
  13. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/sets_results.rb +2 -0
  14. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/supervising.rb +2 -0
  15. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/termination.rb +3 -1
  16. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour.rb +1 -1
  17. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/context.rb +3 -1
  18. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/core.rb +5 -4
  19. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/default_dead_letter_handler.rb +2 -0
  20. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/envelope.rb +2 -0
  21. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/errors.rb +2 -0
  22. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/internal_delegations.rb +3 -0
  23. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/reference.rb +9 -8
  24. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/root.rb +3 -0
  25. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/ad_hoc.rb +2 -0
  26. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/balancer.rb +2 -0
  27. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/broadcast.rb +1 -0
  28. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/pool.rb +1 -0
  29. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor.rb +11 -6
  30. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/base.rb +14 -14
  31. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/dropping.rb +1 -0
  32. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/sliding.rb +1 -0
  33. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/unbuffered.rb +1 -1
  34. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/tick.rb +1 -1
  35. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel.rb +3 -2
  36. data/lib/concurrent-ruby-edge/concurrent/edge/cancellation.rb +107 -0
  37. data/lib/concurrent-ruby-edge/concurrent/edge/channel.rb +453 -0
  38. data/lib/concurrent-ruby-edge/concurrent/edge/erlang_actor.rb +1549 -0
  39. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set/node.rb +2 -2
  40. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set.rb +8 -7
  41. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_queue.rb +2 -0
  42. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/old_channel_integration.rb +2 -0
  43. data/lib/concurrent-ruby-edge/concurrent/edge/processing_actor.rb +184 -0
  44. data/lib/concurrent-ruby-edge/concurrent/edge/promises.rb +174 -0
  45. data/lib/concurrent-ruby-edge/concurrent/edge/throttle.rb +229 -0
  46. data/lib/concurrent-ruby-edge/concurrent/edge/version.rb +3 -0
  47. data/lib/concurrent-ruby-edge/concurrent/edge.rb +21 -0
  48. data/lib/concurrent-ruby-edge/concurrent/executor/wrapping_executor.rb +50 -0
  49. data/lib/concurrent-ruby-edge/concurrent/lazy_register.rb +83 -0
  50. data/lib/{concurrent-edge.rb → concurrent-ruby-edge/concurrent-edge.rb} +5 -4
  51. metadata +71 -67
  52. data/lib/concurrent/edge/atomic_markable_reference.rb +0 -184
  53. data/lib/concurrent/edge/cancellation.rb +0 -138
  54. data/lib/concurrent/edge/lock_free_stack.rb +0 -126
  55. data/lib/concurrent/edge/processing_actor.rb +0 -161
  56. data/lib/concurrent/edge/promises.rb +0 -2111
  57. data/lib/concurrent/edge/throttle.rb +0 -192
  58. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/public_delegations.rb +0 -0
  59. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/type_check.rb +0 -0
  60. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils.rb +0 -0
  61. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/buffered.rb +0 -0
  62. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/ticker.rb +0 -0
  63. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/timer.rb +0 -0
  64. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer.rb +0 -0
  65. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/after_clause.rb +0 -0
  66. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/default_clause.rb +0 -0
  67. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/error_clause.rb +0 -0
  68. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/put_clause.rb +0 -0
  69. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/take_clause.rb +0 -0
  70. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector.rb +0 -0
  71. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set/window.rb +0 -0
@@ -0,0 +1,453 @@
1
+ require 'concurrent/synchronization/object'
2
+ require 'concurrent/edge/promises'
3
+
4
+ # @!macro warn.edge
5
+ module Concurrent
6
+ module Promises
7
+
8
+ # A first in first out channel that accepts messages with push family of methods and returns
9
+ # messages with pop family of methods.
10
+ # Pop and push operations can be represented as futures, see {#pop_op} and {#push_op}.
11
+ # The capacity of the channel can be limited to support back pressure, use capacity option in {#initialize}.
12
+ # {#pop} method blocks ans {#pop_op} returns pending future if there is no message in the channel.
13
+ # If the capacity is limited the {#push} method blocks and {#push_op} returns pending future.
14
+ #
15
+ # {include:file:docs-source/channel.out.md}
16
+ # @!macro warn.edge
17
+ class Channel < Concurrent::Synchronization::Object
18
+
19
+ # TODO (pitr-ch 06-Jan-2019): rename to Conduit?, to be able to place it into Concurrent namespace?
20
+ # TODO (pitr-ch 14-Jan-2019): better documentation, do few examples from go
21
+ # TODO (pitr-ch 12-Dec-2018): implement channel closing,
22
+ # - as a child class? To also have a channel which cannot be closed.
23
+ # TODO (pitr-ch 26-Dec-2016): replace with lock-free implementation, at least getting a message when available should be lock free same goes for push with space available
24
+
25
+ # @!macro channel.warn.blocks
26
+ # @note This function potentially blocks current thread until it can continue.
27
+ # Be careful it can deadlock.
28
+ #
29
+ # @!macro channel.param.timeout
30
+ # @param [Numeric] timeout the maximum time in second to wait.
31
+
32
+ safe_initialization!
33
+
34
+ # Default capacity of the Channel, makes it accept unlimited number of messages.
35
+ UNLIMITED_CAPACITY = ::Object.new
36
+ UNLIMITED_CAPACITY.singleton_class.class_eval do
37
+ include Comparable
38
+
39
+ def <=>(other)
40
+ 1
41
+ end
42
+
43
+ def to_s
44
+ 'unlimited'
45
+ end
46
+ end
47
+
48
+ NOTHING = Object.new
49
+ private_constant :NOTHING
50
+
51
+ # An object which matches anything (with #===)
52
+ ANY = Object.new.tap do |any|
53
+ def any.===(other)
54
+ true
55
+ end
56
+
57
+ def any.to_s
58
+ 'ANY'
59
+ end
60
+ end
61
+
62
+ # Create channel.
63
+ # @param [Integer, UNLIMITED_CAPACITY] capacity the maximum number of messages which can be stored in the channel.
64
+ def initialize(capacity = UNLIMITED_CAPACITY)
65
+ super()
66
+ @Capacity = capacity
67
+ @Mutex = Mutex.new
68
+ # TODO (pitr-ch 28-Jan-2019): consider linked lists or other data structures for following attributes, things are being deleted from the middle
69
+ @Probes = []
70
+ @Messages = []
71
+ @PendingPush = []
72
+ end
73
+
74
+ # Push the message into the channel if there is space available.
75
+ # @param [Object] message
76
+ # @return [true, false]
77
+ def try_push(message)
78
+ @Mutex.synchronize { ns_try_push(message) }
79
+ end
80
+
81
+ # Returns future which will fulfill when the message is pushed to the channel.
82
+ # @!macro chanel.operation_wait
83
+ # If it is later waited on the operation with a timeout e.g.`channel.pop_op.wait(1)`
84
+ # it will not prevent the channel to fulfill the operation later after the timeout.
85
+ # The operation has to be either processed later
86
+ # ```ruby
87
+ # pop_op = channel.pop_op
88
+ # if pop_op.wait(1)
89
+ # process_message pop_op.value
90
+ # else
91
+ # pop_op.then { |message| log_unprocessed_message message }
92
+ # end
93
+ # ```
94
+ # or the operation can be prevented from completion after timing out by using
95
+ # `channel.pop_op.wait(1, [true, nil, nil])`.
96
+ # It will fulfill the operation on timeout preventing channel from doing the operation,
97
+ # e.g. popping a message.
98
+ #
99
+ # @param [Object] message
100
+ # @return [ResolvableFuture(self)]
101
+ def push_op(message)
102
+ @Mutex.synchronize do
103
+ if ns_try_push(message)
104
+ Promises.fulfilled_future self
105
+ else
106
+ pushed = Promises.resolvable_future
107
+ @PendingPush.push message, pushed
108
+ return pushed
109
+ end
110
+ end
111
+ end
112
+
113
+ # Blocks current thread until the message is pushed into the channel.
114
+ #
115
+ # @!macro channel.warn.blocks
116
+ # @param [Object] message
117
+ # @!macro channel.param.timeout
118
+ # @return [self, true, false] self implies timeout was not used, true implies timeout was used
119
+ # and it was pushed, false implies it was not pushed within timeout.
120
+ def push(message, timeout = nil)
121
+ pushed_op = @Mutex.synchronize do
122
+ return timeout ? true : self if ns_try_push(message)
123
+
124
+ pushed = Promises.resolvable_future
125
+ # TODO (pitr-ch 06-Jan-2019): clear timed out pushes in @PendingPush, null messages
126
+ @PendingPush.push message, pushed
127
+ pushed
128
+ end
129
+
130
+ result = pushed_op.wait!(timeout, [true, self, nil])
131
+ result == pushed_op ? self : result
132
+ end
133
+
134
+ # @!macro promises.channel.try_pop
135
+ # Pop a message from the channel if there is one available.
136
+ # @param [Object] no_value returned when there is no message available
137
+ # @return [Object, no_value] message or nil when there is no message
138
+ def try_pop(no_value = nil)
139
+ try_pop_matching ANY, no_value
140
+ end
141
+
142
+ # @!macro promises.channel.try_pop
143
+ # @!macro promises.channel.param.matcher
144
+ # @param [#===] matcher only consider message which matches `matcher === a_message`
145
+ def try_pop_matching(matcher, no_value = nil)
146
+ @Mutex.synchronize do
147
+ message = ns_shift_message matcher
148
+ return message if message != NOTHING
149
+ message = ns_consume_pending_push matcher
150
+ return message != NOTHING ? message : no_value
151
+ end
152
+ end
153
+
154
+ # @!macro promises.channel.pop_op
155
+ # Returns a future witch will become fulfilled with a value from the channel when one is available.
156
+ # @!macro chanel.operation_wait
157
+ #
158
+ # @param [ResolvableFuture] probe the future which will be fulfilled with a channel value
159
+ # @return [Future(Object)] the probe, its value will be the message when available.
160
+ def pop_op(probe = Promises.resolvable_future)
161
+ @Mutex.synchronize { ns_pop_op(ANY, probe, false) }
162
+ end
163
+
164
+ # @!macro promises.channel.pop_op
165
+ # @!macro promises.channel.param.matcher
166
+ def pop_op_matching(matcher, probe = Promises.resolvable_future)
167
+ @Mutex.synchronize { ns_pop_op(matcher, probe, false) }
168
+ end
169
+
170
+ # @!macro promises.channel.pop
171
+ # Blocks current thread until a message is available in the channel for popping.
172
+ #
173
+ # @!macro channel.warn.blocks
174
+ # @!macro channel.param.timeout
175
+ # @param [Object] timeout_value a value returned by the method when it times out
176
+ # @return [Object, nil] message or nil when timed out
177
+ def pop(timeout = nil, timeout_value = nil)
178
+ pop_matching ANY, timeout, timeout_value
179
+ end
180
+
181
+ # @!macro promises.channel.pop
182
+ # @!macro promises.channel.param.matcher
183
+ def pop_matching(matcher, timeout = nil, timeout_value = nil)
184
+ # TODO (pitr-ch 27-Jan-2019): should it try to match pending pushes if it fails to match in the buffer? Maybe only if the size is zero. It could be surprising if it's used as a throttle it might be expected that it will not pop if buffer is full of messages which di not match, it might it expected it will block until the message is added to the buffer
185
+ # that it returns even if the buffer is full. User might expect that it has to be in the buffer first.
186
+ probe = @Mutex.synchronize do
187
+ message = ns_shift_message matcher
188
+ if message == NOTHING
189
+ message = ns_consume_pending_push matcher
190
+ return message if message != NOTHING
191
+ else
192
+ new_message = ns_consume_pending_push ANY
193
+ @Messages.push new_message unless new_message == NOTHING
194
+ return message
195
+ end
196
+
197
+ probe = Promises.resolvable_future
198
+ @Probes.push probe, false, matcher
199
+ probe
200
+ end
201
+
202
+ probe.value!(timeout, timeout_value, [true, timeout_value, nil])
203
+ end
204
+
205
+ # @!macro promises.channel.peek
206
+ # Behaves as {#try_pop} but it does not remove the message from the channel
207
+ # @param [Object] no_value returned when there is no message available
208
+ # @return [Object, no_value] message or nil when there is no message
209
+ def peek(no_value = nil)
210
+ peek_matching ANY, no_value
211
+ end
212
+
213
+ # @!macro promises.channel.peek
214
+ # @!macro promises.channel.param.matcher
215
+ def peek_matching(matcher, no_value = nil)
216
+ @Mutex.synchronize do
217
+ message = ns_shift_message matcher, false
218
+ return message if message != NOTHING
219
+ message = ns_consume_pending_push matcher, false
220
+ return message != NOTHING ? message : no_value
221
+ end
222
+ end
223
+
224
+ # @!macro promises.channel.try_select
225
+ # If message is available in the receiver or any of the provided channels
226
+ # the channel message pair is returned. If there is no message nil is returned.
227
+ # The returned channel is the origin of the message.
228
+ #
229
+ # @param [Channel, ::Array<Channel>] channels
230
+ # @return [::Array(Channel, Object), nil]
231
+ # pair [channel, message] if one of the channels is available for reading
232
+ def try_select(channels)
233
+ try_select_matching ANY, channels
234
+ end
235
+
236
+ # @!macro promises.channel.try_select
237
+ # @!macro promises.channel.param.matcher
238
+ def try_select_matching(matcher, channels)
239
+ message = nil
240
+ channel = [self, *channels].find do |ch|
241
+ message = ch.try_pop_matching(matcher, NOTHING)
242
+ message != NOTHING
243
+ end
244
+ channel ? [channel, message] : nil
245
+ end
246
+
247
+ # @!macro promises.channel.select_op
248
+ # When message is available in the receiver or any of the provided channels
249
+ # the future is fulfilled with a channel message pair.
250
+ # The returned channel is the origin of the message.
251
+ # @!macro chanel.operation_wait
252
+ #
253
+ # @param [Channel, ::Array<Channel>] channels
254
+ # @param [ResolvableFuture] probe the future which will be fulfilled with the message
255
+ # @return [ResolvableFuture(::Array(Channel, Object))] a future which is fulfilled with
256
+ # pair [channel, message] when one of the channels is available for reading
257
+ def select_op(channels, probe = Promises.resolvable_future)
258
+ select_op_matching ANY, channels, probe
259
+ end
260
+
261
+ # @!macro promises.channel.select_op
262
+ # @!macro promises.channel.param.matcher
263
+ def select_op_matching(matcher, channels, probe = Promises.resolvable_future)
264
+ [self, *channels].each { |ch| ch.partial_select_op matcher, probe }
265
+ probe
266
+ end
267
+
268
+ # @!macro promises.channel.select
269
+ # As {#select_op} but does not return future,
270
+ # it block current thread instead until there is a message available
271
+ # in the receiver or in any of the channels.
272
+ #
273
+ # @!macro channel.warn.blocks
274
+ # @param [Channel, ::Array<Channel>] channels
275
+ # @!macro channel.param.timeout
276
+ # @return [::Array(Channel, Object), nil] message or nil when timed out
277
+ # @see #select_op
278
+ def select(channels, timeout = nil)
279
+ select_matching ANY, channels, timeout
280
+ end
281
+
282
+ # @!macro promises.channel.select
283
+ # @!macro promises.channel.param.matcher
284
+ def select_matching(matcher, channels, timeout = nil)
285
+ probe = select_op_matching(matcher, channels)
286
+ probe.value!(timeout, nil, [true, nil, nil])
287
+ end
288
+
289
+ # @return [Integer] The number of messages currently stored in the channel.
290
+ def size
291
+ @Mutex.synchronize { @Messages.size }
292
+ end
293
+
294
+ # @return [Integer] Maximum capacity of the Channel.
295
+ def capacity
296
+ @Capacity
297
+ end
298
+
299
+ # @return [String] Short string representation.
300
+ def to_s
301
+ format '%s capacity taken %s of %s>', super[0..-2], size, @Capacity
302
+ end
303
+
304
+ alias_method :inspect, :to_s
305
+
306
+ class << self
307
+
308
+ # @see #try_select
309
+ # @return [::Array(Channel, Object)]
310
+ def try_select(channels)
311
+ channels.first.try_select(channels[1..-1])
312
+ end
313
+
314
+ # @see #select_op
315
+ # @return [Future(::Array(Channel, Object))]
316
+ def select_op(channels, probe = Promises.resolvable_future)
317
+ channels.first.select_op(channels[1..-1], probe)
318
+ end
319
+
320
+ # @see #select
321
+ # @return [::Array(Channel, Object), nil]
322
+ def select(channels, timeout = nil)
323
+ channels.first.select(channels[1..-1], timeout)
324
+ end
325
+
326
+ # @see #try_select_matching
327
+ # @return [::Array(Channel, Object)]
328
+ def try_select_matching(matcher, channels)
329
+ channels.first.try_select_matching(matcher, channels[1..-1])
330
+ end
331
+
332
+ # @see #select_op_matching
333
+ # @return [Future(::Array(Channel, Object))]
334
+ def select_op_matching(matcher, channels, probe = Promises.resolvable_future)
335
+ channels.first.select_op_matching(matcher, channels[1..-1], probe)
336
+ end
337
+
338
+ # @see #select_matching
339
+ # @return [::Array(Channel, Object), nil]
340
+ def select_matching(matcher, channels, timeout = nil)
341
+ channels.first.select_matching(matcher, channels[1..-1], timeout)
342
+ end
343
+ end
344
+
345
+ # @!visibility private
346
+ def partial_select_op(matcher, probe)
347
+ @Mutex.synchronize { ns_pop_op(matcher, probe, true) }
348
+ end
349
+
350
+ private
351
+
352
+ def ns_pop_op(matcher, probe, include_channel)
353
+ message = ns_shift_message matcher
354
+
355
+ # got message from buffer
356
+ if message != NOTHING
357
+ if probe.fulfill(include_channel ? [self, message] : message, false)
358
+ new_message = ns_consume_pending_push ANY
359
+ @Messages.push new_message unless new_message == NOTHING
360
+ else
361
+ @Messages.unshift message
362
+ end
363
+ return probe
364
+ end
365
+
366
+ # no message in buffer, try to pair with a pending push
367
+ i = 0
368
+ while true
369
+ message, pushed = @PendingPush[i, 2]
370
+ break if pushed.nil?
371
+
372
+ if matcher === message
373
+ value = include_channel ? [self, message] : message
374
+ if Promises::Resolvable.atomic_resolution(probe => [true, value, nil],
375
+ pushed => [true, self, nil])
376
+ @PendingPush[i, 2] = []
377
+ return probe
378
+ end
379
+
380
+ if probe.resolved?
381
+ return probe
382
+ end
383
+
384
+ # so pushed.resolved? has to be true, remove the push
385
+ @PendingPush[i, 2] = []
386
+ end
387
+
388
+ i += 2
389
+ end
390
+
391
+ # no push to pair with
392
+ # TODO (pitr-ch 11-Jan-2019): clear up probes when timed out, use callback
393
+ @Probes.push probe, include_channel, matcher if probe.pending?
394
+ return probe
395
+ end
396
+
397
+ def ns_consume_pending_push(matcher, remove = true)
398
+ i = 0
399
+ while true
400
+ message, pushed = @PendingPush[i, 2]
401
+ return NOTHING unless pushed
402
+
403
+ if matcher === message
404
+ resolved = pushed.resolved?
405
+ @PendingPush[i, 2] = [] if remove || resolved
406
+ # can fail if timed-out, so try without error
407
+ if remove ? pushed.fulfill(self, false) : !resolved
408
+ # pushed fulfilled so actually push the message
409
+ return message
410
+ end
411
+ end
412
+
413
+ i += 2
414
+ end
415
+ end
416
+
417
+ def ns_try_push(message)
418
+ i = 0
419
+ while true
420
+ probe, include_channel, matcher = @Probes[i, 3]
421
+ break unless probe
422
+ if matcher === message && probe.fulfill(include_channel ? [self, message] : message, false)
423
+ @Probes[i, 3] = []
424
+ return true
425
+ end
426
+ i += 3
427
+ end
428
+
429
+ if @Capacity > @Messages.size
430
+ @Messages.push message
431
+ true
432
+ else
433
+ false
434
+ end
435
+ end
436
+
437
+ def ns_shift_message(matcher, remove = true)
438
+ i = 0
439
+ while true
440
+ message = @Messages.fetch(i, NOTHING)
441
+ return NOTHING if message == NOTHING
442
+
443
+ if matcher === message
444
+ @Messages.delete_at i if remove
445
+ return message
446
+ end
447
+
448
+ i += 1
449
+ end
450
+ end
451
+ end
452
+ end
453
+ end