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