solid_objects 0.15.1 → 0.15.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: b9179610a28d7c335fbffb11a9a86009c15ae4b1e7e2e1a4f638a69c64085b85
4
- data.tar.gz: bc67c66ad2f7841eb32724d5d8cbcb744250fb4f0579c7fdb6546e1a9b259fd5
3
+ metadata.gz: 55b365960ffe5f49934eb8914e35d546af1de656e158477977caf8c75db19fcd
4
+ data.tar.gz: fcea896079cc1f85eb60e6cbc13d080c597a8074a077cf32a1e34142b5055886
5
5
  SHA512:
6
- metadata.gz: 9786fdf1d29ff91f145d47f5a5523d6371ebe3bd1623af0fde9652c4327e52c12e90f540e5d9623e5e79ca616656c580759830e31577bd0d0a88335a15a1ddd7
7
- data.tar.gz: 3b1513d225edc9f56e7063e9517d9abe7f41b3e2b59e37c93fb269d0b63a9e6963e7442d3e5033361a9ba7aeb0f39859d0bd4e911be1f51e7d77a0940e999a85
6
+ metadata.gz: 6c21e96eb84ebbaf519c8860b79d952f3daa5d647f6ca0de6d35527b5b24bab53fd086c35fb57446a1173798f4c2849034d368415ff4afaacccedb509e768f23
7
+ data.tar.gz: f11760f2484478b62ad93cf18327aaa9f5a9d53b7b451c8f1233a479854a3f50b75b369f8559d06295794ea19cad30696088889b3258a541c4003f5775cec2fc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.2 - 2026-09-21
4
+
5
+ - Find the actor instance before the insert when an enqueue starts, and lock
6
+ that row by its primary key. A steady-state enqueue now writes no instance
7
+ row and issues 10 statements instead of 12.
8
+ - Stop the deadlock between concurrent enqueues that create the same actor
9
+ inside a transaction that already wrote. MySQL keeps the shared lock of a
10
+ failed insert across a savepoint rollback, so the mailbox reads the winning
11
+ row in shared mode and never asks to upgrade that lock.
12
+
3
13
  ## 0.15.1 - 2026-09-16
4
14
 
5
15
  - Use the existing cleanup index when finding expired actor instances. Preserve
data/docs/roadmap.md CHANGED
@@ -6,7 +6,12 @@
6
6
  - Explicit actor registry, references, JSON state, and state migrations
7
7
  - Fluent direct synchronous RPC, configured `sync`, and durable `async`
8
8
  - Durable message history plus ready/claimed membership tables
9
- - Concurrent sequence allocation and actor creation
9
+ - Concurrent sequence allocation and actor creation. An enqueue finds the
10
+ instance row with an unlocked read, then locks that row by its primary key.
11
+ A steady-state enqueue writes no instance row, and issues 10 statements
12
+ instead of 12. Concurrent creation causes no deadlock on SQLite, PostgreSQL,
13
+ or MySQL. MySQL needs a shared read after a duplicate key, because it uses
14
+ repeatable read. The tests count statements, and do not measure latency.
10
15
  - Activation leases, renewal, unique activation tokens, generations, and
11
16
  fenced commits
12
17
  - Bounded activation passes, idle cache, hot-actor yield, and process records
@@ -99,6 +99,11 @@ module SolidObjects
99
99
  nil
100
100
  end
101
101
 
102
+ # @rbs () -> String?
103
+ def shared_lock
104
+ nil
105
+ end
106
+
102
107
  # @rbs () -> String
103
108
  def current_time_expression
104
109
  "CURRENT_TIMESTAMP"
@@ -163,6 +168,11 @@ module SolidObjects
163
168
  claim_lock ? relation.lock(claim_lock) : relation
164
169
  end
165
170
 
171
+ # @rbs (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
172
+ def share_locked(relation)
173
+ shared_lock ? relation.lock(shared_lock) : relation
174
+ end
175
+
166
176
  private
167
177
 
