iopromise 0.1.0
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 +7 -0
- data/.github/workflows/main.yml +21 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +191 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +9 -0
- data/iopromise.gemspec +30 -0
- data/lib/iopromise.rb +54 -0
- data/lib/iopromise/dalli.rb +13 -0
- data/lib/iopromise/dalli/client.rb +146 -0
- data/lib/iopromise/dalli/executor_pool.rb +13 -0
- data/lib/iopromise/dalli/patch_dalli.rb +337 -0
- data/lib/iopromise/dalli/promise.rb +52 -0
- data/lib/iopromise/dalli/response.rb +25 -0
- data/lib/iopromise/deferred.rb +13 -0
- data/lib/iopromise/deferred/executor_pool.rb +29 -0
- data/lib/iopromise/deferred/promise.rb +38 -0
- data/lib/iopromise/executor_context.rb +114 -0
- data/lib/iopromise/executor_pool/base.rb +47 -0
- data/lib/iopromise/executor_pool/batch.rb +23 -0
- data/lib/iopromise/executor_pool/sequential.rb +32 -0
- data/lib/iopromise/faraday.rb +17 -0
- data/lib/iopromise/faraday/connection.rb +25 -0
- data/lib/iopromise/faraday/continuable_hydra.rb +29 -0
- data/lib/iopromise/faraday/executor_pool.rb +19 -0
- data/lib/iopromise/faraday/multi_socket_action.rb +107 -0
- data/lib/iopromise/faraday/promise.rb +42 -0
- data/lib/iopromise/memcached.rb +13 -0
- data/lib/iopromise/memcached/client.rb +22 -0
- data/lib/iopromise/memcached/executor_pool.rb +61 -0
- data/lib/iopromise/memcached/promise.rb +32 -0
- data/lib/iopromise/rack/context_middleware.rb +20 -0
- data/lib/iopromise/version.rb +5 -0
- data/lib/iopromise/view_component.rb +9 -0
- data/lib/iopromise/view_component/data_loader.rb +62 -0
- metadata +101 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'continuable_hydra'
|
4
|
+
|
5
|
+
module IOPromise
|
6
|
+
module Faraday
|
7
|
+
class FaradayExecutorPool < IOPromise::ExecutorPool::Base
|
8
|
+
def execute_continue(ready_readers, ready_writers, ready_exceptions)
|
9
|
+
# mark all pending promises as executing since they could be started any time now.
|
10
|
+
# ideally we would do this on dequeue.
|
11
|
+
@pending.each do |promise|
|
12
|
+
begin_executing(promise) unless promise.started_executing?
|
13
|
+
end
|
14
|
+
|
15
|
+
ContinuableHydra.for_current_thread.execute_continue(ready_readers, ready_writers, ready_exceptions)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ethon'
|
4
|
+
|
5
|
+
Ethon::Curl.ffi_lib 'curl'
|
6
|
+
Ethon::Curl.attach_function :multi_socket_action, :curl_multi_socket_action, [:pointer, :int, :int, :pointer], :multi_code
|
7
|
+
|
8
|
+
module IOPromise
|
9
|
+
module Faraday
|
10
|
+
class MultiSocketAction < Ethon::Multi
|
11
|
+
CURL_POLL_NONE = 0
|
12
|
+
CURL_POLL_IN = 1
|
13
|
+
CURL_POLL_OUT = 2
|
14
|
+
CURL_POLL_INOUT = 3
|
15
|
+
CURL_POLL_REMOVE = 4
|
16
|
+
|
17
|
+
CURL_SOCKET_BAD = -1
|
18
|
+
CURL_SOCKET_TIMEOUT = CURL_SOCKET_BAD
|
19
|
+
|
20
|
+
CURLM_OK = 0
|
21
|
+
|
22
|
+
CURL_CSELECT_IN = 0x01
|
23
|
+
CURL_CSELECT_OUT = 0x02
|
24
|
+
CURL_CSELECT_ERR = 0x04
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
super(options)
|
28
|
+
|
29
|
+
@read_fds = {}
|
30
|
+
@write_fds = {}
|
31
|
+
@select_timeout = nil
|
32
|
+
|
33
|
+
self.socketfunction = @keep_socketfunction = proc do |handle, sock, what, userp, socketp|
|
34
|
+
if what == CURL_POLL_REMOVE
|
35
|
+
@read_fds.delete(sock)
|
36
|
+
@write_fds.delete(sock)
|
37
|
+
else
|
38
|
+
# reuse existing if we have it anywhere
|
39
|
+
io = @read_fds[sock] || @write_fds[sock] || IO.for_fd(sock).tap { |io| io.autoclose = false }
|
40
|
+
if what == CURL_POLL_INOUT
|
41
|
+
@read_fds[sock] = io
|
42
|
+
@write_fds[sock] = io
|
43
|
+
elsif what == CURL_POLL_IN
|
44
|
+
@read_fds[sock] = io
|
45
|
+
@write_fds.delete(sock)
|
46
|
+
elsif what == CURL_POLL_OUT
|
47
|
+
@read_fds.delete(sock)
|
48
|
+
@write_fds[sock] = io
|
49
|
+
end
|
50
|
+
end
|
51
|
+
CURLM_OK
|
52
|
+
end
|
53
|
+
|
54
|
+
self.timerfunction = @keep_timerfunction = proc do |handle, timeout_ms, userp|
|
55
|
+
if timeout_ms > 0x7fffffffffffffff # FIXME: wrongly encoded
|
56
|
+
@select_timeout = nil
|
57
|
+
else
|
58
|
+
@select_timeout = timeout_ms.to_f / 1_000
|
59
|
+
end
|
60
|
+
CURLM_OK
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def perform
|
65
|
+
# stubbed out, we don't want any of the multi_perform logic
|
66
|
+
end
|
67
|
+
|
68
|
+
def run
|
69
|
+
# stubbed out, we don't want any of the multi_perform logic
|
70
|
+
end
|
71
|
+
|
72
|
+
def execute_continue(ready_readers, ready_writers, ready_exceptions)
|
73
|
+
running_handles = ::FFI::MemoryPointer.new(:int)
|
74
|
+
|
75
|
+
flags = Hash.new(0)
|
76
|
+
|
77
|
+
unless ready_readers.nil?
|
78
|
+
ready_readers.each do |s|
|
79
|
+
flags[s.fileno] |= CURL_CSELECT_IN
|
80
|
+
end
|
81
|
+
end
|
82
|
+
unless ready_writers.nil?
|
83
|
+
ready_writers.each do |s|
|
84
|
+
flags[s.fileno] |= CURL_CSELECT_OUT
|
85
|
+
end
|
86
|
+
end
|
87
|
+
unless ready_exceptions.nil?
|
88
|
+
ready_exceptions.each do |s|
|
89
|
+
flags[s.fileno] |= CURL_CSELECT_ERR
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
flags.each do |fd, bitmask|
|
94
|
+
Ethon::Curl.multi_socket_action(handle, fd, bitmask, running_handles)
|
95
|
+
end
|
96
|
+
|
97
|
+
if flags.empty?
|
98
|
+
Ethon::Curl.multi_socket_action(handle, CURL_SOCKET_TIMEOUT, 0, running_handles)
|
99
|
+
end
|
100
|
+
|
101
|
+
check
|
102
|
+
|
103
|
+
[@read_fds.values, @write_fds.values, [], @select_timeout]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'continuable_hydra'
|
4
|
+
require_relative 'executor_pool'
|
5
|
+
|
6
|
+
module IOPromise
|
7
|
+
module Faraday
|
8
|
+
class FaradayPromise < ::IOPromise::Base
|
9
|
+
def self.parallel_manager
|
10
|
+
ContinuableHydra.for_current_thread
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(response = nil)
|
14
|
+
super()
|
15
|
+
|
16
|
+
@response = response
|
17
|
+
@started = false
|
18
|
+
|
19
|
+
unless @response.nil?
|
20
|
+
@response.on_complete do |response_env|
|
21
|
+
fulfill(@response)
|
22
|
+
execute_pool.complete(self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
::IOPromise::ExecutorContext.current.register(self) unless @response.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def wait
|
30
|
+
if @response.nil?
|
31
|
+
super
|
32
|
+
else
|
33
|
+
::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute_pool
|
38
|
+
FaradayExecutorPool.for(Thread.current)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'memcached'
|
4
|
+
require_relative 'promise'
|
5
|
+
|
6
|
+
module IOPromise
|
7
|
+
module Memcached
|
8
|
+
class Client
|
9
|
+
def initialize(*args, **kwargs)
|
10
|
+
if args.first.is_a?(::Memcached::Client)
|
11
|
+
@client = args.first.clone
|
12
|
+
else
|
13
|
+
@client = ::Memcached::Client.new(*args, **kwargs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_as_promise(key)
|
18
|
+
MemcachePromise.new(@client, key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IOPromise
|
4
|
+
module Memcached
|
5
|
+
class MemcacheExecutorPool < ::IOPromise::ExecutorPool::Batch
|
6
|
+
def next_batch
|
7
|
+
super
|
8
|
+
|
9
|
+
unless @current_batch.empty?
|
10
|
+
@keys_to_promises = @current_batch.group_by { |promise| promise.key }
|
11
|
+
@current_batch.each { |promise| begin_executing(promise) }
|
12
|
+
begin
|
13
|
+
memcache_client.begin_get_multi(@keys_to_promises.keys)
|
14
|
+
rescue => e
|
15
|
+
@keys_to_promises.values.flatten.each do |promise|
|
16
|
+
promise.reject(e)
|
17
|
+
complete(promise)
|
18
|
+
@current_batch.delete(promise)
|
19
|
+
end
|
20
|
+
|
21
|
+
@keys_to_promises = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute_continue(ready_readers, ready_writers, ready_exceptions)
|
27
|
+
if @current_batch.empty?
|
28
|
+
next_batch
|
29
|
+
end
|
30
|
+
|
31
|
+
return [[], [], [], nil] if @current_batch.empty?
|
32
|
+
|
33
|
+
so_far, readers, writers = memcache_client.continue_get_multi
|
34
|
+
|
35
|
+
# when we're done (nothing to wait on), fill in any remaining keys with nil for completions to occur
|
36
|
+
if readers.empty? && writers.empty?
|
37
|
+
@keys_to_promises.each do |key, _|
|
38
|
+
so_far[key] = nil unless so_far.include? key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
so_far.each do |key, value|
|
43
|
+
next unless @keys_to_promises[key]
|
44
|
+
@keys_to_promises[key].each do |promise|
|
45
|
+
next if promise.fulfilled?
|
46
|
+
|
47
|
+
promise.fulfill(value)
|
48
|
+
complete(promise)
|
49
|
+
@current_batch.delete(promise)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
[readers, writers, [], nil]
|
54
|
+
end
|
55
|
+
|
56
|
+
def memcache_client
|
57
|
+
@connection_pool
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'executor_pool'
|
4
|
+
|
5
|
+
module IOPromise
|
6
|
+
module Memcached
|
7
|
+
class MemcachePromise < ::IOPromise::Base
|
8
|
+
attr_reader :key
|
9
|
+
|
10
|
+
def initialize(client = nil, key = nil)
|
11
|
+
super()
|
12
|
+
|
13
|
+
@client = client
|
14
|
+
@key = key
|
15
|
+
|
16
|
+
::IOPromise::ExecutorContext.current.register(self) unless @client.nil? || @key.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait
|
20
|
+
if @client.nil? || @key.nil?
|
21
|
+
super
|
22
|
+
else
|
23
|
+
::IOPromise::ExecutorContext.current.wait_for_all_data(end_when_complete: self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute_pool
|
28
|
+
MemcacheExecutorPool.for(@client)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module IOPromise
|
3
|
+
module Rack
|
4
|
+
class ContextMiddleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
::IOPromise::ExecutorContext.push
|
11
|
+
begin
|
12
|
+
status, headers, body = @app.call(env)
|
13
|
+
ensure
|
14
|
+
::IOPromise::ExecutorContext.pop
|
15
|
+
end
|
16
|
+
[status, headers, body]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "view_component/engine"
|
4
|
+
|
5
|
+
module IOPromise
|
6
|
+
module ViewComponent
|
7
|
+
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
|
22
|
+
|
23
|
+
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
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_in(*)
|
57
|
+
sync
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iopromise
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Theo Julienne
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: promise.rb
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.7.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.7.4
|
27
|
+
description: This gem extends promise.rb promises to support an extremely simple pattern
|
28
|
+
for "continuing" execution of the promise in an asynchronous non-blocking way.
|
29
|
+
email:
|
30
|
+
- theo.julienne@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".github/workflows/main.yml"
|
36
|
+
- ".gitignore"
|
37
|
+
- ".rspec"
|
38
|
+
- CODE_OF_CONDUCT.md
|
39
|
+
- Gemfile
|
40
|
+
- Gemfile.lock
|
41
|
+
- LICENSE
|
42
|
+
- LICENSE.txt
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- bin/console
|
46
|
+
- bin/setup
|
47
|
+
- iopromise.gemspec
|
48
|
+
- 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
|
55
|
+
- lib/iopromise/deferred.rb
|
56
|
+
- lib/iopromise/deferred/executor_pool.rb
|
57
|
+
- lib/iopromise/deferred/promise.rb
|
58
|
+
- lib/iopromise/executor_context.rb
|
59
|
+
- lib/iopromise/executor_pool/base.rb
|
60
|
+
- lib/iopromise/executor_pool/batch.rb
|
61
|
+
- 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
|
+
- lib/iopromise/rack/context_middleware.rb
|
73
|
+
- lib/iopromise/version.rb
|
74
|
+
- lib/iopromise/view_component.rb
|
75
|
+
- lib/iopromise/view_component/data_loader.rb
|
76
|
+
homepage: https://github.com/theojulienne/iopromise
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/theojulienne/iopromise
|
81
|
+
source_code_uri: https://github.com/theojulienne/iopromise
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 2.4.0
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubygems_version: 3.2.9
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Simple non-blocking IO promises for Ruby.
|
101
|
+
test_files: []
|