farce 0.4.1-java → 0.5.0-java

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 (98) 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/lib/farce/abstract/concurrent_map.rb +122 -0
  16. data/lib/farce/abstract/counter.rb +35 -1
  17. data/lib/farce/abstract/executor.rb +30 -7
  18. data/lib/farce/abstract/flag.rb +12 -0
  19. data/lib/farce/abstract/pool.rb +158 -47
  20. data/lib/farce/abstract/priority_queue.rb +1 -1
  21. data/lib/farce/abstract/publisher.rb +189 -0
  22. data/lib/farce/abstract/queue.rb +29 -3
  23. data/lib/farce/abstract/scheduler.rb +41 -14
  24. data/lib/farce/abstract/timer_queue.rb +3 -3
  25. data/lib/farce/abstract/vector.rb +3 -1
  26. data/lib/farce/atom.rb +9 -2
  27. data/lib/farce/clock.rb +12 -0
  28. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  29. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  30. data/lib/farce/concurrent/event.rb +60 -0
  31. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  32. data/lib/farce/concurrent/local_var.rb +65 -0
  33. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  34. data/lib/farce/concurrent/tuple.rb +74 -0
  35. data/lib/farce/concurrent.rb +50 -0
  36. data/lib/farce/config.rb +112 -2
  37. data/lib/farce/counter.rb +0 -16
  38. data/lib/farce/engine/jruby/map.rb +8 -6
  39. data/lib/farce/engine/jruby/signal.rb +2 -2
  40. data/lib/farce/engine/jvm/farce.jar +0 -0
  41. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  42. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  43. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  44. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  45. data/lib/farce/engine/shared/exchanger.rb +2 -2
  46. data/lib/farce/engine/shared/queue.rb +43 -5
  47. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  48. data/lib/farce/engine/shared/strict_map.rb +5 -0
  49. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  50. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  51. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  52. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  53. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  54. data/lib/farce/engine/truffleruby/map.rb +8 -6
  55. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  56. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  57. data/lib/farce/integrations/concurrent.rb +16 -3
  58. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  59. data/lib/farce/internal/flag_waiting.rb +1 -1
  60. data/lib/farce/internal/lease_initialization.rb +1 -1
  61. data/lib/farce/internal/lease_pool_state.rb +1 -1
  62. data/lib/farce/internal/lease_state.rb +2 -2
  63. data/lib/farce/internal/local_executor_methods.rb +3 -3
  64. data/lib/farce/internal/map_value_modes.rb +9 -2
  65. data/lib/farce/internal/pool_settings.rb +9 -0
  66. data/lib/farce/internal/pool_worker.rb +34 -21
  67. data/lib/farce/internal/reservation_waiting.rb +1 -1
  68. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  69. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  70. data/lib/farce/internal.rb +8 -8
  71. data/lib/farce/lazy.rb +3 -0
  72. data/lib/farce/lazy_ref.rb +3 -0
  73. data/lib/farce/local/lease.rb +2 -9
  74. data/lib/farce/local/lease_map.rb +2 -9
  75. data/lib/farce/local/pool.rb +38 -5
  76. data/lib/farce/local/publisher.rb +30 -0
  77. data/lib/farce/local/scheduler.rb +18 -5
  78. data/lib/farce/local/vector.rb +3 -2
  79. data/lib/farce/memoize.rb +314 -0
  80. data/lib/farce/pool.rb +5 -0
  81. data/lib/farce/priority_queue.rb +1 -1
  82. data/lib/farce/publisher.rb +57 -0
  83. data/lib/farce/signal.rb +1 -1
  84. data/lib/farce/strict/publisher.rb +31 -0
  85. data/lib/farce/strict/vector.rb +3 -2
  86. data/lib/farce/subscription/definitions.rb +27 -0
  87. data/lib/farce/subscription.rb +121 -0
  88. data/lib/farce/transaction/map.rb +35 -1
  89. data/lib/farce/unshared/pool.rb +1 -1
  90. data/lib/farce/unshared/publisher.rb +25 -0
  91. data/lib/farce/unshared/scheduler.rb +3 -3
  92. data/lib/farce/unshared/vector.rb +3 -2
  93. data/lib/farce/vector.rb +12 -9
  94. data/lib/farce/version.rb +1 -1
  95. data/lib/farce/walker/definitions.rb +13 -2
  96. data/lib/farce/weak_atom.rb +5 -2
  97. data/lib/farce.rb +3 -1
  98. metadata +19 -2
