farce 0.4.1-aarch64-linux-gnu → 0.6.0-aarch64-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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -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 +32 -0
  14. data/docs/variants.md +9 -0
  15. data/ext/farce/atom.c +23 -7
  16. data/ext/farce/containers.h +3 -0
  17. data/ext/farce/counter.c +4 -6
  18. data/ext/farce/depend +3 -1
  19. data/ext/farce/extconf.rb +19 -14
  20. data/ext/farce/farce.c +110 -3
  21. data/ext/farce/map.c +15 -7
  22. data/ext/farce/priority_queue.c +46 -1
  23. data/ext/farce/queue.c +141 -7
  24. data/ext/farce/trie.c +140 -72
  25. data/ext/farce/trie_search.h +114 -0
  26. data/ext/farce/vector.c +53 -21
  27. data/ext/farce/weak_map.c +16 -11
  28. data/ext/farce/weak_references.h +23 -0
  29. data/ext/java/org/farce/FiberScheduler.java +8 -6
  30. data/ext/java/org/farce/PriorityQueue.java +18 -2
  31. data/lib/farce/abstract/atom.rb +5 -1
  32. data/lib/farce/abstract/concurrent_map.rb +122 -0
  33. data/lib/farce/abstract/counter.rb +36 -1
  34. data/lib/farce/abstract/executor.rb +30 -7
  35. data/lib/farce/abstract/flag.rb +12 -0
  36. data/lib/farce/abstract/molecule.rb +14 -14
  37. data/lib/farce/abstract/pool.rb +158 -47
  38. data/lib/farce/abstract/priority_queue.rb +1 -1
  39. data/lib/farce/abstract/publisher.rb +189 -0
  40. data/lib/farce/abstract/queue.rb +29 -3
  41. data/lib/farce/abstract/scheduler.rb +41 -14
  42. data/lib/farce/abstract/timer_queue.rb +3 -3
  43. data/lib/farce/abstract/vector.rb +3 -1
  44. data/lib/farce/atom.rb +15 -2
  45. data/lib/farce/clock.rb +20 -7
  46. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  47. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  48. data/lib/farce/concurrent/event.rb +60 -0
  49. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  50. data/lib/farce/concurrent/local_var.rb +65 -0
  51. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  52. data/lib/farce/concurrent/tuple.rb +74 -0
  53. data/lib/farce/concurrent.rb +50 -0
  54. data/lib/farce/config.rb +112 -2
  55. data/lib/farce/counter.rb +5 -15
  56. data/lib/farce/deduper.rb +12 -4
  57. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  58. data/lib/farce/engine/jruby/map.rb +8 -6
  59. data/lib/farce/engine/jruby/signal.rb +2 -2
  60. data/lib/farce/engine/jvm/counter.rb +0 -2
  61. data/lib/farce/engine/jvm/farce.jar +0 -0
  62. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  63. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  64. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  65. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  66. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  67. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  68. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  69. data/lib/farce/engine/ruby.rb +5 -1
  70. data/lib/farce/engine/shared/exchanger.rb +2 -2
  71. data/lib/farce/engine/shared/queue.rb +43 -5
  72. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  73. data/lib/farce/engine/shared/strict_map.rb +5 -0
  74. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  75. data/lib/farce/engine/shared/trie.rb +14 -1
  76. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  77. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  78. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  79. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  80. data/lib/farce/engine/shared/vector.rb +2 -0
  81. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  82. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  83. data/lib/farce/engine/shared.rb +2 -0
  84. data/lib/farce/engine/truffleruby/map.rb +8 -6
  85. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  86. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  87. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  88. data/lib/farce/engine/truffleruby.rb +9 -0
  89. data/lib/farce/extension.rb +5 -3
  90. data/lib/farce/integrations/concurrent.rb +16 -3
  91. data/lib/farce/integrations/sorted_set.rb +1 -0
  92. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  93. data/lib/farce/internal/fake_ractor.rb +1 -1
  94. data/lib/farce/internal/flag_waiting.rb +1 -1
  95. data/lib/farce/internal/freeze.rb +1 -5
  96. data/lib/farce/internal/lease_initialization.rb +1 -1
  97. data/lib/farce/internal/lease_pool_state.rb +1 -1
  98. data/lib/farce/internal/lease_state.rb +2 -2
  99. data/lib/farce/internal/local_executor_methods.rb +3 -3
  100. data/lib/farce/internal/map_value_modes.rb +17 -9
  101. data/lib/farce/internal/pool_settings.rb +9 -0
  102. data/lib/farce/internal/pool_worker.rb +34 -21
  103. data/lib/farce/internal/reservation_waiting.rb +1 -1
  104. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  105. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  106. data/lib/farce/internal.rb +19 -8
  107. data/lib/farce/lazy.rb +3 -0
  108. data/lib/farce/lazy_ref.rb +3 -0
  109. data/lib/farce/local/counter.rb +4 -10
  110. data/lib/farce/local/lease.rb +2 -9
  111. data/lib/farce/local/lease_map.rb +2 -9
  112. data/lib/farce/local/pool.rb +38 -5
  113. data/lib/farce/local/publisher.rb +30 -0
  114. data/lib/farce/local/scheduler.rb +18 -5
  115. data/lib/farce/local/vector.rb +3 -2
  116. data/lib/farce/map.rb +8 -1
  117. data/lib/farce/memoize.rb +319 -0
  118. data/lib/farce/pool.rb +5 -0
  119. data/lib/farce/priority_queue.rb +1 -1
  120. data/lib/farce/publisher.rb +57 -0
  121. data/lib/farce/shareable.rb +3 -0
  122. data/lib/farce/signal.rb +1 -1
  123. data/lib/farce/strict/publisher.rb +31 -0
  124. data/lib/farce/strict/vector.rb +3 -2
  125. data/lib/farce/strict/weak_cache.rb +199 -0
  126. data/lib/farce/subscription/definitions.rb +27 -0
  127. data/lib/farce/subscription.rb +121 -0
  128. data/lib/farce/transaction/map.rb +35 -1
  129. data/lib/farce/unshared/pool.rb +1 -1
  130. data/lib/farce/unshared/publisher.rb +25 -0
  131. data/lib/farce/unshared/scheduler.rb +3 -3
  132. data/lib/farce/unshared/vector.rb +3 -2
  133. data/lib/farce/vector.rb +20 -16
  134. data/lib/farce/version.rb +1 -1
  135. data/lib/farce/walker/definitions.rb +31 -2
  136. data/lib/farce/walker/modification.rb +28 -14
  137. data/lib/farce/walker.rb +9 -1
  138. data/lib/farce/weak_atom.rb +5 -2
  139. data/lib/farce/weak_key_map.rb +1 -1
  140. data/lib/farce/weak_map.rb +1 -1
  141. data/lib/farce/weak_value_map.rb +1 -1
  142. data/lib/farce.rb +3 -1
  143. metadata +23 -1
