polyphony 0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +400 -0
  4. data/ext/ev/extconf.rb +19 -0
  5. data/lib/polyphony.rb +26 -0
  6. data/lib/polyphony/core.rb +45 -0
  7. data/lib/polyphony/core/async.rb +36 -0
  8. data/lib/polyphony/core/cancel_scope.rb +61 -0
  9. data/lib/polyphony/core/channel.rb +39 -0
  10. data/lib/polyphony/core/coroutine.rb +106 -0
  11. data/lib/polyphony/core/exceptions.rb +24 -0
  12. data/lib/polyphony/core/fiber_pool.rb +98 -0
  13. data/lib/polyphony/core/supervisor.rb +75 -0
  14. data/lib/polyphony/core/sync.rb +20 -0
  15. data/lib/polyphony/core/thread.rb +49 -0
  16. data/lib/polyphony/core/thread_pool.rb +58 -0
  17. data/lib/polyphony/core/throttler.rb +38 -0
  18. data/lib/polyphony/extensions/io.rb +62 -0
  19. data/lib/polyphony/extensions/kernel.rb +161 -0
  20. data/lib/polyphony/extensions/postgres.rb +96 -0
  21. data/lib/polyphony/extensions/redis.rb +68 -0
  22. data/lib/polyphony/extensions/socket.rb +85 -0
  23. data/lib/polyphony/extensions/ssl.rb +73 -0
  24. data/lib/polyphony/fs.rb +22 -0
  25. data/lib/polyphony/http/agent.rb +214 -0
  26. data/lib/polyphony/http/http1.rb +124 -0
  27. data/lib/polyphony/http/http1_request.rb +71 -0
  28. data/lib/polyphony/http/http2.rb +66 -0
  29. data/lib/polyphony/http/http2_request.rb +69 -0
  30. data/lib/polyphony/http/rack.rb +27 -0
  31. data/lib/polyphony/http/server.rb +43 -0
  32. data/lib/polyphony/line_reader.rb +82 -0
  33. data/lib/polyphony/net.rb +59 -0
  34. data/lib/polyphony/net_old.rb +299 -0
  35. data/lib/polyphony/resource_pool.rb +56 -0
  36. data/lib/polyphony/server_task.rb +18 -0
  37. data/lib/polyphony/testing.rb +34 -0
  38. data/lib/polyphony/version.rb +5 -0
  39. 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
+