@@ -56,6 +56,7 @@ module Farce
56
56
  def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, scope: :ractor,
57
57
  worker: nil, &constructor)
58
58
  @constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
59
+ @capacity = capacity && Integer(capacity)
59
60
  @backend = backend
60
61
  @worker = worker
61
62
  @schedulers = Strict::Map.new
@@ -65,15 +66,26 @@ module Farce
65
66
  super(scope:, capacity: capacity && Integer(capacity), backend:)
66
67
  end
67
68
 
69
+ # Pending-task capacity in this scope. New scopes retain the constructor value.
70
+ def capacity = (scheduler = current_scheduler) ? scheduler.capacity : @capacity
71
+
72
+ # Resize the queue in this scope without starting a worker.
73
+ def capacity=(value)
74
+ raise SchedulerClosedError, "cannot configure a closed scheduler" if @closed_all.value
75
+ scoped_value.capacity = value
76
+ end
77
+
68
78
  # Enqueue a task in the current scope, preserving its block and arguments.
69
79
  # `mode` is ignored when `auto_local` is true. Otherwise, only local and smart modes are accepted.
70
- # @overload schedule(*args, mode: :smart, auto_local: true)
80
+ # @overload schedule(*args, mode: :smart, auto_local: true, timeout: nil)
71
81
  # @param (see Farce::Abstract::Executor#schedule)
72
- # @return [self]
82
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
73
83
  def schedule(...) = enqueue_task(false, ...)
74
84
 
75
85
  # (see Abstract::Scheduler#try_schedule)
76
- def try_schedule(...) = enqueue_task(true, ...)
86
+ def try_schedule(*, mode: :smart, auto_local: true, &)
87
+ enqueue_task(true, *, mode:, auto_local:, &)
88
+ end
77
89
 
78
90
  # Starts a worker for the current scope. Other scopes retain their creation behavior.
79
91
  # @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
@@ -147,12 +159,13 @@ module Farce
147
159
 
148
160
  private
149
161
 
150
- def enqueue_task(non_block, *, mode: :smart, auto_local: true, &block)
162
+ def enqueue_task(non_block, *, mode: :smart, auto_local: true, timeout: nil, &block)
151
163
  raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if @closed_all.value
152
164
  scheduler = scoped_value
153
165
  raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if scheduler.closed?
154
166
  raise LocalJumpError, "Cannot schedule without a block" unless block
155
167
 
168
+ deadline = scheduling_deadline(timeout)
156
169
  validate_local_mode(mode, auto_local)
157
170
  register_scheduler(scheduler)
158
171
 
@@ -160,7 +173,7 @@ module Farce
160
173
  if non_block
161
174
  scheduler.try_schedule(*, mode:, auto_local:, &block)
162
175
  else
163
- scheduler.schedule(*, mode:, auto_local:, &block)
176
+ scheduler.schedule(*, mode:, auto_local:, timeout: Clock.remaining(deadline), &block)
164
177
  end
165
178
 
166
179
  submitted && self
@@ -8,10 +8,11 @@ module Farce
8
8
  #
9
9
  # @!method initialize(source = nil, compare_by_identity: false, scope: :ractor)
10
10
  # @!macro scopes
11
- # @param source [Array, nil] the initial values
11
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil.
12
12
  # @param compare_by_identity [Boolean] whether values are compared by identity
13
13
  # @param scope [Symbol] the scope of the vector
14
14
  # @return [Vector]
15
+ # @raise [ArgumentError] If the size is negative.
15
16
  class Vector < Abstract::Vector
16
17
  include Shareable::Tracked
17
18
  include Scoped::Tracked
@@ -24,7 +25,7 @@ module Farce
24
25
  self.class.new(values, compare_by_identity: compare_by_identity?, scope:)
25
26
  end
26
27
 
27
- private def new_scoped_value(...) = Internal::UnsharedVector.new(...)
28
+ private def new_scoped_value(source = nil, **) = Internal::UnsharedVector.new(normalize_vector_source(source), **)
28
29
  end
29
30
  end
30
31
  end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # Ractor-friendly memoization module.