@@ -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,199 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Strict
7
+ # A weak cache that allows reusing previously generated values without holding strong references to them.
8
+ # Only shareable keys and values can be used.
9
+ #
10
+ # @example
11
+ # cache = Farce::Strict::WeakCache.new
12
+ # value = cache.store_if_absent(:key) { ExpensiveClass.new.freeze }
13
+ class WeakCache
14
+ include Shareable::Delegated
15
+ include Internal::Inspect
16
+
17
+ # Creates a new WeakCache instance if it outperforms {Strict::WeakValueMap} on the current platform.
18
+ # Otherwise it creates a standard WeakValueMap instance.
19
+ # @return [WeakCache, Strict::WeakValueMap] The created cache instance.
20
+ def self.create(...) = Internal.native_weak_maps? ? Strict::WeakValueMap.new(...) : WeakCache.new(...)
21
+
22
+ # @param initial_mapping [Hash, Farce::Abstract::Map, #each, nil]
23
+ # The entries to store initially. Each iteration must yield a key-value pair.
24
+ # Nil creates an empty map.
25
+ # @param compare_by_identity [Boolean]
26
+ # Whether keys use object identity instead of `hash` and `eql?`.
27
+ def initialize(initial_mapping = nil, compare_by_identity: false)
28
+ @map = Strict::WeakValueMap.new(initial_mapping, compare_keys_by_identity: compare_by_identity)
29
+ initialize_cache
30
+ super()
31
+ end
32
+
33
+ # @api private
34
+ def marshal_dump = [1, @map]
35
+
36
+ # @api private
37
+ def marshal_load(data)
38
+ @map, = Internal::MarshalSupport.payload(data, 1)
39
+ initialize_cache
40
+ publish_shareable
41
+ end
42
+
43
+ # Retrieves or stores a value for the given key.
44
+ # The block is called to generate the value if the key is absent.
45
+ # Concurrent calls for the same key reuse the generated value.
46
+ # Once that value is collected, a later call generates it again.
47
+ #
48
+ # @param key [BasicObject] The key to look up.
49
+ # @yield Called without arguments when the key is absent.
50
+ # @yieldreturn [BasicObject] The value to store and return.
51
+ # @return [BasicObject] The associated value.
52
+ def store_if_absent(key, &)
53
+ raise LocalJumpError, "no block given" unless block_given?
54
+ @cache.store_if_absent(key, &)
55
+ end
56
+
57
+ alias compute_if_absent store_if_absent
58
+
59
+ # Store a value only when the key is absent.
60
+ # Existing nil and false values count as present.
61
+ # @param key [BasicObject] The key to look up or store.
62
+ # @param value [BasicObject] The value to store if the key is absent.
63
+ # @return [BasicObject, nil] The existing value, or nil when a value was inserted.
64
+ def put_if_absent(key, value)
65
+ inserted = false
66
+ previous = store_if_absent(key) do
67
+ inserted = true
68
+ value
69
+ end
70
+ inserted ? nil : previous
71
+ end
72
+
73
+ # Look up a value without generating it.
74
+ # @param key [BasicObject] The key to look up.
75
+ # @return [BasicObject, nil] The associated value, or nil if the key is absent.
76
+ def [](key) = @cache.fetch(key) { nil } # rubocop:disable Style/RedundantFetchBlock
77
+
78
+ # Return a lookup key and its associated value if present.
79
+ # @param key [BasicObject] The key to look up.
80
+ # @return [Array(BasicObject, BasicObject), nil] The pair `[key, value]`, or nil if the key is absent.
81
+ def assoc(key)
82
+ value = fetch(key) { return nil }
83
+ [key, value]
84
+ end
85
+
86
+ # Return whether a live entry exists, including entries with nil or false values.
87
+ # @param key [BasicObject] The key to look up.
88
+ # @return [Boolean]
89
+ def key?(key)
90
+ @cache.fetch(key) { return false }
91
+ true
92
+ end
93
+
94
+ alias has_key? key?
95
+ alias include? key?
96
+ alias member? key?
97
+
98
+ # Look up a value in nested maps or arrays.
99
+ # @param key [BasicObject] The key to look up.
100
+ # @param rest [Array<BasicObject>] Additional keys or indices for the nested value.
101
+ # @return [BasicObject, nil] The nested value, or nil if an intermediate value is absent or nil.
102
+ def dig(key, *rest)
103
+ value = self[key]
104
+ return value if rest.empty? || value.nil?
105
+ value.dig(*rest)
106
+ end
107
+
108
+ # Return the stored key matching a lookup key.
109
+ # @param key [BasicObject] The key to look up.
110
+ # @return [BasicObject, nil] The stored key, or nil if the key is absent.
111
+ def getkey(key) = @map.getkey(key)
112
+
113
+ # Fetch the value associated with a key, using the same missing-key behavior as Hash#fetch.
114
+ # If both a default and a block are provided, the block takes precedence.
115
+ #
116
+ # @overload fetch(key)
117
+ # @param key [BasicObject] The key to look up.
118
+ # @return [BasicObject] The associated value.
119
+ # @raise [KeyError] If the key is absent.
120
+ #
121
+ # @overload fetch(key, default)
122
+ # @param key [BasicObject] The key to look up.
123
+ # @param default [BasicObject] The value to return if the key is absent.
124
+ # @return [BasicObject] The associated value or `default`.
125
+ #
126
+ # @overload fetch(key)
127
+ # @param key [BasicObject] The key to look up.
128
+ # @yield [key] Called if the key is absent.
129
+ # @yieldparam key [BasicObject] The missing key.
130
+ # @yieldreturn [BasicObject] The value to return.
131
+ # @return [BasicObject] The associated value or the block result.
132
+ def fetch(*arguments)
133
+ unless arguments.length.between?(1, 2)
134
+ raise ArgumentError, "wrong number of arguments (given #{arguments.length}, expected 1..2)"
135
+ end
136
+
137
+ key, default = arguments
138
+ warn "block supersedes default value argument", uplevel: 1 if block_given? && arguments.length == 2
139
+ @cache.fetch(key) do
140
+ return yield(key) if block_given?
141
+ return default if arguments.length == 2
142
+ raise KeyError.new("key not found: #{key.inspect}", receiver: self, key: key)
143
+ end
144
+ end
145
+
146
+ # Fetch values in the requested order, using {#fetch} for missing keys.
147
+ # @param keys [Array<BasicObject>] The keys to look up.
148
+ # @yield [key] Called for each missing key.
149
+ # @yieldparam key [BasicObject] The missing key.
150
+ # @yieldreturn [BasicObject] The fallback value, which is not stored.
151
+ # @return [Array<BasicObject>] The associated values or block results.
152
+ def fetch_values(*keys, &) = keys.map { fetch(it, &) }
153
+
154
+ # Look up values in the requested order, returning nil for missing keys.
155
+ # @param keys [Array<BasicObject>] The keys to look up.
156
+ # @return [Array<BasicObject>] The associated values, with nil for missing keys.
157
+ def values_at(*keys) = keys.map { self[it] }
158
+
159
+ # @return [Boolean] Whether keys are compared by identity instead of `hash` and `eql?`.
160
+ def compare_keys_by_identity? = @map.compare_keys_by_identity?
161
+
162
+ # (see #compare_keys_by_identity?)
163
+ def compare_by_identity? = compare_keys_by_identity?
164
+
165
+ # @return [Boolean] Whether keys must be Ractor-shareable, including copied mutable strings.
166
+ def shareable_keys? = true
167
+
168
+ # @return [Boolean] Whether values must be Ractor-shareable.
169
+ def shareable_values? = true
170
+
171
+ # @return [Boolean] Whether keys are referenced weakly.
172
+ def weak_keys? = false
173
+
174
+ # @return [Boolean] Whether values are referenced weakly.
175
+ def weak_values? = true
176
+
177
+ # @api private
178
+ def inspect_with(inspector)
179
+ super do
180
+ yield if block_given?
181
+ inspector.breakable
182
+ inspector.group("{", "}") do
183
+ inspector.breakable ""
184
+ inspector.seplist(@map, nil, :each) do |key, value|
185
+ inspector.group { inspector.hash_pair(key, value) }
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ alias to_s inspect
192
+
193
+ private
194
+
195
+ def initialize_cache = @cache = Internal.native_weak_maps? ? @map : Internal::WeakCacheMirror.new(@map)
196
+ def freeze_backend = @map
197
+ end
198
+ end
199
+ end
@@ -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
@@ -16,8 +16,42 @@ module Farce
16
16
  class Map < Abstract::ConcurrentMap
