iopromise 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 323b4f0107580d674fa42c4f65644dcae302c822c9d370dd9e3effbf5b4c13eb
4
- data.tar.gz: dc8b3937fea0505e598c0855ccc695b98dc837e221b8959a61acfbf18838c3b5
3
+ metadata.gz: 3fc60525e45ec8cfa8a6cd5ba34bf396b77e4d42f3fd566137f14367a2ba31ae
4
+ data.tar.gz: 1e70b2147a204a220be6b268ae62f67dc06b79065b8ec07636da414264ae626b
5
5
  SHA512:
6
- metadata.gz: 8737b6c9a1e1ee2376f586ceca83a074679d6e0b53e073e07df8817f3f14fe261b606f5398618bcd984f2441b288b89fb9d660f499428d60c93cf35e7b561f7e
7
- data.tar.gz: c82ed8d1da207ce2bfa766b8a1fd88aa4ba803565b749f4c77e0cb90a5b8f9c0289f60111260daf4acc5b7ddaae43b0ddcadd91212201778516b1637f9db2cb6
6
+ metadata.gz: b92e71ab4fa32e5c45f9b4a4fa607844f2558f270f69ddb3a63b1394633a4b3e7eb101f77b06e8bc4efe45ad657e00a7d4bad10f34374356f2dc660db17af525
7
+ data.tar.gz: 4034ef0ae2010b4cea930894d1f7cc05722e6dd0e3c112fe237ce6488441d24d670c098df525046c13d26514ff39ee672c3a26c49f601ddc40b757fbbe691079
data/Gemfile CHANGED
@@ -23,4 +23,8 @@ group :development, :test do
23
23
  # view_component extensions
24
24
  gem "rails"
25
25
  gem "view_component", require: "view_component/engine"
26
+
27
+ # benchmarking
28
+ gem "benchmark-ips"
29
+ gem "stackprof"
26
30
  end
data/Gemfile.lock CHANGED
@@ -8,8 +8,9 @@ GIT
8
8
  PATH
9
9
  remote: .
10
10
  specs:
11
- iopromise (0.1.0)
12
- promise.rb
11
+ iopromise (0.1.1)
12
+ nio4r
13
+ promise.rb (~> 0.7.4)
13
14
 
14
15
  GEM
15
16
  remote: https://rubygems.org/
@@ -73,6 +74,7 @@ GEM
73
74
  minitest (>= 5.1)
74
75
  tzinfo (~> 2.0)
75
76
  zeitwerk (~> 2.3)
77
+ benchmark-ips (2.9.1)
76
78
  builder (3.2.4)
77
79
  concurrent-ruby (1.1.8)
78
80
  crass (1.0.6)
@@ -161,6 +163,7 @@ GEM
161
163
  actionpack (>= 4.0)
162
164
  activesupport (>= 4.0)
163
165
  sprockets (>= 3.0.0)
166
+ stackprof (0.2.17)
164
167
  thor (1.1.0)
165
168
  typhoeus (1.4.0)
166
169
  ethon (>= 0.9.0)
@@ -177,6 +180,7 @@ PLATFORMS
177
180
  x86_64-linux
178
181
 
179
182
  DEPENDENCIES
183
+ benchmark-ips
180
184
  dalli (= 2.7.11)
181
185
  faraday
182
186
  iopromise!
@@ -184,6 +188,7 @@ DEPENDENCIES
184
188
  rails
185
189
  rake (~> 13.0)
186
190
  rspec (~> 3.0)
191
+ stackprof
187
192
  typhoeus
188
193
  view_component
189
194
 
data/iopromise.gemspec CHANGED
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.add_dependency 'promise.rb', '~> 0.7.4'
30
+ spec.add_dependency 'nio4r'
30
31
  end
data/lib/iopromise.rb CHANGED
@@ -13,32 +13,31 @@ module IOPromise
13
13
  class Error < StandardError; end
14
14
 
15
15
  class Base < ::Promise
16
- def initialize(*)
17
- @instrument_begin = []
18
- @instrument_end = []
19
- @started_executing = false
20
-
21
- super
22
- end
23
-
24
16
  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?