7
+ #
8
+ # Replaces the following pattern:
9
+ #
10
+ # ```ruby
11
+ # class MyClass
12
+ # def time
13
+ # @time ||= Time.now
14
+ # end
15
+ # end
16
+ # ```
17
+ #
18
+ # Using `Farce::Memoize` instead gives you:
19
+ #
20
+ # * Allows memoization after freezing
21
+ # * Is Ractor-safe (with optional scoping or transfer modes) and thread-safe
22
+ # * Adds one instance variable per memoized method (avoids hash lookup and doesn't bust Ruby's data structures)
23
+ # * Is Marshal-dump/load friendly
24
+ # * Allows weak memoization, where the memoized value is kept until it is garbage collected
25
+ # * Does not need to run code on the main Ractor for memoization
26
+ #
27
+ # Methods with arguments cannot be memoized. Explicitly use a {Farce::Abstract::Map map} instead.
28
+ #
29
+ # @example Using `memoize` method
30
+ # class MyClass
31
+ # include Farce::Memoize
32
+ # memoize def time = Time.now
33
+ # end
34
+ #
35
+ # instance = MyClass.new
36
+ # first_time = instance.time
37
+ # second_time = instance.time
38
+ #
39
+ # first_time == second_time # => true
40
+ # first_time == MyClass.new.time # => false
41
+ #
42
+ # @example Scope-specific memoization
43
+ # class MyClass
44
+ # include Farce::Memoize
45
+ #
46
+ # def first = expensive_computation(0)
47
+ # def last = expensive_computation(-1)
48
+ #
49
+ # # Memoize separately per Ractor
50
+ # memoize :first, :last, scope: :ractor
51
+ # end
52
+ #
53
+ # @example Setting a mode
54
+ # class MyClass
55
+ # include Farce::Memoize
56
+ # memoize :expensive, mode: :make_shareable
57
+ # def expensive = expensive_computation
58
+ # end
59
+ #
60
+ # @example Without `memoize` extension
61
+ # class MyClass
62
+ # include Farce::Memoize[:time]
63
+ # def time = Time.now
64
+ # end
65
+ #
66
+ # @see Farce::Lazy
67
+ # @see Farce::LazyRef
68
+ #
69
+ # @!macro memoize_params
70
+ # @overload $0(*methods, scope: :global, mode: :smart, weak: false, visibility: nil)
71
+ # @param methods [Array<Symbol>]
72
+ # The methods to memoize
73
+ #
74
+ # @param scope [Symbol]
75
+ # The scope of the memoization.
76
+ # In addition to the {Farce::SCOPES normal scopes} it may also be set to `:global` or `:unshared`.
77
+ #
78
+ # @param mode [Symbol]
79
+ # The mode of the memoization. Ignored for any scope other than `:global`.
80
+ # Defaults to `:raise` with `weak: true`, which supports only `:raise`, `:make_shareable`, and `:dedup`.
81
+ #
82
+ # @param weak [Boolean]
83
+ # Whether to use weak memoization.
84
+ # If enabled, memoized values may be garbage collected when no longer referenced.
85
+ # In this case, next access to the memoized method may recompute the value.
86
+ #
87
+ # @param visibility [Symbol, nil]
88
+ # The visibility of the memoized methods.
89
+ # May be one of `:public`, `:protected`, or `:private`.
90
+ # If set to `nil`, {Farce::Memoize#memoize} will attempt to infer the visibility based on the context.
91
+ # Other methods will default to `:public`.
92
+ module Memoize
93
+ # A named module preserves the nil marker's identity across Marshal round trips.
94
+ NIL_VALUE = Module.new.freeze
95
+ private_constant :NIL_VALUE
96
+
97
+ # Mixins used to implement memoization for different methods, scopes, and modes
98
+ # @see Farce::Memoize
99
+ class Mixin < Module
100
+ KNOWN = Strict::Map.new
101
+ private_constant :KNOWN
102
+
103
+ # @!macro memoize_params
104
+ # Creates or retrieves a memoization mixin for the given methods and options.
105
+ # @return [Mixin] the memoization mixin
106
+ def self.new(*methods, **options)
107
+ return super unless self == Mixin
108
+ methods = Farce.dedup(methods.map(&:to_sym).uniq.sort)
109
+ options = Farce.dedup(normalize_options(options))
110
+ map = KNOWN.store_if_absent(options) { Strict::Map.new }
111
+ map.store_if_absent(methods) { super(methods, **options) }
112
+ end
113
+
114
+ # @api private
115
+ def self.normalize_options(options)
116
+ options[:visibility] &&= options[:visibility].to_sym
117
+
118
+ options[:scope] ||= :global
119
+ options[:visibility] ||= :public
120
+ options[:weak] ||= false
121
+
122
+ if options[:scope] == :global
123
+ options[:mode] ||= options[:weak] ? :raise : :smart
124
+ else
125
+ options.delete(:mode)
126
+ end
127
+
128
+ options
129
+ end
130
+ private_class_method :normalize_options
131
+
132
+ # @return [Array<Symbol>] the memoized methods
133
+ attr_reader :memoized_methods
134
+
135
+ # @return [Symbol] the scope of the memoization
136
+ attr_reader :scope
137
+
138
+ # @return [Symbol, nil] the transfer mode, or nil for a scoped cache
139
+ attr_reader :mode
140
+
141
+ # @return [Symbol] the visibility of the memoized methods
142
+ attr_reader :visibility
143
+
144
+ # @api private
145
+ def initialize(methods, scope:, weak:, mode: nil, visibility: :public)
146
+ @memoized_methods = methods
147
+ @scope = scope
148
+ @mode = mode
149
+ @weak = !!weak # rubocop:disable Style/DoubleNegation
150
+ @visibility = visibility
151
+
152
+ super()
153
+
154
+ unless %i[public protected private].include?(visibility)
155
+ raise ArgumentError, "unsupported visibility: #{visibility.inspect}"
156
+ end
157
+
158
+ atom_class = weak ? "WeakAtom" : "Atom"
159
+
160
+ case scope
161
+ when :global
162
+ modes = weak ? Internal::WeakModeManager::MODES : MODES
163
+ raise ArgumentError, "unsupported mode: #{mode.inspect}" unless modes.include?(mode)
164
+ init = "::Farce::#{atom_class}.new(mode: #{mode.inspect})"
165
+ args = methods.join(", ")
166
+ args += ", mode: #{mode.inspect}" unless mode == :smart
167
+ when :unshared
168
+ init = "::Farce::Unshared::#{atom_class}.new"
169
+ args = "#{methods.join(", ")}, scope: :unshared"
170
+ when *SCOPES
171
+ init = "::Farce::Local::#{atom_class}.new(scope: #{scope.inspect})"
172
+ args = "#{methods.join(", ")}, scope: #{scope.inspect}"
173
+ else
174
+ raise ArgumentError, "unsupported scope: #{scope.inspect}"
175
+ end
176
+
177
+ set_temporary_name(-"Farce::Memoize[#{args}]") if instance_of?(Mixin)
178
+ initializer = +""
179
+ # Overlapping mixins need separate slots. Stable names also survive Marshal across processes.
180
+ slot = "#{methods.join(",").unpack1("H*")}_#{scope}_#{mode}_#{@weak}_#{visibility}"
181
+
182
+ methods.each do |method|
183
+ unless method =~ /\A[a-z_]\w*[!?]?\z/i && method != :initialize
184
+ raise ArgumentError, "unsuitable method name: #{method.inspect}"
185
+ end
186
+ ivar = "@_#{method.to_s.unpack1("H*")}_memoize_#{slot}_atom"
187
+ initializer << " #{ivar} = #{init}\n"
188
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
189
+ #{visibility} def #{method}
190
+ value = #{ivar}.store_if_absent do
191
+ result = super()
192
+ nil.equal?(result) ? NIL_VALUE : result
193
+ end
194
+ value unless NIL_VALUE.equal?(value)
195
+ end
196
+ RUBY
197
+ end
198
+
199
+ # Explicit forwarding preserves nested constructor keywords on JRuby.
200
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
201
+ def initialize(*, **, &)
202
+ #{initializer}
203
+ super
204
+ end
205
+ RUBY
206
+ end
207
+
208
+ # @return [Boolean] whether the memoization is weak
209
+ def weak? = @weak
210
+
211
+ # allow `include Farce::Memoize[...]` to work like `prepend`
212
+ private def append_features(base) = base.prepend(self)
213
+ end
214
+
215
+ # @api private
216
+ def self.append_features(base) = base.extend(self)
217
+ private_class_method :append_features
218
+
219
+ # Creates or retrieves the memoization mixin for the given methods, scope, and mode
220
+ # @!macro memoize_params
221
+ # @return [Mixin] memoization mixin for the given methods, scope, and mode
222
+ def self.[](...) = Mixin.new(...)
223
+
224
+ # @!macro memoize_params
225
+ # Creates a memoization mixin for the given arguments and adds it to the current class.
226
+ #
227
+ # Can be used as a prefix to `def`:
228
+ #
229
+ # ```ruby
230
+ # memoize def some_method = expensive_computation
231
+ # ```
232
+ #
233
+ # Or called separately with the method names, useful for passing additional options:
234
+ #
235
+ # ```ruby
236
+ # def some_method = expensive_computation
237
+ #
238
+ # # memoize some_method per thread
239
+ # memoize :some_method, scope: :thread
240
+ # ```
241
+ #
242
+ # ## Visibility of Memoized Methods
243
+ #
244
+ # If no visibility is provided, it will determine the visibility based on the existing method definitions.
245
+ # If none exist, it will default to public. This happens when `memoize` is called.
246
+ # Make sure you set the visibility before calling this method.
247
+ #
248
+ # This works:
249
+ #
250
+ # ```ruby
251
+ # memoize private def some_method = expensive_computation
252
+ # ```
253
+ #
254
+ # As does this:
255
+ #
256
+ # ```ruby
257
+ # private
258
+ # memoize def some_method = expensive_computation
259
+ # ```
260
+ #
261
+ # Or this:
262
+ #
263
+ # ```ruby
264
+ # private def some_method = expensive_computation
265
+ # memoize :some_method
266
+ # ```
267
+ #
268
+ # But this doesn't, as the mixin's method will end up public:
269
+ #
270
+ # ```ruby
271
+ # private memoize def some_method = expensive_computation
272
+ # ```
273
+ #
274
+ # Same for this one, as the mixin's method will also end up public:
275
+ #
276
+ # ```ruby
277
+ # memoize :some_method
278
+ # private def some_method = expensive_computation
279
+ # ```
280
+ #
281
+ # You can of course set visibility explicitly when calling `memoize`:
282
+ #
283
+ # ```ruby
284
+ # memoize :some_method, visibility: :private
285
+ # ```
286
+ #
287
+ # @return [Array<Symbol>] the list of memoized methods
288
+ def memoize(*methods, visibility: nil, warn: true, **)
289
+ if visibility.nil?
290
+ groups = methods.group_by do |method|
291
+ next :protected if protected_method_defined?(method)
292
+ next :private if private_method_defined?(method)
293
+ :public
294
+ end
295
+ return groups.flat_map { memoize(*_2, visibility: _1, warn:, **) }
296
+ end
297
+
298
+ if warn
299
+ methods.each do |name|
300
+ method = instance_method(name)
301
+ next if method.parameters.empty?
302
+ warn "#{method.owner}##{name} expects parameters and may be unsuitable for memoization"
303
+ rescue NameError => e
304
+ raise e unless e.name == name.to_sym
305
+ end
306
+ end
307
+
308
+ mixin = Farce::Memoize[*methods, visibility:, **]
309
+ prepend mixin
310
+
311
+ mixin.memoized_methods
312
+ end
313
+ end
314
+ end
data/lib/farce/pool.rb CHANGED
@@ -11,6 +11,11 @@ module Farce
11
11
  # `min_size` retire after `shrink_after` without queued or running work.
