ori-rb 0.4
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.
- checksums.yaml +7 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/LICENSE +9 -0
- data/README.md +444 -0
- data/Rakefile +17 -0
- data/docs/images/example_boundary.png +0 -0
- data/docs/images/example_boundary_cancellation.png +0 -0
- data/docs/images/example_channel.png +0 -0
- data/docs/images/example_mutex.png +0 -0
- data/docs/images/example_promise.png +0 -0
- data/docs/images/example_semaphore.png +0 -0
- data/docs/images/example_trace.png +0 -0
- data/docs/images/example_trace_tag.png +0 -0
- data/lib/ori/channel.rb +148 -0
- data/lib/ori/lazy.rb +163 -0
- data/lib/ori/mutex.rb +9 -0
- data/lib/ori/out/index.html +146 -0
- data/lib/ori/out/script.js +3 -0
- data/lib/ori/promise.rb +39 -0
- data/lib/ori/reentrant_semaphore.rb +68 -0
- data/lib/ori/scope.rb +620 -0
- data/lib/ori/select.rb +35 -0
- data/lib/ori/selectable.rb +9 -0
- data/lib/ori/semaphore.rb +49 -0
- data/lib/ori/task.rb +78 -0
- data/lib/ori/timeout.rb +16 -0
- data/lib/ori/tracer.rb +335 -0
- data/lib/ori/version.rb +5 -0
- data/lib/ori.rb +68 -0
- data/mise-tasks/test +15 -0
- data/mise.toml +40 -0
- data/sorbet/config +8 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
- data/sorbet/rbi/gems/erb@5.1.1.rbi +845 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/io-console@0.8.1.rbi +9 -0
- data/sorbet/rbi/gems/json@2.15.1.rbi +2101 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sorbet/rbi/gems/minitest@5.26.0.rbi +2234 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/nio4r@2.7.4.rbi +293 -0
- data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
- data/sorbet/rbi/gems/parser@3.3.9.0.rbi +5535 -0
- data/sorbet/rbi/gems/pp@0.6.3.rbi +376 -0
- data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
- data/sorbet/rbi/gems/prism@1.5.2.rbi +42056 -0
- data/sorbet/rbi/gems/psych@5.2.6.rbi +2469 -0
- data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
- data/sorbet/rbi/gems/rake@13.3.0.rbi +3036 -0
- data/sorbet/rbi/gems/rbi@0.3.7.rbi +7115 -0
- data/sorbet/rbi/gems/rbs@3.9.5.rbi +6978 -0
- data/sorbet/rbi/gems/rdoc@6.15.0.rbi +12777 -0
- data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3845 -0
- data/sorbet/rbi/gems/reline@0.6.2.rbi +9 -0
- data/sorbet/rbi/gems/rexml@3.4.4.rbi +5285 -0
- data/sorbet/rbi/gems/rubocop-ast@1.47.1.rbi +7780 -0
- data/sorbet/rbi/gems/rubocop-shopify@2.17.1.rbi +9 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.11.0.rbi +2506 -0
- data/sorbet/rbi/gems/rubocop@1.81.1.rbi +63489 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
- data/sorbet/rbi/gems/spoom@1.6.3.rbi +6985 -0
- data/sorbet/rbi/gems/stringio@3.1.7.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
- data/sorbet/rbi/gems/thor@1.4.0.rbi +4399 -0
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
- data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +132 -0
- data/sorbet/rbi/gems/unicode-emoji@4.1.0.rbi +251 -0
- data/sorbet/rbi/gems/vernier@1.8.1-96ce5c739bfe6a18d2f4393f4219a1bf48674b87.rbi +904 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +1429 -0
- data/sorbet/shims/fiber.rbi +21 -0
- data/sorbet/shims/io.rbi +8 -0
- data/sorbet/shims/random.rbi +9 -0
- data/sorbet/shims/rdoc.rbi +3 -0
- data/sorbet/tapioca/require.rb +7 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 47a1dea9f17b477436372cac90626ab10885a35116474b3e83edbf448af359f6
|
4
|
+
data.tar.gz: 652a3eaf18ac42ad31914d68cf70f0495bb17bb27e925d01169e607ea9e6526c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2ab76d413f0b280abdc43a5d2cabc0723b42e034ddcff07c1eeaa81fffb4299b0ff7c84116c7d3bd2f922f973a2764b94ce875d49ae6b5fe5f0ea1d08704c6d6
|
7
|
+
data.tar.gz: 935da20da1fdacb76f2c9df5c94b87b86dafdd737c50b3927da35d03c2415dbf75f433863bd13eab783b05af74cf97a8b6710d2e8069b108b04841aee3abf9de
|
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.4.7
|
data/LICENSE
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Copyright 2025 Shopify Inc.
|
2
|
+
|
3
|
+
Copyright 2025 Jahfer Husain for contributions after October 14, 2025
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,444 @@
|
|
1
|
+
# Ori
|
2
|
+
|
3
|
+
Ori is a library for Ruby that provides a robust set of primitives for building concurrent applications. The name comes from the Japanese word 折り "ori" meaning "fold", reflecting how concurrent operations interleave.
|
4
|
+
|
5
|
+
Ori provides a set of primitives that allow you to build concurrent applications—that is, applications that interleave execution within a single thread—without blocking the entire Ruby interpreter for each task.
|
6
|
+
|
7
|
+
## Table of Contents
|
8
|
+
|
9
|
+
- [Installation](#installation)
|
10
|
+
- [Usage](#usage)
|
11
|
+
- [Defining Boundaries](#defining-boundaries)
|
12
|
+
- [Matching](#matching)
|
13
|
+
- [Timeouts and Cancellation](#timeouts-and-cancellation)
|
14
|
+
- [Enumerables](#enumerables)
|
15
|
+
- [Debugging](#debugging)
|
16
|
+
- [Concurrency Utilities](#concurrency-utilities)
|
17
|
+
- [`Ori::Promise`](#oripromise)
|
18
|
+
- [`Ori::Channel`](#orichannel)
|
19
|
+
- [`Ori::Mutex`](#orimutex)
|
20
|
+
- [`Ori::Semaphore`](#orisemaphore)
|
21
|
+
- [`Ori::Timeout`](#oritimeout)
|
22
|
+
- [Releases](#releases)
|
23
|
+
- [License](#license)
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem "ori-rb", "~> 0.2"
|
29
|
+
```
|
30
|
+
|
31
|
+
Then execute:
|
32
|
+
|
33
|
+
```sh
|
34
|
+
bundle install
|
35
|
+
```
|
36
|
+
|
37
|
+
In your Ruby code, you can then require the library:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require "ori"
|
41
|
+
```
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Ori aims to make concurrency in Ruby simple, intuitive, and easy to manage. There are only two decisions you need to make when using Ori:
|
46
|
+
|
47
|
+
1. What code must complete _before_ other code starts?
|
48
|
+
2. What code can run at the same time as other code?
|
49
|
+
|
50
|
+
### Defining Boundaries
|
51
|
+
|
52
|
+
At the core of Ori is the concurrency boundary. Ori guarantees everything inside of a boundary will complete before any code after the boundary starts. Boundaries can be freely nested, allowing you to define critical sections inside of other critical sections.
|
53
|
+
|
54
|
+
To create a new concurrency boundary, call `Ori.sync` with your block of code. Once inside the boundary, you can use `Ori::Scope#fork` to define and run concurrent work. Code written inside of the boundary but outside of `Ori::Scope#fork` will run synchronously from the perspective of the boundary. `Ori::Scope#fork` will return an `Ori::Task` object, which you can use to wait for the fiber to complete, or retrieve its result.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
Ori.sync do |scope|
|
58
|
+
# This runs in a new fiber
|
59
|
+
scope.fork do
|
60
|
+
sleep 1
|
61
|
+
puts "Hello from fiber!"
|
62
|
+
end
|
63
|
+
|
64
|
+
# This doesn't wait for the first fiber to complete
|
65
|
+
scope.fork do
|
66
|
+
sleep 0.5
|
67
|
+
puts "Another fiber here!"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Ori.sync blocks until all fibers complete
|
72
|
+
puts "Success!"
|
73
|
+
```
|
74
|
+
|
75
|
+
**Output:**
|
76
|
+
|
77
|
+
```
|
78
|
+
Another fiber here!
|
79
|
+
Hello from fiber!
|
80
|
+
Success!
|
81
|
+
```
|
82
|
+
|
83
|
+
<details>
|
84
|
+
<summary>See trace visualization</summary>
|
85
|
+
|
86
|
+

|
87
|
+
|
88
|
+
</details>
|
89
|
+
|
90
|
+
#### Matching
|
91
|
+
|
92
|
+
Ori has powerful support for matching against concurrent resources. If you have a set of blocking resources, you can use `Ori.select` in combination with Ruby's `case … in` pattern-matching to wait on the first available resource.
|
93
|
+
|
94
|
+
`Ori.select` will block until the first resource becomes available, returning that value and cancel waiting for the others. Matching against Ori's utility classes is particularly efficient because Ori can check internally if the blocking resources are available before attempting the heavier task of resuming the code.
|
95
|
+
|
96
|
+
See [Concurrency Utilities](#concurrency-utilities) for more details on these classes.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
promise = Ori::Promise.new
|
100
|
+
mutex = Ori::Mutex.new
|
101
|
+
channel = Ori::Channel.new(1)
|
102
|
+
timeout = Ori::Timeout.new(0.1) # stop after 100ms if no resource completes
|
103
|
+
|
104
|
+
case Ori.select([promise, mutex, channel, timeout])
|
105
|
+
in Ori::Promise(value) then puts "Promise: #{value}"
|
106
|
+
in Ori::Mutex then puts "Mutex acquired!"
|
107
|
+
in Ori::Channel(value) then puts "Channel: #{value}"
|
108
|
+
in Ori::Timeout then puts "Timeout!"
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
This matching syntax can also be leveraged to race multiple tasks against each other, in very compact form:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Ori.sync do |scope|
|
116
|
+
# Spawn 3 tasks
|
117
|
+
tasks = scope.fork_each(3.times).map { do_work }
|
118
|
+
|
119
|
+
# Wait for the first task to complete
|
120
|
+
Ori.select(tasks) => Ori::Task(value)
|
121
|
+
puts "First result: #{value}"
|
122
|
+
|
123
|
+
# Stop processing any further tasks
|
124
|
+
scope.shutdown!
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
If you have multiple of the same resource, you can perform an explicit match using Ruby's pattern matching syntax:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
promise_a = Ori::Promise.new
|
132
|
+
promise_b = Ori::Promise.new
|
133
|
+
|
134
|
+
case Ori.select([promise_a, promise_b])
|
135
|
+
in Ori::Promise(value) => p if p == promise_a
|
136
|
+
puts "Promise A: #{value}"
|
137
|
+
in Ori::Promise(value) => p if p == promise_b
|
138
|
+
puts "Promise B: #{value}"
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Timeouts and Cancellation
|
143
|
+
|
144
|
+
You can also use `Ori.sync` with timeouts to automatically cancel or raise after a specified duration.
|
145
|
+
|
146
|
+
When using `cancel_after: seconds`, the scope will be cancelled but the boundary will close with raising an error. With `raise_after: seconds`, a `Ori::Scope::CancellationError` will be raised from the boundary call site after the specified duration. Both options will properly clean up any internally-spawned fibers and nested scopes.
|
147
|
+
|
148
|
+
A parent scope's deadline is inherited by child scopes, and cancelling a parent scope will cancel all child scopes:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
Ori.sync(raise_after: 5) do |scope|
|
152
|
+
# This inner scope inherits the 5 second deadline
|
153
|
+
scope.fork do
|
154
|
+
# Will raise `Ori::CancellationError` after 5 seconds
|
155
|
+
sleep(10)
|
156
|
+
end
|
157
|
+
|
158
|
+
# This inner scope has a shorter deadline
|
159
|
+
Ori.sync(cancel_after: 2) do |child_scope|
|
160
|
+
child_scope.fork do
|
161
|
+
# Will be cancelled after 2 seconds
|
162
|
+
sleep(10)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
<details>
|
169
|
+
<summary>See trace visualization</summary>
|
170
|
+
|
171
|
+

|
172
|
+
|
173
|
+
</details>
|
174
|
+
|
175
|
+
### Enumerables
|
176
|
+
|
177
|
+
As a convenience, `Ori::Scope` provides an `#fork_each` method that will spawn a new fiber for each item in an enumerable. This can be useful for performing concurrent operations on a collection.
|
178
|
+
|
179
|
+
The following code contains six seconds of `sleep` time, but will take only ~1 second to execute due to the interleaving of the fibers:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
Ori.sync do |scope|
|
183
|
+
# Spawns a new fiber for each item in the array
|
184
|
+
scope.fork_each([1, 2, 3]) do |item|
|
185
|
+
puts "Processing #{item}"
|
186
|
+
sleep(1)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Any Enumerable can be used
|
190
|
+
scope.fork_each(3.times) do |i|
|
191
|
+
puts "Processing #{i}"
|
192
|
+
sleep(1)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
### Debugging
|
198
|
+
|
199
|
+
To help understand your program, Ori comes with several utilities to help you visualize the execution of your program, as well as being supported by the broader Ruby ecosystem.
|
200
|
+
|
201
|
+
#### Vernier
|
202
|
+
|
203
|
+
The HEAD of [jhawthorn/vernier](https://github.com/jhawthorn/vernier) supports tracking the spawning and yielding of fibers, to help analyze your concurrent program over time.
|
204
|
+
|
205
|
+
#### Plain-Text Visualization
|
206
|
+
|
207
|
+
`Ori::Scope#print_ascii_trace` will print the trace to stdout in plaintext. While useful as a quick overview, it's not interactive and the level of detail is limited.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
closed_scope = Ori.sync { ... }
|
211
|
+
closed_scope.print_ascii_trace
|
212
|
+
```
|
213
|
+
|
214
|
+
```
|
215
|
+
Fiber Execution Timeline (0.001s)
|
216
|
+
==============================================================================================
|
217
|
+
Main |▶.........↻.........................↻..................↻........................▒|
|
218
|
+
Fiber 1 |█▶═.╎------▶▒ |
|
219
|
+
Fiber 2 | █▶═══~╎--▶~╎-----------------▶══~╎▶~╎------------▶══▒ |
|
220
|
+
Fiber 3 | █▶╎--▶╎----------------------▶╎----------------▶═~╎-----------------▶══▒ |
|
221
|
+
==============================================================================================
|
222
|
+
Legend: (█ Start) (▒ Finish) (═ Running) (~ IO-Wait) (. Sleeping) (╎ Yield) (✗ Error)
|
223
|
+
```
|
224
|
+
|
225
|
+
#### HTML Visualization
|
226
|
+
|
227
|
+
`Ori::Scope#write_html_trace(dir)` will generate an `index.html` file in the specified directory containing a fully interactive timeline of the scope's execution.
|
228
|
+
|
229
|
+

|
230
|
+
|
231
|
+
##### Tags
|
232
|
+
|
233
|
+
`#write_html_trace` also supports use of `Ori::Scope#tag` to add custom labels to the trace.
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
closed_scope = Ori.sync do |scope|
|
237
|
+
scope.fork do
|
238
|
+
scope.tag("Going to sleep")
|
239
|
+
sleep(0.0001)
|
240
|
+
scope.tag("Woke up")
|
241
|
+
end
|
242
|
+
|
243
|
+
scope.fork do
|
244
|
+
scope.tag("Not sure what to do")
|
245
|
+
Fiber.yield
|
246
|
+
scope.tag("Finished yielding")
|
247
|
+
end
|
248
|
+
|
249
|
+
scope.tag("Finished queueing work")
|
250
|
+
end
|
251
|
+
|
252
|
+
closed_scope.write_html_trace(File.join(__dir_, "out"))
|
253
|
+
```
|
254
|
+
|
255
|
+

|
256
|
+
|
257
|
+
### Concurrency Utilities
|
258
|
+
|
259
|
+
Ori comes with several utilities to help you build concurrent applications. Keep in mind that these utilities are not thread-safe and should only be used in a concurrent context. The particular usefulness of these utilities are primarily how they interact with the scheduler, yielding control back to other fibers when blocked.
|
260
|
+
|
261
|
+
#### `Ori::Promise`
|
262
|
+
|
263
|
+
Promises represent values that may not be immediately available:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
Ori.sync do |scope|
|
267
|
+
promise = Ori::Promise.new
|
268
|
+
scope.fork do
|
269
|
+
sleep(1)
|
270
|
+
promise.resolve("Hello from the future!")
|
271
|
+
end
|
272
|
+
# Wait for the promise to be fulfilled
|
273
|
+
result = promise.await
|
274
|
+
puts result # => "Hello from the future!"
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
<details>
|
279
|
+
<summary>See trace visualization</summary>
|
280
|
+
|
281
|
+

|
282
|
+
|
283
|
+
</details>
|
284
|
+
|
285
|
+
#### `Ori::Channel`
|
286
|
+
|
287
|
+
Channels provide a way to communicate between fibers by passing values between them. Channels can buffer up to a specified number of items. When the channel is full, `put`/`<<` will block until there is room:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
Ori.sync do |scope|
|
291
|
+
channel = Ori::Channel.new(2)
|
292
|
+
# Producer
|
293
|
+
scope.fork do
|
294
|
+
# Will block after the first two puts
|
295
|
+
5.times { |i| channel << i }
|
296
|
+
end
|
297
|
+
|
298
|
+
# Consumer
|
299
|
+
scope.fork do
|
300
|
+
5.times { puts "Received: #{channel.take}" }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
<details>
|
306
|
+
<summary>See trace visualization</summary>
|
307
|
+
|
308
|
+

|
309
|
+
|
310
|
+
</details>
|
311
|
+
|
312
|
+
If a channel has a capacity of `0`, it becomes a simple synchronous queue:
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
channel = Ori::Channel.new(0)
|
316
|
+
channel << 1 # Will block until `take` is called
|
317
|
+
```
|
318
|
+
|
319
|
+
#### `Ori::Mutex`
|
320
|
+
|
321
|
+
When you need to enforce a critical section with strict ordering, use a mutex:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
result = []
|
325
|
+
Ori.sync do |scope|
|
326
|
+
mutex = Ori::Mutex.new
|
327
|
+
counter = 0
|
328
|
+
|
329
|
+
scope.fork do
|
330
|
+
mutex.sync do
|
331
|
+
current = counter
|
332
|
+
result << [:A, :read, current]
|
333
|
+
Fiber.yield # Simulate work
|
334
|
+
counter = current + 1
|
335
|
+
result << [:A, :write, counter]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
scope.fork do
|
340
|
+
mutex.sync do
|
341
|
+
current = counter
|
342
|
+
result << [:B, :read, current]
|
343
|
+
counter = current + 1
|
344
|
+
result << [:B, :write, counter]
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
result.each { |r| puts r.inspect }
|
350
|
+
```
|
351
|
+
|
352
|
+
**Output:**
|
353
|
+
|
354
|
+
```
|
355
|
+
[:A, :read, 0]
|
356
|
+
[:A, :write, 1]
|
357
|
+
[:B, :read, 1]
|
358
|
+
[:B, :write, 2]
|
359
|
+
```
|
360
|
+
|
361
|
+
Without a mutex, the `counter` variable would be read and written in an interleaved manner, leading to race conditions where both fibers read `0`:
|
362
|
+
|
363
|
+
```
|
364
|
+
[:A, :read, 0]
|
365
|
+
[:B, :read, 0]
|
366
|
+
[:B, :write, 1]
|
367
|
+
[:A, :write, 1]
|
368
|
+
```
|
369
|
+
|
370
|
+
<details>
|
371
|
+
<summary>See trace visualization</summary>
|
372
|
+
|
373
|
+

|
374
|
+
|
375
|
+
</details>
|
376
|
+
|
377
|
+
#### `Ori::Semaphore`
|
378
|
+
|
379
|
+
Semaphors are a generalized form of mutexes that can be used to control access to _n_ limited resources:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
Ori.sync do |scope|
|
383
|
+
# Allow up to 3 concurrent operations
|
384
|
+
semaphore = Ori::Semaphore.new(3)
|
385
|
+
|
386
|
+
10.times do |i|
|
387
|
+
scope.fork do
|
388
|
+
semaphore.sync do
|
389
|
+
puts "Processing #{i}"
|
390
|
+
sleep(1) # Simulate work
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
```
|
396
|
+
|
397
|
+
#### `Ori::Timeout`
|
398
|
+
|
399
|
+
A timeout is a special resource that will cancel after a specified duration. It's primary use case is as a resource in `Ori.select`.
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
Ori.sync do |scope|
|
403
|
+
promise = Ori::Promise.new
|
404
|
+
timeout = Ori::Timeout.new(0.1) # stop after 100ms if the promise hasn't resolved
|
405
|
+
|
406
|
+
scope.fork do
|
407
|
+
sleep(0.2)
|
408
|
+
promise.resolve("Hello from the future!")
|
409
|
+
end
|
410
|
+
|
411
|
+
case Ori.select([promise, timeout])
|
412
|
+
in Ori::Promise(value) then puts "Promise: #{value}"
|
413
|
+
in Ori::Timeout then puts "Timeout!"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
```
|
417
|
+
|
418
|
+
**Output:**
|
419
|
+
|
420
|
+
```
|
421
|
+
Timeout!
|
422
|
+
```
|
423
|
+
|
424
|
+
<details>
|
425
|
+
<summary>See trace visualization</summary>
|
426
|
+
|
427
|
+

|
428
|
+
|
429
|
+
</details>
|
430
|
+
|
431
|
+
## Releases
|
432
|
+
|
433
|
+
The procedure to publish a new release version is as follows:
|
434
|
+
|
435
|
+
- Update `lib/ori/version.rb`
|
436
|
+
- Run bundle install to bump the version of the gem in `Gemfile.lock`
|
437
|
+
- Open a pull request, review, and merge
|
438
|
+
- Review commits since the last release to identify user-facing changes that should be included in the release notes
|
439
|
+
- [Create a release on GitHub](https://github.com/jahfer/ori/releases/new) with a version number that matches `lib/ori/version.rb`
|
440
|
+
- Deploy the gem
|
441
|
+
|
442
|
+
## License
|
443
|
+
|
444
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.ruby_opts = ["-W0", "-W:deprecated"]
|
8
|
+
t.libs << "test"
|
9
|
+
t.libs << "lib"
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
11
|
+
end
|
12
|
+
|
13
|
+
require "rubocop/rake_task"
|
14
|
+
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
|
17
|
+
task default: [:test, :rubocop]
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/ori/channel.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module Ori
|
4
|
+
#: [E]
|
5
|
+
class Channel
|
6
|
+
include(Ori::Selectable)
|
7
|
+
|
8
|
+
EMPTY = "empty"
|
9
|
+
|
10
|
+
#: (Integer size) -> void
|
11
|
+
def initialize(size)
|
12
|
+
@size = size
|
13
|
+
if size.zero?
|
14
|
+
# Zero-sized channel state
|
15
|
+
@taker_waiting = false
|
16
|
+
@sender_waiting = false
|
17
|
+
@value = EMPTY
|
18
|
+
else
|
19
|
+
# Buffered channel state
|
20
|
+
@queue = UnboundedQueue.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#: (E item) -> void
|
25
|
+
def put(item)
|
26
|
+
if @size.zero?
|
27
|
+
put_zero_sized(item)
|
28
|
+
else
|
29
|
+
put_buffered(item)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
alias_method(:<<, :put)
|
33
|
+
|
34
|
+
#: () -> E
|
35
|
+
def take
|
36
|
+
if @size.zero?
|
37
|
+
take_zero_sized
|
38
|
+
else
|
39
|
+
take_buffered
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#: () -> E
|
44
|
+
def peek
|
45
|
+
if @size.zero?
|
46
|
+
peek_zero_sized
|
47
|
+
else
|
48
|
+
peek_buffered
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#: () -> bool
|
53
|
+
def value?
|
54
|
+
if @size.zero?
|
55
|
+
@value != EMPTY
|
56
|
+
else
|
57
|
+
@queue.peek != UnboundedQueue::EMPTY
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#: () -> Channel[E]
|
62
|
+
def await
|
63
|
+
peek
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
#: () -> Array[E]
|
68
|
+
def deconstruct
|
69
|
+
Ori.sync { peek }
|
70
|
+
[take]
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Zero-sized channel implementation
|
76
|
+
def put_zero_sized(item)
|
77
|
+
@sender_waiting = true
|
78
|
+
begin
|
79
|
+
@value = item
|
80
|
+
Fiber.yield until @taker_waiting
|
81
|
+
ensure
|
82
|
+
@taker_waiting = false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def take_zero_sized
|
87
|
+
@taker_waiting = true
|
88
|
+
begin
|
89
|
+
Fiber.yield(self) until @value != EMPTY
|
90
|
+
@value
|
91
|
+
ensure
|
92
|
+
@value = EMPTY
|
93
|
+
@sender_waiting = false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def peek_zero_sized
|
98
|
+
Fiber.yield(self) until @sender_waiting
|
99
|
+
@value
|
100
|
+
end
|
101
|
+
|
102
|
+
# Buffered channel implementation
|
103
|
+
def put_buffered(item)
|
104
|
+
Fiber.yield until @queue.size < @size
|
105
|
+
@queue.push(item)
|
106
|
+
end
|
107
|
+
|
108
|
+
def take_buffered
|
109
|
+
Fiber.yield(self) until value?
|
110
|
+
@queue.shift
|
111
|
+
end
|
112
|
+
|
113
|
+
def peek_buffered
|
114
|
+
Fiber.yield(self) until value?
|
115
|
+
@queue.peek
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO: implement sliding queue, dropping queue
|
120
|
+
class UnboundedQueue
|
121
|
+
EMPTY = "empty"
|
122
|
+
|
123
|
+
def initialize
|
124
|
+
@buffer = []
|
125
|
+
end
|
126
|
+
|
127
|
+
def size
|
128
|
+
@buffer.size
|
129
|
+
end
|
130
|
+
|
131
|
+
def push(item)
|
132
|
+
@buffer << item
|
133
|
+
end
|
134
|
+
|
135
|
+
def peek
|
136
|
+
if @buffer.empty?
|
137
|
+
EMPTY
|
138
|
+
else
|
139
|
+
@buffer.first
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def shift
|
144
|
+
@buffer.shift
|
145
|
+
end
|
146
|
+
end
|
147
|
+
private_constant(:UnboundedQueue)
|
148
|
+
end
|