17
17
  include MapOperations
18
18
 
19
+ # (see Abstract::ConcurrentMap#store_if_present)
20
+ # @param mode [Symbol, nil] The result's transfer mode, or nil to use the default.
21
+ def store_if_present(key, mode: nil)
22
+ require_block!(block_given?)
23
+ write do
24
+ canonical = prepare(key)
25
+ next unless @working.key?(canonical)
26
+ replacement = yield(unwrap_stored(@working[canonical]))
27
+ unwrap_stored(store_value(canonical, wrap(replacement, mode:)))
28
+ end
29
+ end
30
+
31
+ # (see Abstract::ConcurrentMap#replace_if_exists)
32
+ # @param mode [Symbol, nil] The replacement's transfer mode, or nil to use the default.
33
+ def replace_if_exists(key, replacement, mode: nil)
34
+ write do
35
+ canonical = prepare(key)
36
+ next unless @working.key?(canonical)
37
+ previous = @working[canonical]
38
+ store_value(canonical, wrap(replacement, mode:))
39
+ unwrap_stored(previous)
40
+ end
41
+ end
42
+
43
+ # (see Abstract::ConcurrentMap#delete_pair)
44
+ # A failed comparison invalidates the transaction attempt.
45
+ def delete_pair(key, expected)
46
+ removed = modify_entry(key) do |present, value|
47
+ present && values_equal?(value, expected) ? Internal::MAP_DELETE : Internal::MAP_KEEP
48
+ end
49
+ compared(removed)
50
+ end
51
+
19
52
  Wrapper.inherit(self, *MapOperations::READ_HELPERS, :delete_if, :reject!, :compact!, :keep_if,
20
- :select!, :filter!, :transform_values!, :merge!)
53
+ :select!, :filter!, :transform_values!, :merge!, :put, :get_and_set, :compute_if_absent,
54
+ :compute_if_present, :replace_pair, :compute, :merge_pair, :fetch_or_store, :put_if_absent)
21
55
 
