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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +129 -8
  4. data/docs/architecture.md +62 -15
  5. data/docs/authorization.md +16 -3
  6. data/docs/benchmarks.md +6 -2
  7. data/docs/correctness.md +19 -1
  8. data/docs/database-schema.md +5 -0
  9. data/docs/development.md +40 -0
  10. data/docs/fit.md +10 -2
  11. data/docs/migrating-existing-state.md +8 -1
  12. data/docs/operations.md +74 -36
  13. data/docs/roadmap.md +8 -2
  14. data/docs/security.md +25 -3
  15. data/docs/state-migrations.md +2 -0
  16. data/lib/generators/solid_objects/templates/solid_objects.rb +30 -2
  17. data/lib/solid_objects/activation.rb +33 -4
  18. data/lib/solid_objects/actor.rb +47 -6
  19. data/lib/solid_objects/actor_definition.rb +2 -0
  20. data/lib/solid_objects/actor_snapshot.rb +10 -4
  21. data/lib/solid_objects/application_write_guard.rb +24 -0
  22. data/lib/solid_objects/caller_process.rb +28 -0
  23. data/lib/solid_objects/cli.rb +44 -5
  24. data/lib/solid_objects/client.rb +98 -5
  25. data/lib/solid_objects/commit_action_registry.rb +42 -0
  26. data/lib/solid_objects/configuration.rb +27 -1
  27. data/lib/solid_objects/database_adapter.rb +28 -1
  28. data/lib/solid_objects/database_adapters/mysql.rb +46 -0
  29. data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
  30. data/lib/solid_objects/database_adapters/sqlite.rb +28 -0
  31. data/lib/solid_objects/errors.rb +144 -0
  32. data/lib/solid_objects/executor.rb +64 -4
  33. data/lib/solid_objects/instance_pruner.rb +97 -0
  34. data/lib/solid_objects/message_pruner.rb +97 -0
  35. data/lib/solid_objects/message_reference.rb +9 -0
  36. data/lib/solid_objects/process_pruner.rb +49 -0
  37. data/lib/solid_objects/reference.rb +5 -0
  38. data/lib/solid_objects/state_snapshot.rb +41 -0
  39. data/lib/solid_objects/sync_deadline.rb +57 -0
  40. data/lib/solid_objects/sync_diagnostics.rb +133 -0
  41. data/lib/solid_objects/synchronous_invocation.rb +26 -7
  42. data/lib/solid_objects/test_helper.rb +78 -0
  43. data/lib/solid_objects/version.rb +1 -1
  44. data/lib/solid_objects/worker.rb +1 -1
  45. data/lib/solid_objects.rb +34 -0
  46. data/sig/generated/lib/solid_objects/activation.rbs +3 -0
  47. data/sig/generated/lib/solid_objects/actor.rbs +26 -0
  48. data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
  49. data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
  50. data/sig/generated/lib/solid_objects/cli.rbs +11 -2
  51. data/sig/generated/lib/solid_objects/client.rbs +15 -0
  52. data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
  53. data/sig/generated/lib/solid_objects/configuration.rbs +27 -7
  54. data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
  55. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
  56. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
  57. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +8 -0
  58. data/sig/generated/lib/solid_objects/errors.rbs +118 -0
  59. data/sig/generated/lib/solid_objects/executor.rbs +12 -0
  60. data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
  61. data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
  62. data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
  63. data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
  64. data/sig/generated/lib/solid_objects/reference.rbs +3 -0
  65. data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
  66. data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
  67. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
  68. data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
  69. data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
  70. data/sig/generated/lib/solid_objects.rbs +12 -0
  71. 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
- deadline = monotonic_now + timeout.to_f
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
- raise SyncTimeout, "actor invocation timed out after #{timeout} seconds"
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
- wait(remaining) if processed.zero?
39
+ remaining = deadline - monotonic_now
40
+ wait(remaining) if processed.zero? && remaining.positive?
23
41
  end
24
- rescue ActiveRecord::RecordNotFound
25
- raise ActorDestroyed, "actor was destroyed while waiting for its result"
26
- end
42
+ rescue DatabaseDeadlineExceeded
43
+ message = load_message(message_reference)
44
+ return completed_result(message) if message.completed? || message.dead?
27
45
 
28
- private
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
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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",