iopromise 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +21 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +26 -0
  7. data/Gemfile.lock +191 -0
  8. data/LICENSE +21 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +41 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +9 -0
  14. data/iopromise.gemspec +30 -0
  15. data/lib/iopromise.rb +54 -0
  16. data/lib/iopromise/dalli.rb +13 -0
  17. data/lib/iopromise/dalli/client.rb +146 -0
  18. data/lib/iopromise/dalli/executor_pool.rb +13 -0
  19. data/lib/iopromise/dalli/patch_dalli.rb +337 -0
  20. data/lib/iopromise/dalli/promise.rb +52 -0
  21. data/lib/iopromise/dalli/response.rb +25 -0
  22. data/lib/iopromise/deferred.rb +13 -0
  23. data/lib/iopromise/deferred/executor_pool.rb +29 -0
  24. data/lib/iopromise/deferred/promise.rb +38 -0
  25. data/lib/iopromise/executor_context.rb +114 -0
  26. data/lib/iopromise/executor_pool/base.rb +47 -0
  27. data/lib/iopromise/executor_pool/batch.rb +23 -0
  28. data/lib/iopromise/executor_pool/sequential.rb +32 -0
  29. data/lib/iopromise/faraday.rb +17 -0
  30. data/lib/iopromise/faraday/connection.rb +25 -0
  31. data/lib/iopromise/faraday/continuable_hydra.rb +29 -0
  32. data/lib/iopromise/faraday/executor_pool.rb +19 -0
  33. data/lib/iopromise/faraday/multi_socket_action.rb +107 -0
  34. data/lib/iopromise/faraday/promise.rb +42 -0
  35. data/lib/iopromise/memcached.rb +13 -0
  36. data/lib/iopromise/memcached/client.rb +22 -0
  37. data/lib/iopromise/memcached/executor_pool.rb +61 -0
  38. data/lib/iopromise/memcached/promise.rb +32 -0
  39. data/lib/iopromise/rack/context_middleware.rb +20 -0
  40. data/lib/iopromise/version.rb +5 -0
  41. data/lib/iopromise/view_component.rb +9 -0
  42. data/lib/iopromise/view_component/data_loader.rb +62 -0
  43. metadata +101 -0
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "iopromise"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # bring up a memcached for testing
9
+ docker run -d -p 11211:11211 memcached:alpine
data/iopromise.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/iopromise/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "iopromise"
7
+ spec.version = IOPromise::VERSION
8
+ spec.authors = ["Theo Julienne"]
9
+ spec.email = ["theo.julienne@gmail.com"]
10
+
11
+ spec.summary = "Simple non-blocking IO promises for Ruby."
12
+ spec.description = "This gem extends promise.rb promises to support an extremely simple pattern for \"continuing\" execution of the promise in an asynchronous non-blocking way."
13
+ spec.homepage = "https://github.com/theojulienne/iopromise"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency 'promise.rb', '~> 0.7.4'
30
+ end
data/lib/iopromise.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "promise"
4
+
5
+ require_relative "iopromise/version"
6
+
7
+ require_relative "iopromise/executor_context"
8
+ require_relative "iopromise/executor_pool/base"
9
+ require_relative "iopromise/executor_pool/batch"
10
+ require_relative "iopromise/executor_pool/sequential"
11
+
12
+ module IOPromise
13
+ class Error < StandardError; end
14
+
15
+ class Base < ::Promise
16
+ def initialize(*)
17
+ @instrument_begin = []
18
+ @instrument_end = []
19
+ @started_executing = false
20
+
21
+ super
22
+ end
23
+
24
+ def instrument(begin_cb = nil, end_cb = nil)
25
+ raise ::IOPromise::Error.new("Instrumentation called after promise already started executing") if @started_executing
26
+ @instrument_begin << begin_cb unless begin_cb.nil?
27
+ @instrument_end << end_cb unless end_cb.nil?
28
+ end
29
+
30
+ def beginning
31
+ @instrument_begin.each { |cb| cb.call(self) }
32
+ @started_executing = true
33
+ end
34
+
35
+ def started_executing?
36
+ @started_executing
37
+ end
38
+
39
+ def notify_completion
40
+ @instrument_end.each { |cb| cb.call(self) }
41
+ @instrument_end = []
42
+ end
43
+
44
+ def fulfill(value)
45
+ notify_completion
46
+ super(value)
47
+ end
48
+
49
+ def reject(reason)
50
+ notify_completion
51
+ super(reason)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,146 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,337 @@
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)