22
56
  private def enlist(backend) = @transaction.enlist(backend, per_key: true)
23
57
  end
@@ -36,7 +36,7 @@ module Farce
36
36
  Internal::LocalScheduledTask.new(args, block)
37
37
  end
38
38
 
39
- def schedule_scaler(started, delay = @grow_after)
39
+ def schedule_scaler(started, delay = settings.grow_after)
40
40
  Internal::ThreadPoolSupervisor.current.schedule(self, started, delay)
41
41
  end
42
42
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Unshared
7
+ # Unshared variant of {Farce::Publisher}.
8
+ class Publisher < Abstract::Publisher
9
+ include Unshareable
10
+
11
+ # @overload initialize(*subscribers)
12
+ # (see Farce::Abstract#initialize)
13
+ def initialize(...)
14
+ @map = Unshared::Map.new
15
+ super
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :map
21
+
22
+ def default_mode = :local
23
+ end
24
+ end
25
+ end
@@ -20,12 +20,12 @@ module Farce
20
20
  # results.pop # => :done
21
21
  # scheduler.close
22
22
  #
23
- # @!method schedule(*args, mode: :smart, auto_local: true)
23
+ # @!method schedule(*args, mode: :smart, auto_local: true, timeout: nil)
24
24
  # Enqueue a task while retaining its block and argument references.
