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