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.
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 +47 -18
  9. data/lib/iopromise/cancel_context.rb +51 -0
  10. data/lib/iopromise/data_loader.rb +58 -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
@@ -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
@@ -26,13 +30,22 @@ module IOPromise
26
30
  end
27
31
 
28
32
  def run_deferred
29
- return if @block.nil?
33
+ return if @block.nil? || !pending?
30
34
  begin
31
35
  fulfill(@block.call)
32
36
  rescue => exception
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
@@ -1,68 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'nio'
4
5
 
5
6
  module IOPromise
6
7
  class ExecutorContext
7
8
  class << self
8
- def push
9
- @contexts ||= []
10
- @contexts << ExecutorContext.new
11
- end
12
-
13
9
  def current
14
- @contexts.last
15
- end
16
-
17
- def pop
18
- @contexts.pop
10
+ @context ||= ExecutorContext.new
19
11
  end
20
12
  end
21
13
 
22
14
  def initialize
23
- @pools = Set.new
24
-
25
- @pool_ready_readers = {}
26
- @pool_ready_writers = {}
27
- @pool_ready_exceptions = {}
15
+ @pools = {}
28
16
 
29
17
  @pending_registrations = []
18
+
19
+ @selector = NIO::Selector.new
20
+
21
+ super
22
+ end
23
+
24
+ def register_observer_io(observer, io, interest)
25
+ monitor = @selector.register(io, interest)
26
+ monitor.value = observer
27
+ monitor
30
28
  end
31
29
 
32
30
  def register(promise)
33
31
  @pending_registrations << promise
32
+ IOPromise::CancelContext.current&.subscribe(promise)
34
33
  end
35
34
 
36
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
+
37
40
  loop do
38
41
  complete_pending_registrations
39
42
 
40
- readers, writers, exceptions, wait_time = continue_to_read_pools
41
-
43
+ @pools.each do |pool, _|
44
+ pool.execute_continue
45
+ end
46
+
42
47
  unless end_when_complete.nil?
43
48
  return unless end_when_complete.pending?
44
49
  end
45
-
46
- break if readers.empty? && writers.empty? && exceptions.empty? && @pending_registrations.empty?
50
+
51
+ break if @selector.empty?
47
52
 
48
53
  # if we have any pending promises to register, we'll not block at all so we immediately continue
49
- wait_time = 0 unless @pending_registrations.empty?
50
-
51
- # we could be clever and decide which ones to "continue" on next
52
- ready = IO.select(readers.keys, writers.keys, exceptions.keys, wait_time)
53
- ready = [[], [], []] if ready.nil?
54
- ready_readers, ready_writers, ready_exceptions = ready
55
-
56
- # group by the pool object that provided the fd
57
- @pool_ready_readers = ready_readers.group_by { |i| readers[i] }
58
- @pool_ready_writers = ready_writers.group_by { |i| writers[i] }
59
- @pool_ready_exceptions = ready_exceptions.group_by { |i| exceptions[i] }
54
+ unless @pending_registrations.empty?
55
+ wait_time = 0
56
+ else
57
+ wait_time = nil
58
+ @pools.each do |pool, _|
59
+ timeout = pool.select_timeout
60
+ wait_time = timeout if wait_time.nil? || (!timeout.nil? && timeout < wait_time)
61
+ end
62
+ end
63
+
64
+ ready_count = select(wait_time)
60
65
  end
61
-
66
+
62
67
  unless end_when_complete.nil?
63
68
  raise ::IOPromise::Error.new('Internal error: IO loop completed without fulfilling the desired promise')
64
69
  else
65
- @pools.each do |pool|
70
+ @pools.each do |pool, _|
66
71
  pool.wait
67
72
  end
68
73
  end
@@ -72,43 +77,26 @@ module IOPromise
72
77
 
73
78
  private
74
79
 
80
+ def select(wait_time)
81
+ @selector.select(wait_time) do |monitor|
82
+ observer = monitor.value
83
+ observer.monitor_ready(monitor, monitor.readiness)
84
+ end
85
+ end
86
+
75
87
  def complete_pending_registrations
