polyphony 0.13
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/CHANGELOG.md +86 -0
- data/README.md +400 -0
- data/ext/ev/extconf.rb +19 -0
- data/lib/polyphony.rb +26 -0
- data/lib/polyphony/core.rb +45 -0
- data/lib/polyphony/core/async.rb +36 -0
- data/lib/polyphony/core/cancel_scope.rb +61 -0
- data/lib/polyphony/core/channel.rb +39 -0
- data/lib/polyphony/core/coroutine.rb +106 -0
- data/lib/polyphony/core/exceptions.rb +24 -0
- data/lib/polyphony/core/fiber_pool.rb +98 -0
- data/lib/polyphony/core/supervisor.rb +75 -0
- data/lib/polyphony/core/sync.rb +20 -0
- data/lib/polyphony/core/thread.rb +49 -0
- data/lib/polyphony/core/thread_pool.rb +58 -0
- data/lib/polyphony/core/throttler.rb +38 -0
- data/lib/polyphony/extensions/io.rb +62 -0
- data/lib/polyphony/extensions/kernel.rb +161 -0
- data/lib/polyphony/extensions/postgres.rb +96 -0
- data/lib/polyphony/extensions/redis.rb +68 -0
- data/lib/polyphony/extensions/socket.rb +85 -0
- data/lib/polyphony/extensions/ssl.rb +73 -0
- data/lib/polyphony/fs.rb +22 -0
- data/lib/polyphony/http/agent.rb +214 -0
- data/lib/polyphony/http/http1.rb +124 -0
- data/lib/polyphony/http/http1_request.rb +71 -0
- data/lib/polyphony/http/http2.rb +66 -0
- data/lib/polyphony/http/http2_request.rb +69 -0
- data/lib/polyphony/http/rack.rb +27 -0
- data/lib/polyphony/http/server.rb +43 -0
- data/lib/polyphony/line_reader.rb +82 -0
- data/lib/polyphony/net.rb +59 -0
- data/lib/polyphony/net_old.rb +299 -0
- data/lib/polyphony/resource_pool.rb +56 -0
- data/lib/polyphony/server_task.rb +18 -0
- data/lib/polyphony/testing.rb +34 -0
- data/lib/polyphony/version.rb +5 -0
- metadata +170 -0
data/ext/ev/extconf.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
|
5
|
+
require "mkmf"
|
6
|
+
|
7
|
+
have_header("unistd.h")
|
8
|
+
|
9
|
+
$defs << "-DEV_USE_SELECT" if have_header("sys/select.h")
|
10
|
+
$defs << "-DEV_USE_POLL" if have_type("port_event_t", "poll.h")
|
11
|
+
$defs << "-DEV_USE_EPOLL" if have_header("sys/epoll.h")
|
12
|
+
$defs << "-DEV_USE_KQUEUE" if have_header("sys/event.h") && have_header("sys/queue.h")
|
13
|
+
$defs << "-DEV_USE_PORT" if have_type("port_event_t", "port.h")
|
14
|
+
$defs << "-DHAVE_SYS_RESOURCE_H" if have_header("sys/resource.h")
|
15
|
+
|
16
|
+
CONFIG["optflags"] << " -fno-strict-aliasing"
|
17
|
+
|
18
|
+
dir_config "ev_ext"
|
19
|
+
create_makefile "ev_ext"
|
data/lib/polyphony.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modulation/gem'
|
4
|
+
|
5
|
+
export_default :Polyphony
|
6
|
+
|
7
|
+
Polyphony = import('./polyphony/core')
|
8
|
+
Exceptions = import('./polyphony/core/exceptions')
|
9
|
+
|
10
|
+
module Polyphony
|
11
|
+
Cancel = Exceptions::Cancel
|
12
|
+
MoveOn = Exceptions::MoveOn
|
13
|
+
|
14
|
+
auto_import(
|
15
|
+
Channel: './polyphony/core/channel',
|
16
|
+
Coroutine: './polyphony/core/coroutine',
|
17
|
+
Sync: './polyphony/core/sync',
|
18
|
+
Thread: './polyphony/core/thread',
|
19
|
+
ThreadPool: './polyphony/core/thread_pool',
|
20
|
+
|
21
|
+
FS: './polyphony/fs',
|
22
|
+
Net: './polyphony/net',
|
23
|
+
ResourcePool: './polyphony/resource_pool',
|
24
|
+
Supervisor: './polyphony/supervisor'
|
25
|
+
)
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Core
|
4
|
+
|
5
|
+
require 'fiber'
|
6
|
+
require_relative '../ev_ext'
|
7
|
+
|
8
|
+
import('./extensions/kernel')
|
9
|
+
FiberPool = import('./core/fiber_pool')
|
10
|
+
|
11
|
+
# Core module, containing async and reactor methods
|
12
|
+
module Core
|
13
|
+
def self.trap(sig, ref = false, &callback)
|
14
|
+
sig = Signal.list[sig.to_s.upcase] if sig.is_a?(Symbol)
|
15
|
+
watcher = EV::Signal.new(sig, &callback)
|
16
|
+
EV.unref unless ref
|
17
|
+
watcher
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.fork(&block)
|
21
|
+
EV.break
|
22
|
+
pid = Kernel.fork do
|
23
|
+
FiberPool.reset!
|
24
|
+
EV.post_fork
|
25
|
+
Fiber.current.coroutine = Coroutine.new(Fiber.current)
|
26
|
+
|
27
|
+
block.()
|
28
|
+
|
29
|
+
# We cannot simply depend on the at_exit block (see below) to yield to the
|
30
|
+
# reactor fiber. Doing that will raise a FiberError complaining: "fiber
|
31
|
+
# called across stack rewinding barrier". Apparently this is a bug in
|
32
|
+
# Ruby, so the workaround is to yield just before exiting.
|
33
|
+
suspend
|
34
|
+
end
|
35
|
+
EV.restart
|
36
|
+
pid
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
at_exit do
|
41
|
+
# in most cases, by the main fiber is done there are still pending or other
|
42
|
+
# or asynchronous operations going on. If the reactor loop is not done, we
|
43
|
+
# suspend the root fiber until it is done
|
44
|
+
suspend if $__reactor_fiber__.alive?
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :async_decorate, :call_proc_with_optional_block
|
4
|
+
|
5
|
+
Coroutine = import('./coroutine')
|
6
|
+
FiberPool = import('./fiber_pool')
|
7
|
+
|
8
|
+
# Converts a regular method into an async method, i.e. a method that returns a
|
9
|
+
# proc that eventually executes the original code.
|
10
|
+
# @param receiver [Object] object receiving the method call
|
11
|
+
# @param sym [Symbol] method name
|
12
|
+
# @return [void]
|
13
|
+
def async_decorate(receiver, sym)
|
14
|
+
sync_sym = :"sync_#{sym}"
|
15
|
+
receiver.alias_method(sync_sym, sym)
|
16
|
+
receiver.define_method(sym) do |*args, &block|
|
17
|
+
Coroutine.new { send(sync_sym, *args, &block) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Calls a proc with a block if both are given. Otherwise, call the first
|
22
|
+
# non-nil proc. This allows syntax such as:
|
23
|
+
#
|
24
|
+
# # in fact, the call to #nexus returns a proc which takes a block
|
25
|
+
# await nexus { ... }
|
26
|
+
#
|
27
|
+
# @param proc [Proc] proc A
|
28
|
+
# @param block [Proc] proc B
|
29
|
+
# @return [any] return value of proc invocation
|
30
|
+
def call_proc_with_optional_block(proc, block)
|
31
|
+
if proc && block
|
32
|
+
proc.call(&block)
|
33
|
+
else
|
34
|
+
(proc || block).call
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :CancelScope
|
4
|
+
|
5
|
+
require 'fiber'
|
6
|
+
|
7
|
+
Exceptions = import('./exceptions')
|
8
|
+
|
9
|
+
# A cancellation scope that can be used to cancel an asynchronous task
|
10
|
+
class CancelScope
|
11
|
+
def initialize(opts = {})
|
12
|
+
@opts = opts
|
13
|
+
@error_class = @opts[:mode] == :cancel ? Exceptions::Cancel : Exceptions::MoveOn
|
14
|
+
end
|
15
|
+
|
16
|
+
def cancel!
|
17
|
+
@cancelled = true
|
18
|
+
@fiber.cancelled = true
|
19
|
+
@fiber.transfer @error_class.new(self, @opts[:value])
|
20
|
+
end
|
21
|
+
|
22
|
+
def start_timeout
|
23
|
+
@timeout = EV::Timer.new(@opts[:timeout], 0)
|
24
|
+
@timeout.start { cancel! }
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset_timeout
|
28
|
+
@timeout.reset
|
29
|
+
end
|
30
|
+
|
31
|
+
def disable
|
32
|
+
@timeout&.stop
|
33
|
+
end
|
34
|
+
|
35
|
+
def call
|
36
|
+
start_timeout if @opts[:timeout]
|
37
|
+
@fiber = Fiber.current
|
38
|
+
@fiber.cancelled = nil
|
39
|
+
yield self
|
40
|
+
rescue Exceptions::MoveOn => e
|
41
|
+
e.scope == self ? e.value : raise(e)
|
42
|
+
ensure
|
43
|
+
@timeout&.stop
|
44
|
+
protect(&@when_cancelled) if @cancelled && @when_cancelled
|
45
|
+
end
|
46
|
+
|
47
|
+
def when_cancelled(&block)
|
48
|
+
@when_cancelled = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def cancelled?
|
52
|
+
@cancelled
|
53
|
+
end
|
54
|
+
|
55
|
+
def protect(&block)
|
56
|
+
@fiber.cancelled = false
|
57
|
+
block.()
|
58
|
+
ensure
|
59
|
+
@fiber.cancelled = @cancelled
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Channel
|
4
|
+
|
5
|
+
Exceptions = import('./exceptions')
|
6
|
+
|
7
|
+
class Channel
|
8
|
+
def initialize
|
9
|
+
@payload_queue = []
|
10
|
+
@waiting_queue = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def close
|
14
|
+
stop = Exceptions::MoveOn.new
|
15
|
+
@waiting_queue.slice(0..-1).each { |f| f.schedule(stop) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(o)
|
19
|
+
if @waiting_queue.empty?
|
20
|
+
@payload_queue << o
|
21
|
+
else
|
22
|
+
@waiting_queue.shift&.schedule(o)
|
23
|
+
end
|
24
|
+
EV.snooze
|
25
|
+
end
|
26
|
+
|
27
|
+
def receive
|
28
|
+
EV.ref
|
29
|
+
if @payload_queue.empty?
|
30
|
+
@waiting_queue << Fiber.current
|
31
|
+
else
|
32
|
+
payload = @payload_queue.shift
|
33
|
+
Fiber.current.schedule(payload)
|
34
|
+
end
|
35
|
+
suspend
|
36
|
+
ensure
|
37
|
+
EV.unref
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Coroutine
|
4
|
+
|
5
|
+
import('../extensions/kernel')
|
6
|
+
|
7
|
+
FiberPool = import('./fiber_pool')
|
8
|
+
Exceptions = import('./exceptions')
|
9
|
+
|
10
|
+
# Encapsulates an asynchronous task
|
11
|
+
class Coroutine
|
12
|
+
attr_reader :result, :fiber
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(fiber = nil, &block)
|
16
|
+
@fiber = fiber
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(&block2)
|
21
|
+
@caller = caller if Exceptions.debug
|
22
|
+
|
23
|
+
@fiber = FiberPool.spawn do
|
24
|
+
@fiber.coroutine = self
|
25
|
+
@result = (@block || block2).call(self)
|
26
|
+
rescue Exceptions::MoveOn, Exceptions::Stop => e
|
27
|
+
@result = e.value
|
28
|
+
rescue Exception => e
|
29
|
+
e.cleanup_backtrace(@caller) if Exceptions.debug
|
30
|
+
@result = e
|
31
|
+
ensure
|
32
|
+
@fiber.coroutine = nil
|
33
|
+
@fiber = nil
|
34
|
+
@awaiting_fiber&.schedule @result
|
35
|
+
@when_done&.()
|
36
|
+
|
37
|
+
# if result is an error and nobody's waiting on us, we need to raise it
|
38
|
+
raise @result if @result.is_a?(Exception) && !@awaiting_fiber
|
39
|
+
end
|
40
|
+
|
41
|
+
@ran = true
|
42
|
+
@fiber.schedule
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def <<(o)
|
47
|
+
@mailbox ||= []
|
48
|
+
@mailbox << o
|
49
|
+
@fiber&.schedule if @receive_waiting
|
50
|
+
EV.snooze
|
51
|
+
end
|
52
|
+
|
53
|
+
def receive
|
54
|
+
EV.ref
|
55
|
+
@receive_waiting = true
|
56
|
+
@fiber&.schedule if @mailbox && @mailbox.size > 0
|
57
|
+
suspend
|
58
|
+
@mailbox.shift
|
59
|
+
ensure
|
60
|
+
EV.unref
|
61
|
+
@receive_waiting = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def running?
|
65
|
+
@fiber
|
66
|
+
end
|
67
|
+
|
68
|
+
# Kernel.await expects the given argument / block to be a callable, so #call
|
69
|
+
# in fact waits for the coroutine to finish
|
70
|
+
def await
|
71
|
+
run unless @ran
|
72
|
+
if @fiber
|
73
|
+
@awaiting_fiber = Fiber.current
|
74
|
+
suspend
|
75
|
+
else
|
76
|
+
@result
|
77
|
+
end
|
78
|
+
ensure
|
79
|
+
# if awaiting was interrupted and the coroutine is still running, we need to stop it
|
80
|
+
if @fiber
|
81
|
+
@fiber&.schedule(Exceptions::MoveOn.new)
|
82
|
+
suspend
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def when_done(&block)
|
87
|
+
@when_done = block
|
88
|
+
end
|
89
|
+
|
90
|
+
def resume(value = nil)
|
91
|
+
@fiber&.schedule(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def interrupt(value = Exceptions::MoveOn.new)
|
95
|
+
@fiber&.schedule(value)
|
96
|
+
end
|
97
|
+
alias_method :stop, :interrupt
|
98
|
+
|
99
|
+
def cancel!
|
100
|
+
interrupt(Exceptions::Cancel.new)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.current
|
104
|
+
Fiber.current.coroutine
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :CoroutineInterrupt, :MoveOn, :Stop, :Cancel, :debug, :debug=
|
4
|
+
|
5
|
+
class CoroutineInterrupt < ::Exception
|
6
|
+
attr_reader :scope, :value
|
7
|
+
|
8
|
+
def initialize(scope = nil, value = nil)
|
9
|
+
@scope = scope
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Stop < CoroutineInterrupt; end
|
15
|
+
class MoveOn < CoroutineInterrupt; end
|
16
|
+
class Cancel < CoroutineInterrupt; end
|
17
|
+
|
18
|
+
def debug
|
19
|
+
@debug
|
20
|
+
end
|
21
|
+
|
22
|
+
def debug=(value)
|
23
|
+
@debug = value
|
24
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :available,
|
4
|
+
:checked_out,
|
5
|
+
:reset!,
|
6
|
+
:size,
|
7
|
+
:spawn
|
8
|
+
|
9
|
+
require 'fiber'
|
10
|
+
|
11
|
+
# Array of available fibers
|
12
|
+
@pool = []
|
13
|
+
|
14
|
+
# Array of fibers in use
|
15
|
+
@checked_out = {}
|
16
|
+
|
17
|
+
# Fiber count
|
18
|
+
@count = 0
|
19
|
+
|
20
|
+
# Returns number of available fibers in pool
|
21
|
+
# @return [Integer] available fibers count
|
22
|
+
def available
|
23
|
+
@pool.size
|
24
|
+
end
|
25
|
+
|
26
|
+
def checked_out
|
27
|
+
@checked_out.size
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns size of fiber pool (including currently used fiber)
|
31
|
+
# @return [Integer] fiber pool size
|
32
|
+
def size
|
33
|
+
@count
|
34
|
+
end
|
35
|
+
|
36
|
+
def downsize
|
37
|
+
return if @count < 5
|
38
|
+
max_available = @count >= 5 ? @count / 5 : 2
|
39
|
+
if @pool.count > max_available
|
40
|
+
@pool.slice!(max_available, 50).each { |f| f.transfer :stop }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@downsize_timer = EV::Timer.new(5, 5)
|
45
|
+
@downsize_timer.start { downsize }
|
46
|
+
EV.unref
|
47
|
+
|
48
|
+
# Invokes the given block using a fiber taken from the fiber pool. If the pool
|
49
|
+
# is exhausted, a new fiber will be created.
|
50
|
+
# @return [Fiber]
|
51
|
+
def spawn(&block)
|
52
|
+
fiber = @pool.empty? ? new_fiber : @pool.shift
|
53
|
+
fiber.next_job = block
|
54
|
+
fiber
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset!
|
58
|
+
@count = 0
|
59
|
+
@pool = []
|
60
|
+
@checked_out = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Creates a new fiber to be added to the pool
|
64
|
+
# @return [Fiber] new fiber
|
65
|
+
def new_fiber
|
66
|
+
Fiber.new { fiber_loop }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Runs a job-processing loop inside the current fiber
|
70
|
+
# @return [void]
|
71
|
+
def fiber_loop
|
72
|
+
fiber = Fiber.current
|
73
|
+
@count += 1
|
74
|
+
error = nil
|
75
|
+
loop do
|
76
|
+
job, fiber.next_job = fiber.next_job, nil
|
77
|
+
@checked_out[fiber] = true
|
78
|
+
fiber.cancelled = nil
|
79
|
+
|
80
|
+
job&.(fiber)
|
81
|
+
|
82
|
+
@pool << fiber
|
83
|
+
@checked_out.delete(fiber)
|
84
|
+
break if suspend == :stop
|
85
|
+
end
|
86
|
+
rescue => e
|
87
|
+
# uncaught error
|
88
|
+
error = e
|
89
|
+
ensure
|
90
|
+
@pool.delete(self)
|
91
|
+
@checked_out.delete(fiber)
|
92
|
+
@count -= 1
|
93
|
+
|
94
|
+
# We need to explicitly transfer control to reactor fiber, otherwise it will
|
95
|
+
# be transferred to the main fiber, which would normally be blocking on
|
96
|
+
# something
|
97
|
+
$__reactor_fiber__.transfer unless error
|
98
|
+
end
|