farce 0.4.1 → 0.5.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -0
  15. data/ext/farce/farce.c +9 -0
  16. data/ext/farce/map.c +14 -3
  17. data/ext/farce/priority_queue.c +46 -1
  18. data/ext/farce/queue.c +141 -7
  19. data/ext/farce/vector.c +3 -3
  20. data/ext/farce/weak_map.c +12 -3
  21. data/ext/java/org/farce/PriorityQueue.java +18 -2
  22. data/lib/farce/abstract/concurrent_map.rb +122 -0
  23. data/lib/farce/abstract/counter.rb +35 -1
  24. data/lib/farce/abstract/executor.rb +30 -7
  25. data/lib/farce/abstract/flag.rb +12 -0
  26. data/lib/farce/abstract/pool.rb +158 -47
  27. data/lib/farce/abstract/priority_queue.rb +1 -1
  28. data/lib/farce/abstract/publisher.rb +189 -0
  29. data/lib/farce/abstract/queue.rb +29 -3
  30. data/lib/farce/abstract/scheduler.rb +41 -14
  31. data/lib/farce/abstract/timer_queue.rb +3 -3
  32. data/lib/farce/abstract/vector.rb +3 -1
  33. data/lib/farce/atom.rb +9 -2
  34. data/lib/farce/clock.rb +12 -0
  35. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  36. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  37. data/lib/farce/concurrent/event.rb +60 -0
  38. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  39. data/lib/farce/concurrent/local_var.rb +65 -0
  40. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  41. data/lib/farce/concurrent/tuple.rb +74 -0
  42. data/lib/farce/concurrent.rb +50 -0
  43. data/lib/farce/config.rb +112 -2
  44. data/lib/farce/counter.rb +0 -16
  45. data/lib/farce/engine/jruby/map.rb +8 -6
  46. data/lib/farce/engine/jruby/signal.rb +2 -2
  47. data/lib/farce/engine/jvm/farce.jar +0 -0
  48. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  49. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  50. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  51. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  52. data/lib/farce/engine/shared/exchanger.rb +2 -2
  53. data/lib/farce/engine/shared/queue.rb +43 -5
  54. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  55. data/lib/farce/engine/shared/strict_map.rb +5 -0
  56. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  57. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  58. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  59. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  60. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  61. data/lib/farce/engine/truffleruby/map.rb +8 -6
  62. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  63. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  64. data/lib/farce/integrations/concurrent.rb +16 -3
  65. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  66. data/lib/farce/internal/flag_waiting.rb +1 -1
  67. data/lib/farce/internal/lease_initialization.rb +1 -1
  68. data/lib/farce/internal/lease_pool_state.rb +1 -1
  69. data/lib/farce/internal/lease_state.rb +2 -2
  70. data/lib/farce/internal/local_executor_methods.rb +3 -3
  71. data/lib/farce/internal/map_value_modes.rb +9 -2
  72. data/lib/farce/internal/pool_settings.rb +9 -0
  73. data/lib/farce/internal/pool_worker.rb +34 -21
  74. data/lib/farce/internal/reservation_waiting.rb +1 -1
  75. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  76. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  77. data/lib/farce/internal.rb +8 -8
  78. data/lib/farce/lazy.rb +3 -0
  79. data/lib/farce/lazy_ref.rb +3 -0
  80. data/lib/farce/local/lease.rb +2 -9
  81. data/lib/farce/local/lease_map.rb +2 -9
  82. data/lib/farce/local/pool.rb +38 -5
  83. data/lib/farce/local/publisher.rb +30 -0
  84. data/lib/farce/local/scheduler.rb +18 -5
  85. data/lib/farce/local/vector.rb +3 -2
  86. data/lib/farce/memoize.rb +314 -0
  87. data/lib/farce/pool.rb +5 -0
  88. data/lib/farce/priority_queue.rb +1 -1
  89. data/lib/farce/publisher.rb +57 -0
  90. data/lib/farce/signal.rb +1 -1
  91. data/lib/farce/strict/publisher.rb +31 -0
  92. data/lib/farce/strict/vector.rb +3 -2
  93. data/lib/farce/subscription/definitions.rb +27 -0
  94. data/lib/farce/subscription.rb +121 -0
  95. data/lib/farce/transaction/map.rb +35 -1
  96. data/lib/farce/unshared/pool.rb +1 -1
  97. data/lib/farce/unshared/publisher.rb +25 -0
  98. data/lib/farce/unshared/scheduler.rb +3 -3
  99. data/lib/farce/unshared/vector.rb +3 -2
  100. data/lib/farce/vector.rb +12 -9
  101. data/lib/farce/version.rb +1 -1
  102. data/lib/farce/walker/definitions.rb +13 -2
  103. data/lib/farce/weak_atom.rb +5 -2
  104. data/lib/farce.rb +3 -1
  105. metadata +18 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daac1fc9dbb66e9473f1a61dc6fcaf4a4a0875a9c9f98670d47759d450f2b248
4
- data.tar.gz: 3991bfd67d083fe58cd0e808f810e49809d667c11a005f68e63887cd29038b79
3
+ metadata.gz: 0010e09d494cac546062e8d36c1a5f4479b31210f429ab07ec02d7fa04044418
4
+ data.tar.gz: d09a395c757a2ad0e90725b53a45703706a092c94f800b59b6fdd51611939697
5
5
  SHA512:
