async 1.25.2 → 1.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/async/barrier.rb +1 -1
  3. data/lib/async/clock.rb +33 -1
  4. data/lib/async/logger.rb +1 -6
  5. data/lib/async/node.rb +20 -2
  6. data/lib/async/queue.rb +5 -1
  7. data/lib/async/reactor.rb +73 -12
  8. data/lib/async/scheduler.rb +112 -0
  9. data/lib/async/task.rb +11 -3
  10. data/lib/async/version.rb +1 -1
  11. metadata +46 -104
  12. data/.editorconfig +0 -6
  13. data/.github/workflows/development.yml +0 -55
  14. data/.gitignore +0 -14
  15. data/.rspec +0 -3
  16. data/.yardopts +0 -1
  17. data/Gemfile +0 -20
  18. data/Guardfile +0 -14
  19. data/README.md +0 -385
  20. data/Rakefile +0 -40
  21. data/async.gemspec +0 -34
  22. data/bake.rb +0 -33
  23. data/benchmark/async_vs_lightio.rb +0 -84
  24. data/benchmark/fiber_count.rb +0 -10
  25. data/benchmark/rubies/README.md +0 -51
  26. data/benchmark/rubies/benchmark.rb +0 -220
  27. data/benchmark/thread_count.rb +0 -9
  28. data/benchmark/thread_vs_fiber.rb +0 -45
  29. data/examples/async_method.rb +0 -60
  30. data/examples/callback/loop.rb +0 -44
  31. data/examples/capture/README.md +0 -59
  32. data/examples/capture/capture.rb +0 -116
  33. data/examples/fibers.rb +0 -178
  34. data/examples/queue/producer.rb +0 -28
  35. data/examples/sleep_sort.rb +0 -40
  36. data/examples/stop/condition.rb +0 -31
  37. data/examples/stop/sleep.rb +0 -42
  38. data/gems/event.gemfile +0 -4
  39. data/logo.png +0 -0
  40. data/logo.svg +0 -64
  41. data/papers/1982 Grossman.pdf +0 -0
  42. data/papers/1987 ODell.pdf +0 -0
  43. data/spec/async/barrier_spec.rb +0 -116
  44. data/spec/async/chainable_async_examples.rb +0 -13
  45. data/spec/async/clock_spec.rb +0 -37
  46. data/spec/async/condition_examples.rb +0 -105
  47. data/spec/async/condition_spec.rb +0 -72
  48. data/spec/async/logger_spec.rb +0 -65
  49. data/spec/async/node_spec.rb +0 -193
  50. data/spec/async/notification_spec.rb +0 -66
  51. data/spec/async/performance_spec.rb +0 -72
  52. data/spec/async/queue_spec.rb +0 -129
  53. data/spec/async/reactor/nested_spec.rb +0 -52
  54. data/spec/async/reactor_spec.rb +0 -253
  55. data/spec/async/semaphore_spec.rb +0 -169
  56. data/spec/async/task_spec.rb +0 -476
  57. data/spec/async/wrapper_spec.rb +0 -203
  58. data/spec/async_spec.rb +0 -33
  59. data/spec/enumerator_spec.rb +0 -83
  60. data/spec/kernel/async_spec.rb +0 -33
  61. data/spec/kernel/sync_spec.rb +0 -54
  62. data/spec/spec_helper.rb +0 -18
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- threads = []
4
-
5
- (1..).each do |i|
6
- threads << Thread.new{sleep}
7
- puts i
8
- end
9
-
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
- #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the "Software"), to deal
8
- # in the Software without restriction, including without limitation the rights
9
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
12
- #
13
- # The above copyright notice and this permission notice shall be included in
14
- # all copies or substantial portions of the Software.
15
- #
16
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- # THE SOFTWARE.
23
-
24
- require 'benchmark/ips'
25
-
26
- GC.disable
27
-
28
- Benchmark.ips do |benchmark|
29
- benchmark.time = 1
30
- benchmark.warmup = 1
31
-
32
- benchmark.report("Thread.new{}") do |count|
33
- while count > 0
34
- Thread.new{count -= 1}.join
35
- end
36
- end
37
-
38
- benchmark.report("Fiber.new{}") do |count|
39
- while count > 0
40
- Fiber.new{count -= 1}.resume
41
- end
42
- end
43
-
44
- benchmark.compare!
45
- end
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative '../lib/async'
5
-
6
- module Async::Methods
7
- def sleep(*args)
8
- Async::Task.current.sleep(*args)
9
- end
10
-
11
- def async(name)
12
- original_method = self.method(name)
13
-
14
- define_method(name) do |*args|
15
- Async::Reactor.run do |task|
16
- original_method.call(*args)
17
- end
18
- end
19
- end
20
-
21
- def await(&block)
22
- block.call.wait
23
- end
24
-
25
- def barrier!
26
- Async::Task.current.children.each(&:wait)
27
- end
28
- end
29
-
30
- include Async::Methods
31
-
32
- async def count_chickens(area_name)
33
- 3.times do |i|
34
- sleep rand
35
-
36
- puts "Found a chicken in the #{area_name}!"
37
- end
38
- end
39
-
40
- async def find_chicken(areas)
41
- puts "Searching for chicken..."
42
-
43
- sleep rand * 5
44
-
45
- return areas.sample
46
- end
47
-
48
- async def count_all_chckens
49
- # These methods all run at the same time.
50
- count_chickens("garden")
51
- count_chickens("house")
52
- count_chickens("tree")
53
-
54
- # Wait for all previous async work to complete...
55
- barrier!
56
-
57
- puts "There was a chicken in the #{find_chicken(["garden", "house", "tree"]).wait}"
58
- end
59
-
60
- count_all_chckens
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'async/reactor'
4
-
5
- class Callback
6
- def initialize
7
- @reactor = Async::Reactor.new
8
- end
9
-
10
- def close
11
- @reactor.close
12
- end
13
-
14
- # If duration is 0, it will happen immediately after the task is started.
15
- def run(duration = 0)
16
- @reactor.run do |task|
17
- @reactor.after(duration) do
18
- @reactor.stop
19
- end
20
-
21
- yield(task) if block_given?
22
- end
23
- end
24
- end
25
-
26
-
27
- callback = Callback.new
28
-
29
- begin
30
- callback.run do |task|
31
- while true
32
- task.sleep(2)
33
- puts "Hello from task!"
34
- end
35
- end
36
-
37
- while true
38
- callback.run(0)
39
- puts "Sleeping for 1 second"
40
- sleep(1)
41
- end
42
- ensure
43
- callback.close
44
- end
@@ -1,59 +0,0 @@
1
- # Capture
2
-
3
- ## Falcon
4
-
5
- ```
6
- % wrk -t 8 -c 32 http://localhost:9292/
7
- Running 10s test @ http://localhost:9292/
8
- 8 threads and 32 connections
9
- Thread Stats Avg Stdev Max +/- Stdev
10
- Latency 106.31ms 10.20ms 211.79ms 98.00%
11
- Req/Sec 37.94 5.43 40.00 84.24%
12
- 3003 requests in 10.01s, 170.16KB read
13
- Requests/sec: 299.98
14
- Transfer/sec: 17.00KB
15
- ```
16
-
17
- ```
18
- 0.0s: Process 28065 start times:
19
- | #<struct Process::Tms utime=2.38, stime=0.0, cutime=0.0, cstime=0.2>
20
- ^C15.11s: strace -p 28065
21
- | ["sendto", {:"% time"=>57.34, :seconds=>0.595047, :"usecs/call"=>14, :calls=>39716, :errors=>32, :syscall=>"sendto"}]
22
- | ["recvfrom", {:"% time"=>42.58, :seconds=>0.441867, :"usecs/call"=>12, :calls=>36718, :errors=>70, :syscall=>"recvfrom"}]
23
- | ["read", {:"% time"=>0.07, :seconds=>0.000723, :"usecs/call"=>7, :calls=>98, :errors=>nil, :syscall=>"read"}]
24
- | ["write", {:"% time"=>0.01, :seconds=>0.000112, :"usecs/call"=>56, :calls=>2, :errors=>nil, :syscall=>"write"}]
25
- | [:total, {:"% time"=>100.0, :seconds=>1.037749, :"usecs/call"=>nil, :calls=>76534, :errors=>102, :syscall=>"total"}]
26
- 15.11s: Process 28065 end times:
27
- | #<struct Process::Tms utime=3.93, stime=0.0, cutime=0.0, cstime=0.2>
28
- 15.11s: Process Waiting: 1.0377s out of 1.55s
29
- | Wait percentage: 66.95%
30
- ```
31
-
32
- ## Puma
33
-
34
- ```
35
- wrk -t 8 -c 32 http://localhost:9292/
36
- Running 10s test @ http://localhost:9292/
37
- 8 threads and 32 connections
38
- Thread Stats Avg Stdev Max +/- Stdev
39
- Latency 108.83ms 3.50ms 146.38ms 86.58%
40
- Req/Sec 34.43 6.70 40.00 92.68%
41
- 1371 requests in 10.01s, 81.67KB read
42
- Requests/sec: 136.94
43
- Transfer/sec: 8.16KB
44
- ```
45
-
46
- ```
47
- 0.0s: Process 28448 start times:
48
- | #<struct Process::Tms utime=0.63, stime=0.0, cutime=0.0, cstime=0.2>
49
- ^C24.89s: strace -p 28448
50
- | ["recvfrom", {:"% time"=>64.65, :seconds=>0.595275, :"usecs/call"=>13, :calls=>44476, :errors=>27769, :syscall=>"recvfrom"}]
51
- | ["sendto", {:"% time"=>30.68, :seconds=>0.282467, :"usecs/call"=>18, :calls=>15288, :errors=>nil, :syscall=>"sendto"}]
52
- | ["write", {:"% time"=>4.66, :seconds=>0.042921, :"usecs/call"=>15, :calls=>2772, :errors=>nil, :syscall=>"write"}]
53
- | ["read", {:"% time"=>0.02, :seconds=>0.000157, :"usecs/call"=>8, :calls=>19, :errors=>1, :syscall=>"read"}]
54
- | [:total, {:"% time"=>100.0, :seconds=>0.92082, :"usecs/call"=>nil, :calls=>62555, :errors=>27770, :syscall=>"total"}]
55
- 24.89s: Process 28448 end times:
56
- | #<struct Process::Tms utime=3.19, stime=0.0, cutime=0.0, cstime=0.2>
57
- 24.89s: Process Waiting: 0.9208s out of 2.56s
58
- | Wait percentage: 35.97%
59
- ```
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'irb'
5
- require 'console'
6
-
7
- pids = ARGV.collect(&:to_i)
8
-
9
- TICKS = Process.clock_getres(:TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID, :hertz).to_f
10
-
11
- def getrusage(pid)
12
- fields = File.read("/proc/#{pid}/stat").split(/\s+/)
13
-
14
- return Process::Tms.new(
15
- fields[14].to_f / TICKS,
16
- fields[15].to_f / TICKS,
17
- fields[16].to_f / TICKS,
18
- fields[17].to_f / TICKS,
19
- )
20
- end
21
-
22
- def parse(value)
23
- case value
24
- when /^\s*\d+\.\d+/
25
- Float(value)
26
- when /^\s*\d+/
27
- Integer(value)
28
- else
29
- value = value.strip
30
- if value.empty?
31
- nil
32
- else
33
- value
34
- end
35
- end
36
- end
37
-
38
- def strace(pid, duration = 60)
39
- input, output = IO.pipe
40
-
41
- pid = Process.spawn("strace", "-p", pid.to_s, "-cqf", "-w", "-e", "!futex", err: output)
42
-
43
- output.close
44
-
45
- Signal.trap(:INT) do
46
- Process.kill(:INT, pid)
47
- Signal.trap(:INT, :DEFAULT)
48
- end
49
-
50
- Thread.new do
51
- sleep duration
52
- Process.kill(:INT, pid)
53
- end
54
-
55
- summary = {}
56
-
57
- if first_line = input.gets
58
- if rule = input.gets # horizontal separator
59
- pattern = Regexp.new(
60
- rule.split(/\s/).map{|s| "(.{1,#{s.size}})"}.join(' ')
61
- )
62
-
63
- header = pattern.match(first_line).captures.map{|key| key.strip.to_sym}
64
- end
65
-
66
- while line = input.gets
67
- break if line == rule
68
- row = pattern.match(line).captures.map{|value| parse(value)}
69
- fields = header.zip(row).to_h
70
-
71
- summary[fields[:syscall]] = fields
72
- end
73
-
74
- if line = input.gets
75
- row = pattern.match(line).captures.map{|value| parse(value)}
76
- fields = header.zip(row).to_h
77
- summary[:total] = fields
78
- end
79
- end
80
-
81
- _, status = Process.waitpid2(pid)
82
-
83
- Console.logger.error(status) do |buffer|
84
- buffer.puts first_line
85
- end unless status.success?
86
-
87
- return summary
88
- end
89
-
90
- pids.each do |pid|
91
- start_times = getrusage(pid)
92
- Console.logger.info("Process #{pid} start times:", start_times)
93
-
94
- # sleep 60
95
- summary = strace(pid)
96
-
97
- Console.logger.info("strace -p #{pid}") do |buffer|
98
- summary.each do |fields|
99
- buffer.puts fields.inspect
100
- end
101
- end
102
-
103
- end_times = getrusage(pid)
104
- Console.logger.info("Process #{pid} end times:", end_times)
105
-
106
- if total = summary[:total]
107
- process_duration = end_times.utime - start_times.utime
108
- wait_duration = summary[:total][:seconds]
109
-
110
- Console.logger.info("Process Waiting: #{wait_duration.round(4)}s out of #{process_duration.round(4)}s") do |buffer|
111
- buffer.puts "Wait percentage: #{(wait_duration / process_duration * 100.0).round(2)}%"
112
- end
113
- else
114
- Console.logger.warn("No system calls detected.")
115
- end
116
- end
@@ -1,178 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fiber'
4
-
5
- class IO
6
- READABLE = 1
7
- WRITABLE = 2
8
-
9
- # rb_wait_for_single_fd (int fd, int events, struct timeval *tv)
10
- def self.wait(descriptor, events, duration)
11
- fiber = Fiber.current
12
- reactor = fiber.reactor
13
-
14
- monitor = reactor.add_io(fiber, descriptor, state)
15
-
16
- fiber.with_timeout(duration) do
17
- result = Fiber.yield
18
- raise result if result.is_a? Exception
19
- end
20
-
21
- return result
22
- ensure
23
- reactor.remove_io(monitor)
24
- end
25
-
26
- def wait_readable(duration = nil)
27
- wait_any(READABLE)
28
- end
29
-
30
- def wait_writable(duration = nil)
31
- wait_any(WRITABLE)
32
- end
33
-
34
- def wait_until(events = READABLE|WRITABLE, duration = nil)
35
- IO.wait_for_io(self.fileno, events, duration)
36
- end
37
- end
38
-
39
- class Fiber
40
- # Raised when a task times out.
41
- class TimeoutError < RuntimeError
42
- end
43
-
44
- # This should be inherited by nested fibers.
45
- attr :reactor
46
-
47
- def timeout(duration)
48
- reactor = self.reactor
49
- backtrace = caller
50
-
51
- timer = reactor.add_timer(duration) do
52
- if self.alive?
53
- error = Fiber::TimeoutError.new("execution expired")
54
- error.set_backtrace backtrace
55
- self.resume error
56
- end
57
- end
58
-
59
- yield
60
- ensure
61
- reactor.cancel_timer(timer)
62
- end
63
- end
64
-
65
- # Can be standard implementation, but could also be provided by external gem/library.
66
- class Fiber::Reactor
67
- # Add IO to the reactor. The reactor will call `fiber.resume` when the event is triggered.
68
- # Returns an opaque monitor object which can be passed to `remove_io` to stop waiting for events.
69
- def add_io(fiber, io, state)
70
- # The motivation for add_io and remove_io is that it's how a lot of the underlying APIs work, where remove_io just takes the file descriptor.
71
- # It also avoids the need for any memory allocation, and maps well to how it's typically used (i.e. in an implementation of `IO#read`).
72
- # An efficient implementation might do it's job and then just:
73
- return io
74
- end
75
-
76
- def remove_io(monitor)
77
- end
78
-
79
- # The reactor will call the block at some point after duration time has elapsed.
80
- # Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening.
81
- def add_timer(duration, &block)
82
- end
83
-
84
- def cancel_timer(timer)
85
- end
86
-
87
- # Run until idle (no registered io/timers), or duration time has passed if specified.
88
- def run(duration = nil)
89
- end
90
-
91
- # Stop the reactor as soon as possible. Can be called from another thread.
92
- def stop
93
- end
94
-
95
- def close
96
- # Close the reactor so it can no longer be used.
97
- end
98
- end
99
-
100
- # Basic non-blocking task:
101
- reactor = Fiber::Reactor.new
102
-
103
- # User could provide their own reactor, it might even do other things, but the basic interface above should continue to work.
104
-
105
- Fiber.new(reactor: reactor) do
106
- # Blocking operations call Fiber.yield, which goes to...
107
- end.resume
108
-
109
- # ...here, which starts running the reactor which can also be controlled (e.g. duration, stopping)
110
- reactor.run
111
-
112
- # Here is a rough outline of the reactor concept implementation using NIO4R
113
- # Can be standard implementation, but could also be provided by external gem/library.
114
- class NIO::Reactor
115
- def initialize
116
- @selector = NIO::Selector.new
117
- @timers = Timers::Group.new
118
-
119
- @stopped = true
120
- end
121
-
122
- EVENTS = [
123
- :r,
124
- :w,
125
- :rw
126
- ]
127
-
128
- def add_io(fiber, io, event)
129
- monitor = @selector.register(io, EVENTS[event])
130
- monitor.value = fiber
131
- end
132
-
133
- def remove_io(monitor)
134
- monitor.cancel
135
- end
136
-
137
- # The reactor will call `fiber.resume(Fiber::TimeoutError)` at some point after duration time has elapsed.
138
- # Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening.
139
- def add_timer(fiber, duration)
140
- @timers.after(duration, &block)
141
- end
142
-
143
- def cancel_timer(timer)
144
- timer.cancel
145
- end
146
-
147
- # Run until idle (no registered io/timers), or duration time has passed if specified.
148
- def run(duration = nil)
149
- @timers.wait do |interval|
150
- # - nil: no timers
151
- # - -ve: timers expired already
152
- # - 0: timers ready to fire
153
- # - +ve: timers waiting to fire
154
- interval = 0 if interval && interval < 0
155
-
156
- # If there is nothing to do, then finish:
157
- return if @fibers.empty? && interval.nil?
158
-
159
- if monitors = @selector.select(interval)
160
- monitors.each do |monitor|
161
- if fiber = monitor.value
162
- fiber.resume
163
- end
164
- end
165
- end
166
- end until @stopped
167
- end
168
-
169
- def stop
170
- @stopped = true
171
- @selector.wakeup
172
- end
173
-
174
- def close
175
- @seletor.close
176
- end
177
- end
178
-