25
25
  # `mode` is ignored when `auto_local` is true. Otherwise, only local and
26
26
  # smart modes are accepted.
27
27
  # @param (see Farce::Abstract::Executor#schedule)
28
- # @return [self]
28
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
29
29
  class Scheduler < Abstract::Scheduler
30
30
  include Unshareable
31
31
  include Internal::LocalExecutorMethods
@@ -80,7 +80,7 @@ module Farce
80
80
  def new_queue(capacity:) = Internal::UnsharedQueue.new(capacity:)
81
81
  def launch_callback = ->(_scheduler, priority, name) { launch!(name, priority) }
82
82
 
83
- def prepare_task(args, block, mode, auto_local)
83
+ def prepare_task(args, block, mode, auto_local, **)
84
84
  validate_local_mode(mode, auto_local)
85
85
  Internal::LocalScheduledTask.new(args, block)
86
86
  end
@@ -9,10 +9,11 @@ module Farce
9
9
  include Unshareable
10
10
  include Internal::Freeze::Delegated
11
11
 
12
- # @param source [Array, nil] Initial values. Values are retained directly.
12
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. Values are retained directly.
13
13
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
14
+ # @raise [ArgumentError] If the size is negative.
14
15
  def initialize(source = nil, compare_by_identity: false)
15
- @vector = Internal::UnsharedVector.new(source, compare_by_identity:)
16
+ @vector = Internal::UnsharedVector.new(normalize_vector_source(source), compare_by_identity:)
16
17
  super()
17
18
  end
18
19
 
data/lib/farce/vector.rb CHANGED
@@ -20,18 +20,21 @@ module Farce
20
20
  include Shareable::Delegated
21
21
 
22
22
  # @!macro modes
23
- # @param source [Array, nil] Initial values. The source array is not retained.
23
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
24
24
  # @param mode [Symbol] The default value transfer mode.
25
25
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
26
+ # @raise [ArgumentError] If the size is negative.
26
27
  def initialize(source = nil, mode: :smart, compare_by_identity: false)
27
- raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
28
- unless true.equal?(compare_by_identity) || false.equal?(compare_by_identity)
29
- raise ArgumentError, "compare_by_identity must be true or false"
30
- end
28
+ source = normalize_vector_source(source)
29
+ raise TypeError, "source must be an Array" unless source.nil? || Integer === source || source.is_a?(Array)
31
30
 
32
- @compare_by_identity = compare_by_identity
31
+ @compare_by_identity = compare_by_identity ? true : false
33
32
  @manager = ModeManager.new(mode:)
34
- @vector = Internal::Vector.new(source&.map { @manager.wrap(it) }, compare_by_identity: true)
33
+ if source && !(Integer === source)
34
+ source = source.map { @manager.wrap(it) }
35
+ raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
36
+ end
37
+ @vector = Internal::Vector.new_for_modes(source)
35
38
  super()
36
39
  end
37
40
 
@@ -51,7 +54,7 @@ module Farce
51
54
  def marshal_load(data)