6
- metadata.gz: 0cbcf7e5769c5084198c06576d27d919501c5a54eafaacd0dccb8d268db416e784ac3556e773a64a4380a4f5c18cbe65fdedfb040ec5c4b0342c75d68eddaae0
7
- data.tar.gz: 142f08011fd2704395a1d7ed3f04366e425f9f6a539ef525206b41c14707b03abc4f71d62c53ddcd1a634d11ddf1bcf14f62da034eec94b86ff31905069c78b2
6
+ metadata.gz: '019155c1fdddbbb9a26aef82fcb399cb409e7e3aa25b462961b3b8b9998ec6ec7b077ceb44e71177a9ab5c2d2952cfe21affe9b989fecc6c4c1f390b2765219a'
7
+ data.tar.gz: 0babf9eccef51c77c3a936322f105789c724ffb3a43412250fd781e4292e1ca4f341fdba35f9989599625b38fc8020439813c20b666160b21e557dcd37f2700b
data/CHANGELOG.md CHANGED
@@ -2,9 +2,87 @@
2
2
 
3
3
  Farce follows [Semantic Versioning 2.0](http://semver.org/). Any public method on any public class or module documented in the README or via YARD and not declared private is part of the public API.
4
4
 
5
- ## Development Releases
5
+ ## Overview
6
6
 
7
- ### Farce 0.4.1 (2026-10-16)
7
+ Version | Date | Important Changes
8
+ --------------------------------|------------|--------------------------------------
9
+ [0.5.0](#farce-050-2026-10-21) | 2026-10-21 | Publishers, memoization, pool and queue resizing, and better concurrent-ruby compatibility
10
+ [0.4.1](#farce-041-2026-10-18) | 2026-10-18 | Include improved io_uring support in prebuilt Linux binaries
11
+ [0.4.0](#farce-040-2026-10-16) | 2026-10-16 | Introduce `Farce::Backports`
12
+ [0.3.0](#farce-030-2026-10-13) | 2026-10-13 | Introduce `Farce::Task` and executor improvements
13
+ [0.2.0](#farce-020-2026-10-11) | 2026-10-11 | Improvements for flags, vectors, and timer queues, and performance
14
+ [0.1.0](#farce-010-2026-10-09) | 2026-10-09 | Initial release
15
+
16
+ ## Release History
17
+
18
+ ### Farce 0.5.0 (2026-10-21)
19
+
20
+ #### New Features
21
+
22
+ ##### Publishers
23
+
24
+ `Farce::Publisher` broadcasts messages to multiple subscribers. It supports Ractors, ports, queues, atoms, counters, flags, collections, signals, other publishers, and callbacks.
25
+
26
+ ```ruby
27
+ publisher = Farce::Publisher.new
28
+ counter = Farce::Counter.new
29
+ queue = Farce::Queue.new
30
+
31
+ publisher.subscribe(counter)
32
+ publisher.subscribe(queue)
33
+ publisher.publish(:hello)
34
+
35
+ counter.value # => 1
36
+ queue.pop # => :hello
37
+ ```
38
+
39
+ ##### Memoization
40
+
41
+ Added `Farce::Memoize` for memoization with scopes, transfer modes, and weak values, including after freezing.
42
+
43
+ ```ruby
44
+ class MyClass
45
+ include Farce::Memoize
46
+
47
+ # Memoize #expensive_method, shared across Ractors
48
+ memoize def expensive_method = expensive_computation
49
+
50
+ # Memoize #method_with_unsharable_value for each Ractor separately
51
+ def method_with_unsharable_value = unsharable_computation
52
+ memoize :method_with_unsharable_value, scope: :ractor
53
+ end
54
+ ```
55
+
56
+ ##### Better `concurrent-ruby` Compatibility
57
+
58
+ * Added `Farce::Concurrent`, with replacements for `CountDownLatch`, `CyclicBarrier`, `Event`, `FiberLocalVar`, `ThreadLocalVar`, `LockLocalVar`, and `Tuple`, plus aliases for existing Farce classes.
59
+ * Maps now support all public API methods of `Concurrent::Map`.
60
+ * Added `Counter#update` to atomically replace the value using a block.
61
+ * Added `true?`, `false?`, `make_true`, and `make_false` to flags.
62
+
63
+ ##### Queue and Pool Tuning
64
+
65
+ * Queues, priority queues, timer queues, schedulers, and pools now support resizing.
66
+ * Pools now support tuning after construction. Increasing the minimum starts workers immediately. Excess workers retire when idle without interrupting running tasks.
67
+ * Added `parallel_pool_*` configuration options for the shared CRuby pool used by `Farce.in_parallel`.
68
+
69
+ ##### Miscellaneous
70
+
71
+ * Added `timeout:` to executor `schedule` and `execute` methods.
72
+ * Added `Farce::Clock.remaining` and refactored code to use it.
73
+ * Vectors now accept an integer size in their constructors, filling the initial entries with `nil`.
74
+
75
+ #### Bug Fixes
76
+
77
+ * Prevent waits from spinning when an unchanged value has non-reflexive equality, such as `Float::NAN`, or overrides `==`, `!=`, or `equal?`. Predicates receive unwrapped values.
78
+ * Prevent graph traversal from retrying indefinitely when a map value cannot be matched using ordinary equality.
79
+ * Fix vector wait timeout fallbacks on CRuby and TruffleRuby.
80
+
81
+ #### Other changes
82
+
83
+ * Added `pairing_heap` to the priority queue benchmark and refreshed the published benchmark results. It is the fastest priority queue implementation written in pure Ruby.
84
+
85
+ ### Farce 0.4.1 (2026-10-18)
8
86
 
9
87
  * Make sure prebuilt Linux binaries include liburing support.
10
88
  * Use a Futex for efficient fiber synchronization if possible.
data/README.md CHANGED
@@ -39,6 +39,7 @@ Use familiar Ruby APIs with coordinated access to shared state:
39
39
  - **Maps, vectors, and sets** for collections, with sorted and weak-reference variants.
40
40
  - **Atoms, counters, flags, and molecules** for individual values and structured records.
41
41
  - **FIFO, priority, and timer queues** for passing work, applying back pressure, or delaying delivery.
42
+ - **Publishers and subscribers** for one-to-many communication, where a single publisher can broadcast messages to multiple subscribers.
42
43
 
43
44
  For example, use a Vector as a shared list:
44
45
 
@@ -68,6 +69,7 @@ cache.key?(:a) # => false, the least recently used entry was evicted
68
69
  - **`Farce::Port`** adds transfer modes, receive timeouts, and Fiber scheduler support.
69
70
  - **`Farce::WeakRef` and `Farce::Resolv`** work outside the main Ractor.
70
71
  - **`Farce::Backports`** backports newer `Ractor` and `Ractor::Port` features to older Ruby versions. Can be invoked directly, used with refinements, or installed globally.
72
+ - **`Farce::Concurrent`** provides Ractor-friendly replacements for some classes provided by `concurrent-ruby`.
71
73
 
72
74
  Send a result back from a worker:
73
75
 
@@ -210,7 +212,7 @@ config.class # => Farce::Map
210
212
  config[:retries].class # => Farce::Vector
211
213
  ```
212
214
 
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.
215
+ Other helpers freeze object graphs, deduplicate values, offer method memoization, 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.
214
216
 
215
217
  → [Conversion helpers and extension points](docs/utilities.md).
216
218
 
data/docs/benchmarks.md CHANGED
@@ -57,6 +57,7 @@ Also note that `Ratomic::Map` has a significant performance benefit over all oth
57
57
  concurrent-ruby-ext | 1.2x slower | same-ish | 32-bit | no ractor support
58
58
  ratomic | 1.5x slower | 1.6x slower | 64-bit | no overflow protection
59
59
  ractor_safe | 2.1x slower | 2.1x slower | 64-bit |
60
+ atomic-ruby | 10x slower | 2.9x slower | any | via `Atom`
60
61
  concurrent-ruby | 12x slower | 5x slower | 32-bit | no ractor support
61
62
 
62
63
  Performance differences between implementations are consistent between single-threaded and multi-threaded benchmarks.
@@ -101,13 +102,14 @@ In the producer/consumer workload in `benchmark/queue.rb`, Ruby's built-in `Thre
101
102
  Implementation | Performance | Notes
102
103
  -------------------------------|-------------|-------------
103
104
  `Thread::Queue` | Fastest | no ractor support
104
- `Thread::SizedQueue` | 1.4x slower | no ractor support
105
- `Farce::Strict::Queue` | 1.5x slower | only allows sharable objects
106
- `Farce::Queue` | 2.1x slower |
107
- `RactorSafe::Queue` | 2.9x slower | only allows sharable objects
108
- `Ratomic::Queue` | 9.8x slower | breaks ractor isolation
109
- `RactorQueue` | 30x slower | breaks ractor isolation
110
- `Ractor::Port` (multiplexing) | 100x slower |
105
+ `AtomicQueue` | 1.5x slower | no ractor support
106
+ `Thread::SizedQueue` | 1.5x slower | no ractor support
107
+ `Farce::Strict::Queue` | 1.6x slower | only allows sharable objects
108
+ `Farce::Queue` | 2.2x slower |
109
+ `RactorSafe::Queue` | 2.8x slower | only allows sharable objects
110
+ `Ratomic::Queue` | 12x slower | breaks ractor isolation
111
+ `RactorQueue` | 29x slower | breaks ractor isolation
112
+ `Ractor::Port` (multiplexing) | 110x slower |
111
113
 
112
114
  ## Priority queue performance
113
115
 
@@ -126,14 +128,14 @@ The below numbers compare non-blocking APIs, as only Farce implements a blocking
126
128
  <th>↓ Gem</th>
127
129
  <th>Insertion order →</th>
128
130
  <th>Random</th>
129
- <th>Descending</th>
131
+ <th>Sorted</th>
130
132
  <th>Random</th>
131
- <th>Descending</th>
133
+ <th>Sorted</th>
132
134
  </tr>
133
135
  </thead>
134
136
  <tbody>
135
137
  <tr>
136
- <td colspan="2">farce 0.1.0</td>
138
+ <td colspan="2">farce 0.4.1</td>
137
139
  <td>fastest</td>
138
140
  <td>fastest</td>
139
141
  <td>fastest</td>
@@ -141,43 +143,57 @@ The below numbers compare non-blocking APIs, as only Farce implements a blocking
141
143
  </tr>
142
144
  <tr>
143
145
  <td colspan="2"><a href="https://github.com/mame/rbtree">rbtree</a> 0.4.7</td>
144
- <td>1.1x slower</td>
145
- <td>1.2x slower</td>
146
+ <td>1.6x slower</td>
147
+ <td>2.9x slower</td>
146
148
  <td>–</td>
147
149
  <td>–</td>
148
150
  </tr>
149
151
  <tr>
150
152
  <td colspan="2"><a href="https://github.com/boborbt/priority_queue_cxx">priority_queue_cxx</a> 0.3.7</td>
151
- <td>1.7x slower</td>
152
- <td>1.7x slower</td>
153
+ <td>2.7x slower</td>
154
+ <td>4.0x slower</td>
153
155
  <td>–</td>
154
156
  <td>–</td>
155
157
  </tr>
156
158
  <tr>
157
- <td colspan="2"><a href="https://github.com/socketry/io-event">io-event</a> 1.21.1</td>
158
- <td>2.6x slower</td>
159
- <td>3.9x slower</td>
159
+ <td colspan="2"><a href="https://github.com/mhib/pairing_heap">pairing_heap</a> 3.1.2 (simple)</td>
160
+ <td>3.0x slower</td>
161
+ <td>1.3x slower</td>
162
+ <td>2.3x slower</td>
163
+ <td>1.1x slower</td>
164
+ </tr>
165
+ <tr>
166
+ <td colspan="2"><a href="https://github.com/socketry/io-event">io-event</a> 1.22.3</td>
167
+ <td>4.5x slower</td>
168
+ <td>9.6x slower</td>
160
169
  <td>1.3x slower</td>
161
170
  <td>1.7x slower</td>
162
171
  </tr>
172
+ <tr>
173
+ <td colspan="2"><a href="https://github.com/mhib/pairing_heap">pairing_heap</a> 3.1.2</td>
174
+ <td>5.6x slower</td>
175
+ <td>4.1x slower</td>
176
+ <td>3.6x slower</td>
177
+ <td>1.9x slower</td>
178
+ </tr>
163
179
  <tr>
164
180
  <td colspan="2"><a href="https://github.com/rubyworks/pqueue">pqueue</a> 2.2.0</td>
165
- <td>9.5x slower</td>
166
- <td>8.0x slower</td>
181
+ <td>15.1x slower</td>
182
+ <td>19.7x slower</td>
167
183
  <td>2.2x slower</td>
168
184
  <td>1.9x slower</td>
169
185
  </tr>
170
186
  <tr>
171
187
  <td colspan="2"><a href="https://github.com/matiasbattocchia/lazy-priority-queue">lazy_priority_queue</a> 0.1.1</td>
172
188
  <td>9.9x slower</td>
173
- <td>7.9x slower</td>
189
+ <td>20x slower</td>
174
190
  <td>3.9x slower</td>
175
191
  <td>3.2x slower</td>
176
192
  </tr>
177
193
  <tr>
178
194
  <td colspan="2"><a href="https://github.com/philiprehberger/rb-priority-queue">philiprehberger-priority_queue</a> 0.5.0</td>
179
195
  <td>13x slower</td>
180
- <td>19x slower</td>
196
+ <td>48x slower</td>
181
197
  <td>7.6x slower</td>
182
198
  <td>11x slower</td>
183
199
  </tr>
@@ -69,6 +69,19 @@ This means:
69
69
  * `Kernel.instance_method(:frozen?).bind_call(object)` might report a different value from `object.frozen?`
70
70
  * C-level checks for frozen state, such as `RB_OBJ_FROZEN`, might differ from what Ruby-level methods report.
71
71
 
72
+ ### Fiber schedulers and Ractors don't play nice on Ruby 3.4
73
+
74
+ Fiber schedulers running in a non-main Ractor have a chance of causing a segmentation fault on x86_64 Linux with Ruby 3.4.11 and earlier.
75
+
76
+ This issue is [unrelated to Farce itself](https://gist.github.com/rkh/f330bd5c4accc9718f5ca227ae020c94) and typically only surfaces irregularly under a high number of Ractor shutdowns.
77
+ It does however surface from time to time when running the Farce test suite on affected Ruby versions and platforms.
78
+
79
+ Installing and loading Farce, using Farce data structures, etc, does not increase the likelihood of encountering this issue.
80
+
81
+ ### GC related tests are flaky in certain environments
82
+
83
+ Garbage collection related tests may exhibit flakiness on some Ruby versions and platforms. This can lead to unexpected failures when running the test suite.
84
+
72
85
  ## Read Next
73
86
 
74
87
  [README](../README.md#features) · [Installation](installation.md) · [Integrations](integrations.md)
@@ -22,6 +22,7 @@ Farce provides a range of data structures that are ractor-safe, mutable, and exp
22
22
  - [Priority Queues](#priority-queues)
23
23
  - [Timer Queues](#timer-queues)
24
24
  - [Queue capacity](#queue-capacity)
25
+ - [Publishers](#publishers)
25
26
  - [Read Next](#read-next)
26
27
 
27
28
  ## Maps
@@ -308,6 +309,42 @@ queue.try_push(:c) # => false
308
309
  Normal queues have a default capacity of 1024. You can explicitly set the capacity to `nil` to create an unbounded queue.
309
310
  Priority queues and timer queues do not have a default capacity, so earlier entries are not getting blocked unexpectedly.
310
311
 
312
+ ## Publishers
313
+
314
+ Both ports and queues deliver messages exactly ones. Multiple receivers can read from the same port (if they are running inside the same ractor) or queue, but each message will only be delivered once.
315
+
316
+ A publisher allows one-to-many communication, where a single publisher can broadcast messages to multiple subscribers. Published messages are sent to enabled subscribers.
317
+
318
+ Subscribers can be:
319
+
320
+ * Ractors, ports, queues, or other publishers, using their respective message sending mechanisms.
321
+ * A [vector](#vectors) or [set](#sets), in which case each message will be added to the collection.
322
+ * A [counter](#counters), incrementing it for each message received. This avoids potential copying of the message itself.
323
+ * A [flag](#flags), setting it to true whenever a message is received. This avoids potential copying of the message itself.
324
+ * A `Farce::Concurrent::Event`, setting it for each message received.
325
+ * A `Farce::Concurrent::CountDownLatch`, counting down for each message received.
326
+ * A callback, executed for each message received.
327
+
328
+ For unshared and local publisher [variants](variants-and-scopes.md) subscribers may also include:
329
+
330
+ * Any unshared variant of the subscriber list above.
331
+ * Ruby's `Thread::Queue` or `Thread::SizedQueue`
332
+ * `Concurrent::AtomicReference`, `Concurrent::AtomicFixnum`, `Concurrent::AtomicBoolean`, `Concurrent::Event`, and `Concurrent::CountDownLatch` if the [concurrent-ruby integration](integrations.md#concurrent-ruby) is active.
333
+
334
+ ```ruby
335
+ publisher = Farce::Publisher.new
336
+ counter = Farce::Counter.new
337
+
338
+ publisher.subscribe do |message|
339
+ puts "Received: #{message}"
340
+ end
341
+
342
+ publisher.publish(:hello)
343
+ publisher.publish(:world)
344
+
345
+ counter.to_i # => 2
346
+ ```
347
+
311
348
  ## Read Next
312
349
 
313
350
  [README](../README.md#features) · [Sharing Unshareable Data](sharing.md) · [Concurrency](concurrency.md) · [Variants and Scopes](variants-and-scopes.md)
data/docs/integrations.md CHANGED
@@ -82,6 +82,7 @@ The integration adds the following features if [concurrent-ruby](https://github.
82
82
 
83
83
  * [Transaction support for `Concurrent::TVar`](concurrency.md#tvars)
84
84
  * Automatic conversion of `Concurrent::Map` instances to [maps](data-structures.md#maps) via [`Farce.enfarce`](utilities.md#top-level-methods).
85
+ * [Subscriber support](data-structures.md#publishers) for `Concurrent::AtomicReference`, `Concurrent::AtomicFixnum`, `Concurrent::AtomicBoolean`, `Concurrent::Event`, and `Concurrent::CountDownLatch` with unshared and local publishers.
85
86
 
86
87
  ## Ractor Sharing
87
88
 
data/docs/introduction.md CHANGED
@@ -137,6 +137,25 @@ Both the Ruby core library, and other libraries, like the very popular [concurre
137
137
  <td>❌ <b>not supported</b></td>
138
138
  <td>⚠️ CRuby only</td>
139
139
  </tr>
140
+ <tr>
141
+ <td rowspan="3"><a href="https://github.com/joshuay03/atomic-ruby">atomic-ruby</a></td>
142
+ <td>AtomicQueue, AtomicThreadPool</td>
143
+ <td>⚠️ CRuby only</td>
144
+ <td>⚠️ CRuby only</td>
145
+ <td>❌ <b>not supported</b></td>
146
+ </tr>
147
+ <tr>
148
+ <td>Atom, AtomicCountDownLatch, AtomicBoolean</td>
149
+ <td>⚠️ CRuby only<b>¹</b></td>
150
+ <td>⚠️ CRuby only<b>¹</b></td>
151
+ <td>⚠️ CRuby only<b>¹</b></td>
152
+ </tr>
153
+ <tr>
154
+ <td>AtomicConditionVariable</td>
155
+ <td>⚠️ CRuby only</td>
156
+ <td>❌ <b>not supported</b></td>
157
+ <td>❌ <b>not supported</b></td>
158
+ </tr>
140
159
  </tbody>
141
160
  </table>
142
161
 
data/docs/modes.md CHANGED
@@ -390,8 +390,10 @@ The same choices appear in several Farce APIs. Containers normally default to `:
390
390
  | `Farce::Exchanger` | `new`, `exchange` | The value offered to a partner. |
391
391
  | `Farce::WeakAtom` | `new`, `store`, `update`, and other replacement operations | Weakly held values. Supports only `:raise`, `:make_shareable`, and `:dedup`. Defaults to `:raise`. |
392
392
  | `Farce::Atom` | `new`, `store`, `swap`, `update`, and other replacement operations | The stored value. |
393
+ | `Farce::Memoize` | `memoize`, `[]` | Memoized results with `scope: :global` (the default). With `weak: true`, supports only `:raise`, `:make_shareable`, and `:dedup`, and defaults to `:raise`. Other scopes ignore the mode. |
393
394
  | `Farce::Lazy`, `Farce::LazyRef` | `new` | The result computed once by the factory. |
394
395
  | `Farce::Task` | `new`, `schedule`, `try_schedule`, `in_parallel`, `on_main`, `add_argument` | Arguments and the successful result. `add_argument` overrides the mode for one argument. Scheduling can select local mode automatically. |
396
+ | `Farce::Publisher`, `Farce::Unshared::Publisher`, `Farce::Local::Publisher` | `subscribe` with a block | Callback arguments and queued payloads. Defaults to `:smart` for `Publisher` and `:local` for its Unshared and Local variants. |
395
397
  | `Farce::Molecule` | `define`, `new` | Newly created field atoms. |
396
398
  | `Farce::Vector` | `new`, `push`, `store`, `update`, and other replacement operations | Element values. |
397
399
  | `Farce::WeakSet` | `new`, `add`, `add?` | Weakly held elements. Supports only `:raise`, `:make_shareable`, and `:dedup`. Defaults to `:raise`. |
data/docs/scheduling.md CHANGED
@@ -43,7 +43,7 @@ sleep 1.1
43
43
 
44
44
  As you can see in the [shared-state example](introduction.md#example), creating Ractors ad hoc because you want to run something in parallel is quite expensive.
45
45
 
46
- Use `Farce.in_parallel` to schedule parallel work. On CRuby, it manages a shared [Ractor pool](#ractor-pools), starting workers when the current ones cannot keep up with the load, up to the number of CPU cores. It shuts unused workers down after some inactivity. On JRuby and TruffleRuby, it starts a new thread for each task.
46
+ Use `Farce.in_parallel` to schedule parallel work. On CRuby, it manages a shared [Ractor pool](#ractor-pools), starting workers when the current ones cannot keep up with the load, up to the number of CPU cores by default. It shuts unused workers down after some inactivity. On JRuby and TruffleRuby, it starts a new thread for each task.
47
47
 
48
48
  ```ruby
49
49
  input = "this is my input"
@@ -52,6 +52,21 @@ Farce.in_parallel(input) { expensive_computation(it) }
52
52
 
53
53
  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.
54
54
 
55
+ Configure the CRuby pool before creating any scheduler:
56
+
57
+ ```ruby
58
+ Farce.config do |config|
59
+ config.parallel_pool_min_size = 0
60
+ config.parallel_pool_max_size = 8
61
+ config.parallel_pool_max_inflight = 64
62
+ config.parallel_pool_grow_after = 0.005
63
+ config.parallel_pool_shrink_after = 30
64
+ config.parallel_pool_capacity = 1024
65
+ end
66
+ ```
67
+
68
+ These are the defaults, except `parallel_pool_max_size` defaults to `nil` to use the CPU count. Set `parallel_pool_capacity` to `nil` for an unbounded queue, `parallel_pool_max_inflight` to `nil` to disable the per-worker task limit, or `parallel_pool_shrink_after` to `nil` to retain idle workers. A positive `parallel_pool_min_size` starts workers when the pool is created. These options are ignored on JRuby and TruffleRuby.
69
+
55
70
  ## `Farce.schedule`
56
71
 
57
72
  `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?
@@ -98,6 +113,23 @@ Scheduler instances are Ractor-shareable.
98
113
 
99
114
  `Farce::Pool` implements the same scheduling interface, but manages a pool of Ractors. This is what [`in_parallel`](#farcein_parallel) uses under the hood.
100
115
 
116
+ You can change a pool's settings while it runs:
117
+
118
+ ```ruby
119
+ pool = Farce::Pool.new
120
+ pool.max_size = 8
121
+ pool.min_size = 2
122
+ pool.max_inflight = 32
123
+ pool.grow_after = 0.01
124
+ pool.shrink_after = 10
125
+ pool.capacity = 2048
126
+ ```
127
+
128
+ Increasing `min_size` starts workers immediately. Lowering `max_size` retires excess workers as they become idle. Busy workers continue servicing queued work so running tasks can wait for tasks queued on the same pool. The size can remain above the new maximum while workers stay busy. Running tasks are never interrupted. `min_size` must not exceed `max_size`. Changes to growth and idle delays recheck queued work and restart idle waits. Setting `max_inflight` or `shrink_after` to `nil` disables that limit.
129
+
130
+ On CRuby, these settings are also available through `Farce.in_parallel`, such as `Farce.in_parallel.max_size = 8`. Its initial values come from `Farce::Config`. `Farce::Unshared::Pool` supports the same setters. Changes to `Farce::Local::Pool` affect only the current scope. New scopes retain the constructor settings.
131
+
132
+
101
133
  ```ruby
102
134
  # At least four Ractors, up to ten. Launch a new one if a task waits longer than 100 milliseconds.
103
135
  pool = Farce::Pool.new(min_size: 4, max_size: 10, grow_after: 0.1)
@@ -167,9 +199,9 @@ Farce.in_parallel do
167
199
  end
168
200
  ```
169
201
 
170
- 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`.
202
+ 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`.
171
203
 
172
- 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`.
204
+ Note that most of these are outdated and don't work with any recent Ruby version, with the notable exception of `async`, `carbon_fiber`, and `itsi-scheduler`.
173
205
 
174
206
  ## Read Next
175
207
 
data/docs/scopes.md CHANGED
@@ -238,7 +238,7 @@ Pass the needed data as task arguments when crossing an execution boundary. Use
238
238
 
239
239
  ## Classes that support scopes
240
240
 
241
- The classes in this table accept the same five scopes. The scope determines which backing container or resource an operation uses. Each class retains its own behavior, such as queue ordering or weak references.
241
+ The classes and modules in this table accept the same five scopes. The scope determines which backing container or resource an operation uses. Each class retains its own behavior, such as queue ordering or weak references.
242
242
 
243
243
  | Class | What each scope gets |
244
244
  | --- | --- |
@@ -259,8 +259,10 @@ The classes in this table accept the same five scopes. The scope determines whic
259
259
  | `Farce::Local::WeakSet` | A set that retains its elements weakly. |
260
260
  | `Farce::Local::Vector` | An indexed collection. |
261
261
  | `Farce::Local::Queue` | A FIFO queue with independent capacity and lifecycle. |
262
+ | `Farce::Local::Publisher` | An independent set of subscriptions. |
262
263
  | `Farce::Local::PriorityQueue` | A queue ordered by priority. |
263
264
  | `Farce::Local::TimerQueue` | A queue whose values become available at scheduled times. |
265
+ | `Farce::Memoize` | A cached result per instance and method. Also accepts `:global` (the default) for a shared cache or `:unshared` for a cache within one Ractor. |
264
266
  | `Farce::Local::Lazy` | A value computed on first access in that scope. |
265
267
  | `Farce::Local::LazyRef` | A reference that delegates to a scoped lazy value. |
266
268
  | `Farce::Local::Lease` | An independently initialized resource to borrow. |
data/docs/shims.md CHANGED
@@ -139,6 +139,59 @@ Farce implements a version based on weak [atoms](data-structures.md#atoms). Not
139
139
 
140
140
  `Farce::Resolv` is a version of Ruby's [`Resolv`](https://docs.ruby-lang.org/en/4.0/Resolv.html), using [counters](data-structures.md#counters) instead of a global instance variable, and can therefore be used outside the main Ractor.
141
141
 
142
+ ## Concurrent
143
+
144
+ `Farce::Concurrent` includes the following classes:
145
+
146
+ Class | Description
147
+ ------------------|---------------------------------------------------------------
148
+ `Array` | Alias for `Farce::Vector`
149
+ `Atom` | Alias for `Farce::Atom`
150
+ `AtomicBoolean` | Alias for `Farce::Flag`
151
+ `AtomicFixnum` | Alias for `Farce::Counter`
152
+ `CountDownLatch` | Drop-in replacement for `concurrent-ruby`'s `CountDownLatch`
153
+ `CyclicBarrier` | Replacement for `concurrent-ruby`'s `CyclicBarrier`
154
+ `Event` | Drop-in replacement for `concurrent-ruby`'s `Event`
155
+ `Exchanger` | Alias for `Farce::Exchanger`
156
+ `FiberLocalVar` | Drop-in replacement for `concurrent-ruby`'s `FiberLocalVar`
157
+ `Hash` | Alias for `Farce::Map`
158
+ `Lock` | Alias for `Farce::Lock`
159
+ `LockLocalVar` | Drop-in replacement for `concurrent-ruby`'s `LockLocalVar`
160
+ `ReadWriteLock` | Alias for `Farce::ReadWriteLock`
161
+ `ThreadLocalVar` | Drop-in replacement for `concurrent-ruby`'s `ThreadLocalVar`
162
+ `Tuple` | Replacement for `concurrent-ruby`'s `Tuple`
163
+
164
+ The nice thing is that these are either always Ractor-shareable, or in the case of `FiberLocalVar` and `ThreadLocalVar`, are Ractor-shareable if their default values or blocks are.
165
+
166
+ For instance, you can replace threads with ractors in concurrent-ruby's own `CountDownLatch` example:
167
+
168
+ ```ruby
169
+ latch = Farce::Concurrent::CountDownLatch.new(3)
170
+
171
+ waiter = Ractor.new(latch) do |latch|
172
+ latch.wait
173
+ puts "Waiter released"
174
+ end
175
+
176
+ decrementer = Ractor.new(latch) do |latch|
177
+ sleep 1
178
+ latch.count_down
179
+ puts latch.count
180
+
181
+ sleep 1
182
+ latch.count_down
183
+ puts latch.count
184
+
185
+ sleep 1
186
+ latch.count_down
187
+ puts latch.count
188
+ end
189
+
190
+ waiter.join
191
+ decrementer.join
192
+
193
+ ```
194
+
142
195
  ## Read Next
143
196
 
144
197
  [README](../README.md#features) · [Sharing Unshareable Data](sharing.md) · [Scheduling Code](scheduling.md)
data/docs/utilities.md CHANGED
@@ -81,6 +81,37 @@ And also mixins to prevent them from being shareable:
81
81
 
82
82
  `Unshareable::Copyable` and `Unshareable::Movable` may be combined.
83
83
 
84
+ ## Memoization
85
+
86
+ `Farce::Memoize` offers Ractor-friendly memoization:
87
+
88
+ ```ruby
89
+ class MyClass
90
+ include Farce::Memoize
91
+
92
+ # Memoize #expensive_method, shared across Ractors
93
+ memoize def expensive_method = expensive_computation
94
+
95
+ # Memoize #method_with_unsharable_value for each Ractor separately
96
+ def method_with_unsharable_value = unsharable_computation
97
+ memoize :method_with_unsharable_value, scope: :ractor
98
+
99
+ private
100
+
101
+ # The memoized method will also be private
102
+ memoize def private_method = expensive_computation
103
+ end
104
+ ```
105
+
106
+ Or as a one-liner:
107
+
108
+ ```ruby
109
+ class MyClass
110
+ # Memoize return values of #foo and #bar until they get garbage collected
111
+ include Farce::Memoize[:foo, :bar, weak: true]
112
+ end
113
+ ```
114
+
84
115
  ## Constants
85
116
 
86
117
  * `Farce::MODES`: List of supported [sharing modes](sharing.md#sharing-modes).
data/docs/variants.md CHANGED
@@ -201,6 +201,15 @@ end
201
201
  <td align="center" style="text-align: center">🔗</td> <!-- Unsafe -->
202
202
  <td align="center" style="text-align: center">➖</td> <!-- Transaction -->
203
203
  </tr>
204
+ <tr>
205
+ <td><tt>Publisher</tt></td>
206
+ <td align="center" style="text-align: center">✅</td> <!-- Farce -->
207
+ <td align="center" style="text-align: center">✅</td> <!-- Strict -->
208
+ <td align="center" style="text-align: center">✅</td> <!-- Local -->
209
+ <td align="center" style="text-align: center">✅</td> <!-- Unshared -->
210
+ <td align="center" style="text-align: center">🔗</td> <!-- Unsafe -->
211
+ <td align="center" style="text-align: center">➖</td> <!-- Transaction -->
212
+ </tr>
204
213
  <tr>
205
214
  <td><tt>Queue</tt></td>
206
215
  <td align="center" style="text-align: center">✅</td> <!-- Farce -->
data/ext/farce/farce.c CHANGED
@@ -306,6 +306,14 @@ containers_raise_key_error(VALUE receiver, VALUE key)
306
306
  rb_exc_raise(exception);
307
307
  }
308
308
 
309
+ static VALUE
310
+ farce_value_changed(VALUE namespace, VALUE current, VALUE expected, VALUE identity)
311
+ {
312
+ if (current == expected) return Qfalse;
313
+ if (RTEST(identity)) return identity;
314
+ return RTEST(rb_equal(current, expected)) ? Qfalse : Qtrue;
315
+ }
316
+
309
317
  static VALUE
310
318
  farce_futex_supported(VALUE namespace)
311
319
  {
@@ -336,6 +344,7 @@ Init_farce(void)
336
344
  VALUE mInternal = rb_const_get(mFarce, rb_intern("Internal"));
337
345
  VALUE singleton = rb_singleton_class(mInternal);
338
346
  eIsolationError = rb_const_get(rb_cRactor, rb_intern("IsolationError"));
347
+ rb_define_singleton_method(mInternal, "value_changed?", farce_value_changed, 3);
339
348
  #if RUBY_API_VERSION_CODE == 30400
340
349
  rb_define_singleton_method(mInternal, "lock_native_thread", farce_lock_native_thread, 0);
341
350
  #endif
data/ext/farce/map.c CHANGED
@@ -1417,9 +1417,15 @@ map_modify_body(VALUE opaque)
1417
1417
  }
1418
1418
 
1419
1419
  static VALUE
1420
- map_modify(VALUE self, VALUE key)
1420
+ map_modify(int argc, VALUE *argv, VALUE self)
1421
1421
  {
1422
1422
  rb_check_frozen(self);
1423
+ VALUE key;
1424
+ VALUE keywords = Qnil;
1425
+ VALUE keyword_values[1] = {Qundef};
1426
+ ID keyword_ids[] = {rb_intern("timeout")};
1427
+ rb_scan_args(argc, argv, "1:", &key, &keywords);
1428
+ if (!NIL_P(keywords)) rb_get_kwargs(keywords, keyword_ids, 0, 1, keyword_values);
1423
1429
  rb_need_block();
1424
1430
  map_t *map = get_map(self);
1425
1431
  key = map_prepare_key(map, key);
@@ -1432,7 +1438,12 @@ map_modify(VALUE self, VALUE key)
1432
1438
  };
1433
1439
  operation.hash = map_key_hash(map, key);
1434
1440
  rb_check_frozen(self);
1435
- map_lock_for_key(map, key, operation.hash, &operation.execution);
1441
+ map_timeout_t timeout = map_parse_timeout(
1442
+ keyword_values[0] == Qundef ? Qnil : keyword_values[0]
1443
+ );
1444
+ if (!map_lock_for_key_with_timeout(
1445
+ map, key, operation.hash, &timeout, &operation.execution
1446
+ )) return Qfalse;
1436
1447
  bool found;
1437
1448
  size_t index = map_find_slot(map, key, operation.hash, &found, &operation.execution);
1438
1449
  if (RB_OBJ_FROZEN(self)) {
@@ -2151,7 +2162,7 @@ map_define_methods(VALUE klass)
2151
2162
  rb_define_method(klass, "store", map_store_with_timeout, -1);
2152
2163
  rb_define_method(klass, "swap", map_swap, -1);
2153
2164
  rb_define_method(klass, "update", map_update, -1);
2154
- rb_define_method(klass, "modify", map_modify, 1);
2165
+ rb_define_method(klass, "modify", map_modify, -1);
2155
2166
  rb_define_method(klass, "wait_until_changed", map_wait_until_changed, -1);
2156
2167
  rb_define_method(klass, "wait_until_non_nil", map_wait_until_non_nil, -1);
2157
2168
  rb_define_method(klass, "store_if_absent", map_store_if_absent, -1);