88
+ return if @pending_registrations.empty?
76
89
  pending = @pending_registrations
77
90
  @pending_registrations = []
78
91
  pending.each do |promise|
79
- register_now(promise)
80
- end
81
- end
82
-
83
- def continue_to_read_pools
84
- readers = {}
85
- writers = {}
86
- exceptions = {}
87
- max_timeout = nil
88
-
89
- @pools.each do |pool|
90
- rd, wr, ex, ti = pool.execute_continue(@pool_ready_readers[pool], @pool_ready_writers[pool], @pool_ready_exceptions[pool])
91
- rd.each do |io|
92
- readers[io] = pool
93
- end
94
- wr.each do |io|
95
- writers[io] = pool
96
- end
97
- ex.each do |io|
98
- exceptions[io] = pool
99
- end
100
- if max_timeout.nil? || (!ti.nil? && ti < max_timeout)
101
- max_timeout = ti
102
- end
92
+ register_now(promise) unless promise.cancelled?
103
93
  end
104
-
105
- [readers, writers, exceptions, max_timeout]
106
94
  end
107
95
 
108
96
  def register_now(promise)
109
97
  pool = promise.execute_pool
110
98
  pool.register(promise)
111
- @pools.add(pool)
99
+ @pools[pool] = true
112
100
  end
113
101
  end
114
102
  end
@@ -1,25 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'promise/observer'
4
+
3
5
  module IOPromise
4
6
  module ExecutorPool
5
7
  class Base
8
+ include Promise::Observer
9
+
6
10
  class << self
7
11
  def for(connection_pool)
8
12
  @executors ||= {}
9
13
  @executors[connection_pool] ||= new(connection_pool)
10
14
  end
11
15
  end
16
+
17
+ attr_accessor :select_timeout
12
18
 
13
19
  def initialize(connection_pool)
14
20
  @connection_pool = connection_pool
15
21
  @pending = []
22
+
23
+ @monitors = {}
24
+
25
+ @select_timeout = nil
16
26
  end
17
27
 
18
28
  def register(item)
19
29
  @pending << item
30
+ item.subscribe(self, item, item)
20
31
  end
21
32
 
22
- def complete(item)
33
+ def promise_fulfilled(_value, item)
34
+ @pending.delete(item)
35
+ end
36
+ def promise_rejected(_reason, item)
37
+ @pending.delete(item)
38
+ end
39
+ def promise_cancelled(item)
23
40
  @pending.delete(item)
24
41
  end
25
42
 
@@ -28,15 +45,17 @@ module IOPromise
28
45
  end
29
46
 
30
47
  # Continue execution of one or more pending IOPromises assigned to this pool.
31
- # Returns [readers, writers, exceptions, max_timeout], which are arrays of the
32
- # readers, writers, and exceptions to select on. The timeout specifies the maximum
33
- # time to block waiting for one of these IO objects to become ready, after which
34
- # this function is called again with empty "ready" arguments.
48
+ # Implementations may choose to pre-register IO handled using:
49
+ # ExecutorContext.current.register_observer_io(...)
50
+ # Alternatively, they can be registered when this function is called.
51
+ # During this function, implementations should check for timeouts and run
52
+ # any housekeeping operations.
53
+ #
35
54
  # Must be implemented by subclasses.
36
- def execute_continue(ready_readers, ready_writers, ready_exceptions)
55
+ def execute_continue
37
56
  raise NotImplementedError
38
57
  end
39
-
58
+
40
59
  def sync
41
60
  @pending.each do |promise|
42
61
  promise.sync if promise.is_a?(Promise)
@@ -3,8 +3,8 @@
3
3
  module IOPromise
4
4
  module ExecutorPool
5
5
  class Batch < Base
6
- def initialize(connection_pool)
7
- super(connection_pool)
6
+ def initialize(*)
7
+ super
8
8
 
9
9
  @current_batch = []
10
10
  end
@@ -16,7 +16,9 @@ module IOPromise
16
16
  end
17
17
 
18
18
  # every pending operation becomes part of the current batch