17
+ raise ::IOPromise::Error.new("Instrumentation called after promise already started executing") if started_executing?
18
+ unless begin_cb.nil?
19
+ @instrument_begin ||= []
20
+ @instrument_begin << begin_cb
21
+ end
22
+ unless end_cb.nil?
23
+ @instrument_end ||= []
24
+ @instrument_end << end_cb
25
+ end
28
26
  end
29
27
 
30
28
  def beginning
31
- @instrument_begin.each { |cb| cb.call(self) }
29
+ @instrument_begin&.each { |cb| cb.call(self) }
30
+ @instrument_begin&.clear
32
31
  @started_executing = true
33
32
  end
34
33
 
35
34
  def started_executing?
36
- @started_executing
35
+ !!@started_executing
37
36
  end
38
37
 
39
38
  def notify_completion(value: nil, reason: nil)
40
- @instrument_end.each { |cb| cb.call(self, value: value, reason: reason) }
41
- @instrument_end = []
39
+ @instrument_end&.each { |cb| cb.call(self, value: value, reason: reason) }
40
+ @instrument_end&.clear
42
41
  end
43
42
 
44
43
  def fulfill(value)
@@ -21,7 +21,7 @@ module IOPromise
21
21
  # Returns a promise that resolves to a IOPromise::Dalli::Response with the
22
22
  # value for the given key, or +nil+ if the key is not found.
23
23
  def get(key, options = nil)
24
- execute_as_promise(:get, key, options)
24
+ @client.perform(:get, key, options)
25
25
  end
26
26
 
27
27
  # Convenience function that attempts to fetch the given key, or set
@@ -40,7 +40,7 @@ module IOPromise
40
40
  !response.exist? :
41
41
  response.value.nil?
42
42
  if not_found && !block.nil?
43
- Promise.resolve(block.call).then do |new_val|
43
+ block.call.then do |new_val|
44
44
  # delay the final resolution here until after the add succeeds,
45
45
  # to guarantee errors are caught. we could potentially allow
46
46
  # the add to resolve once it's sent (without confirmation), but
@@ -48,7 +48,7 @@ module IOPromise
48
48
  add(key, new_val, ttl, options).then { new_val }
49
49
  end
50
50
  else
51
- Promise.resolve(response.value)
51
+ response.value
52
52
  end
53
53
  end
54
54
  end
@@ -56,40 +56,40 @@ module IOPromise
56
56
  # Unconditionally sets the +key+ to the +value+ specified.
57
57
  # Returns a promise that resolves to a IOPromise::Dalli::Response.
58
58
  def set(key, value, ttl = nil, options = nil)
59
- execute_as_promise(:set, key, value, ttl_or_default(ttl), 0, options)
59
+ @client.perform(:set, key, value, ttl_or_default(ttl), 0, options)
60
60
  end
61
61
 
62
62
  # Conditionally sets the +key+ to the +value+ specified.
63
63
  # Returns a promise that resolves to a IOPromise::Dalli::Response.
64
64
  def add(key, value, ttl = nil, options = nil)
65
- execute_as_promise(:add, key, value, ttl_or_default(ttl), options)
65
+ @client.perform(:add, key, value, ttl_or_default(ttl), options)
66
66
  end
67
67
 
68
68
  # Conditionally sets the +key+ to the +value+ specified only
69
69
  # if the key already exists.
70
70
  # Returns a promise that resolves to a IOPromise::Dalli::Response.
71
71
  def replace(key, value, ttl = nil, options = nil)
72
- execute_as_promise(:replace, key, value, ttl_or_default(ttl), 0, options)
72
+ @client.perform(:replace, key, value, ttl_or_default(ttl), 0, options)
73
73
  end
74
74
 
75
75
  # Deletes the specified key, resolving the promise when complete.
76
76
  def delete(key)
77
- execute_as_promise(:delete, key, 0)
77
+ @client.perform(:delete, key, 0)
78
78
  end
79
79
 
80
80
  # Appends a value to the specified key, resolving the promise when complete.
