solid_objects 0.17.3 → 0.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/docs/agents.md +3 -0
- data/docs/observability.md +1 -0
- data/docs/operations.md +21 -0
- data/docs/roadmap.md +8 -2
- data/docs/virtual-actors.md +4 -0
- data/lib/solid_objects/activation.rb +5 -2
- data/lib/solid_objects/executor.rb +12 -4
- data/lib/solid_objects/lease.rb +1 -0
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/lib/solid_objects/executor.rbs +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ba9bcffaef380dc24ebcb6b4da8893e467e5501fe71ed77f6f1e6d95520f516
|
|
4
|
+
data.tar.gz: ed791f24f434f52efb3a5080f37d47c5778e0f5b60be605ef9cf6aed2ac14471
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e116a5c98d6e5dfcb6ff6b4a3cf37993b7e6ea320b2b87d1fe91f080ffc11b169c97ef5381746a193f2d5d89782eefe147542a9f0f53c597e49c622956f15ff1
|
|
7
|
+
data.tar.gz: 51bf5f1a8f3c87e72ce86cfcf8cd9cf08e3fd24854dce7446e3ef3e723e02a7b74efa9c2f2ea2a2a3bf73cf14ae1716b29687c5fdff734103dc05b391b6df4ad
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.5 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Back off repeated interrupted-message recovery using `retry_delay`.
|
|
6
|
+
The first claimed attempt recovers immediately; after attempts 2, 3, 4,
|
|
7
|
+
and onward, the delay callback receives 1, 2, 3, and onward (defaults:
|
|
8
|
+
1, 2, 4 seconds, capped at 60 seconds). Total persisted claim attempts,
|
|
9
|
+
including earlier handler failures, determine the delay. The ready timestamp
|
|
10
|
+
survives worker replacement and preserves mailbox order while other actors
|
|
11
|
+
can proceed. Lease loss alone still does not dead-letter a message; handler
|
|
12
|
+
failure attempt limits, setting names, defaults, and schema stay unchanged.
|
|
13
|
+
- Emit `activation.lost` with the message ID and total attempt count when a
|
|
14
|
+
turn loses its fence, including while saving a handler failure or rejection,
|
|
15
|
+
matching JavaScript instrumentation. Shutdown requested
|
|
16
|
+
by that callback stops the worker before another pass. Skip a cached actor
|
|
17
|
+
whose first message is delayed so later ready messages cannot starve peers.
|
|
18
|
+
|
|
19
|
+
- Preserve caller lifecycle guarantees with regression tests for concurrent
|
|
20
|
+
synchronous calls retaining their staged effects and for caller process
|
|
21
|
+
re-registration after stale-process cleanup and retention. Both cases
|
|
22
|
+
already work in Ruby; no runtime behavior or configuration changes.
|
|
23
|
+
The concurrency test waits for the second caller's busy-actor wait before
|
|
24
|
+
allowing the first caller to commit its staged effect.
|
|
25
|
+
|
|
26
|
+
## 0.17.4 - 2026-10-12
|
|
27
|
+
|
|
28
|
+
- Renew activations between turns in a drain pass when renewal is due,
|
|
29
|
+
including when the renewal thread has not run.
|
|
30
|
+
- Keep the activation fence and expiry while an outstanding message claim
|
|
31
|
+
remains. The next worker pass recovers an expired turn in mailbox order,
|
|
32
|
+
including with `max_attempts: 1`. Reclaiming increments `attempt_count`;
|
|
33
|
+
lease loss alone does not create a dead letter.
|
|
34
|
+
|
|
3
35
|
## 0.17.3 - 2026-10-09
|
|
4
36
|
|
|
5
37
|
- Add four problem guides in `docs/guides/`: preventing race conditions in
|
data/docs/agents.md
CHANGED
|
@@ -173,6 +173,9 @@ Obey these rules in actor code:
|
|
|
173
173
|
write in the same database.
|
|
174
174
|
- Do not call an external API in a handler. Use `emit` and an effect handler.
|
|
175
175
|
- Write each handler so that it can run again. Delivery is at least once.
|
|
176
|
+
- Repeated lease losses back off but do not become dead letters. Observe
|
|
177
|
+
`activation.lost` and set an application stopping policy if a hard bound is
|
|
178
|
+
required; increasing the handler-failure attempt limit does not bound lease loss.
|
|
176
179
|
- A reminder changes state only when it runs, and it runs only while
|
|
177
180
|
`solid_objects start` runs. Do not compute expiry from the clock in a query;
|
|
178
181
|
read the state that the reminder committed.
|
data/docs/observability.md
CHANGED
|
@@ -45,6 +45,7 @@ JavaScript does not await exporters. Telemetry is not a durable audit trail.
|
|
|
45
45
|
| ------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
46
46
|
| `activation.started/completed/failed` | Local actor activation hook lifecycle |
|
|
47
47
|
| `message.started/completed/failed/rejected` | One attempt's execution outcome |
|
|
48
|
+
| `activation.lost` | A message turn lost its activation fence; includes message ID and total attempt count |
|
|
48
49
|
| `message.retry` | Failed attempt durably queued for another attempt |
|
|
49
50
|
| `dead_letter.created` | Message exhausted retries or failed permanently |
|
|
50
51
|
| `commit_action.started/completed/failed` | One registered commit action inside the commit transaction |
|
data/docs/operations.md
CHANGED
|
@@ -606,3 +606,24 @@ heartbeat cleanup.
|
|
|
606
606
|
Back up actor tables with the same consistency guarantees as application data.
|
|
607
607
|
Restoring only instances without their mailboxes/outboxes, or vice versa, can
|
|
608
608
|
violate application expectations.
|
|
609
|
+
|
|
610
|
+
## Interrupted-message recovery
|
|
611
|
+
|
|
612
|
+
An interrupted first claimed attempt recovers immediately. Recovery after
|
|
613
|
+
persisted attempts 2, 3, 4, ... calls `retry_delay` with 1, 2, 3, ... . The
|
|
614
|
+
callback returns seconds; defaults grow from 1 to 2 to 4 seconds up to 60 seconds.
|
|
615
|
+
This uses total claim attempts, including earlier handler failures, not a
|
|
616
|
+
separate lease-loss counter. The delay is stored in ready-message availability
|
|
617
|
+
when a stale claim is recovered, survives worker replacement, and keeps later
|
|
618
|
+
messages behind that message. Other actors can still run. Custom delays,
|
|
619
|
+
including zero, remain authoritative.
|
|
620
|
+
|
|
621
|
+
Lease loss alone does not create a dead letter, even with `max_attempts: 1`.
|
|
622
|
+
Handler failures still use the full attempt count for `max_attempts` and
|
|
623
|
+
`retry_delay`. An always-losing message remains pending indefinitely. Observe
|
|
624
|
+
`activation.lost` (message ID and total attempt count) and impose an application
|
|
625
|
+
stopping policy if a hard bound is required. Calling `request_shutdown` from
|
|
626
|
+
instrumentation stops the worker before its next pass. Ruby uses a separate
|
|
627
|
+
renewal thread and waits after lost turns; it does not have the JavaScript
|
|
628
|
+
microtask starvation pattern. A handler must still return before its worker
|
|
629
|
+
can finish shutting down.
|
data/docs/roadmap.md
CHANGED
|
@@ -23,8 +23,14 @@
|
|
|
23
23
|
instead of 12. Concurrent creation causes no deadlock on SQLite, PostgreSQL,
|
|
24
24
|
or MySQL. MySQL needs a shared read after a duplicate key, because it uses
|
|
25
25
|
repeatable read. The tests count statements, and do not measure latency.
|
|
26
|
-
- Activation leases, renewal
|
|
27
|
-
|
|
26
|
+
- Activation leases, renewal between turns as well as in the renewal thread,
|
|
27
|
+
unique activation tokens, generations, and
|
|
28
|
+
fenced commits. Interrupted first attempts recover immediately; repeated
|
|
29
|
+
interruptions use durable retry delays from total claim attempts minus one,
|
|
30
|
+
preserving mailbox order and progress for other actors. Lease loss emits
|
|
31
|
+
`activation.lost` with the message ID and attempt, including losses while
|
|
32
|
+
saving a handler failure or rejection. There is no hard retry cap;
|
|
33
|
+
applications needing one must impose a stopping policy.
|
|
28
34
|
- Bounded activation passes, idle cache, hot-actor yield, and process records
|
|
29
35
|
- At-least-once retries, terminal domain rejection, strict poison ordering,
|
|
30
36
|
dead letters, and tail retry
|
data/docs/virtual-actors.md
CHANGED
|
@@ -170,6 +170,10 @@ again without harm.
|
|
|
170
170
|
a crash or a lost lease.
|
|
171
171
|
- Ordered turns do not cancel stale Ruby code. Fencing stops a stale activation
|
|
172
172
|
from a commit, but that code can continue to run.
|
|
173
|
+
- An interrupted first attempt recovers immediately. Repeated interruptions
|
|
174
|
+
use growing, durable retry delays while keeping mailbox order. Lease loss
|
|
175
|
+
alone has no attempt limit; use instrumentation and an application stopping
|
|
176
|
+
policy when a hard bound is required.
|
|
173
177
|
- External effects can run more than once. Use the stable effect ID, or
|
|
174
178
|
another durable key, as the idempotency key at the provider.
|
|
175
179
|
- There are no transactions across actor identities, and there is no replay of
|
|
@@ -52,7 +52,8 @@ module SolidObjects
|
|
|
52
52
|
# @rbs () -> bool
|
|
53
53
|
def ready?
|
|
54
54
|
now = SolidObjects.database_adapter.database_now
|
|
55
|
-
ReadyMessage.where(instance_id: lease.instance_id
|
|
55
|
+
ready_message = ReadyMessage.where(instance_id: lease.instance_id).order(:sequence).first
|
|
56
|
+
(ready_message && ready_message.available_at <= now) ||
|
|
56
57
|
ClaimedMessage.where(
|
|
57
58
|
instance_id: lease.instance_id,
|
|
58
59
|
process_id: lease.owner_id,
|
|
@@ -139,6 +140,7 @@ module SolidObjects
|
|
|
139
140
|
break
|
|
140
141
|
end
|
|
141
142
|
|
|
143
|
+
renew_lease if processed_count.positive? && lease_renewal_due?
|
|
142
144
|
message = claim_next_message
|
|
143
145
|
break unless message
|
|
144
146
|
|
|
@@ -226,11 +228,12 @@ module SolidObjects
|
|
|
226
228
|
claimed_message.destroy!
|
|
227
229
|
return if message.completed? || message.dead?
|
|
228
230
|
|
|
231
|
+
delay = (message.attempt_count <= 1) ? 0 : SolidObjects.configuration.retry_delay.call(message.attempt_count - 1)
|
|
229
232
|
ReadyMessage.create!(
|
|
230
233
|
message:,
|
|
231
234
|
instance:,
|
|
232
235
|
sequence: message.sequence,
|
|
233
|
-
available_at: SolidObjects.database_adapter.database_now
|
|
236
|
+
available_at: SolidObjects.database_adapter.database_now + delay
|
|
234
237
|
)
|
|
235
238
|
end
|
|
236
239
|
|
|
@@ -16,6 +16,18 @@ module SolidObjects
|
|
|
16
16
|
|
|
17
17
|
# @rbs () -> bool
|
|
18
18
|
def call
|
|
19
|
+
execute
|
|
20
|
+
rescue LostActivation
|
|
21
|
+
SolidObjects.instrument(:"activation.lost", **instrumentation_payload, duration_milliseconds: elapsed_milliseconds)
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :activation, :message
|
|
28
|
+
|
|
29
|
+
# @rbs () -> bool
|
|
30
|
+
def execute
|
|
19
31
|
state_before = actor.state.to_h
|
|
20
32
|
observables_before = actor.observable_values
|
|
21
33
|
message_context = MessageContext.new(
|
|
@@ -53,10 +65,6 @@ module SolidObjects
|
|
|
53
65
|
false
|
|
54
66
|
end
|
|
55
67
|
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
attr_reader :activation, :message
|
|
59
|
-
|
|
60
68
|
# @rbs () -> Actor
|
|
61
69
|
def actor
|
|
62
70
|
activation.actor
|
data/lib/solid_objects/lease.rb
CHANGED
|
@@ -123,6 +123,7 @@ module SolidObjects
|
|
|
123
123
|
instance = Instance.lock.find_by(id: instance_id)
|
|
124
124
|
next false unless instance
|
|
125
125
|
next false unless owned_generation?(instance)
|
|
126
|
+
next false if ClaimedMessage.where(instance_id:).exists?
|
|
126
127
|
|
|
127
128
|
instance.update!(
|
|
128
129
|
activation_owner_id: nil,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.17.
|
|
4
|
+
version: 0.17.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|