async 0.13.0 → 0.14.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
  SHA1:
3
- metadata.gz: 2fbc8a7a4d1987be4082ebae4b613319a95e936f
4
- data.tar.gz: a6eb19cd5a9485c3de75d4498e7f96b0f0755334
3
+ metadata.gz: 881ace206b49720782f4bf53c50cea4997efe5ac
4
+ data.tar.gz: f9b1ecbd174370ce7b62c6f4fdbd6a0dddda858c
5
5
  SHA512:
6
- metadata.gz: 9f53412d28ed692ca27c059371ea97bca9a7424e6dc37ad3b97e910c8031d475388de304030a8004cce54712712c59915071bff1fd281892a1ed96fb7bedbe14
7
- data.tar.gz: 6d2691f41bdd26610261269e8c15ef210696f15dba391119437758e604ac524fb0d65c871bdcc164babd6570b3c8fbea2784b4d3cd57ef9f4d6e6ced48552f67
6
+ metadata.gz: ef14bfef3d114ef2ee95f15fc239efad2b1f91efe5b0e99d8243dc3d5d487188cf41a7c5e36708c07d8f007db0b5fd7dfaf3ba731ab2a3fb98751bf0a6938b8c
7
+ data.tar.gz: fc1eeaadb11ec7dafe4e21ea2afa251508d1eaab8820d86b4e8a93aa7eca1828caa3c9bf42384cae7aec11afca8a0159b608fd732e26dcdf25118832fbcaa748
data/.rspec CHANGED
@@ -1,4 +1,3 @@
1
- --color
2
1
  --format documentation
3
2
  --warnings
4
3
  --require spec_helper
@@ -10,7 +10,9 @@ rvm:
10
10
  - 2.4
11
11
  - jruby-head
12
12
  - ruby-head
13
+ - rbx-3
13
14
  matrix:
14
15
  allow_failures:
15
16
  - rvm: ruby-head
16
17
  - rvm: jruby-head
18
+ - rvm: rbx-3
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Async
2
2
 
3
- Asynchronous I/O framework for Ruby based on [nio4r] and [timers].
3
+ Async is a composable asynchronous I/O framework for Ruby based on [nio4r] and [timers].
4
4
 
5
5
  [timers]: https://github.com/socketry/timers
6
6
  [nio4r]: https://github.com/socketry/nio4r
@@ -39,53 +39,69 @@ Or install it yourself as:
39
39
 
40
40
  ## Usage
41
41
 
42
- Implementing an asynchronous client/server is easy:
42
+ `Async::Reactor` is the top level IO reactor, and runs `Async::Task`s asynchronously. The reactor itself is not thread-safe, so you'd typically have one reactor per thread.
43
+
44
+ An `Async::Task` runs using a `Fiber` and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can succeed.
45
+
46
+ 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`.
47
+
48
+ ### Main Entry Points
49
+
50
+ #### `Async::Reactor.run`
51
+
52
+ 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.
43
53
 
44
54
  ```ruby
45
- #!/usr/bin/env ruby
46
-
47
- require 'async'
48
- require 'async/tcp_socket'
49
-
50
- def echo_server
51
- Async::Reactor.run do |task|
52
- # This is a synchronous block within the current task:
53
- task.with(TCPServer.new('localhost', 9000)) do |server|
54
-
55
- # This is an asynchronous block within the current reactor:
56
- task.reactor.with(server.accept) do |client|
57
- data = client.read(512)
58
-
59
- task.sleep(rand)
60
-
61
- client.write(data)
62
- end while true
63
- end
64
- end
55
+ def run_server
56
+ Async::Reactor.run do |task|
57
+ # ... acccept connections
58
+ end
65
59
  end
60
+ ```
61
+
62
+ 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)`.
63
+
64
+ 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.
65
+
66
+ #### `Async::Task#async`
67
+
68
+ 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.
66
69
 
67
- def echo_client(data)
68
- Async::Reactor.run do |task|
69
- Async::TCPServer.connect('localhost', 9000) do |socket|
70
- socket.write(data)
71
- puts "echo_client: #{socket.read(512)}"
72
- end
73
- end
70
+ ```ruby
71
+ def do_request(task: Task.current)
72
+ task.async do
73
+ # ... do some actual work
74
+ end
74
75
  end
76
+ ```
77
+
78
+ 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.
79
+
80
+ ### Reactor Tree
81
+
82
+ `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.
75
83
 
84
+ When invoking `Async::Reactor#stop`, you will stop *all* children tasks of that reactor. Tasks will raise `Async::Interrupt` 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.
85
+
86
+ ### Resource Management
87
+
88
+ In order to ensure your resources are cleaned up correctly, make sure you wrap resources appropriately, e.g.:
89
+
90
+ ```ruby
76
91
  Async::Reactor.run do
77
- # Start the echo server:
78
- server = echo_server
79
-
80
- 5.times.collect do |i|
81
- echo_client("Hello World #{i}")
82
- end.each(&:wait) # Wait until all clients are finished.
83
-
84
- # Terminate the server and all tasks created within it's async scope:
85
- server.stop
92
+ begin
93
+ socket = connect(remote_address) # May raise Async::Interrupt so socket could be nil
94
+
95
+ socket.write(...) # May raise Async::Interrupt
96
+ socket.read(...) # May raise Async::Interrupt
97
+ ensure
98
+ socket.close if socket
99
+ end
86
100
  end
87
101
  ```
