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 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