iopromise 0.1.1 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -2
  3. data/Gemfile +4 -10
  4. data/Gemfile.lock +7 -30
  5. data/README.md +23 -3
  6. data/bin/setup +0 -3
  7. data/iopromise.gemspec +1 -0
  8. data/lib/iopromise.rb +44 -15
  9. data/lib/iopromise/cancel_context.rb +51 -0
  10. data/lib/iopromise/data_loader.rb +65 -0
  11. data/lib/iopromise/deferred.rb +2 -2
  12. data/lib/iopromise/deferred/executor_pool.rb +26 -10
  13. data/lib/iopromise/deferred/promise.rb +15 -2
  14. data/lib/iopromise/executor_context.rb +47 -59
  15. data/lib/iopromise/executor_pool/base.rb +26 -7
  16. data/lib/iopromise/executor_pool/batch.rb +5 -3
  17. data/lib/iopromise/executor_pool/sequential.rb +6 -14
  18. data/lib/iopromise/rack/context_middleware.rb +5 -6
  19. data/lib/iopromise/version.rb +1 -1
  20. data/lib/iopromise/view_component/data_loader.rb +3 -44
  21. metadata +18 -18
  22. data/lib/iopromise/dalli.rb +0 -13
  23. data/lib/iopromise/dalli/client.rb +0 -146
  24. data/lib/iopromise/dalli/executor_pool.rb +0 -13
  25. data/lib/iopromise/dalli/patch_dalli.rb +0 -337
  26. data/lib/iopromise/dalli/promise.rb +0 -52
  27. data/lib/iopromise/dalli/response.rb +0 -25
  28. data/lib/iopromise/faraday.rb +0 -17
  29. data/lib/iopromise/faraday/connection.rb +0 -25
  30. data/lib/iopromise/faraday/continuable_hydra.rb +0 -29
  31. data/lib/iopromise/faraday/executor_pool.rb +0 -19
  32. data/lib/iopromise/faraday/multi_socket_action.rb +0 -107
  33. data/lib/iopromise/faraday/promise.rb +0 -42
  34. data/lib/iopromise/memcached.rb +0 -13
  35. data/lib/iopromise/memcached/client.rb +0 -22
  36. data/lib/iopromise/memcached/executor_pool.rb +0 -61
  37. data/lib/iopromise/memcached/promise.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 323b4f0107580d674fa42c4f65644dcae302c822c9d370dd9e3effbf5b4c13eb
4
- data.tar.gz: dc8b3937fea0505e598c0855ccc695b98dc837e221b8959a61acfbf18838c3b5
3
+ metadata.gz: 566685c55c8c71504647196e5cc9edcb96cde013c316a9b5a8b2ad771afde67e
4
+ data.tar.gz: 4928d07726ea8b3df4a79fa056fd3b0ce3c6e5b2493f166b5ea0d7261346b8c6
5
5
  SHA512:
6
- metadata.gz: 8737b6c9a1e1ee2376f586ceca83a074679d6e0b53e073e07df8817f3f14fe261b606f5398618bcd984f2441b288b89fb9d660f499428d60c93cf35e7b561f7e
7
- data.tar.gz: c82ed8d1da207ce2bfa766b8a1fd88aa4ba803565b749f4c77e0cb90a5b8f9c0289f60111260daf4acc5b7ddaae43b0ddcadd91212201778516b1637f9db2cb6
6
+ metadata.gz: 9b849e9d40682cda87bf901c734a65a7db10f27c9bcfa22f9760a06943c386c617dccc7f1f186fd92da6fa42956ac8b56384b7be0e4f081bd4e78e5cfd7ecf1f
7
+ data.tar.gz: 174b5ab72c8a3ef1c6de7a412bcaa35f2a1165066494b7b9e4cbe22d8b858ea57ed0fb274a20a34020dcaffdbd4e998ddd067512ac031174fb4b3ff9c069420a
@@ -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.0)
12
- promise.rb
4
+ iopromise (0.1.5)
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
- dalli (= 2.7.11)
181
- faraday
159
+ benchmark-ips
182
160
  iopromise!