12
12
  # By default, the pool starts with no workers and can shrink back to zero.
13
13
  #
14
+ # Worker limits, task limits, scaling delays, and queue capacity can change while
15
+ # the pool runs. Lowering `max_size` retires extra workers as they become idle.
16
+ # Busy workers keep queued work runnable. Raising `min_size` starts workers
17
+ # immediately. Queued tasks are retained.
18
+ #
14
19
  # Most importantly, it exposes a {#schedule} method compatible with {Scheduler#schedule}.
15
20
  #
16
21
  # @example
@@ -42,7 +42,7 @@ module Farce
42
42
  result = @reverse_order ? @queue.pop_last { empty = true } : @queue.pop { empty = true }
43
43
 
44
44
  return @manager.unwrap(result) unless empty
45
- remaining = remaining_timeout(deadline)
45
+ remaining = Clock.remaining(deadline)
46
46
  return block_given? ? yield : nil unless wait_pop(timeout: remaining)
47
47
  end
48
48
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # A message multiplexer.
7
+ # Implements the publisher-subscriber protocol.
8
+ #
9
+ # @example
10
+ # publisher = Farce::Publisher.new
11
+ #
12
+ # # Count how many messages have been received
13
+ # counter = Farce::Counter.new
14
+ # publisher.subscribe(counter)
15
+ #
16
+ # # Trigger a callback whenever a message is published
17
+ # publisher.subscribe do |message|
18
+ # puts "Received message: #{message}"
19
+ # end
20
+ #
21
+ # # Send messages to a queue
22
+ # queue = Farce::Queue.new
23
+ # publisher.subscribe(queue)
24
+ #
25
+ # # Keep track of the last published value
26
+ # atom = Farce::Atom.new
27
+ # publisher.subscribe(atom)
28
+ #
29
+ # # Send messages to a Ractor
30
+ # ractor = Ractor.new do
31
+ # while message = Ractor.receive
32
+ # puts "Received message in Ractor: #{message}"
33
+ # end
34
+ # end
35
+ # publisher.subscribe(ractor)
36
+ #
37
+ # # Publish some messages
38
+ # publisher.publish("Hello, world!")
39
+ # publisher.publish("Another message")
40
+ #
41
+ # queue.pop # => "Hello, world!"
42
+ # atom.value # => "Another message"
43
+ # counter.value # => 2
44
+ class Publisher < Farce::Abstract::Publisher
45
+ include Shareable::Unfreezable
46
+
47
+ # @overload initialize(*subscribers)
48
+ # (see Farce::Abstract#initialize)
49
+ def initialize(*subscribers)
50
+ @map = Strict::Map.new
51
+ super()
52
+ subscribers.each { subscribe(it) }
53
+ end
54
+
55
+ private attr_reader :map
56
+ end
57
+ end
data/lib/farce/signal.rb CHANGED
@@ -80,7 +80,7 @@ module Farce
80
80
  result = yield
