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