iopromise 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +21 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +26 -0
  7. data/Gemfile.lock +191 -0
  8. data/LICENSE +21 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +41 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +9 -0
  14. data/iopromise.gemspec +30 -0
  15. data/lib/iopromise.rb +54 -0
  16. data/lib/iopromise/dalli.rb +13 -0
  17. data/lib/iopromise/dalli/client.rb +146 -0
  18. data/lib/iopromise/dalli/executor_pool.rb +13 -0
  19. data/lib/iopromise/dalli/patch_dalli.rb +337 -0
  20. data/lib/iopromise/dalli/promise.rb +52 -0
  21. data/lib/iopromise/dalli/response.rb +25 -0
  22. data/lib/iopromise/deferred.rb +13 -0
  23. data/lib/iopromise/deferred/executor_pool.rb +29 -0
  24. data/lib/iopromise/deferred/promise.rb +38 -0
  25. data/lib/iopromise/executor_context.rb +114 -0
  26. data/lib/iopromise/executor_pool/base.rb +47 -0
  27. data/lib/iopromise/executor_pool/batch.rb +23 -0
  28. data/lib/iopromise/executor_pool/sequential.rb +32 -0
  29. data/lib/iopromise/faraday.rb +17 -0
  30. data/lib/iopromise/faraday/connection.rb +25 -0
  31. data/lib/iopromise/faraday/continuable_hydra.rb +29 -0
  32. data/lib/iopromise/faraday/executor_pool.rb +19 -0
  33. data/lib/iopromise/faraday/multi_socket_action.rb +107 -0
  34. data/lib/iopromise/faraday/promise.rb +42 -0
  35. data/lib/iopromise/memcached.rb +13 -0
  36. data/lib/iopromise/memcached/client.rb +22 -0
  37. data/lib/iopromise/memcached/executor_pool.rb +61 -0
  38. data/lib/iopromise/memcached/promise.rb +32 -0
  39. data/lib/iopromise/rack/context_middleware.rb +20 -0
  40. data/lib/iopromise/version.rb +5 -0
  41. data/lib/iopromise/view_component.rb +9 -0
  42. data/lib/iopromise/view_component/data_loader.rb +62 -0
  43. metadata +101 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executor_pool'
