iopromise 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)