async 1.12.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40cffda648844d0b0a3ec212b9038bb0a211ed98675a9265336af674684ca5a6
4
- data.tar.gz: b903703231b783e04911f5ea3df63774524fc4248a0c61d3f3a50a74881b4825
3
+ metadata.gz: 8c180517501e43ab8b6048d65a5e88a7091dd0822de02c6459cbaa2be84a6660
4
+ data.tar.gz: a1dc52af966a3fe42123b6aaab823b1b49fe9ec88679bbdfaddecc9727bd8c57
5
5
  SHA512:
6
- metadata.gz: 5dc2640c9bc451b0a4d43b6a3a54769f564ed139492f18d8350d9e800257d13ac75b2dc321e7b3f9e5d95752e6caab8f7c9b0c593c3cebe590c0eea805b0697a
7
- data.tar.gz: 3a408f2188b5d2c8ae42267aee14a63aa3e6cff08128a879551506b79de861243d8f5c7200f47a22990a12f99d9a97a61b4194c7770d03c89c9a0191f0cc7f81
6
+ metadata.gz: cb96b95bfcaea2b302a4223dcfaa722c14640e256871723d4fa155be323e9a9ac68bc4e8abd0818ede9d875ed52a39310c1bada0ba986a3b8a8b2575e15e7988
7
+ data.tar.gz: 9f0eb6968fc754f579579e416038b8f9e32d6bca7d73bb58601418814816d66d30a296853555a64da0298f479e2bb8b392fd7e09b20b394c7ea1b52c5c96668e
@@ -10,9 +10,11 @@ addons:
10
10
  before_script:
11
11
  - sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'
12
12
 
13
+ after_success:
14
+ - bundle exec rake external
15
+
13
16
  matrix:
14
17
  include:
15
- - rvm: 2.2
16
18
  - rvm: 2.3
17
19
  - rvm: 2.4
18
20
  - rvm: 2.5
