iopromise 0.1.0 → 0.1.4
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 +1 -2
- data/Gemfile +4 -10
- data/Gemfile.lock +7 -30
- data/README.md +23 -3
- data/bin/setup +0 -3
- data/iopromise.gemspec +1 -0
- data/lib/iopromise.rb +47 -18
- data/lib/iopromise/cancel_context.rb +51 -0
- data/lib/iopromise/data_loader.rb +58 -0
- data/lib/iopromise/deferred.rb +2 -2
- data/lib/iopromise/deferred/executor_pool.rb +26 -10
- data/lib/iopromise/deferred/promise.rb +15 -2
- data/lib/iopromise/executor_context.rb +47 -59
- data/lib/iopromise/executor_pool/base.rb +26 -7
- data/lib/iopromise/executor_pool/batch.rb +5 -3
- data/lib/iopromise/executor_pool/sequential.rb +6 -14
- data/lib/iopromise/rack/context_middleware.rb +5 -6
- data/lib/iopromise/version.rb +1 -1
- data/lib/iopromise/view_component/data_loader.rb +3 -44
- metadata +18 -18
- data/lib/iopromise/dalli.rb +0 -13
- data/lib/iopromise/dalli/client.rb +0 -146
- data/lib/iopromise/dalli/executor_pool.rb +0 -13
- data/lib/iopromise/dalli/patch_dalli.rb +0 -337
- data/lib/iopromise/dalli/promise.rb +0 -52
- data/lib/iopromise/dalli/response.rb +0 -25
- data/lib/iopromise/faraday.rb +0 -17
- data/lib/iopromise/faraday/connection.rb +0 -25
- data/lib/iopromise/faraday/continuable_hydra.rb +0 -29
- data/lib/iopromise/faraday/executor_pool.rb +0 -19
- data/lib/iopromise/faraday/multi_socket_action.rb +0 -107
- data/lib/iopromise/faraday/promise.rb +0 -42
- data/lib/iopromise/memcached.rb +0 -13
- data/lib/iopromise/memcached/client.rb +0 -22
- data/lib/iopromise/memcached/executor_pool.rb +0 -61
- 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: 1189b2ad73f76eff39b347922c03e2e40fc188ff95b7a6771da921d193c88ed5
|
4
|
+
data.tar.gz: 169d59659740616c94508bdd3fabfd8fd1850c2a593dade86fda7267efa03eb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11f75193659f6f37295faf3d0ecea8166aa446da826da462cfc8ea5631eae2f107a65c1d093f1c082ae1e2690166f124244ea2ddf9bcc837ae3c6763e13ae063
|
7
|
+
data.tar.gz: d16beca1753c4c31994801787925b25713ac315599cc246dc19a92169fab7afe5948717454e9511be59735e542e8eea4805660c86c6c8de7a906a4e376848dc4
|
data/.github/workflows/main.yml
CHANGED
@@ -8,8 +8,7 @@ jobs:
|
|
8
8
|
steps:
|
9
9
|
- uses: actions/checkout@v2
|
10
10
|
- name: Install dependencies
|
11
|
-
run: sudo apt-get install libcurl4-openssl-dev libsasl2-dev
|
12
|
-
- uses: niden/actions-memcached@v7
|
11
|
+
run: sudo apt-get update && sudo apt-get install libcurl4-openssl-dev libsasl2-dev
|
13
12
|
- name: Set up Ruby
|
14
13
|
uses: ruby/setup-ruby@v1
|
15
14
|
with:
|
data/Gemfile
CHANGED
@@ -10,17 +10,11 @@ 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"
|
16
|
+
|
17
|
+
# benchmarking
|
18
|
+
gem "benchmark-ips"
|
19
|
+
gem "stackprof"
|
26
20
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,15 +1,9 @@
|
|
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.
|
12
|
-
|
4
|
+
iopromise (0.1.4)
|
5
|
+
nio4r
|
6
|
+
promise.rb (~> 0.7.4)
|
13
7
|
|
14
8
|
GEM
|
15
9
|
remote: https://rubygems.org/
|
@@ -73,24 +67,12 @@ GEM
|
|
73
67
|
minitest (>= 5.1)
|
74
68
|
tzinfo (~> 2.0)
|
75
69
|
zeitwerk (~> 2.3)
|
70
|
+
benchmark-ips (2.9.1)
|
76
71
|
builder (3.2.4)
|
77
72
|
concurrent-ruby (1.1.8)
|
78
73
|
crass (1.0.6)
|
79
|
-
dalli (2.7.11)
|
80
74
|
diff-lcs (1.4.4)
|
81
75
|
erubi (1.10.0)
|
82
|
-
ethon (0.14.0)
|
83
|
-
ffi (>= 1.15.0)
|
84
|
-
faraday (1.4.1)
|
85
|
-
faraday-excon (~> 1.1)
|
86
|
-
faraday-net_http (~> 1.0)
|
87
|
-
faraday-net_http_persistent (~> 1.1)
|
88
|
-
multipart-post (>= 1.2, < 3)
|
89
|
-
ruby2_keywords (>= 0.0.4)
|
90
|
-
faraday-excon (1.1.0)
|
91
|
-
faraday-net_http (1.0.1)
|
92
|
-
faraday-net_http_persistent (1.1.0)
|
93
|
-
ffi (1.15.0)
|
94
76
|
globalid (0.4.2)
|
95
77
|
activesupport (>= 4.2.0)
|
96
78
|
i18n (1.8.10)
|
@@ -104,7 +86,6 @@ GEM
|
|
104
86
|
method_source (1.0.0)
|
105
87
|
mini_mime (1.0.3)
|
106
88
|
minitest (5.14.4)
|
107
|
-
multipart-post (2.1.1)
|
108
89
|
nio4r (2.5.7)
|
109
90
|
nokogiri (1.11.3-x86_64-linux)
|
110
91
|
racc (~> 1.4)
|
@@ -153,7 +134,6 @@ GEM
|
|
153
134
|
diff-lcs (>= 1.2.0, < 2.0)
|
154
135
|
rspec-support (~> 3.10.0)
|
155
136
|
rspec-support (3.10.2)
|
156
|
-
ruby2_keywords (0.0.4)
|
157
137
|
sprockets (4.0.2)
|
158
138
|
concurrent-ruby (~> 1.0)
|
159
139
|
rack (> 1, < 3)
|
@@ -161,9 +141,8 @@ GEM
|
|
161
141
|
actionpack (>= 4.0)
|
162
142
|
activesupport (>= 4.0)
|
163
143
|
sprockets (>= 3.0.0)
|
144
|
+
stackprof (0.2.17)
|
164
145
|
thor (1.1.0)
|
165
|
-
typhoeus (1.4.0)
|
166
|
-
ethon (>= 0.9.0)
|
167
146
|
tzinfo (2.0.4)
|
168
147
|
concurrent-ruby (~> 1.0)
|
169
148
|
view_component (2.31.1)
|
@@ -177,14 +156,12 @@ PLATFORMS
|
|
177
156
|
x86_64-linux
|
178
157
|
|
179
158
|
DEPENDENCIES
|
180
|
-
|
181
|
-
faraday
|
159
|
+
benchmark-ips
|
182
160
|
iopromise!
|
183
|
-
memcached!
|
184
161
|
rails
|
185
162
|
rake (~> 13.0)
|
186
163
|
rspec (~> 3.0)
|
187
|
-
|
164
|
+
stackprof
|
188
165
|
view_component
|
189
166
|
|
190
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,10 @@ 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.
|
43
|
+
* [iopromise-dalli](https://github.com/iopromise-ruby/iopromise-dalli) supports [dalli](https://github.com/petergoldstein/dalli) memcached requests.
|
24
44
|
|
25
45
|
## Development
|
26
46
|
|
data/bin/setup
CHANGED
data/iopromise.gemspec
CHANGED
data/lib/iopromise.rb
CHANGED
@@ -4,6 +4,7 @@ require "promise"
|
|
4
4
|
|
5
5
|
require_relative "iopromise/version"
|
6
6
|
|
7
|
+
require_relative "iopromise/cancel_context"
|
7
8
|
require_relative "iopromise/executor_context"
|
8
9
|
require_relative "iopromise/executor_pool/base"
|
9
10
|
require_relative "iopromise/executor_pool/batch"
|
@@ -11,44 +12,72 @@ require_relative "iopromise/executor_pool/sequential"
|
|
11
12
|
|
12
13
|
module IOPromise
|
13
14
|
class Error < StandardError; end
|
15
|
+
class CancelledError < Error; end
|
14
16
|
|
15
17
|
class Base < ::Promise
|
16
|
-
def initialize(*)
|
17
|
-
@instrument_begin = []
|
18
|
-
@instrument_end = []
|
19
|
-
@started_executing = false
|
20
|
-
|
21
|
-
super
|
22
|
-
end
|
23
|
-
|
24
18
|
def instrument(begin_cb = nil, end_cb = nil)
|
25
|
-
raise ::IOPromise::Error.new("Instrumentation called after promise already started executing") if
|
26
|
-
|
27
|
-
|
19
|
+
raise ::IOPromise::Error.new("Instrumentation called after promise already started executing") if started_executing?
|
20
|
+
unless begin_cb.nil?
|
21
|
+
@instrument_begin ||= []
|
22
|
+
@instrument_begin << begin_cb
|
23
|
+
end
|
24
|
+
unless end_cb.nil?
|
25
|
+
@instrument_end ||= []
|
26
|
+
@instrument_end << end_cb
|
27
|
+
end
|
28
28
|
end
|
29
29
|
|
30
30
|
def beginning
|
31
|
-
@instrument_begin
|
31
|
+
@instrument_begin&.each { |cb| cb.call(self) }
|
32
|
+
@instrument_begin&.clear
|
32
33
|
@started_executing = true
|
33
34
|
end
|
34
35
|
|
35
36
|
def started_executing?
|
36
|
-
|
37
|
+
!!@started_executing
|
37
38
|
end
|
38
39
|
|
39
|
-
def notify_completion
|
40
|
-
@instrument_end
|
41
|
-
@instrument_end
|
40
|
+
def notify_completion(value: nil, reason: nil)
|
41
|
+
@instrument_end&.each { |cb| cb.call(self, value: value, reason: reason) }
|
42
|
+
@instrument_end&.clear
|
42
43
|
end
|
43
44
|
|
44
45
|
def fulfill(value)
|
45
|
-
|
46
|
+
return if cancelled?
|
47
|
+
notify_completion(value: value)
|
46
48
|
super(value)
|
47
49
|
end
|
48
50
|
|
49
51
|
def reject(reason)
|
50
|
-
|
52
|
+
return if cancelled?
|
53
|
+
notify_completion(reason: reason)
|
51
54
|
super(reason)
|
52
55
|
end
|
56
|
+
|
57
|
+
def wait
|
58
|
+
raise IOPromise::CancelledError if cancelled?
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
# Subclasses are expected to implement 'execute_pool' to return an IOPromise::ExecutorPool
|
63
|
+
# that is responsible for completing the given promise.
|
64
|
+
def execute_pool
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
# makes this promise inert, ensuring that promise chains do not continue
|
69
|
+
# propegation once this promise has been cancelled.
|
70
|
+
def cancel
|
71
|
+
return unless pending?
|
72
|
+
|
73
|
+
@cancelled = true
|
74
|
+
@observers = []
|
75
|
+
|
76
|
+
execute_pool.promise_cancelled(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
def cancelled?
|
80
|
+
!!defined?(@cancelled)
|
81
|
+
end
|
53
82
|
end
|
54
83
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IOPromise
|
4
|
+
class CancelContext
|
5
|
+
class << self
|
6
|
+
def context_stack
|
7
|
+
Thread.current[:iopromise_context_stack] ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def current
|
11
|
+
context_stack.last
|
12
|
+
end
|
13
|
+
|
14
|
+
def push
|
15
|
+
new_ctx = CancelContext.new(current)
|
16
|
+
context_stack.push(new_ctx)
|
17
|
+
new_ctx
|
18
|
+
end
|
19
|
+
|
20
|
+
def pop
|
21
|
+
ctx = context_stack.pop
|
22
|
+
ctx.cancel
|
23
|
+
ctx
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_new_context
|
27
|
+
ctx = push
|
28
|
+
yield ctx
|
29
|
+
ensure
|
30
|
+
pop
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(parent)
|
35
|
+
parent.subscribe(self) unless parent.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def subscribe(observer)
|
39
|
+
@observers ||= []
|
40
|
+
@observers.push observer
|
41
|
+
end
|
42
|
+
|
43
|
+
def cancel
|
44
|
+
return unless defined?(@observers)
|
45
|
+
@observers.each do |o|
|
46
|
+
o.cancel
|
47
|
+
end
|
48
|
+
@observers = []
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IOPromise
|
4
|
+
module DataLoader
|
5
|
+
module ClassMethods
|
6
|
+
def attr_promised_data(*args)
|
7
|
+
@promised_data_keys ||= []
|
8
|
+
@promised_data_keys.concat(args)
|
9
|
+
|
10
|
+
args.each do |arg|
|
11
|
+
self.class_eval("def #{arg};@#{arg}.sync;end")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def promised_data_keys
|
16
|
+
@promised_data_keys ||= []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(base)
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
def data_promises
|
25
|
+
self.class.promised_data_keys.flat_map do |k|
|
26
|
+
p = instance_variable_get('@' + k.to_s)
|
27
|
+
case p
|
28
|
+
when ::IOPromise::DataLoader
|
29
|
+
# greedily, recursively preload all nested data that we know about immediately
|
30
|
+
p.data_promises
|
31
|
+
when ::Promise
|
32
|
+
# allow nesting of dataloaders chained behind other promises
|
33
|
+
resolved = p.then do |result|
|
34
|
+
if result.is_a?(::IOPromise::DataLoader)
|
35
|
+
# likewise, if we resolved a promise that we can recurse, load that data too.
|
36
|
+
result.data_as_promise
|
37
|
+
else
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
[resolved]
|
43
|
+
else
|
44
|
+
raise TypeError.new("Instance variable #{k.to_s} used with attr_promised_data but was not a promise or a IOPromise::DataLoader.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def data_as_promise
|
50
|
+
@data_as_promise ||= Promise.all(data_promises)
|
51
|
+
end
|
52
|
+
|
53
|
+
def sync
|
54
|
+
data_as_promise.sync
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/iopromise/deferred.rb
CHANGED
@@ -5,8 +5,8 @@ require_relative 'deferred/promise'
|
|
5
5
|
module IOPromise
|
6
6
|
module Deferred
|
7
7
|
class << self
|
8
|
-
def new(&block)
|
9
|
-
::IOPromise::Deferred::DeferredPromise.new(&block)
|
8
|
+
def new(*args, **kwargs, &block)
|
9
|
+
::IOPromise::Deferred::DeferredPromise.new(*args, **kwargs, &block)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -3,26 +3,42 @@
|
|
3
3
|
module IOPromise
|
4
4
|
module Deferred
|
5
5
|
class DeferredExecutorPool < ::IOPromise::ExecutorPool::Batch
|
6
|
-
def
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
|
9
|
+
# register a dummy reader that never fires, to indicate to the event loop that
|
10
|
+
# there is a valid, active ExecutorPool.
|
11
|
+
@pipe_rd, @pipe_wr = IO.pipe
|
12
|
+
@iop_monitor = ::IOPromise::ExecutorContext.current.register_observer_io(self, @pipe_rd, :r)
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute_continue
|
7
16
|
if @current_batch.empty?
|
8
17
|
next_batch
|
9
18
|
end
|
10
19
|
|
11
|
-
|
12
|
-
|
13
|
-
|
20
|
+
# we are just running this in the sync cycle, in a blocking way.
|
21
|
+
timeouts = []
|
22
|
+
@current_batch.each do |promise|
|
23
|
+
time_until_execution = promise.time_until_execution
|
24
|
+
if time_until_execution <= 0
|
14
25
|
begin_executing(promise)
|
15
26
|
promise.run_deferred
|
16
|
-
|
27
|
+
else
|
28
|
+
timeouts << time_until_execution
|
17
29
|
end
|
30
|
+
end
|
18
31
|
|
19
|
-
|
20
|
-
|
21
|
-
|
32
|
+
if timeouts.empty?
|
33
|
+
@select_timeout = nil
|
34
|
+
else
|
35
|
+
# ensure we get back to this loop not too long after
|
36
|
+
@select_timeout = timeouts.min
|
22
37
|
end
|
23
38
|
|
24
|
-
# we
|
25
|
-
|
39
|
+
# we reset the batch - the promises that are not completed will still be
|
40
|
+
# pending and will be available next time we are called.
|
41
|
+
@current_batch = []
|
26
42
|
end
|
27
43
|
end
|
28
44
|
end
|