File: README.md

package info (click to toggle)
ruby-async 1.30.3-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 536 kB
  • sloc: ruby: 3,651; makefile: 4
file content (234 lines) | stat: -rw-r--r-- 6,705 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# Getting Started

This guide explains how to use `async` for event-driven systems.

## Installation

Add the gem to your project:

~~~ bash
$ bundle add async
~~~

## Core Concepts

`async` has several core concepts:

- A {ruby Async::Task} instance which captures your sequential computations.
- A {ruby Async::Reactor} instance which implements the core event loop.

## Creating Tasks

The main entry point for creating tasks is the {ruby Kernel#Async} method. Because this method is defined on `Kernel`, it's available in all areas of your program.

~~~ ruby
require 'async'

Async do |task|
	puts "Hello World!"
end
~~~

An {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete.

At the top level, `Async do ... end` will create an event loop, and nested `Async` blocks will reuse the existing event loop. This allows the caller to have either blocking or non-blocking behaviour.

~~~ ruby
require 'async'

def sleepy(duration = 1)
	Async do |task|
		task.sleep duration
		puts "I'm done sleeping, time for action!"
	end
end

# Synchronous operation:
sleepy

# Asynchronous operation:
Async do
	# These two functions will sleep simultaneously.
	sleepy
	sleepy
end
~~~

If you want to guarantee synchronous execution, you can use {ruby Kernel#Sync} which is semantically identical to `Async` except that in all cases it will wait until the given block completes execution.

### Nested Tasks

Sometimes it's convenient to explicitly nest tasks. There are a variety of reasons to do this, including grouping tasks in order to wait for completion. In the most basic case, you can make a child task using the {ruby Async::Task#async} method.

~~~ ruby
require 'async'

def nested_sleepy(task: Async::Task.current)
	# Block caller
	task.sleep 0.1
	
	# Schedule nested task:
	subtask = task.async(annotation: "Sleeping") do |subtask|
		puts "I'm going to sleep..."
		subtask.sleep 1.0
	ensure
		puts "I'm waking up!"
	end
end

Async(annotation: "Top Level") do |task|
	subtask = nested_sleepy(task: task)
	
	task.reactor.print_hierarchy
	#<Async::Reactor:0x64 1 children (running)>
				#<Async::Task:0x78 Top Level (running)>
								#<Async::Task:0x8c Sleeping (running)>
end
~~~

This example creates a child `subtask` from the given parent `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.

## Waiting For Results

Like promises, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}:

~~~ ruby
require 'async'

task = Async do
	rand
end

puts task.wait
~~~

### Waiting For Multiple Tasks

You can use {ruby Async::Barrier#async} to create multiple child tasks, and wait for them all to complete using {ruby Async::Barrier#wait}.

{ruby Async::Barrier} and {ruby Async::Semaphore} are designed to be compatible with each other, and with other tasks that nest `#async` invocations. There are other similar situations where you may want to pass in a parent task, e.g. {ruby Async::IO::Endpoint#bind}.

~~~ ruby
barrier = Async::Barrier.new
semaphore = Async::Semaphore.new(2)

semaphore.async(parent: barrier) do
	# ...
end
~~~

A `parent:` in this context is anything that responds to `#async` in the same way that {ruby Async::Task} responds to `#async`. In situations where you strictly depend on the interface of {ruby Async::Task}, use the `task: Task.current` pattern.

### Stopping Tasks

Use {ruby Async::Task#stop} to stop tasks. This function raises {ruby Async::Stop} on the target task and all descendent tasks.

~~~ ruby
require 'async'

Async do
	sleepy = Async do |task|
		task.sleep 1000
	end
	
	sleepy.stop
end
~~~

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.

## Resource Management

In order to ensure your resources are cleaned up correctly, make sure you wrap resources appropriately, e.g.:

~~~ ruby
Async::Reactor.run do
	begin
		socket = connect(remote_address) # May raise Async::Stop

		socket.write(...) # May raise Async::Stop
		socket.read(...) # May raise Async::Stop
	ensure
		socket.close if socket
	end
end
~~~

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.

## Exception Handling

{ruby 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.

~~~ ruby
require 'async'

task = Async do
	# Exception will be logged and task will be failed.
	raise "Boom"
end

puts task.status # failed
puts task.result # raises RuntimeError: Boom
~~~

### Propagating Exceptions

If a task has finished due to an exception, calling `Task#wait` will re-raise the exception.

~~~ ruby
require 'async'

Async do
	task = Async do
		raise "Boom"
	end
	
	begin
		task.wait # Re-raises above exception.
	rescue
		puts "It went #{$!}!"
	end
end
~~~

## Timeouts

You can wrap asynchronous operations in a timeout. This ensures that malicious services don't cause your code to block indefinitely.

~~~ ruby
require 'async'

Async do |task|
	task.with_timeout(1) do
		task.sleep 100
	rescue Async::TimeoutError
		puts "I timed out!"
	end
end
~~~

### Reoccurring Timers

Sometimes you need to do some periodic work in a loop.

~~~ ruby
require 'async'

Async do |task|
	while true
		puts Time.now
		task.sleep 1
	end
end
~~~

## Caveats

### Enumerators

Due to limitations within Ruby and the nature of this library, it is not possible to use `to_enum` on methods which invoke asynchronous behaviour. We hope to [fix this issue in the future](https://github.com/socketry/async/issues/23).

### Blocking Methods in Standard Library

Blocking Ruby methods such as `pop` in the `Queue` class require access to their own threads and will not yield control back to the reactor which can result in a deadlock.  As a substitute for the standard library `Queue`, the {ruby Async::Queue} class can be used.