solid_objects 0.2.0 → 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 +23 -0
- data/README.md +223 -15
- data/benchmark/adoption_latency.rb +5 -0
- data/benchmark/support.rb +35 -0
- data/docs/architecture.md +62 -15
- data/docs/authorization.md +98 -0
- data/docs/benchmarks.md +75 -1
- data/docs/correctness.md +19 -1
- data/docs/database-schema.md +5 -0
- data/docs/development.md +45 -4
- data/docs/fit.md +98 -0
- data/docs/migrating-existing-state.md +140 -0
- data/docs/operations.md +94 -4
- data/docs/roadmap.md +12 -3
- data/docs/security.md +28 -3
- data/docs/state-migrations.md +6 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +45 -0
- 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/doctor.rb +311 -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/lib/tasks/solid_objects_tasks.rake +10 -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/doctor.rbs +111 -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 +26 -1
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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class InstancePruner
|
|
5
|
+
# @rbs @now: Time
|
|
6
|
+
# @rbs @batch_size: Integer
|
|
7
|
+
|
|
8
|
+
# @rbs (?now: Time, ?batch_size: Integer) -> void
|
|
9
|
+
def initialize(
|
|
10
|
+
now: SolidObjects.database_adapter.database_now,
|
|
11
|
+
batch_size: SolidObjects.configuration.prune_batch_size
|
|
12
|
+
)
|
|
13
|
+
@now = now
|
|
14
|
+
@batch_size = batch_size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs () -> Integer
|
|
18
|
+
def preview
|
|
19
|
+
policy_relations.sum(&:count)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () -> Integer
|
|
23
|
+
def prune
|
|
24
|
+
policy_relations.sum { |relation| prune_relation(relation) }.tap do |count|
|
|
25
|
+
SolidObjects.instrument(:"instances.pruned", count:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :now, :batch_size
|
|
32
|
+
|
|
33
|
+
# @rbs () -> Array[ActiveRecord::Relation[Instance]]
|
|
34
|
+
def policy_relations
|
|
35
|
+
SolidObjects.configuration.instance_retention_by_actor_type.map do |actor_type, retention|
|
|
36
|
+
prunable
|
|
37
|
+
.where(actor_type: actor_type.to_s)
|
|
38
|
+
.where("COALESCE(last_used_at, created_at) < ?", now - retention)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs () -> ActiveRecord::Relation[Instance]
|
|
43
|
+
def prunable
|
|
44
|
+
Instance
|
|
45
|
+
.where(activation_owner_id: nil, paused_at: nil)
|
|
46
|
+
.where.not(id: ReadyMessage.select(:instance_id))
|
|
47
|
+
.where.not(id: ClaimedMessage.select(:instance_id))
|
|
48
|
+
.where.not(
|
|
49
|
+
id: Reminder.where(status: "scheduled").select(:instance_id)
|
|
50
|
+
)
|
|
51
|
+
.where.not(
|
|
52
|
+
id: Effect.where.not(status: "completed").select(:instance_id)
|
|
53
|
+
)
|
|
54
|
+
.where.not(
|
|
55
|
+
id: Broadcast.where.not(status: "delivered").select(:instance_id)
|
|
56
|
+
)
|
|
57
|
+
.where.not(id: DeadLetter.select(:instance_id))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs (ActiveRecord::Relation[Instance]) -> Integer
|
|
61
|
+
def prune_relation(relation)
|
|
62
|
+
deleted = 0
|
|
63
|
+
|
|
64
|
+
loop do
|
|
65
|
+
instance_ids = relation.limit(batch_size).pluck(:id)
|
|
66
|
+
break if instance_ids.empty?
|
|
67
|
+
|
|
68
|
+
instance_ids.each do |instance_id|
|
|
69
|
+
deleted += prune_instance(instance_id, relation)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
deleted
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs (Integer, ActiveRecord::Relation[Instance]) -> Integer
|
|
77
|
+
def prune_instance(instance_id, relation)
|
|
78
|
+
expired_actor = SolidObjects.database_adapter.transaction do
|
|
79
|
+
instance = Instance.lock.find_by(id: instance_id)
|
|
80
|
+
next unless instance
|
|
81
|
+
next unless relation.where(id: instance_id).exists?
|
|
82
|
+
|
|
83
|
+
identity = {
|
|
84
|
+
instance_id:,
|
|
85
|
+
actor_type: instance.actor_type,
|
|
86
|
+
actor_id: instance.actor_id
|
|
87
|
+
}
|
|
88
|
+
instance.delete
|
|
89
|
+
identity
|
|
90
|
+
end
|
|
91
|
+
return 0 unless expired_actor
|
|
92
|
+
|
|
93
|
+
SolidObjects.instrument(:"actor.expired", **expired_actor)
|
|
94
|
+
1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class MessagePruner
|
|
5
|
+
# @rbs @now: Time
|
|
6
|
+
# @rbs @batch_size: Integer
|
|
7
|
+
|
|
8
|
+
# @rbs (?now: Time, ?batch_size: Integer) -> void
|
|
9
|
+
def initialize(
|
|
10
|
+
now: SolidObjects.database_adapter.database_now,
|
|
11
|
+
batch_size: SolidObjects.configuration.prune_batch_size
|
|
12
|
+
)
|
|
13
|
+
@now = now
|
|
14
|
+
@batch_size = batch_size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs () -> Integer
|
|
18
|
+
def preview
|
|
19
|
+
policy_relations.sum(&:count)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () -> Integer
|
|
23
|
+
def prune
|
|
24
|
+
policy_relations.sum { |relation| prune_relation(relation) }.tap do |count|
|
|
25
|
+
SolidObjects.instrument(:"messages.pruned", count:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :now, :batch_size
|
|
32
|
+
|
|
33
|
+
# @rbs () -> Array[ActiveRecord::Relation[Message]]
|
|
34
|
+
def policy_relations
|
|
35
|
+
overrides = retention_overrides
|
|
36
|
+
relations = overrides.map do |actor_type, retention|
|
|
37
|
+
prunable.where(actor_type:, completed_at: ...retention_cutoff(retention))
|
|
38
|
+
end
|
|
39
|
+
default_relation = prunable.where(completed_at: ...retention_cutoff(default_retention))
|
|
40
|
+
default_relation = default_relation.where.not(actor_type: overrides.keys) if overrides.any?
|
|
41
|
+
relations << default_relation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @rbs () -> ActiveRecord::Relation[Message]
|
|
45
|
+
def prunable
|
|
46
|
+
Message
|
|
47
|
+
.where.not(id: ReadyMessage.select(:message_id))
|
|
48
|
+
.where.not(id: ClaimedMessage.select(:message_id))
|
|
49
|
+
.where.not(id: DeadLetter.select(:message_id))
|
|
50
|
+
.where.not(
|
|
51
|
+
id: DeadLetter
|
|
52
|
+
.where.not(retried_message_id: nil)
|
|
53
|
+
.select(:retried_message_id)
|
|
54
|
+
)
|
|
55
|
+
.where.not(
|
|
56
|
+
id: Effect
|
|
57
|
+
.where.not(status: "completed")
|
|
58
|
+
.select(:message_id)
|
|
59
|
+
)
|
|
60
|
+
.where.not(
|
|
61
|
+
id: Broadcast
|
|
62
|
+
.where.not(status: "delivered")
|
|
63
|
+
.select(:message_id)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs (ActiveRecord::Relation[Message]) -> Integer
|
|
68
|
+
def prune_relation(relation)
|
|
69
|
+
deleted = 0
|
|
70
|
+
|
|
71
|
+
loop do
|
|
72
|
+
message_ids = relation.limit(batch_size).pluck(:id)
|
|
73
|
+
break if message_ids.empty?
|
|
74
|
+
|
|
75
|
+
deleted += Message.where(id: message_ids).delete_all
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
deleted
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @rbs () -> Numeric
|
|
82
|
+
def default_retention
|
|
83
|
+
SolidObjects.configuration.message_retention
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs () -> Hash[String, Numeric]
|
|
87
|
+
def retention_overrides
|
|
88
|
+
SolidObjects.configuration.message_retention_by_actor_type
|
|
89
|
+
.to_h { |actor_type, retention| [ actor_type.to_s, retention ] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @rbs (Numeric) -> Time
|
|
93
|
+
def retention_cutoff(retention)
|
|
94
|
+
now - retention
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -49,5 +49,14 @@ module SolidObjects
|
|
|
49
49
|
def result
|
|
50
50
|
Message.find(id).result
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
# @rbs (?timeout: Numeric, ?authorization_context: untyped) -> untyped
|
|
54
|
+
def wait(timeout: 5.seconds, authorization_context: nil)
|
|
55
|
+
SolidObjects.client.wait(
|
|
56
|
+
self,
|
|
57
|
+
timeout:,
|
|
58
|
+
authorization_context:
|
|
59
|
+
)
|
|
60
|
+
end
|
|
52
61
|
end
|
|
53
62
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class ProcessPruner
|
|
5
|
+
# @rbs @now: Time
|
|
6
|
+
# @rbs @batch_size: Integer
|
|
7
|
+
|
|
8
|
+
# @rbs (?now: Time, ?batch_size: Integer) -> void
|
|
9
|
+
def initialize(
|
|
10
|
+
now: SolidObjects.database_adapter.database_now,
|
|
11
|
+
batch_size: SolidObjects.configuration.prune_batch_size
|
|
12
|
+
)
|
|
13
|
+
@now = now
|
|
14
|
+
@batch_size = batch_size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs () -> Integer
|
|
18
|
+
def preview
|
|
19
|
+
prunable.count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () -> Integer
|
|
23
|
+
def prune
|
|
24
|
+
deleted = 0
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
process_ids = prunable.limit(batch_size).pluck(:id)
|
|
28
|
+
break if process_ids.empty?
|
|
29
|
+
|
|
30
|
+
deleted += Process.where(id: process_ids).delete_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
SolidObjects.instrument(:"processes.pruned", count: deleted)
|
|
34
|
+
deleted
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :now, :batch_size
|
|
40
|
+
|
|
41
|
+
# @rbs () -> ActiveRecord::Relation[Process]
|
|
42
|
+
def prunable
|
|
43
|
+
Process.where(
|
|
44
|
+
shutdown_state: "stopped",
|
|
45
|
+
stopped_at: ...(now - SolidObjects.configuration.process_retention)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -58,6 +58,11 @@ module SolidObjects
|
|
|
58
58
|
SolidObjects.client.destroy(self, authorization_context:)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# @rbs (?authorization_context: untyped) -> StateSnapshot
|
|
62
|
+
def snapshot(authorization_context: nil)
|
|
63
|
+
SolidObjects.client.snapshot(self, authorization_context:)
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
# @rbs (Symbol, *untyped, **untyped) -> untyped
|
|
62
67
|
def method_missing(name, *arguments, **keywords)
|
|
63
68
|
return super if arguments.any? || block_given?
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class StateSnapshot
|
|
5
|
+
# @rbs @actor_class: Class
|
|
6
|
+
# @rbs @data: Hash[String, untyped]
|
|
7
|
+
|
|
8
|
+
# @rbs (Reference) -> void
|
|
9
|
+
def initialize(reference)
|
|
10
|
+
actor_snapshot = ActorSnapshot.new(reference)
|
|
11
|
+
@actor_class = actor_snapshot.actor_class
|
|
12
|
+
@data = Serialization.readonly_copy(actor_snapshot.actor.state.to_h)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs () -> Hash[String, untyped]
|
|
16
|
+
def to_h
|
|
17
|
+
data
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs (Symbol, *untyped, **untyped) -> untyped
|
|
21
|
+
def method_missing(name, *arguments, **keywords)
|
|
22
|
+
return data.fetch(name.to_s) if arguments.empty? && keywords.empty? && attribute?(name)
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs (Symbol, bool) -> bool
|
|
28
|
+
def respond_to_missing?(name, include_private = false)
|
|
29
|
+
attribute?(name) || super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :actor_class, :data
|
|
35
|
+
|
|
36
|
+
# @rbs (Symbol | String) -> bool
|
|
37
|
+
def attribute?(name)
|
|
38
|
+
actor_class.definition.state_definition.to_h.key?(name.to_sym)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class SyncDeadline
|
|
5
|
+
KEY = :solid_objects_sync_deadline
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# @rbs (timeout: Numeric) { () -> untyped } -> untyped
|
|
9
|
+
def with(timeout:)
|
|
10
|
+
previous_deadline = ActiveSupport::IsolatedExecutionState[KEY]
|
|
11
|
+
ActiveSupport::IsolatedExecutionState[KEY] = monotonic_now + timeout.to_f
|
|
12
|
+
yield
|
|
13
|
+
ensure
|
|
14
|
+
ActiveSupport::IsolatedExecutionState[KEY] = previous_deadline
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs () -> bool
|
|
18
|
+
def active?
|
|
19
|
+
!deadline.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () -> bool
|
|
23
|
+
def expired?
|
|
24
|
+
active? && !remaining.positive?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs () -> Float
|
|
28
|
+
def remaining
|
|
29
|
+
return Float::INFINITY unless deadline
|
|
30
|
+
|
|
31
|
+
deadline - monotonic_now
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs () -> Integer
|
|
35
|
+
def remaining_milliseconds
|
|
36
|
+
[ (remaining * 1_000).floor, 1 ].max
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs () -> Integer
|
|
40
|
+
def remaining_seconds
|
|
41
|
+
[ remaining.ceil, 1 ].max
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# @rbs () -> Float?
|
|
47
|
+
def deadline
|
|
48
|
+
ActiveSupport::IsolatedExecutionState[KEY]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @rbs () -> Float
|
|
52
|
+
def monotonic_now
|
|
53
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|