farce 0.3.0-arm-linux-gnu → 0.4.0-arm-linux-gnu
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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Compatibility and Dependencies
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Compatibility and Dependencies
|
|
6
|
+
|
|
7
|
+
Farce has no mandatory dependencies beyond Ruby itself.
|
|
8
|
+
|
|
9
|
+
## Ruby
|
|
10
|
+
|
|
11
|
+
Each Farce release is expected to be compatible with:
|
|
12
|
+
|
|
13
|
+
* 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/).
|
|
14
|
+
* Ruby's [master branch](https://github.com/ruby/ruby/tree/master) at the time of release (i.e., the upcoming major version of CRuby).
|
|
15
|
+
* The latest stable release of [JRuby](https://www.jruby.org/) and [TruffleRuby](https://truffleruby.dev/) (both in native and GraalVM modes).
|
|
16
|
+
|
|
17
|
+
Moreover:
|
|
18
|
+
|
|
19
|
+
* Dropping support for a CRuby version is only done in major releases.
|
|
20
|
+
* 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/).
|
|
21
|
+
|
|
22
|
+
## Optional Dependencies
|
|
23
|
+
|
|
24
|
+
Farce's optional [integrations](integrations.md#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.
|
|
25
|
+
|
|
26
|
+
Farce should be compatible with these versions:
|
|
27
|
+
|
|
28
|
+
| Gem | Minimum Version | Release Date |
|
|
29
|
+
|-------------------|-----------------|--------------|
|
|
30
|
+
| `ractor-tmvar` | 0.4.0 | 2026-10-11 |
|
|
31
|
+
| `msgpack` | 1.7.0 | 2023-03-29 |
|
|
32
|
+
| `concurrent-ruby` | 1.1.8 | 2021-01-20 |
|
|
33
|
+
| `dry-types` | 1.0.0 | 2019-04-23 |
|
|
34
|
+
| `activesupport` | 5.1.0 | 2017-04-27 |
|
|
35
|
+
| `oj` | 2.8.1 | 2014-04-21 |
|
|
36
|
+
| `bson` | 2.0.0 | 2013-12-02 |
|
|
37
|
+
| `psych` | 1.1.0 | 2011-03-30 |
|
|
38
|
+
|
|
39
|
+
Any version of `cbor`, `json`, `ractor-sharing`, `sorted_set`, and `weakref` should be supported.
|
|
40
|
+
|
|
41
|
+
If you want to use any of these in a version that is incompatible with Farce, you can [disable automatic integration loading](integrations.md#disable-automatic-loading).
|
|
42
|
+
|
|
43
|
+
## Similar Projects
|
|
44
|
+
|
|
45
|
+
* [ractor-shim](https://github.com/eregon/ractor-shim/) provides similar functionality to `Farce::Ractor`. See [the comparison document](gems/ractor-shim.md) for more details.
|
|
46
|
+
* [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.
|
|
47
|
+
* [ratomic](https://mperham.github.io/ratomic/) has overlapping functionality with Farce for basic data structures like maps, counters, and queues.
|
|
48
|
+
* [ractor_safe](https://github.com/jhawthorn/ractor_safe/) has overlapping functionality with Farce for basic data structures like maps, counters, and queues.
|
|
49
|
+
* [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.
|
|
50
|
+
|
|
51
|
+
All of the above projects can safely be used alongside Farce in the same application.
|
|
52
|
+
|
|
53
|
+
## Known Issues and Limitations
|
|
54
|
+
|
|
55
|
+
### Possible discrepancy regarding frozen state in Ruby and C
|
|
56
|
+
|
|
57
|
+
> 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.
|
|
58
|
+
> — *Yukihiro Matsumoto* (Ruby Issue [#22291](https://bugs.ruby-lang.org/issues/22291#note-4), emphasis added)
|
|
59
|
+
|
|
60
|
+
In Ruby, Farce objects reflect their frozen state accurately. If a map returns `true` for `frozen?`, you cannot add, remove, or replace its entries.
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
To work around this, Farce follows a hybrid approach, marking objects with state purely defined in a C extension as Ractor shareable without freezing them, and reimplementing freezing behavior in Ruby for objects where this isn't safely possible.
|
|
65
|
+
|
|
66
|
+
This means:
|
|
67
|
+
|
|
68
|
+
* `frozen?` and `freeze` will behave as expected for all Farce classes.
|
|
69
|
+
* `Kernel.instance_method(:frozen?).bind_call(object)` might report a different value from `object.frozen?`
|
|
70
|
+
* C-level checks for frozen state, such as `RB_OBJ_FROZEN`, might differ from what Ruby-level methods report.
|
|
71
|
+
|
|
72
|
+
## Read Next
|
|
73
|
+
|
|
74
|
+
[README](../README.md#features) · [Installation](installation.md) · [Integrations](integrations.md)
|
data/docs/concurrency.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Concurrency
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Concurrency
|
|
6
|
+
|
|
7
|
+
Farce is built for concurrent and parallel code execution.
|
|
8
|
+
|
|
9
|
+
## Locks
|
|
10
|
+
|
|
11
|
+
Farce ships with two lock classes:
|
|
12
|
+
* `Lock` is a drop-in replacement for Ruby's `Mutex`.
|
|
13
|
+
* `ReadWriteLock` exposes `with_read_lock`/`with_write_lock` to allow multiple concurrent reads, but exclusive write access.
|
|
14
|
+
|
|
15
|
+
However, Farce exposes many APIs to eliminate the need for locks altogether.
|
|
16
|
+
|
|
17
|
+
## Atomic Operations
|
|
18
|
+
|
|
19
|
+
Farce's [data structures](data-structures.md#data-structures) all expose a range of methods for atomic operations:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# A Hash-like object
|
|
23
|
+
map = Farce::Map.new
|
|
24
|
+
|
|
25
|
+
# Atomically store something for :key if it hasn't been set
|
|
26
|
+
map.store_if_absent(:key) { "initial value" }
|
|
27
|
+
|
|
28
|
+
# Atomically update :key
|
|
29
|
+
map.update(:key, &:upcase)
|
|
30
|
+
|
|
31
|
+
# An Array-like object
|
|
32
|
+
list = Farce::Vector.new
|
|
33
|
+
list[0] = 42
|
|
34
|
+
|
|
35
|
+
# Atomically replace list[0] with 256 if the value is still 42
|
|
36
|
+
list.compare_and_set(0, 42, 256)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Observability and Signaling
|
|
40
|
+
|
|
41
|
+
All the [data structures](data-structures.md#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).
|
|
42
|
+
|
|
43
|
+
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).
|
|
44
|
+
|
|
45
|
+
So instead of sharing locks you can simply wait for a change to happen!
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# block until the stored value for :key is greater than 10
|
|
49
|
+
map.wait_until(:key) { it > 10 }
|
|
50
|
+
|
|
51
|
+
# block until the value for an atom is no longer :initial
|
|
52
|
+
atom.wait_until_changed(:initial)
|
|
53
|
+
|
|
54
|
+
# block until a counter has reached at least 20
|
|
55
|
+
counter.wait_while_below(20)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If these conditions are met right away, these never block.
|
|
59
|
+
|
|
60
|
+
But what about more complex conditions, involving multiple variables, or objects from other libraries that don't implement similar methods? No need to reach for a lock! You can use a Signal!
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
signal = Farce::Signal.new
|
|
64
|
+
target = 100
|
|
65
|
+
counter = Farce::Counter.new
|
|
66
|
+
|
|
67
|
+
# A Thread that keeps reducing the target value every 20 milliseconds
|
|
68
|
+
Thread.new do
|
|
69
|
+
while target.positive?
|
|
70
|
+
sleep 0.02
|
|
71
|
+
target -= 1
|
|
72
|
+
signal.broadcast # notify everyone else
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A Ractor that counts up in 10 millisecond intervals
|
|
77
|
+
Ractor.new(counter, signal) do |counter, signal|
|
|
78
|
+
while counter < 100
|
|
79
|
+
sleep 0.01
|
|
80
|
+
counter.increment
|
|
81
|
+
signal.broadcast # notify everyone else
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# wait until the counter is at or above the target
|
|
86
|
+
signal.wait_until { counter >= target }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Transactions
|
|
90
|
+
|
|
91
|
+
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).
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
Otherwise the block is rerun.
|
|
98
|
+
|
|
99
|
+
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](data-structures.md#maps), [vectors](data-structures.md#vectors), [sets](data-structures.md#sets), [atoms](data-structures.md#atoms), and [molecules](data-structures.md#molecules). Moreover, it also supports [mutables](sharing.md#mutables), meaning you can turn most Ruby objects into something transaction compatible fairly easily!
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
accounts = Farce::Map.new({a: 100, b: 200})
|
|
105
|
+
|
|
106
|
+
# transfer 80 from :b to :a, but only if both succeed
|
|
107
|
+
success = Farce.transaction(accounts) do |tx, accounts|
|
|
108
|
+
tx.abort! if accounts[:b] < 80
|
|
109
|
+
accounts[:a] += 80
|
|
110
|
+
accounts[:b] -= 80
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if success
|
|
114
|
+
puts "Transaction succeeded"
|
|
115
|
+
else
|
|
116
|
+
puts "Transaction failed"
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In the above example, `accounts` was added to the transaction right away. But you can also add new objects to the transaction programmatically by calling `tx[object]`. All reads and writes need to happen through the wrapper object returned by that call (or passed to the block).
|
|
121
|
+
|
|
122
|
+
### Maps and Sets
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
Similarly, if your transaction uses `Transaction::Map#size` as input, only a change in size would trigger a rerun, not a change in content.
|
|
127
|
+
|
|
128
|
+
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).
|
|
129
|
+
|
|
130
|
+
### TVars
|
|
131
|
+
|
|
132
|
+
Farce doesn't come with a `TVar` class. You can just use an [atom](data-structures.md#atoms) instead.
|
|
133
|
+
|
|
134
|
+
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.md#integrations)).
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
tvar = Concurrent::TVar.new(50)
|
|
138
|
+
map = Farce::Map.new({a: 100, b: 200})
|
|
139
|
+
|
|
140
|
+
Farce.transaction(map, tvar) do |_, map, tvar|
|
|
141
|
+
# subtract tvar's value from a and b, then set tvar to 0
|
|
142
|
+
# no invalid in-between state is ever visible to anything outside of this transaction
|
|
143
|
+
map[:a] -= tvar.value
|
|
144
|
+
map[:b] -= tvar.value
|
|
145
|
+
tvar.value = 0
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Read Next
|
|
150
|
+
|
|
151
|
+
[README](../README.md#features) · [Data Structures](data-structures.md) · [Scheduling Code](scheduling.md)
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Data Structures
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Data Structures
|
|
6
|
+
|
|
7
|
+
Farce provides a range of data structures that are ractor-safe, mutable, and expose high-level concurrency APIs in addition to standard Ruby APIs for the built-in classes it offers replacements for.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Data Structures](#data-structures)
|
|
12
|
+
- [Contents](#contents)
|
|
13
|
+
- [Maps](#maps)
|
|
14
|
+
- [Vectors](#vectors)
|
|
15
|
+
- [Sets](#sets)
|
|
16
|
+
- [Atoms](#atoms)
|
|
17
|
+
- [Counters](#counters)
|
|
18
|
+
- [Flags](#flags)
|
|
19
|
+
- [References](#references)
|
|
20
|
+
- [Molecules](#molecules)
|
|
21
|
+
- [Queues](#queues)
|
|
22
|
+
- [Priority Queues](#priority-queues)
|
|
23
|
+
- [Timer Queues](#timer-queues)
|
|
24
|
+
- [Queue capacity](#queue-capacity)
|
|
25
|
+
- [Read Next](#read-next)
|
|
26
|
+
|
|
27
|
+
## Maps
|
|
28
|
+
|
|
29
|
+
Maps are Hash-like key-value data structures. They do not preserve insertion order.
|
|
30
|
+
|
|
31
|
+
They implement almost all methods Ruby's Hash offers:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
map = Farce::Map.new
|
|
35
|
+
map[:foo] = :bar
|
|
36
|
+
|
|
37
|
+
map.merge! answer: 42
|
|
38
|
+
map.transform_values! { -it.to_s }
|
|
39
|
+
map.to_h # => {answer: "42", foo: "bar"}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
In addition, they come with a range of [atomic operations](concurrency.md#atomic-operations), and have built-in key normalization support:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# normalize_keys can be a symbol, proc, hash, or another map
|
|
46
|
+
map = Farce::Map.new({ a: 10 }, normalize_keys: :to_s)
|
|
47
|
+
|
|
48
|
+
# Atomic upsert operation
|
|
49
|
+
2.times { map.upsert(:b, 42) { it * map[:a] } }
|
|
50
|
+
|
|
51
|
+
# Keys have been converted to strings
|
|
52
|
+
map.keys.sort # => ["a", "b"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Besides the standard map implementation, Farce includes a range of specialized maps:
|
|
56
|
+
|
|
57
|
+
* `LRUMap` will evict the least recently used entry to not grow beyond the maximum size.
|
|
58
|
+
* `LFUMap` will evict the least frequently used entry to not grow beyond the maximum size.
|
|
59
|
+
* `LeaseMap` manages [leases](sharing.md#leases) associated with known keys.
|
|
60
|
+
* `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.
|
|
61
|
+
* `WeakMap` only holds weak references to its keys and values, automatically removing entries when the corresponding key or value gets garbage collected.
|
|
62
|
+
* `WeakKeyMap` only holds weak references to its keys, automatically removing entries when the corresponding key gets garbage collected.
|
|
63
|
+
* `WeakValueMap` only holds weak references to its values, automatically removing entries when the corresponding value gets garbage collected.
|
|
64
|
+
|
|
65
|
+
For example, bounded maps are useful for implementing caches:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
cache = Farce::LRUMap.new(max_size: 2)
|
|
69
|
+
|
|
70
|
+
cache[:first] = 1
|
|
71
|
+
cache[:second] = 2
|
|
72
|
+
cache[:first] # makes sure :first was accessed more recently than :second
|
|
73
|
+
cache[:third] = 3
|
|
74
|
+
|
|
75
|
+
cache.key?(:second) # => false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Vectors
|
|
79
|
+
|
|
80
|
+
`Farce::Vector` is to `Array` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
list = Farce::Vector.new
|
|
84
|
+
10.times { list << it }
|
|
85
|
+
list.to_a # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
86
|
+
list.sum # => 45
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
There is one important difference to arrays: Bulk mutations, such as `Vector#map!` are atomic. Either the block succeeds for all entries, or if it raises an exception then none of the changes are applied. Such mutations may also not write to the same vector from within the block.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
list = Farce::Vector[1, 2, 3, 4]
|
|
93
|
+
|
|
94
|
+
begin
|
|
95
|
+
list.map! do |value|
|
|
96
|
+
raise "error" if value == 2
|
|
97
|
+
value * 2
|
|
98
|
+
end
|
|
99
|
+
rescue => e
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# This would return 2 if list was an array
|
|
103
|
+
list.first # => 1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Sets
|
|
107
|
+
|
|
108
|
+
`Farce::Set` is to `Set` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
jobs = Farce::Set[:compile, :test]
|
|
112
|
+
jobs.add? :publish # => jobs
|
|
113
|
+
jobs.add? :test # => nil
|
|
114
|
+
jobs.include? :publish # => true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Besides the standard set implementation, Farce includes two specialized sets:
|
|
118
|
+
|
|
119
|
+
* `SortedSet` keeps its entries in order based on value comparison.
|
|
120
|
+
* `WeakSet` only holds weak references to its entries.
|
|
121
|
+
|
|
122
|
+
## Atoms
|
|
123
|
+
|
|
124
|
+
Atoms are value containers. These store a single reference to any Ruby object, and expose atomic operations for mutating these values:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
atom = Farce::Atom.new("initial value")
|
|
128
|
+
atom.value # => "initial value"
|
|
129
|
+
|
|
130
|
+
# atoms are ractor-shareable
|
|
131
|
+
Ractor.new(atom) do |atom|
|
|
132
|
+
atom.update { |current| current + " updated" } # => "initial value updated"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# waits for the other ractor to perform its update
|
|
136
|
+
atom.wait_until_changed "initial value"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`WeakAtom` only keeps a weak reference for its current value, reverting to `nil` when the current value gets garbage collected.
|
|
140
|
+
|
|
141
|
+
## Counters
|
|
142
|
+
|
|
143
|
+
Ractor-shareable, atomic counter. This is a stateful, numeric object.
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
counter = Farce::Counter.new
|
|
149
|
+
counter.value # => 0
|
|
150
|
+
|
|
151
|
+
# Increase the counter by 1
|
|
152
|
+
counter.increment
|
|
153
|
+
counter.value # => 1
|
|
154
|
+
|
|
155
|
+
# Increase the counter by 5 on another Ractor
|
|
156
|
+
Ractor.new(counter) { it.add(5) }.join
|
|
157
|
+
|
|
158
|
+
counter.value # => 6
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Flags
|
|
162
|
+
|
|
163
|
+
Ractor-shareable, atomic boolean.
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
flag = Farce::Flag.new(true)
|
|
169
|
+
flag.value # => true
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## References
|
|
173
|
+
|
|
174
|
+
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:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
atom = Farce::Atom.new(42)
|
|
178
|
+
ref = Farce::Reference.new(atom)
|
|
179
|
+
|
|
180
|
+
ref.to_s # => "42"
|
|
181
|
+
ref > 10 # => true
|
|
182
|
+
|
|
183
|
+
atom.value = :foo
|
|
184
|
+
ref.to_s # => "foo"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Molecules
|
|
188
|
+
|
|
189
|
+
Molecules are structures made up of multiple atoms. Clever naming, right?
|
|
190
|
+
|
|
191
|
+
They are similar to Ruby's built-in `Struct` and `Data` classes, but their members are stored in atoms:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
Request = Farce::Molecule.define(:verb, :path)
|
|
195
|
+
request = Request.new("GET", "/index.html")
|
|
196
|
+
request.verb # => "GET"
|
|
197
|
+
request.verb = "POST"
|
|
198
|
+
|
|
199
|
+
# This doesn't succeed, as the verb isn't "HEAD"
|
|
200
|
+
request.verb_atom.compare_and_set("HEAD", "GET")
|
|
201
|
+
request.verb # => "POST"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
You can also pass a block to add additional methods:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
Request = Farce::Molecule.define(:verb, :path) do
|
|
208
|
+
def to_s = "#{verb} #{path}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
Request.new(verb: "GET", path: "/").to_s # => "GET /"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
You can also inherit from the generated class:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
class Request < Farce::Molecule.define(:verb, :path)
|
|
218
|
+
def to_s = "#{verb} #{path}"
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Queues
|
|
223
|
+
|
|
224
|
+
`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.md#sharing-modes).
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
queue = Farce::Queue.new
|
|
228
|
+
queue.push(:ok)
|
|
229
|
+
queue.pop # => :ok
|
|
230
|
+
queue.try_pop # => nil
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Queues provide additional features over Ruby's queue:
|
|
234
|
+
* Like the rest of Farce, they work with Ractors (implementing [sharing modes](sharing.md#sharing-modes) for their [default variant](variants-and-scopes.md#variants-and-scopes)).
|
|
235
|
+
* In addition to closing, they allow sealing, which prevents pushes but still allows pulling from the queue until it has been drained.
|
|
236
|
+
* 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.
|
|
237
|
+
* 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).
|
|
238
|
+
|
|
239
|
+
The [strict variant](variants-and-scopes.md#available-variants) has comparable performance to `SizedQueue`, while the default variant pays a fixed overhead for [handling unshareable data](sharing.md#sharing-unshareable-data). Both variants outperform other third-party, Ractor-shareable queues (none of which are Fiber scheduler compatible, which Farce's queues are).
|
|
240
|
+
|
|
241
|
+
### Priority Queues
|
|
242
|
+
|
|
243
|
+
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:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# use order: :descending to reverse priorities
|
|
247
|
+
queue = Farce::PriorityQueue.new
|
|
248
|
+
|
|
249
|
+
queue.push :a, priority: 2
|
|
250
|
+
queue.push :b, priority: 1
|
|
251
|
+
|
|
252
|
+
queue.pop # => :b
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Timer Queues
|
|
256
|
+
|
|
257
|
+
Timer queues are similar to priority queues, sorted by float values, but these values are timestamps (based on [Farce::Clock](utilities.md#additional-classes), not on Ruby's Time, which isn't monotonic). Any `pop` will block until the next timestamp has been reached.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
queue = Farce::TimerQueue.new
|
|
261
|
+
queue.push(:ok, in: 1.5)
|
|
262
|
+
|
|
263
|
+
# blocks for ~1.5 seconds
|
|
264
|
+
queue.pop # => :ok
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
queue = Farce::TimerQueue.new
|
|
271
|
+
entry = queue.push(:work, in: 60)
|
|
272
|
+
|
|
273
|
+
entry.delete # => true
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
You can also delete a value if you know its timestamp:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
queue = Farce::TimerQueue.new
|
|
280
|
+
timestamp = Farce.clock(in: 0.2)
|
|
281
|
+
|
|
282
|
+
queue.push(:first, at: timestamp - 0.1)
|
|
283
|
+
queue.push(:second, at: timestamp)
|
|
284
|
+
queue.push(:third, at: timestamp + 0.1)
|
|
285
|
+
|
|
286
|
+
fourth = queue.push(:fourth, at: timestamp + 0.2)
|
|
287
|
+
queue.delete(fourth)
|
|
288
|
+
|
|
289
|
+
queue.delete(:second, at: timestamp)
|
|
290
|
+
|
|
291
|
+
result = []
|
|
292
|
+
result << queue.pop until queue.empty?
|
|
293
|
+
result # => [:first, :third]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Queue capacity
|
|
297
|
+
|
|
298
|
+
Queues support setting a maximum capacity. If the capacity has been reached it will block any pushes to create back pressure.
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
queue = Farce::Queue.new(capacity: 2)
|
|
302
|
+
|
|
303
|
+
queue.try_push(:a) # => true
|
|
304
|
+
queue.try_push(:b) # => true
|
|
305
|
+
queue.try_push(:c) # => false
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Normal queues have a default capacity of 1024. You can explicitly set the capacity to `nil` to create an unbounded queue.
|
|
309
|
+
Priority queues and timer queues do not have a default capacity, so earlier entries are not getting blocked unexpectedly.
|
|
310
|
+
|
|
311
|
+
## Read Next
|
|
312
|
+
|
|
313
|
+
[README](../README.md#features) · [Sharing Unshareable Data](sharing.md) · [Concurrency](concurrency.md) · [Variants and Scopes](variants-and-scopes.md)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Installation
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
|
|
7
|
+
## Globally
|
|
8
|
+
|
|
9
|
+
To install Farce globally, you can use the following command:
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
$ gem install farce
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## As a project dependency
|
|
16
|
+
|
|
17
|
+
If you want to use Farce directly in your project, it is recommended to do so via [Bundler](https://bundler.io).
|
|
18
|
+
Add Farce to your `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
source "https://gem.coop" # or "https://rubygems.org"
|
|
22
|
+
|
|
23
|
+
gem "farce"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then run `bundle install` to install the dependencies.
|
|
27
|
+
|
|
28
|
+
## As a library dependency
|
|
29
|
+
|
|
30
|
+
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:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Gem::Specification.new do |spec|
|
|
34
|
+
# ...
|
|
35
|
+
spec.add_dependency "farce"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Local setup
|
|
40
|
+
|
|
41
|
+
If you want to work on Farce itself, you can clone the repository and use [mise](https://mise.jdx.dev) to set everything up:
|
|
42
|
+
|
|
43
|
+
For more details, or if you aren't using mise, check the [contribution guidelines](../CONTRIBUTING.md).
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
$ git clone https://github.com/rkh/farce.git # prefix with `jj` if you're using Jujutsu
|
|
47
|
+
$ cd farce
|
|
48
|
+
$ mise run
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Loading Farce
|
|
52
|
+
You should always require `farce`, rather than any other files in `lib`. Other files are not intended as entry points.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
require "farce"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Constants (classes, modules, etc.) under the `Farce` namespace are loaded lazily (thread- and ractor-safe), so there is no
|
|
59
|
+
need to specifically load any particular file.
|
|
60
|
+
|
|
61
|
+
## Read Next
|
|
62
|
+
|
|
63
|
+
[README](../README.md#features) · [Introduction](introduction.md) · [Compatibility and Dependencies](compatibility.md)
|