polyphony 0.13 → 0.14

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitbook.yaml +5 -0
  3. data/.gitignore +55 -0
  4. data/.rubocop.yml +49 -0
  5. data/CHANGELOG.md +13 -2
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +31 -0
  8. data/LICENSE +21 -0
  9. data/README.md +35 -18
  10. data/Rakefile +20 -0
  11. data/TODO.md +49 -0
  12. data/docs/getting-started/getting-started.md +10 -0
  13. data/docs/getting-started/tutorial.md +2 -0
  14. data/docs/summary.md +9 -0
  15. data/examples/core/cancel.rb +10 -0
  16. data/examples/core/channel_echo.rb +43 -0
  17. data/examples/core/enumerator.rb +14 -0
  18. data/examples/core/fork.rb +22 -0
  19. data/examples/core/genserver.rb +74 -0
  20. data/examples/core/lock.rb +20 -0
  21. data/examples/core/move_on.rb +11 -0
  22. data/examples/core/move_on_twice.rb +17 -0
  23. data/examples/core/move_on_with_ensure.rb +17 -0
  24. data/examples/core/multiple_async.rb +17 -0
  25. data/examples/core/nested_async.rb +18 -0
  26. data/examples/core/nested_cancel.rb +41 -0
  27. data/examples/core/nested_multiple_async.rb +19 -0
  28. data/examples/core/next_tick.rb +13 -0
  29. data/examples/core/pulse.rb +13 -0
  30. data/examples/core/resource.rb +29 -0
  31. data/examples/core/resource_cancel.rb +34 -0
  32. data/examples/core/resource_delegate.rb +32 -0
  33. data/examples/core/sleep.rb +9 -0
  34. data/examples/core/sleep2.rb +13 -0
  35. data/examples/core/spawn.rb +15 -0
  36. data/examples/core/spawn_cancel.rb +19 -0
  37. data/examples/core/spawn_error.rb +28 -0
  38. data/examples/core/supervisor.rb +22 -0
  39. data/examples/core/supervisor_with_cancel_scope.rb +24 -0
  40. data/examples/core/supervisor_with_error.rb +23 -0
  41. data/examples/core/supervisor_with_manual_move_on.rb +25 -0
  42. data/examples/core/thread.rb +30 -0
  43. data/examples/core/thread_cancel.rb +30 -0
  44. data/examples/core/thread_pool.rb +60 -0
  45. data/examples/core/throttle.rb +17 -0
  46. data/examples/fs/read.rb +37 -0
  47. data/examples/interfaces/pg_client.rb +38 -0
  48. data/examples/interfaces/pg_pool.rb +37 -0
  49. data/examples/interfaces/pg_query.rb +32 -0
  50. data/examples/interfaces/redis_channels.rb +119 -0
  51. data/examples/interfaces/redis_client.rb +21 -0
  52. data/examples/interfaces/redis_pubsub.rb +26 -0
  53. data/examples/interfaces/redis_pubsub_perf.rb +65 -0
  54. data/examples/io/config.ru +3 -0
  55. data/examples/io/echo_client.rb +22 -0
  56. data/examples/io/echo_server.rb +14 -0
  57. data/examples/io/echo_server_with_timeout.rb +33 -0
  58. data/examples/io/echo_stdin.rb +15 -0
  59. data/examples/io/happy_eyeballs.rb +32 -0
  60. data/examples/io/http_client.rb +19 -0
  61. data/examples/io/http_server.js +24 -0
  62. data/examples/io/http_server.rb +16 -0
  63. data/examples/io/http_server_forked.rb +27 -0
  64. data/examples/io/http_server_throttled.rb +16 -0
  65. data/examples/io/http_ws_server.rb +42 -0
  66. data/examples/io/https_client.rb +17 -0
  67. data/examples/io/https_server.rb +23 -0
  68. data/examples/io/https_wss_server.rb +46 -0
  69. data/examples/io/rack_server.rb +19 -0
  70. data/examples/io/rack_server_https.rb +24 -0
  71. data/examples/io/rack_server_https_forked.rb +32 -0
  72. data/examples/io/websocket_server.rb +33 -0
  73. data/examples/io/ws_page.html +34 -0
  74. data/examples/io/wss_page.html +34 -0
  75. data/examples/performance/perf_multi_snooze.rb +21 -0
  76. data/examples/performance/perf_snooze.rb +30 -0
  77. data/examples/performance/thread-vs-fiber/polyphony_server.rb +63 -0
  78. data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
  79. data/examples/streams/lines.rb +27 -0
  80. data/examples/streams/stdio.rb +18 -0
  81. data/ext/ev/async.c +168 -0
  82. data/ext/ev/child.c +169 -0
  83. data/ext/ev/ev.h +32 -0
  84. data/ext/ev/ev_ext.c +20 -0
  85. data/ext/ev/ev_module.c +222 -0
  86. data/ext/ev/io.c +405 -0
  87. data/ext/ev/libev.h +9 -0
  88. data/ext/ev/signal.c +119 -0
  89. data/ext/ev/timer.c +197 -0
  90. data/ext/libev/Changes +513 -0
  91. data/ext/libev/LICENSE +37 -0
  92. data/ext/libev/README +58 -0
  93. data/ext/libev/README.embed +3 -0
  94. data/ext/libev/ev.c +5214 -0
  95. data/ext/libev/ev.h +849 -0
  96. data/ext/libev/ev_epoll.c +285 -0
  97. data/ext/libev/ev_kqueue.c +218 -0
  98. data/ext/libev/ev_poll.c +151 -0
  99. data/ext/libev/ev_port.c +189 -0
  100. data/ext/libev/ev_select.c +316 -0
  101. data/ext/libev/ev_vars.h +204 -0
  102. data/ext/libev/ev_win32.c +162 -0
  103. data/ext/libev/ev_wrap.h +200 -0
  104. data/ext/libev/test_libev_win32.c +123 -0
  105. data/lib/polyphony.rb +7 -2
  106. data/lib/polyphony/core.rb +1 -1
  107. data/lib/polyphony/core/{coroutine.rb → coprocess.rb} +10 -10
  108. data/lib/polyphony/core/exceptions.rb +5 -5
  109. data/lib/polyphony/core/supervisor.rb +16 -16
  110. data/lib/polyphony/core/thread.rb +1 -1
  111. data/lib/polyphony/extensions/io.rb +43 -42
  112. data/lib/polyphony/extensions/kernel.rb +10 -34
  113. data/lib/polyphony/extensions/postgres.rb +3 -2
  114. data/lib/polyphony/extensions/redis.rb +1 -1
  115. data/lib/polyphony/extensions/socket.rb +8 -4
  116. data/lib/polyphony/extensions/ssl.rb +0 -54
  117. data/lib/polyphony/http/agent.rb +4 -10
  118. data/lib/polyphony/http/http1.rb +25 -25
  119. data/lib/polyphony/http/http1_request.rb +38 -26
  120. data/lib/polyphony/http/http2.rb +4 -5
  121. data/lib/polyphony/http/http2_request.rb +12 -18
  122. data/lib/polyphony/http/rack.rb +1 -3
  123. data/lib/polyphony/http/server.rb +9 -9
  124. data/lib/polyphony/net.rb +2 -2
  125. data/lib/polyphony/resource_pool.rb +5 -1
  126. data/lib/polyphony/version.rb +1 -1
  127. data/lib/polyphony/websocket.rb +52 -0
  128. data/polyphony.gemspec +31 -0
  129. data/test/test_coprocess.rb +131 -0
  130. data/test/test_core.rb +274 -0
  131. data/test/test_ev.rb +117 -0
  132. data/test/test_io.rb +38 -0
  133. metadata +113 -7
  134. data/lib/polyphony/core/async.rb +0 -36
  135. data/lib/polyphony/net_old.rb +0 -299
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ spawn do
8
+ puts "going to sleep..."
9
+ cancel_after(1) do
10
+ async {
11
+ sleep(2)
12
+ }.await
13
+ end
14
+ rescue Polyphony::Cancel => e
15
+ puts "got error: #{e}"
16
+ ensure
17
+ puts "woke up"
18
+ end
19
+
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ Polyphony.debug = true
7
+
8
+ def error(t)
9
+ raise "hello #{t}"
10
+ end
11
+
12
+ def spawn_with_error
13
+ spawn { error(2) }
14
+ end
15
+
16
+ spawn do
17
+ error(1)
18
+ rescue => e
19
+ e.cleanup_backtrace
20
+ puts "error: #{e.inspect}"
21
+ puts "backtrace:"
22
+ puts e.backtrace.reverse.join("\n")
23
+ puts
24
+ end
25
+
26
+ spawn_with_error
27
+
28
+ puts "done spawning"
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ async def my_sleep(t)
8
+ puts "#{t} start"
9
+ sleep(t)
10
+ puts "#{t} done"
11
+ end
12
+
13
+ puts "#{Time.now} waiting..."
14
+ supervise do |s|
15
+ s.spawn my_sleep(1)
16
+ s.spawn my_sleep(2)
17
+ s.spawn my_sleep(3)
18
+ s.spawn {
19
+ puts "fiber count: #{Polyphony::FiberPool.size}"
20
+ }
21
+ end
22
+ puts "#{Time.now} done waiting"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ async def my_sleep(t)
8
+ puts "start: #{t}"
9
+ r = sleep(t)
10
+ puts "my_sleep result #{r.inspect}"
11
+ puts "done: #{t}"
12
+ end
13
+
14
+ puts "#{Time.now} going to sleep..."
15
+ move_on_after(0.5) do
16
+ supervise do |s|
17
+ puts "supervise block"
18
+ s.spawn my_sleep(1)
19
+ s.spawn my_sleep(2)
20
+ s.spawn my_sleep(3)
21
+ end
22
+ puts "supervisor done"
23
+ end
24
+ puts "#{Time.now} woke up"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ async def my_sleep(t)
8
+ sleep(t)
9
+ raise "blah"
10
+ end
11
+
12
+ spawn do
13
+ puts "#{Time.now} going to sleep..."
14
+ supervise do |s|
15
+ s.spawn my_sleep(1)
16
+ s.spawn my_sleep(2)
17
+ s.spawn my_sleep(3)
18
+ end
19
+ rescue => e
20
+ puts "exception from supervisor: #{e}"
21
+ ensure
22
+ puts "#{Time.now} woke up"
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ async def my_sleep(t)
8
+ puts "start: #{t}"
9
+ sleep(t)
10
+ puts "done: #{t}"
11
+ end
12
+
13
+ puts "#{Time.now} going to sleep..."
14
+ result = supervise do |s|
15
+ fiber = Fiber.current
16
+ spawn do
17
+ sleep(0.5)
18
+ puts "stopping supervisor..."
19
+ s.stop!
20
+ end
21
+ s.spawn my_sleep(1)
22
+ s.spawn my_sleep(2)
23
+ s.spawn my_sleep(3)
24
+ end
25
+ puts "#{Time.now} woke up with #{result.inspect}"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'digest'
5
+ require 'socket'
6
+
7
+ Polyphony = import('../../lib/polyphony')
8
+
9
+ def lengthy_op
10
+ IO.read('../../docs/reality-ui.bmpr')
11
+ end
12
+
13
+ X = 100
14
+
15
+ def blocking
16
+ t0 = Time.now
17
+ data = lengthy_op
18
+ X.times { lengthy_op }
19
+ puts "read blocking #{data.bytesize} bytes (#{Time.now - t0}s)"
20
+ end
21
+
22
+ def threaded
23
+ t0 = Time.now
24
+ data = Polyphony::Thread.spawn { lengthy_op }.await
25
+ X.times { Polyphony::Thread.spawn { lengthy_op }.await }
26
+ puts "read threaded #{data.bytesize} bytes (#{Time.now - t0}s)"
27
+ end
28
+
29
+ blocking
30
+ threaded
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'digest'
5
+ require 'socket'
6
+
7
+ Polyphony = import('../../lib/polyphony')
8
+
9
+ @op_count = 0
10
+
11
+ def lengthy_op
12
+ @op_count += 1
13
+ acc = 0
14
+ count = 0
15
+ 100.times { acc += IO.read('../../docs/reality-ui.bmpr').bytesize; count += 1; p count }
16
+ acc / count
17
+ end
18
+
19
+ spawn do
20
+ t0 = Time.now
21
+ cancel_after(0.01) do
22
+ data = Polyphony::Thread.spawn { lengthy_op }.await
23
+ puts "read #{data.bytesize} bytes (#{Time.now - t0}s)"
24
+ end
25
+ rescue Exception => e
26
+ puts "error: #{e}"
27
+ ensure
28
+ p @op_count
29
+ end
30
+
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'digest'
5
+ require 'socket'
6
+
7
+ Polyphony = import('../../lib/polyphony')
8
+
9
+ def lengthy_op
10
+ data = IO.read('../../docs/reality-ui.bmpr')
11
+ data.clear
12
+ # Socket.getaddrinfo('debian.org', 80)
13
+ #Digest::SHA256.digest(IO.read('doc/Promise.html'))
14
+ end
15
+
16
+ X = 1000
17
+
18
+ def compare_performance
19
+ t0 = Time.now
20
+ X.times { lengthy_op }
21
+ native_perf = X / (Time.now - t0)
22
+ puts "native performance: #{native_perf}"
23
+ # puts "*" * 40
24
+
25
+ begin
26
+ 1.times do
27
+ t0 = Time.now
28
+ X.times do
29
+ Polyphony::ThreadPool.process { lengthy_op }
30
+ end
31
+ async_perf = X / (Time.now - t0)
32
+ puts "seq thread pool performance: %g (X %0.2f)" % [
33
+ async_perf, async_perf / native_perf
34
+ ]
35
+ end
36
+
37
+ acc = 0
38
+ count = 0
39
+ 10.times do |i|
40
+ t0 = Time.now
41
+ supervise do |s|
42
+ X.times do
43
+ s.spawn Polyphony::ThreadPool.process { lengthy_op }
44
+ end
45
+ end
46
+ thread_pool_perf = X / (Time.now - t0)
47
+ acc += thread_pool_perf
48
+ count += 1
49
+ end
50
+ avg_perf = acc / count
51
+ puts "avg thread pool performance: %g (X %0.2f)" % [
52
+ avg_perf, avg_perf / native_perf
53
+ ]
54
+ rescue Exception => e
55
+ p e
56
+ puts e.backtrace.join("\n")
57
+ end
58
+ end
59
+
60
+ spawn { compare_performance }
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ spawn {
8
+ throttled_loop(3) { STDOUT << '.' }
9
+ }
10
+
11
+ spawn {
12
+ throttled_loop(rate: 2) { STDOUT << '?' }
13
+ }
14
+
15
+ spawn {
16
+ throttled_loop(interval: 1) { STDOUT << '*' }
17
+ }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ PATH = File.expand_path('../../../../docs/dev-journal.md', __dir__)
8
+
9
+ def raw_read_file(x)
10
+ t0 = Time.now
11
+ x.times { IO.orig_read(PATH) }
12
+ puts "raw_read_file: #{Time.now - t0}"
13
+ end
14
+
15
+ X = 100
16
+ Y = 10
17
+
18
+ async def async_read_file
19
+ X.times { IO.read(PATH) }
20
+ end
21
+
22
+ def do_read(supervisor, x)
23
+ x.times { nexus << async { read_file } }
24
+ end
25
+
26
+ raw_read_file(X * Y)
27
+
28
+ spawn do
29
+ t0 = Time.now
30
+ supervise do |s|
31
+ 4.times { s.spawn async_read_file }
32
+ end
33
+ puts "thread_pool_read_file: #{Time.now - t0}"
34
+ rescue Exception => e
35
+ p e
36
+ puts e.backtrace.join("\n")
37
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ Postgres = import('../../lib/polyphony/extensions/postgres')
7
+
8
+ def get_records
9
+ res = $db.query("select 1 as test")
10
+ # puts "got #{res.ntuples} records: #{res.to_a}"
11
+ rescue => e
12
+ puts "got error: #{e.inspect}"
13
+ puts e.backtrace.join("\n")
14
+ end
15
+
16
+ time_printer = spawn do
17
+ last = Time.now
18
+ throttled_loop(10) do
19
+ now = Time.now
20
+ puts now - last
21
+ last = now
22
+ end
23
+ end
24
+
25
+ $db = PG.connect(
26
+ host: '/tmp',
27
+ user: 'reality',
28
+ password: nil,
29
+ dbname: 'reality',
30
+ sslmode: 'require'
31
+ )
32
+
33
+ X = 10000
34
+ t0 = Time.now
35
+ X.times { get_records }
36
+ puts "query rate: #{X / (Time.now - t0)} reqs/s"
37
+
38
+ time_printer.stop
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ Postgres = import('../../lib/polyphony/extensions/postgres')
7
+
8
+ PGOPTS = {
9
+ host: '/tmp',
10
+ user: 'reality',
11
+ password: nil,
12
+ dbname: 'reality',
13
+ sslmode: 'require'
14
+ }
15
+
16
+ DBPOOL = Polyphony::ResourcePool.new(limit: 8) { PG.connect(PGOPTS) }
17
+
18
+ def get_records(db)
19
+ res = db.query("select pg_sleep(0.0001) as test")
20
+ # puts "got #{res.ntuples} records: #{res.to_a}"
21
+ rescue => e
22
+ puts "got error: #{e.inspect}"
23
+ puts e.backtrace.join("\n")
24
+ end
25
+
26
+ CONCURRENCY = ARGV.first ? ARGV.first.to_i : 10
27
+ puts "concurrency: #{CONCURRENCY}"
28
+
29
+ DBPOOL.preheat!
30
+ t0 = Time.now
31
+ count = 0
32
+ coprocs = CONCURRENCY.times.map {
33
+ spawn { loop { DBPOOL.acquire { |db| get_records(db); count += 1 } } }
34
+ }
35
+ sleep 3
36
+ puts "count: #{count} query rate: #{count / (Time.now - t0)} queries/s"
37
+ coprocs.each(&:interrupt)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ Postgres = import('../../lib/polyphony/extensions/postgres')
7
+
8
+ DB = PG.connect(
9
+ host: '/tmp',
10
+ user: 'reality',
11
+ password: nil,
12
+ dbname: 'reality',
13
+ sslmode: 'require'
14
+ )
15
+
16
+ def perform(error)
17
+ puts "*" * 40
18
+ DB.transaction do
19
+ res = DB.query("select 1 as test")
20
+ puts "result: #{res.to_a}"
21
+ raise 'hello' if error
22
+ DB.transaction do
23
+ res = DB.query("select 2 as test")
24
+ puts "result: #{res.to_a}"
25
+ end
26
+ end
27
+ rescue => e
28
+ puts "error: #{e.inspect}"
29
+ end
30
+
31
+ perform(true)
32
+ perform(false)
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ import('../../lib/polyphony/extensions/redis')
7
+
8
+ class RedisChannel < Polyphony::Channel
9
+ def self.publish_connection
10
+ @publish_connection ||= Redis.new
11
+ end
12
+
13
+ def self.subscribe_connection
14
+ @subscribe_connection ||= Redis.new
15
+ end
16
+
17
+ CHANNEL_MASTER_TOPIC = 'channel_master'
18
+
19
+ def self.start_monitor
20
+ @channels = {}
21
+ @monitor = spawn do
22
+ subscribe_connection.subscribe(CHANNEL_MASTER_TOPIC) do |on|
23
+ on.message do |topic, message|
24
+ message = Marshal.load(message)
25
+ topic == CHANNEL_MASTER_TOPIC ? handle_master_message(message) :
26
+ handle_channel_message(topic, message)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.stop_monitor
33
+ @monitor&.interrupt
34
+ end
35
+
36
+ def self.handle_master_message(message)
37
+ case message[:kind]
38
+ when :subscribe
39
+ subscribe_connection.subscribe(message[:topic])
40
+ when :unsubscribe
41
+ subscribe_connection.unsubscribe(message[:topic])
42
+ end
43
+ end
44
+
45
+ def self.handle_channel_message(topic, message)
46
+ channel = @channels[topic]
47
+ channel&.did_receive(message)
48
+ end
49
+
50
+ def self.watch(channel)
51
+ @channels[channel.topic] = channel
52
+ spawn do
53
+ publish_connection.publish(CHANNEL_MASTER_TOPIC, Marshal.dump({
54
+ kind: :subscribe,
55
+ topic: channel.topic
56
+ }))
57
+ end
58
+ end
59
+
60
+ def self.unwatch(channel)
61
+ @channels.delete(channel.topic)
62
+ spawn do
63
+ publish_connection.publish(CHANNEL_MASTER_TOPIC, Marshal.dump({
64
+ kind: :unsubscribe,
65
+ topic: channel.topic
66
+ }))
67
+ end
68
+ end
69
+
70
+ def self.channel_topic(channel)
71
+ "channel_#{channel.object_id}"
72
+ end
73
+
74
+ attr_reader :topic
75
+
76
+ def initialize(topic)
77
+ @topic = topic
78
+ @waiting_queue = []
79
+ RedisChannel.watch(self)
80
+ end
81
+
82
+ def close
83
+ super
84
+ RedisChannel.unwatch(self)
85
+ end
86
+
87
+ def <<(o)
88
+ RedisChannel.publish_connection.publish(@topic, Marshal.dump(o))
89
+ end
90
+
91
+ def did_receive(o)
92
+ @waiting_queue.shift&.schedule(o)
93
+ end
94
+
95
+ def receive
96
+ @waiting_queue << Fiber.current
97
+ suspend
98
+ end
99
+ end
100
+
101
+ RedisChannel.start_monitor
102
+ channel = RedisChannel.new('channel1')
103
+
104
+ spawn do
105
+ loop do
106
+ message = channel.receive
107
+ puts "got message: #{message}"
108
+ end
109
+ end
110
+
111
+ spawn do
112
+ move_on_after(3) do
113
+ throttled_loop(1) do
114
+ channel << Time.now
115
+ end
116
+ end
117
+ channel.close
118
+ RedisChannel.stop_monitor
119
+ end