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
|
@@ -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
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class SyncDiagnostics
|
|
5
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
6
|
+
def call(message, timeout:)
|
|
7
|
+
message = Message.uncached { Message.find(message.id) }
|
|
8
|
+
instance = Instance.uncached { Instance.find(message.instance_id) }
|
|
9
|
+
blocker = earlier_blocker(message)
|
|
10
|
+
status = message_status(message)
|
|
11
|
+
waiting_on = waiting_reason(message, instance, blocker)
|
|
12
|
+
activation = activation_details(instance)
|
|
13
|
+
error = SyncTimeout.new(
|
|
14
|
+
timeout:,
|
|
15
|
+
actor_type: message.actor_type,
|
|
16
|
+
actor_id: message.actor_id,
|
|
17
|
+
message_name: message.message_name,
|
|
18
|
+
message_id: message.id,
|
|
19
|
+
request_id: message.request_id,
|
|
20
|
+
sequence: message.sequence,
|
|
21
|
+
status:,
|
|
22
|
+
waiting_on:,
|
|
23
|
+
activation:,
|
|
24
|
+
blocker: blocker_details(blocker)
|
|
25
|
+
)
|
|
26
|
+
SolidObjects.instrument(
|
|
27
|
+
:"sync.timeout",
|
|
28
|
+
message_id: message.id,
|
|
29
|
+
request_id: message.request_id,
|
|
30
|
+
actor_type: message.actor_type,
|
|
31
|
+
actor_id: message.actor_id,
|
|
32
|
+
sequence: message.sequence,
|
|
33
|
+
status:,
|
|
34
|
+
waiting_on:,
|
|
35
|
+
activation_owner_id: activation["owner_id"],
|
|
36
|
+
activation_generation: activation["generation"]
|
|
37
|
+
)
|
|
38
|
+
error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @rbs (Message) -> String
|
|
44
|
+
def message_status(message)
|
|
45
|
+
return "rejected" if message.rejected?
|
|
46
|
+
return "completed" if message.completed?
|
|
47
|
+
return "dead" if message.dead?
|
|
48
|
+
return "claimed" if ClaimedMessage.where(message_id: message.id).exists?
|
|
49
|
+
return "ready" if ReadyMessage.where(message_id: message.id).exists?
|
|
50
|
+
|
|
51
|
+
"unknown"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs (Message, Instance, Message?) -> String
|
|
55
|
+
def waiting_reason(message, instance, blocker)
|
|
56
|
+
return "actor_paused" if instance.paused_at
|
|
57
|
+
return "activation_held" if live_activation?(instance)
|
|
58
|
+
return "earlier_message" if blocker
|
|
59
|
+
return "message_claimed" if ClaimedMessage.where(message_id: message.id).exists?
|
|
60
|
+
|
|
61
|
+
ready_message = ReadyMessage.find_by(message_id: message.id)
|
|
62
|
+
return "not_yet_available" if future?(ready_message)
|
|
63
|
+
return "ready_unclaimed" if ready_message
|
|
64
|
+
|
|
65
|
+
"unknown"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @rbs (ReadyMessage?) -> bool
|
|
69
|
+
def future?(ready_message)
|
|
70
|
+
ready_message&.available_at.present? &&
|
|
71
|
+
ready_message.available_at > SolidObjects.database_adapter.database_now
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @rbs (Instance) -> bool
|
|
75
|
+
def live_activation?(instance)
|
|
76
|
+
instance.activation_owner_id.present? &&
|
|
77
|
+
instance.activation_expires_at.present? &&
|
|
78
|
+
instance.activation_expires_at > SolidObjects.database_adapter.database_now
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @rbs (Message) -> Message?
|
|
82
|
+
def earlier_blocker(message)
|
|
83
|
+
ready = Message
|
|
84
|
+
.joins(:ready_message)
|
|
85
|
+
.where(instance_id: message.instance_id, sequence: ...message.sequence)
|
|
86
|
+
.order(:sequence)
|
|
87
|
+
.first
|
|
88
|
+
claimed = Message
|
|
89
|
+
.joins(:claimed_message)
|
|
90
|
+
.where(instance_id: message.instance_id, sequence: ...message.sequence)
|
|
91
|
+
.order(:sequence)
|
|
92
|
+
.first
|
|
93
|
+
|
|
94
|
+
[ ready, claimed ].compact.min_by(&:sequence)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @rbs (Instance) -> Hash[String, untyped]
|
|
98
|
+
def activation_details(instance)
|
|
99
|
+
process_record = Process.find_by(id: instance.activation_owner_id)
|
|
100
|
+
{
|
|
101
|
+
"owner_id" => instance.activation_owner_id,
|
|
102
|
+
"generation" => instance.activation_generation,
|
|
103
|
+
"expires_at" => instance.activation_expires_at&.iso8601(6),
|
|
104
|
+
"process" => process_details(process_record)
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @rbs (Process?) -> Hash[String, untyped]?
|
|
109
|
+
def process_details(process_record)
|
|
110
|
+
return unless process_record
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
"kind" => process_record.kind,
|
|
114
|
+
"hostname" => process_record.hostname,
|
|
115
|
+
"pid" => process_record.pid,
|
|
116
|
+
"last_heartbeat_at" => process_record.last_heartbeat_at&.iso8601(6),
|
|
117
|
+
"shutdown_state" => process_record.shutdown_state
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs (Message?) -> Hash[String, untyped]?
|
|
122
|
+
def blocker_details(blocker)
|
|
123
|
+
return unless blocker
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
"message_id" => blocker.id,
|
|
127
|
+
"sequence" => blocker.sequence,
|
|
128
|
+
"message_name" => blocker.message_name,
|
|
129
|
+
"status" => message_status(blocker)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -2,12 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "solid_objects/activation_manager"
|
|
4
4
|
require "solid_objects/lease_renewer"
|
|
5
|
+
require "solid_objects/sync_diagnostics"
|
|
5
6
|
|
|
6
7
|
module SolidObjects
|
|
7
8
|
class SynchronousInvocation
|
|
8
9
|
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
9
10
|
def call(message_reference, timeout:)
|
|
10
|
-
|
|
11
|
+
return call_before_deadline(message_reference, timeout:) if SyncDeadline.active?
|
|
12
|
+
|
|
13
|
+
SyncDeadline.with(timeout:) do
|
|
14
|
+
call_before_deadline(message_reference, timeout:)
|
|
15
|
+
end
|
|
16
|
+
rescue ActiveRecord::RecordNotFound
|
|
17
|
+
raise ActorDestroyed, "actor was destroyed while waiting for its result"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
23
|
+
def call_before_deadline(message_reference, timeout:)
|
|
24
|
+
deadline = monotonic_now + SyncDeadline.remaining
|
|
11
25
|
|
|
12
26
|
loop do
|
|
13
27
|
message = load_message(message_reference)
|
|
@@ -15,17 +29,22 @@ module SolidObjects
|
|
|
15
29
|
|
|
16
30
|
remaining = deadline - monotonic_now
|
|
17
31
|
unless remaining.positive?
|
|
18
|
-
|
|
32
|
+
message = load_message(message_reference)
|
|
33
|
+
return completed_result(message) if message.completed? || message.dead?
|
|
34
|
+
|
|
35
|
+
raise SyncDiagnostics.new.call(message, timeout:)
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
processed = assist(message, deadline:)
|
|
22
|
-
|
|
39
|
+
remaining = deadline - monotonic_now
|
|
40
|
+
wait(remaining) if processed.zero? && remaining.positive?
|
|
23
41
|
end
|
|
24
|
-
rescue
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
rescue DatabaseDeadlineExceeded
|
|
43
|
+
message = load_message(message_reference)
|
|
44
|
+
return completed_result(message) if message.completed? || message.dead?
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
raise SyncDiagnostics.new.call(message, timeout:)
|
|
47
|
+
end
|
|
29
48
|
|
|
30
49
|
# @rbs (MessageReference) -> Message
|
|
31
50
|
def load_message(message_reference)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require "solid_objects"
|
|
4
|
+
|
|
5
|
+
module SolidObjects
|
|
6
|
+
module TestHelper
|
|
7
|
+
class << self
|
|
8
|
+
# @rbs (Class) -> void
|
|
9
|
+
def included(test_case)
|
|
10
|
+
test_case.use_transactional_tests = false if test_case.respond_to?(:use_transactional_tests=)
|
|
11
|
+
test_case.setup { reset_actors! }
|
|
12
|
+
test_case.teardown { reset_actors! }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
def reset_actors!
|
|
17
|
+
SolidObjects.reset_caller_process!
|
|
18
|
+
Instance.delete_all
|
|
19
|
+
Process.delete_all
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs () -> void
|
|
24
|
+
def reset_actors!
|
|
25
|
+
TestHelper.reset_actors!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs (?roles: Array[Symbol], ?max_passes: Integer) -> Integer
|
|
29
|
+
def drain_solid_objects(
|
|
30
|
+
roles: [ :reminders, :actors, :effects, :broadcasts ],
|
|
31
|
+
max_passes: 1_000
|
|
32
|
+
)
|
|
33
|
+
runners = build_solid_objects_runners(roles)
|
|
34
|
+
total = 0
|
|
35
|
+
|
|
36
|
+
max_passes.times do
|
|
37
|
+
processed = runners.sum { |runner| solid_objects_runner_result(runner) }
|
|
38
|
+
break if processed.zero?
|
|
39
|
+
|
|
40
|
+
total += processed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
total
|
|
44
|
+
ensure
|
|
45
|
+
runners&.uniq&.each(&:stop)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @rbs (Array[Symbol]) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
|
|
51
|
+
def build_solid_objects_runners(roles)
|
|
52
|
+
unknown_roles = roles - [ :actors, :effects, :reminders, :broadcasts ]
|
|
53
|
+
raise ArgumentError, "unknown Solid Objects test roles: #{unknown_roles.join(", ")}" if unknown_roles.any?
|
|
54
|
+
|
|
55
|
+
runners = {}
|
|
56
|
+
runners[:actors] = Worker.new if roles.include?(:actors)
|
|
57
|
+
runners[:effects] = EffectExecutor.new if roles.include?(:effects)
|
|
58
|
+
runners[:reminders] = ReminderScheduler.new if roles.include?(:reminders)
|
|
59
|
+
runners[:broadcasts] = BroadcastExecutor.new if roles.include?(:broadcasts)
|
|
60
|
+
|
|
61
|
+
[
|
|
62
|
+
runners[:reminders],
|
|
63
|
+
runners[:actors],
|
|
64
|
+
runners[:effects],
|
|
65
|
+
runners[:actors],
|
|
66
|
+
runners[:broadcasts]
|
|
67
|
+
].compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs (Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor) -> Integer
|
|
71
|
+
def solid_objects_runner_result(runner)
|
|
72
|
+
result = runner.run_once
|
|
73
|
+
return result if result.is_a?(Integer)
|
|
74
|
+
|
|
75
|
+
result ? 1 : 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/solid_objects/worker.rb
CHANGED
|
@@ -43,7 +43,7 @@ module SolidObjects
|
|
|
43
43
|
rescue ActorDestroyed
|
|
44
44
|
release_activation(activation) if activation
|
|
45
45
|
0
|
|
46
|
-
rescue StateMigrationError, LostActivation => error
|
|
46
|
+
rescue StateMigrationError, ApplicationWriteForbidden, LostActivation => error
|
|
47
47
|
release_activation(activation) if activation
|
|
48
48
|
SolidObjects.configuration.logger.error(
|
|
49
49
|
event: "solid_objects.worker.error",
|