183
- memcached!
184
161
  rails
185
162
  rake (~> 13.0)
186
163
  rspec (~> 3.0)
187
- typhoeus
164
+ stackprof
188
165
  view_component
189
166
 
190
167
  BUNDLED WITH
data/README.md CHANGED
@@ -1,6 +1,23 @@
1
- # iopromise
1
+ # IOPromise
2
2
 
3
- This **experimental pre-release** gem extends promise.rb promises to support an extremely simple pattern for \"continuing\" execution of all pending promises in an asynchronous non-blocking way.
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
- TODO: Write usage instructions here
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
@@ -4,6 +4,3 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
-
8
- # bring up a memcached for testing
9
- docker run -d -p 11211:11211 memcached:alpine
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
@@ -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 @started_executing
26
- @instrument_begin << begin_cb unless begin_cb.nil?
27
- @instrument_end << end_cb unless end_cb.nil?
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.each { |cb| cb.call(self) }
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
- @started_executing
37
+ !!@started_executing
37
38
  end
38
39
 
39
40
  def notify_completion(value: nil, reason: nil)
40
- @instrument_end.each { |cb| cb.call(self, value: value, reason: reason) }
41
- @instrument_end = []
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)
46
+ return if cancelled?
45
47
  notify_completion(value: value)
46
48
  super(value)
47
49
  end
48
50
 
49
51
  def reject(reason)
52
+ return if cancelled?
50
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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module DataLoader
5
+ module ClassMethods
6
+ def attr_async(attr_name, build_func = nil)
7
+ self.attr_async_names << attr_name
8
+
9
+ if build_func.nil?
10
+ self.class_eval("def async_#{attr_name};@#{attr_name};end")
11
+ else
12
+ self.define_method("async_#{attr_name}") do
13
+ @attr_async_memo ||= {}
14
+ @attr_async_memo[attr_name] ||= self.instance_exec(&build_func)
15
+ end
16
+ end
17
+
18
+ self.class_eval("def #{attr_name};async_#{attr_name}.sync;end")
19
+ end
20
+
21
+ def attr_async_names
22
+ @attr_async_names ||= []
23
+ end
24
+ end
25
+
26
+ def self.included(base)
27
+ base.extend(ClassMethods)
28
+ end
29
+
30
+ def async_attributes
31
+ @async_attributes ||= Promise.all(attr_async_promises)
32
+ end
33
+
34
+ def sync
35
+ async_attributes.sync
36
+ self
37
+ end
38
+
39
+ protected
40
+ def attr_async_promises
41
+ self.class.attr_async_names.flat_map do |k|
42
+ p = send("async_#{k}")
43
+ case p
44
+ when ::IOPromise::DataLoader
45
+ # greedily, recursively preload all nested data that we know about immediately
46
+ p.attr_async_promises
47
+ when ::Promise
48
+ # allow nesting of dataloaders chained behind other promises
49
+ resolved = p.then do |result|
50
+ if result.is_a?(::IOPromise::DataLoader)
51
+ # likewise, if we resolved a promise that we can recurse, load that data too.
52
+ result.async_attributes
53
+ else
54
+ result
55
+ end
56
+ end
57
+
58
+ [resolved]
59
+ else
60
+ raise TypeError.new("Instance variable #{k.to_s} used with attr_async but was not a promise or a IOPromise::DataLoader.")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -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 execute_continue(ready_readers, ready_writers, ready_exceptions)
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
- until @current_batch.empty?
12
- # we are just running this in the sync cycle, in a blocking way.
13
- @current_batch.each do |promise|
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
- complete(promise)
27
+ else
28
+ timeouts << time_until_execution
17
29
  end
30
+ end
18
31
 
19
- @current_batch = []
20
-
21
- next_batch
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 always fully complete each cycle
25
- return [[], [], [], nil]
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