81
81
  # Appending only works for values stored with :raw => true.
82
82
  def append(key, value)
83
- Promise.resolve(value).then do |resolved_value|
84
- execute_as_promise(:append, key, resolved_value.to_s)
83
+ value.then do |resolved_value|
84
+ @client.perform(:append, key, resolved_value.to_s)
85
85
  end
86
86
  end
87
87
 
88
88
  # Prepend a value to the specified key, resolving the promise when complete.
89
89
  # Prepending only works for values stored with :raw => true.
90
90
  def prepend(key, value)
91
- Promise.resolve(value).then do |resolved_value|
92
- execute_as_promise(:prepend, key, resolved_value.to_s)
91
+ value.then do |resolved_value|
92
+ @client.perform(:prepend, key, resolved_value.to_s)
93
93
  end
94
94
  end
95
95
 
@@ -106,7 +106,7 @@ module IOPromise
106
106
  # #cas.
107
107
  def incr(key, amt = 1, ttl = nil, default = nil)
108
108
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
109
- execute_as_promise(:incr, key, amt.to_i, ttl_or_default(ttl), default)
109
+ @client.perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
110
110
  end
111
111
 
112
112
  ##
@@ -125,17 +125,13 @@ module IOPromise
125
125
  # #cas.
126
126
  def decr(key, amt = 1, ttl = nil, default = nil)
127
127
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
128
- execute_as_promise(:decr, key, amt.to_i, ttl_or_default(ttl), default)
128
+ @client.perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
129
129
  end
130
130
 
131
131
  # TODO: touch, gat, CAS operations
132
132
 
133
133
  private
134
134
 
135
- def execute_as_promise(*args)
136
- @client.perform_async(*args)
137
- end
138
-
139
135
  def ttl_or_default(ttl)
140
136
  (ttl || @options[:expires_in]).to_i
141
137
  rescue NoMethodError
@@ -3,10 +3,43 @@
3
3
  module IOPromise
4
4
  module Dalli
5
5
  class DalliExecutorPool < IOPromise::ExecutorPool::Base
6
- def execute_continue(ready_readers, ready_writers, ready_exceptions)
7
- dalli_server = @connection_pool
6
+ def initialize(*)
7
+ super
8
8
 
9
- dalli_server.execute_continue(ready_readers, ready_writers, ready_exceptions)
9
+ @iop_monitor = nil
10
+ end
11
+
12
+ def dalli_server
13
+ @connection_pool
14
+ end
15
+
16
+ def execute_continue
17
+ dalli_server.execute_continue
18
+ end
19
+
20
+ def connected_socket(sock)
21
+ close_socket
22
+
23
+ @iop_monitor = ::IOPromise::ExecutorContext.current.register_observer_io(self, sock, :r)
24
+ end
25
+
26
+ def close_socket
27
+ unless @iop_monitor.nil?
28
+ @iop_monitor.close
29
+ @iop_monitor = nil
30
+ end
31
+ end
32
+
33
+ def monitor_ready(monitor, readiness)
34
+ dalli_server.async_io_ready(monitor.readable?, monitor.writable?)
35
+ end
36
+
37
+ def set_interest(direction, interested)
38
+ if interested
39
+ @iop_monitor.add_interest(direction)
40
+ else
41
+ @iop_monitor.remove_interest(direction)
42
+ end
10
43
  end
11
44
  end
12
45
  end
@@ -12,16 +12,16 @@ module IOPromise
12
12
  super
13
13
  end
14
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."
15
+ def perform(*)
16
+ return super unless @async
17
+
18
+ begin
19
+ super
20
+ rescue => ex
21
+ # Wrap any connection errors into a promise, this is more forwards-compatible
22
+ # if we ever attempt to make connecting/server fallback nonblocking too.
23
+ Promise.new.tap { |p| p.reject(ex) }
20
24
  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
25
  end
26
26
  end
27
27
 
@@ -30,10 +30,14 @@ module IOPromise
30
30
  @async = options.delete(:iopromise_async) == true
31
31
 
