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
@@ -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
|
+
|