81
81
  return result if result
82
82
 
83
- remaining = deadline - Clock.now if deadline
83
+ remaining = Clock.remaining(deadline)
84
84
  return if remaining && !remaining.positive?
85
85
  return unless wait(observed, timeout: remaining)
86
86
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Strict
7
+ # Strict variant of {Farce::Publisher}.
8
+ class Publisher < Farce::Abstract::Publisher
9
+ include Shareable::Unfreezable
10
+
11
+ # @overload initialize(*subscribers)
12
+ # (see Farce::Abstract#initialize)
13
+ def initialize(*subscribers)
14
+ @map = Strict::Map.new
15
+ super()
16
+ subscribers.each { subscribe(it) }
17
+ end
18
+
19
+ # (see Farce::Abstract::Publisher#publish)
20
+ def publish(message = nil)
21
+ return super if Ractor.shareable?(message)
22
+ raise Ractor::IsolationError, "value must be Ractor-shareable"
23
+ end
24
+ alias send publish
25
+ alias << publish
26
+ alias broadcast publish
27
+
28
+ private attr_reader :map
29
+ end
30
+ end
31
+ end
@@ -8,10 +8,11 @@ module Farce
8
8
  class Vector < Abstract::Vector
9
9
  include Shareable::Delegated
10
10
 