32
32
  if @async
33
+ @write_buffer = +""
34
+ @read_buffer = +""
33
35
  async_reset
34
36
 
35
37
  @next_opaque_id = 0
36
38
  @pending_ops = {}
39
+
40
+ @executor_pool = DalliExecutorPool.for(self)
37
41
  end
38
42
 
39
43
  super
@@ -51,60 +55,53 @@ module IOPromise
51
55
  super
52
56
  end
53
57
 
54
- def async_reset
55
- @write_buffer = +""
56
- @write_offset = 0
57
-
58
- @read_buffer = +""
59
- @read_offset = 0
60
- end
58
+ def connect
59
+ super
61
60
 
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
61
+ if async?
62
+ @executor_pool.connected_socket(@sock)
67
63
  end
64
+ end
68
65
 
69
- readers_empty = ready_readers.nil? || ready_readers.empty?
70
- exceptions_empty = ready_exceptions.nil? || ready_exceptions.empty?
66
+ def async_reset
67
+ @write_buffer.clear
68
+ @write_offset = 0
71
69
 
72
- if !readers_empty || !exceptions_empty
73
- sock_read_nonblock
74
- end
70
+ @read_buffer.clear
71
+ @read_offset = 0
75
72
 
76
- readers = []
77
- writers = []
78
- exceptions = [@sock]
79
- timeout = nil
73
+ @executor_pool.close_socket if defined? @executor_pool
74
+ end
80
75
 
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
76
+ def async_io_ready(readable, writable)
77
+ async_sock_write_nonblock if writable
78
+ async_sock_read_nonblock if readable
79
+ end
87
80
 
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
81
+ # called by ExecutorPool to continue processing for this server
82
+ def execute_continue
83
+ timeout = @options[:socket_timeout]
84
+ @pending_ops.select! do |key, op|
85
+ if op.timeout?
86
+ op.reject(Timeout::Error.new)
87
+ next false # this op is done
88
+ end
93
89
 
94
90
  # let all pending operations know that they are seeing the
95
91
  # select loop. this starts the timer for the operation, because
96
92
  # it guarantees we're now working on it.
97
93
  # this is more accurate than starting the timer when we buffer
98
94
  # the write.
99
- @pending_ops.each do |_, op|
100
- op.in_select_loop
101
- end
95
+ op.in_select_loop
102
96
 
103
- # mark the amount of time left of the closest to timeout.
104
- timeout = @pending_ops.map { |_, op| op.timeout_remaining }.min
97
+ remaining = op.timeout_remaining
98
+ timeout = remaining if remaining < timeout
99
+
100
+ true # keep
105
101
  end
106
102
 
107
- [readers, writers, exceptions, timeout]
103
+ @executor_pool.select_timeout = timeout
104
+ @executor_pool.set_interest(:r, !@pending_ops.empty?)
108
105
  end
109
106
 
110
107
  private
@@ -115,8 +112,14 @@ module IOPromise
115
112
 
116
113
 
117
114
  def promised_request(key, &block)
118
- promise, opaque = new_pending(key)
119
- buffered_write(block.call(opaque))
115
+ promise = ::IOPromise::Dalli::DalliPromise.new(self, key)
116
+
117
+ new_id = @next_opaque_id
118
+ @pending_ops[new_id] = promise
119
+ @next_opaque_id = (@next_opaque_id + 1) & 0xffff_ffff
120
+
121
+ async_buffered_write(block.call(new_id))
122
+
120
123
  promise
121
124
  end
122
125
 
@@ -128,12 +131,12 @@ module IOPromise
128
131
  end
129
132
  end
130
133
 
131
- def generic_write_op(op, key, value, ttl, cas, options)
132
- Promise.resolve(value).then do |value|
134
+ def async_generic_write_op(op, key, value, ttl, cas, options)
135
+ value.then do |value|
133
136
  (value, flags) = serialize(key, value, options)
134
137
  ttl = sanitize_ttl(ttl)
135
138
 
136
- guard_max_value(key, value)
139
+ guard_max_value_with_raise(key, value)
137
140
 