88
102
 
103
+ 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.
104
+
89
105
  ## Supported Ruby Versions
90
106
 
91
107
  This library aims to support and is [tested against][travis] the following Ruby
@@ -119,7 +135,9 @@ dropped.
119
135
 
120
136
  ## See Also
121
137
 
138
+ - [async-io](https://github.com/socketry/async-io) — Asynchronous networking and sockets.
122
139
  - [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
140
+ - [async-rspec](https://github.com/socketry/async-rspec) — Shared contexts for running async specs.
123
141
  - [rubydns](https://github.com/ioquatix/rubydns) — A easy to use Ruby DNS server.
124
142
 
125
143
  ## License
data/Rakefile CHANGED
@@ -4,3 +4,7 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:test)
5
5
 
6
6
  task :default => :test
7
+
8
+ task :coverage do
9
+ ENV['COVERAGE'] = 'y'
10
+ end
@@ -21,13 +21,14 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
  spec.has_rdoc = "yard"
23
23
 
24
- spec.required_ruby_version = ">= 2.0.0"
24
+ spec.required_ruby_version = "~> 2.0"
25
25
 
26
26
  spec.add_runtime_dependency "nio4r"
27
27
  spec.add_runtime_dependency "timers", "~> 4.1"
28
28
 
29
+ spec.add_development_dependency "async-rspec", "~> 1.0"
30
+
29
31
  spec.add_development_dependency "bundler", "~> 1.3"
30
- spec.add_development_dependency "process-daemon", "~> 1.0.0"
31
- spec.add_development_dependency "rspec", "~> 3.4.0"
32
+ spec.add_development_dependency "rspec", "~> 3.4"
32
33
  spec.add_development_dependency "rake"
33
34
  end
@@ -29,6 +29,9 @@ module Async
29
29
  @children = Set.new
30
30
  @parent = nil
31
31
 
32
+ @annotation = nil
33
+ @object_name = nil
34
+
32
35
  if parent
33
36
  self.parent = parent
34
37
  end
@@ -40,6 +43,34 @@ module Async
40
43
  # @attr children [Set<Node>]
41
44
  attr :children
42
45
 
46
+ # A useful identifier for the current node.
47
+ attr :annotation
48
+
49
+ def annotate(annotation)
50
+ if block_given?
51
+ previous_annotation = @annotation
52
+ @annotation = annotation
53
+ yield
54
+ @annotation = previous_annotation
55
+ else
56
+ @annotation = annotation
57
+ end
58
+ end
59
+
60
+ def description
61
+ @object_name ||= "#{self.class}:0x#{object_id.to_s(16)}"
62
+
63
+ if @annotation
64
+ "#{@object_name} #{@annotation}"
65
+ else
66
+ @object_name
67
+ end
68
+ end
69
+
70
+ def to_s
71
+ "\#<#{description}>"
72
+ end
73
+
43
74
  # Change the parent of this node.
44
75
  # @param parent [Node, nil] the parent to attach to, or nil to detach.
45
76
  # @return [self]
@@ -81,5 +112,21 @@ module Async
81
112
  def reap(child)
82
113
  @children.delete(child)
83
114
  end
115
+
116
+ # Traverse the tree.
117
+ # @yield [node, level] The node and the level relative to the given root.
118
+ def traverse(level = 0, &block)
119
+ yield self, level
120
+
121
+ @children.each do |child|
122
+ child.traverse(level + 1, &block)
123
+ end
124
+ end
125
+
126
+ def print_hierarchy(out = $stdout)
127
+ self.traverse do |node, level|
128
+ out.puts "#{"\t" * level}#{node}"
129
+ end
130
+ end
84
131
  end
85
132
  end
@@ -35,7 +35,7 @@ module Async
35
35
  class Reactor < Node
36
36
  extend Forwardable
37
37
 
38
- # The preferred method to invoke asynchronous behavior.
38
+ # The preferred method to invoke asynchronous behavior at the top level.
39
39
  #
40
40
  # - When invoked within an existing reactor task, it will run the given block
41
41
  # asynchronously. Will return the task once it has been scheduled.
@@ -59,43 +59,35 @@ module Async
59
59
  return reactor
60
60
  end
61
61
  end
62
-
63
- # @param wrappers [Hash] A mapping for wrapping pre-existing IO objects.
64
- def initialize(wrappers: IO)
65
- super(nil)
66
-
67
- @wrappers = wrappers
62
+
63
+ def initialize
64
+ super
68
65
 
69
66
  @selector = NIO::Selector.new
70
67
  @timers = Timers::Group.new
71
68
 
72
69
  @stopped = true
73
70
  end
74
-
75
- # @attr wrappers [Object]
76
- attr :wrappers
77
- # @attr stopped [Boolean]
71
+
72
+ def to_s
73
+ "<#{self.description} stopped=#{@stopped}>"
74
+ end
75
+
76
+ # @attr stopped [Boolean]
78
77
  attr :stopped
79
78
 
80
79
  def_delegators :@timers, :every, :after
81
-
82
- # Wrap a given IO object and associate it with a specific task.
83
- # @param io The `IO` instance to wrap.
84
- # @param task [Task] The task which manages the wrapper.
85
- # @return [Wrapper]
86
- def wrap(io, task)
87
- @wrappers[io].new(io, task)
88
- end
89
-
90
- def with(io, &block)
91
- async do |task|
92
- task.with(io, &block)
93
- end
94
- end
95
-
96
- # @return [Task]
97
- def async(*ios, &block)
98
- task = Task.new(ios, self, &block)
80
+
81
+ # Start an asynchronous task within the specified reactor. The task will be
82
+ # executed until the first blocking call, at which point it will yield and
83
+ # and this method will return.
84
+ #
85
+ # This is the main entry point for scheduling asynchronus tasks.
86
+ #
87
+ # @yield [Task] Executed within the asynchronous task.
88
+ # @return [Task] The task that was
89
+ def async(*args, &block)
90
+ task = Task.new(self, &block)
99
91
 
100
92
  # I want to take a moment to explain the logic of this.
101
93
  # When calling an async block, we deterministically execute it until the
@@ -104,7 +96,7 @@ module Async
104
96
  # - Fail at the point of call where possible.
105
97
  # - Execute determinstically where possible.
106
98
  # - Avoid overhead if no blocking operation is performed.
107
- task.run
99
+ task.run(*args)
108
100
 
109
101
  # Async.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
110
102
  return task
@@ -141,7 +133,6 @@ module Async
141
133
  interval = 0 if interval && interval < 0
142
134
 
143
135
  Async.logger.debug{"[#{self} Pre] Updating #{@children.count} children..."}
144
- Async.logger.debug{@children.collect{|child| [child.to_s, child.alive?]}.inspect}
145
136
  # As timeouts may have been updated, and caused fibers to complete, we should check this.
146
137
 
147
138
  # If there is nothing to do, then finish:
@@ -161,16 +152,18 @@ module Async
161
152
 
162
153
  return self
163
154
  ensure
164
- Async.logger.debug{"[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{$!})..."}
165
- Async.logger.debug{@children.collect{|child| [child.to_s, child.alive?]}.inspect}
155
+ Async.logger.debug{"[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{$!.inspect})..."}
166
156
  @stopped = true
167
157
  end
168
158
 
169
- # Close each of the children tasts and selector.
159
+ # Stop each of the children tasks and close the selector.
160
+ #
170
161
  # @return [void]
171
162
  def close
172
163
  @children.each(&:stop)
173
164
 
165
+ # TODO Should we also clear all timers?
166
+
174
167
  @selector.close
175
168
  @selector = nil
176
169
  end
@@ -53,34 +53,25 @@ module Async
53
53
  return result
54
54
  end
55
55
  end
56
-
56
+
57
57
  # Create a new task.
58
- # @param ios [Array] an array of `IO` objects such as `TCPServer`, `Socket`, etc.
59
- # @param reactor [Async::Reactor]
60
- # @return [void]
61
- def initialize(ios, reactor)
62
- if parent = Task.current?
63
- super(parent)
64
- else
65
- super(reactor)
66
- end
67
-
68
- @ios = Hash[
69
- ios.collect{|io| [io.fileno, reactor.wrap(io, self)]}
70
- ]
58
+ # @param reactor [Async::Reactor] the reactor this task will run within.
59
+ # @param parent [Async::Task] the parent task.
60
+ def initialize(reactor, parent = Task.current?)
61
+ super(parent || reactor)
71
62
 
72
63
  @reactor = reactor
73
64
 
74
- @status = :running
65
+ @status = :initialized
75
66
  @result = nil
76
67
 
77
68
  @condition = nil
78
69
 
79
- @fiber = Fiber.new do
70
+ @fiber = Fiber.new do |args|
80
71
  set!
81
72
 
82
73
  begin
83
- @result = yield(*@ios.values, self)
74
+ @result = yield(self, *args)
84
75
  @status = :complete
85
76
  # Async.logger.debug("Task #{self} completed normally.")
86
77
  rescue Interrupt
@@ -93,19 +84,14 @@ module Async
93
84
  raise
94
85
  ensure
95
86
  # Async.logger.debug("Task #{self} closing: #{$!}")
96
- close
87
+ finish!
97
88
  end
98
89
  end
99
90
  end
100
91
 
101
- # Show the current status of the task as a string.
102
- # @todo (picat) Add test for this method?
103
92
  def to_s
104
- "#{super}[#{@status}]"
93
+ "<#{self.description} status=#{@status}>"
105
94
  end
106
-
107
- # @attr ios [Array<IO>] All wrappers associated with this task.
108
- attr :ios
109
95
 
110
96
  # @attr ios [Reactor] The reactor the task was created within.
111
97
  attr :reactor
@@ -119,10 +105,23 @@ module Async
119
105
  attr :status
120
106
 
121
107
  # Resume the execution of the task.
122
- def run
123
- @fiber.resume
108
+ def run(*args)
109
+ if @status == :initialized
110
+ @status = :running
111
+ @fiber.resume(*args)
112
+ else
113
+ raise RuntimeError, "Task already running!"
114
+ end
124
115
  end
125
-
116
+
117
+ def async(*args, &block)
118
+ task = Task.new(@reactor, self, &block)
119
+
120
+ task.run(*args)
121
+
122
+ return task
123
+ end
124
+
126
125
  # Retrieve the current result of the task. Will cause the caller to wait until result is available.
127
126
  # @raise [RuntimeError] if the task's fiber is the current fiber.
128
127
  # @return [Object]
@@ -150,62 +149,35 @@ module Async
150
149
  end
151
150
  end
152
151
 
153
- # Provide a wrapper to an IO object with a Reactor.
154
- # @yield [Async::Wrapper] a wrapped object.
155
- def with(io, *args)
156
- wrapper = @reactor.wrap(io, self)
157
- yield wrapper, *args
158
- ensure
159
- wrapper.close if wrapper
160
- io.close if io
161
- end
162
-
163
- # Wrap and bind the given object to the reactor.
164
- # @param io the native object to bind to this task.
165
- # @return [Wrapper] The wrapped object.
166
- def bind(io)
167
- @ios[io.fileno] ||= @reactor.wrap(io, self)
168
- end
169
-
170
- # Register a given IO with given interests to be able to monitor it.
171
- # @param io [IO] a native io object.
172
- # @param interests [Symbol] One of `:r`, `:w` or `:rw`.
173
- # @return [NIO::Monitor]
174
- def register(io, interests)
175
- @reactor.register(io, interests)
176
- end
177
-
178
152
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
179
153
  # @return [Async::Task]
180
154
  # @raise [RuntimeError] if task was not {set!} for the current fiber.
181
155
  def self.current
182
156
  Thread.current[:async_task] or raise RuntimeError, "No async task available!"
183
157
  end
184
-
185
-
158
+
186
159
  # Check if there is a task defined for the current fiber.
187
160
  # @return [Async::Task, nil]
188
161
  def self.current?
189
162
  Thread.current[:async_task]
190
163
  end
191
-
164
+
192
165
  # Check if the task is running.
193
166
  # @return [Boolean]
194
167
  def running?
195
168
  @status == :running
196
169
  end
197
-
170
+
198
171
  # Whether we can remove this node from the reactor graph.
199
172
  # @return [Boolean]
200
173
  def finished?
201
174
  super && @status != :running
202
175
  end
203
-
204
- # Close all bound IO objects.
205
- def close
206
- @ios.each_value(&:close)
207
- @ios = nil
208
-
176
+
177
+ private
178
+
179
+ # Finish the current task, and all bound bound IO objects.
180
+ def finish!
209
181
  # Attempt to remove this node from the task tree.
210
182
  consume
211
183
 
@@ -214,9 +186,7 @@ module Async
214
186
  @condition.signal(@result)
215
187
  end
216
188
  end
217
-
218
- private
219
-
189
+
220
190
  # Set the current fiber's `:async_task` to this task.
221
191
  def set!
222
192
  # This is actually fiber-local: