solid_objects 0.2.1 → 0.3.0
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 +14 -0
- data/README.md +129 -8
- data/docs/architecture.md +62 -15
- data/docs/authorization.md +16 -3
- data/docs/benchmarks.md +6 -2
- data/docs/correctness.md +19 -1
- data/docs/database-schema.md +5 -0
- data/docs/development.md +40 -0
- data/docs/fit.md +10 -2
- data/docs/migrating-existing-state.md +8 -1
- data/docs/operations.md +74 -36
- data/docs/roadmap.md +8 -2
- data/docs/security.md +25 -3
- data/docs/state-migrations.md +2 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +30 -2
- data/lib/solid_objects/activation.rb +33 -4
- data/lib/solid_objects/actor.rb +47 -6
- data/lib/solid_objects/actor_definition.rb +2 -0
- data/lib/solid_objects/actor_snapshot.rb +10 -4
- data/lib/solid_objects/application_write_guard.rb +24 -0
- data/lib/solid_objects/caller_process.rb +28 -0
- data/lib/solid_objects/cli.rb +44 -5
- data/lib/solid_objects/client.rb +98 -5
- data/lib/solid_objects/commit_action_registry.rb +42 -0
- data/lib/solid_objects/configuration.rb +27 -1
- data/lib/solid_objects/database_adapter.rb +28 -1
- data/lib/solid_objects/database_adapters/mysql.rb +46 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +28 -0
- data/lib/solid_objects/errors.rb +144 -0
- data/lib/solid_objects/executor.rb +64 -4
- data/lib/solid_objects/instance_pruner.rb +97 -0
- data/lib/solid_objects/message_pruner.rb +97 -0
- data/lib/solid_objects/message_reference.rb +9 -0
- data/lib/solid_objects/process_pruner.rb +49 -0
- data/lib/solid_objects/reference.rb +5 -0
- data/lib/solid_objects/state_snapshot.rb +41 -0
- data/lib/solid_objects/sync_deadline.rb +57 -0
- data/lib/solid_objects/sync_diagnostics.rb +133 -0
- data/lib/solid_objects/synchronous_invocation.rb +26 -7
- data/lib/solid_objects/test_helper.rb +78 -0
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/worker.rb +1 -1
- data/lib/solid_objects.rb +34 -0
- data/sig/generated/lib/solid_objects/activation.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor.rbs +26 -0
- data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
- data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
- data/sig/generated/lib/solid_objects/cli.rbs +11 -2
- data/sig/generated/lib/solid_objects/client.rbs +15 -0
- data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +27 -7
- data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +8 -0
- data/sig/generated/lib/solid_objects/errors.rbs +118 -0
- data/sig/generated/lib/solid_objects/executor.rbs +12 -0
- data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
- data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
- data/sig/generated/lib/solid_objects/reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
- data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
- data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
- data/sig/generated/lib/solid_objects.rbs +12 -0
- metadata +19 -1
data/lib/solid_objects/client.rb
CHANGED
|
@@ -50,16 +50,57 @@ module SolidObjects
|
|
|
50
50
|
arguments,
|
|
51
51
|
authorization_context:
|
|
52
52
|
)
|
|
53
|
-
|
|
53
|
+
reject_sync_inside_transaction!(reference, message_name)
|
|
54
|
+
SyncDeadline.with(timeout:) do
|
|
55
|
+
message_reference = enqueue_sync(
|
|
56
|
+
reference,
|
|
57
|
+
message_name,
|
|
58
|
+
arguments,
|
|
59
|
+
idempotency_key:,
|
|
60
|
+
timeout:
|
|
61
|
+
)
|
|
62
|
+
SynchronousInvocation.new.call(message_reference, timeout:)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @rbs (MessageReference, timeout: Numeric, ?authorization_context: untyped) -> untyped
|
|
67
|
+
def wait(message_reference, timeout:, authorization_context: nil)
|
|
68
|
+
message = Message.find(message_reference.id)
|
|
69
|
+
validate_message_reference!(message_reference, message)
|
|
70
|
+
reference = Reference.new(
|
|
71
|
+
actor_type: message.actor_type,
|
|
72
|
+
actor_id: message.actor_id
|
|
73
|
+
)
|
|
74
|
+
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
75
|
+
query = actor_class.definition.queries.key?(message.message_name.to_sym)
|
|
76
|
+
actor_message = actor_class.definition.messages.key?(message.message_name.to_sym)
|
|
77
|
+
unless query || actor_message
|
|
78
|
+
raise UnknownMessage, "unknown message #{message.message_name.inspect}"
|
|
79
|
+
end
|
|
80
|
+
authorize!(
|
|
81
|
+
query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
54
82
|
reference,
|
|
55
|
-
message_name,
|
|
56
|
-
arguments,
|
|
57
|
-
|
|
58
|
-
idempotency_key:
|
|
83
|
+
message.message_name,
|
|
84
|
+
message.arguments,
|
|
85
|
+
authorization_context:
|
|
59
86
|
)
|
|
87
|
+
reject_sync_inside_transaction!(reference, message.message_name)
|
|
60
88
|
SynchronousInvocation.new.call(message_reference, timeout:)
|
|
61
89
|
end
|
|
62
90
|
|
|
91
|
+
# @rbs (Reference, ?authorization_context: untyped) -> StateSnapshot
|
|
92
|
+
def snapshot(reference, authorization_context: nil)
|
|
93
|
+
SolidObjects.registry.fetch(reference.actor_type)
|
|
94
|
+
authorize!(
|
|
95
|
+
SolidObjects.configuration.authorize_query,
|
|
96
|
+
reference,
|
|
97
|
+
"__snapshot__",
|
|
98
|
+
{},
|
|
99
|
+
authorization_context:
|
|
100
|
+
)
|
|
101
|
+
StateSnapshot.new(reference)
|
|
102
|
+
end
|
|
103
|
+
|
|
63
104
|
# @rbs (Reference, ?authorization_context: untyped) -> bool
|
|
64
105
|
def destroy(reference, authorization_context: nil)
|
|
65
106
|
raise ActorCallCycle, "actors cannot synchronously destroy another actor" if Context.current_actor
|
|
@@ -93,6 +134,58 @@ module SolidObjects
|
|
|
93
134
|
|
|
94
135
|
attr_reader :mailbox
|
|
95
136
|
|
|
137
|
+
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], idempotency_key: String?, timeout: Numeric) -> MessageReference
|
|
138
|
+
def enqueue_sync(reference, message_name, arguments, idempotency_key:, timeout:)
|
|
139
|
+
mailbox.enqueue(
|
|
140
|
+
reference,
|
|
141
|
+
message_name,
|
|
142
|
+
arguments,
|
|
143
|
+
kind: "sync",
|
|
144
|
+
idempotency_key:
|
|
145
|
+
)
|
|
146
|
+
rescue DatabaseDeadlineExceeded
|
|
147
|
+
SolidObjects.instrument(
|
|
148
|
+
:"sync.enqueue_timeout",
|
|
149
|
+
actor_type: reference.actor_type,
|
|
150
|
+
actor_id: reference.actor_id,
|
|
151
|
+
message_name: message_name.to_s
|
|
152
|
+
)
|
|
153
|
+
raise SyncEnqueueTimeout.new(
|
|
154
|
+
timeout:,
|
|
155
|
+
actor_type: reference.actor_type,
|
|
156
|
+
actor_id: reference.actor_id,
|
|
157
|
+
message_name: message_name.to_s
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs (MessageReference, Message) -> void
|
|
162
|
+
def validate_message_reference!(message_reference, message)
|
|
163
|
+
valid = message_reference.request_id == message.request_id &&
|
|
164
|
+
message_reference.actor_type == message.actor_type &&
|
|
165
|
+
message_reference.actor_id == message.actor_id &&
|
|
166
|
+
message_reference.sequence == message.sequence
|
|
167
|
+
return if valid
|
|
168
|
+
|
|
169
|
+
raise ActorDestroyed, "message reference no longer identifies this invocation"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @rbs (Reference, Symbol | String) -> void
|
|
173
|
+
def reject_sync_inside_transaction!(reference, message_name)
|
|
174
|
+
return unless SolidObjects::Record.connection.transaction_open?
|
|
175
|
+
|
|
176
|
+
SolidObjects.instrument(
|
|
177
|
+
:"sync.transaction_rejected",
|
|
178
|
+
actor_type: reference.actor_type,
|
|
179
|
+
actor_id: reference.actor_id,
|
|
180
|
+
message_name: message_name.to_s
|
|
181
|
+
)
|
|
182
|
+
raise SyncInsideTransaction.new(
|
|
183
|
+
actor_type: reference.actor_type,
|
|
184
|
+
actor_id: reference.actor_id,
|
|
185
|
+
message_name: message_name.to_s
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
96
189
|
# @rbs (Proc, Reference, Symbol | String, Hash[Symbol | String, untyped], authorization_context: untyped) -> void
|
|
97
190
|
def authorize!(hook, reference, message_name, arguments, authorization_context:)
|
|
98
191
|
authorized = hook.call(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
CommitActionContext = Data.define(
|
|
5
|
+
:message_id,
|
|
6
|
+
:request_id,
|
|
7
|
+
:actor_type,
|
|
8
|
+
:actor_id,
|
|
9
|
+
:activation_generation
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
class CommitActionRegistry
|
|
13
|
+
# @rbs @handlers: Hash[String, Proc]
|
|
14
|
+
# @rbs @mutex: Mutex
|
|
15
|
+
|
|
16
|
+
# @rbs () -> void
|
|
17
|
+
def initialize
|
|
18
|
+
@handlers = {}
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs (String | Symbol, Proc) -> Proc
|
|
23
|
+
def register(name, handler)
|
|
24
|
+
action_name = name.to_s
|
|
25
|
+
raise ArgumentError, "commit action name cannot be empty" if action_name.empty?
|
|
26
|
+
|
|
27
|
+
mutex.synchronize { handlers[action_name] = handler }
|
|
28
|
+
handler
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @rbs (String | Symbol) -> Proc
|
|
32
|
+
def fetch(name)
|
|
33
|
+
handlers.fetch(name.to_s) do
|
|
34
|
+
raise UnknownCommitAction, "no commit action registered for #{name.inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :handlers, :mutex
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -20,6 +20,11 @@ module SolidObjects
|
|
|
20
20
|
# @rbs @process_heartbeat_interval: Float
|
|
21
21
|
# @rbs @process_alive_threshold: Float
|
|
22
22
|
# @rbs @shutdown_timeout: Float
|
|
23
|
+
# @rbs @message_retention: Numeric
|
|
24
|
+
# @rbs @message_retention_by_actor_type: Hash[String, Numeric]
|
|
25
|
+
# @rbs @instance_retention_by_actor_type: Hash[String, Numeric]
|
|
26
|
+
# @rbs @process_retention: Numeric
|
|
27
|
+
# @rbs @prune_batch_size: Integer
|
|
23
28
|
# @rbs @worker_count: Integer
|
|
24
29
|
# @rbs @effect_worker_count: Integer
|
|
25
30
|
# @rbs @broadcast_worker_count: Integer
|
|
@@ -53,6 +58,11 @@ module SolidObjects
|
|
|
53
58
|
:process_heartbeat_interval,
|
|
54
59
|
:process_alive_threshold,
|
|
55
60
|
:shutdown_timeout,
|
|
61
|
+
:message_retention,
|
|
62
|
+
:message_retention_by_actor_type,
|
|
63
|
+
:instance_retention_by_actor_type,
|
|
64
|
+
:process_retention,
|
|
65
|
+
:prune_batch_size,
|
|
56
66
|
:worker_count,
|
|
57
67
|
:effect_worker_count,
|
|
58
68
|
:broadcast_worker_count,
|
|
@@ -88,6 +98,11 @@ module SolidObjects
|
|
|
88
98
|
@process_heartbeat_interval = 15.0
|
|
89
99
|
@process_alive_threshold = 60.0
|
|
90
100
|
@shutdown_timeout = 15.0
|
|
101
|
+
@message_retention = 30.days
|
|
102
|
+
@message_retention_by_actor_type = {}
|
|
103
|
+
@instance_retention_by_actor_type = {}
|
|
104
|
+
@process_retention = 7.days
|
|
105
|
+
@prune_batch_size = 1_000
|
|
91
106
|
@worker_count = 1
|
|
92
107
|
@effect_worker_count = 1
|
|
93
108
|
@broadcast_worker_count = 1
|
|
@@ -128,6 +143,14 @@ module SolidObjects
|
|
|
128
143
|
positive_values.each do |name, value|
|
|
129
144
|
raise ArgumentError, "#{name} must be positive" unless value.positive?
|
|
130
145
|
end
|
|
146
|
+
message_retention_by_actor_type.each do |actor_type, retention|
|
|
147
|
+
raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
|
|
148
|
+
raise ArgumentError, "message retention must be positive" unless retention.positive?
|
|
149
|
+
end
|
|
150
|
+
instance_retention_by_actor_type.each do |actor_type, retention|
|
|
151
|
+
raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
|
|
152
|
+
raise ArgumentError, "instance retention must be positive" unless retention.positive?
|
|
153
|
+
end
|
|
131
154
|
|
|
132
155
|
self
|
|
133
156
|
end
|
|
@@ -151,7 +174,10 @@ module SolidObjects
|
|
|
151
174
|
max_attempts:,
|
|
152
175
|
process_heartbeat_interval:,
|
|
153
176
|
process_alive_threshold:,
|
|
154
|
-
shutdown_timeout
|
|
177
|
+
shutdown_timeout:,
|
|
178
|
+
message_retention:,
|
|
179
|
+
process_retention:,
|
|
180
|
+
prune_batch_size:
|
|
155
181
|
}
|
|
156
182
|
end
|
|
157
183
|
|
|
@@ -54,9 +54,22 @@ module SolidObjects
|
|
|
54
54
|
|
|
55
55
|
# @rbs () { () -> untyped } -> untyped
|
|
56
56
|
def transaction(&block)
|
|
57
|
+
raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
|
|
58
|
+
|
|
57
59
|
with_connection do |connection|
|
|
58
|
-
connection
|
|
60
|
+
with_transaction_deadline(connection) do
|
|
61
|
+
connection.transaction(requires_new: true) do
|
|
62
|
+
configure_transaction_deadline(connection)
|
|
63
|
+
block.call
|
|
64
|
+
end
|
|
65
|
+
end
|
|
59
66
|
end
|
|
67
|
+
rescue => error
|
|
68
|
+
raise unless deadline_error?(error)
|
|
69
|
+
|
|
70
|
+
raise DatabaseDeadlineExceeded,
|
|
71
|
+
"database lock wait exceeded the synchronous invocation deadline",
|
|
72
|
+
cause: error
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
# @rbs (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
|
|
@@ -68,6 +81,20 @@ module SolidObjects
|
|
|
68
81
|
|
|
69
82
|
attr_reader :connection_pool, :fixed_connection
|
|
70
83
|
|
|
84
|
+
# @rbs (untyped) { () -> untyped } -> untyped
|
|
85
|
+
def with_transaction_deadline(_connection)
|
|
86
|
+
yield
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @rbs (untyped) -> void
|
|
90
|
+
def configure_transaction_deadline(_connection)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @rbs (Exception) -> bool
|
|
94
|
+
def deadline_error?(_error)
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
71
98
|
# @rbs () { (untyped) -> untyped } -> untyped
|
|
72
99
|
def with_connection
|
|
73
100
|
return yield fixed_connection unless connection_pool
|
|
@@ -17,6 +17,52 @@ module SolidObjects
|
|
|
17
17
|
def current_time_expression
|
|
18
18
|
"CURRENT_TIMESTAMP(6)"
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# @rbs (untyped) { () -> untyped } -> untyped
|
|
24
|
+
def with_transaction_deadline(connection)
|
|
25
|
+
return yield unless SyncDeadline.active?
|
|
26
|
+
|
|
27
|
+
previous_lock_timeout = connection.select_value(
|
|
28
|
+
"SELECT @@SESSION.innodb_lock_wait_timeout"
|
|
29
|
+
).to_i
|
|
30
|
+
previous_execution_timeout = connection.select_value(
|
|
31
|
+
"SELECT @@SESSION.max_execution_time"
|
|
32
|
+
).to_i
|
|
33
|
+
connection.execute(
|
|
34
|
+
"SET SESSION innodb_lock_wait_timeout = #{SyncDeadline.remaining_seconds}"
|
|
35
|
+
)
|
|
36
|
+
connection.execute(
|
|
37
|
+
"SET SESSION max_execution_time = #{SyncDeadline.remaining_milliseconds}"
|
|
38
|
+
)
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
if previous_lock_timeout
|
|
42
|
+
connection.execute(
|
|
43
|
+
"SET SESSION innodb_lock_wait_timeout = #{previous_lock_timeout}"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
if previous_execution_timeout
|
|
47
|
+
connection.execute(
|
|
48
|
+
"SET SESSION max_execution_time = #{previous_execution_timeout}"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs (Exception) -> bool
|
|
54
|
+
def deadline_error?(error)
|
|
55
|
+
return false unless SyncDeadline.active?
|
|
56
|
+
return true if error.is_a?(ActiveRecord::LockWaitTimeout)
|
|
57
|
+
|
|
58
|
+
cause = error
|
|
59
|
+
while cause
|
|
60
|
+
return true if cause.respond_to?(:error_number) && cause.error_number == 3024
|
|
61
|
+
|
|
62
|
+
cause = cause.cause
|
|
63
|
+
end
|
|
64
|
+
false
|
|
65
|
+
end
|
|
20
66
|
end
|
|
21
67
|
end
|
|
22
68
|
end
|
|
@@ -12,6 +12,35 @@ module SolidObjects
|
|
|
12
12
|
def claim_lock
|
|
13
13
|
"FOR UPDATE SKIP LOCKED"
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# @rbs (untyped) -> void
|
|
19
|
+
def configure_transaction_deadline(connection)
|
|
20
|
+
return unless SyncDeadline.active?
|
|
21
|
+
|
|
22
|
+
connection.execute(
|
|
23
|
+
"SET LOCAL lock_timeout = #{SyncDeadline.remaining_milliseconds}"
|
|
24
|
+
)
|
|
25
|
+
connection.execute(
|
|
26
|
+
"SET LOCAL statement_timeout = #{SyncDeadline.remaining_milliseconds}"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs (Exception) -> bool
|
|
31
|
+
def deadline_error?(error)
|
|
32
|
+
return false unless SyncDeadline.active?
|
|
33
|
+
return true if error.is_a?(ActiveRecord::LockWaitTimeout)
|
|
34
|
+
|
|
35
|
+
cause = error
|
|
36
|
+
while cause
|
|
37
|
+
result = cause.respond_to?(:result) ? cause.result : nil
|
|
38
|
+
return true if result&.error_field(PG::Result::PG_DIAG_SQLSTATE) == "57014"
|
|
39
|
+
|
|
40
|
+
cause = cause.cause
|
|
41
|
+
end
|
|
42
|
+
false
|
|
43
|
+
end
|
|
15
44
|
end
|
|
16
45
|
end
|
|
17
46
|
end
|
|
@@ -7,6 +7,34 @@ module SolidObjects
|
|
|
7
7
|
def current_time_expression
|
|
8
8
|
"STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @rbs (untyped) { () -> untyped } -> untyped
|
|
14
|
+
def with_transaction_deadline(connection)
|
|
15
|
+
return yield unless SyncDeadline.active?
|
|
16
|
+
|
|
17
|
+
previous_timeout = connection.select_value("PRAGMA busy_timeout").to_i
|
|
18
|
+
connection.execute(
|
|
19
|
+
"PRAGMA busy_timeout = #{SyncDeadline.remaining_milliseconds}"
|
|
20
|
+
)
|
|
21
|
+
yield
|
|
22
|
+
ensure
|
|
23
|
+
connection.execute("PRAGMA busy_timeout = #{previous_timeout}") if previous_timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs (Exception) -> bool
|
|
27
|
+
def deadline_error?(error)
|
|
28
|
+
return false unless SyncDeadline.active?
|
|
29
|
+
|
|
30
|
+
cause = error
|
|
31
|
+
while cause
|
|
32
|
+
return true if cause.class.name.match?(/BusyException|BusyError/)
|
|
33
|
+
|
|
34
|
+
cause = cause.cause
|
|
35
|
+
end
|
|
36
|
+
false
|
|
37
|
+
end
|
|
10
38
|
end
|
|
11
39
|
end
|
|
12
40
|
end
|
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -4,6 +4,9 @@ module SolidObjects
|
|
|
4
4
|
class Error < StandardError
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
class NonRetryableError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
7
10
|
class UnsupportedDatabase < Error
|
|
8
11
|
end
|
|
9
12
|
|
|
@@ -40,7 +43,148 @@ module SolidObjects
|
|
|
40
43
|
class ActorDestroyed < LostActivation
|
|
41
44
|
end
|
|
42
45
|
|
|
46
|
+
class DatabaseDeadlineExceeded < Error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class SyncEnqueueTimeout < Error
|
|
50
|
+
# @rbs @timeout: Numeric
|
|
51
|
+
# @rbs @actor_type: String
|
|
52
|
+
# @rbs @actor_id: String
|
|
53
|
+
# @rbs @message_name: String
|
|
54
|
+
|
|
55
|
+
attr_reader :timeout, :actor_type, :actor_id, :message_name
|
|
56
|
+
|
|
57
|
+
# @rbs (timeout: Numeric, actor_type: String, actor_id: String, message_name: String) -> void
|
|
58
|
+
def initialize(timeout:, actor_type:, actor_id:, message_name:)
|
|
59
|
+
@timeout = timeout
|
|
60
|
+
@actor_type = actor_type
|
|
61
|
+
@actor_id = actor_id
|
|
62
|
+
@message_name = message_name
|
|
63
|
+
super(
|
|
64
|
+
"actor invocation could not be durably enqueued within #{timeout} seconds for " \
|
|
65
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name}"
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
43
70
|
class SyncTimeout < Error
|
|
71
|
+
# @rbs @timeout: Numeric
|
|
72
|
+
# @rbs @actor_type: String
|
|
73
|
+
# @rbs @actor_id: String
|
|
74
|
+
# @rbs @message_name: String
|
|
75
|
+
# @rbs @message_id: Integer
|
|
76
|
+
# @rbs @request_id: String
|
|
77
|
+
# @rbs @sequence: Integer
|
|
78
|
+
# @rbs @status: String
|
|
79
|
+
# @rbs @waiting_on: String
|
|
80
|
+
# @rbs @activation: Hash[String, untyped]
|
|
81
|
+
# @rbs @blocker: Hash[String, untyped]?
|
|
82
|
+
|
|
83
|
+
attr_reader :timeout,
|
|
84
|
+
:actor_type,
|
|
85
|
+
:actor_id,
|
|
86
|
+
:message_name,
|
|
87
|
+
:message_id,
|
|
88
|
+
:request_id,
|
|
89
|
+
:sequence,
|
|
90
|
+
:status,
|
|
91
|
+
:waiting_on,
|
|
92
|
+
:activation,
|
|
93
|
+
:blocker
|
|
94
|
+
|
|
95
|
+
# @rbs (timeout: Numeric, actor_type: String, actor_id: String, message_name: String, message_id: Integer, request_id: String, sequence: Integer, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> void
|
|
96
|
+
def initialize(
|
|
97
|
+
timeout:,
|
|
98
|
+
actor_type:,
|
|
99
|
+
actor_id:,
|
|
100
|
+
message_name:,
|
|
101
|
+
message_id:,
|
|
102
|
+
request_id:,
|
|
103
|
+
sequence:,
|
|
104
|
+
status:,
|
|
105
|
+
waiting_on:,
|
|
106
|
+
activation:,
|
|
107
|
+
blocker:
|
|
108
|
+
)
|
|
109
|
+
@timeout = timeout
|
|
110
|
+
@actor_type = actor_type
|
|
111
|
+
@actor_id = actor_id
|
|
112
|
+
@message_name = message_name
|
|
113
|
+
@message_id = message_id
|
|
114
|
+
@request_id = request_id
|
|
115
|
+
@sequence = sequence
|
|
116
|
+
@status = status
|
|
117
|
+
@waiting_on = waiting_on
|
|
118
|
+
@activation = Serialization.readonly_copy(activation)
|
|
119
|
+
@blocker = Serialization.readonly_copy(blocker)
|
|
120
|
+
super(
|
|
121
|
+
"actor invocation timed out after #{timeout} seconds for " \
|
|
122
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} " \
|
|
123
|
+
"message_id=#{message_id} sequence=#{sequence} status=#{status} waiting_on=#{waiting_on}"
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @rbs () -> MessageReference
|
|
128
|
+
def message_reference
|
|
129
|
+
MessageReference.new(
|
|
130
|
+
id: message_id,
|
|
131
|
+
request_id:,
|
|
132
|
+
actor_type:,
|
|
133
|
+
actor_id:,
|
|
134
|
+
sequence:
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class SyncInsideTransaction < Error
|
|
140
|
+
# @rbs @actor_type: String
|
|
141
|
+
# @rbs @actor_id: String
|
|
142
|
+
# @rbs @message_name: String
|
|
143
|
+
|
|
144
|
+
attr_reader :actor_type, :actor_id, :message_name
|
|
145
|
+
|
|
146
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
147
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
148
|
+
@actor_type = actor_type
|
|
149
|
+
@actor_id = actor_id
|
|
150
|
+
@message_name = message_name
|
|
151
|
+
super(
|
|
152
|
+
"cannot synchronously invoke #{actor_type}(#{actor_id.inspect}).#{message_name} " \
|
|
153
|
+
"inside an open database transaction; call it before the transaction or enqueue it with async"
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class ApplicationWriteForbidden < NonRetryableError
|
|
159
|
+
# @rbs @actor_type: String
|
|
160
|
+
# @rbs @actor_id: String
|
|
161
|
+
# @rbs @message_name: String
|
|
162
|
+
|
|
163
|
+
attr_reader :actor_type, :actor_id, :message_name
|
|
164
|
+
|
|
165
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
166
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
167
|
+
@actor_type = actor_type
|
|
168
|
+
@actor_id = actor_id
|
|
169
|
+
@message_name = message_name
|
|
170
|
+
super(
|
|
171
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} attempted an Active Record write; " \
|
|
172
|
+
"mutate actor state, stage a commit action, or emit a durable effect instead"
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class CommitActionUnavailable < NonRetryableError
|
|
178
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
179
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
180
|
+
super(
|
|
181
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} staged a commit action, " \
|
|
182
|
+
"but Solid Objects uses a separate database from Active Record"
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class UnknownCommitAction < NonRetryableError
|
|
44
188
|
end
|
|
45
189
|
|
|
46
190
|
class Rejected < Error
|
|
@@ -24,9 +24,7 @@ module SolidObjects
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
SolidObjects.instrument(:"message.started", **instrumentation_payload)
|
|
27
|
-
result =
|
|
28
|
-
actor.invoke(message.message_name, message.arguments)
|
|
29
|
-
end
|
|
27
|
+
result = invoke_actor(message_context)
|
|
30
28
|
ensure_query_did_not_mutate_state!(state_before)
|
|
31
29
|
observable_changes = changed_observables(observables_before, actor.observable_values)
|
|
32
30
|
complete(result, observable_changes)
|
|
@@ -54,6 +52,13 @@ module SolidObjects
|
|
|
54
52
|
activation.actor
|
|
55
53
|
end
|
|
56
54
|
|
|
55
|
+
# @rbs (MessageContext) -> untyped
|
|
56
|
+
def invoke_actor(message_context)
|
|
57
|
+
Context.with(actor:, message: message_context) do
|
|
58
|
+
actor.invoke(message.message_name, message.arguments)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
# @rbs (Hash[String, untyped]) -> void
|
|
58
63
|
def ensure_query_did_not_mutate_state!(state_before)
|
|
59
64
|
return unless message.message_kind == "sync"
|
|
@@ -81,6 +86,7 @@ module SolidObjects
|
|
|
81
86
|
max_bytes: SolidObjects.configuration.max_result_bytes
|
|
82
87
|
)
|
|
83
88
|
effect_intents = actor.drain_effect_intents
|
|
89
|
+
commit_action_intents = actor.drain_commit_action_intents
|
|
84
90
|
reminder_intents = actor.drain_reminder_intents
|
|
85
91
|
outbound_message_intents = actor.drain_outbound_message_intents
|
|
86
92
|
enqueued_effects = []
|
|
@@ -88,6 +94,7 @@ module SolidObjects
|
|
|
88
94
|
activation.lease.fenced_transaction do |instance|
|
|
89
95
|
claimed_message = matching_claim!
|
|
90
96
|
locked_message = Message.lock.find(message.id)
|
|
97
|
+
execute_commit_actions(commit_action_intents)
|
|
91
98
|
instance.update!(
|
|
92
99
|
state: serialized_state,
|
|
93
100
|
state_version: actor.class.state_version,
|
|
@@ -128,6 +135,58 @@ module SolidObjects
|
|
|
128
135
|
SolidObjects.wake_up.signal
|
|
129
136
|
end
|
|
130
137
|
|
|
138
|
+
# @rbs (Array[Actor::CommitActionIntent]) -> void
|
|
139
|
+
def execute_commit_actions(intents)
|
|
140
|
+
ensure_application_database_is_shared! if intents.any?
|
|
141
|
+
context = CommitActionContext.new(
|
|
142
|
+
message_id: message.id,
|
|
143
|
+
request_id: message.request_id,
|
|
144
|
+
actor_type: message.actor_type,
|
|
145
|
+
actor_id: message.actor_id,
|
|
146
|
+
activation_generation: activation.lease.generation
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
intents.each do |intent|
|
|
150
|
+
handler = SolidObjects.commit_action_registry.fetch(intent.name)
|
|
151
|
+
execute_commit_action(intent, handler, context)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (Actor::CommitActionIntent, Proc, CommitActionContext) -> untyped
|
|
156
|
+
def execute_commit_action(intent, handler, context)
|
|
157
|
+
payload = {
|
|
158
|
+
commit_action_name: intent.name,
|
|
159
|
+
message_id: message.id,
|
|
160
|
+
request_id: message.request_id,
|
|
161
|
+
actor_type: message.actor_type,
|
|
162
|
+
actor_id: message.actor_id,
|
|
163
|
+
activation_generation: activation.lease.generation
|
|
164
|
+
}
|
|
165
|
+
SolidObjects.instrument(:"commit_action.started", **payload)
|
|
166
|
+
handler.call(intent.arguments, context).tap do
|
|
167
|
+
SolidObjects.instrument(:"commit_action.completed", **payload)
|
|
168
|
+
end
|
|
169
|
+
rescue => error
|
|
170
|
+
SolidObjects.instrument(
|
|
171
|
+
:"commit_action.failed",
|
|
172
|
+
**payload,
|
|
173
|
+
error_class: error.class.name,
|
|
174
|
+
error_message: error.message
|
|
175
|
+
)
|
|
176
|
+
raise
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @rbs () -> void
|
|
180
|
+
def ensure_application_database_is_shared!
|
|
181
|
+
return if SolidObjects::Record.connection_pool.equal?(ActiveRecord::Base.connection_pool)
|
|
182
|
+
|
|
183
|
+
raise CommitActionUnavailable.new(
|
|
184
|
+
actor_type: message.actor_type,
|
|
185
|
+
actor_id: message.actor_id,
|
|
186
|
+
message_name: message.message_name
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
131
190
|
# @rbs (Message, Instance, Array[Actor::EffectIntent]) -> Array[Effect]
|
|
132
191
|
def enqueue_effects(locked_message, instance, intents)
|
|
133
192
|
intents.map do |intent|
|
|
@@ -218,7 +277,8 @@ module SolidObjects
|
|
|
218
277
|
locked_message.update!(error: error_details, last_failed_at: now)
|
|
219
278
|
claimed_message.destroy!
|
|
220
279
|
|
|
221
|
-
if
|
|
280
|
+
if error.is_a?(NonRetryableError) ||
|
|
281
|
+
locked_message.attempt_count >= locked_message.max_attempts
|
|
222
282
|
create_dead_letter(locked_message, error_details, now)
|
|
223
283
|
dead = true
|
|
224
284
|
else
|