farce 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +117 -1489
- data/docs/compatibility.md +74 -0
- data/docs/concurrency.md +151 -0
- data/docs/data-structures.md +313 -0
- data/docs/installation.md +63 -0
- data/docs/integrations.md +124 -0
- data/docs/introduction.md +266 -0
- data/docs/scheduling.md +176 -0
- data/docs/sharing.md +211 -0
- data/docs/shims.md +144 -0
- data/docs/utilities.md +93 -0
- data/docs/variants-and-scopes.md +92 -0
- data/lib/farce/abstract/flag.rb +6 -0
- data/lib/farce/backports.rb +274 -0
- data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
- data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
- data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
- data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
- data/lib/farce/engine/ruby/3.4/port.rb +13 -12
- data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
- data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
- data/lib/farce/engine/ruby/4.0/port.rb +1 -2
- data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/null_port.rb +18 -0
- data/lib/farce/engine/shared/port.rb +41 -3
- data/lib/farce/engine/shared/queue.rb +4 -4
- data/lib/farce/engine/shared/ractor_methods.rb +2 -2
- data/lib/farce/engine/shared/ractor_selector.rb +73 -0
- data/lib/farce/extension.rb +150 -0
- data/lib/farce/integrations/ractor_tmvar.rb +4 -1
- data/lib/farce/internal/fake_ractor.rb +10 -2
- data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
- data/lib/farce/null_port.rb +12 -0
- data/lib/farce/system.rb +10 -2
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +1 -1
- metadata +22 -1
data/README.md
CHANGED
|
@@ -1,1619 +1,247 @@
|
|
|
1
1
|
# Farce: Fiber and Ractor Compatibility Enabler
|
|
2
2
|
|
|
3
|
-
Farce provides
|
|
3
|
+
Farce provides mutable data structures and coordination tools for Ruby code that uses **threads, Ractors, and Fiber schedulers**. Use it to share state between workers, connect background computation to an event loop, or make a gem compatible with the caller's concurrency model.
|
|
4
4
|
|
|
5
|
-
**Here's a taste:**
|
|
5
|
+
**Here's a taste:** four workers updating the same map, without losing increments or managing a lock.
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
|
|
9
|
-
output = Farce::Mutable.new "Hello from Farce! The map has 0 entries, with a total sum of 0."
|
|
10
|
-
|
|
11
|
-
# Mutate output in another Ractor - this would not work with a String.
|
|
12
|
-
# Using Farce::Ractor here instead of Ractor so it also works on JRuby and TruffleRuby.
|
|
13
|
-
ractor = Farce::Ractor.new(output) do |output|
|
|
14
|
-
sleep rand # I laugh in the face of race conditions. Ha, ha, ha, ha!
|
|
15
|
-
output.gsub!("Farce", "✨ Farce ✨")
|
|
16
|
-
end
|
|
8
|
+
require "farce"
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
100.times
|
|
22
|
-
|
|
23
|
-
# And of course we need to do it all in parallel for maximum performance!
|
|
24
|
-
# Let's ignore the fact that this would be much faster if we didn't.
|
|
25
|
-
# Starting 100 ractors is the main performance issue here.
|
|
26
|
-
# But that wouldn't be an interesting example, right?
|
|
27
|
-
Farce::Ractor.new(output, map) do |output, map|
|
|
28
|
-
key = %i[foo bar baz].sample
|
|
29
|
-
|
|
30
|
-
# Wrap the modifications in a transaction, so map and output never disagree
|
|
31
|
-
Farce.transaction(output, map) do |_, output, map|
|
|
32
|
-
|
|
33
|
-
# Increase the value we have for the given key by one!
|
|
34
|
-
map[key] ||= 0
|
|
35
|
-
map[key] += 1
|
|
36
|
-
|
|
37
|
-
# Could also replace the lines above with:
|
|
38
|
-
#
|
|
39
|
-
# map.upsert(key, 1) { it + 1 }
|
|
40
|
-
#
|
|
41
|
-
# … which would be atomic.
|
|
42
|
-
# But we're in a transaction, so everything is atomic! ☢️
|
|
43
|
-
|
|
44
|
-
output.sub!(/\d+ entries/, "#{map.size} entries")
|
|
45
|
-
output.sub!(/sum of \d+/, "sum of #{map.values.sum}")
|
|
10
|
+
counts = Farce::Map.new
|
|
11
|
+
workers = 4.times.map do
|
|
12
|
+
Farce.in_parallel(counts) do |counts|
|
|
13
|
+
100.times do
|
|
14
|
+
counts.upsert(:completed, 1) { |count| count + 1 }
|
|
46
15
|
end
|
|
47
16
|
end
|
|
48
|
-
|
|
49
|
-
end.each(&:join)
|
|
50
|
-
|
|
51
|
-
# Let's be sure that first ractor has completed its modification
|
|
52
|
-
ractor.join
|
|
53
|
-
|
|
54
|
-
# Hello from ✨ Farce ✨! The map has 3 entries with a total sum of 100.
|
|
55
|
-
puts output
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
🤨 **You don't see why you'd want this?**<br>
|
|
59
|
-
→ Start with the [Introduction](#introduction).
|
|
60
|
-
|
|
61
|
-
📖 **Now you want to know what else Farce can do?**<br>
|
|
62
|
-
→ Check out the [Features](#features)!
|
|
63
|
-
|
|
64
|
-
🔎 **Returning user and you need the details?**<br>
|
|
65
|
-
→ See the [API reference](https://rkh.github.io/farce/).
|
|
66
|
-
|
|
67
|
-
🛠️ **Want to contribute?**<br>
|
|
68
|
-
→ Read the [contribution guide](CONTRIBUTING.md), [code of conduct](CODE_OF_CONDUCT.md), and [security policy](SECURITY.md).
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## Table of Contents
|
|
72
|
-
|
|
73
|
-
- [Farce: Fiber and Ractor Compatibility Enabler](#farce-fiber-and-ractor-compatibility-enabler)
|
|
74
|
-
- [Table of Contents](#table-of-contents)
|
|
75
|
-
- [Introduction](#introduction)
|
|
76
|
-
- [The Problem](#the-problem)
|
|
77
|
-
- [The Solution](#the-solution)
|
|
78
|
-
- [Features](#features)
|
|
79
|
-
- [Data Structures](#data-structures)
|
|
80
|
-
- [Maps](#maps)
|
|
81
|
-
- [Vectors](#vectors)
|
|
82
|
-
- [Sets](#sets)
|
|
83
|
-
- [Atoms](#atoms)
|
|
84
|
-
- [Counters](#counters)
|
|
85
|
-
- [Flags](#flags)
|
|
86
|
-
- [References](#references)
|
|
87
|
-
- [Molecules](#molecules)
|
|
88
|
-
- [Queues](#queues)
|
|
89
|
-
- [Priority Queues](#priority-queues)
|
|
90
|
-
- [Timer Queues](#timer-queues)
|
|
91
|
-
- [Queue capacity](#queue-capacity)
|
|
92
|
-
- [Shims, Polyfills, and Extensions](#shims-polyfills-and-extensions)
|
|
93
|
-
- [Ractor](#ractor)
|
|
94
|
-
- [Port](#port)
|
|
95
|
-
- [WeakRef](#weakref)
|
|
96
|
-
- [Resolv](#resolv)
|
|
97
|
-
- [Sharing Unshareable Data](#sharing-unshareable-data)
|
|
98
|
-
- [Sharing Modes](#sharing-modes)
|
|
99
|
-
- [Envelopes](#envelopes)
|
|
100
|
-
- [Mutables](#mutables)
|
|
101
|
-
- [Proxies](#proxies)
|
|
102
|
-
- [Mode Managers](#mode-managers)
|
|
103
|
-
- [Leases](#leases)
|
|
104
|
-
- [Variants and Scopes](#variants-and-scopes)
|
|
105
|
-
- [Available Variants](#available-variants)
|
|
106
|
-
- [Provided scopes](#provided-scopes)
|
|
107
|
-
- [Concurrency](#concurrency)
|
|
108
|
-
- [Locks](#locks)
|
|
109
|
-
- [Atomic Operations](#atomic-operations)
|
|
110
|
-
- [Observability and Signaling](#observability-and-signaling)
|
|
111
|
-
- [Transactions](#transactions)
|
|
112
|
-
- [Maps and Sets](#maps-and-sets)
|
|
113
|
-
- [TVars](#tvars)
|
|
114
|
-
- [Scheduling Code](#scheduling-code)
|
|
115
|
-
- [`Farce.on_main`](#farceon_main)
|
|
116
|
-
- [`Farce.in_parallel`](#farcein_parallel)
|
|
117
|
-
- [`Farce.schedule`](#farceschedule)
|
|
118
|
-
- [Schedulers](#schedulers)
|
|
119
|
-
- [Ractor Pools](#ractor-pools)
|
|
120
|
-
- [Tasks](#tasks)
|
|
121
|
-
- [Third-Party Fiber Schedulers](#third-party-fiber-schedulers)
|
|
122
|
-
- [Integrations](#integrations)
|
|
123
|
-
- [Active Support](#active-support)
|
|
124
|
-
- [Dry Types](#dry-types)
|
|
125
|
-
- [JSON, YAML, etc](#json-yaml-etc)
|
|
126
|
-
- [Concurrent Ruby](#concurrent-ruby)
|
|
127
|
-
- [Ractor Sharing](#ractor-sharing)
|
|
128
|
-
- [Additional Integrations](#additional-integrations)
|
|
129
|
-
- [Disable Automatic loading](#disable-automatic-loading)
|
|
130
|
-
- [Miscellaneous](#miscellaneous)
|
|
131
|
-
- [Top Level Methods](#top-level-methods)
|
|
132
|
-
- [Additional Classes](#additional-classes)
|
|
133
|
-
- [Shareability Mixins](#shareability-mixins)
|
|
134
|
-
- [Constants](#constants)
|
|
135
|
-
- [Compatibility and Dependencies](#compatibility-and-dependencies)
|
|
136
|
-
- [Ruby](#ruby)
|
|
137
|
-
- [Optional Dependencies](#optional-dependencies)
|
|
138
|
-
- [Similar Projects](#similar-projects)
|
|
139
|
-
- [Installation](#installation)
|
|
140
|
-
- [Globally](#globally)
|
|
141
|
-
- [As a project dependency](#as-a-project-dependency)
|
|
142
|
-
- [As a library dependency](#as-a-library-dependency)
|
|
143
|
-
- [Local setup](#local-setup)
|
|
144
|
-
- [Loading Farce](#loading-farce)
|
|
145
|
-
- [Known Issues and Limitations](#known-issues-and-limitations)
|
|
146
|
-
- [Possible discrepancy regarding frozen state in Ruby and C](#possible-discrepancy-regarding-frozen-state-in-ruby-and-c)
|
|
147
|
-
- [Housekeeping](#housekeeping)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
## Introduction
|
|
151
|
-
|
|
152
|
-
Ruby 3.0 introduced two powerful concurrency primitives: **Fiber schedulers** and **Ractors**, besides the already existing **Threads**:
|
|
153
|
-
|
|
154
|
-
* **Threads** are the classic concurrency primitive in Ruby, and mostly map to native operating system threads. This way any system calls, IO, or other blocking operations will block a thread, allowing another one to run. However, on the official Ruby implementation (often referred to as CRuby or MRI), Threads cannot run in parallel due to the Global VM Lock (GVL). This means that even if you have multiple threads, only one of them can execute Ruby code at a time.
|
|
155
|
-
* **Ractors** provide parallelism, even on CRuby. They make it very hard to introduce concurrency issues between them, but put severe limitations on state sharing and cross-ractor communication. Ractors are only supported by CRuby, but that is generally not an issue, as other implementations, such as JRuby and TruffleRuby, support true parallelism with Threads. As of Ruby 4.0, Ractors are still considered experimental.
|
|
156
|
-
* **Fiber schedulers** allow you to run multiple Fibers concurrently on a single Thread, without having to explicitly pass control between them. This is a very efficient way to run concurrent code, especially for IO-bound workloads. As of Ruby 4.0, Fiber schedulers with IO support are still considered experimental.
|
|
157
|
-
|
|
158
|
-
All three of these need some form of **concurrency coordination** for shared state. These are usually the most complicated for Threads, as state is shared between them freely, and an interrupt can happen at any time. Fibers on the other hand have explicit control transfer (only on blocking operations or when giving up control), so their behavior is much more predictable. Ractors disallow sharing mutable state between them (or have built-in locking for the few cases where they allow it).
|
|
159
|
-
|
|
160
|
-
### The Problem
|
|
161
|
-
|
|
162
|
-
> [!NOTE]
|
|
163
|
-
> Please keep in mind that many of these libraries may gain better Ractor support in the future, and that the table might already be outdated. Feel free to open an issue if you find any inaccuracies. This is also in no way meant as a criticism of any of the libraries mentioned. They all have their own scopes, goals and priorities, and largely rely on volunteer contributions.
|
|
164
|
-
|
|
165
|
-
Both the Ruby core library, and other libraries, like the very popular [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem, provide plenty of primitives to solve this. However, most of them are not compatible with Ractors. And those that are, are then in turn usually not compatible with Fiber schedulers. Due to this, most of the popular frameworks and libraries out there do not support Ractors at all:
|
|
166
|
-
|
|
167
|
-
<table>
|
|
168
|
-
<thead>
|
|
169
|
-
<tr>
|
|
170
|
-
<th>Library</th>
|
|
171
|
-
<th>Example classes</th>
|
|
172
|
-
<th>Multithreading</th>
|
|
173
|
-
<th>Non-blocking Fibers</th>
|
|
174
|
-
<th>Cross-Ractor Usage</th>
|
|
175
|
-
</tr>
|
|
176
|
-
</thead>
|
|
177
|
-
<tbody>
|
|
178
|
-
<tr>
|
|
179
|
-
<td rowspan="5"><a href="https://www.ruby-lang.org/">Ruby</a></td>
|
|
180
|
-
<td>Array, Hash, String, Set, …</td>
|
|
181
|
-
<td>⚠️ not thread-safe</td>
|
|
182
|
-
<td>✅ supported</td>
|
|
183
|
-
<td>⚠️ immutable/copy/move only</td>
|
|
184
|
-
</tr>
|
|
185
|
-
<tr>
|
|
186
|
-
<td>Thread, Fiber</td>
|
|
187
|
-
<td>✅ supported</td>
|
|
188
|
-
<td>✅ supported</td>
|
|
189
|
-
<td>❌ <b>not supported</b></td>
|
|
190
|
-
</tr>
|
|
191
|
-
<tr>
|
|
192
|
-
<td>Mutex, ConditionVariable, Queue</td>
|
|
193
|
-
<td>✅ supported</td>
|
|
194
|
-
<td>⚠️ CRuby only</td>
|
|
195
|
-
<td>❌ <b>not supported</b></td>
|
|
196
|
-
</tr>
|
|
197
|
-
<tr>
|
|
198
|
-
<td>Ractor, Ractor::Port</td>
|
|
199
|
-
<td>✅ supported</td>
|
|
200
|
-
<td>❌ <b>not supported</b></td>
|
|
201
|
-
<td>✅ supported</td>
|
|
202
|
-
</tr>
|
|
203
|
-
<tr>
|
|
204
|
-
<td>WeakRef, Ruby::Box</td>
|
|
205
|
-
<td>⚠️ Main Ractor only</td>
|
|
206
|
-
<td>⚠️ Main Ractor only</td>
|
|
207
|
-
<td>❌ <b>not supported</b></td>
|
|
208
|
-
</tr>
|
|
209
|
-
<tr>
|
|
210
|
-
<td><a href="https://github.com/ruby-concurrency/concurrent-ruby">concurrent-ruby</a></td>
|
|
211
|
-
<td>Future, Exchanger, Tuple, …</td>
|
|
212
|
-
<td>⚠️ Main Ractor only</td>
|
|
213
|
-
<td>⚠️ Main Ractor only</td>
|
|
214
|
-
<td>❌ <b>not supported</b></td>
|
|
215
|
-
</tr>
|
|
216
|
-
<tr>
|
|
217
|
-
<td rowspan="2"><a href="https://github.com/socketry/async">async</a></td>
|
|
218
|
-
<td>Promise, Condition, Queue, …</td>
|
|
219
|
-
<td>✅ supported</td>
|
|
220
|
-
<td>✅ supported</td>
|
|
221
|
-
<td>❌ <b>not supported</b></td>
|
|
222
|
-
</tr>
|
|
223
|
-
<tr>
|
|
224
|
-
<td>Scheduler</td>
|
|
225
|
-
<td>⚠️ Main Ractor only</td>
|
|
226
|
-
<td>⚠️ Main Ractor only</td>
|
|
227
|
-
<td>❌ <b>not supported</b></td>
|
|
228
|
-
</tr>
|
|
229
|
-
<tr>
|
|
230
|
-
<td rowspan="3"><a href="https://github.com/mperham/ratomic">ratomic</a></td>
|
|
231
|
-
<td>Pool</td>
|
|
232
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
233
|
-
<td>❌ <b>not supported</b></td>
|
|
234
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
235
|
-
</tr>
|
|
236
|
-
<tr>
|
|
237
|
-
<td>LocalPool</td>
|
|
238
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
239
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
240
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
241
|
-
</tr>
|
|
242
|
-
<tr>
|
|
243
|
-
<td>Queue, Map</td>
|
|
244
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
245
|
-
<td>❌ <b>not supported</b></td>
|
|
246
|
-
<td>💣 <b>breaks isolation¹²</b></td>
|
|
247
|
-
</tr>
|
|
248
|
-
<tr>
|
|
249
|
-
<td><a href="https://github.com/MadBomber/ractor_queue">ractor_queue</a></td>
|
|
250
|
-
<td>RactorQueue</td>
|
|
251
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
252
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
253
|
-
<td>💣 <b>breaks isolation¹²</b></td>
|
|
254
|
-
</tr>
|
|
255
|
-
<tr>
|
|
256
|
-
<td>
|
|
257
|
-
<a href="https://github.com/hamstergem/hamster">hamster</a> /
|
|
258
|
-
<a href="https://github.com/immutable-ruby/immutable-ruby">immutable</a>
|
|
259
|
-
</td>
|
|
260
|
-
<td>Hash, Vector, Set, List, …</td>
|
|
261
|
-
<td>⚠️ Main Ractor only</td>
|
|
262
|
-
<td>⚠️ Main Ractor only</td>
|
|
263
|
-
<td>❌ <b>not supported</b></td>
|
|
264
|
-
</tr>
|
|
265
|
-
<tr>
|
|
266
|
-
<td><a href="https://github.com/ko1/ractor-sharing">ractor-sharing</a></td>
|
|
267
|
-
<td>TVar, LockVar, LockHash, …</td>
|
|
268
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
269
|
-
<td>❌ <b>not supported</b></td>
|
|
270
|
-
<td>⚠️ CRuby only<b>¹</b></td>
|
|
271
|
-
</tr>
|
|
272
|
-
<tr>
|
|
273
|
-
<td rowspan="2"><a href="https://github.com/jhawthorn/ractor_safe">ractor_safe</a></td>
|
|
274
|
-
<td>HashMap, AtomicInteger</td>
|
|
275
|
-
<td>⚠️ CRuby only</td>
|
|
276
|
-
<td>⚠️ CRuby only</td>
|
|
277
|
-
<td>✅ supported</td>
|
|
278
|
-
</tr>
|
|
279
|
-
<tr>
|
|
280
|
-
<td>Queue</td>
|
|
281
|
-
<td>⚠️ CRuby only</td>
|
|
282
|
-
<td>❌ <b>not supported</b></td>
|
|
283
|
-
<td>✅ supported</td>
|
|
284
|
-
</tr>
|
|
285
|
-
</tbody>
|
|
286
|
-
</table>
|
|
287
|
-
|
|
288
|
-
Notes:
|
|
289
|
-
1. Incorrectly flags mutable objects as frozen.
|
|
290
|
-
2. Breaking Ractor isolation introduces concurrency issues not just in application code, but in Ruby itself, possibly leading to segmentation faults or undefined behavior. Note that `ractor_queue` has the ability to enforce shareability, but this feature is opt-in.
|
|
291
|
-
|
|
292
|
-
### The Solution
|
|
293
|
-
|
|
294
|
-
**Farce is an attempt to solve this.** Code relying on Farce will work well with any concurrency primitive. Use an **event loop with the [async](https://github.com/socketry/async)** gem? Farce will fit right in. Running a **pool of Ractors** with [Kino](https://github.com/yaroslav/kino)? Farce got you covered! You manually manage a single **background thread**? Farce solves that, too!
|
|
295
|
-
|
|
296
|
-
And it does so in a non-invasive way. You should be able to use Farce alongside any other gem!
|
|
297
|
-
|
|
298
|
-
```ruby
|
|
299
|
-
# Using a counter as an example, there are many more classes provided by Farce
|
|
300
|
-
counter = Farce::Counter.new
|
|
301
|
-
|
|
302
|
-
# Works in code without concurrency
|
|
303
|
-
counter.add(5)
|
|
304
|
-
counter.to_i # => 5
|
|
305
|
-
|
|
306
|
-
# Works with threads
|
|
307
|
-
wait_for = 5.times.map do
|
|
308
|
-
Thread.new { counter.increment }
|
|
309
17
|
end
|
|
18
|
+
workers.each(&:join)
|
|
310
19
|
|
|
311
|
-
#
|
|
312
|
-
wait_for += 5.times.map do
|
|
313
|
-
Ractor.new(counter) { it.increment }
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Works with the async gem
|
|
317
|
-
Async do
|
|
318
|
-
5.times do
|
|
319
|
-
Async { counter.increment }
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
wait_for.each(&:join)
|
|
324
|
-
counter.to_i # => 20
|
|
20
|
+
counts[:completed] # => 400
|
|
325
21
|
```
|
|
326
22
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
It does so by choosing the best implementation for the situation. The counter in the above example will use Java's `AtomicLong` on JRuby and TruffleRuby in GraalVM mode, an `AtomicReference` on TruffleRuby in native mode, and an atomic, native counter on CRuby, so it runs without having to use any locks on any of these platforms.
|
|
330
|
-
|
|
331
|
-
This is especially useful when coordinating work between Fiber schedulers and Ractors:
|
|
23
|
+
[Introduction](#introduction) · [Features](#features) · [Installation](#installation) · [Compatibility](#compatibility-and-dependencies) · [API reference](https://rkh.github.io/farce/)
|
|
332
24
|
|
|
333
|
-
|
|
334
|
-
queue = Farce::Queue.new
|
|
25
|
+
## Introduction
|
|
335
26
|
|
|
336
|
-
|
|
337
|
-
# This would not work with Ruby's built-in Queue
|
|
338
|
-
Ractor.new(queue) do |queue|
|
|
339
|
-
loop { queue.push expensive_work }
|
|
340
|
-
end
|
|
27
|
+
Threads share memory, Ractors enable parallel Ruby execution on CRuby with restricted sharing, and Fiber schedulers handle concurrent IO efficiently. A primitive that works with one may fail with another.
|
|
341
28
|
|
|
342
|
-
|
|
343
|
-
Async do
|
|
344
|
-
# Task waiting for work from the queue
|
|
345
|
-
Async { loop { do_something queue.pop } }
|
|
29
|
+
Farce's shared containers preserve Ractor isolation. Their blocking operations suspend non-blocking fibers without stopping the scheduler. You can use them alongside existing gems, without adopting Farce's own schedulers.
|
|
346
30
|
|
|
347
|
-
|
|
348
|
-
Async { unrelated_work }
|
|
349
|
-
end
|
|
350
|
-
```
|
|
31
|
+
→ [Why Farce exists, comparisons, and a larger example](docs/introduction.md).
|
|
351
32
|
|
|
352
33
|
## Features
|
|
353
34
|
|
|
354
35
|
### Data Structures
|
|
355
36
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
#### Maps
|
|
37
|
+
Use familiar Ruby APIs with coordinated access to shared state:
|
|
359
38
|
|
|
360
|
-
Maps
|
|
39
|
+
- **Maps, vectors, and sets** for collections, with sorted and weak-reference variants.
|
|
40
|
+
- **Atoms, counters, flags, and molecules** for individual values and structured records.
|
|
41
|
+
- **FIFO, priority, and timer queues** for passing work, applying back pressure, or delaying delivery.
|
|
361
42
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
```ruby
|
|
365
|
-
map = Farce::Map.new
|
|
366
|
-
map[:foo] = :bar
|
|
367
|
-
|
|
368
|
-
map.merge! answer: 42
|
|
369
|
-
map.transform_values! { -it.to_s }
|
|
370
|
-
map.to_h # => {answer: "42", foo: "bar"}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
In addition, they come with a range of [atomic operations](#atomic-operations), and have built-in key normalization support:
|
|
374
|
-
|
|
375
|
-
```ruby
|
|
376
|
-
# normalize_keys can be a symbol, proc, hash, or another map
|
|
377
|
-
map = Farce::Map.new({ a: 10 }, normalize_keys: :to_s)
|
|
378
|
-
|
|
379
|
-
# Atomic upsert operation
|
|
380
|
-
2.times { map.upsert(:b, 42) { it * map[:a] } }
|
|
381
|
-
|
|
382
|
-
# Keys have been converted to strings
|
|
383
|
-
map.keys.sort # => ["a", "b"]
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Besides the standard map implementation, Farce includes a range of specialized maps:
|
|
387
|
-
|
|
388
|
-
* `LRUMap` will evict the least recently used entry to not grow beyond the maximum size.
|
|
389
|
-
* `LFUMap` will evict the least frequently used entry to not grow beyond the maximum size.
|
|
390
|
-
* `LeaseMap` manages [leases](#leases) associated with known keys.
|
|
391
|
-
* `TreeMap` uses a [red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) instead of a [hash table](https://en.wikipedia.org/wiki/Hash_table) to store its entries, keeping them sorted by the key's value.
|
|
392
|
-
* `WeakMap` only holds weak references to its keys and values, automatically removing entries when the corresponding key or value gets garbage collected.
|
|
393
|
-
* `WeakKeyMap` only holds weak references to its keys, automatically removing entries when the corresponding key gets garbage collected.
|
|
394
|
-
* `WeakValueMap` only holds weak references to its values, automatically removing entries when the corresponding value gets garbage collected.
|
|
395
|
-
|
|
396
|
-
For example, bounded maps are useful for implementing caches:
|
|
397
|
-
|
|
398
|
-
```ruby
|
|
399
|
-
cache = Farce::LRUMap.new(max_size: 2)
|
|
400
|
-
|
|
401
|
-
cache[:first] = 1
|
|
402
|
-
cache[:second] = 2
|
|
403
|
-
cache[:first] # makes sure :first was accessed more recently than :second
|
|
404
|
-
cache[:third] = 3
|
|
405
|
-
|
|
406
|
-
cache.key?(:second) # => false
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
#### Vectors
|
|
410
|
-
|
|
411
|
-
`Farce::Vector` is to `Array` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
|
|
43
|
+
For example, use a Vector as a shared list:
|
|
412
44
|
|
|
413
45
|
```ruby
|
|
414
46
|
list = Farce::Vector.new
|
|
415
|
-
10.times { list << it }
|
|
416
|
-
list.to_a # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
417
|
-
list.sum # => 45
|
|
418
|
-
```
|
|
419
47
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
```ruby
|
|
423
|
-
list = Farce::Vector[1, 2, 3, 4]
|
|
424
|
-
|
|
425
|
-
begin
|
|
426
|
-
list.map! do |value|
|
|
427
|
-
raise "error" if value == 2
|
|
428
|
-
value * 2
|
|
429
|
-
end
|
|
430
|
-
rescue => e
|
|
48
|
+
100.times do
|
|
49
|
+
Farce.in_parallel(list) { it << Some.expensive_computation }
|
|
431
50
|
end
|
|
432
|
-
|
|
433
|
-
# This would return 2 if list was an array
|
|
434
|
-
list.first # => 1
|
|
435
51
|
```
|
|
436
52
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
`Farce::Set` is to `Set` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
|
|
53
|
+
Or use a bounded map as a shared cache:
|
|
440
54
|
|
|
441
55
|
```ruby
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
Besides the standard set implementation, Farce includes two specialized sets:
|
|
449
|
-
|
|
450
|
-
* `SortedSet` keeps its entries in order based on value comparison.
|
|
451
|
-
* `WeakSet` only holds weak references to its entries.
|
|
452
|
-
|
|
453
|
-
#### Atoms
|
|
454
|
-
|
|
455
|
-
Atoms are value containers. These store a single reference to any Ruby object, and expose atomic operations for mutating these values:
|
|
456
|
-
|
|
457
|
-
```ruby
|
|
458
|
-
atom = Farce::Atom.new("initial value")
|
|
459
|
-
atom.value # => "initial value"
|
|
460
|
-
|
|
461
|
-
# atoms are ractor-shareable
|
|
462
|
-
Ractor.new(atom) do |atom|
|
|
463
|
-
atom.update { |current| current + " updated" } # => "initial value updated"
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# waits for the other ractor to perform its update
|
|
467
|
-
atom.wait_until_changed "initial value"
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
`WeakAtom` only keeps a weak reference for its current value, reverting to `nil` when the current value gets garbage collected.
|
|
471
|
-
|
|
472
|
-
#### Counters
|
|
473
|
-
|
|
474
|
-
Ractor-shareable, atomic counter. This is a stateful, numeric object.
|
|
475
|
-
|
|
476
|
-
It is similar to creating an [atom](#atoms) for an integer, but significantly faster, as it will use a truly lock-free implementation on most CPU/Ruby combinations.
|
|
477
|
-
|
|
478
|
-
```ruby
|
|
479
|
-
counter = Farce::Counter.new
|
|
480
|
-
counter.value # => 0
|
|
481
|
-
|
|
482
|
-
# Increase the counter by 1
|
|
483
|
-
counter.increment
|
|
484
|
-
counter.value # => 1
|
|
485
|
-
|
|
486
|
-
# Increase the counter by 5 on another Ractor
|
|
487
|
-
Ractor.new(counter) { it.add(5) }
|
|
488
|
-
|
|
489
|
-
# Give the other ractor time to run
|
|
490
|
-
sleep 0.1
|
|
491
|
-
|
|
492
|
-
counter.value # => 6
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
#### Flags
|
|
496
|
-
|
|
497
|
-
Ractor-shareable, atomic boolean.
|
|
498
|
-
|
|
499
|
-
Just like Counter for Integer, this is similar to creating an [atom](#atoms) for a boolean, but significantly faster, as it will use a truly lock-free implementation on most CPU/Ruby combinations.
|
|
500
|
-
|
|
501
|
-
```ruby
|
|
502
|
-
flag = Farce::Flag.new(true)
|
|
503
|
-
flag.value # => true
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
#### References
|
|
507
|
-
|
|
508
|
-
Atoms, as well as other value objects, like counters and flags, can be wrapped in a reference object, which delegates all methods to the given atom for convenience:
|
|
509
|
-
|
|
510
|
-
```ruby
|
|
511
|
-
atom = Farce::Atom.new(42)
|
|
512
|
-
ref = Farce::Reference.new(atom)
|
|
513
|
-
|
|
514
|
-
ref.to_s # => "42"
|
|
515
|
-
ref > 10 # => true
|
|
516
|
-
|
|
517
|
-
atom.value = :foo
|
|
518
|
-
ref.to_s # => "foo"
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
#### Molecules
|
|
522
|
-
|
|
523
|
-
Molecules are structures made up of multiple atoms. Clever naming, right?
|
|
524
|
-
|
|
525
|
-
They are similar to Ruby's built-in `Struct` and `Data` classes, but their members are stored in atoms:
|
|
526
|
-
|
|
527
|
-
```ruby
|
|
528
|
-
Request = Farce::Molecule.define(:verb, :path)
|
|
529
|
-
request = Request.new("GET", "/index.html")
|
|
530
|
-
request.verb # => "GET"
|
|
531
|
-
request.verb = "POST"
|
|
532
|
-
|
|
533
|
-
# This doesn't succeed, as the verb isn't "HEAD"
|
|
534
|
-
request.verb_atom.compare_and_set("HEAD", "GET")
|
|
535
|
-
request.verb # => "POST"
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
You can also pass a block to add additional methods:
|
|
539
|
-
|
|
540
|
-
```ruby
|
|
541
|
-
Request = Farce::Molecule.define(:verb, :path) do
|
|
542
|
-
def to_s = "#{verb} #{path}"
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
Request.new(verb: "GET", path: "/").to_s # => "GET /"
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
You can also inherit from the generated class:
|
|
549
|
-
|
|
550
|
-
```ruby
|
|
551
|
-
class Request < Farce::Molecule.define(:verb, :path)
|
|
552
|
-
def to_s = "#{verb} #{path}"
|
|
553
|
-
end
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
#### Queues
|
|
557
|
-
|
|
558
|
-
`Farce::Queue` is an alternative to Ruby's `Queue` and `SizedQueue`. It provides a superset of their API, offering both blocking and a non-blocking API, while implementing Ractor-sharing via [sharing modes](#sharing-modes).
|
|
559
|
-
|
|
560
|
-
```ruby
|
|
561
|
-
queue = Farce::Queue.new
|
|
562
|
-
queue.push(:ok)
|
|
563
|
-
queue.pop # => :ok
|
|
564
|
-
queue.try_pop # => nil
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
Queues provide additional features over Ruby's queue:
|
|
568
|
-
* Like the rest of Farce, they work with Ractors (implementing [sharing modes](#sharing-modes) for their [default variant](#variants-and-scopes)).
|
|
569
|
-
* In addition to closing, they allow sealing, which prevents pushes but still allows pulling from the queue until it has been drained.
|
|
570
|
-
* They have an API to wait for them to be ready for a push or pull based on capacity without actually adding or removing values.
|
|
571
|
-
* They can optionally monitor how long the oldest item has been sitting in the queue (which is great for building auto-scaling on top of the queue).
|
|
572
|
-
|
|
573
|
-
The [strict variant](#available-variants) has comparable performance to `SizedQueue`, while the default variant pays a fixed overhead for [handling unshareable data](#sharing-unshareable-data). Both variants outperform other third-party, Ractor-shareable queues (none of which are Fiber scheduler compatible, which Farce's queues are).
|
|
574
|
-
|
|
575
|
-
##### Priority Queues
|
|
576
|
-
|
|
577
|
-
In addition to normal queues, which are FIFO, Farce also offers a `PriorityQueue`, where values are removed from the queue in order of priority instead of insertion order:
|
|
578
|
-
|
|
579
|
-
```ruby
|
|
580
|
-
# use order: :descending to reverse priorities
|
|
581
|
-
queue = Farce::PriorityQueue.new
|
|
582
|
-
|
|
583
|
-
queue.push :a, priority: 2
|
|
584
|
-
queue.push :b, priority: 1
|
|
585
|
-
|
|
586
|
-
queue.pop # => :b
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
##### Timer Queues
|
|
590
|
-
|
|
591
|
-
Timer queues are similar to priority queues, sorted by float values, but these values are timestamps (based on [Farce::Clock](#additional-classes), not on Ruby's Time, which isn't monotonic). Any `pop` will block until the next timestamp has been reached.
|
|
592
|
-
|
|
593
|
-
```ruby
|
|
594
|
-
queue = Farce::TimerQueue.new
|
|
595
|
-
queue.push(:ok, in: 1.5)
|
|
596
|
-
|
|
597
|
-
# blocks for ~1.5 seconds
|
|
598
|
-
queue.pop # => :ok
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
Timer queues are useful for implementing schedulers (which is what Farce uses them for under the hood). Both `push` and `try_push` return a cancellation handle when they succeed. Use `entry.delete` or `queue.delete(entry)` to cancel a scheduled value. The handle exposes its `queue` and scheduled `clock` time.
|
|
602
|
-
|
|
603
|
-
```ruby
|
|
604
|
-
queue = Farce::TimerQueue.new
|
|
605
|
-
entry = queue.push(:work, in: 60)
|
|
606
|
-
|
|
607
|
-
entry.delete # => true
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
You can also delete a value if you know its timestamp:
|
|
611
|
-
|
|
612
|
-
```ruby
|
|
613
|
-
queue = Farce::TimerQueue.new
|
|
614
|
-
timestamp = Farce.clock(in: 0.2)
|
|
615
|
-
|
|
616
|
-
queue.push(:first, at: timestamp - 0.1)
|
|
617
|
-
queue.push(:second, at: timestamp)
|
|
618
|
-
queue.push(:third, at: timestamp + 0.1)
|
|
619
|
-
|
|
620
|
-
fourth = queue.push(:fourth, at: timestamp + 0.2)
|
|
621
|
-
queue.delete(fourth)
|
|
622
|
-
|
|
623
|
-
queue.delete(:second, at: timestamp)
|
|
624
|
-
|
|
625
|
-
result = []
|
|
626
|
-
result << queue.pop until queue.empty?
|
|
627
|
-
result # => [:first, :third]
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
##### Queue capacity
|
|
631
|
-
|
|
632
|
-
Queues support setting a maximum capacity. If the capacity has been reached it will block any pushes to create back pressure.
|
|
633
|
-
|
|
634
|
-
```ruby
|
|
635
|
-
queue = Farce::Queue.new(capacity: 2)
|
|
636
|
-
|
|
637
|
-
queue.try_push(:a) # => true
|
|
638
|
-
queue.try_push(:b) # => true
|
|
639
|
-
queue.try_push(:c) # => false
|
|
56
|
+
cache = Farce::LRUMap.new(max_size: 2)
|
|
57
|
+
cache[:a] = 1
|
|
58
|
+
cache[:b] = 2
|
|
59
|
+
cache[:c] = 3
|
|
60
|
+
cache.key?(:a) # => false, the least recently used entry was evicted
|
|
640
61
|
```
|
|
641
62
|
|
|
642
|
-
|
|
643
|
-
Priority queues and timer queues do not have a default capacity, so earlier entries are not getting blocked unexpectedly.
|
|
63
|
+
→ [Collection APIs and examples](docs/data-structures.md).
|
|
644
64
|
|
|
645
65
|
### Shims, Polyfills, and Extensions
|
|
646
66
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
Farce
|
|
650
|
-
|
|
651
|
-
On platforms that don't support Ractors, a Thread-based implementation is provided that tracks Ractor-membership via Thread
|
|
652
|
-
groups.
|
|
653
|
-
|
|
654
|
-
```ruby
|
|
655
|
-
ractor = Farce::Ractor.new do
|
|
656
|
-
value = receive
|
|
657
|
-
puts "Received #{value.inspect}"
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
ractor.send 42
|
|
661
|
-
```
|
|
67
|
+
- **`Farce::Ractor`** uses native Ractors on CRuby and threads on JRuby and TruffleRuby.
|
|
68
|
+
- **`Farce::Port`** adds transfer modes, receive timeouts, and Fiber scheduler support.
|
|
69
|
+
- **`Farce::WeakRef` and `Farce::Resolv`** work outside the main Ractor.
|
|
70
|
+
- **`Farce::Backports`** backports newer `Ractor` and `Ractor::Port` features to older Ruby versions. Can be invoked directly, used with refinements, or installed globally.
|
|
662
71
|
|
|
663
|
-
|
|
72
|
+
Send a result back from a worker:
|
|
664
73
|
|
|
665
|
-
Farce includes `Farce::Port`, which is either an extended subclass of `Ractor::Port` if it is available, or a polyfill if it isn't.
|
|
666
|
-
|
|
667
|
-
In addition to `Ractor::Port` it supports the following features:
|
|
668
|
-
* [Sharing modes](#sharing-modes) both for `.new` and `#send`.
|
|
669
|
-
* Opt-in auto-local sharing: If the port belongs to the current Ractor, any object can be sent to it without copying or moving it.
|
|
670
|
-
* `#receive` supports a `timeout` option on all Ruby implementations and versions, not just CRuby 4.1+
|
|
671
|
-
* You can use `#owned?` to check if the current Ractor owns a port.
|
|
672
|
-
* `#receive` does not block a Fiber scheduler when called from a non-blocking Fiber.
|
|
673
|
-
|
|
674
74
|
```ruby
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
port.send(object)
|
|
680
|
-
port.receive.equal?(object) # => true
|
|
75
|
+
results = Farce::Port.new
|
|
76
|
+
worker = Farce::Ractor.new(results) { |port| port.send(6 * 7) }
|
|
77
|
+
results.receive # => 42
|
|
681
78
|
```
|
|
682
79
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
`Farce::WeakRef` is a drop-in replacement for Ruby's [`WeakRef`](https://docs.ruby-lang.org/en/4.0/WeakRef.html), which at the time of writing cannot be used outside of the main Ractor at all.
|
|
686
|
-
|
|
687
|
-
Farce implements a version based on weak [atoms](#atoms). Not only can it be used on non-main Ractors, `Farce::WeakRef` instances themselves are Ractor-shareable if the value they reference is also Ractor-shareable (or has been garbage collected). Moreover, calling `Ractor.make_shareable(weak_ref)` will propagate through to the referenced value.
|
|
688
|
-
|
|
689
|
-
#### Resolv
|
|
690
|
-
|
|
691
|
-
`Farce::Resolv` is a version of Ruby's [`Resolv`](https://docs.ruby-lang.org/en/4.0/Resolv.html), using [counters](#counters) instead of a global instance variable, and can therefore be used outside the main Ractor.
|
|
80
|
+
→ [Portable Ractor APIs and extensions](docs/shims.md).
|
|
692
81
|
|
|
693
82
|
### Sharing Unshareable Data
|
|
694
83
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
1. If the object is Ractor shareable, pass it by reference.
|
|
698
|
-
2. If the object isn't Ractor shareable and the `move` option hasn't been set to `true`, pass it by value (copy it).
|
|
699
|
-
3. Otherwise move it from the sending Ractor to the receiving Ractor, invalidating any previous references.
|
|
700
|
-
|
|
701
|
-
This is a great start but has some complications:
|
|
702
|
-
* Copying can lead to a lot of data duplication, as large object trees may be copied between Ractors over and over again.
|
|
703
|
-
* Moving may invalidate nested references unexpectedly, breaking code in the sending Ractor.
|
|
704
|
-
* If you want to prevent sending unshareable objects to other Ractors, you have to manually check them every time.
|
|
705
|
-
|
|
706
|
-
Farce has mechanisms and tools to improve the situation.
|
|
707
|
-
|
|
708
|
-
#### Sharing Modes
|
|
709
|
-
|
|
710
|
-
> [!TIP]
|
|
711
|
-
> Learn more in the dedicated [modes documentation](docs/modes.md).
|
|
712
|
-
|
|
713
|
-
The [default variants](#variants-and-scopes) of most [data structures](#data-structures), as well as [`Farce::Port`](#port), implement a range of sharing modes, typically as a keyword argument for initialization as well as for methods that modify their content.
|
|
714
|
-
|
|
715
|
-
These modes are:
|
|
716
|
-
|
|
717
|
-
| Mode | What happens to non-shareable data | A typical use |
|
|
718
|
-
| --- | --- | --- |
|
|
719
|
-
| `:smart` (default) | Applies smart rules or a dedicated proxy, raising for unsupported values. | Share supported values with less copying. |
|
|
720
|
-
| `:smart_raise` | Applies smart rules or a dedicated proxy, with a raising fallback. | Reject unsupported transfers. |
|
|
721
|
-
| `:smart_copy` | Applies smart rules or a dedicated proxy, then tries copying and Marshal. Default for `Farce::Port`. | Send values with a copying fallback. |
|
|
722
|
-
| `:smart_move` | Applies smart rules or a dedicated proxy, then tries moving. | Hand off unsupported values. |
|
|
723
|
-
| `:copy` | Transfers a copy and leaves the original usable. | Send a snapshot of a request. |
|
|
724
|
-
| `:marshal` | Serializes a snapshot and reconstructs it once per receiving Ractor. | Transfer an Unshared container.|
|
|
725
|
-
| `:move` | Transfers ownership and makes the original inaccessible. | Hand a completed batch to a consumer. |
|
|
726
|
-
| `:local` | Keeps the same object in its originating Ractor. | Pass work between local threads or fibers. |
|
|
727
|
-
| `:make_shareable` | Calls `Ractor.make_shareable` on the original. | Publish finished configuration. |
|
|
728
|
-
| `:mutable` | Copies non-shareable objects into a [`Farce::Mutable`](#mutables). | Synchronize mutations across Ractors. |
|
|
729
|
-
| `:shareable_copy` | Makes a shareable copy and leaves the original alone. | Publish a snapshot of an editable document. |
|
|
730
|
-
| `:dedup` | Deduplicates the value, then makes it shareable. May update and freeze the original. | Reuse repeated message contents. |
|
|
731
|
-
| `:proxy` | Creates a [`Farce::Proxy`](#proxies) that executes calls in the original Ractor. | Share access to a mutable object. |
|
|
732
|
-
| `:raise` | Raises `Ractor::IsolationError`. | Enforce a shareable-data boundary. |
|
|
733
|
-
|
|
734
|
-
Here's an example:
|
|
735
|
-
|
|
736
|
-
```ruby
|
|
737
|
-
map = Farce::Map.new(mode: :mutable)
|
|
738
|
-
map[:content] = "Hi there!"
|
|
739
|
-
|
|
740
|
-
Ractor.new(map) { it[:content] << " How are you doing?" }.join
|
|
741
|
-
|
|
742
|
-
# Hi there! How are you doing?
|
|
743
|
-
puts map[:content]
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
#### Envelopes
|
|
747
|
-
|
|
748
|
-
Farce allows you to explicitly wrap an object in an envelope. This is useful if you want to pass a value around between multiple Ractors (or multiple times between the same Ractors) without copying, wrapping, converting, or moving it every single time.
|
|
749
|
-
|
|
750
|
-
Instead, you can wrap it in an envelope, and then later explicitly retrieve the value again.
|
|
84
|
+
Transfer modes control non-shareable values on native Ractors. For example, `:copy` sends a snapshot while leaving the original usable:
|
|
751
85
|
|
|
752
86
|
```ruby
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
Ractor.new(envelope) do |envelope|
|
|
758
|
-
# we have a reference to envelope, but not a copy of its data
|
|
759
|
-
# this allows us to pass it on easily
|
|
760
|
-
queue = Farce::Queue.new(mode: :raise)
|
|
761
|
-
queue.push(envelope) # this is okay, the envelope is shareable
|
|
762
|
-
|
|
763
|
-
# We can pass the envelope by other means, like a queue!
|
|
764
|
-
Ractor.new(queue) do |queue|
|
|
765
|
-
message = queue.pop
|
|
766
|
-
# okay, let's get the copy, bypassing the outer ractor
|
|
767
|
-
copy_of_array = message.value
|
|
768
|
-
puts "Size of the array: #{copy_of_array.size}"
|
|
769
|
-
end
|
|
770
|
-
end
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
Envelopes support `copy`, `move`, `local`, as well as a dummy envelope for wrapping shareable objects.
|
|
774
|
-
|
|
775
|
-
#### Mutables
|
|
776
|
-
|
|
777
|
-
Mutables are special wrappers for Ruby objects that are Ractor-shareable when frozen but can be modified when not.
|
|
778
|
-
|
|
779
|
-
```ruby
|
|
780
|
-
mutable = Farce::Mutable.new("content")
|
|
781
|
-
Ractor.new(mutable) { it << " & additional content" }.join
|
|
782
|
-
mutable # => #<Farce::Mutable[String] "content & additional content">
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
They expose a mutable API by keeping a frozen snapshot of the wrapped object, atomically unfreezing, mutating, and freezing it whenever a method otherwise throws a `FrozenError`. This means non-mutating methods have almost no additional cost, and in contrast to [proxies](#proxies), method calls do not have to be dispatched across Ractors, even when modifying the object. However, mutating methods will copy the wrapped object's content. This may be fine, but could be expensive when used repeatedly on large objects.
|
|
786
|
-
|
|
787
|
-
#### Proxies
|
|
788
|
-
|
|
789
|
-
A proxy mimics the API of another object, executing method calls within the Ractor that created it. This is an easy way to have drop-in replacements for objects that cannot be shared across Ractors.
|
|
790
|
-
|
|
791
|
-
```ruby
|
|
792
|
-
# Mutable arrays aren't Ractor-shareable
|
|
793
|
-
array = []
|
|
794
|
-
proxied = []
|
|
795
|
-
proxy = Farce::Proxy.new(proxied)
|
|
796
|
-
|
|
797
|
-
Farce::Ractor.new(array, proxy) do |*list|
|
|
798
|
-
list.each { it << 42 }
|
|
799
|
-
end.join
|
|
800
|
-
|
|
801
|
-
# The array got copied instead of being modified in place
|
|
802
|
-
array # => []
|
|
803
|
-
|
|
804
|
-
# The proxy didn't get copied
|
|
805
|
-
proxied # => [42]
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
Use this for very large objects, where copying the object would be more expensive than the Ractor-coordination overhead, or objects that can not be moved or copied between Ractors.
|
|
809
|
-
|
|
810
|
-
The downside is a dispatch to another Ractor on every method call.
|
|
811
|
-
If copying on writes is an option, consider [`Farce::Mutable`](#mutables) instead.
|
|
812
|
-
If the object may be moved across Ractors, maybe a [`Farce::Lease`](#leases) is a better option.
|
|
813
|
-
|
|
814
|
-
There is an advanced [customization API](https://rkh.github.io/farce/main/Farce/Proxy.html), which allows fine-tuning and reducing overhead.
|
|
815
|
-
|
|
816
|
-
#### Mode Managers
|
|
817
|
-
|
|
818
|
-
You can use a mode manager if you want to support sharing modes for a custom object.
|
|
87
|
+
queue = Farce::Queue.new(mode: :copy)
|
|
88
|
+
job = { ids: [1, 2] }
|
|
89
|
+
queue.push(job)
|
|
90
|
+
job[:ids] << 3 # added after copying
|
|
819
91
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
```ruby
|
|
823
|
-
manager = Farce::ModeManager.new(mode: :move)
|
|
824
|
-
payload = ["inside a mutable array"]
|
|
825
|
-
Ractor.shareable?(payload) # => false
|
|
826
|
-
|
|
827
|
-
shareable = manager.wrap(payload) # => #<Farce::Envelope::Move>
|
|
828
|
-
Ractor.shareable?(shareable) # => true
|
|
829
|
-
|
|
830
|
-
payload = shareable.value
|
|
831
|
-
Ractor.shareable?(payload) # => false
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
#### Leases
|
|
835
|
-
|
|
836
|
-
You can think of a `Farce::Lease` as a reusable `move` envelope. Or [Haskell's MVar](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Concurrent-MVar.html) if that's more your jam.
|
|
837
|
-
|
|
838
|
-
You can store any object in it, including non-shareable ones (if they are movable). A Ractor can check objects out (at which point they get moved into that Ractor), work with them, and move them back into the lease container. The checkout call will block while another Ractor holds the lease, serializing any operations.
|
|
839
|
-
|
|
840
|
-
This is very useful for objects that can be moved but can't be copied, especially IO-based objects like database connections.
|
|
841
|
-
|
|
842
|
-
```ruby
|
|
843
|
-
file_lease = Farce::Lease.new { File.open("example.txt", "w") }
|
|
844
|
-
|
|
845
|
-
Ractor.new(file_lease) do |file_lease|
|
|
846
|
-
file_lease.checkout do |file|
|
|
847
|
-
file.puts "Written from another Ractor!"
|
|
848
|
-
end
|
|
849
|
-
end.join
|
|
850
|
-
|
|
851
|
-
# make sure we close the file
|
|
852
|
-
file_lease.checkout(&:close)
|
|
853
|
-
|
|
854
|
-
# dereference the lease so it can be garbage collected
|
|
855
|
-
file_lease = nil
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
However, this might not be enough for handling database connections, where you'd usually have a pool of connections. You can use `Farce::LeasePool` which will manage multiple objects, only blocking on `checkout` if all objects have been leased.
|
|
859
|
-
|
|
860
|
-
Moreover, it can generate these objects for you on demand (until a certain number has been reached).
|
|
861
|
-
|
|
862
|
-
```ruby
|
|
863
|
-
# db connections are created on demand if there are fewer than five
|
|
864
|
-
pool = Farce::LeasePool.new(max_size: 5) { DB.connect }
|
|
865
|
-
pool.size # => 0
|
|
866
|
-
pool.max_size # => 5
|
|
867
|
-
|
|
868
|
-
pool.checkout do |db|
|
|
869
|
-
# ... do something with db ...
|
|
92
|
+
Ractor.new(queue) do |queue|
|
|
93
|
+
queue.pop # => { ids: [1, 2] }
|
|
870
94
|
end
|
|
871
95
|
```
|
|
872
96
|
|
|
873
|
-
|
|
97
|
+
JRuby and TruffleRuby share ordinary values directly (`Farce::Ractor.shareable?` reflects this).
|
|
874
98
|
|
|
875
|
-
|
|
876
|
-
leases = Farce::LeaseMap.new { { primary: [], replica: [] } }
|
|
877
|
-
leases.checkout(:primary) { |items| items << :updated }
|
|
878
|
-
leases.checkout(:primary, &:dup) # => [:updated]
|
|
879
|
-
```
|
|
99
|
+
Other tools cover different ownership needs:
|
|
880
100
|
|
|
881
|
-
|
|
101
|
+
- **Envelopes** carry values through several Ractors without repeated transfers.
|
|
102
|
+
- **Mutables** synchronize edits to wrapped objects.
|
|
103
|
+
- **Proxies** run calls in the object's original Ractor.
|
|
104
|
+
- **Leases and lease pools** give callers exclusive access to reusable resources, such as connections.
|
|
882
105
|
|
|
883
|
-
|
|
884
|
-
leases = Farce::LeaseMap.new { { primary: [], replica: [] } }
|
|
885
|
-
|
|
886
|
-
leases.auto_lease do
|
|
887
|
-
leases[:primary] << :updated
|
|
888
|
-
leases[:replica] << :replicated
|
|
889
|
-
leases[:primary] << :verified # Reuses the same checkout
|
|
890
|
-
end
|
|
891
|
-
|
|
892
|
-
# Both resources are checked back in when the block exits, even on an exception.
|
|
893
|
-
leases.available?(:primary) # => true
|
|
894
|
-
leases.checkout(:primary, &:dup) # => [:updated, :verified]
|
|
895
|
-
```
|
|
106
|
+
→ [Transfer modes, wrappers, and resource checkout](docs/sharing.md).
|
|
896
107
|
|
|
897
108
|
### Variants and Scopes
|
|
898
109
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
`Farce::Map`, `Farce::Strict::Map`, `Farce::Local::Map`, and `Farce::Unshared::Map` all support the same API, except that:
|
|
902
|
-
|
|
903
|
-
* `Farce::Map` accepts an optional `mode` keyword for its initializer and many methods.
|
|
904
|
-
* `Farce::Strict::Map` will not allow any non-shareable data to be stored in it. It is slightly faster than `Farce::Map`.
|
|
905
|
-
* `Farce::Local::Map` will accept an optional `scope` keyword for its initializer, and will have different content for each [scope](#provided-scopes).
|
|
906
|
-
* `Farce::Unshared::Map` cannot be shared across Ractors, but in turn can store any Ruby object directly, including unshareable objects. It is as fast as `Farce::Strict::Map`.
|
|
907
|
-
|
|
908
|
-
#### Available Variants
|
|
909
|
-
|
|
910
|
-
> [!TIP]
|
|
911
|
-
> Learn more in the dedicated [variants documentation](docs/variants.md).
|
|
912
|
-
|
|
913
|
-
These variants are provided by Farce:
|
|
914
|
-
|
|
915
|
-
1. **Default** variants, under the `Farce` namespace:
|
|
916
|
-
* Can **store unshareable values**, usually via [modes](#sharing-modes).
|
|
917
|
-
For maps, this is only supported for values. Keys must be shareable.
|
|
918
|
-
* Instances are **Ractor-shareable**.
|
|
919
|
-
* The data structure is **thread-safe**.
|
|
920
|
-
2. **Strict** variants, under the `Farce::Strict` namespace:
|
|
921
|
-
* **Forbid unshareable values**
|
|
922
|
-
* Instances are **Ractor-shareable**.
|
|
923
|
-
* The data structure is **thread-safe**.
|
|
924
|
-
* May provide performance benefits over the default variant.
|
|
925
|
-
3. **Local** variants, under the `Farce::Local` namespace:
|
|
926
|
-
* Will have **different content** for each [scope](#provided-scopes).
|
|
927
|
-
* Can **store unshareable values**, including unshareable map keys.
|
|
928
|
-
* Instances are **Ractor-shareable**.
|
|
929
|
-
* The data structure is **thread-safe**.
|
|
930
|
-
4. **Unshared** variants, under `Farce::Unshared` namespace:
|
|
931
|
-
* Can **store unshareable values**, including unshareable map keys.
|
|
932
|
-
* Instances are **<u>not</u> Ractor-shareable**.
|
|
933
|
-
* The data structure is **thread-safe**.
|
|
934
|
-
* May provide performance benefits over the default variant.
|
|
935
|
-
5. **Unsafe** variants, under `Farce::Unsafe` namespace:
|
|
936
|
-
* Same as Unshared, except they do **<u>not</u> guarantee thread-safety**
|
|
937
|
-
* May provide performance benefits over all other variants.
|
|
938
|
-
6. **Transaction** variants, under `Farce::Transaction` namespace:
|
|
939
|
-
* Created as mirrors of another object within a [transaction](#transactions).
|
|
940
|
-
* Should not be shared across transaction boundaries.
|
|
941
|
-
* Should not be initialized directly.
|
|
942
|
-
|
|
943
|
-
The following is true for all variants:
|
|
944
|
-
* Blocking operations will suspend a non-blocking fiber, but not block the underlying scheduler.
|
|
945
|
-
* The return value of `frozen?` will correctly reflect whether an instance is mutable or not.
|
|
946
|
-
|
|
947
|
-
However, keep the following in mind:
|
|
948
|
-
* Not all classes implement all variants. Check out the [full list](docs/variants.md#classes-implementing-variants)
|
|
949
|
-
* Other classes that don't implement variants are also nested under the `Farce` namespace.
|
|
950
|
-
|
|
951
|
-
#### Provided scopes
|
|
952
|
-
|
|
953
|
-
> [!TIP]
|
|
954
|
-
> Learn more in the dedicated [scopes documentation](docs/scopes.md).
|
|
955
|
-
|
|
956
|
-
Local variants support scopes:
|
|
957
|
-
|
|
958
|
-
```ruby
|
|
959
|
-
map = Farce::Map.new(scope: :thread)
|
|
960
|
-
map[:id] = 1
|
|
961
|
-
|
|
962
|
-
Thread.new do
|
|
963
|
-
map[:id] = 2
|
|
964
|
-
map[:id] # => 2
|
|
965
|
-
end.join
|
|
966
|
-
|
|
967
|
-
map[:id] # => 1
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
The following scopes are available:
|
|
971
|
-
|
|
972
|
-
| Scope | Description |
|
|
973
|
-
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
974
|
-
| `ractor` (default) | Content varies by [Ractor](https://docs.ruby-lang.org/en/4.0/Ractor.html) |
|
|
975
|
-
| `thread_group` | Content varies by [ThreadGroup](https://docs.ruby-lang.org/en/4.0/ThreadGroup.html) |
|
|
976
|
-
| `thread` | Content varies by [Thread](https://docs.ruby-lang.org/en/4.0/Thread.html) |
|
|
977
|
-
| `fiber_storage` | Content varies by [Fiber storage](https://docs.ruby-lang.org/en/4.0/Fiber.html#method-i-storage) |
|
|
978
|
-
| `fiber` | Content varies by [Fiber](https://docs.ruby-lang.org/en/4.0/Fiber.html) |
|
|
979
|
-
|
|
980
|
-
Fiber storage is typically inherited by a blocking fiber from the fiber creating it.
|
|
981
|
-
|
|
982
|
-
### Concurrency
|
|
983
|
-
|
|
984
|
-
Farce is built for concurrent and parallel code execution.
|
|
985
|
-
|
|
986
|
-
#### Locks
|
|
987
|
-
|
|
988
|
-
Farce ships with two lock classes:
|
|
989
|
-
* `Lock` is a drop-in replacement for Ruby's `Mutex`.
|
|
990
|
-
* `ReadWriteLock` exposes `with_read_lock`/`with_write_lock` to allow multiple concurrent reads, but exclusive write access.
|
|
991
|
-
|
|
992
|
-
However, Farce exposes many APIs to eliminate the need for locks altogether.
|
|
110
|
+
Choose where state lives and what it can contain:
|
|
993
111
|
|
|
994
|
-
|
|
112
|
+
| Variant | Use it when… |
|
|
113
|
+
|-------------------|---------------------------------------------------------------------------------------|
|
|
114
|
+
| `Farce` | Workers across Ractors need shared state, including values handled by transfer modes. |
|
|
115
|
+
| `Farce::Strict` | All stored values must already be shareable. |
|
|
116
|
+
| `Farce::Unshared` | Threads in one Ractor need to store arbitrary Ruby objects directly. |
|
|
117
|
+
| `Farce::Local` | Each Ractor, thread, or fiber needs its own contents behind one handle. |
|
|
995
118
|
|
|
996
|
-
|
|
119
|
+
For example, keep request context separate for each fiber:
|
|
997
120
|
|
|
998
121
|
```ruby
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
#
|
|
1003
|
-
map.store_if_absent(:key) { "initial value" }
|
|
1004
|
-
|
|
1005
|
-
# Atomically update :key
|
|
1006
|
-
map.update(:key, &:upcase)
|
|
1007
|
-
|
|
1008
|
-
# An Array-like object
|
|
1009
|
-
list = Farce::Vector.new
|
|
1010
|
-
list[0] = 42
|
|
1011
|
-
|
|
1012
|
-
# Atomically replace list[0] with 256 if the value is still 42
|
|
1013
|
-
list.compare_and_set(0, 42, 256)
|
|
122
|
+
context = Farce::Local::Map.new(scope: :fiber)
|
|
123
|
+
context[:request_id] = :parent
|
|
124
|
+
Fiber.new { context[:request_id] = :child }.resume
|
|
125
|
+
context[:request_id] # => :parent
|
|
1014
126
|
```
|
|
1015
127
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
All the [data structures](#data-structures) come with extra observability methods, which eliminate the need for using [condition variables](https://docs.ruby-lang.org/en/4.0/Thread/ConditionVariable.html) and [mutexes](https://docs.ruby-lang.org/en/4.0/Thread/Mutex.html).
|
|
1019
|
-
|
|
1020
|
-
Even setting aside that these don't work across ractors, this drastically reduces the risk of race conditions (very easy to do if you <u>don't</u> use the same mutex everywhere) or blocking code you don't need to block (very easy to do if you <u>do</u> use the same mutex everywhere).
|
|
128
|
+
Available variants depend on the class. → [All variants and scopes](docs/variants-and-scopes.md).
|
|
1021
129
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
```ruby
|
|
1025
|
-
# block until the stored value for :key is greater than 10
|
|
1026
|
-
map.wait_until(:key) { it > 10 }
|
|
1027
|
-
|
|
1028
|
-
# block until the value for an atom is no longer :initial
|
|
1029
|
-
map.wait_until_changed(:initial)
|
|
1030
|
-
|
|
1031
|
-
# block until a counter has reached at least 20
|
|
1032
|
-
counter.wait_while_below(20)
|
|
1033
|
-
```
|
|
130
|
+
### Concurrency
|
|
1034
131
|
|
|
1035
|
-
|
|
132
|
+
- **Atomic operations**, such as `upsert` above, combine reads and writes.
|
|
133
|
+
- **Wait methods and signals** wait for state changes without managing condition variables.
|
|
134
|
+
- **Transactions** publish coupled changes together. Locks are available too.
|
|
1036
135
|
|
|
1037
|
-
|
|
136
|
+
Use a flag to wait for an event:
|
|
1038
137
|
|
|
1039
138
|
```ruby
|
|
1040
|
-
|
|
1041
|
-
target = 100
|
|
1042
|
-
counter = Farce::Counter.new
|
|
139
|
+
flag = Farce::Flag.new
|
|
1043
140
|
|
|
1044
|
-
# A Thread that keeps reducing the target value every 20 milliseconds
|
|
1045
141
|
Thread.new do
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
target -= 1
|
|
1049
|
-
signal.broadcast # notify everyone else
|
|
1050
|
-
end
|
|
142
|
+
lots_of_work_here
|
|
143
|
+
flag.set
|
|
1051
144
|
end
|
|
1052
145
|
|
|
1053
|
-
|
|
1054
|
-
Ractor.new(counter, signal) do |counter, signal|
|
|
1055
|
-
while counter < 100
|
|
1056
|
-
sleep 0.01
|
|
1057
|
-
counter.increment
|
|
1058
|
-
signal.broadcast # notify everyone else
|
|
1059
|
-
end
|
|
1060
|
-
end
|
|
1061
|
-
|
|
1062
|
-
# wait until the counter is at or above the target
|
|
1063
|
-
signal.wait_until { counter >= target }
|
|
146
|
+
flag.wait_until_set
|
|
1064
147
|
```
|
|
1065
148
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
This all sounds great. But you still might want to reach for a lock if you want to modify more than one data structure, if their state is tightly coupled (i.e., updating one without yet updating the other would leave your code in an invalid state).
|
|
1069
|
-
|
|
1070
|
-
And a lock is an acceptable solution here. Again, you might want to wrap all the read access in a lock, as there will still be an invalid state. And you also want to roll back any changes already made while holding the lock, if an exception occurs. That is the standard approach in a lot of Ruby code.
|
|
1071
|
-
|
|
1072
|
-
Farce offers an alternative. It implements [STM-style transactions](https://en.wikipedia.org/wiki/Software_transactional_memory). You supply a block of code that makes changes to multiple data structures. These changes are only written to these data structures in a commit phase after the block finishes, and they either all succeed or all fail, and they only succeed if the values you've read from any of these data structures haven't changed.
|
|
1073
|
-
|
|
1074
|
-
Otherwise the block is rerun.
|
|
1075
|
-
|
|
1076
|
-
And this isn't limited to special `TVar` containers, like the APIs provided by concurrent-ruby or ractor-sharing. It supports Farce's main data structures, including [maps](#maps), [vectors](#vectors), [sets](#sets), [atoms](#atoms), and [molecules](#molecules). Moreover, it also supports [mutables](#mutables), meaning you can turn most Ruby objects into something transaction compatible fairly easily!
|
|
1077
|
-
|
|
1078
|
-
In contrast to other implementations mentioned above, there is no implicit tracking. You need to explicitly add an object to a transaction. This also avoids any uncertainty around nested transactions and unexpected rollbacks.
|
|
149
|
+
Transfer funds without exposing a partially updated balance:
|
|
1079
150
|
|
|
1080
151
|
```ruby
|
|
1081
|
-
accounts = Farce::Map.new({
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
accounts[:a] += 80
|
|
1087
|
-
accounts[:b] -= 80
|
|
1088
|
-
end
|
|
1089
|
-
|
|
1090
|
-
if success
|
|
1091
|
-
puts "Transaction succeeded"
|
|
1092
|
-
else
|
|
1093
|
-
puts "Transaction failed"
|
|
152
|
+
accounts = Farce::Map.new({ checking: 100, savings: 0 })
|
|
153
|
+
Farce.transaction(accounts) do |tx, balances|
|
|
154
|
+
tx.abort! if balances[:checking] < 20
|
|
155
|
+
balances[:checking] -= 20
|
|
156
|
+
balances[:savings] += 20
|
|
1094
157
|
end
|
|
158
|
+
accounts.to_h # => { checking: 80, savings: 20 }
|
|
1095
159
|
```
|
|
1096
160
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
##### Maps and Sets
|
|
1100
|
-
|
|
1101
|
-
Maps and sets use fine-grained transaction tracking. In the above example, if accounts had another entry, `:c`, its value changing would not impact the transaction at all.
|
|
161
|
+
Transaction blocks may rerun after a conflict. Keep external side effects outside them.
|
|
1102
162
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
This is ideal for scenarios where you use maps as a general data store and sets to track whether an operation was performed on an object (a common pattern to prevent infinite recursion).
|
|
1106
|
-
|
|
1107
|
-
##### TVars
|
|
1108
|
-
|
|
1109
|
-
Farce doesn't come with a `TVar` class. You can just use an [atom](#atoms) instead.
|
|
1110
|
-
|
|
1111
|
-
But it does come with built-in support for [`Concurrent::TVar`](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/TVar.html) and [`Ractor::TVar`](https://github.com/ko1/ractor-sharing/blob/main/docs/tvar.md) (assuming you also load these libraries, see [integrations](#integrations)).
|
|
1112
|
-
|
|
1113
|
-
```ruby
|
|
1114
|
-
tvar = Concurrent::TVar.new(50)
|
|
1115
|
-
map = Farce::Map.new({a: 100, b: 200})
|
|
1116
|
-
|
|
1117
|
-
Farce.transaction(map, tvar) do |map, tvar|
|
|
1118
|
-
# subtract tvar's value from a and b, then set tvar to 0
|
|
1119
|
-
# no invalid in-between state is ever visible to anything outside of this transaction
|
|
1120
|
-
map[:a] -= tvar.value
|
|
1121
|
-
map[:b] -= tvar.value
|
|
1122
|
-
tvar.value = 0
|
|
1123
|
-
end
|
|
1124
|
-
```
|
|
163
|
+
→ [Atomic updates, waiting, locks, and transactions](docs/concurrency.md).
|
|
1125
164
|
|
|
1126
165
|
### Scheduling Code
|
|
1127
166
|
|
|
1128
|
-
Farce
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
Some code has to be executed on the main Ractor, especially when working with legacy code that doesn't support Ractors.
|
|
1133
|
-
|
|
1134
|
-
You can pass a block to `Farce.on_main` to run code on the main Ractor. The method call will block until the code has been executed.
|
|
1135
|
-
|
|
1136
|
-
```ruby
|
|
1137
|
-
# most of concurrent-ruby is not usable outside of the main-ractor
|
|
1138
|
-
$tvar = Concurrent::TVar
|
|
1139
|
-
|
|
1140
|
-
# let's run computation outside of the main ractor
|
|
1141
|
-
Ractor.new do
|
|
1142
|
-
value = compute_expensive_value
|
|
1143
|
-
|
|
1144
|
-
# need to report back to the main ractor
|
|
1145
|
-
Farce.on_main(value) { $tvar.value = it }
|
|
1146
|
-
end
|
|
1147
|
-
```
|
|
1148
|
-
|
|
1149
|
-
Sometimes you don't need to wait for the code to be done running on the main Ractor. In such cases, you can use `schedule`, as `on_main` returns a [scheduler](#schedulers) instance when called without a block:
|
|
1150
|
-
|
|
1151
|
-
```ruby
|
|
1152
|
-
Ractor.new do
|
|
1153
|
-
Farce.on_main.schedule do
|
|
1154
|
-
sleep 1
|
|
1155
|
-
puts "Hello from the main Ractor"
|
|
1156
|
-
end
|
|
1157
|
-
puts "Hello from the nested Ractor"
|
|
1158
|
-
end
|
|
1159
|
-
|
|
1160
|
-
sleep 1.1
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
#### `Farce.in_parallel`
|
|
1164
|
-
|
|
1165
|
-
As you can see in the [very first example](#farce-fiber-and-ractor-compatibility-enabler), creating Ractors ad hoc because you want to run something in parallel is quite expensive.
|
|
1166
|
-
|
|
1167
|
-
Farce offers `Farce.in_parallel` instead, which will manage a [Ractor Pool](#ractor-pools), starting new ractors if the current ones cannot keep up with the current load (but at most as many as the system's CPU cores). It also shuts unused Ractors down again after some inactivity.
|
|
1168
|
-
|
|
1169
|
-
```ruby
|
|
1170
|
-
input = "this is my input"
|
|
1171
|
-
Farce.in_parallel(input) { expensive_computation(it) }
|
|
1172
|
-
```
|
|
1173
|
-
|
|
1174
|
-
Like most of Farce, this is completely opt-in. If you never use this feature, no pool is being set up and no extra Ractors are created.
|
|
1175
|
-
|
|
1176
|
-
#### `Farce.schedule`
|
|
1177
|
-
|
|
1178
|
-
`Farce.in_parallel` might not always be what you want. Maybe you are working on a library and don't want to dictate a concurrency model?
|
|
1179
|
-
|
|
1180
|
-
You can use `Farce.schedule`, which will automatically figure out how to run code off-band. You can use `mode: local` to force execution inside the current Ractor (which it will prefer by default if there already is some scheduler running, but not enforce).
|
|
1181
|
-
|
|
1182
|
-
```ruby
|
|
1183
|
-
# just run this asynchronously, don't care how
|
|
1184
|
-
Farce.schedule("hello") { |message| puts message.upcase }
|
|
1185
|
-
```
|
|
1186
|
-
|
|
1187
|
-
"Some scheduler?" you might say? Some scheduler! It will automatically detect if there is a local scheduler. This can either be a Fiber scheduler, like [async](https://socketry.github.io/async/) or [Carbon Fiber](https://yaroslav.io/opensource/carbon_fiber), or an internal scheduler created by Farce:
|
|
1188
|
-
|
|
1189
|
-
```ruby
|
|
1190
|
-
Async do
|
|
1191
|
-
# this is basically the same as calling Async { do_something }
|
|
1192
|
-
Farce.schedule { do_something }
|
|
1193
|
-
end
|
|
1194
|
-
```
|
|
1195
|
-
|
|
1196
|
-
#### Schedulers
|
|
1197
|
-
|
|
1198
|
-
You can create your own `Farce::Scheduler`, running on a dedicated Ractor:
|
|
1199
|
-
|
|
1200
|
-
```ruby
|
|
1201
|
-
scheduler = Farce::Scheduler.create
|
|
1202
|
-
scheduler.schedule("hello") { |message| puts message }
|
|
1203
|
-
scheduler.close # we're done, shut it down
|
|
1204
|
-
```
|
|
1205
|
-
|
|
1206
|
-
Or you can set one up as a Fiber scheduler for the current Thread:
|
|
1207
|
-
|
|
1208
|
-
```ruby
|
|
1209
|
-
scheduler = Farce::Scheduler.new
|
|
1210
|
-
Fiber.set_scheduler(scheduler)
|
|
1211
|
-
Fiber.schedule { puts "Hello from the scheduler!" }
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
Scheduler instances are Ractor-shareable.
|
|
1215
|
-
|
|
1216
|
-
#### Ractor Pools
|
|
1217
|
-
|
|
1218
|
-
`Farce::Pool` implements the same scheduling interface, but manages a pool of Ractors. This is what [`in_parallel`](#farcein_parallel) uses under the hood.
|
|
1219
|
-
|
|
1220
|
-
```ruby
|
|
1221
|
-
# At least two Ractors, up to four. Launch a new one if a task waits longer than 100 milliseconds.
|
|
1222
|
-
pool = Farce::Pool.new(min_size: 4, max_size: 10, grow_after: 0.1)
|
|
1223
|
-
pool.schedule { puts "Hello from the pool!" }
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
#### Tasks
|
|
1227
|
-
|
|
1228
|
-
If you want to retrieve the result of an asynchronous operation, or monitor if it is still running, you can use `Farce::Task`:
|
|
1229
|
-
|
|
1230
|
-
```ruby
|
|
1231
|
-
task = Farce::Task.schedule(1, 2) do |a, b|
|
|
1232
|
-
sleep 1 # simulate intense computation
|
|
1233
|
-
a + b
|
|
1234
|
-
end
|
|
1235
|
-
|
|
1236
|
-
task.alive? # => true
|
|
1237
|
-
task.value # => 3 (after approximately 1 second)
|
|
1238
|
-
task.join # would block, but doesn't, as the task has already completed
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
Tasks can also be created without immediately scheduling them, allowing you to control when and how they are executed:
|
|
1242
|
-
|
|
1243
|
-
```ruby
|
|
1244
|
-
task = Farce::Task.new { :success }
|
|
1245
|
-
task.alive? # => true
|
|
1246
|
-
task.value(0.1) # => nil (after 100 milliseconds)
|
|
1247
|
-
|
|
1248
|
-
Farce.schedule(&task)
|
|
1249
|
-
task.value # => :success
|
|
1250
|
-
```
|
|
1251
|
-
|
|
1252
|
-
This allows you to use third-party executors:
|
|
1253
|
-
|
|
1254
|
-
```ruby
|
|
1255
|
-
require "concurrent"
|
|
1256
|
-
executor = Concurrent.global_fast_executor
|
|
1257
|
-
task = Farce::Task.new { :success }
|
|
1258
|
-
executor.post(&task)
|
|
1259
|
-
|
|
1260
|
-
task.value # => :success
|
|
1261
|
-
```
|
|
1262
|
-
|
|
1263
|
-
#### Third-Party Fiber Schedulers
|
|
167
|
+
- **`Farce.schedule`** runs background work, using an available local scheduler when possible.
|
|
168
|
+
- **`Farce.on_main`** dispatches calls to the main Ractor.
|
|
169
|
+
- **`Farce.in_parallel`** schedules parallel work, reusing a Ractor pool on CRuby.
|
|
1264
170
|
|
|
1265
|
-
|
|
171
|
+
Use a task when you need the result:
|
|
1266
172
|
|
|
1267
173
|
```ruby
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
pool.schedule { puts "⚡️ Running Carbon Fiber on a pool of Ractors! ⚡️" }
|
|
174
|
+
task = Farce::Task.in_parallel(21) { |number| number * 2 }
|
|
175
|
+
task.value # => 42, waits for completion
|
|
1271
176
|
```
|
|
1272
177
|
|
|
1273
|
-
You can also
|
|
178
|
+
You can also create your own schedulers and pools, or use third-party Fiber schedulers.
|
|
1274
179
|
|
|
1275
|
-
|
|
1276
|
-
# This needs to happen before the first scheduler call.
|
|
1277
|
-
Farce.configure do |config|
|
|
1278
|
-
config.fiber_scheduler = :carbon_fiber
|
|
1279
|
-
|
|
1280
|
-
# or, alternatively:
|
|
1281
|
-
config.fiber_scheduler { CarbonFiber::Scheduler.new }
|
|
1282
|
-
end
|
|
1283
|
-
|
|
1284
|
-
Farce.in_parallel do
|
|
1285
|
-
Fiber.scheduler.class # => CarbonFiber::Scheduler
|
|
1286
|
-
end
|
|
1287
|
-
```
|
|
1288
|
-
|
|
1289
|
-
If a string or symbol is provided (like `carbon_fiber`), it will first resolve this to a constant (i.e., `CarbonFiber`). If that constant is a class, it will use that for creating the fiber scheduler. If it is a module, it will look for a `Scheduler` constant inside it, which matches the established pattern implemented by most gems, including `farce`, `async`, `carbon_fiber`, `libev_scheduler` (use `libev` as value), `itsi_scheduler` (use `itsi` as value), but it also works for gems that define their scheduler class at top level, like `fiber_scheduler`.
|
|
1290
|
-
|
|
1291
|
-
Note that most of these are outdated and don't work with any recent Ruby version, with the notable exception of `async` and `carbon_fiber`.
|
|
180
|
+
→ [Tasks, execution choices, and scheduler lifecycle](docs/scheduling.md).
|
|
1292
181
|
|
|
1293
182
|
### Integrations
|
|
1294
183
|
|
|
1295
|
-
Farce
|
|
1296
|
-
|
|
1297
|
-
#### Active Support
|
|
1298
|
-
|
|
1299
|
-
The Active Support integration adds the following methods:
|
|
1300
|
-
|
|
1301
|
-
* For all data structures: `as_json`, `blank?`, `deep_dup`, and `duplicable?`
|
|
1302
|
-
* For maps: `assert_valid_keys`, `compact_blank`, `reverse_merge`, `stringify_keys`, `symbolize_keys`, `to_param`, `to_query`, `with_defaults`, and `with_indifferent_access`
|
|
1303
|
-
* For vectors: `compact_blank`, `excluding`, `from`, `including`, `inquiry`, `in_groups`, `in_groups_of`, `in_order_of`, `maximum`, `minimum`, `pluck`, `pick`, `split`, `to`, `to_fs`, `to_param`, `to_sentence`, `to_query`, `to_xml`, `second`, `third`, `fourth`, `fifth`, `forty_two`, `third_to_last`, and `second_to_last`
|
|
184
|
+
Use Farce collections with existing libraries:
|
|
1304
185
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
Other methods are already being inherited by various objects via `Object`, `Enumerable` for vectors, sets, and maps, `Numeric` for counters, etc.
|
|
1310
|
-
|
|
1311
|
-
```ruby
|
|
1312
|
-
require "active_support/all"
|
|
1313
|
-
require "farce"
|
|
1314
|
-
|
|
1315
|
-
map = Farce::Map.new.with_indifferent_access
|
|
1316
|
-
map[:a] = 10
|
|
1317
|
-
|
|
1318
|
-
map.blank? # => false
|
|
1319
|
-
map["a"] # => 10
|
|
1320
|
-
```
|
|
1321
|
-
|
|
1322
|
-
The integration isn't triggered by loading `active_support`, but instead looks for `active_support/core_ext`, so you should require that (or `active_support/all`, which in turn requires it).
|
|
1323
|
-
|
|
1324
|
-
#### Dry Types
|
|
1325
|
-
|
|
1326
|
-
Adds support for Farce [data structures](#data-structures) to [Dry Types](https://hanakai.org/learn/dry/dry-types):
|
|
1327
|
-
|
|
1328
|
-
```ruby
|
|
1329
|
-
require "dry-types"
|
|
1330
|
-
require "farce"
|
|
1331
|
-
|
|
1332
|
-
module Types
|
|
1333
|
-
include Dry.Types()
|
|
1334
|
-
include Farce.DryTypes()
|
|
1335
|
-
|
|
1336
|
-
IntegerVector = Vector.of(Coercible::Integer)
|
|
1337
|
-
end
|
|
1338
|
-
|
|
1339
|
-
numbers = Types::IntegerVector[["1", 2]]
|
|
1340
|
-
numbers.class # => Farce::Vector
|
|
1341
|
-
numbers.to_a # => [1, 2]
|
|
1342
|
-
```
|
|
1343
|
-
|
|
1344
|
-
Check out the [dedicated documentation](docs/gems/dry-types.md) to learn more.
|
|
1345
|
-
|
|
1346
|
-
#### JSON, YAML, etc
|
|
1347
|
-
|
|
1348
|
-
It ships integrations for serialization (and some deserialization) with the following gems:
|
|
1349
|
-
|
|
1350
|
-
* For BSON support: `bson`
|
|
1351
|
-
* For CBOR support: `cbor`
|
|
1352
|
-
* For JSON support: `json` (from the Ruby standard library), `oj`, and `yajl`
|
|
1353
|
-
* For MessagePack: `msgpack` – see the [detailed documentation](docs/gems/msgpack.md)
|
|
1354
|
-
* For YAML: `psych` (from the Ruby standard library)
|
|
1355
|
-
|
|
1356
|
-
Example:
|
|
186
|
+
- **Active Support** adds helpers such as indifferent key access.
|
|
187
|
+
- **Dry Types** validates and coerces Farce collections.
|
|
188
|
+
- **JSON, YAML, MessagePack, BSON, and CBOR** serialize them.
|
|
189
|
+
- **TVar adapters** bring existing transactional values into Farce transactions.
|
|
1357
190
|
|
|
1358
191
|
```ruby
|
|
1359
192
|
require "json"
|
|
1360
193
|
require "farce"
|
|
1361
194
|
|
|
1362
|
-
map = Farce::Map.new
|
|
1363
|
-
map
|
|
1364
|
-
map.to_json # => '{"x":[1,2,3]}'
|
|
1365
|
-
```
|
|
1366
|
-
|
|
1367
|
-
#### Concurrent Ruby
|
|
1368
|
-
|
|
1369
|
-
The integration adds the following features if [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) has been loaded:
|
|
1370
|
-
|
|
1371
|
-
* [Transaction support for `Concurrent::TVar`](#tvars)
|
|
1372
|
-
* Automatic conversion of `Concurrent::Map` instances to [maps](#maps) via [`Farce.enfarce`](#top-level-methods).
|
|
1373
|
-
|
|
1374
|
-
#### Ractor Sharing
|
|
1375
|
-
|
|
1376
|
-
The integration adds the following features if [ractor-sharing](https://github.com/ko1/ractor-sharing) has been loaded:
|
|
1377
|
-
|
|
1378
|
-
* [Transaction support for `Ractor::TVar`](#tvars)
|
|
1379
|
-
* Support for traversing and deep freezing `Ractor::TVar`, `Ractor::LockVar`, `Ractor::LockHash`, and `Ractor::KeyLockHash`
|
|
1380
|
-
* Support for converting `Ractor::LockHash` and `Ractor::KeyLockHash` to [maps](#maps) via [`Farce.enfarce`](#top-level-methods).
|
|
1381
|
-
|
|
1382
|
-
#### Additional Integrations
|
|
1383
|
-
|
|
1384
|
-
* [`sorted_set`](https://github.com/knu/sorted_set): Add support for walking and converting them.
|
|
1385
|
-
* [`ractor-tmvar`](https://github.com/yoshitsugu/ractor-tmvar): Adds transaction support.
|
|
1386
|
-
* [`weakref`](https://github.com/ruby/weakref): Add support for walking and converting them.
|
|
1387
|
-
|
|
1388
|
-
#### Disable Automatic loading
|
|
1389
|
-
|
|
1390
|
-
You can set the environment variable `FARCE_AUTOLOAD_INTEGRATIONS` to `false` or `0` to disable automatic integration loading.
|
|
1391
|
-
|
|
1392
|
-
Or you can use `Farce.configure` to disable it. But you may have to do so before loading `farce` unless you're absolutely certain the other gem has not yet been loaded:
|
|
1393
|
-
|
|
1394
|
-
```ruby
|
|
1395
|
-
# This could go in an initializer
|
|
1396
|
-
require "farce/config"
|
|
1397
|
-
|
|
1398
|
-
Farce.configure do |config|
|
|
1399
|
-
config.autoload_integrations = false
|
|
1400
|
-
end
|
|
1401
|
-
|
|
1402
|
-
require "farce"
|
|
1403
|
-
|
|
1404
|
-
# you now need to load any integrations you might want explicitly
|
|
1405
|
-
# these are available via "farce/integrations/#{gem_name}"
|
|
1406
|
-
require "farce/integrations/json"
|
|
1407
|
-
require "farce/integrations/concurrent"
|
|
195
|
+
map = Farce::Map.new({ ids: Farce::Vector[1, 2] })
|
|
196
|
+
map.to_json # => '{"ids":[1,2]}'
|
|
1408
197
|
```
|
|
1409
198
|
|
|
1410
|
-
|
|
199
|
+
Integrations load when both libraries have been required, in either order. Farce does not load the other library for you.
|
|
1411
200
|
|
|
1412
|
-
|
|
201
|
+
→ [Integration APIs and disabling automatic loading](docs/integrations.md).
|
|
1413
202
|
|
|
1414
|
-
|
|
1415
|
-
* `Farce.config` returns the global configuration object.
|
|
1416
|
-
* `Farce.configure` allows you to configure Farce.
|
|
1417
|
-
* `Farce.dedup` de-duplicates the given object based on a deduplication cache shared by all Ractors.
|
|
1418
|
-
* `Farce.enfarce` turns a vanilla data structure into its Farce equivalent.
|
|
1419
|
-
* `Farce.freeze_graph` recursively freezes an object graph.
|
|
1420
|
-
* `Farce.in_parallel`, `Farce.on_main`, and `Farce.schedule`, see [Scheduling Code](#scheduling-code)
|
|
1421
|
-
* `Farce.rebind` rebinds a proc or lambda while preserving its Ractor-shareability.
|
|
1422
|
-
* `Farce.transaction` creates and runs a [transaction](#transactions).
|
|
203
|
+
### Utilities and Custom Classes
|
|
1423
204
|
|
|
1424
|
-
|
|
205
|
+
Adapt existing data without rebuilding it by hand:
|
|
1425
206
|
|
|
1426
207
|
```ruby
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
# Farce.clock
|
|
1431
|
-
Farce.clock # => 0.017476999908685684
|
|
1432
|
-
Farce.clock(in: 10) # => 10.017520000003278
|
|
1433
|
-
|
|
1434
|
-
# Farce.dedup
|
|
1435
|
-
a.equal? b # => false
|
|
1436
|
-
Farce.dedup(a).equal? Farce.dedup(b) # => true
|
|
1437
|
-
|
|
1438
|
-
# Farce.enfarce
|
|
1439
|
-
Farce.enfarce(a) # => #<Farce::Map {a: #<Farce::Vector ["b"]>}>
|
|
1440
|
-
Farce::Strict.enfarce(a) # => #<Farce::Strict::Map {a: #<Farce::Strict::Vector ["b"]>}>
|
|
1441
|
-
|
|
1442
|
-
# Farce.freeze_graph
|
|
1443
|
-
Farce.freeze_graph(a)
|
|
1444
|
-
|
|
1445
|
-
# Farce.rebind
|
|
1446
|
-
callback = ->(add) { self + add }
|
|
1447
|
-
rebound = Farce.rebind(callback, self: 42)
|
|
1448
|
-
rebound.call(18) # => 50
|
|
208
|
+
config = Farce.enfarce({ retries: [1, 2, 3] })
|
|
209
|
+
config.class # => Farce::Map
|
|
210
|
+
config[:retries].class # => Farce::Vector
|
|
1449
211
|
```
|
|
1450
212
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
Other classes Farce provides include:
|
|
1454
|
-
* `Config`: Configuration class, see [Third-Party Fiber Schedulers](#third-party-fiber-schedulers) example.
|
|
1455
|
-
* `ClassMirror`: inheritance-aware registry for classes
|
|
1456
|
-
* `Clock`: Timing functions based on a monotonic clock rather than on `Time`.
|
|
1457
|
-
* `Deduper`: Create your own deduplication cache. Direct usage isn't recommended, use [`Farce.dedup`](#top-level-methods) instead.
|
|
1458
|
-
* `Exchanger`: A synchronization point for two-way data swapping between Threads, Ractors, and/or Fibers. Drop-in replacement for concurrent-ruby's exchanger.
|
|
1459
|
-
* `Lazy`: Lazily initialized value.
|
|
1460
|
-
* `LazyRef`: A [reference](#references) for a lazily initialized value.
|
|
1461
|
-
* `ThreadExecutor`: A [task executor](#scheduling-code) creating a new thread for each unit of work. Used on TruffleRuby.
|
|
1462
|
-
* `Walker`: A tool for walking a Ruby object tree.
|
|
1463
|
-
* `WeakValue`: A value object version of [`WeakRef`](#weakref). Allows handling references more explicitly, without automatic method delegation.
|
|
1464
|
-
|
|
1465
|
-
In addition, Farce includes a range of error classes not listed here. Check the [API documentation](https://rkh.github.io/farce/) or [code base](lib/farce/error.rb) for these.
|
|
1466
|
-
|
|
1467
|
-
#### Shareability Mixins
|
|
1468
|
-
|
|
1469
|
-
Farce includes mixins to help you make your custom classes shareable:
|
|
1470
|
-
|
|
1471
|
-
* `Shareable` automatically marks objects as shareable (via `Ractor.make_shareable`) after initialization.
|
|
1472
|
-
* `Shareable::Delegated` delegates `freeze` and `frozen?` to another object holding your object's state.
|
|
1473
|
-
* `Shareable::Immutable` instances are always immutable, being frozen after initialization.
|
|
1474
|
-
* `Shareable::Native` is for objects implemented in a native extension, which allows setting the frozen and shareable state separately.
|
|
1475
|
-
* `Shareable::Tracked` for objects implementing frozen tracking (via an internal [flag](#flags)).
|
|
1476
|
-
* `Shareable::Unfreezable` for objects that cannot be frozen (like [queues](#queues)).
|
|
1477
|
-
|
|
1478
|
-
And also mixins to prevent them from being shareable:
|
|
1479
|
-
|
|
1480
|
-
* `Unshareable`: Instances aren't shareable and cannot be made shareable. By default, they also cannot be copied or moved between Ractors.
|
|
1481
|
-
* `Unshareable::Copyable`: Instances aren't shareable but may be copied to another Ractor.
|
|
1482
|
-
* `Unshareable::Movable`: Instances aren't shareable but may be moved to another Ractor.
|
|
1483
|
-
|
|
1484
|
-
`Unshareable::Copyable` and `Unshareable::Movable` may be combined.
|
|
1485
|
-
|
|
1486
|
-
#### Constants
|
|
1487
|
-
|
|
1488
|
-
* `Farce::MODES`: List of supported [sharing modes](#sharing-modes).
|
|
1489
|
-
* `Farce::SCOPES`: List of [provided scopes](#provided-scopes).
|
|
1490
|
-
* `Farce::VERSION`: The current version.
|
|
1491
|
-
|
|
1492
|
-
## Compatibility and Dependencies
|
|
1493
|
-
|
|
1494
|
-
Farce has no mandatory dependencies beyond Ruby itself.
|
|
1495
|
-
|
|
1496
|
-
### Ruby
|
|
1497
|
-
|
|
1498
|
-
Each Farce release is expected to be compatible with:
|
|
1499
|
-
|
|
1500
|
-
* The [latest patch release](https://www.ruby-lang.org/en/downloads/releases/) for each [CRuby](https://www.ruby-lang.org/en/) version [still receiving bug fixes](https://www.ruby-lang.org/en/downloads/branches/).
|
|
1501
|
-
* Ruby's [master branch](https://github.com/ruby/ruby/tree/master) at the time of release (i.e., the upcoming major version of CRuby).
|
|
1502
|
-
* The latest stable release of [JRuby](https://www.jruby.org/) and [TruffleRuby](https://truffleruby.dev/) (both in native and GraalVM modes).
|
|
1503
|
-
|
|
1504
|
-
Moreover:
|
|
1505
|
-
|
|
1506
|
-
* Dropping support for a CRuby version is only done in major releases.
|
|
1507
|
-
* If support for an older CRuby version is dropped, Farce will still backport security fixes for at least as long as that CRuby version is [still receiving security fixes](https://www.ruby-lang.org/en/downloads/branches/).
|
|
1508
|
-
|
|
1509
|
-
### Optional Dependencies
|
|
1510
|
-
|
|
1511
|
-
Farce's optional [integrations](#integrations) depend on external libraries. Automated tests only run against recent versions of these libraries, but the code is written to be compatible with as wide a range as possible.
|
|
1512
|
-
|
|
1513
|
-
Farce should be compatible with these versions:
|
|
1514
|
-
|
|
1515
|
-
| Gem | Minimum Version | Release Date |
|
|
1516
|
-
|-------------------|-----------------|--------------|
|
|
1517
|
-
| `ractor-tmvar` | 0.3.0 | 2026-10-06 |
|
|
1518
|
-
| `msgpack` | 1.7.0 | 2023-03-29 |
|
|
1519
|
-
| `concurrent-ruby` | 1.1.8 | 2021-01-20 |
|
|
1520
|
-
| `dry-types` | 1.0.0 | 2019-04-23 |
|
|
1521
|
-
| `activesupport` | 5.1.0 | 2017-04-27 |
|
|
1522
|
-
| `oj` | 2.8.1 | 2014-04-21 |
|
|
1523
|
-
| `bson` | 2.0.0 | 2013-12-02 |
|
|
1524
|
-
| `psych` | 1.1.0 | 2011-03-30 |
|
|
1525
|
-
|
|
1526
|
-
Any version of `cbor`, `json`, `ractor-sharing`, `sorted_set`, and `weakref` should be supported
|
|
1527
|
-
|
|
1528
|
-
If you want to use any of these in a version that is incompatible with Farce, you can [disable automatic integration loading](#disable-automatic-loading).
|
|
213
|
+
Other helpers freeze object graphs, deduplicate values, and measure monotonic time. Lazy values and exchangers coordinate initialization and handoffs. Shareability mixins define how custom classes can be frozen, shared, copied, or moved.
|
|
1529
214
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
* [ractor-shim](https://github.com/eregon/ractor-shim/) provides similar functionality to `Farce::Ractor`. See [the comparison document](docs/gems/ractor-shim.md) for more details.
|
|
1533
|
-
* [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) provides a more complete set of concurrency primitives than Farce, but is not compatible with Ractors.
|
|
1534
|
-
* [ratomic](https://mperham.github.io/ratomic/) has overlapping functionality with Farce for basic data structures like maps, counters, and queues.
|
|
1535
|
-
* [ractor_safe](https://github.com/jhawthorn/ractor_safe/) has overlapping functionality with Farce for basic data structures like maps, counters, and queues.
|
|
1536
|
-
* [ractor-sharing](https://github.com/ko1/ractor-sharing) has overlapping functionality with Farce for basic data structures like maps, counters, queues, as well as software transactional memory.
|
|
1537
|
-
|
|
1538
|
-
All of the above projects can safely be used alongside Farce in the same application.
|
|
215
|
+
→ [Conversion helpers and extension points](docs/utilities.md).
|
|
1539
216
|
|
|
1540
217
|
## Installation
|
|
1541
218
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
To install Farce globally, you can use the following command:
|
|
1545
|
-
|
|
1546
|
-
```console
|
|
1547
|
-
$ gem install farce
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
### As a project dependency
|
|
1551
|
-
|
|
1552
|
-
If you want to use Farce directly in your project, it is recommended to do so via [Bundler](https://bundler.io).
|
|
1553
|
-
Add Farce to your `Gemfile`:
|
|
219
|
+
Add to your application's `Gemfile`, then run `bundle install`:
|
|
1554
220
|
|
|
1555
221
|
```ruby
|
|
1556
|
-
source "https://gem.coop" # or "https://rubygems.org"
|
|
1557
|
-
|
|
1558
222
|
gem "farce"
|
|
1559
223
|
```
|
|
1560
224
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
### As a library dependency
|
|
1564
|
-
|
|
1565
|
-
Farce's main purpose is to be used as a dependency for other libraries. As such, it will most commonly be added as a [runtime dependency](https://guides.rubygems.org/specification-reference/#add_dependency) to your gemspec:
|
|
1566
|
-
|
|
1567
|
-
```ruby
|
|
1568
|
-
Gem::Specification.new do |spec|
|
|
1569
|
-
# ...
|
|
1570
|
-
spec.add_dependency "farce"
|
|
1571
|
-
end
|
|
1572
|
-
```
|
|
1573
|
-
|
|
1574
|
-
### Local setup
|
|
1575
|
-
|
|
1576
|
-
If you want to work on Farce itself, you can clone the repository and use [mise](https://mise.jdx.dev) to set everything up:
|
|
225
|
+
For a library, add `spec.add_dependency "farce"` to its gemspec. Load the public entry point with `require "farce"`. Classes load lazily.
|
|
1577
226
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
```console
|
|
1581
|
-
$ git clone https://github.com/rkh/farce.git # prefix with `jj` if you're using Jujutsu
|
|
1582
|
-
$ cd farce
|
|
1583
|
-
$ mise run
|
|
1584
|
-
```
|
|
227
|
+
→ [Global installation and local development setup](docs/installation.md).
|
|
1585
228
|
|
|
1586
|
-
|
|
1587
|
-
You should always require `farce`, rather than any other files in `lib`. Other files are not intended as entry points.
|
|
229
|
+
## Compatibility and Dependencies
|
|
1588
230
|
|
|
1589
|
-
|
|
1590
|
-
require "farce"
|
|
1591
|
-
```
|
|
231
|
+
Farce has **no mandatory dependencies beyond Ruby**. It supports maintained CRuby versions, the upcoming CRuby release, and current stable JRuby and TruffleRuby releases.
|
|
1592
232
|
|
|
1593
|
-
|
|
1594
|
-
need to specifically load any particular file.
|
|
233
|
+
→ [Support policy, optional gem versions, and related projects](docs/compatibility.md) · [Benchmarks](docs/benchmarks.md).
|
|
1595
234
|
|
|
1596
235
|
## Known Issues and Limitations
|
|
1597
236
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
> I agree that **freezing means the object's own state is immutable**, not just its instance variables, so we should not freeze [*shareable, mutable object*]. Forbidding instance variables on them is the right approach. [...] **A shareable object that is not frozen never has instance variables**. This should also hold when C extensions define such objects in the future.
|
|
1601
|
-
> — *Yukihiro Matsumoto* (Ruby Issue [#22291](https://bugs.ruby-lang.org/issues/22291#note-4), emphasis added)
|
|
1602
|
-
|
|
1603
|
-
In Ruby, Farce objects reflect their frozen state accurately. If a map returns `true` for `frozen?`, you cannot add, remove, or replace its entries.
|
|
1604
|
-
|
|
1605
|
-
There are some technical challenges implementing this behavior: From within Ruby, you cannot mark an object as Ractor shareable without freezing it first. This is possible from a C-extension, but then instance variables can no longer be used, so state tracking needs to happen purely at the C level or outside of the Ruby object.
|
|
237
|
+
Ruby's `freeze` and `frozen?` reflect Farce objects' mutability. Direct C-level frozen checks can report a different state for some objects.
|
|
1606
238
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
This means:
|
|
1610
|
-
|
|
1611
|
-
* `frozen?` and `freeze` will behave as expected for all Farce classes.
|
|
1612
|
-
* `Kernel.instance_method(:frozen?).bind_call(object)` might report a different value from `object.frozen?`
|
|
1613
|
-
* C-level checks for frozen state, such as `RB_OBJ_FROZEN`, might differ from what Ruby-level methods report.
|
|
239
|
+
→ [Frozen-state behavior for extension authors](docs/compatibility.md#known-issues-and-limitations).
|
|
1614
240
|
|
|
1615
241
|
## Housekeeping
|
|
1616
242
|
|
|
243
|
+
Want to contribute? Read the [contribution guide](CONTRIBUTING.md), [code of conduct](CODE_OF_CONDUCT.md), and [security policy](SECURITY.md).
|
|
244
|
+
|
|
1617
245
|
Farce follows [Semantic Versioning](https://semver.org/) and [the RubyGems versioning policy](https://guides.rubygems.org/patterns/#versioning). Farce's original code is released under the [MIT License](MIT-LICENSE). Native builds also contain Kazlib 1.20-derived `dict.c` and `dict.h`; their original permissive license and copyright notice are retained in those files. The native gem therefore declares `MIT` and `LicenseRef-Kazlib-1.20`, while the pure-Java gem declares only `MIT` because it does not ship Kazlib.
|
|
1618
246
|
|
|
1619
247
|
Built with love in Berlin, by Konstantin Haase.
|