solid_objects 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0614e42e260b81ab323d292435cd4f518a4f0b26a21865e5f63c9768db362231
4
- data.tar.gz: a8c7d4bc22e431b89ab07058c0ad35ca82a44eef08d30b0a547b28c0dd76f82e
3
+ metadata.gz: 3c9697331316791269c98ec881f02a59e7556c2ab2abf98c3feb662e5f8cf742
4
+ data.tar.gz: 12b0390bfcceca1362e7e1779bcb1562ea2e640259ca9ccefac9835b0b2d9a2e
5
5
  SHA512:
6
- metadata.gz: 07caec47a1c3452bb3f3b6fc6f950f60f9c68277ebb558630d112fcc7bfb9e3591f893e8bdaefea4329680cde6d6a86c36c0d9b6531d6ac889507abfda939920
7
- data.tar.gz: 23d756629a2b8212d06077569b9d0c35552c6ee416c95851ee78357ecfe1abf672e0a973d9715f0b66942ecebf8e1c007f21d33bde3bfb7e79af30f323a809bf
6
+ metadata.gz: 1acdc877187c455dee2843527e8826a719ab66c2f3917233eb1d565c19b6662a9fd6d319213f3340ada04e60f2d299dff8ad675de8762f65e336630a40b7ef79
7
+ data.tar.gz: f69a34ac8c724164ec597ad3b78637d8537c1d76a527998aa50fa9f7bd7c3327244449be9555a2214c2a9ae7f019e119b5f35c58ad2d28528ea824aad0a0f538
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2 - 2026-08-09
4
+
5
+ - Render batched component partials as HTML regardless of the request format.
6
+ The batch endpoint is requested with a JSON `Accept` header, so Rails looked
7
+ for JSON templates, raised `ActionView::MissingTemplate`, and the batch
8
+ returned 404 for applications whose components are ordinary
9
+ `.html.erb` partials. The outer response is still JSON. Single-component
10
+ refresh was never affected and is unchanged.
11
+ - Pass `registrations:` to `component_authorization_context`: one registration
12
+ for a single refresh, all of them for a batch, so applications no longer have
13
+ to inspect `params[:tokens]`. Callbacks accepting only `controller:` keep
14
+ working unchanged.
15
+
16
+ ## 0.7.1 - 2026-08-09
17
+
18
+ - Retry a contended SQLite write outside a synchronous deadline. Asynchronous
19
+ enqueue had no Ruby-level retry budget, so it depended entirely on SQLite's
20
+ busy handler and raised `SQLite3::BusyException` once concurrent writers
21
+ exhausted it. Bounded by the new `lock_retry_attempts` setting.
22
+ - Pin every GitHub Actions reference to a commit SHA.
23
+ - Add a benchmark comparing individual, batched, and payload delivery for one
24
+ mutation that changes three components.
25
+
3
26
  ## 0.7.0 - 2026-08-09
4
27
 
5
28
  - Add `batch:` to reactive components. Components sharing a batch in one actor
@@ -43,10 +43,7 @@ module SolidObjects
43
43
  return head :conflict
44
44
  end
45
45
 
46
- authorization_context = SolidObjects
47
- .configuration
48
- .component_authorization_context
49
- .call(controller: self)
46
+ authorization_context = component_authorization_context(registrations)
50
47
  frames = registrations.map do |registration|
