iopromise 0.1.2 → 0.1.3
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 +4 -4
- data/.github/workflows/main.yml +0 -1
- data/Gemfile +0 -10
- data/Gemfile.lock +1 -29
- data/README.md +22 -3
- data/bin/setup +0 -3
- data/lib/iopromise/version.rb +1 -1
- metadata +1 -11
- data/lib/iopromise/dalli.rb +0 -13
- data/lib/iopromise/dalli/client.rb +0 -142
- data/lib/iopromise/dalli/executor_pool.rb +0 -46
- data/lib/iopromise/dalli/patch_dalli.rb +0 -353
- data/lib/iopromise/dalli/promise.rb +0 -60
- data/lib/iopromise/dalli/response.rb +0 -25
- data/lib/iopromise/memcached.rb +0 -13
- data/lib/iopromise/memcached/client.rb +0 -22
- data/lib/iopromise/memcached/executor_pool.rb +0 -82
- data/lib/iopromise/memcached/promise.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f58c2a28347c1f21fe6c660eff9f2b7ce1b078b1a06a37c91de0d6ec05db1c66
|
4
|
+
data.tar.gz: f5b2f6f66c5f79bb3f68f8cd041f6fecdde33dba2700b3f474ae9a76995378c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '09e91e024b854c41dd60942dc76af20795ca88807f3d6548bbf7d96982babd4c618e7054f9e63c640cca3eb4add0d08c347d8bb7a5ac5357c7969645817562c9'
|
7
|
+
data.tar.gz: d44281e24ab187944c15da2ceae76a1b8be8d8b54dd6a117f2783aec3418e12231283cfe3adc548d8d4d78536d5712c493126c0618f820f541e6142d33cc27dc
|
data/.github/workflows/main.yml
CHANGED
data/Gemfile
CHANGED
@@ -10,16 +10,6 @@ gem "rake", "~> 13.0"
|
|
10
10
|
gem "rspec", "~> 3.0"
|
11
11
|
|
12
12
|
group :development, :test do
|
13
|
-
# faraday adapter
|
14
|
-
gem 'faraday'
|
15
|
-
gem 'typhoeus'
|
16
|
-
|
17
|
-
# memcached adapter
|
18
|
-
gem 'memcached', :git => 'https://github.com/theojulienne/memcached.git', :branch => 'continuable-get'
|
19
|
-
|
20
|
-
# dalli adapter
|
21
|
-
gem 'dalli', "= 2.7.11"
|
22
|
-
|
23
13
|
# view_component extensions
|
24
14
|
gem "rails"
|
25
15
|
gem "view_component", require: "view_component/engine"
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,7 @@
|
|
1
|
-
GIT
|
2
|
-
remote: https://github.com/theojulienne/memcached.git
|
3
|
-
revision: 18c1da3708f3e7dca316b2f0143b4f05116f7672
|
4
|
-
branch: continuable-get
|
5
|
-
specs:
|
6
|
-
memcached (2.0.0.alpha)
|
7
|
-
|
8
1
|
PATH
|
9
2
|
remote: .
|
10
3
|
specs:
|
11
|
-
iopromise (0.1.
|
4
|
+
iopromise (0.1.3)
|
12
5
|
nio4r
|
13
6
|
promise.rb (~> 0.7.4)
|
14
7
|
|
@@ -78,21 +71,8 @@ GEM
|
|
78
71
|
builder (3.2.4)
|
79
72
|
concurrent-ruby (1.1.8)
|
80
73
|
crass (1.0.6)
|
81
|
-
dalli (2.7.11)
|
82
74
|
diff-lcs (1.4.4)
|
83
75
|
erubi (1.10.0)
|
84
|
-
ethon (0.14.0)
|
85
|
-
ffi (>= 1.15.0)
|
86
|
-
faraday (1.4.1)
|
87
|
-
faraday-excon (~> 1.1)
|
88
|
-
faraday-net_http (~> 1.0)
|
89
|
-
faraday-net_http_persistent (~> 1.1)
|
90
|
-
multipart-post (>= 1.2, < 3)
|
91
|
-
ruby2_keywords (>= 0.0.4)
|
92
|
-
faraday-excon (1.1.0)
|
93
|
-
faraday-net_http (1.0.1)
|
94
|
-
faraday-net_http_persistent (1.1.0)
|
95
|
-
ffi (1.15.0)
|
96
76
|
globalid (0.4.2)
|
97
77
|
activesupport (>= 4.2.0)
|
98
78
|
i18n (1.8.10)
|
@@ -106,7 +86,6 @@ GEM
|
|
106
86
|
method_source (1.0.0)
|
107
87
|
mini_mime (1.0.3)
|
108
88
|
minitest (5.14.4)
|
109
|
-
multipart-post (2.1.1)
|
110
89
|
nio4r (2.5.7)
|
111
90
|
nokogiri (1.11.3-x86_64-linux)
|
112
91
|
racc (~> 1.4)
|
@@ -155,7 +134,6 @@ GEM
|
|
155
134
|
diff-lcs (>= 1.2.0, < 2.0)
|
156
135
|
rspec-support (~> 3.10.0)
|
157
136
|
rspec-support (3.10.2)
|
158
|
-
ruby2_keywords (0.0.4)
|
159
137
|
sprockets (4.0.2)
|
160
138
|
concurrent-ruby (~> 1.0)
|
161
139
|
rack (> 1, < 3)
|
@@ -165,8 +143,6 @@ GEM
|
|
165
143
|
sprockets (>= 3.0.0)
|
166
144
|
stackprof (0.2.17)
|
167
145
|
thor (1.1.0)
|
168
|
-
typhoeus (1.4.0)
|
169
|
-
ethon (>= 0.9.0)
|
170
146
|
tzinfo (2.0.4)
|
171
147
|
concurrent-ruby (~> 1.0)
|
172
148
|
view_component (2.31.1)
|
@@ -181,15 +157,11 @@ PLATFORMS
|
|
181
157
|
|
182
158
|
DEPENDENCIES
|
183
159
|
benchmark-ips
|
184
|
-
dalli (= 2.7.11)
|
185
|
-
faraday
|
186
160
|
iopromise!
|
187
|
-
memcached!
|
188
161
|
rails
|
189
162
|
rake (~> 13.0)
|
190
163
|
rspec (~> 3.0)
|
191
164
|
stackprof
|
192
|
-
typhoeus
|
193
165
|
view_component
|
194
166
|
|
195
167
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
|
-
#
|
1
|
+
# IOPromise
|
2
2
|
|
3
|
-
|
3
|
+
IOPromise is a pattern that allows parallel execution of IO-bound requests (data store and RPCs) behind the abstraction of promises, without needing to introduce the complexity of threading. It uses [promise.rb](https://github.com/lgierth/promise.rb) for promises, and [nio4r](https://github.com/socketry/nio4r) to implement the IO loop.
|
4
|
+
|
5
|
+
A simple example of this behaviour is using [iopromise-faraday](https://github.com/iopromise-ruby/iopromise-faraday) to perform concurrent HTTP requests:
|
6
|
+
```ruby
|
7
|
+
require 'iopromise/faraday'
|
8
|
+
|
9
|
+
conn = IOPromise::Faraday.new('https://github.com/')
|
10
|
+
|
11
|
+
promises = (1..3).map do
|
12
|
+
conn.get('/status')
|
13
|
+
end
|
14
|
+
|
15
|
+
Promise.all(promises).then do |responses|
|
16
|
+
responses.each_with_index do |response, i|
|
17
|
+
puts "#{i}: #{response.body.strip} #{response.headers["x-github-request-id"]}"
|
18
|
+
end
|
19
|
+
end.sync
|
20
|
+
```
|
4
21
|
|
5
22
|
## Installation
|
6
23
|
|
@@ -20,7 +37,9 @@ Or install it yourself as:
|
|
20
37
|
|
21
38
|
## Usage
|
22
39
|
|
23
|
-
|
40
|
+
IOPromise itself is a base library that makes it easy to wrap other IO-based workloads inside a promise-based API that back to an event loop. To use IOPromise, look at the following gems:
|
41
|
+
|
42
|
+
* [iopromise-faraday](https://github.com/iopromise-ruby/iopromise-faraday) supports [faraday](https://github.com/lostisland/faraday) HTTP requests, backed by libcurl/ethon/typhoeus.
|
24
43
|
|
25
44
|
## Development
|
26
45
|
|
data/bin/setup
CHANGED
data/lib/iopromise/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iopromise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Theo Julienne
|
@@ -60,12 +60,6 @@ files:
|
|
60
60
|
- bin/setup
|
61
61
|
- iopromise.gemspec
|
62
62
|
- lib/iopromise.rb
|
63
|
-
- lib/iopromise/dalli.rb
|
64
|
-
- lib/iopromise/dalli/client.rb
|
65
|
-
- lib/iopromise/dalli/executor_pool.rb
|
66
|
-
- lib/iopromise/dalli/patch_dalli.rb
|
67
|
-
- lib/iopromise/dalli/promise.rb
|
68
|
-
- lib/iopromise/dalli/response.rb
|
69
63
|
- lib/iopromise/deferred.rb
|
70
64
|
- lib/iopromise/deferred/executor_pool.rb
|
71
65
|
- lib/iopromise/deferred/promise.rb
|
@@ -73,10 +67,6 @@ files:
|
|
73
67
|
- lib/iopromise/executor_pool/base.rb
|
74
68
|
- lib/iopromise/executor_pool/batch.rb
|
75
69
|
- lib/iopromise/executor_pool/sequential.rb
|
76
|
-
- lib/iopromise/memcached.rb
|
77
|
-
- lib/iopromise/memcached/client.rb
|
78
|
-
- lib/iopromise/memcached/executor_pool.rb
|
79
|
-
- lib/iopromise/memcached/promise.rb
|
80
70
|
- lib/iopromise/version.rb
|
81
71
|
- lib/iopromise/view_component.rb
|
82
72
|
- lib/iopromise/view_component/data_loader.rb
|
data/lib/iopromise/dalli.rb
DELETED
@@ -1,142 +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
|
-
@client.perform(: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
|
-
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
|
-
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
|
-
@client.perform(: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
|
-
@client.perform(: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
|
-
@client.perform(: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
|
-
@client.perform(: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
|
-
value.then do |resolved_value|
|
84
|
-
@client.perform(: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
|
-
value.then do |resolved_value|
|
92
|
-
@client.perform(: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
|
-
@client.perform(: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
|
-
@client.perform(: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 ttl_or_default(ttl)
|
136
|
-
(ttl || @options[:expires_in]).to_i
|
137
|
-
rescue NoMethodError
|
138
|
-
raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module IOPromise
|
4
|
-
module Dalli
|
5
|
-
class DalliExecutorPool < IOPromise::ExecutorPool::Base
|
6
|
-
def initialize(*)
|
7
|
-
super
|
8
|
-
|
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
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,353 +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(*)
|
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) }
|
24
|
-
end
|
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
|
-
@write_buffer = +""
|
34
|
-
@read_buffer = +""
|
35
|
-
async_reset
|
36
|
-
|
37
|
-
@next_opaque_id = 0
|
38
|
-
@pending_ops = {}
|
39
|
-
|
40
|
-
@executor_pool = DalliExecutorPool.for(self)
|
41
|
-
end
|
42
|
-
|
43
|
-
super
|
44
|
-
end
|
45
|
-
|
46
|
-
def async?
|
47
|
-
@async
|
48
|
-
end
|
49
|
-
|
50
|
-
def close
|
51
|
-
if async?
|
52
|
-
async_reset
|
53
|
-
end
|
54
|
-
|
55
|
-
super
|
56
|
-
end
|
57
|
-
|
58
|
-
def connect
|
59
|
-
super
|
60
|
-
|
61
|
-
if async?
|
62
|
-
@executor_pool.connected_socket(@sock)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def async_reset
|
67
|
-
@write_buffer.clear
|
68
|
-
@write_offset = 0
|
69
|
-
|
70
|
-
@read_buffer.clear
|
71
|
-
@read_offset = 0
|
72
|
-
|
73
|
-
@executor_pool.close_socket if defined? @executor_pool
|
74
|
-
end
|
75
|
-
|
76
|
-
def async_io_ready(readable, writable)
|
77
|
-
async_sock_write_nonblock if writable
|
78
|
-
async_sock_read_nonblock if readable
|
79
|
-
end
|
80
|
-
|
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
|
89
|
-
|
90
|
-
# let all pending operations know that they are seeing the
|
91
|
-
# select loop. this starts the timer for the operation, because
|
92
|
-
# it guarantees we're now working on it.
|
93
|
-
# this is more accurate than starting the timer when we buffer
|
94
|
-
# the write.
|
95
|
-
op.in_select_loop
|
96
|
-
|
97
|
-
remaining = op.timeout_remaining
|
98
|
-
timeout = remaining if remaining < timeout
|
99
|
-
|
100
|
-
true # keep
|
101
|
-
end
|
102
|
-
|
103
|
-
@executor_pool.select_timeout = timeout
|
104
|
-
@executor_pool.set_interest(:r, !@pending_ops.empty?)
|
105
|
-
end
|
106
|
-
|
107
|
-
private
|
108
|
-
|
109
|
-
REQUEST = ::Dalli::Server::REQUEST
|
110
|
-
OPCODES = ::Dalli::Server::OPCODES
|
111
|
-
FORMAT = ::Dalli::Server::FORMAT
|
112
|
-
|
113
|
-
|
114
|
-
def promised_request(key, &block)
|
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
|
-
|
123
|
-
promise
|
124
|
-
end
|
125
|
-
|
126
|
-
def get(key, options = nil)
|
127
|
-
return super unless async?
|
128
|
-
|
129
|
-
promised_request(key) do |opaque|
|
130
|
-
[REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, opaque, 0, key].pack(FORMAT[:get])
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def async_generic_write_op(op, key, value, ttl, cas, options)
|
135
|
-
value.then do |value|
|
136
|
-
(value, flags) = serialize(key, value, options)
|
137
|
-
ttl = sanitize_ttl(ttl)
|
138
|
-
|
139
|
-
guard_max_value_with_raise(key, value)
|
140
|
-
|
141
|
-
promised_request(key) do |opaque|
|
142
|
-
[REQUEST, OPCODES[op], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, opaque, cas, flags, ttl, key, value].pack(FORMAT[op])
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def set(key, value, ttl, cas, options)
|
148
|
-
return super unless async?
|
149
|
-
async_generic_write_op(:set, key, value, ttl, cas, options)
|
150
|
-
end
|
151
|
-
|
152
|
-
def add(key, value, ttl, options)
|
153
|
-
return super unless async?
|
154
|
-
|
155
|
-
async_generic_write_op(:add, key, value, ttl, 0, options)
|
156
|
-
end
|
157
|
-
|
158
|
-
def replace(key, value, ttl, cas, options)
|
159
|
-
return super unless async?
|
160
|
-
|
161
|
-
async_generic_write_op(:replace, key, value, ttl, cas, options)
|
162
|
-
end
|
163
|
-
|
164
|
-
def delete(key, cas)
|
165
|
-
return super unless async?
|
166
|
-
|
167
|
-
promised_request(key) do |opaque|
|
168
|
-
[REQUEST, OPCODES[:delete], key.bytesize, 0, 0, 0, key.bytesize, opaque, cas, key].pack(FORMAT[:delete])
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def async_append_prepend_op(op, key, value)
|
173
|
-
promised_request(key) do |opaque|
|
174
|
-
[REQUEST, OPCODES[op], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, opaque, 0, key, value].pack(FORMAT[op])
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def append(key, value)
|
179
|
-
return super unless async?
|
180
|
-
|
181
|
-
async_append_prepend_op(:append, key, value)
|
182
|
-
end
|
183
|
-
|
184
|
-
def prepend(key, value)
|
185
|
-
return super unless async?
|
186
|
-
|
187
|
-
async_append_prepend_op(:prepend, key, value)
|
188
|
-
end
|
189
|
-
|
190
|
-
def flush
|
191
|
-
return super unless async?
|
192
|
-
|
193
|
-
promised_request(nil) do |opaque|
|
194
|
-
[REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, opaque, 0, 0].pack(FORMAT[:flush])
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def async_decr_incr(opcode, key, count, ttl, default)
|
199
|
-
expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
|
200
|
-
default ||= 0
|
201
|
-
(h, l) = split(count)
|
202
|
-
(dh, dl) = split(default)
|
203
|
-
promised_request(key) do |opaque|
|
204
|
-
req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, opaque, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
def decr(key, count, ttl, default)
|
209
|
-
return super unless async?
|
210
|
-
|
211
|
-
async_decr_incr :decr, key, count, ttl, default
|
212
|
-
end
|
213
|
-
|
214
|
-
def incr(key, count, ttl, default)
|
215
|
-
return super unless async?
|
216
|
-
|
217
|
-
async_decr_incr :incr, key, count, ttl, default
|
218
|
-
end
|
219
|
-
|
220
|
-
def async_buffered_write(data)
|
221
|
-
@write_buffer << data
|
222
|
-
async_sock_write_nonblock
|
223
|
-
end
|
224
|
-
|
225
|
-
def async_sock_write_nonblock
|
226
|
-
remaining = @write_buffer.byteslice(@write_offset, @write_buffer.length)
|
227
|
-
begin
|
228
|
-
bytes_written = @sock.write_nonblock(remaining, exception: false)
|
229
|
-
rescue Errno::EINTR
|
230
|
-
retry
|
231
|
-
end
|
232
|
-
|
233
|
-
return if bytes_written == :wait_writable
|
234
|
-
|
235
|
-
@write_offset += bytes_written
|
236
|
-
completed = (@write_offset == @write_buffer.length)
|
237
|
-
if completed
|
238
|
-
@write_buffer.clear
|
239
|
-
@write_offset = 0
|
240
|
-
end
|
241
|
-
@executor_pool.set_interest(:w, !completed)
|
242
|
-
rescue SystemCallError, Timeout::Error => e
|
243
|
-
failure!(e)
|
244
|
-
end
|
245
|
-
|
246
|
-
FULL_HEADER = 'CCnCCnNNQ'
|
247
|
-
|
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
|
265
|
-
|
266
|
-
buf = @read_buffer
|
267
|
-
pos = @read_offset
|
268
|
-
|
269
|
-
while buf.bytesize - pos >= 24
|
270
|
-
header = buf.byteslice(pos, 24)
|
271
|
-
(magic, opcode, key_length, extra_length, data_type, status, body_length, opaque, cas) = header.unpack(FULL_HEADER)
|
272
|
-
|
273
|
-
if buf.bytesize - pos >= 24 + body_length
|
274
|
-
exists = (status != 1) # Key not found
|
275
|
-
this_pos = pos
|
276
|
-
|
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
|
279
|
-
|
280
|
-
pos = pos + 24 + body_length
|
281
|
-
|
282
|
-
promise = @pending_ops.delete(opaque)
|
283
|
-
next if promise.nil?
|
284
|
-
|
285
|
-
begin
|
286
|
-
raise Dalli::DalliError, "Response error #{status}: #{Dalli::RESPONSE_CODES[status]}" unless status == 0 || status == 1 || status == 2 || status == 5
|
287
|
-
|
288
|
-
final_value = nil
|
289
|
-
if opcode == OPCODES[:incr] || opcode == OPCODES[:decr]
|
290
|
-
final_value = value.unpack1("Q>")
|
291
|
-
elsif exists
|
292
|
-
flags = if extra_length >= 4
|
293
|
-
buf.byteslice(this_pos + 24, 4).unpack1("N")
|
294
|
-
else
|
295
|
-
0
|
296
|
-
end
|
297
|
-
final_value = deserialize(value, flags)
|
298
|
-
end
|
299
|
-
|
300
|
-
response = ::IOPromise::Dalli::Response.new(
|
301
|
-
key: promise.key,
|
302
|
-
value: final_value,
|
303
|
-
exists: exists,
|
304
|
-
stored: !(status == 2 || status == 5), # Key exists or Item not stored
|
305
|
-
cas: cas,
|
306
|
-
)
|
307
|
-
|
308
|
-
promise.fulfill(response)
|
309
|
-
rescue => ex
|
310
|
-
promise.reject(ex)
|
311
|
-
end
|
312
|
-
else
|
313
|
-
# not enough data yet, wait for more
|
314
|
-
break
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
if pos == @read_buffer.length
|
319
|
-
@read_buffer.clear
|
320
|
-
@read_offset = 0
|
321
|
-
else
|
322
|
-
@read_offset = pos
|
323
|
-
end
|
324
|
-
|
325
|
-
rescue SystemCallError, Timeout::Error, EOFError => e
|
326
|
-
failure!(e)
|
327
|
-
end
|
328
|
-
|
329
|
-
def failure!(ex)
|
330
|
-
if async?
|
331
|
-
# all pending operations need to be rejected when a failure occurs
|
332
|
-
@pending_ops.each do |op|
|
333
|
-
op.reject(ex)
|
334
|
-
end
|
335
|
-
@pending_ops = {}
|
336
|
-
end
|
337
|
-
|
338
|
-
super
|
339
|
-
end
|
340
|
-
|
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)
|
343
|
-
return if value.bytesize <= @options[:value_max_bytes]
|
344
|
-
|
345
|
-
message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
|
346
|
-
raise Dalli::ValueOverMaxSize, message
|
347
|
-
end
|
348
|
-
end
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
::Dalli::Server.prepend(IOPromise::Dalli::AsyncServer)
|
353
|
-
::Dalli::Client.prepend(IOPromise::Dalli::AsyncClient)
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'executor_pool'
|
4
|
-
|
5
|
-
module IOPromise
|
6
|
-
module Dalli
|
7
|
-
class DalliPromise < ::IOPromise::Base
|
8
|
-
attr_reader :key
|
9
|
-
|
10
|
-
def initialize(server = nil, key = nil)
|
11
|
-
super()
|
12
|
-
|
13
|
-
# when created from a 'then' call, initialize nothing
|
14
|
-
return if server.nil? || key.nil?
|
15
|
-
|
16
|
-
@server = server
|
17
|
-
@key = key
|
18
|
-
@start_time = nil
|
19
|
-
|
20
|
-
::IOPromise::ExecutorContext.current.register(self)
|
21
|
-
end
|
22
|
-
|
23
|
-
def wait
|
24
|
-
unless defined?(@server)
|
25
|
-
super
|
26
|
-
else
|
27
|
-
::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def execute_pool
|
32
|
-
return @pool if defined?(@pool)
|
33
|
-
if defined?(@server)
|
34
|
-
@pool = DalliExecutorPool.for(@server)
|
35
|
-
else
|
36
|
-
@pool = nil
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def in_select_loop
|
41
|
-
if @start_time.nil?
|
42
|
-
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def timeout_remaining
|
47
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
48
|
-
elapsed = now - @start_time
|
49
|
-
remaining = @server.options[:socket_timeout] - elapsed
|
50
|
-
return 0 if remaining < 0
|
51
|
-
remaining
|
52
|
-
end
|
53
|
-
|
54
|
-
def timeout?
|
55
|
-
return false if @start_time.nil?
|
56
|
-
timeout_remaining <= 0
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module IOPromise
|
4
|
-
module Dalli
|
5
|
-
class Response
|
6
|
-
attr_reader :key, :value, :cas
|
7
|
-
|
8
|
-
def initialize(key:, value:, exists: false, stored: false, cas: nil)
|
9
|
-
@key = key
|
10
|
-
@value = value
|
11
|
-
@exists = exists
|
12
|
-
@stored = stored
|
13
|
-
@cas = cas
|
14
|
-
end
|
15
|
-
|
16
|
-
def exist?
|
17
|
-
@exists
|
18
|
-
end
|
19
|
-
|
20
|
-
def stored?
|
21
|
-
@stored
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
data/lib/iopromise/memcached.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'memcached'
|
4
|
-
require_relative 'promise'
|
5
|
-
|
6
|
-
module IOPromise
|
7
|
-
module Memcached
|
8
|
-
class Client
|
9
|
-
def initialize(*args, **kwargs)
|
10
|
-
if args.first.is_a?(::Memcached::Client)
|
11
|
-
@client = args.first.clone
|
12
|
-
else
|
13
|
-
@client = ::Memcached::Client.new(*args, **kwargs)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def get_as_promise(key)
|
18
|
-
MemcachePromise.new(@client, key)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,82 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module IOPromise
|
4
|
-
module Memcached
|
5
|
-
class MemcacheExecutorPool < ::IOPromise::ExecutorPool::Batch
|
6
|
-
def initialize(*)
|
7
|
-
super
|
8
|
-
|
9
|
-
@monitors = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def next_batch
|
13
|
-
super
|
14
|
-
|
15
|
-
unless @current_batch.empty?
|
16
|
-
@keys_to_promises = @current_batch.group_by { |promise| promise.key }
|
17
|
-
@current_batch.each { |promise| begin_executing(promise) }
|
18
|
-
begin
|
19
|
-
memcache_client.begin_get_multi(@keys_to_promises.keys)
|
20
|
-
rescue => e
|
21
|
-
@keys_to_promises.values.flatten.each do |promise|
|
22
|
-
promise.reject(e)
|
23
|
-
@current_batch.delete(promise)
|
24
|
-
end
|
25
|
-
|
26
|
-
@keys_to_promises = nil
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def execute_continue
|
32
|
-
if @current_batch.empty?
|
33
|
-
next_batch
|
34
|
-
end
|
35
|
-
|
36
|
-
if @current_batch.empty?
|
37
|
-
@monitors.each do |_, monitor|
|
38
|
-
monitor.interests = nil
|
39
|
-
end
|
40
|
-
return
|
41
|
-
end
|
42
|
-
|
43
|
-
so_far, readers, writers = memcache_client.continue_get_multi
|
44
|
-
|
45
|
-
# when we're done (nothing to wait on), fill in any remaining keys with nil for completions to occur
|
46
|
-
if readers.empty? && writers.empty?
|
47
|
-
@keys_to_promises.each do |key, _|
|
48
|
-
so_far[key] = nil unless so_far.include? key
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
so_far.each do |key, value|
|
53
|
-
next unless @keys_to_promises[key]
|
54
|
-
@keys_to_promises[key].each do |promise|
|
55
|
-
next if promise.fulfilled?
|
56
|
-
|
57
|
-
promise.fulfill(value)
|
58
|
-
@current_batch.delete(promise)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
@monitors.each do |_, monitor|
|
63
|
-
monitor.interests = nil
|
64
|
-
end
|
65
|
-
|
66
|
-
readers.each do |reader|
|
67
|
-
@monitors[reader] ||= ::IOPromise::ExecutorContext.current.register_observer_io(self, reader, :r)
|
68
|
-
@monitors[reader].add_interest(:r)
|
69
|
-
end
|
70
|
-
|
71
|
-
writers.each do |writer|
|
72
|
-
@monitors[writer] ||= ::IOPromise::ExecutorContext.current.register_observer_io(self, writer, :w)
|
73
|
-
@monitors[writer].add_interest(:w)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def memcache_client
|
78
|
-
@connection_pool
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'executor_pool'
|
4
|
-
|
5
|
-
module IOPromise
|
6
|
-
module Memcached
|
7
|
-
class MemcachePromise < ::IOPromise::Base
|
8
|
-
attr_reader :key
|
9
|
-
|
10
|
-
def initialize(client = nil, key = nil)
|
11
|
-
super()
|
12
|
-
|
13
|
-
@client = client
|
14
|
-
@key = key
|
15
|
-
|
16
|
-
::IOPromise::ExecutorContext.current.register(self) unless @client.nil? || @key.nil?
|
17
|
-
end
|
18
|
-
|
19
|
-
def wait
|
20
|
-
if @client.nil? || @key.nil?
|
21
|
-
super
|
22
|
-
else
|
23
|
-
::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def execute_pool
|
28
|
-
MemcacheExecutorPool.for(@client)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|