data/README.md CHANGED
@@ -14,15 +14,18 @@ Async is a composable asynchronous I/O framework for Ruby based on [nio4r] and [
14
14
 
15
15
  Several years ago, I was hosting websites on a server in my garage. Back then, my ADSL modem was very basic, and I wanted to have a DNS server which would resolve to an internal IP address when the domain itself resolved to my public IP. Thus was born [RubyDNS]. This project [was originally built on](https://github.com/ioquatix/rubydns/tree/v0.8.5) top of [EventMachine], but a lack of support for [IPv6 at the time](https://github.com/ioquatix/rubydns/issues/45) and [other problems](https://github.com/ioquatix/rubydns/issues/14), meant that I started looking for other options. Around that time [Celluloid] was picking up steam. I had not encountered actors before and I wanted to learn more about it. So, [I reimplemented RubyDNS on top of Celluloid](https://github.com/ioquatix/rubydns/tree/v0.9.0) and this eventually became the first stable release.
16
16
 
17
- Moving forward, I refactored the internals of RubyDNS into [Celluloid::DNS]. This rewrite helped solidify the design of RubyDNS and to a certain extent it works. However, [unfixed bugs and design problems](https://github.com/celluloid/celluloid/pull/710) in Celluloid meant that RubyDNS 2.0 was delayed by almost 2 years. I wasn't happy releasing it with known bugs and problems. After sitting on the problem for a while, and thinking about possible solutions, I decided to build a small event reactor using [nio4r] and [timers], the core parts of [Celluloid::IO] which made it work so well. The result is this project.
17
+ Moving forward, I refactored the internals of RubyDNS into [Celluloid::DNS]. This rewrite helped solidify the design of RubyDNS and to a certain extent it works. However, [unfixed bugs and design problems](https://github.com/celluloid/celluloid/pull/710) in Celluloid meant that RubyDNS 2.0 was delayed by almost 2 years. I wasn't happy releasing it with known bugs and problems. After working on the issues for a while, and thinking about possible solutions, I decided to build a small event reactor using [nio4r] and [timers], the core parts of [Celluloid::IO] which made it work so well. The result is this project.
18
18
 
19
- In addition, there is a [similarly designed C++ library of the same name](https://github.com/kurocha/async). These two libraries share similar design principles, but are different in some areas due to the underlying semantic differences of the languages.
19
+ One observation I made when looking at existing gems for asynchronous IO was a tendency to try and do everything within a single code-base. The design of this core library is deliberately simple. Additional libraries provide asynchronous networking, process management, etc. It's likely you will prefer to depend on [async-io] for actual wrappers around `IO` and `Socket`. This helps to ensure a clean separation of concerns.
20
+
21
+ In designing this library, I also built a [similarly designed C++ library of the same name](https://github.com/kurocha/async). These two libraries share similar design principles.
20
22
 
21
23
  [Celluloid]: https://github.com/celluloid/celluloid
22
24
  [Celluloid::IO]: https://github.com/celluloid/celluloid-io
23
25
  [Celluloid::DNS]: https://github.com/celluloid/celluloid-dns
24
26
  [EventMachine]: https://github.com/eventmachine/eventmachine
25
27
  [RubyDNS]: https://github.com/ioquatix/rubydns
28
+ [async-io]: https://github.com/socketry/async-io
26
29
 
27
30
  ## Installation
28
31
 
@@ -42,49 +45,167 @@ Or install it yourself as:
42
45
 
43
46
  ## Usage
44
47
 
45
- `Async::Reactor` is the top level IO reactor, and runs multiple tasks asynchronously. The reactor itself is not thread-safe, so you'd typically have [one reactor per thread or process](https://github.com/socketry/async-container).
46
-
47
- An `Async::Task` runs using a `Fiber` and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can succeed.
48
-
49
- The design of this core library is deliberately simple in scope. Additional libraries provide asynchronous networking, process management, etc. It's likely you will prefer to depend on `async-io` for actual wrappers around `IO` and `Socket`.
48
+ ### Tasks
50
49
 
51
- ### Main Entry Points
50
+ An `Async::Task` runs using a `Fiber` and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. There are two main methods to create tasks.
52
51
 
53
- #### `Async::Reactor.run`
52
+ #### `Async{...}`
54
53
 
55
- The highest level entry point is `Async::Reactor.run`. It's useful if you are building a library and you want well defined asynchronous semantics.
54
+ The highest level entry point is `Async{...}`. It's useful if you are building a library and you want well defined asynchronous semantics. This internally invokes `Async::Reactor.run{...}`.
56
55
 
57
56
  ```ruby
58
57
  def run_server
59
- Async::Reactor.run do |task|
58
+ Async do |task|
60
59
  # ... acccept connections
61
60
  end
62
61
  end
63
62
  ```
64
63
 
65
- If `Async::Reactor.run(&block)` happens within an existing reactor, it will schedule an asynchronous task and return. If `Async::Reactor.run(&block)` happens outside of an existing reactor, it will create a reactor, schedule the asynchronous task, and block until it completes. The task is scheduled by calling `Async::Reactor.async(&block)`.
64
+ If `Async(&block)` happens within an existing reactor, it will schedule an asynchronous task and return. If `Async(&block)` happens outside of an existing reactor, it will create a reactor, schedule the asynchronous task, and block until it completes. The task is scheduled by calling `Async::Reactor#async(&block)`.
65
+
66
+ This allows the caller to have either blocking or non-blocking behaviour.
67
+
68
+ ```ruby
69
+ require 'async'
66
70
 
67
- This puts the power into the hands of the client, who can either have blocking or non-blocking behaviour by explicitly wrapping the call in a reactor (or not). The cost of using `Async::Reactor.run` is minimal for initialization/server setup, but is not ideal for per-connection tasks.
71
+ def sleepy(duration = 1)
72
+ Async do |task|
73
+ task.sleep duration
74
+ puts "I'm done sleeping, time for action!"
75
+ end
76
+ end
77
+
78
+ # Synchronous operation:
79
+ sleepy
80
+
81
+ # Asynchronous operation:
82
+ Async do
83
+ # These two functions will sleep simultaneously.
84
+ sleepy
85
+ sleepy
86
+ end
87
+ ```
88
+
89
+ The cost of using `Async{...}` is minimal for initialization/server setup, but is not ideal for per-connection tasks.
68
90
 
69
91
  #### `Async::Task#async`
70
92
 
71
93
  If you can guarantee you are running within a task, and have access to it (e.g. via an argument), you can efficiently schedule new tasks using the `Async::Task#async(&block)` method.
72
94
 
73
95
  ```ruby
74
- def do_request(task: Task.current)
75
- task.async do
76
- # ... do some actual work
96
+ require 'async'
97
+
98
+ def nested_sleepy(task: Async::Task.current)
99
+ # Block caller
100
+ task.sleep 0.1
101
+
102
+ # Schedule nested task:
103
+ subtask = task.async do |subtask|
104
+ puts "I'm going to sleep..."
105
+ subtask.sleep 1.0
106
+ ensure
107
+ puts "I'm waking up!"
77
108
  end
78
109
  end
110
+
111
+ Async do |task|
112
+ subtask = nested_sleepy
113
+ end
79
114
  ```
80
115
 
81
116
  This method effectively creates a child task. It's the most efficient way to schedule a task. The task is executed until the first blocking operation, at which point it will yield control and `#async` will return. The result of this method is the task itself.
82
117
 
83
- ### Reactor Tree
118
+ ### Waiting for Results
119
+
120
+ Like promises, `Async::Task` produces results. In order to wait for these results, you must invoke `Async::Task#wait`:
121
+
122
+ ```ruby
123
+ require 'async'
124
+
125
+ task = Async do
126
+ rand
127
+ end
128
+
129
+ puts task.wait
130
+ ```
131
+
132
+ ### Stopping Tasks
133
+
134
+ Use `Async::Task#stop` to stop tasks. This function raises `Async::Stop` on the target task and all descendent tasks.
135
+
136
+ ```ruby
137
+ require 'async'
138
+
139
+ Async do
140
+ sleepy = Async do |task|
141
+ task.sleep 1000
142
+ end
143
+
144
+ sleepy.stop
145
+ end
146
+ ```
147
+
148
+ When you design a server, you should return the task back to the caller. They can use this task to stop the server if needed, independently of any other unrelated tasks within the reactor, and it will correctly clean up all related tasks.
149
+
150
+ ### Reactors
151
+
152
+ `Async::Reactor` is the top level IO reactor, and runs multiple tasks asynchronously. The reactor itself is not thread-safe, so you'd typically have [one reactor per thread or process](https://github.com/socketry/async-container).
153
+
154
+ #### Hierarchy
84
155
 
85
156
  `Async::Reactor` and `Async::Task` form nodes in a tree. Reactors and tasks can spawn children tasks. When you invoke `Async::Reactor#async`, the parent task is determined by calling `Async::Task.current?` which uses fiber local storage. A slightly more efficient method is to use `Async::Task#async`, which uses `self` as the parent task.
86
157
 
87
- When invoking `Async::Reactor#stop`, you will stop *all* children tasks of that reactor. Tasks will raise `Async::Stop` if they are in a blocking operation. In addition, it's possible to only stop a sub-tree by issuing `Async::Task#stop`, which will stop that task and all it's children (recursively). When you design a server, you should return the task back to the caller. They can use this task to stop the server if needed, independently of any other unrelated tasks within the reactor, and it will correctly clean up all related tasks.
158
+
159
+ ```ruby
160
+ require 'async'
161
+
162
+ def sleepy(duration, task: Async::Task.current)
163
+ task.async do |subtask|
164
+ subtask.annotate "I'm going to sleep #{duration}s..."
165
+ subtask.sleep duration
166
+ puts "I'm done sleeping!"
167
+ end
168
+ end
169
+
170
+ def nested_sleepy(task: Async::Task.current)
171
+ task.async do |subtask|
172
+ subtask.annotate "Invoking sleepy 5 times..."
173
+ 5.times do |index|
174
+ sleepy(index, task: subtask)
175
+ end
176
+ end
177
+ end
178
+
179
+ Async do |task|
180
+ task.annotate "Invoking nested_sleepy..."
181
+ subtask = nested_sleepy
182
+
183
+ # Print out all running tasks in a tree:
184
+ task.print_hierarchy($stderr)
185
+
186
+ # Kill the subtask
187
+ subtask.stop
188
+ end
189
+ ```
190
+
191
+ #### Stopping Reactors
192
+
193
+ `Async::Reactor#run` will run until the reactor runs out of work to do or is explicitly stopped.
194
+
195
+ ```ruby
196
+ require 'async'
197
+
198
+ Async.logger.debug!
199
+ reactor = Async::Reactor.new
200
+
201
+ # Run the reactor for 1 second:
202
+ reactor.run do |task|
203
+ task.sleep 1
204
+ reactor.stop
205
+ end
206
+ ```
207
+
208
+ You can use this approach to embed the reactor in another event loop. `Async::Reactor#stop` is can be called safely from a different thread.
88
209
 
89
210
  ### Resource Management
90
211
 
@@ -105,6 +226,73 @@ end
105
226
 
106
227
  As tasks run synchronously until they yield back to the reactor, you can guarantee this model works correctly. While in theory `IO#autoclose` allows you to automatically close file descriptors when they go out of scope via the GC, it may produce unpredictable behavour (exhaustion of file descriptors, flushing data at odd times), so it's not recommended.
107
228
 
229
+ ### Exception Handling
230
+
231
+ `Async::Task` captures and logs exceptions. All unhandled exceptions will cause the enclosing task to enter the `:failed` state. Non-`StandardError` exceptions are re-raised immediately and will generally cause the reactor to fail. This ensures that exceptions will always be visible and cause the program to fail appropriately.
232
+
233
+ ```ruby
234
+ require 'async'
235
+
236
+ task = Async do
237
+ # Exception will be logged and task will be failed.
238
+ raise "Boom"
239
+ end
240
+
241
+ puts task.status # failed
242
+ puts task.result # raises RuntimeError: Boom
243
+ ```
244
+
245
+ #### Propagating Exceptions
246
+
247
+ If a task has finished due to an exception, calling `Task#wait` will re-raise the exception.
248
+
249
+ ```ruby
250
+ require 'async'
251
+
252
+ Async do
253
+ task = Async do
254
+ raise "Boom"
255
+ end
256
+
257
+ begin
258
+ task.wait # Re-raises above exception.
259
+ rescue
260
+ puts "It went #{$!}!"
261
+ end
262
+ end
263
+ ```
264
+
265
+ #### Timeouts
266
+
267
+ You can wrap asynchronous operations in a timeout. This ensures that malicious services don't cause your code to block indefinitely.
268
+
269
+ ```ruby
270
+ require 'async'
271
+
272
+ Async do |task|
273
+ task.timeout(1) do
274
+ task.sleep 100
275
+ rescue Async::TimeoutError
276
+ puts "I timed out!"
277
+ end
278
+ end
279
+ ```
280
+
281
+ ### Reoccurring Timers
282
+
283
+ Sometimes you need to do some periodic work in a loop.
284
+
285
+ ```ruby
286
+ require 'async'
287
+
288
+ Async do |task|
289
+ while true
290
+ puts Time.now
291
+ task.sleep 1
292
+ end
293
+ end
294
+ ```
295
+
108
296
  ## Caveats
109
297
 
110
298
  ### Enumerators
@@ -133,6 +321,7 @@ Due to limitations within Ruby and the nature of this library, it is not possibl
133
321
  - [ciri](https://github.com/ciri-ethereum/ciri) - An Ethereum implementation written in Ruby.
134
322
  - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
135
323
  - [rubydns](https://github.com/ioquatix/rubydns) — A easy to use Ruby DNS server.
324
+ - [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots.
136
325
 
137
326
  ## License
138
327
 
data/Rakefile CHANGED
@@ -27,6 +27,7 @@ task :external do
27
27
  clone_and_test("async-dns")
28
28
  clone_and_test("async-http")
29
29
  clone_and_test("falcon")
30
+ clone_and_test("async-rest")
30
31
  end
31
32
  end
32
33
 
@@ -27,4 +27,10 @@ module Async
27
27
  def self.run(*args, &block)
28
28
  Reactor.run(*args, &block)
29
29
  end
30
- end
30
+ end
31
+
32
+ module Kernel
33
+ def Async(*args, &block)
34
+ Async::Reactor.run(*args, &block)
35
+ end
36
+ end
@@ -18,9 +18,138 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'logger'
21
+ require_relative 'terminal'
22
22
 
23
23
  module Async
24
+ class Logger
25
+ LEVELS = {debug: 0, info: 1, warn: 2, error: 3, fatal: 4}
26
+
27
+ LEVELS.each do |name, level|
28
+ const_set(name.to_s.upcase, level)
29
+
30
+ define_method(name) do |subject = nil, *arguments, &block|
31
+ enabled = @subjects[subject.class]
32
+
33
+ if enabled == true or (enabled != false and level >= @level)
34
+ self.format(subject, *arguments, &block)
35
+ end
36
+ end
37
+
38
+ define_method("#{name}!") do
39
+ @level = level
40
+ end
41
+ end
42
+
43
+ def initialize(output, level: 1)
44
+ @output = output
45
+ @level = level
46
+ @start = Time.now
47
+
48
+ @terminal = Terminal.new(output)
49
+ @reset_style = @terminal.reset
50
+ @prefix_style = @terminal.color(Terminal::Colors::CYAN)
51
+ @subject_style = @terminal.color(nil, nil, Terminal::Attributes::BOLD)
52
+ @exception_title_style = @terminal.color(Terminal::Colors::RED, nil, Terminal::Attributes::BOLD)
53
+ @exception_line_style = @terminal.color(Terminal::Colors::RED)
54
+
55
+ @subjects = {}
56
+ end
57
+
58
+ attr :level
59
+
60
+ def level= value
61
+ if value.is_a? Symbol
62
+ @level = LEVELS[value]
63
+ else
64
+ @level = value
65
+ end
66
+ end
67
+
68
+ def enabled?(subject)
69
+ @subjects[subject.class] == true
70
+ end
71
+
72
+ def enable(subject)
73
+ @subjects[subject.class] = true
74
+ end
75
+
76
+ def disable(subject)
77
+ @subjects[subject.class] = false
78
+ end
79
+
80
+ def log(level, *args, &block)
81
+ unless level.is_a? Symbol
82
+ level = LEVELS[level]
83
+ end
84
+
85
+ self.send(level, *args, &block)
86
+ end
87
+
88
+ def format(subject = nil, *arguments, &block)
89
+ prefix = time_offset_prefix
90
+ indent = " " * prefix.size
91
+
92
+ if block_given?
93
+ arguments << yield
94
+ end
95
+
96
+ if subject
97
+ format_subject(prefix, subject)
98
+ end
99
+
100
+ arguments.each do |argument|
101
+ format_argument(indent, argument)
102
+ end
103
+ end
104
+
105
+ def format_argument(prefix, argument)
106
+ if argument.is_a? Exception
107
+ format_exception(prefix, argument)
108
+ else
109
+ format_value(prefix, argument)
110
+ end
111
+ end
112
+
113
+ def format_exception(indent, exception, prefix = nil, pwd: Dir.pwd)
114
+ @output.puts "#{indent}| #{prefix}#{@exception_title_style}#{exception.class}#{@reset_style}: #{exception}"
115
+
116
+ exception.backtrace.each_with_index do |line, index|
117
+ path, offset, message = line.split(":")
118
+
119
+ # Make the path a bit more readable
120
+ path.gsub!(/^#{pwd}\//, "./")
121
+
122
+ @output.puts "#{indent}| #{index == 0 ? "→" : " "} #{@exception_line_style}#{path}:#{offset}#{@reset_style} #{message}"
123
+ end
124
+
125
+ if exception.cause
126
+ @output.puts "#{indent}|"
127
+
128
+ format_exception(indent, exception.cause, "Caused by ", pwd: pwd)
129
+ end
130
+ end
131
+
132
+ def format_subject(prefix, subject)
133
+ @output.puts "#{@prefix_style}#{prefix}: #{@subject_style}#{subject}#{@reset_style}"
134
+ end
135
+
136
+ def format_value(indent, value)
137
+ @output.puts "#{indent}: #{value}"
138
+ end
139
+
140
+ def time_offset_prefix
141
+ offset = Time.now - @start
142
+ minutes = (offset/60).floor
143
+ seconds = (offset - (minutes*60))
144
+
145
+ if minutes > 0
146
+ "#{minutes}m#{seconds.floor}s"
147
+ else
148
+ "#{seconds.round(2)}s"
149
+ end.rjust(6)
150
+ end
151
+ end
152
+
24
153
  # The Async Logger class.
25
154
  class << self
26
155
  # @attr logger [Logger] the global logger instance used by `Async`.
@@ -39,5 +168,5 @@ module Async
39
168
  end
40
169
 
41
170
  # Create the logger instance.
42
- @logger = Logger.new($stderr).tap{|logger| logger.level = default_log_level}
171
+ @logger = Logger.new($stderr, level: self.default_log_level)
43
172
  end