52
55
  entries, @manager, identity, frozen = Internal::MarshalSupport.payload(data, 4)
53
56
  @compare_by_identity = identity
54
- @vector = Internal::Vector.new(compare_by_identity: true)
57
+ @vector = Internal::Vector.new_for_modes(nil)
55
58
  publish_shareable
56
59
  entries.each { @vector.push(Internal::MarshalSupport.restore_value(it)) }
57
60
  Internal::MarshalSupport.freeze(self, frozen)
@@ -176,11 +179,18 @@ module Farce
176
179
  replacement = @manager.wrap(replacement, mode:)
177
180
  wrapped = true
178
181
  end
179
- return true if @vector.compare_and_set(index, current, replacement, timeout: remaining_timeout(deadline))
182
+ return true if @vector.compare_and_set(index, current, replacement, timeout: Clock.remaining(deadline))
180
183
  return false if nil.equal?(current) || (deadline && Clock.now >= deadline)
181
184
  end
182
185
  end
183
186
 
187
+ # (see Farce::Abstract::Vector#wait_until)
188
+ def wait_until(index, timeout: nil)
189
+ raise LocalJumpError, "no block given" unless block_given?
190
+ result = Internal.wait_until(@vector, index, timeout:) { |current| yield(@manager.unwrap(current)) }
191
+ @manager.unwrap(result)
192
+ end
193
+
184
194
  # (see Farce::Abstract::Vector#wait_until_changed)
185
195
  def wait_until_changed(index, expected, timeout: nil)
186
196
  deadline = timeout_deadline(timeout)
@@ -189,7 +199,7 @@ module Farce
189
199
  current = @vector[index]
190
200
  return @manager.unwrap(current) unless values_equal?(current, expected)
191
201
  return if deadline && Clock.now >= deadline
192
- @vector.wait_until_changed(index, current, timeout: remaining_timeout(deadline))
202
+ @vector.wait_until_changed(index, current, timeout: Clock.remaining(deadline))
193
203
  end
194
204
  end
195
205
 
@@ -243,11 +253,5 @@ module Farce
243
253
  raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
244
254
  Clock.now + timeout
245
255
  end
246
-
247
- def remaining_timeout(deadline)
248
- return unless deadline
249
- remaining = deadline - Clock.now
250
- remaining.negative? ? 0 : remaining
251
- end
252
256
  end
253
257
  end
data/lib/farce/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
 
5
5
  module Farce
6
6
  # @return [String] the current version of Farce
7
- VERSION = "0.4.1"
7
+ VERSION = "0.6.0"
8
8
  end
@@ -118,8 +118,19 @@ module Farce
118
118
  if !key.equal?(new_key)
119
119
  entries << [key, new_key, result]
120
120
  elsif !value.equal?(result)
121
- until target.compare_and_set(key, value, result)
122
- value = target[key]
121
+ loop do
122
+ retry_update = false
123
+ send_to(target, :modify_entry, key, canonical: true) do |present, current|
124
+ next Internal::MAP_KEEP unless present
125
+ if Internal.value_changed?(current, value, true)
126
+ value = current
127
+ retry_update = true
128
+ next Internal::MAP_KEEP
129
+ end
130
+ result
131
+ end
132
+ break unless retry_update
133
+ # Traverse replacements after releasing the key's update reservation.
123
134
  result = walker.visit(value)
124
135
  end
125
136
  end
@@ -179,6 +190,24 @@ module Farce
179
190
  super(object, walker)
180
191
  end
181
192
 
193
+ define(Struct) do |object, walker|
194
+ result = walker.update(object, object.to_a) do |target, results|
195
+ results.each_with_index { |value, index| target[index] = value unless target[index].equal?(value) }
196
+ target
197
+ end
198
+ super(result, walker)
199
+ end
200
+
201
+ define(Range) do |object, walker|
202
+ names = object.instance_variables
203
+ values = [object.begin, object.end, *names.map { object.instance_variable_get(it) }]
204
+ walker.update(object, values) do |target, results|
205
+ names.each_with_index { |name, index| target.instance_variable_set(name, results[index + 2]) }
206
+ Internal.initialize_range(target, results[0], results[1], object.exclude_end?)
207
+ target
208
+ end
209
+ end
210
+
182
211
  define(Data) do |object, walker|
183
212
  walker.rebuild_data(object)
184
213
  end