51
48
  rendered = ComponentRenderer.new(
52
49
  snapshot:,
@@ -112,10 +109,7 @@ module SolidObjects
112
109
  return head :conflict
113
110
  end
114
111
 
115
- authorization_context = SolidObjects
116
- .configuration
117
- .component_authorization_context
118
- .call(controller: self)
112
+ authorization_context = component_authorization_context([ registration ])
119
113
  rendered = ComponentRenderer.new(
120
114
  snapshot:,
121
115
  registration:,
@@ -138,6 +132,34 @@ module SolidObjects
138
132
  head :bad_request
139
133
  end
140
134
 
135
+ # Callbacks written before batching accept only `controller:`. Those keep
136
+ # working; a callback that also accepts `registrations:` receives one
137
+ # registration for a single refresh and all of them for a batch.
138
+ # @rbs (Array[ComponentRegistration]) -> untyped
139
+ def component_authorization_context(registrations)
140
+ callable = SolidObjects.configuration.component_authorization_context
141
+ return callable.call(controller: self) unless accepts_registrations?(callable)
142
+
143
+ callable.call(controller: self, registrations:)
144
+ end
145
+
146
+ # A lambda answers `parameters` directly; a callable object answers it
147
+ # through its `call` method.
148
+ # @rbs (untyped) -> bool
149
+ def accepts_registrations?(callable)
150
+ callable_parameters(callable).any? do |type, name|
151
+ type == :keyrest || (%i[key keyreq].include?(type) && name == :registrations)
152
+ end
153
+ end
154
+
155
+ # @rbs (untyped) -> Array[[ Symbol, Symbol ]]
156
+ def callable_parameters(callable)
157
+ return callable.parameters if callable.respond_to?(:parameters)
158
+ return callable.method(:call).parameters if callable.respond_to?(:call)
159
+
160
+ []
161
+ end
162
+
141
163
  # @rbs (ComponentRegistration) -> Hash[Symbol, untyped]
142
164
  def registration_payload(registration)
143
165
  {
@@ -0,0 +1,5 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative "support"
4
+
5
+ SolidObjectsBenchmark.component_delivery
data/benchmark/support.rb CHANGED
@@ -22,6 +22,43 @@ module SolidObjectsBenchmark
22
22
  end
23
23
  end
24
24
 
25
+ class BenchmarkConnection
26
+ attr_reader :session_id
27
+
28
+ def initialize(session_id)
29
+ @session_id = session_id
30
+ end
31
+ end
32
+
33
+ class PlaymatActor < SolidObjects::Actor
34
+ actor_type "benchmark-playmat"
35
+
36
+ attribute :player, default: "unseated"
37
+ attribute :player_controls, default: -> { [] }
38
+ attribute :library_search, default: -> { [] }
39
+ attribute :hands, default: -> { {} }
40
+
41
+ observable :player
42
+ observable :player_controls
43
+ observable :library_search
44
+
45
+ broadcast_payload :playmat_state do |actor, context|
46
+ {
47
+ "player" => actor.player,
48
+ "controls" => actor.player_controls,
49
+ "library" => actor.library_search,
50
+ "hand" => actor.hands.fetch(context.session_id, [])
51
+ }
52
+ end
53
+
54
+ def seat(player:)
55
+ self.player = player
56
+ self.player_controls = %w[untap draw]
57
+ self.library_search = %w[Island Forest]
58
+ self.hands = hands.merge(player => %w[Island])
59
+ end
60
+ end
61
+
25
62
  class << self
26
63
  # @rbs () -> Integer
27
64
  def count
@@ -189,6 +226,69 @@ module SolidObjectsBenchmark
189
226
  worker&.stop
190
227
  end
191
228
 
229
+ # Compares how many browser requests one actor mutation costs across the
230
+ # three delivery paths, and how long the server spends producing them.
231
+ # @rbs () -> void
232
+ def component_delivery
233
+ require "action_controller"
234
+ require "action_view"
235
+ require "action_view/testing/resolvers"
236
+
237
+ SolidObjects.configuration.stream_signing_secret = "benchmark-secret"
238
+ SolidObjects.configuration.authorize_query = ->(**) { true }
239
+ reference = PlaymatActor.ref("table")
240
+ reference.seat(player: "alice")
241
+
242
+ view_context = benchmark_view_context
243
+ snapshot = SolidObjects::ActorSnapshot.new(reference)
244
+ registrations = %w[player player_controls library_search].map do |name|
245
+ SolidObjects::ComponentRegistration.issue(
246
+ reference:,
247
+ component_name: name,
248
+ component_key: nil,
249
+ dependencies: [ name ],
250
+ locals: {},
251
+ refresh_method: "morph",
252
+ snapshot:,
253
+ refresh_path: "/solid_objects/components",
254
+ batch: "playmat"
255
+ )
256
+ end
257
+
258
+ individual = measure_delivery(count) do
259
+ registrations.each do |registration|
260
+ render_component(registration, view_context)
261
+ end
262
+ end
263
+ batched = measure_delivery(count) do
264
+ current = SolidObjects::ActorSnapshot.new(reference)
265
+ registrations.each do |registration|
266
+ render_component(registration, view_context, snapshot: current)
267
+ end
268
+ end
269
+ payload = measure_delivery(count) do
270
+ SolidObjects::PayloadBroadcast.new(
271
+ snapshot: SolidObjects::ActorSnapshot.new(reference),
272
+ name: "playmat_state",
273
+ authorization_context: BenchmarkConnection.new("alice")
274
+ ).call
275
+ end
276
+
277
+ puts "three components changing in one mutation, #{count} iterations"
278
+ puts format(
279
+ " individual refreshes: 3 requests, %.3fms per mutation",
280
+ individual
281
+ )
282
+ puts format(
283
+ " batched refresh: 1 request, %.3fms per mutation",
284
+ batched
285
+ )
286
+ puts format(
287
+ " state payload: 0 requests, %.3fms per mutation",
288
+ payload
289
+ )
290
+ end
291
+
192
292
  # @rbs () -> void
193
293
  def query_count
194
294
  CounterActor.ref("queries").async(:increment)
@@ -209,6 +309,37 @@ module SolidObjectsBenchmark
209
309
 
210
310
  private
211
311
 
312
+ # @rbs (ComponentRegistration, untyped, ?snapshot: ActorSnapshot?) -> untyped
313
+ def render_component(registration, view_context, snapshot: nil)
314
+ SolidObjects::ComponentRenderer.new(
315
+ snapshot: snapshot || SolidObjects::ActorSnapshot.new(registration.reference),
316
+ registration:,
317
+ view_context:,
318
+ authorization_context: nil
319
+ ).call
320
+ end
321
+
322
+ # @rbs (Integer) { () -> untyped } -> Float
323
+ def measure_delivery(iterations)
324
+ yield
325
+ elapsed = Benchmark.realtime { iterations.times { yield } }
326
+ (elapsed / iterations) * 1_000
327
+ end
328
+
329
+ # @rbs () -> untyped
330
+ def benchmark_view_context
331
+ resolver = ActionView::FixtureResolver.new(
332
+ "actors/solid_objects_benchmark/playmat_actor/_player.html.erb" => "<p><%= actor.player %></p>",
333
+ "actors/solid_objects_benchmark/playmat_actor/_player_controls.html.erb" => "<ul><% actor.player_controls.each do |c| %><li><%= c %></li><% end %></ul>",
334
+ "actors/solid_objects_benchmark/playmat_actor/_library_search.html.erb" => "<ul><% actor.library_search.each do |c| %><li><%= c %></li><% end %></ul>"
335
+ )
336
+ ActionView::Base.with_empty_template_cache.new(
337
+ ActionView::LookupContext.new([ resolver ]),
338
+ {},
339
+ nil
340
+ )
341
+ end
342
+
212
343
  # @rbs () -> void
213
344
  def establish_connection
214
345
  database_url = ENV["SOLID_OBJECTS_DATABASE_URL"]
data/docs/benchmarks.md CHANGED
@@ -54,6 +54,28 @@ result is why Solid Objects does not publish one latency promise. Network
54
54
  topology, adapter behavior, host schema, logging, callbacks, and contention all
55
55
  matter.
56
56
 
57
+ ## Reactive delivery paths
58
+
59
+ Measured 2026-08-09 on an Apple M5 with 200 iterations, for one actor mutation
60
+ that changes three components.
61
+
62
+ | Delivery path | Browser requests | Server render time |
63
+ | --- | ---: | ---: |
64
+ | Individual component refreshes | 3 | 0.535 ms |
65
+ | Batched refresh | 1 | 0.249 ms |
66
+ | State payload broadcast | 0 | 0.100 ms |
67
+
68
+ The request column is the headline. Server render time is small in every path,
69
+ so the win is not faster rendering, it is fewer round trips: each individual
70
+ refresh costs a full HTTP request through the Rails middleware stack, and a
71
+ batch replaces three of those with one. A state payload removes the HTTP leg
72
+ entirely by travelling on the Action Cable connection the page already holds.
73
+
74
+ These are server-side numbers. They do not include network latency, Action
75
+ Cable delivery, or browser rendering, which dominate wall-clock time in a real
76
+ deployment and make the request-count difference matter more than it appears
77
+ here. End-to-end latency against a deployed application has not been measured.
78
+
57
79
  ## Durable row growth
58
80
 
59
81
  The storage cost is deterministic even when latency is not:
data/docs/realtime.md CHANGED
@@ -290,6 +290,20 @@ commonly resolve it to `Current.user`:
290
290
  configuration.component_authorization_context = ->(controller:) { Current.user }
291
291
  ```
292
292
 
293
+ A callback may also accept `registrations:`, which receives one registration for
294
+ a single component refresh and every registration in the group for a batch
295
+ refresh. This avoids decoding `params[:tokens]` by hand when a policy depends on
296
+ which components were requested:
297
+
298
+ ```ruby
299
+ configuration.component_authorization_context = lambda do |controller:, registrations:|
300
+ Current.user if registrations.all? { |registration| registration.component_key == controller.session[:seat] }
301
+ end
302
+ ```
303
+
304
+ Callbacks that accept only `controller:` continue to work; the extra keyword is
305
+ passed only to callables that declare it.
306
+
293
307
  The three contexts are intentionally different:
294
308
 
295
309
  | Boundary | Authorization context |
data/docs/roadmap.md CHANGED
@@ -17,18 +17,26 @@
17
17
  - One-shot and recurring reminders with `:latest` or `:all` catch-up
18
18
  - Durable observable invalidations, scalar Turbo replacement, keyed ERB
19
19
  components, signed component locals, and authorized replace or morph refresh
20
+ - Batched component refreshes: components sharing a signed `batch:` collapse to
21
+ one browser request per revision, served as HTML frames in a JSON envelope
22
+ - Personalized state payload broadcasts computed per subscriber under that
23
+ subscriber's authorization context, fenced by actor revision
20
24
  - Reconciliation read APIs
21
25
  - Installation doctor, authorization reference, fit guide, and legacy-state
22
26
  migration cookbook
23
27
  - Handler Active Record write isolation, same-database commit actions, ambient
24
- transaction rejection, adapter lock/query deadlines, structured sync timeout
25
- diagnostics, and result recovery
28
+ transaction rejection, adapter lock/query deadlines, bounded SQLite lock
29
+ retries outside those deadlines, structured sync timeout diagnostics, and
30
+ result recovery
26
31
  - Bounded message/process pruning, actor-type opt-in instance expiration,
27
32
  graceful caller shutdown, committed state snapshots, and an opt-in Minitest
28
33
  helper
29
34
  - SQLite, PostgreSQL, and MySQL integration suites
30
35
  - Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact
31
36
  RuboCop policy, and a warning-free Brakeman scan
37
+ - A JavaScript suite covering the state payload and batched refresh browser
38
+ modules, run in CI with Node's test runner and jsdom, with every GitHub
39
+ Actions reference pinned to a commit SHA
32
40
 
33
41
  ## Partially implemented
34
42
 
@@ -36,14 +44,24 @@
36
44
  role or run periodic maintenance automatically.
37
45
  - Wake-up strategy: in-process signaling plus durable polling and injection are
38
46
  implemented; PostgreSQL `LISTEN/NOTIFY` and optional Redis adapters are not.
47
+ Signaling cannot cross process boundaries, so a commit in a web process does
48
+ not wake a broadcast executor in a worker process; that delivery waits up to
49
+ `polling_interval`, 100 ms by default. This is the largest remaining term in
50
+ reactive update latency, and neither batching nor state payloads reduce it.
39
51
  - Realtime: scalar and dependency-driven keyed ERB component replacement or
40
52
  morphing, personalized refresh authorization, revision fencing, coalescing,
41
- and reconnect convergence are implemented; application-directed Turbo
42
- append intents are not.
53
+ reconnect convergence, batched refreshes, and personalized state payloads are
54
+ implemented; application-directed Turbo append intents are not. Batch
55
+ coalescing happens in the browser rather than the broadcast executor, so one
56
+ commit still sends one Action Cable message per changed observable even
57
+ though it costs one browser request.
43
58
  - Backpressure: mailbox/payload/state/result caps and fair yields exist;
44
59
  distributed per-actor rate limits and global admission control do not.
45
60
  - Administration: actor and dead-letter views plus policy hooks exist; richer
46
61
  filtering, audit records, and bulk-safe tools do not.
62
+ - Browser module coverage: the state payload and batched refresh modules have
63
+ JavaScript tests; `component_refresh.js`, which drives individual morph
64
+ refreshes, does not.
47
65
  - Outboxes use portable status rows with polling indexes; future versions may
48
66
  introduce narrow ready/claimed membership tables for very large outboxes.
49
67
 
@@ -51,7 +69,8 @@
51
69
 
52
70
  1. Add automatic supervisor role replacement and periodic dead-process cleanup.
53
71
  2. Add PostgreSQL notification and optional Redis wake-up adapters with latency
54
- benchmarks and polling-race tests.
72
+ benchmarks and polling-race tests, removing the cross-process polling delay
73
+ rather than shrinking it with a smaller `polling_interval`.
55
74
  3. Add result lookup by request ID and broader deadlock retry classification.
56
75
  4. Add scheduled retention and stale-process maintenance.
57
76
  5. Add database/server-version checks and MySQL InnoDB verification at boot.
@@ -61,7 +80,9 @@
61
80
  8. Expand security scanning and run compatibility CI across supported Rails and
62
81
  Ruby versions.
63
82
  9. Benchmark all workloads under documented hardware/database settings and
64
- publish adapter-specific adoption measurements.
83
+ publish adapter-specific adoption measurements. Throughput, synchronous
84
+ latency, query counts, and the three reactive delivery paths are measured on
85
+ SQLite; adapter-specific and end-to-end browser measurements are not.
65
86
 
66
87
  No production-ready claim should be made until these hardening milestones have
67
88
  operational soak evidence.
@@ -32,6 +32,7 @@ module SolidObjects
32
32
  )
33
33
  view_context.render(
34
34
  partial: default_partial,
35
+ formats: [ :html ],
35
36
  locals: registration.locals.transform_keys(&:to_sym).merge(
36
37
  actor:,
37
38
  authorization_context:,
@@ -17,6 +17,7 @@ module SolidObjects
17
17
  # @rbs @max_result_bytes: Integer
18
18
  # @rbs @max_attempts: Integer
19
19
  # @rbs @retry_delay: Proc
20
+ # @rbs @lock_retry_attempts: Integer
20
21
  # @rbs @process_heartbeat_interval: Float
21
22
  # @rbs @process_alive_threshold: Float
22
23
  # @rbs @shutdown_timeout: Float
@@ -57,6 +58,7 @@ module SolidObjects
57
58
  :max_result_bytes,
58
59
  :max_attempts,
59
60
  :retry_delay,
61
+ :lock_retry_attempts,
60
62
  :process_heartbeat_interval,
61
63
  :process_alive_threshold,
62
64
  :shutdown_timeout,
@@ -99,6 +101,7 @@ module SolidObjects
99
101
  @max_result_bytes = 1.megabyte
100
102
  @max_attempts = 5
101
103
  @retry_delay = ->(attempt) { [ 2**(attempt - 1), 60 ].min.to_f }
104
+ @lock_retry_attempts = 10
102
105
  @process_heartbeat_interval = 15.0
103
106
  @process_alive_threshold = 60.0
104
107
  @shutdown_timeout = 15.0
@@ -4,6 +4,7 @@ module SolidObjects
4
4
  module DatabaseAdapters
5
5
  class Sqlite < DatabaseAdapter
6
6
  LOCK_RETRY_INTERVAL = 0.001
7
+ MAXIMUM_BUSY_RETRY_INTERVAL = 0.25
7
8
  LOCK_RETRY_MUTEX = Thread::Mutex.new
8
9
  LOCK_RETRY_CONDITION = Thread::ConditionVariable.new
9
10
 
@@ -14,9 +15,28 @@ module SolidObjects
14
15
 
15
16
  # @rbs () { () -> untyped } -> untyped
16
17
  def transaction(&block)
17
- return super unless SyncDeadline.active?
18
+ return with_lock_retry { super } if SyncDeadline.active?
18
19
 
19
- with_lock_retry { super }
20
+ with_busy_retry { super }
21
+ end
22
+
23
+ # A write outside a synchronous deadline has no Ruby-level budget, so it
24
+ # depends entirely on SQLite's busy handler. Concurrent writers can
25
+ # exhaust that, which surfaces as a lock error the caller cannot retry.
26
+ # @rbs () { () -> untyped } -> untyped
27
+ def with_busy_retry
28
+ attempts = 0
29
+ begin
30
+ yield
31
+ rescue => error
32
+ raise unless busy_error?(error)
33
+
34
+ attempts += 1
35
+ raise if attempts > SolidObjects.configuration.lock_retry_attempts
36
+
37
+ wait_before_busy_retry(attempts)
38
+ retry
39
+ end
20
40
  end
21
41
 
22
42
  # @rbs () { () -> untyped } -> untyped
@@ -116,6 +136,11 @@ module SolidObjects
116
136
  def deadline_error?(error)
117
137
  return false unless SyncDeadline.active?
118
138
 
139
+ busy_error?(error)
140
+ end
141
+
142
+ # @rbs (Exception) -> bool
143
+ def busy_error?(error)
119
144
  cause = error
120
145
  while cause
121
146
  return true if cause.class.name.match?(/BusyException|BusyError/)
@@ -125,6 +150,16 @@ module SolidObjects
125
150
  false
126
151
  end
127
152
 
153
+ # @rbs (Integer) -> void
154
+ def wait_before_busy_retry(attempts)
155
+ LOCK_RETRY_MUTEX.synchronize do
156
+ LOCK_RETRY_CONDITION.wait(
157
+ LOCK_RETRY_MUTEX,
158
+ [ LOCK_RETRY_INTERVAL * (2**(attempts - 1)), MAXIMUM_BUSY_RETRY_INTERVAL ].min
159
+ )
160
+ end
161
+ end
162
+
128
163
  # @rbs () -> void
129
164
  def wait_before_retry
130
165
  LOCK_RETRY_MUTEX.synchronize do
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.7.0"
4
+ VERSION = "0.7.2"
5
5
  end
@@ -21,6 +21,20 @@ module SolidObjects
21
21
  # @rbs (Hash[Symbol, untyped]) -> void
22
22
  def refresh: (Hash[Symbol, untyped]) -> void
23
23
 
24
+ # Callbacks written before batching accept only `controller:`. Those keep
25
+ # working; a callback that also accepts `registrations:` receives one
26
+ # registration for a single refresh and all of them for a batch.
27
+ # @rbs (Array[ComponentRegistration]) -> untyped
28
+ def component_authorization_context: (Array[ComponentRegistration]) -> untyped
29
+
30
+ # A lambda answers `parameters` directly; a callable object answers it
31
+ # through its `call` method.
32
+ # @rbs (untyped) -> bool
33
+ def accepts_registrations?: (untyped) -> bool
34
+
35
+ # @rbs (untyped) -> Array[[ Symbol, Symbol ]]
36
+ def callable_parameters: (untyped) -> Array[[ Symbol, Symbol ]]
37
+
24
38
  # @rbs (ComponentRegistration) -> Hash[Symbol, untyped]
25
39
  def registration_payload: (ComponentRegistration) -> Hash[Symbol, untyped]
26
40
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Configuration
5
- @table_name_prefix: String
5
+ @process_alive_threshold: Float
6
6
 
7
7
  @shutdown_timeout: Float
8
8
 
@@ -48,6 +48,8 @@ module SolidObjects
48
48
 
49
49
  @authorize_administration: Proc
50
50
 
51
+ @table_name_prefix: String
52
+
51
53
  @polling_interval: Float
52
54
 
53
55
  @sync_polling_interval: Float
@@ -76,9 +78,9 @@ module SolidObjects
76
78
 
77
79
  @retry_delay: Proc
78
80
 
79
- @process_heartbeat_interval: Float
81
+ @lock_retry_attempts: Integer
80
82
 
81
- @process_alive_threshold: Float
83
+ @process_heartbeat_interval: Float
82
84
 
83
85
  attr_accessor table_name_prefix: untyped
84
86
 
@@ -110,6 +112,8 @@ module SolidObjects
110
112
 
111
113
  attr_accessor retry_delay: untyped
112
114
 
115
+ attr_accessor lock_retry_attempts: untyped
116
+
113
117
  attr_accessor process_heartbeat_interval: untyped
114
118
 
115
119
  attr_accessor process_alive_threshold: untyped
@@ -5,6 +5,8 @@ module SolidObjects
5
5
  class Sqlite < DatabaseAdapter
6
6
  LOCK_RETRY_INTERVAL: ::Float
7
7
 
8
+ MAXIMUM_BUSY_RETRY_INTERVAL: ::Float
9
+
8
10
  LOCK_RETRY_MUTEX: untyped
9
11
 
10
12
  LOCK_RETRY_CONDITION: untyped
@@ -15,6 +17,12 @@ module SolidObjects
15
17
  # @rbs () { () -> untyped } -> untyped
16
18
  def transaction: () { () -> untyped } -> untyped
17
19
 
20
+ # A write outside a synchronous deadline has no Ruby-level budget, so it
21
+ # depends entirely on SQLite's busy handler. Concurrent writers can
22
+ # exhaust that, which surfaces as a lock error the caller cannot retry.
23
+ # @rbs () { () -> untyped } -> untyped
24
+ def with_busy_retry: () { () -> untyped } -> untyped
25
+
18
26
  # @rbs () { () -> untyped } -> untyped
19
27
  def with_lock_retry: () { () -> untyped } -> untyped
20
28
 
@@ -38,6 +46,12 @@ module SolidObjects
38
46
  # @rbs (Exception) -> bool
39
47
  def deadline_error?: (Exception) -> bool
40
48
 
49
+ # @rbs (Exception) -> bool
50
+ def busy_error?: (Exception) -> bool
51
+
52
+ # @rbs (Integer) -> void
53
+ def wait_before_busy_retry: (Integer) -> void
54
+
41
55
  # @rbs () -> void
42
56
  def wait_before_retry: () -> void
43
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson
@@ -287,6 +287,7 @@ files:
287
287
  - benchmark/adoption_latency.rb
288
288
  - benchmark/claim.rb
289
289
  - benchmark/cold_actors.rb
290
+ - benchmark/component_delivery.rb
290
291
  - benchmark/concurrent_actors.rb
291
292
  - benchmark/enqueue.rb
292
293
  - benchmark/hot_actor.rb