4
+
5
+ module IOPromise
6
+ module Dalli
7
+ class DalliPromise < ::IOPromise::Base
8
+ attr_reader :key
9
+
10
+ def initialize(server = nil, key = nil)
11
+ super()
12
+
13
+ @server = server
14
+ @key = key
15
+ @start_time = nil
16
+
17
+ ::IOPromise::ExecutorContext.current.register(self) unless @server.nil? || @key.nil?
18
+ end
19
+
20
+ def wait
21
+ if @server.nil? || @key.nil?
22
+ super
23
+ else
24
+ ::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
25
+ end
26
+ end
27
+
28
+ def execute_pool
29
+ DalliExecutorPool.for(@server)
30
+ end
31
+
32
+ def in_select_loop
33
+ if @start_time.nil?
34
+ @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
+ end
36
+ end
37
+
38
+ def timeout_remaining
39
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
40
+ elapsed = now - @start_time
41
+ remaining = @server.options[:socket_timeout] - elapsed
42
+ return 0 if remaining < 0
43
+ remaining
44
+ end
45
+
46
+ def timeout?
47
+ return false if @start_time.nil?
48
+ timeout_remaining <= 0
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module Dalli
5
+ class Response
6
+ attr_reader :key, :value, :cas
7
+
8
+ def initialize(key:, value:, exists: false, stored: false, cas: nil)
9
+ @key = key
10
+ @value = value
11
+ @exists = exists
12
+ @stored = stored
13
+ @cas = cas
14
+ end
15
+
16
+ def exist?
17
+ @exists
18
+ end
19
+
20
+ def stored?
21
+ @stored
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'deferred/promise'
4
+
5
+ module IOPromise
6
+ module Deferred
7
+ class << self
8
+ def new(&block)
9
+ ::IOPromise::Deferred::DeferredPromise.new(&block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module Deferred
5
+ class DeferredExecutorPool < ::IOPromise::ExecutorPool::Batch
6
+ def execute_continue(ready_readers, ready_writers, ready_exceptions)
7
+ if @current_batch.empty?
8
+ next_batch
9
+ end
10
+
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|
14
+ begin_executing(promise)
15
+ promise.run_deferred
16
+ complete(promise)
17
+ end
18
+
19
+ @current_batch = []
20
+
21
+ next_batch
22
+ end
23
+
24
+ # we always fully complete each cycle
25
+ return [[], [], [], nil]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executor_pool'
4
+
5
+ module IOPromise
6
+ module Deferred
7
+ class DeferredPromise < ::IOPromise::Base
8
+ def initialize(&block)
9
+ super()
10
+
11
+ @block = block
12
+
13
+ ::IOPromise::ExecutorContext.current.register(self) unless @block.nil?
14
+ end
15
+
16
+ def wait
17
+ if @block.nil?
18
+ super
19
+ else
20
+ ::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
21
+ end
22
+ end
23
+
24
+ def execute_pool
25
+ DeferredExecutorPool.for(Thread.current)
26
+ end
27
+
28
+ def run_deferred
29
+ return if @block.nil?
30
+ begin
31
+ fulfill(@block.call)
32
+ rescue => exception
33
+ reject(exception)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module IOPromise
6
+ class ExecutorContext
7
+ class << self
8
+ def push
9
+ @contexts ||= []
10
+ @contexts << ExecutorContext.new
11
+ end
12
+
13
+ def current
14
+ @contexts.last
15
+ end
16
+
17
+ def pop
18
+ @contexts.pop
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ @pools = Set.new
24
+
25
+ @pool_ready_readers = {}
26
+ @pool_ready_writers = {}
27
+ @pool_ready_exceptions = {}
28
+
29
+ @pending_registrations = []
30
+ end
31
+
32
+ def register(promise)
33
+ @pending_registrations << promise
34
+ end
35
+
36
+ def wait_for_all_data(end_when_complete: nil)
37
+ loop do
38
+ complete_pending_registrations
39
+
40
+ readers, writers, exceptions, wait_time = continue_to_read_pools
41
+
42
+ unless end_when_complete.nil?
43
+ return unless end_when_complete.pending?
44
+ end
45
+
46
+ break if readers.empty? && writers.empty? && exceptions.empty? && @pending_registrations.empty?
47
+
48
+ # 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] }
60
+ end
61
+
62
+ unless end_when_complete.nil?
63
+ raise ::IOPromise::Error.new('Internal error: IO loop completed without fulfilling the desired promise')
64
+ else
65
+ @pools.each do |pool|
66
+ pool.wait
67
+ end
68
+ end
69
+ ensure
70
+ complete_pending_registrations
71
+ end
72
+
73
+ private
74
+
75
+ def complete_pending_registrations
76
+ pending = @pending_registrations
77
+ @pending_registrations = []
78
+ 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
103
+ end
104
+
105
+ [readers, writers, exceptions, max_timeout]
106
+ end
107
+
108
+ def register_now(promise)
109
+ pool = promise.execute_pool
110
+ pool.register(promise)
111
+ @pools.add(pool)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module ExecutorPool
5
+ class Base
6
+ class << self
7
+ def for(connection_pool)
8
+ @executors ||= {}
9
+ @executors[connection_pool] ||= new(connection_pool)
10
+ end
11
+ end
12
+
13
+ def initialize(connection_pool)
14
+ @connection_pool = connection_pool
15
+ @pending = []
16
+ end
17
+
18
+ def register(item)
19
+ @pending << item
20
+ end
21
+
22
+ def complete(item)
23
+ @pending.delete(item)
24
+ end
25
+
26
+ def begin_executing(item)
27
+ item.beginning
28
+ end
29
+
30
+ # 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.
35
+ # Must be implemented by subclasses.
36
+ def execute_continue(ready_readers, ready_writers, ready_exceptions)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def sync
41
+ @pending.each do |promise|
42
+ promise.sync if promise.is_a?(Promise)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module ExecutorPool
5
+ class Batch < Base
6
+ def initialize(connection_pool)
7
+ super(connection_pool)
8
+
9
+ @current_batch = []
10
+ end
11
+
12
+ def next_batch
13
+ # ensure that all current items are fully completed
14
+ @current_batch.each do |promise|
15
+ promise.wait
16
+ end
17
+
18
+ # every pending operation becomes part of the current batch
19
+ @current_batch = @pending.dup
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOPromise
4
+ module ExecutorPool
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)
8
+ end
9
+
10
+ def execute_continue(ready_readers, ready_writers, ready_exceptions)
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
17
+
18
+ unless status.nil?
19
+ # 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)
24
+ end
25
+ end
26
+
27
+ # if we fall through to here, we have nothing to wait on.
28
+ [[], [], [], nil]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'faraday/connection'
4
+
5
+ module IOPromise
6
+ module Faraday
7
+ class << self
8
+ def new(url = nil, options = {}, &block)
9
+ options = ::Faraday.default_connection_options.merge(options)
10
+ ::IOPromise::Faraday::Connection.new(url, options) do |faraday|
11
+ faraday.adapter :typhoeus
12
+ block.call unless block.nil?
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ require_relative 'promise'
6
+
7
+ module IOPromise
8
+ module Faraday
9
+ class Connection < ::Faraday::Connection
10
+ def with_deferred_parallel
11
+ @parallel_manager = FaradayPromise.parallel_manager
12
+ yield
13
+ ensure
14
+ @parallel_manager = nil
15
+ end
16
+
17
+ def get_as_promise(*args, **kwargs)
18
+ @parallel_manager = FaradayPromise.parallel_manager
19
+ FaradayPromise.new(get(*args, **kwargs))
20
+ ensure
21
+ @parallel_manager = nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typhoeus'
4
+ require_relative 'multi_socket_action'
5
+
6
+ module IOPromise
7
+ module Faraday
8
+ class ContinuableHydra < Typhoeus::Hydra
9
+ class << self
10
+ def for_current_thread
11
+ Thread.current[:faraday_promise_typhoeus_hydra] ||= new
12
+ end
13
+ end
14
+
15
+ def initialize(options = {})
16
+ super(options)
17
+
18
+ @multi = MultiSocketAction.new(options.reject{|k,_| k==:max_concurrency})
19
+ end
20
+
21
+ def execute_continue(ready_readers, ready_writers, ready_exceptions)
22
+ # fill up the curl easy handle as much as possible
23
+ dequeue_many
24
+
25
+ @multi.execute_continue(ready_readers, ready_writers, ready_exceptions)
26
+ end
27
+ end
28
+ end
29
+ end