polyphony 0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|