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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +223 -15
  4. data/benchmark/adoption_latency.rb +5 -0
  5. data/benchmark/support.rb +35 -0
  6. data/docs/architecture.md +62 -15
  7. data/docs/authorization.md +98 -0
  8. data/docs/benchmarks.md +75 -1
  9. data/docs/correctness.md +19 -1
  10. data/docs/database-schema.md +5 -0
  11. data/docs/development.md +45 -4
  12. data/docs/fit.md +98 -0
  13. data/docs/migrating-existing-state.md +140 -0
  14. data/docs/operations.md +94 -4
  15. data/docs/roadmap.md +12 -3
  16. data/docs/security.md +28 -3
  17. data/docs/state-migrations.md +6 -0
  18. data/lib/generators/solid_objects/templates/solid_objects.rb +45 -0
  19. data/lib/solid_objects/activation.rb +33 -4
  20. data/lib/solid_objects/actor.rb +47 -6
  21. data/lib/solid_objects/actor_definition.rb +2 -0
  22. data/lib/solid_objects/actor_snapshot.rb +10 -4
  23. data/lib/solid_objects/application_write_guard.rb +24 -0
  24. data/lib/solid_objects/caller_process.rb +28 -0
  25. data/lib/solid_objects/cli.rb +44 -5
  26. data/lib/solid_objects/client.rb +98 -5
  27. data/lib/solid_objects/commit_action_registry.rb +42 -0
  28. data/lib/solid_objects/configuration.rb +27 -1
  29. data/lib/solid_objects/database_adapter.rb +28 -1
  30. data/lib/solid_objects/database_adapters/mysql.rb +46 -0
  31. data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
  32. data/lib/solid_objects/database_adapters/sqlite.rb +28 -0
  33. data/lib/solid_objects/doctor.rb +311 -0
  34. data/lib/solid_objects/errors.rb +144 -0
  35. data/lib/solid_objects/executor.rb +64 -4
  36. data/lib/solid_objects/instance_pruner.rb +97 -0
  37. data/lib/solid_objects/message_pruner.rb +97 -0
  38. data/lib/solid_objects/message_reference.rb +9 -0
  39. data/lib/solid_objects/process_pruner.rb +49 -0
  40. data/lib/solid_objects/reference.rb +5 -0
  41. data/lib/solid_objects/state_snapshot.rb +41 -0
  42. data/lib/solid_objects/sync_deadline.rb +57 -0
  43. data/lib/solid_objects/sync_diagnostics.rb +133 -0
  44. data/lib/solid_objects/synchronous_invocation.rb +26 -7
  45. data/lib/solid_objects/test_helper.rb +78 -0
  46. data/lib/solid_objects/version.rb +1 -1
  47. data/lib/solid_objects/worker.rb +1 -1
  48. data/lib/solid_objects.rb +34 -0
  49. data/lib/tasks/solid_objects_tasks.rake +10 -0
  50. data/sig/generated/lib/solid_objects/activation.rbs +3 -0
  51. data/sig/generated/lib/solid_objects/actor.rbs +26 -0
  52. data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
  53. data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
  54. data/sig/generated/lib/solid_objects/cli.rbs +11 -2
  55. data/sig/generated/lib/solid_objects/client.rbs +15 -0
  56. data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
  57. data/sig/generated/lib/solid_objects/configuration.rbs +27 -7
  58. data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
  59. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
  60. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
  61. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +8 -0
  62. data/sig/generated/lib/solid_objects/doctor.rbs +111 -0
  63. data/sig/generated/lib/solid_objects/errors.rbs +118 -0
  64. data/sig/generated/lib/solid_objects/executor.rbs +12 -0
  65. data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
  66. data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
  67. data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
  68. data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
  69. data/sig/generated/lib/solid_objects/reference.rbs +3 -0
  70. data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
  71. data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
  72. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
  73. data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
  74. data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
  75. data/sig/generated/lib/solid_objects.rbs +12 -0
  76. metadata +26 -1
@@ -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 = Context.with(actor:, message: message_context) do
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 locked_message.attempt_count >= locked_message.max_attempts
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