19
- @current_batch = @pending.dup
19
+ # we don't include promises with a source set, because that
20
+ # indicates that they depend on another promise now.
21
+ @current_batch = @pending.select { |p| p.pending? && p.source.nil? }
20
22
  end
21
23
  end
22
24
  end
@@ -3,29 +3,21 @@
3
3
  module IOPromise
4
4
  module ExecutorPool
5
5
  class Sequential < Base
6
- def execute_continue_item(item, ready_readers, ready_writers, ready_exceptions)
7
- item.execute_continue(ready_readers, ready_writers, ready_exceptions)
6
+ def execute_continue_item(item)
7
+ item.execute_continue
8
8
  end
9
9
 
10
- def execute_continue(ready_readers, ready_writers, ready_exceptions)
10
+ def execute_continue
11
11
  @pending.dup.each do |active|
12
- status = if active.fulfilled?
13
- nil
14
- else
15
- execute_continue_item(active, ready_readers, ready_writers, ready_exceptions)
16
- end
12
+ execute_continue_item(active)
17
13
 
18
- unless status.nil?
14
+ unless active.fulfilled?
19
15
  # once we're waiting on our one next item, we're done
20
- return status
21
- else
22
- # we're done with this one, so remove it
23
- complete(active)
16
+ return
24
17
  end
25
18
  end
26
19
 
27
20
  # if we fall through to here, we have nothing to wait on.
28
- [[], [], [], nil]
29
21
  end
30
22
  end
31
23
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'iopromise'
1
4
 
2
5
  module IOPromise
3
6
  module Rack
@@ -7,13 +10,9 @@ module IOPromise
7
10
  end
8
11
 
9
12
  def call(env)
10
- ::IOPromise::ExecutorContext.push
11
- begin
12
- status, headers, body = @app.call(env)
13
- ensure
14
- ::IOPromise::ExecutorContext.pop
13
+ IOPromise::CancelContext.with_new_context do
14
+ @app.call(env)
15
15
  end
16
- [status, headers, body]
17
16
  end
18
17
  end
19
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IOPromise
4
- VERSION = '0.1.0'
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.0
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-05-27 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
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.7.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: nio4r
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: This gem extends promise.rb promises to support an extremely simple pattern
28
42
  for "continuing" execution of the promise in an asynchronous non-blocking way.
29
43
  email:
@@ -46,12 +60,8 @@ files:
46
60
  - bin/setup
47
61
  - iopromise.gemspec
48
62
  - lib/iopromise.rb
49
- - lib/iopromise/dalli.rb
50
- - lib/iopromise/dalli/client.rb
51
- - lib/iopromise/dalli/executor_pool.rb
52
- - lib/iopromise/dalli/patch_dalli.rb
53
- - lib/iopromise/dalli/promise.rb
54
- - lib/iopromise/dalli/response.rb
63
+ - lib/iopromise/cancel_context.rb
64
+ - lib/iopromise/data_loader.rb
55
65
  - lib/iopromise/deferred.rb
56
66
  - lib/iopromise/deferred/executor_pool.rb
57
67
  - lib/iopromise/deferred/promise.rb
@@ -59,16 +69,6 @@ files:
59
69
  - lib/iopromise/executor_pool/base.rb
60
70
  - lib/iopromise/executor_pool/batch.rb
61
71
  - lib/iopromise/executor_pool/sequential.rb
62
- - lib/iopromise/faraday.rb
63
- - lib/iopromise/faraday/connection.rb
64
- - lib/iopromise/faraday/continuable_hydra.rb
65
- - lib/iopromise/faraday/executor_pool.rb
66
- - lib/iopromise/faraday/multi_socket_action.rb
67
- - lib/iopromise/faraday/promise.rb
68
- - lib/iopromise/memcached.rb
69
- - lib/iopromise/memcached/client.rb
70
- - lib/iopromise/memcached/executor_pool.rb
71
- - lib/iopromise/memcached/promise.rb
72
72
  - lib/iopromise/rack/context_middleware.rb
73
73
  - lib/iopromise/version.rb
74
74
  - lib/iopromise/view_component.rb