11
- # @param source [Array, nil] Initial values. The source array is not retained.
11
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
12
12
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
13
+ # @raise [ArgumentError] If the size is negative.
13
14
  def initialize(source = nil, compare_by_identity: false)
14
- @vector = Internal::Vector.new(source, compare_by_identity:)
15
+ @vector = Internal::Vector.new(normalize_vector_source(source), compare_by_identity:)
15
16
  super()
16
17
  end
17
18
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ class Subscription
7
+ Farce.on_main do
8
+ ractors = Internal.native_ractors? ? [::Ractor] : [Internal::FakeRactor::Main, Internal::FakeRactor::NotMain]
9
+
10
+ define(Internal::BasePort, *ractors) { subscriber.send(it) }
11
+ define(Abstract::Publisher) { subscriber.publish(it) }
12
+ define(Abstract::Queue) { subscriber.try_push(it) }
13
+ define(Internal::Counter, Local::Counter) { |_| subscriber.increment }
14
+ define(Internal::Flag, Local::Flag) { |_| subscriber.set }
15
+ define(Abstract::Atom) { subscriber.value = it }
16
+ define(Abstract::Set, Abstract::Vector) { subscriber << it }
17
+ define(Signal) { |_| subscriber.broadcast }
18
+
19
+ define(::Thread::Queue) { subscriber.push(it) }
20
+ define(::Thread::SizedQueue) do |payload|
21
+ subscriber.push(payload, true)
22
+ rescue ThreadError
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # A publisher subscription that can be paused or permanently canceled.
7
+ # Disabling skips new messages. Unsubscribing drains an existing callback queue before stopping it.
8
+ class Subscription
9
+ include Internal::Freeze::Unfreezable
10
+ include Internal::Noncopyable
11
+
12
+ # Raised when an operation is attempted on an unsubscribed subscription.
13
+ UnsubscribedError = Class.new(StandardError)
14
+
15
+ REGISTER = ClassMirror.new(self)
16
+ private_constant :REGISTER
17
+
18
+ class << self
19
+ protected :new
20
+ end
21
+
22
+ # Register delivery behavior for subscriber classes from the main Ractor.
23
+ # Subclasses inherit the nearest registered delivery method.
24
+ #
25
+ # @example
26
+ # Farce::Subscription.define(MyInbox) { |payload| subscriber.push(payload) }
27
+ #
28
+ # @param classes [Array<Class>] the subscriber classes to support
29
+ # @yieldparam payload [BasicObject] the published payload
30
+ # @yieldreceiver [Subscription] the subscription whose {#subscriber} receives the payload
31
+ # @note The block must be convertible to a shareable proc.
32
+ # @return [void]
33
+ def self.define(*classes, &)
34
+ raise LocalJumpError, "no block given" unless block_given?
35
+ definition = Internal.prepare_method_definition(&)
36
+ classes.each do |klass|
37
+ REGISTER.define(klass) do
38
+ define_method(:deliver, definition)
39
+ private :deliver
40
+ end
41
+ end
42
+ end
43
+
44
+ # @api private
45
+ def self.build(publisher, subscriber, ...)
46
+ handler = REGISTER[subscriber.class]
47
+ unless handler.private_method_defined?(:deliver)
48
+ raise ArgumentError, "Unsupported subscriber type: #{subscriber.class}"
49
+ end
50
+ handler.new(publisher, subscriber, ...)
51
+ end
52
+
53
+ # @return [BasicObject] the subscriber associated with this subscription
54
+ attr_reader :subscriber
55
+
56
+ # @return [Farce::Abstract::Publisher] the publisher associated with this subscription
57
+ attr_reader :publisher
58
+
59
+ # @api private
60
+ attr_reader :key
61
+
62
+ # @!visibility private
63
+ def initialize(publisher, subscriber, map, callback: false, source: nil)
64
+ # Keep Proc identities alive without making their captured state shareable.
65
+ @source = source && Envelope.new(source, mode: :local)
66
+ @callback = callback
67
+ @enabled = Flag.new(true)
68
+ @key = (source || subscriber).__id__
69
+ @map = map
70
+ @publisher = publisher
71
+ @subscribed = Flag.new(true)
72
+ @subscriber = subscriber
73
+ super()
74
+ __freeze__
75
+ end
76
+
77
+ # @api private
78
+ def publish(payload)
79
+ return unless enabled?
80
+ deliver(payload)
81
+ true
82
+ rescue ::ClosedQueueError, Ractor::ClosedError, FrozenError
83
+ unsubscribe
84
+ false
85
+ end
86
+
87
+ # @return [Boolean] whether the subscription is currently enabled
88
+ def enabled? = @subscribed.value && @enabled.value
89
+
90
+ # @return [Boolean] whether the subscription has not been permanently canceled
91
+ def subscribed? = @subscribed.value
92
+
93
+ # Enables the subscription, allowing it to receive published payloads.
94
+ # @raise [UnsubscribedError] if the subscription has been unsubscribed
95
+ # @return [void]
96
+ def enable
97
+ raise UnsubscribedError, "Cannot enable a subscription that has been unsubscribed" unless @subscribed.value
98
+ @enabled.value = true
99
+ end
100
+
101
+ # Disables the subscription, preventing it from receiving published payloads.
102
+ # @return [void]
103
+ def disable = @enabled.value = false
104
+
105
+ # Removes the subscription permanently, allowing it and the subscriber to be garbage collected.
106
+ # @return [BasicObject, nil] the subscriber, or nil if already unsubscribed
107
+ def unsubscribe
108
+ return unless @subscribed.compare_and_set(true, false)
109
+ @map.delete_pair(key, self)
110
+ subscriber.seal if @callback
111
+ subscriber
112
+ end
113
+ alias cancel unsubscribe
114
+
115
+ private
116
+
117
+ define_method(:__freeze__, Kernel.instance_method(:freeze))
118
+
119
+ require "farce/subscription/definitions"
120
+ end
121
+ end