iopromise 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f58c2a28347c1f21fe6c660eff9f2b7ce1b078b1a06a37c91de0d6ec05db1c66
4
- data.tar.gz: f5b2f6f66c5f79bb3f68f8cd041f6fecdde33dba2700b3f474ae9a76995378c9
3
+ metadata.gz: 1189b2ad73f76eff39b347922c03e2e40fc188ff95b7a6771da921d193c88ed5
4
+ data.tar.gz: 169d59659740616c94508bdd3fabfd8fd1850c2a593dade86fda7267efa03eb7
5
5
  SHA512:
6
- metadata.gz: '09e91e024b854c41dd60942dc76af20795ca88807f3d6548bbf7d96982babd4c618e7054f9e63c640cca3eb4add0d08c347d8bb7a5ac5357c7969645817562c9'
7
- data.tar.gz: d44281e24ab187944c15da2ceae76a1b8be8d8b54dd6a117f2783aec3418e12231283cfe3adc548d8d4d78536d5712c493126c0618f820f541e6142d33cc27dc
6
+ metadata.gz: 11f75193659f6f37295faf3d0ecea8166aa446da826da462cfc8ea5631eae2f107a65c1d093f1c082ae1e2690166f124244ea2ddf9bcc837ae3c6763e13ae063
7
+ data.tar.gz: d16beca1753c4c31994801787925b25713ac315599cc246dc19a92169fab7afe5948717454e9511be59735e542e8eea4805660c86c6c8de7a906a4e376848dc4
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- iopromise (0.1.3)
4
+ iopromise (0.1.4)
5
5
  nio4r
6
6
  promise.rb (~> 0.7.4)
7
7
 
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
@@ -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
- 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
27
+ else
28
+ timeouts << time_until_execution
16
29
  end
30
+ end
17
31
 
18
- @current_batch = []
19
-
20
- 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
21
37
  end
22
38
 
23
- # we always fully complete each cycle
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
 
@@ -36,6 +36,9 @@ module IOPromise
36
36
  def promise_rejected(_reason, item)
37
37
  @pending.delete(item)
38
38
  end
39
+ def promise_cancelled(item)
40
+ @pending.delete(item)
41
+ end
39
42
 
40
43
  def begin_executing(item)
41
44
  item.beginning
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IOPromise
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
5
5
  end
@@ -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
- module ClassMethods
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.3
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-06-18 00:00:00.000000000 Z
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