solid_objects 0.2.1 → 0.4.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 +24 -0
- data/README.md +261 -31
- data/app/controllers/solid_objects/components_controller.rb +78 -0
- data/app/helpers/solid_objects/actor_helper.rb +13 -4
- data/config/routes.rb +1 -0
- data/db/migrate/20260806000000_add_state_revision_to_solid_objects_instances.rb +12 -0
- data/docs/architecture.md +116 -23
- data/docs/authorization.md +53 -3
- data/docs/benchmarks.md +6 -2
- data/docs/correctness.md +45 -1
- data/docs/database-schema.md +18 -5
- 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/realtime.md +106 -12
- data/docs/roadmap.md +14 -7
- data/docs/security.md +25 -3
- data/docs/state-migrations.md +2 -0
- data/examples/application/README.md +5 -4
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +1 -1
- data/examples/application/app/views/chat_rooms/show.html.erb +2 -2
- data/examples/application/config/initializers/solid_objects.rb +9 -3
- data/lib/generators/solid_objects/templates/solid_objects.rb +33 -2
- data/lib/solid_objects/activation.rb +33 -4
- data/lib/solid_objects/actor.rb +58 -6
- data/lib/solid_objects/actor_channel.rb +73 -5
- data/lib/solid_objects/actor_definition.rb +2 -0
- data/lib/solid_objects/actor_snapshot.rb +35 -10
- data/lib/solid_objects/actor_view.rb +103 -10
- 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/component_path_resolver.rb +27 -0
- data/lib/solid_objects/component_registration.rb +117 -0
- data/lib/solid_objects/component_renderer.rb +82 -0
- data/lib/solid_objects/component_subscriptions.rb +109 -0
- data/lib/solid_objects/component_token.rb +139 -0
- data/lib/solid_objects/component_view.rb +67 -0
- data/lib/solid_objects/configuration.rb +39 -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 +40 -0
- data/lib/solid_objects/errors.rb +156 -0
- data/lib/solid_objects/executor.rb +65 -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/stream_token.rb +32 -13
- 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/turbo_stream_renderer.rb +45 -1
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/worker.rb +1 -1
- data/lib/solid_objects.rb +40 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +22 -0
- data/sig/generated/lib/solid_objects/activation.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor.rbs +29 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +20 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +17 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +31 -2
- 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/component_path_resolver.rbs +13 -0
- data/sig/generated/lib/solid_objects/component_registration.rbs +51 -0
- data/sig/generated/lib/solid_objects/component_renderer.rbs +39 -0
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +40 -0
- data/sig/generated/lib/solid_objects/component_token.rbs +38 -0
- data/sig/generated/lib/solid_objects/component_view.rbs +43 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +35 -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 +13 -0
- data/sig/generated/lib/solid_objects/errors.rbs +130 -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/stream_token.rbs +9 -4
- 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/turbo_stream_renderer.rbs +10 -0
- data/sig/generated/lib/solid_objects.rbs +12 -0
- metadata +34 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class ComponentView
|
|
5
|
+
# @rbs @snapshot: ActorSnapshot
|
|
6
|
+
# @rbs @dependencies: Array[String]
|
|
7
|
+
# @rbs @authorization_context: untyped
|
|
8
|
+
|
|
9
|
+
attr_reader :authorization_context
|
|
10
|
+
|
|
11
|
+
# @rbs (snapshot: ActorSnapshot, dependencies: Array[String], authorization_context: untyped) -> void
|
|
12
|
+
def initialize(snapshot:, dependencies:, authorization_context:)
|
|
13
|
+
@snapshot = snapshot
|
|
14
|
+
@dependencies = dependencies
|
|
15
|
+
@authorization_context = authorization_context
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs () -> Reference
|
|
19
|
+
def reference
|
|
20
|
+
snapshot.reference
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs () -> String
|
|
24
|
+
def actor_id
|
|
25
|
+
reference.actor_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs () -> void
|
|
29
|
+
def state
|
|
30
|
+
raise UnknownComponentDependency,
|
|
31
|
+
"reactive components must read declared observables instead of actor.state"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (Symbol, *untyped, **untyped) -> untyped
|
|
35
|
+
def method_missing(name, *arguments, **keywords)
|
|
36
|
+
return super unless arguments.empty? && keywords.empty?
|
|
37
|
+
return observable_value(name) if observable?(name)
|
|
38
|
+
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs (Symbol, bool) -> bool
|
|
43
|
+
def respond_to_missing?(name, include_private = false)
|
|
44
|
+
observable?(name) || super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :snapshot, :dependencies
|
|
50
|
+
|
|
51
|
+
# @rbs (Symbol | String) -> bool
|
|
52
|
+
def observable?(name)
|
|
53
|
+
snapshot.actor_class.definition.observables.key?(name.to_sym)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs (Symbol | String) -> untyped
|
|
57
|
+
def observable_value(name)
|
|
58
|
+
dependency = name.to_s
|
|
59
|
+
unless dependencies.include?(dependency)
|
|
60
|
+
raise UnknownComponentDependency,
|
|
61
|
+
"observable #{dependency.inspect} is not declared by this component"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
snapshot.observable_value(dependency)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
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
|
|
@@ -29,6 +34,8 @@ module SolidObjects
|
|
|
29
34
|
# @rbs @stream_signing_secret: String?
|
|
30
35
|
# @rbs @broadcast_adapter: Proc?
|
|
31
36
|
# @rbs @wake_up_adapter: untyped
|
|
37
|
+
# @rbs @component_path_resolver: Proc?
|
|
38
|
+
# @rbs @component_authorization_context: Proc
|
|
32
39
|
# @rbs @authorize_message: Proc
|
|
33
40
|
# @rbs @authorize_query: Proc
|
|
34
41
|
# @rbs @authorize_destroy: Proc
|
|
@@ -53,6 +60,11 @@ module SolidObjects
|
|
|
53
60
|
:process_heartbeat_interval,
|
|
54
61
|
:process_alive_threshold,
|
|
55
62
|
:shutdown_timeout,
|
|
63
|
+
:message_retention,
|
|
64
|
+
:message_retention_by_actor_type,
|
|
65
|
+
:instance_retention_by_actor_type,
|
|
66
|
+
:process_retention,
|
|
67
|
+
:prune_batch_size,
|
|
56
68
|
:worker_count,
|
|
57
69
|
:effect_worker_count,
|
|
58
70
|
:broadcast_worker_count,
|
|
@@ -62,6 +74,8 @@ module SolidObjects
|
|
|
62
74
|
:stream_signing_secret,
|
|
63
75
|
:broadcast_adapter,
|
|
64
76
|
:wake_up_adapter,
|
|
77
|
+
:component_path_resolver,
|
|
78
|
+
:component_authorization_context,
|
|
65
79
|
:authorize_message,
|
|
66
80
|
:authorize_query,
|
|
67
81
|
:authorize_destroy,
|
|
@@ -88,6 +102,11 @@ module SolidObjects
|
|
|
88
102
|
@process_heartbeat_interval = 15.0
|
|
89
103
|
@process_alive_threshold = 60.0
|
|
90
104
|
@shutdown_timeout = 15.0
|
|
105
|
+
@message_retention = 30.days
|
|
106
|
+
@message_retention_by_actor_type = {}
|
|
107
|
+
@instance_retention_by_actor_type = {}
|
|
108
|
+
@process_retention = 7.days
|
|
109
|
+
@prune_batch_size = 1_000
|
|
91
110
|
@worker_count = 1
|
|
92
111
|
@effect_worker_count = 1
|
|
93
112
|
@broadcast_worker_count = 1
|
|
@@ -96,6 +115,8 @@ module SolidObjects
|
|
|
96
115
|
@stream_signing_secret = nil
|
|
97
116
|
@broadcast_adapter = nil
|
|
98
117
|
@wake_up_adapter = nil
|
|
118
|
+
@component_path_resolver = nil
|
|
119
|
+
@component_authorization_context = ->(controller:) { controller }
|
|
99
120
|
@logger = if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
100
121
|
Rails.logger
|
|
101
122
|
else
|
|
@@ -128,6 +149,20 @@ module SolidObjects
|
|
|
128
149
|
positive_values.each do |name, value|
|
|
129
150
|
raise ArgumentError, "#{name} must be positive" unless value.positive?
|
|
130
151
|
end
|
|
152
|
+
message_retention_by_actor_type.each do |actor_type, retention|
|
|
153
|
+
raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
|
|
154
|
+
raise ArgumentError, "message retention must be positive" unless retention.positive?
|
|
155
|
+
end
|
|
156
|
+
instance_retention_by_actor_type.each do |actor_type, retention|
|
|
157
|
+
raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
|
|
158
|
+
raise ArgumentError, "instance retention must be positive" unless retention.positive?
|
|
159
|
+
end
|
|
160
|
+
unless component_path_resolver.nil? || component_path_resolver.respond_to?(:call)
|
|
161
|
+
raise ArgumentError, "component_path_resolver must respond to call"
|
|
162
|
+
end
|
|
163
|
+
unless component_authorization_context.respond_to?(:call)
|
|
164
|
+
raise ArgumentError, "component_authorization_context must respond to call"
|
|
165
|
+
end
|
|
131
166
|
|
|
132
167
|
self
|
|
133
168
|
end
|
|
@@ -151,7 +186,10 @@ module SolidObjects
|
|
|
151
186
|
max_attempts:,
|
|
152
187
|
process_heartbeat_interval:,
|
|
153
188
|
process_alive_threshold:,
|
|
154
|
-
shutdown_timeout
|
|
189
|
+
shutdown_timeout:,
|
|
190
|
+
message_retention:,
|
|
191
|
+
process_retention:,
|
|
192
|
+
prune_batch_size:
|
|
155
193
|
}
|
|
156
194
|
end
|
|
157
195
|
|
|
@@ -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
|
|
@@ -3,10 +3,50 @@
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class Sqlite < DatabaseAdapter
|
|
6
|
+
LOCK_RETRY_INTERVAL = 0.001
|
|
7
|
+
|
|
6
8
|
# @rbs () -> String
|
|
7
9
|
def current_time_expression
|
|
8
10
|
"STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
|
|
9
11
|
end
|
|
12
|
+
|
|
13
|
+
# @rbs () { () -> untyped } -> untyped
|
|
14
|
+
def transaction(&block)
|
|
15
|
+
return super unless SyncDeadline.active?
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
rescue DatabaseDeadlineExceeded
|
|
19
|
+
raise if SyncDeadline.expired?
|
|
20
|
+
|
|
21
|
+
sleep [ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
|
|
22
|
+
retry
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @rbs (untyped) { () -> untyped } -> untyped
|
|
28
|
+
def with_transaction_deadline(connection)
|
|
29
|
+
return yield unless SyncDeadline.active?
|
|
30
|
+
|
|
31
|
+
previous_timeout = connection.select_value("PRAGMA busy_timeout").to_i
|
|
32
|
+
connection.execute("PRAGMA busy_timeout = 0")
|
|
33
|
+
yield
|
|
34
|
+
ensure
|
|
35
|
+
connection.execute("PRAGMA busy_timeout = #{previous_timeout}") if previous_timeout
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @rbs (Exception) -> bool
|
|
39
|
+
def deadline_error?(error)
|
|
40
|
+
return false unless SyncDeadline.active?
|
|
41
|
+
|
|
42
|
+
cause = error
|
|
43
|
+
while cause
|
|
44
|
+
return true if cause.class.name.match?(/BusyException|BusyError/)
|
|
45
|
+
|
|
46
|
+
cause = cause.cause
|
|
47
|
+
end
|
|
48
|
+
false
|
|
49
|
+
end
|
|
10
50
|
end
|
|
11
51
|
end
|
|
12
52
|
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
|
|
|
@@ -34,13 +37,166 @@ module SolidObjects
|
|
|
34
37
|
class InvalidStreamToken < Error
|
|
35
38
|
end
|
|
36
39
|
|
|
40
|
+
class InvalidComponentToken < Error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class UnknownComponent < Error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class UnknownComponentDependency < Error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class UnsupportedComponentRendering < Error
|
|
50
|
+
end
|
|
51
|
+
|
|
37
52
|
class LostActivation < Error
|
|
38
53
|
end
|
|
39
54
|
|
|
40
55
|
class ActorDestroyed < LostActivation
|
|
41
56
|
end
|
|
42
57
|
|
|
58
|
+
class DatabaseDeadlineExceeded < Error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class SyncEnqueueTimeout < Error
|
|
62
|
+
# @rbs @timeout: Numeric
|
|
63
|
+
# @rbs @actor_type: String
|
|
64
|
+
# @rbs @actor_id: String
|
|
65
|
+
# @rbs @message_name: String
|
|
66
|
+
|
|
67
|
+
attr_reader :timeout, :actor_type, :actor_id, :message_name
|
|
68
|
+
|
|
69
|
+
# @rbs (timeout: Numeric, actor_type: String, actor_id: String, message_name: String) -> void
|
|
70
|
+
def initialize(timeout:, actor_type:, actor_id:, message_name:)
|
|
71
|
+
@timeout = timeout
|
|
72
|
+
@actor_type = actor_type
|
|
73
|
+
@actor_id = actor_id
|
|
74
|
+
@message_name = message_name
|
|
75
|
+
super(
|
|
76
|
+
"actor invocation could not be durably enqueued within #{timeout} seconds for " \
|
|
77
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
43
82
|
class SyncTimeout < Error
|
|
83
|
+
# @rbs @timeout: Numeric
|
|
84
|
+
# @rbs @actor_type: String
|
|
85
|
+
# @rbs @actor_id: String
|
|
86
|
+
# @rbs @message_name: String
|
|
87
|
+
# @rbs @message_id: Integer
|
|
88
|
+
# @rbs @request_id: String
|
|
89
|
+
# @rbs @sequence: Integer
|
|
90
|
+
# @rbs @status: String
|
|
91
|
+
# @rbs @waiting_on: String
|
|
92
|
+
# @rbs @activation: Hash[String, untyped]
|
|
93
|
+
# @rbs @blocker: Hash[String, untyped]?
|
|
94
|
+
|
|
95
|
+
attr_reader :timeout,
|
|
96
|
+
:actor_type,
|
|
97
|
+
:actor_id,
|
|
98
|
+
:message_name,
|
|
99
|
+
:message_id,
|
|
100
|
+
:request_id,
|
|
101
|
+
:sequence,
|
|
102
|
+
:status,
|
|
103
|
+
:waiting_on,
|
|
104
|
+
:activation,
|
|
105
|
+
:blocker
|
|
106
|
+
|
|
107
|
+
# @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
|
|
108
|
+
def initialize(
|
|
109
|
+
timeout:,
|
|
110
|
+
actor_type:,
|
|
111
|
+
actor_id:,
|
|
112
|
+
message_name:,
|
|
113
|
+
message_id:,
|
|
114
|
+
request_id:,
|
|
115
|
+
sequence:,
|
|
116
|
+
status:,
|
|
117
|
+
waiting_on:,
|
|
118
|
+
activation:,
|
|
119
|
+
blocker:
|
|
120
|
+
)
|
|
121
|
+
@timeout = timeout
|
|
122
|
+
@actor_type = actor_type
|
|
123
|
+
@actor_id = actor_id
|
|
124
|
+
@message_name = message_name
|
|
125
|
+
@message_id = message_id
|
|
126
|
+
@request_id = request_id
|
|
127
|
+
@sequence = sequence
|
|
128
|
+
@status = status
|
|
129
|
+
@waiting_on = waiting_on
|
|
130
|
+
@activation = Serialization.readonly_copy(activation)
|
|
131
|
+
@blocker = Serialization.readonly_copy(blocker)
|
|
132
|
+
super(
|
|
133
|
+
"actor invocation timed out after #{timeout} seconds for " \
|
|
134
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} " \
|
|
135
|
+
"message_id=#{message_id} sequence=#{sequence} status=#{status} waiting_on=#{waiting_on}"
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @rbs () -> MessageReference
|
|
140
|
+
def message_reference
|
|
141
|
+
MessageReference.new(
|
|
142
|
+
id: message_id,
|
|
143
|
+
request_id:,
|
|
144
|
+
actor_type:,
|
|
145
|
+
actor_id:,
|
|
146
|
+
sequence:
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class SyncInsideTransaction < Error
|
|
152
|
+
# @rbs @actor_type: String
|
|
153
|
+
# @rbs @actor_id: String
|
|
154
|
+
# @rbs @message_name: String
|
|
155
|
+
|
|
156
|
+
attr_reader :actor_type, :actor_id, :message_name
|
|
157
|
+
|
|
158
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
159
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
160
|
+
@actor_type = actor_type
|
|
161
|
+
@actor_id = actor_id
|
|
162
|
+
@message_name = message_name
|
|
163
|
+
super(
|
|
164
|
+
"cannot synchronously invoke #{actor_type}(#{actor_id.inspect}).#{message_name} " \
|
|
165
|
+
"inside an open database transaction; call it before the transaction or enqueue it with async"
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class ApplicationWriteForbidden < NonRetryableError
|
|
171
|
+
# @rbs @actor_type: String
|
|
172
|
+
# @rbs @actor_id: String
|
|
173
|
+
# @rbs @message_name: String
|
|
174
|
+
|
|
175
|
+
attr_reader :actor_type, :actor_id, :message_name
|
|
176
|
+
|
|
177
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
178
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
179
|
+
@actor_type = actor_type
|
|
180
|
+
@actor_id = actor_id
|
|
181
|
+
@message_name = message_name
|
|
182
|
+
super(
|
|
183
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} attempted an Active Record write; " \
|
|
184
|
+
"mutate actor state, stage a commit action, or emit a durable effect instead"
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
class CommitActionUnavailable < NonRetryableError
|
|
190
|
+
# @rbs (actor_type: String, actor_id: String, message_name: String) -> void
|
|
191
|
+
def initialize(actor_type:, actor_id:, message_name:)
|
|
192
|
+
super(
|
|
193
|
+
"#{actor_type}(#{actor_id.inspect}).#{message_name} staged a commit action, " \
|
|
194
|
+
"but Solid Objects uses a separate database from Active Record"
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class UnknownCommitAction < NonRetryableError
|
|
44
200
|
end
|
|
45
201
|
|
|
46
202
|
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,9 +94,11 @@ 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,
|
|
101
|
+
state_revision: locked_message.sequence,
|
|
94
102
|
last_used_at: SolidObjects.database_adapter.database_now
|
|
95
103
|
)
|
|
96
104
|
locked_message.update!(
|
|
@@ -128,6 +136,58 @@ module SolidObjects
|
|
|
128
136
|
SolidObjects.wake_up.signal
|
|
129
137
|
end
|
|
130
138
|
|
|
139
|
+
# @rbs (Array[Actor::CommitActionIntent]) -> void
|
|
140
|
+
def execute_commit_actions(intents)
|
|
141
|
+
ensure_application_database_is_shared! if intents.any?
|
|
142
|
+
context = CommitActionContext.new(
|
|
143
|
+
message_id: message.id,
|
|
144
|
+
request_id: message.request_id,
|
|
145
|
+
actor_type: message.actor_type,
|
|
146
|
+
actor_id: message.actor_id,
|
|
147
|
+
activation_generation: activation.lease.generation
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
intents.each do |intent|
|
|
151
|
+
handler = SolidObjects.commit_action_registry.fetch(intent.name)
|
|
152
|
+
execute_commit_action(intent, handler, context)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @rbs (Actor::CommitActionIntent, Proc, CommitActionContext) -> untyped
|
|
157
|
+
def execute_commit_action(intent, handler, context)
|
|
158
|
+
payload = {
|
|
159
|
+
commit_action_name: intent.name,
|
|
160
|
+
message_id: message.id,
|
|
161
|
+
request_id: message.request_id,
|
|
162
|
+
actor_type: message.actor_type,
|
|
163
|
+
actor_id: message.actor_id,
|
|
164
|
+
activation_generation: activation.lease.generation
|
|
165
|
+
}
|
|
166
|
+
SolidObjects.instrument(:"commit_action.started", **payload)
|
|
167
|
+
handler.call(intent.arguments, context).tap do
|
|
168
|
+
SolidObjects.instrument(:"commit_action.completed", **payload)
|
|
169
|
+
end
|
|
170
|
+
rescue => error
|
|
171
|
+
SolidObjects.instrument(
|
|
172
|
+
:"commit_action.failed",
|
|
173
|
+
**payload,
|
|
174
|
+
error_class: error.class.name,
|
|
175
|
+
error_message: error.message
|
|
176
|
+
)
|
|
177
|
+
raise
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @rbs () -> void
|
|
181
|
+
def ensure_application_database_is_shared!
|
|
182
|
+
return if SolidObjects::Record.connection_pool.equal?(ActiveRecord::Base.connection_pool)
|
|
183
|
+
|
|
184
|
+
raise CommitActionUnavailable.new(
|
|
185
|
+
actor_type: message.actor_type,
|
|
186
|
+
actor_id: message.actor_id,
|
|
187
|
+
message_name: message.message_name
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
131
191
|
# @rbs (Message, Instance, Array[Actor::EffectIntent]) -> Array[Effect]
|
|
132
192
|
def enqueue_effects(locked_message, instance, intents)
|
|
133
193
|
intents.map do |intent|
|
|
@@ -218,7 +278,8 @@ module SolidObjects
|
|
|
218
278
|
locked_message.update!(error: error_details, last_failed_at: now)
|
|
219
279
|
claimed_message.destroy!
|
|
220
280
|
|
|
221
|
-
if
|
|
281
|
+
if error.is_a?(NonRetryableError) ||
|
|
282
|
+
locked_message.attempt_count >= locked_message.max_attempts
|
|
222
283
|
create_dead_letter(locked_message, error_details, now)
|
|
223
284
|
dead = true
|
|
224
285
|
else
|