138
141
  promised_request(key) do |opaque|
139
142
  [REQUEST, OPCODES[op], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, opaque, cas, flags, ttl, key, value].pack(FORMAT[op])
@@ -143,20 +146,19 @@ module IOPromise
143
146
 
144
147
  def set(key, value, ttl, cas, options)
145
148
  return super unless async?
146
-
147
- generic_write_op(:set, key, value, ttl, cas, options)
149
+ async_generic_write_op(:set, key, value, ttl, cas, options)
148
150
  end
149
151
 
150
152
  def add(key, value, ttl, options)
151
153
  return super unless async?
152
154
 
153
- generic_write_op(:add, key, value, ttl, 0, options)
155
+ async_generic_write_op(:add, key, value, ttl, 0, options)
154
156
  end
155
157
 
156
158
  def replace(key, value, ttl, cas, options)
157
159
  return super unless async?
158
160
 
159
- generic_write_op(:replace, key, value, ttl, cas, options)
161
+ async_generic_write_op(:replace, key, value, ttl, cas, options)
160
162
  end
161
163
 
162
164
  def delete(key, cas)
@@ -167,7 +169,7 @@ module IOPromise
167
169
  end
168
170
  end
169
171
 
170
- def append_prepend_op(op, key, value)
172
+ def async_append_prepend_op(op, key, value)
171
173
  promised_request(key) do |opaque|
172
174
  [REQUEST, OPCODES[op], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, opaque, 0, key, value].pack(FORMAT[op])
173
175
  end
@@ -176,13 +178,13 @@ module IOPromise
176
178
  def append(key, value)
177
179
  return super unless async?
178
180
 
179
- append_prepend_op(:append, key, value)
181
+ async_append_prepend_op(:append, key, value)
180
182
  end
181
183
 
182
184
  def prepend(key, value)
183
185
  return super unless async?
184
186
 
185
- append_prepend_op(:prepend, key, value)
187
+ async_append_prepend_op(:prepend, key, value)
186
188
  end
187
189
 
188
190
  def flush
@@ -193,7 +195,7 @@ module IOPromise
193
195
  end
194
196
  end
195
197
 
196
- def decr_incr(opcode, key, count, ttl, default)
198
+ def async_decr_incr(opcode, key, count, ttl, default)
197
199
  expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
198
200
  default ||= 0
199
201
  (h, l) = split(count)
@@ -206,103 +208,118 @@ module IOPromise
206
208
  def decr(key, count, ttl, default)
207
209
  return super unless async?
208
210
 
209
- decr_incr :decr, key, count, ttl, default
211
+ async_decr_incr :decr, key, count, ttl, default
210
212
  end
211
213
 
212
214
  def incr(key, count, ttl, default)
213
215
  return super unless async?
214
216
 
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]
217
+ async_decr_incr :incr, key, count, ttl, default
224
218
  end
225
219
 
226
- def buffered_write(data)
220
+ def async_buffered_write(data)
227
221
  @write_buffer << data
228
- sock_write_nonblock
222
+ async_sock_write_nonblock
229
223
  end
230
224
 
231
- def sock_write_nonblock
225
+ def async_sock_write_nonblock
226
+ remaining = @write_buffer.byteslice(@write_offset, @write_buffer.length)
232
227
  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
228
+ bytes_written = @sock.write_nonblock(remaining, exception: false)
229
+ rescue Errno::EINTR
230
+ retry
236
231
  end
232
+
233
+ return if bytes_written == :wait_writable
237
234
 
238
235
  @write_offset += bytes_written
239
- if @write_offset == @write_buffer.length
240
- @write_buffer = +""
236
+ completed = (@write_offset == @write_buffer.length)
237
+ if completed
238
+ @write_buffer.clear
241
239
  @write_offset = 0
242
240
  end
241
+ @executor_pool.set_interest(:w, !completed)
243
242
  rescue SystemCallError, Timeout::Error => e
244
243
  failure!(e)
245
244
  end
246
245
 
247
246
  FULL_HEADER = 'CCnCCnNNQ'
