iopromise 0.1.3 → 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 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/lib/iopromise.rb +30 -0
- 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 +25 -7
- data/lib/iopromise/deferred/promise.rb +14 -1
- data/lib/iopromise/executor_context.rb +6 -1
- data/lib/iopromise/executor_pool/base.rb +3 -0
- data/lib/iopromise/rack/context_middleware.rb +19 -0
- data/lib/iopromise/version.rb +1 -1
- data/lib/iopromise/view_component/data_loader.rb +3 -44
- metadata +5 -2
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,7 +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
|
11
|
+
run: sudo apt-get update && sudo apt-get install libcurl4-openssl-dev libsasl2-dev
|
12
12
|
- name: Set up Ruby
|
13
13
|
uses: ruby/setup-ruby@v1
|
14
14
|
with:
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -40,6 +40,7 @@ Or install it yourself as:
|
|
40
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
41
|
|
42
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.
|
43
44
|
|
44
45
|
## Development
|
45
46
|
|
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,6 +12,7 @@ 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
18
|
def instrument(begin_cb = nil, end_cb = nil)
|
@@ -41,13 +43,41 @@ module IOPromise
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def fulfill(value)
|
46
|
+
return if cancelled?
|
44
47
|
notify_completion(value: value)
|
45
48
|
super(value)
|
46
49
|
end
|
47
50
|
|
48
51
|
def reject(reason)
|
52
|
+
return if cancelled?
|
49
53
|
notify_completion(reason: reason)
|
50
54
|
super(reason)
|
51
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
|
52
82
|
end
|
53
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,24 +3,42 @@
|
|
3
3
|
module IOPromise
|
4
4
|
module Deferred
|
5
5
|
class DeferredExecutorPool < ::IOPromise::ExecutorPool::Batch
|
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
|
+
|
6
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
|
27
|
+
else
|
28
|
+
timeouts << time_until_execution
|
16
29
|
end
|
30
|
+
end
|
17
31
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
21
37
|
end
|
22
38
|
|
23
|
-
# we
|
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 = []
|
24
42
|
end
|
25
43
|
end
|
26
44
|
end
|
@@ -5,10 +5,14 @@ require_relative 'executor_pool'
|
|
5
5
|
module IOPromise
|
6
6
|
module Deferred
|
7
7
|
class DeferredPromise < ::IOPromise::Base
|
8
|
-
def initialize(&block)
|
8
|
+
def initialize(timeout: nil, &block)
|
9
9
|
super()
|
10
10
|
|
11
11
|
@block = block
|
12
|
+
|
13
|
+
unless timeout.nil?
|
14
|
+
@defer_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
15
|
+
end
|
12
16
|
|
13
17
|
::IOPromise::ExecutorContext.current.register(self) unless @block.nil?
|
14
18
|
end
|
@@ -33,6 +37,15 @@ module IOPromise
|
|
33
37
|
reject(exception)
|
34
38
|
end
|
35
39
|
end
|
40
|
+
|
41
|
+
def time_until_execution
|
42
|
+
return 0 unless defined?(@defer_until)
|
43
|
+
|
44
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
45
|
+
return 0 if now > @defer_until
|
46
|
+
|
47
|
+
@defer_until - now
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
@@ -29,9 +29,14 @@ module IOPromise
|
|
29
29
|
|
30
30
|
def register(promise)
|
31
31
|
@pending_registrations << promise
|
32
|
+
IOPromise::CancelContext.current&.subscribe(promise)
|
32
33
|
end
|
33
34
|
|
34
35
|
def wait_for_all_data(end_when_complete: nil)
|
36
|
+
unless end_when_complete.nil?
|
37
|
+
raise IOPromise::CancelledError if end_when_complete.cancelled?
|
38
|
+
end
|
39
|
+
|
35
40
|
loop do
|
36
41
|
complete_pending_registrations
|
37
42
|
|
@@ -84,7 +89,7 @@ module IOPromise
|
|
84
89
|
pending = @pending_registrations
|
85
90
|
@pending_registrations = []
|
86
91
|
pending.each do |promise|
|
87
|
-
register_now(promise)
|
92
|
+
register_now(promise) unless promise.cancelled?
|
88
93
|
end
|
89
94
|
end
|
90
95
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'iopromise'
|
4
|
+
|
5
|
+
module IOPromise
|
6
|
+
module Rack
|
7
|
+
class ContextMiddleware
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
IOPromise::CancelContext.with_new_context do
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/iopromise/version.rb
CHANGED
@@ -1,56 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../data_loader"
|
3
4
|
require "view_component/engine"
|
4
5
|
|
5
6
|
module IOPromise
|
6
7
|
module ViewComponent
|
7
8
|
module DataLoader
|
8
|
-
|
9
|
-
def attr_promised_data(*args)
|
10
|
-
@promised_data ||= []
|
11
|
-
@promised_data.concat(args)
|
12
|
-
|
13
|
-
args.each do |arg|
|
14
|
-
self.class_eval("def #{arg};@#{arg}.sync;end")
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def promised_data_keys
|
19
|
-
@promised_data ||= []
|
20
|
-
end
|
21
|
-
end
|
9
|
+
include ::IOPromise::DataLoader
|
22
10
|
|
23
11
|
def self.included(base)
|
24
|
-
base.extend(ClassMethods)
|
25
|
-
end
|
26
|
-
|
27
|
-
def data_as_promise
|
28
|
-
@data_promise ||= begin
|
29
|
-
promises = self.class.promised_data_keys.map do |k|
|
30
|
-
p = instance_variable_get('@' + k.to_s)
|
31
|
-
if p.is_a?(IOPromise::ViewComponent::DataLoader)
|
32
|
-
# recursively preload all nested data
|
33
|
-
p.data_as_promise
|
34
|
-
else
|
35
|
-
# for any local promises, we'll unwrap them before completing
|
36
|
-
p.then do |result|
|
37
|
-
if result.is_a?(IOPromise::ViewComponent::DataLoader)
|
38
|
-
# likewise, if we resolved a promise that we can recurse, load that data too.
|
39
|
-
result.data_as_promise
|
40
|
-
else
|
41
|
-
result
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
Promise.all(promises)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def sync
|
52
|
-
data_as_promise.sync
|
53
|
-
self
|
12
|
+
base.extend(::IOPromise::DataLoader::ClassMethods)
|
54
13
|
end
|
55
14
|
|
56
15
|
def render_in(*)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Theo Julienne
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: promise.rb
|
@@ -60,6 +60,8 @@ files:
|
|
60
60
|
- bin/setup
|
61
61
|
- iopromise.gemspec
|
62
62
|
- lib/iopromise.rb
|
63
|
+
- lib/iopromise/cancel_context.rb
|
64
|
+
- lib/iopromise/data_loader.rb
|
63
65
|
- lib/iopromise/deferred.rb
|
64
66
|
- lib/iopromise/deferred/executor_pool.rb
|
65
67
|
- lib/iopromise/deferred/promise.rb
|
@@ -67,6 +69,7 @@ files:
|
|
67
69
|
- lib/iopromise/executor_pool/base.rb
|
68
70
|
- lib/iopromise/executor_pool/batch.rb
|
69
71
|
- lib/iopromise/executor_pool/sequential.rb
|
72
|
+
- lib/iopromise/rack/context_middleware.rb
|
70
73
|
- lib/iopromise/version.rb
|
71
74
|
- lib/iopromise/view_component.rb
|
72
75
|
- lib/iopromise/view_component/data_loader.rb
|