iopromise 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +21 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +191 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +9 -0
- data/iopromise.gemspec +30 -0
- data/lib/iopromise.rb +54 -0
- data/lib/iopromise/dalli.rb +13 -0
- data/lib/iopromise/dalli/client.rb +146 -0
- data/lib/iopromise/dalli/executor_pool.rb +13 -0
- data/lib/iopromise/dalli/patch_dalli.rb +337 -0
- data/lib/iopromise/dalli/promise.rb +52 -0
- data/lib/iopromise/dalli/response.rb +25 -0
- data/lib/iopromise/deferred.rb +13 -0
- data/lib/iopromise/deferred/executor_pool.rb +29 -0
- data/lib/iopromise/deferred/promise.rb +38 -0
- data/lib/iopromise/executor_context.rb +114 -0
- data/lib/iopromise/executor_pool/base.rb +47 -0
- data/lib/iopromise/executor_pool/batch.rb +23 -0
- data/lib/iopromise/executor_pool/sequential.rb +32 -0
- data/lib/iopromise/faraday.rb +17 -0
- data/lib/iopromise/faraday/connection.rb +25 -0
- data/lib/iopromise/faraday/continuable_hydra.rb +29 -0
- data/lib/iopromise/faraday/executor_pool.rb +19 -0
- data/lib/iopromise/faraday/multi_socket_action.rb +107 -0
- data/lib/iopromise/faraday/promise.rb +42 -0
- data/lib/iopromise/memcached.rb +13 -0
- data/lib/iopromise/memcached/client.rb +22 -0
- data/lib/iopromise/memcached/executor_pool.rb +61 -0
- data/lib/iopromise/memcached/promise.rb +32 -0
- data/lib/iopromise/rack/context_middleware.rb +20 -0
- data/lib/iopromise/version.rb +5 -0
- data/lib/iopromise/view_component.rb +9 -0
- data/lib/iopromise/view_component/data_loader.rb +62 -0
- 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
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,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)
|