248
247
 
249
- def sock_read_nonblock
250
- @read_buffer << @sock.read_available
248
+ def read_available
249
+ loop do
250
+ result = @sock.read_nonblock(8196, exception: false)
251
+ if result == :wait_readable
252
+ break
253
+ elsif result == :wait_writable
254
+ break
255
+ elsif result
256
+ @read_buffer << result
257
+ else
258
+ raise Errno::ECONNRESET, "Connection reset: #{safe_options.inspect}"
259
+ end
260
+ end
261
+ end
262
+
263
+ def async_sock_read_nonblock
264
+ read_available
251
265
 
252
266
  buf = @read_buffer
253
267
  pos = @read_offset
254
268
 
255
269
  while buf.bytesize - pos >= 24
256
- header = buf.slice(pos, 24)
270
+ header = buf.byteslice(pos, 24)
257
271
  (magic, opcode, key_length, extra_length, data_type, status, body_length, opaque, cas) = header.unpack(FULL_HEADER)
258
272
 
259
273
  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
274
+ exists = (status != 1) # Key not found
275
+ this_pos = pos
264
276
 
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)
277
+ # key = buf.byteslice(this_pos + 24 + extra_length, key_length)
278
+ value = buf.byteslice(this_pos + 24 + extra_length + key_length, body_length - key_length - extra_length) if exists
267
279
 
268
280
  pos = pos + 24 + body_length
269
281
 
270
282
  promise = @pending_ops.delete(opaque)
271
283
  next if promise.nil?
272
284
 
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
285
+ begin
286
+ raise Dalli::DalliError, "Response error #{status}: #{Dalli::RESPONSE_CODES[status]}" unless status == 0 || status == 1 || status == 2 || status == 5
287
+
277
288
  final_value = nil
278
289
  if opcode == OPCODES[:incr] || opcode == OPCODES[:decr]
279
290
  final_value = value.unpack1("Q>")
280
291
  elsif exists
292
+ flags = if extra_length >= 4
293
+ buf.byteslice(this_pos + 24, 4).unpack1("N")
294
+ else
295
+ 0
296
+ end
281
297
  final_value = deserialize(value, flags)
282
298
  end
283
299
 
284
- ::IOPromise::Dalli::Response.new(
300
+ response = ::IOPromise::Dalli::Response.new(
285
301
  key: promise.key,
286
302
  value: final_value,
287
303
  exists: exists,
288
304
  stored: !(status == 2 || status == 5), # Key exists or Item not stored
289
305
  cas: cas,
290
306
  )
291
- end
292
307
 
293
- promise.fulfill(result)
294
- promise.execute_pool.complete(promise)
308
+ promise.fulfill(response)
309
+ rescue => ex
310
+ promise.reject(ex)
311
+ end
295
312
  else
296
313
  # not enough data yet, wait for more
297
314
  break
298
315
  end
299
316
  end
300
317
 
301
- @read_offset = pos
302
-
303
- if @read_offset == @read_buffer.length
304
- @read_buffer = +""
318
+ if pos == @read_buffer.length
319
+ @read_buffer.clear
305
320
  @read_offset = 0
321
+ else
322
+ @read_offset = pos
306
323
  end
307
324
 
308
325
  rescue SystemCallError, Timeout::Error, EOFError => e
@@ -314,7 +331,6 @@ module IOPromise
314
331
  # all pending operations need to be rejected when a failure occurs
315
332
  @pending_ops.each do |op|
316
333
  op.reject(ex)
317
- op.execute_pool.complete(op)
318
334
  end
319
335
  @pending_ops = {}
320
336
  end
@@ -322,8 +338,8 @@ module IOPromise
322
338
  super
323
339
  end
324
340
 
325
- # FIXME: this is from the master version, rather than using the yield block.
326
- def guard_max_value(key, value)
341
+ # this is guard_max_value from the master version, rather than using the yield block.
342
+ def guard_max_value_with_raise(key, value)
327
343
  return if value.bytesize <= @options[:value_max_bytes]
328
344
 
329
345
  message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"