solid_objects 0.4.2 → 0.4.3
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 +7 -0
- data/README.md +8 -3
- data/docs/correctness.md +7 -4
- data/docs/operations.md +6 -0
- data/lib/solid_objects/activation.rb +9 -5
- data/lib/solid_objects/actor_registry.rb +6 -1
- data/lib/solid_objects/application_actor_loader.rb +53 -0
- data/lib/solid_objects/caller_process.rb +6 -4
- data/lib/solid_objects/cli.rb +2 -0
- data/lib/solid_objects/client.rb +26 -20
- data/lib/solid_objects/database_adapter.rb +10 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +51 -2
- data/lib/solid_objects/process_registry.rb +21 -14
- data/lib/solid_objects/sync_diagnostics.rb +57 -3
- data/lib/solid_objects/synchronous_invocation.rb +45 -13
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/lib/solid_objects/actor_registry.rbs +3 -0
- data/sig/generated/lib/solid_objects/application_actor_loader.rbs +30 -0
- data/sig/generated/lib/solid_objects/database_adapter.rbs +8 -2
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +13 -0
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +9 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +9 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e027a73cc399aa779d8a60acae69f3f379ce505e060123ad49d5d64dcda4809
|
|
4
|
+
data.tar.gz: 5eb1be4b51649c2c8e6056f0138f130f4f1a8cf2e55e46f86061fc82849658aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f36f021e2707bbd67fcca022a88a4aa70b5cc35f8175c0ee526defcfb66fb1d3113434d8f700aad1f813ca9ccf49bc6a7e59e8d7c95717f007a660ca7535bd4
|
|
7
|
+
data.tar.gz: 246e46e83aae40b366992c7c5e763dc65e00945d57b0c0e724595d2f6af0a66ea5c09d5d4dbc5f4c4bdc1ed4de6766d217560e6b376832a98afab158f82e16ea
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 - 2026-08-07
|
|
4
|
+
|
|
5
|
+
- Bound SQLite caller-process registration, reuse, heartbeat, and synchronous
|
|
6
|
+
result observation retries by the original invocation deadline.
|
|
7
|
+
- Load host application actors from `app/actors` before CLI workers start,
|
|
8
|
+
including development environments with eager loading disabled.
|
|
9
|
+
|
|
3
10
|
## 0.4.2 - 2026-08-07
|
|
4
11
|
|
|
5
12
|
- Decode Action Cable broadcast payloads before parsing observable invalidations
|
data/README.md
CHANGED
|
@@ -588,9 +588,9 @@ polling as the fallback. A timeout never cancels the durable invocation.
|
|
|
588
588
|
durable status, mailbox blocker, and activation-owner diagnostics without
|
|
589
589
|
including message arguments. The configured timeout also bounds adapter
|
|
590
590
|
database lock waits from the enqueue attempt through result observation.
|
|
591
|
-
PostgreSQL uses transaction lock and statement timeouts, SQLite
|
|
592
|
-
|
|
593
|
-
lock-wait granularity.
|
|
591
|
+
PostgreSQL uses transaction lock and statement timeouts, SQLite retries busy
|
|
592
|
+
coordination operations only until the original call deadline, and MySQL uses
|
|
593
|
+
its execution timeout plus InnoDB's one-second minimum lock-wait granularity.
|
|
594
594
|
|
|
595
595
|
The durable call can finish after its original caller gives up. Reauthorize and
|
|
596
596
|
recover its eventual result through the durable message identity:
|
|
@@ -896,6 +896,11 @@ and marks process rows stopped on graceful shutdown. A hard-killed worker's
|
|
|
896
896
|
claimed turn is recovered after its process heartbeat or activation lease
|
|
897
897
|
becomes stale.
|
|
898
898
|
|
|
899
|
+
Before any role starts, the CLI loads actors from the host application's
|
|
900
|
+
`app/actors` directories through Rails' main autoloader. This works when
|
|
901
|
+
development eager loading is disabled and does not require actor references in
|
|
902
|
+
an initializer.
|
|
903
|
+
|
|
899
904
|
See the [operations guide](docs/operations.md) for monitoring, reconciliation,
|
|
900
905
|
shutdown, retention, and backup guidance.
|
|
901
906
|
|
data/docs/correctness.md
CHANGED
|
@@ -155,10 +155,13 @@ Timeout raises `SolidObjects::SyncTimeout` but does not cancel the message.
|
|
|
155
155
|
The exception reports actor identity, message ID and sequence, durable status,
|
|
156
156
|
an earlier mailbox blocker, and activation-owner metadata without exposing
|
|
157
157
|
arguments. Its `message_reference` can reauthorize and wait for the eventual
|
|
158
|
-
result. Adapter lock/query deadlines cover the durable enqueue
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
result. Adapter lock/query deadlines cover the durable enqueue, caller-process
|
|
159
|
+
registration and heartbeat, activation coordination, and result observation.
|
|
160
|
+
SQLite retries busy coordination operations only within the original call
|
|
161
|
+
deadline and reports `waiting_on=database_contention` when the database cannot
|
|
162
|
+
be inspected at timeout. If enqueue cannot commit, `SyncEnqueueTimeout` is
|
|
163
|
+
raised and no message reference exists. MySQL lock waits have one-second InnoDB
|
|
164
|
+
granularity. Ruby handlers that already started are not preempted.
|
|
162
165
|
|
|
163
166
|
A synchronous call made while the Solid Objects connection already has an open
|
|
164
167
|
transaction raises `SolidObjects::SyncInsideTransaction` before the message is
|
data/docs/operations.md
CHANGED
|
@@ -23,6 +23,12 @@ Start all configured roles:
|
|
|
23
23
|
bundle exec solid_objects start
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
The command loads the host application's `app/actors` directories before
|
|
27
|
+
starting any runtime role, even when Rails eager loading is disabled. Actors in
|
|
28
|
+
the conventional directory do not need initializer references. The targeted
|
|
29
|
+
loader participates in Rails preparation callbacks so a development reload can
|
|
30
|
+
replace a registered actor class without loading unrelated application code.
|
|
31
|
+
|
|
26
32
|
Inspect process records and clean stale ownership:
|
|
27
33
|
|
|
28
34
|
```bash
|
|
@@ -13,7 +13,9 @@ module SolidObjects
|
|
|
13
13
|
# @rbs (lease: Lease) -> void
|
|
14
14
|
def initialize(lease:)
|
|
15
15
|
@lease = lease
|
|
16
|
-
instance =
|
|
16
|
+
instance = SolidObjects.database_adapter.with_lock_retry do
|
|
17
|
+
Instance.find(lease.instance_id)
|
|
18
|
+
end
|
|
17
19
|
@actor_class = SolidObjects.registry.fetch(instance.actor_type)
|
|
18
20
|
@actor = build_actor(instance)
|
|
19
21
|
@last_used_at = monotonic_now
|
|
@@ -74,10 +76,12 @@ module SolidObjects
|
|
|
74
76
|
|
|
75
77
|
# @rbs () -> void
|
|
76
78
|
def yield_ready_messages
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
SolidObjects.database_adapter.transaction do
|
|
80
|
+
now = SolidObjects.database_adapter.database_now
|
|
81
|
+
ReadyMessage
|
|
82
|
+
.where(instance_id: lease.instance_id, available_at: ..now)
|
|
83
|
+
.update_all(available_at: now)
|
|
84
|
+
end
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
# @rbs (Hash[String, untyped]) -> void
|
|
@@ -18,7 +18,7 @@ module SolidObjects
|
|
|
18
18
|
|
|
19
19
|
mutex.synchronize do
|
|
20
20
|
existing = actors[actor_type]
|
|
21
|
-
if existing && existing != actor_class
|
|
21
|
+
if existing && existing != actor_class && !reload_of?(existing, actor_class)
|
|
22
22
|
raise InvalidActor, "#{actor_type.inspect} is already registered by #{existing.name}"
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -61,5 +61,10 @@ module SolidObjects
|
|
|
61
61
|
|
|
62
62
|
raise InvalidActor, "registered actor must inherit from SolidObjects::Actor"
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
# @rbs (Class, Class) -> bool
|
|
66
|
+
def reload_of?(existing, candidate)
|
|
67
|
+
!existing.name.nil? && existing.name == candidate.name
|
|
68
|
+
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class ApplicationActorLoader
|
|
5
|
+
# @rbs @application: untyped
|
|
6
|
+
# @rbs @autoloader: untyped
|
|
7
|
+
|
|
8
|
+
# @rbs (?application: untyped, ?autoloader: untyped) -> void
|
|
9
|
+
def initialize(application: Rails.application, autoloader: Rails.autoloaders.main)
|
|
10
|
+
@application = application
|
|
11
|
+
@autoloader = autoloader
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @rbs () -> void
|
|
15
|
+
def call
|
|
16
|
+
actor_directories.each { |directory| autoloader.eager_load_dir(directory) }
|
|
17
|
+
current_actor_classes.each(&:ensure_registered!)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs () -> void
|
|
21
|
+
def install
|
|
22
|
+
application.reloader.to_prepare { call }
|
|
23
|
+
call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :application, :autoloader
|
|
29
|
+
|
|
30
|
+
# @rbs () -> Array[String]
|
|
31
|
+
def actor_directories
|
|
32
|
+
configured_directories = application.paths["app/actors"]&.existent || []
|
|
33
|
+
conventional_directories = application.paths["app"].existent.select do |application_directory|
|
|
34
|
+
File.basename(application_directory) == "actors"
|
|
35
|
+
end
|
|
36
|
+
managed_directories = autoloader.dirs.map { |directory| File.expand_path(directory) }
|
|
37
|
+
|
|
38
|
+
(configured_directories + conventional_directories)
|
|
39
|
+
.select { |directory| Dir.exist?(directory) }
|
|
40
|
+
.map { |directory| File.expand_path(directory) }
|
|
41
|
+
.select { |directory| managed_directories.include?(directory) }
|
|
42
|
+
.uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs () -> Array[Class]
|
|
46
|
+
def current_actor_classes
|
|
47
|
+
Actor.descendants.select do |actor_class|
|
|
48
|
+
actor_class.name &&
|
|
49
|
+
actor_class.name.safe_constantize.equal?(actor_class)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -52,19 +52,21 @@ module SolidObjects
|
|
|
52
52
|
def reusable_registry?
|
|
53
53
|
return false unless registry&.process_record
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
SolidObjects.database_adapter.with_lock_retry do
|
|
56
|
+
registry.process_record.reload.shutdown_state == "running"
|
|
57
|
+
end
|
|
56
58
|
rescue ActiveRecord::RecordNotFound
|
|
57
59
|
false
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# @rbs () -> ProcessRegistry
|
|
61
63
|
def register
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
process_registry = ProcessRegistry.new
|
|
65
|
+
process_registry.register(
|
|
64
66
|
kind: "caller",
|
|
65
67
|
metadata: { execution: "synchronous" }
|
|
66
68
|
)
|
|
67
|
-
registry
|
|
69
|
+
@registry = process_registry
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
# @rbs () -> void
|
data/lib/solid_objects/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# rbs_inline: enabled
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
+
require "solid_objects/application_actor_loader"
|
|
4
5
|
|
|
5
6
|
module SolidObjects
|
|
6
7
|
class CLI < Thor
|
|
@@ -133,6 +134,7 @@ module SolidObjects
|
|
|
133
134
|
end
|
|
134
135
|
|
|
135
136
|
require path
|
|
137
|
+
ApplicationActorLoader.new.install
|
|
136
138
|
end
|
|
137
139
|
|
|
138
140
|
# @rbs (Symbol, Integer) -> Integer
|
data/lib/solid_objects/client.rb
CHANGED
|
@@ -65,27 +65,33 @@ module SolidObjects
|
|
|
65
65
|
|
|
66
66
|
# @rbs (MessageReference, timeout: Numeric, ?authorization_context: untyped) -> untyped
|
|
67
67
|
def wait(message_reference, timeout:, authorization_context: nil)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
SyncDeadline.with(timeout:) do
|
|
69
|
+
message = SolidObjects.database_adapter.with_lock_retry do
|
|
70
|
+
Message.find(message_reference.id)
|
|
71
|
+
end
|
|
72
|
+
validate_message_reference!(message_reference, message)
|
|
73
|
+
reference = Reference.new(
|
|
74
|
+
actor_type: message.actor_type,
|
|
75
|
+
actor_id: message.actor_id
|
|
76
|
+
)
|
|
77
|
+
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
78
|
+
query = actor_class.definition.queries.key?(message.message_name.to_sym)
|
|
79
|
+
actor_message = actor_class.definition.messages.key?(message.message_name.to_sym)
|
|
80
|
+
unless query || actor_message
|
|
81
|
+
raise UnknownMessage, "unknown message #{message.message_name.inspect}"
|
|
82
|
+
end
|
|
83
|
+
authorize!(
|
|
84
|
+
query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
85
|
+
reference,
|
|
86
|
+
message.message_name,
|
|
87
|
+
message.arguments,
|
|
88
|
+
authorization_context:
|
|
89
|
+
)
|
|
90
|
+
reject_sync_inside_transaction!(reference, message.message_name)
|
|
91
|
+
SynchronousInvocation.new.call(message_reference, timeout:)
|
|
79
92
|
end
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
reference,
|
|
83
|
-
message.message_name,
|
|
84
|
-
message.arguments,
|
|
85
|
-
authorization_context:
|
|
86
|
-
)
|
|
87
|
-
reject_sync_inside_transaction!(reference, message.message_name)
|
|
88
|
-
SynchronousInvocation.new.call(message_reference, timeout:)
|
|
93
|
+
rescue DatabaseDeadlineExceeded
|
|
94
|
+
raise SyncDiagnostics.new.database_contention_for(message_reference, timeout:)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
# @rbs (Reference, ?authorization_context: untyped) -> StateSnapshot
|
|
@@ -52,6 +52,16 @@ module SolidObjects
|
|
|
52
52
|
value.is_a?(Time) ? value.utc : Time.parse("#{value} UTC").utc
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# @rbs () { () -> untyped } -> untyped
|
|
56
|
+
def with_lock_retry
|
|
57
|
+
yield
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs () { () -> untyped } -> untyped
|
|
61
|
+
def with_lock_probe
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
# @rbs () { () -> untyped } -> untyped
|
|
56
66
|
def transaction(&block)
|
|
57
67
|
raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
|
|
@@ -4,6 +4,8 @@ module SolidObjects
|
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class Sqlite < DatabaseAdapter
|
|
6
6
|
LOCK_RETRY_INTERVAL = 0.001
|
|
7
|
+
LOCK_RETRY_MUTEX = Thread::Mutex.new
|
|
8
|
+
LOCK_RETRY_CONDITION = Thread::ConditionVariable.new
|
|
7
9
|
|
|
8
10
|
# @rbs () -> String
|
|
9
11
|
def current_time_expression
|
|
@@ -14,14 +16,51 @@ module SolidObjects
|
|
|
14
16
|
def transaction(&block)
|
|
15
17
|
return super unless SyncDeadline.active?
|
|
16
18
|
|
|
17
|
-
super
|
|
19
|
+
with_lock_retry { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () { () -> untyped } -> untyped
|
|
23
|
+
def with_lock_retry
|
|
24
|
+
return yield unless SyncDeadline.active?
|
|
25
|
+
|
|
26
|
+
raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
|
|
27
|
+
|
|
28
|
+
with_connection do |connection|
|
|
29
|
+
with_transaction_deadline(connection) { yield }
|
|
30
|
+
end
|
|
18
31
|
rescue DatabaseDeadlineExceeded
|
|
19
32
|
raise if SyncDeadline.expired?
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
wait_before_retry
|
|
35
|
+
retry
|
|
36
|
+
rescue => error
|
|
37
|
+
raise unless deadline_error?(error)
|
|
38
|
+
|
|
39
|
+
if SyncDeadline.expired?
|
|
40
|
+
raise DatabaseDeadlineExceeded,
|
|
41
|
+
"database lock wait exceeded the synchronous invocation deadline",
|
|
42
|
+
cause: error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
wait_before_retry
|
|
22
46
|
retry
|
|
23
47
|
end
|
|
24
48
|
|
|
49
|
+
# @rbs () { () -> untyped } -> untyped
|
|
50
|
+
def with_lock_probe
|
|
51
|
+
return yield unless SyncDeadline.active?
|
|
52
|
+
|
|
53
|
+
with_connection do |connection|
|
|
54
|
+
with_transaction_deadline(connection) { yield }
|
|
55
|
+
end
|
|
56
|
+
rescue => error
|
|
57
|
+
raise unless deadline_error?(error)
|
|
58
|
+
|
|
59
|
+
raise DatabaseDeadlineExceeded,
|
|
60
|
+
"database remained locked at the synchronous invocation deadline",
|
|
61
|
+
cause: error
|
|
62
|
+
end
|
|
63
|
+
|
|
25
64
|
private
|
|
26
65
|
|
|
27
66
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
@@ -47,6 +86,16 @@ module SolidObjects
|
|
|
47
86
|
end
|
|
48
87
|
false
|
|
49
88
|
end
|
|
89
|
+
|
|
90
|
+
# @rbs () -> void
|
|
91
|
+
def wait_before_retry
|
|
92
|
+
LOCK_RETRY_MUTEX.synchronize do
|
|
93
|
+
LOCK_RETRY_CONDITION.wait(
|
|
94
|
+
LOCK_RETRY_MUTEX,
|
|
95
|
+
[ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
50
99
|
end
|
|
51
100
|
end
|
|
52
101
|
end
|
|
@@ -80,18 +80,21 @@ module SolidObjects
|
|
|
80
80
|
|
|
81
81
|
# @rbs (?kind: String, ?metadata: Hash[String | Symbol, untyped]) -> Process
|
|
82
82
|
def register(kind: "worker", metadata: {})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
process_record = SolidObjects.database_adapter.with_lock_retry do
|
|
84
|
+
now = SolidObjects.database_adapter.database_now
|
|
85
|
+
Process.create!(
|
|
86
|
+
id: SecureRandom.uuid,
|
|
87
|
+
kind:,
|
|
88
|
+
hostname: Socket.gethostname,
|
|
89
|
+
pid: ::Process.pid,
|
|
90
|
+
started_at: now,
|
|
91
|
+
last_heartbeat_at: now,
|
|
92
|
+
metadata: Serialization.dump(default_metadata.merge(metadata))
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
@process_record = process_record
|
|
93
96
|
@last_heartbeat_at = monotonic_now
|
|
94
|
-
|
|
97
|
+
process_record
|
|
95
98
|
end
|
|
96
99
|
|
|
97
100
|
# @rbs () -> bool
|
|
@@ -99,9 +102,13 @@ module SolidObjects
|
|
|
99
102
|
return false unless process_record
|
|
100
103
|
return false if heartbeat_recent?
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
updated = SolidObjects.database_adapter.with_lock_retry do
|
|
106
|
+
process_record.update(
|
|
107
|
+
last_heartbeat_at: SolidObjects.database_adapter.database_now
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
@last_heartbeat_at = monotonic_now if updated
|
|
111
|
+
updated
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
# @rbs () -> bool
|
|
@@ -10,6 +10,62 @@ module SolidObjects
|
|
|
10
10
|
status = message_status(message)
|
|
11
11
|
waiting_on = waiting_reason(message, instance, blocker)
|
|
12
12
|
activation = activation_details(instance)
|
|
13
|
+
build_error(
|
|
14
|
+
message,
|
|
15
|
+
timeout:,
|
|
16
|
+
status:,
|
|
17
|
+
waiting_on:,
|
|
18
|
+
activation:,
|
|
19
|
+
blocker: blocker_details(blocker)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
24
|
+
def database_contention(message, timeout:)
|
|
25
|
+
build_error(
|
|
26
|
+
message,
|
|
27
|
+
timeout:,
|
|
28
|
+
status: "unknown",
|
|
29
|
+
waiting_on: "database_contention",
|
|
30
|
+
activation: {},
|
|
31
|
+
blocker: nil
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (MessageReference, timeout: Numeric) -> SyncTimeout
|
|
36
|
+
def database_contention_for(message_reference, timeout:)
|
|
37
|
+
error = SyncTimeout.new(
|
|
38
|
+
timeout:,
|
|
39
|
+
actor_type: message_reference.actor_type,
|
|
40
|
+
actor_id: message_reference.actor_id,
|
|
41
|
+
message_name: "unknown",
|
|
42
|
+
message_id: message_reference.id,
|
|
43
|
+
request_id: message_reference.request_id,
|
|
44
|
+
sequence: message_reference.sequence,
|
|
45
|
+
status: "unknown",
|
|
46
|
+
waiting_on: "database_contention",
|
|
47
|
+
activation: {},
|
|
48
|
+
blocker: nil
|
|
49
|
+
)
|
|
50
|
+
SolidObjects.instrument(
|
|
51
|
+
:"sync.timeout",
|
|
52
|
+
message_id: message_reference.id,
|
|
53
|
+
request_id: message_reference.request_id,
|
|
54
|
+
actor_type: message_reference.actor_type,
|
|
55
|
+
actor_id: message_reference.actor_id,
|
|
56
|
+
sequence: message_reference.sequence,
|
|
57
|
+
status: "unknown",
|
|
58
|
+
waiting_on: "database_contention",
|
|
59
|
+
activation_owner_id: nil,
|
|
60
|
+
activation_generation: nil
|
|
61
|
+
)
|
|
62
|
+
error
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @rbs (Message, timeout: Numeric, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> SyncTimeout
|
|
68
|
+
def build_error(message, timeout:, status:, waiting_on:, activation:, blocker:)
|
|
13
69
|
error = SyncTimeout.new(
|
|
14
70
|
timeout:,
|
|
15
71
|
actor_type: message.actor_type,
|
|
@@ -21,7 +77,7 @@ module SolidObjects
|
|
|
21
77
|
status:,
|
|
22
78
|
waiting_on:,
|
|
23
79
|
activation:,
|
|
24
|
-
blocker:
|
|
80
|
+
blocker:
|
|
25
81
|
)
|
|
26
82
|
SolidObjects.instrument(
|
|
27
83
|
:"sync.timeout",
|
|
@@ -38,8 +94,6 @@ module SolidObjects
|
|
|
38
94
|
error
|
|
39
95
|
end
|
|
40
96
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
97
|
# @rbs (Message) -> String
|
|
44
98
|
def message_status(message)
|
|
45
99
|
return "rejected" if message.rejected?
|
|
@@ -22,33 +22,49 @@ module SolidObjects
|
|
|
22
22
|
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
23
23
|
def call_before_deadline(message_reference, timeout:)
|
|
24
24
|
deadline = monotonic_now + SyncDeadline.remaining
|
|
25
|
+
message = nil
|
|
25
26
|
|
|
26
27
|
loop do
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
remaining = deadline - monotonic_now
|
|
31
|
-
unless remaining.positive?
|
|
32
|
-
message = load_message(message_reference)
|
|
33
|
-
return completed_result(message) if message.completed? || message.dead?
|
|
28
|
+
unless (deadline - monotonic_now).positive?
|
|
29
|
+
final_message = load_message_at_deadline(message_reference)
|
|
30
|
+
return completed_result(final_message) if final_message&.completed? || final_message&.dead?
|
|
34
31
|
|
|
35
|
-
raise
|
|
32
|
+
raise final_message ?
|
|
33
|
+
diagnose_timeout(final_message, timeout:) :
|
|
34
|
+
contention_timeout(message, message_reference, timeout:)
|
|
36
35
|
end
|
|
37
36
|
|
|
37
|
+
message = load_message(message_reference)
|
|
38
|
+
return completed_result(message) if message.completed? || message.dead?
|
|
39
|
+
|
|
38
40
|
processed = assist(message, deadline:)
|
|
39
41
|
remaining = deadline - monotonic_now
|
|
40
42
|
wait(remaining) if processed.zero? && remaining.positive?
|
|
41
43
|
end
|
|
42
44
|
rescue DatabaseDeadlineExceeded
|
|
43
|
-
message
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
raise SyncDiagnostics.new.call(message, timeout:)
|
|
45
|
+
raise message ?
|
|
46
|
+
diagnose_timeout(message, timeout:) :
|
|
47
|
+
contention_timeout(message, message_reference, timeout:)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# @rbs (MessageReference) -> Message
|
|
50
51
|
def load_message(message_reference)
|
|
51
|
-
|
|
52
|
+
SolidObjects.database_adapter.with_lock_retry do
|
|
53
|
+
Message.uncached do
|
|
54
|
+
Message.includes(:dead_letter).find(message_reference.id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @rbs (MessageReference) -> Message?
|
|
60
|
+
def load_message_at_deadline(message_reference)
|
|
61
|
+
SolidObjects.database_adapter.with_lock_probe do
|
|
62
|
+
Message.uncached do
|
|
63
|
+
Message.includes(:dead_letter).find(message_reference.id)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue DatabaseDeadlineExceeded
|
|
67
|
+
nil
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
# @rbs (Message) -> untyped
|
|
@@ -104,6 +120,22 @@ module SolidObjects
|
|
|
104
120
|
)
|
|
105
121
|
end
|
|
106
122
|
|
|
123
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
124
|
+
def diagnose_timeout(message, timeout:)
|
|
125
|
+
SolidObjects.database_adapter.with_lock_probe do
|
|
126
|
+
SyncDiagnostics.new.call(message, timeout:)
|
|
127
|
+
end
|
|
128
|
+
rescue DatabaseDeadlineExceeded
|
|
129
|
+
SyncDiagnostics.new.database_contention(message, timeout:)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @rbs (Message?, MessageReference, timeout: Numeric) -> SyncTimeout
|
|
133
|
+
def contention_timeout(message, message_reference, timeout:)
|
|
134
|
+
return SyncDiagnostics.new.database_contention(message, timeout:) if message
|
|
135
|
+
|
|
136
|
+
SyncDiagnostics.new.database_contention_for(message_reference, timeout:)
|
|
137
|
+
end
|
|
138
|
+
|
|
107
139
|
# @rbs () -> Float
|
|
108
140
|
def monotonic_now
|
|
109
141
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/application_actor_loader.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class ApplicationActorLoader
|
|
5
|
+
@application: untyped
|
|
6
|
+
|
|
7
|
+
@autoloader: untyped
|
|
8
|
+
|
|
9
|
+
# @rbs (?application: untyped, ?autoloader: untyped) -> void
|
|
10
|
+
def initialize: (?application: untyped, ?autoloader: untyped) -> void
|
|
11
|
+
|
|
12
|
+
# @rbs () -> void
|
|
13
|
+
def call: () -> void
|
|
14
|
+
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
def install: () -> void
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader application: untyped
|
|
21
|
+
|
|
22
|
+
attr_reader autoloader: untyped
|
|
23
|
+
|
|
24
|
+
# @rbs () -> Array[String]
|
|
25
|
+
def actor_directories: () -> Array[String]
|
|
26
|
+
|
|
27
|
+
# @rbs () -> Array[Class]
|
|
28
|
+
def current_actor_classes: () -> Array[Class]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -5,10 +5,10 @@ module SolidObjects
|
|
|
5
5
|
# @rbs (untyped) -> DatabaseAdapter
|
|
6
6
|
def self.for: (untyped) -> DatabaseAdapter
|
|
7
7
|
|
|
8
|
-
@connection_pool: untyped
|
|
9
|
-
|
|
10
8
|
@fixed_connection: untyped
|
|
11
9
|
|
|
10
|
+
@connection_pool: untyped
|
|
11
|
+
|
|
12
12
|
# @rbs (untyped) -> void
|
|
13
13
|
def initialize: (untyped) -> void
|
|
14
14
|
|
|
@@ -24,6 +24,12 @@ module SolidObjects
|
|
|
24
24
|
# @rbs () -> Time
|
|
25
25
|
def database_now: () -> Time
|
|
26
26
|
|
|
27
|
+
# @rbs () { () -> untyped } -> untyped
|
|
28
|
+
def with_lock_retry: () { () -> untyped } -> untyped
|
|
29
|
+
|
|
30
|
+
# @rbs () { () -> untyped } -> untyped
|
|
31
|
+
def with_lock_probe: () { () -> untyped } -> untyped
|
|
32
|
+
|
|
27
33
|
# @rbs () { () -> untyped } -> untyped
|
|
28
34
|
def transaction: () { () -> untyped } -> untyped
|
|
29
35
|
|
|
@@ -5,12 +5,22 @@ module SolidObjects
|
|
|
5
5
|
class Sqlite < DatabaseAdapter
|
|
6
6
|
LOCK_RETRY_INTERVAL: ::Float
|
|
7
7
|
|
|
8
|
+
LOCK_RETRY_MUTEX: untyped
|
|
9
|
+
|
|
10
|
+
LOCK_RETRY_CONDITION: untyped
|
|
11
|
+
|
|
8
12
|
# @rbs () -> String
|
|
9
13
|
def current_time_expression: () -> String
|
|
10
14
|
|
|
11
15
|
# @rbs () { () -> untyped } -> untyped
|
|
12
16
|
def transaction: () { () -> untyped } -> untyped
|
|
13
17
|
|
|
18
|
+
# @rbs () { () -> untyped } -> untyped
|
|
19
|
+
def with_lock_retry: () { () -> untyped } -> untyped
|
|
20
|
+
|
|
21
|
+
# @rbs () { () -> untyped } -> untyped
|
|
22
|
+
def with_lock_probe: () { () -> untyped } -> untyped
|
|
23
|
+
|
|
14
24
|
private
|
|
15
25
|
|
|
16
26
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
@@ -18,6 +28,9 @@ module SolidObjects
|
|
|
18
28
|
|
|
19
29
|
# @rbs (Exception) -> bool
|
|
20
30
|
def deadline_error?: (Exception) -> bool
|
|
31
|
+
|
|
32
|
+
# @rbs () -> void
|
|
33
|
+
def wait_before_retry: () -> void
|
|
21
34
|
end
|
|
22
35
|
end
|
|
23
36
|
end
|
|
@@ -5,8 +5,17 @@ module SolidObjects
|
|
|
5
5
|
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
6
6
|
def call: (Message, timeout: Numeric) -> SyncTimeout
|
|
7
7
|
|
|
8
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
9
|
+
def database_contention: (Message, timeout: Numeric) -> SyncTimeout
|
|
10
|
+
|
|
11
|
+
# @rbs (MessageReference, timeout: Numeric) -> SyncTimeout
|
|
12
|
+
def database_contention_for: (MessageReference, timeout: Numeric) -> SyncTimeout
|
|
13
|
+
|
|
8
14
|
private
|
|
9
15
|
|
|
16
|
+
# @rbs (Message, timeout: Numeric, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> SyncTimeout
|
|
17
|
+
def build_error: (Message, timeout: Numeric, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> SyncTimeout
|
|
18
|
+
|
|
10
19
|
# @rbs (Message) -> String
|
|
11
20
|
def message_status: (Message) -> String
|
|
12
21
|
|
|
@@ -13,6 +13,9 @@ module SolidObjects
|
|
|
13
13
|
# @rbs (MessageReference) -> Message
|
|
14
14
|
def load_message: (MessageReference) -> Message
|
|
15
15
|
|
|
16
|
+
# @rbs (MessageReference) -> Message?
|
|
17
|
+
def load_message_at_deadline: (MessageReference) -> Message?
|
|
18
|
+
|
|
16
19
|
# @rbs (Message) -> untyped
|
|
17
20
|
def completed_result: (Message) -> untyped
|
|
18
21
|
|
|
@@ -25,6 +28,12 @@ module SolidObjects
|
|
|
25
28
|
# @rbs (Numeric) -> void
|
|
26
29
|
def wait: (Numeric) -> void
|
|
27
30
|
|
|
31
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
32
|
+
def diagnose_timeout: (Message, timeout: Numeric) -> SyncTimeout
|
|
33
|
+
|
|
34
|
+
# @rbs (Message?, MessageReference, timeout: Numeric) -> SyncTimeout
|
|
35
|
+
def contention_timeout: (Message?, MessageReference, timeout: Numeric) -> SyncTimeout
|
|
36
|
+
|
|
28
37
|
# @rbs () -> Float
|
|
29
38
|
def monotonic_now: () -> Float
|
|
30
39
|
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.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
@@ -346,6 +346,7 @@ files:
|
|
|
346
346
|
- lib/solid_objects/actor_registry.rb
|
|
347
347
|
- lib/solid_objects/actor_snapshot.rb
|
|
348
348
|
- lib/solid_objects/actor_view.rb
|
|
349
|
+
- lib/solid_objects/application_actor_loader.rb
|
|
349
350
|
- lib/solid_objects/application_write_guard.rb
|
|
350
351
|
- lib/solid_objects/broadcast_executor.rb
|
|
351
352
|
- lib/solid_objects/caller_process.rb
|
|
@@ -415,6 +416,7 @@ files:
|
|
|
415
416
|
- sig/generated/lib/solid_objects/actor_registry.rbs
|
|
416
417
|
- sig/generated/lib/solid_objects/actor_snapshot.rbs
|
|
417
418
|
- sig/generated/lib/solid_objects/actor_view.rbs
|
|
419
|
+
- sig/generated/lib/solid_objects/application_actor_loader.rbs
|
|
418
420
|
- sig/generated/lib/solid_objects/application_write_guard.rbs
|
|
419
421
|
- sig/generated/lib/solid_objects/broadcast_executor.rbs
|
|
420
422
|
- sig/generated/lib/solid_objects/caller_process.rbs
|