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
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Supervisor
|
4
|
+
|
5
|
+
Coroutine = import('./coroutine')
|
6
|
+
Exceptions = import('./exceptions')
|
7
|
+
|
8
|
+
class Supervisor
|
9
|
+
def initialize
|
10
|
+
@coroutines = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def await(&block)
|
14
|
+
@supervisor_fiber = Fiber.current
|
15
|
+
block&.(self)
|
16
|
+
suspend
|
17
|
+
rescue Exceptions::MoveOn => e
|
18
|
+
e.value
|
19
|
+
ensure
|
20
|
+
if still_running?
|
21
|
+
stop_all_tasks
|
22
|
+
suspend
|
23
|
+
else
|
24
|
+
@supervisor_fiber = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def spawn(proc = nil, &block)
|
29
|
+
if proc.is_a?(Coroutine)
|
30
|
+
spawn_coroutine(proc)
|
31
|
+
else
|
32
|
+
spawn_proc(block || proc)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def spawn_coroutine(proc)
|
37
|
+
@coroutines << proc
|
38
|
+
proc.when_done { task_completed(proc) }
|
39
|
+
proc.run unless proc.running?
|
40
|
+
proc
|
41
|
+
end
|
42
|
+
|
43
|
+
def spawn_proc(proc)
|
44
|
+
@coroutines << Object.spawn do |coroutine|
|
45
|
+
proc.call(coroutine)
|
46
|
+
task_completed(coroutine)
|
47
|
+
rescue Exception => e
|
48
|
+
task_completed(coroutine)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def still_running?
|
53
|
+
!@coroutines.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop!(result = nil)
|
57
|
+
return unless @supervisor_fiber
|
58
|
+
|
59
|
+
@supervisor_fiber&.transfer Exceptions::MoveOn.new(nil, result)
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop_all_tasks
|
63
|
+
exception = Exceptions::Stop.new
|
64
|
+
@coroutines.each do |c|
|
65
|
+
EV.next_tick { c.interrupt(exception) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def task_completed(coroutine)
|
70
|
+
return unless @coroutines.include?(coroutine)
|
71
|
+
|
72
|
+
@coroutines.delete(coroutine)
|
73
|
+
@supervisor_fiber&.transfer if @coroutines.empty?
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Mutex
|
4
|
+
|
5
|
+
# Implements mutex lock for synchronizing async operations
|
6
|
+
class Mutex
|
7
|
+
def initialize
|
8
|
+
@waiting = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def synchronize
|
12
|
+
fiber = Fiber.current
|
13
|
+
@waiting << fiber
|
14
|
+
suspend if @waiting.size > 1
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
@waiting.delete(fiber)
|
18
|
+
EV.next_tick { @waiting[0]&.transfer } unless @waiting.empty?
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :spawn
|
4
|
+
|
5
|
+
Exceptions = import('./exceptions')
|
6
|
+
|
7
|
+
# Runs the given block in a separate thread, returning a promise fulfilled
|
8
|
+
# once the thread is done. The signalling for the thread is done using an
|
9
|
+
# I/O pipe.
|
10
|
+
# @return [Proc]
|
11
|
+
def spawn(&block)
|
12
|
+
async do
|
13
|
+
ctx = {
|
14
|
+
fiber: Fiber.current,
|
15
|
+
watcher: EV::Async.new { complete_thread_task(ctx) },
|
16
|
+
thread: Thread.new { run_in_thread(ctx, &block) }
|
17
|
+
}
|
18
|
+
ctx[:thread].report_on_exception = false
|
19
|
+
ctx[:thread].abort_on_exception = false
|
20
|
+
wait_for_thread(ctx)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for_thread(ctx)
|
25
|
+
suspend
|
26
|
+
rescue Exceptions::CoroutineInterrupt => e
|
27
|
+
ctx[:fiber] = nil
|
28
|
+
ctx[:thread]&.raise(e)
|
29
|
+
raise e
|
30
|
+
ensure
|
31
|
+
ctx[:watcher]&.stop
|
32
|
+
end
|
33
|
+
|
34
|
+
def complete_thread_task(ctx)
|
35
|
+
ctx[:fiber]&.transfer ctx[:value]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Runs the given block, passing the result or exception to the given context
|
39
|
+
# @param ctx [Hash] context
|
40
|
+
# @return [void]
|
41
|
+
def run_in_thread(ctx)
|
42
|
+
ctx[:value] = yield
|
43
|
+
ctx[:thread] = nil
|
44
|
+
ctx[:watcher].signal!
|
45
|
+
rescue Exception => e
|
46
|
+
ctx[:value] = e
|
47
|
+
ctx[:watcher].signal! if ctx[:fiber]
|
48
|
+
raise e
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :process, :setup, :size=, :busy?
|
4
|
+
|
5
|
+
@size = 10
|
6
|
+
|
7
|
+
def process(&block)
|
8
|
+
setup unless @task_queue
|
9
|
+
|
10
|
+
start_task_on_thread(block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_task_on_thread(block)
|
14
|
+
EV.ref
|
15
|
+
@task_queue << [block, Fiber.current]
|
16
|
+
suspend
|
17
|
+
ensure
|
18
|
+
EV.unref
|
19
|
+
end
|
20
|
+
|
21
|
+
def size=(size)
|
22
|
+
@size = size
|
23
|
+
end
|
24
|
+
|
25
|
+
def busy?
|
26
|
+
!@queue.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup
|
30
|
+
@task_queue = ::Queue.new
|
31
|
+
@resolve_queue = ::Queue.new
|
32
|
+
|
33
|
+
@async_watcher = EV::Async.new { resolve_from_queue }
|
34
|
+
EV.unref
|
35
|
+
|
36
|
+
@threads = (1..@size).map { Thread.new { thread_loop } }
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve_from_queue
|
40
|
+
until @resolve_queue.empty?
|
41
|
+
(fiber, result) = @resolve_queue.pop(true)
|
42
|
+
fiber.transfer result unless fiber.cancelled?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def thread_loop
|
47
|
+
loop { run_queued_task }
|
48
|
+
end
|
49
|
+
|
50
|
+
def run_queued_task
|
51
|
+
(block, fiber) = @task_queue.pop
|
52
|
+
result = block.()
|
53
|
+
@resolve_queue << [fiber, result]
|
54
|
+
@async_watcher.signal!
|
55
|
+
rescue Exception => e
|
56
|
+
@resolve_queue << [fiber, e]
|
57
|
+
@async_watcher.signal!
|
58
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Throttler
|
4
|
+
|
5
|
+
class Throttler
|
6
|
+
def initialize(rate)
|
7
|
+
@rate = rate_from_argument(rate)
|
8
|
+
@min_dt = 1.0 / @rate
|
9
|
+
@last_iteration_clock = clock - @min_dt
|
10
|
+
end
|
11
|
+
|
12
|
+
def clock
|
13
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(&block)
|
17
|
+
now = clock
|
18
|
+
dt = now - @last_iteration_clock
|
19
|
+
if dt < @min_dt
|
20
|
+
sleep(@min_dt - dt)
|
21
|
+
end
|
22
|
+
@last_iteration_clock = dt > @min_dt ? now : @last_iteration_clock + @min_dt
|
23
|
+
block.call(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :process, :call
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def rate_from_argument(arg)
|
31
|
+
return arg if arg.is_a?(Numeric)
|
32
|
+
if arg.is_a?(Hash)
|
33
|
+
return 1.0 / arg[:interval] if arg[:interval]
|
34
|
+
return arg[:rate] if arg[:rate]
|
35
|
+
end
|
36
|
+
raise "Invalid rate argument #{arg.inspect}"
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ::IO
|
4
|
+
def read_watcher
|
5
|
+
@read_watcher ||= EV::IO.new(self, :r)
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_watcher
|
9
|
+
@write_watcher ||= EV::IO.new(self, :w)
|
10
|
+
end
|
11
|
+
|
12
|
+
def stop_watchers
|
13
|
+
@read_watcher&.stop
|
14
|
+
@write_watcher&.stop
|
15
|
+
end
|
16
|
+
|
17
|
+
NO_EXCEPTION = { exception: false }.freeze
|
18
|
+
|
19
|
+
def read(max = 8192, outbuf = nil)
|
20
|
+
outbuf ||= (@read_buffer ||= +'')
|
21
|
+
loop do
|
22
|
+
result = read_nonblock(max, outbuf, NO_EXCEPTION)
|
23
|
+
case result
|
24
|
+
when nil then raise IOError
|
25
|
+
when :wait_readable then read_watcher.await
|
26
|
+
else return result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
ensure
|
30
|
+
@read_watcher&.stop
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(data)
|
34
|
+
loop do
|
35
|
+
result = write_nonblock(data, NO_EXCEPTION)
|
36
|
+
case result
|
37
|
+
when nil then raise IOError
|
38
|
+
when :wait_writable then write_watcher.await
|
39
|
+
else
|
40
|
+
(result == data.bytesize) ? (return result) : (data = data[result..-1])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
@write_watcher&.stop
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_list(*list)
|
48
|
+
list.each do |data|
|
49
|
+
loop do
|
50
|
+
result = write_nonblock(data, NO_EXCEPTION)
|
51
|
+
case result
|
52
|
+
when nil then raise IOError
|
53
|
+
when :wait_writable then write_watcher.await
|
54
|
+
else
|
55
|
+
(result == data.bytesize) ? (break result) : (data = data[result..-1])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
@write_watcher&.stop
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
CancelScope = import('../core/cancel_scope')
|
6
|
+
Coroutine = import('../core/coroutine')
|
7
|
+
Exceptions = import('../core/exceptions')
|
8
|
+
Supervisor = import('../core/supervisor')
|
9
|
+
Throttler = import('../core/throttler')
|
10
|
+
|
11
|
+
# Fiber extensions
|
12
|
+
class ::Fiber
|
13
|
+
attr_writer :cancelled
|
14
|
+
attr_accessor :next_job, :coroutine
|
15
|
+
|
16
|
+
def cancelled?
|
17
|
+
@cancelled
|
18
|
+
end
|
19
|
+
|
20
|
+
def root?
|
21
|
+
@root
|
22
|
+
end
|
23
|
+
|
24
|
+
def schedule(value = nil)
|
25
|
+
EV.schedule_fiber(self, value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_root!
|
29
|
+
@root = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.root
|
33
|
+
@@root
|
34
|
+
end
|
35
|
+
|
36
|
+
@@root = Fiber.current
|
37
|
+
@@root.set_root!
|
38
|
+
|
39
|
+
# Associate a (pseudo-)coroutine with the main fiber
|
40
|
+
current.coroutine = Coroutine.new(Fiber.current)
|
41
|
+
end
|
42
|
+
|
43
|
+
class ::Exception
|
44
|
+
SANITIZE_RE = /lib\/polyphony/.freeze
|
45
|
+
SANITIZE_PROC = proc { |l| l !~ SANITIZE_RE }
|
46
|
+
|
47
|
+
def cleanup_backtrace(caller = nil)
|
48
|
+
combined = caller ? backtrace + caller : backtrace
|
49
|
+
set_backtrace(combined.select(&SANITIZE_PROC))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Kernel extensions (methods available to all objects)
|
54
|
+
module ::Kernel
|
55
|
+
def after(duration, &block)
|
56
|
+
EV::Timer.new(freq, 0).start(&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def async(sym = nil, &block)
|
60
|
+
if sym
|
61
|
+
async_decorate(is_a?(Class) ? self : singleton_class, sym)
|
62
|
+
else
|
63
|
+
Coroutine.new(&block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Converts a regular method into an async method, i.e. a method that returns a
|
68
|
+
# proc that eventually executes the original code.
|
69
|
+
# @param receiver [Object] object receiving the method call
|
70
|
+
# @param sym [Symbol] method name
|
71
|
+
# @return [void]
|
72
|
+
def async_decorate(receiver, sym)
|
73
|
+
sync_sym = :"sync_#{sym}"
|
74
|
+
receiver.alias_method(sync_sym, sym)
|
75
|
+
receiver.define_method(sym) do |*args, &block|
|
76
|
+
Coroutine.new { send(sync_sym, *args, &block) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def cancel_after(duration, &block)
|
81
|
+
CancelScope.new(timeout: duration, mode: :cancel).(&block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def every(freq, &block)
|
85
|
+
EV::Timer.new(freq, freq).start(&block)
|
86
|
+
end
|
87
|
+
|
88
|
+
def move_on_after(duration, &block)
|
89
|
+
CancelScope.new(timeout: duration).(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
class Pulser
|
93
|
+
def initialize(freq)
|
94
|
+
fiber = Fiber.current
|
95
|
+
@timer = EV::Timer.new(freq, freq)
|
96
|
+
@timer.start { fiber.transfer freq }
|
97
|
+
end
|
98
|
+
|
99
|
+
def await
|
100
|
+
suspend
|
101
|
+
rescue Exception => e
|
102
|
+
@timer.stop
|
103
|
+
raise e
|
104
|
+
end
|
105
|
+
|
106
|
+
def stop
|
107
|
+
@timer.stop
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def pulse(freq)
|
112
|
+
Pulser.new(freq)
|
113
|
+
end
|
114
|
+
|
115
|
+
def receive
|
116
|
+
Fiber.current.coroutine.receive
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :sync_sleep, :sleep
|
120
|
+
def sleep(duration)
|
121
|
+
timer = EV::Timer.new(duration, 0)
|
122
|
+
timer.await
|
123
|
+
ensure
|
124
|
+
timer.stop
|
125
|
+
end
|
126
|
+
|
127
|
+
def spawn(proc = nil, &block)
|
128
|
+
if proc.is_a?(Coroutine)
|
129
|
+
proc.run
|
130
|
+
else
|
131
|
+
Coroutine.new(&(block || proc)).run
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def supervise(&block)
|
136
|
+
Supervisor.new.await(&block)
|
137
|
+
end
|
138
|
+
|
139
|
+
# @@reactor_fiber = Fiber.root
|
140
|
+
|
141
|
+
# def suspend
|
142
|
+
# result = @@reactor_fiber.transfer
|
143
|
+
# result.is_a?(Exception) ? raise(result) : result
|
144
|
+
# end
|
145
|
+
|
146
|
+
def throttled_loop(rate, &block)
|
147
|
+
throttler = Throttler.new(rate)
|
148
|
+
loop do
|
149
|
+
throttler.(&block)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def throttle(rate)
|
154
|
+
Throttler.new(rate)
|
155
|
+
end
|
156
|
+
|
157
|
+
def timeout(duration, opts = {}, &block)
|
158
|
+
CancelScope.new(opts.merge(timeout: duration)).(&block)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|