iopromise 0.1.0 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -2
  3. data/Gemfile +4 -10
  4. data/Gemfile.lock +7 -30
  5. data/README.md +23 -3
  6. data/bin/setup +0 -3
  7. data/iopromise.gemspec +1 -0
  8. data/lib/iopromise.rb +47 -18
  9. data/lib/iopromise/cancel_context.rb +51 -0
  10. data/lib/iopromise/data_loader.rb +58 -0
  11. data/lib/iopromise/deferred.rb +2 -2
  12. data/lib/iopromise/deferred/executor_pool.rb +26 -10
  13. data/lib/iopromise/deferred/promise.rb +15 -2
  14. data/lib/iopromise/executor_context.rb +47 -59
  15. data/lib/iopromise/executor_pool/base.rb +26 -7
  16. data/lib/iopromise/executor_pool/batch.rb +5 -3
  17. data/lib/iopromise/executor_pool/sequential.rb +6 -14
  18. data/lib/iopromise/rack/context_middleware.rb +5 -6
  19. data/lib/iopromise/version.rb +1 -1
  20. data/lib/iopromise/view_component/data_loader.rb +3 -44
  21. metadata +18 -18
  22. data/lib/iopromise/dalli.rb +0 -13
  23. data/lib/iopromise/dalli/client.rb +0 -146
  24. data/lib/iopromise/dalli/executor_pool.rb +0 -13
  25. data/lib/iopromise/dalli/patch_dalli.rb +0 -337
  26. data/lib/iopromise/dalli/promise.rb +0 -52
  27. data/lib/iopromise/dalli/response.rb +0 -25
  28. data/lib/iopromise/faraday.rb +0 -17
  29. data/lib/iopromise/faraday/connection.rb +0 -25
  30. data/lib/iopromise/faraday/continuable_hydra.rb +0 -29
  31. data/lib/iopromise/faraday/executor_pool.rb +0 -19
  32. data/lib/iopromise/faraday/multi_socket_action.rb +0 -107
  33. data/lib/iopromise/faraday/promise.rb +0 -42
  34. data/lib/iopromise/memcached.rb +0 -13
  35. data/lib/iopromise/memcached/client.rb +0 -22
  36. data/lib/iopromise/memcached/executor_pool.rb +0 -61
  37. data/lib/iopromise/memcached/promise.rb +0 -32
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'dalli/client'
4
-
5
- module IOPromise
6
- module Dalli
7
- class << self
8
- def new(*args, **kwargs)
9
- ::IOPromise::Dalli::Client.new(*args, **kwargs)
10
- end
11
- end
12
- end
13
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dalli'
4
- require_relative 'promise'
5
- require_relative 'patch_dalli'
6
-
7
- module IOPromise
8
- module Dalli
9
- class Client
10
- # General note:
11
- # There is no need for explicit get_multi or batching, as requests
12
- # are sent as soon as the IOPromise is created, multiple can be
13
- # awaiting response at any time, and responses are automatically demuxed.
14
- def initialize(servers = nil, options = {})
15
- @cache_nils = !!options[:cache_nils]
16
- options[:iopromise_async] = true
17
- @options = options
18
- @client = ::Dalli::Client.new(servers, options)
19
- end
20
-
21
- # Returns a promise that resolves to a IOPromise::Dalli::Response with the
22
- # value for the given key, or +nil+ if the key is not found.
23
- def get(key, options = nil)
24
- execute_as_promise(:get, key, options)
25
- end
26
-
27
- # Convenience function that attempts to fetch the given key, or set
28
- # the key with a dynamically generated value if it does not exist.
29
- # Either way, the returned promise will resolve to the cached or computed
30
- # value.
31
- #
32
- # If the value does not exist then the provided block is run to generate
33
- # the value (which can also be a promise), after which the value is set
34
- # if it still doesn't exist.
35
- def fetch(key, ttl = nil, options = nil, &block)
36
- # match the Dalli behaviour exactly
37
- options = options.nil? ? ::Dalli::Client::CACHE_NILS : options.merge(::Dalli::Client::CACHE_NILS) if @cache_nils
38
- get(key, options).then do |response|
39
- not_found = @options[:cache_nils] ?
40
- !response.exist? :
41
- response.value.nil?
42
- if not_found && !block.nil?
43
- Promise.resolve(block.call).then do |new_val|
44
- # delay the final resolution here until after the add succeeds,
45
- # to guarantee errors are caught. we could potentially allow
46
- # the add to resolve once it's sent (without confirmation), but
47
- # we do need to wait on the add promise to ensure it's sent.
48
- add(key, new_val, ttl, options).then { new_val }
49
- end
50
- else
51
- Promise.resolve(response.value)
52
- end
53
- end
54
- end
55
-
56
- # Unconditionally sets the +key+ to the +value+ specified.
57
- # Returns a promise that resolves to a IOPromise::Dalli::Response.
58
- def set(key, value, ttl = nil, options = nil)
59
- execute_as_promise(:set, key, value, ttl_or_default(ttl), 0, options)
60
- end
61
-
62
- # Conditionally sets the +key+ to the +value+ specified.
63
- # Returns a promise that resolves to a IOPromise::Dalli::Response.
64
- def add(key, value, ttl = nil, options = nil)
65
- execute_as_promise(:add, key, value, ttl_or_default(ttl), options)
66
- end
67
-
68
- # Conditionally sets the +key+ to the +value+ specified only
69
- # if the key already exists.
70
- # Returns a promise that resolves to a IOPromise::Dalli::Response.
71
- def replace(key, value, ttl = nil, options = nil)
72
- execute_as_promise(:replace, key, value, ttl_or_default(ttl), 0, options)
73
- end
74
-
75
- # Deletes the specified key, resolving the promise when complete.
76
- def delete(key)
77
- execute_as_promise(:delete, key, 0)
78
- end
79
-
80
- # Appends a value to the specified key, resolving the promise when complete.
81
- # Appending only works for values stored with :raw => true.
82
- def append(key, value)
83
- Promise.resolve(value).then do |resolved_value|
84
- execute_as_promise(:append, key, resolved_value.to_s)
85
- end
86
- end
87
-
88
- # Prepend a value to the specified key, resolving the promise when complete.
89
- # Prepending only works for values stored with :raw => true.
90
- def prepend(key, value)
91
- Promise.resolve(value).then do |resolved_value|
92
- execute_as_promise(:prepend, key, resolved_value.to_s)
93
- end
94
- end
95
-
96
- ##
97
- # Incr adds the given amount to the counter on the memcached server.
98
- # Amt must be a positive integer value.
99
- #
100
- # If default is nil, the counter must already exist or the operation
101
- # will fail and will return nil. Otherwise this method will return
102
- # the new value for the counter.
103
- #
104
- # Note that the ttl will only apply if the counter does not already
105
- # exist. To increase an existing counter and update its TTL, use
106
- # #cas.
107
- def incr(key, amt = 1, ttl = nil, default = nil)
108
- raise ArgumentError, "Positive values only: #{amt}" if amt < 0
109
- execute_as_promise(:incr, key, amt.to_i, ttl_or_default(ttl), default)
110
- end
111
-
112
- ##
113
- # Decr subtracts the given amount from the counter on the memcached server.
114
- # Amt must be a positive integer value.
115
- #
116
- # memcached counters are unsigned and cannot hold negative values. Calling
117
- # decr on a counter which is 0 will just return 0.
118
- #
119
- # If default is nil, the counter must already exist or the operation
120
- # will fail and will return nil. Otherwise this method will return
121
- # the new value for the counter.
122
- #
123
- # Note that the ttl will only apply if the counter does not already
124
- # exist. To decrease an existing counter and update its TTL, use
125
- # #cas.
126
- def decr(key, amt = 1, ttl = nil, default = nil)
127
- raise ArgumentError, "Positive values only: #{amt}" if amt < 0
128
- execute_as_promise(:decr, key, amt.to_i, ttl_or_default(ttl), default)
129
- end
130
-
131
- # TODO: touch, gat, CAS operations
132
-
133
- private
134
-
135
- def execute_as_promise(*args)
136
- @client.perform_async(*args)
137
- end
138
-
139
- def ttl_or_default(ttl)
140
- (ttl || @options[:expires_in]).to_i
141
- rescue NoMethodError
142
- raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
143
- end
144
- end
145
- end
146
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module IOPromise
4
- module Dalli
5
- class DalliExecutorPool < IOPromise::ExecutorPool::Base
6
- def execute_continue(ready_readers, ready_writers, ready_exceptions)
7
- dalli_server = @connection_pool
8
-
9
- dalli_server.execute_continue(ready_readers, ready_writers, ready_exceptions)
10
- end
11
- end
12
- end
13
- end
@@ -1,337 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dalli'
4
- require_relative 'response'
5
-
6
- module IOPromise
7
- module Dalli
8
- module AsyncClient
9
- def initialize(servers = nil, options = {})
10
- @async = options[:iopromise_async] == true
11
-
12
- super
13
- end
14
-
15
- def perform_async(*args)
16
- if @async
17
- perform(*args)
18
- else
19
- raise ArgumentError, "Cannot perform_async when async is not enabled."
20
- end
21
- rescue => ex
22
- # Wrap any connection errors into a promise, this is more forwards-compatible
23
- # if we ever attempt to make connecting/server fallback nonblocking too.
24
- Promise.new.tap { |p| p.reject(ex) }
25
- end
26
- end
27
-
28
- module AsyncServer
29
- def initialize(attribs, options = {})
30
- @async = options.delete(:iopromise_async) == true
31
-
32
- if @async
33
- async_reset
34
-
35
- @next_opaque_id = 0
36
- @pending_ops = {}
37
- end
38
-
39
- super
40
- end
41
-
42
- def async?
43
- @async
44
- end
45
-
46
- def close
47
- if async?
48
- async_reset
49
- end
50
-
51
- super
52
- end
53
-
54
- def async_reset
55
- @write_buffer = +""
56
- @write_offset = 0
57
-
58
- @read_buffer = +""
59
- @read_offset = 0
60
- end
61
-
62
- # called by ExecutorPool to continue processing for this server
63
- def execute_continue(ready_readers, ready_writers, ready_exceptions)
64
- unless ready_writers.nil? || ready_writers.empty?
65
- # we are able to write, so write as much as we can.
66
- sock_write_nonblock
67
- end
68
-
69
- readers_empty = ready_readers.nil? || ready_readers.empty?
70
- exceptions_empty = ready_exceptions.nil? || ready_exceptions.empty?
71
-
72
- if !readers_empty || !exceptions_empty
73
- sock_read_nonblock
74
- end
75
-
76
- readers = []
77
- writers = []
78
- exceptions = [@sock]
79
- timeout = nil
80
-
81
- to_timeout = @pending_ops.select { |key, op| op.timeout? }
82
- to_timeout.each do |key, op|
83
- @pending_ops.delete(key)
84
- op.reject(Timeout::Error.new)
85
- op.execute_pool.complete(op)
86
- end
87
-
88
- unless @pending_ops.empty?
89
- # wait for writability if we have pending data to write
90
- writers << @sock if @write_buffer.bytesize > @write_offset
91
- # and always call back when there is data available to read
92
- readers << @sock
93
-
94
- # let all pending operations know that they are seeing the
95
- # select loop. this starts the timer for the operation, because
96
- # it guarantees we're now working on it.
97
- # this is more accurate than starting the timer when we buffer
98
- # the write.
99
- @pending_ops.each do |_, op|
100
- op.in_select_loop
101
- end
102
-
103
- # mark the amount of time left of the closest to timeout.
104
- timeout = @pending_ops.map { |_, op| op.timeout_remaining }.min
105
- end
106
-
107
- [readers, writers, exceptions, timeout]
108
- end
109
-
110
- private
111
-
112
- REQUEST = ::Dalli::Server::REQUEST
113
- OPCODES = ::Dalli::Server::OPCODES
114
- FORMAT = ::Dalli::Server::FORMAT
115
-
116
-
117
- def promised_request(key, &block)
118
- promise, opaque = new_pending(key)
119
- buffered_write(block.call(opaque))
120
- promise
121
- end
122
-
123
- def get(key, options = nil)
124
- return super unless async?
125
-
126
- promised_request(key) do |opaque|
127
- [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, opaque, 0, key].pack(FORMAT[:get])
128
- end
129
- end
130
-
131
- def generic_write_op(op, key, value, ttl, cas, options)
132
- Promise.resolve(value).then do |value|
133
- (value, flags) = serialize(key, value, options)
134
- ttl = sanitize_ttl(ttl)
135
-
136
- guard_max_value(key, value)
137
-
138
- promised_request(key) do |opaque|
139
- [REQUEST, OPCODES[op], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, opaque, cas, flags, ttl, key, value].pack(FORMAT[op])
140
- end
141
- end
142
- end
143
-
144
- def set(key, value, ttl, cas, options)
145
- return super unless async?
146
-
147
- generic_write_op(:set, key, value, ttl, cas, options)
148
- end
149
-
150
- def add(key, value, ttl, options)
151
- return super unless async?
152
-
153
- generic_write_op(:add, key, value, ttl, 0, options)
154
- end
155
-
156
- def replace(key, value, ttl, cas, options)
157
- return super unless async?
158
-
159
- generic_write_op(:replace, key, value, ttl, cas, options)
160
- end
161
-
162
- def delete(key, cas)
163
- return super unless async?
164
-
165
- promised_request(key) do |opaque|
166
- [REQUEST, OPCODES[:delete], key.bytesize, 0, 0, 0, key.bytesize, opaque, cas, key].pack(FORMAT[:delete])
167
- end
168
- end
169
-
170
- def append_prepend_op(op, key, value)
171
- promised_request(key) do |opaque|
172
- [REQUEST, OPCODES[op], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, opaque, 0, key, value].pack(FORMAT[op])
173
- end
174
- end
175
-
176
- def append(key, value)
177
- return super unless async?
178
-
179
- append_prepend_op(:append, key, value)
180
- end
181
-
182
- def prepend(key, value)
183
- return super unless async?
184
-
185
- append_prepend_op(:prepend, key, value)
186
- end
187
-
188
- def flush
189
- return super unless async?
190
-
191
- promised_request(nil) do |opaque|
192
- [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, opaque, 0, 0].pack(FORMAT[:flush])
193
- end
194
- end
195
-
196
- def decr_incr(opcode, key, count, ttl, default)
197
- expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
198
- default ||= 0
199
- (h, l) = split(count)
200
- (dh, dl) = split(default)
201
- promised_request(key) do |opaque|
202
- req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, opaque, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
203
- end
204
- end
205
-
206
- def decr(key, count, ttl, default)
207
- return super unless async?
208
-
209
- decr_incr :decr, key, count, ttl, default
210
- end
211
-
212
- def incr(key, count, ttl, default)
213
- return super unless async?
214
-
215
- decr_incr :incr, key, count, ttl, default
216
- end
217
-
218
- def new_pending(key)
219
- promise = ::IOPromise::Dalli::DalliPromise.new(self, key)
220
- new_id = @next_opaque_id
221
- @pending_ops[new_id] = promise
222
- @next_opaque_id = (@next_opaque_id + 1) & 0xffff_ffff
223
- [promise, new_id]
224
- end
225
-
226
- def buffered_write(data)
227
- @write_buffer << data
228
- sock_write_nonblock
229
- end
230
-
231
- def sock_write_nonblock
232
- begin
233
- bytes_written = @sock.write_nonblock(@write_buffer.byteslice(@write_offset..-1))
234
- rescue IO::WaitWritable, Errno::EINTR
235
- return # no room to write immediately
236
- end
237
-
238
- @write_offset += bytes_written
239
- if @write_offset == @write_buffer.length
240
- @write_buffer = +""
241
- @write_offset = 0
242
- end
243
- rescue SystemCallError, Timeout::Error => e
244
- failure!(e)
245
- end
246
-
247
- FULL_HEADER = 'CCnCCnNNQ'
248
-
249
- def sock_read_nonblock
250
- @read_buffer << @sock.read_available
251
-
252
- buf = @read_buffer
253
- pos = @read_offset
254
-
255
- while buf.bytesize - pos >= 24
256
- header = buf.slice(pos, 24)
257
- (magic, opcode, key_length, extra_length, data_type, status, body_length, opaque, cas) = header.unpack(FULL_HEADER)
258
-
259
- if buf.bytesize - pos >= 24 + body_length
260
- flags = 0
261
- if extra_length >= 4
262
- flags = buf.slice(pos + 24, 4).unpack1("N")
263
- end
264
-
265
- key = buf.slice(pos + 24 + extra_length, key_length)
266
- value = buf.slice(pos + 24 + extra_length + key_length, body_length - key_length - extra_length)
267
-
268
- pos = pos + 24 + body_length
269
-
270
- promise = @pending_ops.delete(opaque)
271
- next if promise.nil?
272
-
273
- result = Promise.resolve(true).then do # auto capture exceptions below
274
- raise Dalli::DalliError, "Response error #{status}: #{Dalli::RESPONSE_CODES[status]}" unless [0,1,2,5].include?(status)
275
-
276
- exists = (status != 1) # Key not found
277
- final_value = nil
278
- if opcode == OPCODES[:incr] || opcode == OPCODES[:decr]
279
- final_value = value.unpack1("Q>")
280
- elsif exists
281
- final_value = deserialize(value, flags)
282
- end
283
-
284
- ::IOPromise::Dalli::Response.new(
285
- key: promise.key,
286
- value: final_value,
287
- exists: exists,
288
- stored: !(status == 2 || status == 5), # Key exists or Item not stored
289
- cas: cas,
290
- )
291
- end
292
-
293
- promise.fulfill(result)
294
- promise.execute_pool.complete(promise)
295
- else
296
- # not enough data yet, wait for more
297
- break
298
- end
299
- end
300
-
301
- @read_offset = pos
302
-
303
- if @read_offset == @read_buffer.length
304
- @read_buffer = +""
305
- @read_offset = 0
306
- end
307
-
308
- rescue SystemCallError, Timeout::Error, EOFError => e
309
- failure!(e)
310
- end
311
-
312
- def failure!(ex)
313
- if async?
314
- # all pending operations need to be rejected when a failure occurs
315
- @pending_ops.each do |op|
316
- op.reject(ex)
317
- op.execute_pool.complete(op)
318
- end
319
- @pending_ops = {}
320
- end
321
-
322
- super
323
- end
324
-
325
- # FIXME: this is from the master version, rather than using the yield block.
326
- def guard_max_value(key, value)
327
- return if value.bytesize <= @options[:value_max_bytes]
328
-
329
- message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
330
- raise Dalli::ValueOverMaxSize, message
331
- end
332
- end
333
- end
334
- end
335
-
336
- ::Dalli::Server.prepend(IOPromise::Dalli::AsyncServer)
337
- ::Dalli::Client.prepend(IOPromise::Dalli::AsyncClient)