iopromise 0.1.0 → 0.1.4

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