168
178
  attr_reader :connection_pool, :fixed_connection
@@ -15,6 +15,11 @@ module SolidObjects
15
15
  "FOR UPDATE SKIP LOCKED"
16
16
  end
17
17
 
18
+ # @rbs () -> String
19
+ def shared_lock
20
+ "FOR SHARE"
21
+ end
22
+
18
23
  # A non-transactional engine would silently break fenced commits, so the
19
24
  # storage engine is verified rather than assumed.
20
25
  # @rbs () -> Array[String]
@@ -52,7 +52,6 @@ module SolidObjects
52
52
  max_bytes: SolidObjects.configuration.max_payload_bytes
53
53
  )
54
54
  instance = find_or_create_instance(reference, actor_class)
55
- instance.lock!
56
55
 
57
56
  existing = find_idempotent_message(instance, idempotency_key)
58
57
  if existing
@@ -108,13 +107,23 @@ module SolidObjects
108
107
 
109
108
  # @rbs (Reference, Class) -> Instance
110
109
  def find_or_create_instance(reference, actor_class)
111
- Instance.create_or_find_by!(
112
- actor_type: reference.actor_type,
113
- actor_id: reference.actor_id
114
- ) do |instance|
115
- instance.state = {}
116
- instance.state_version = actor_class.state_version
110
+ identity = { actor_type: reference.actor_type, actor_id: reference.actor_id }
111
+ identifier = Instance.where(identity).pick(:id)
112
+ return lock_instance!(identifier) if identifier
113
+
114
+ Instance.transaction(requires_new: true) do
115
+ Instance.create!(**identity, state: {}, state_version: actor_class.state_version)
117
116
  end
117
+ rescue ActiveRecord::RecordNotUnique
118
+ lock_instance!(database_adapter.share_locked(Instance.where(identity)).pick(:id))
119
+ end
120
+
121
+ # @rbs (Integer?) -> Instance
122
+ def lock_instance!(identifier)
123
+ instance = identifier && Instance.lock.find_by(id: identifier)
124
+ return instance if instance
125
+
126
+ raise ActiveRecord::RecordNotFound, "actor instance disappeared while enqueueing"
118
127
  end
119
128
 
120
129
  # @rbs (Instance, String?) -> Message?
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.15.1"
4
+ VERSION = "0.15.2"
5
5
  end
@@ -49,6 +49,9 @@ module SolidObjects
49
49
  # @rbs () -> String?
50
50
  def claim_lock: () -> String?
51
51
 
52
+ # @rbs () -> String?
53
+ def shared_lock: () -> String?
54
+
52
55
  # @rbs () -> String
53
56
  def current_time_expression: () -> String
54
57
 
@@ -70,6 +73,9 @@ module SolidObjects
70
73
  # @rbs (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
71
74
  def lock_candidates: (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
72
75
 
76
+ # @rbs (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
77
+ def share_locked: (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
78
+
73
79
  private
74
80
 
75
81
  attr_reader connection_pool: untyped
@@ -11,6 +11,9 @@ module SolidObjects
11
11
  # @rbs () -> String
12
12
  def claim_lock: () -> String
13
13
 
14
+ # @rbs () -> String
15
+ def shared_lock: () -> String
16
+
14
17
  # A non-transactional engine would silently break fenced commits, so the
15
18
  # storage engine is verified rather than assumed.
16
19
  # @rbs () -> Array[String]
@@ -28,6 +28,9 @@ module SolidObjects
28
28
  # @rbs (Reference, Class) -> Instance
29
29
  def find_or_create_instance: (Reference, Class) -> Instance
30
30
 
31
+ # @rbs (Integer?) -> Instance
32
+ def lock_instance!: (Integer?) -> Instance
33
+
31
34
  # @rbs (Instance, String?) -> Message?
32
35
  def find_idempotent_message: (Instance, String?) -> Message?
33
36
 
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.15.1
4
+ version: 0.15.2
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-09-16 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actioncable