solid_objects 0.1.0 → 0.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +67 -37
  4. data/app/models/solid_objects/claimed_message.rb +2 -0
  5. data/app/models/solid_objects/message.rb +6 -1
  6. data/benchmark/support.rb +10 -15
  7. data/benchmark/{ask_latency.rb → sync_latency.rb} +1 -1
  8. data/db/migrate/20260805000000_create_solid_objects_tables.rb +15 -3
  9. data/docs/adr/0006-at-least-once-delivery.md +1 -1
  10. data/docs/adr/0008-actor-communication.md +4 -1
  11. data/docs/adr/0011-wake-up-strategy.md +13 -4
  12. data/docs/architecture.md +51 -20
  13. data/docs/benchmarks.md +9 -9
  14. data/docs/correctness.md +28 -9
  15. data/docs/database-schema.md +15 -7
  16. data/docs/development.md +3 -3
  17. data/docs/implementation-plan.md +22 -16
  18. data/docs/operations.md +3 -3
  19. data/docs/research/solid_queue.md +2 -1
  20. data/docs/roadmap.md +5 -2
  21. data/docs/security.md +4 -2
  22. data/lib/solid_objects/activation.rb +35 -19
  23. data/lib/solid_objects/activation_manager.rb +22 -6
  24. data/lib/solid_objects/actor.rb +16 -3
  25. data/lib/solid_objects/caller_process.rb +57 -0
  26. data/lib/solid_objects/client.rb +6 -37
  27. data/lib/solid_objects/configuration.rb +4 -4
  28. data/lib/solid_objects/engine.rb +1 -0
  29. data/lib/solid_objects/errors.rb +17 -1
  30. data/lib/solid_objects/executor.rb +42 -2
  31. data/lib/solid_objects/lease.rb +22 -10
  32. data/lib/solid_objects/message_reference.rb +1 -0
  33. data/lib/solid_objects/process_registry.rb +5 -1
  34. data/lib/solid_objects/reference.rb +8 -8
  35. data/lib/solid_objects/synchronous_invocation.rb +93 -0
  36. data/lib/solid_objects/version.rb +1 -1
  37. data/lib/solid_objects.rb +7 -0
  38. data/sig/generated/lib/solid_objects/activation.rbs +6 -0
  39. data/sig/generated/lib/solid_objects/activation_manager.rbs +6 -0
  40. data/sig/generated/lib/solid_objects/actor.rbs +9 -6
  41. data/sig/generated/lib/solid_objects/caller_process.rbs +32 -0
  42. data/sig/generated/lib/solid_objects/client.rbs +2 -8
  43. data/sig/generated/lib/solid_objects/configuration.rbs +2 -2
  44. data/sig/generated/lib/solid_objects/errors.rbs +18 -1
  45. data/sig/generated/lib/solid_objects/executor.rbs +3 -0
  46. data/sig/generated/lib/solid_objects/lease.rbs +14 -10
  47. data/sig/generated/lib/solid_objects/reference.rbs +3 -3
  48. data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +28 -0
  49. data/sig/generated/lib/solid_objects.rbs +3 -0
  50. data/sig/generated/models/solid_objects/message.rbs +3 -0
  51. metadata +8 -8
@@ -150,7 +150,17 @@ module SolidObjects
150
150
  Context.current_message
151
151
  end
152
152
 
153
- # @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> EffectIntent
153
+ # @rbs (Symbol | String, String, ?details: Hash[String | Symbol, untyped]) -> bot
154
+ def reject(code, message, details: {})
155
+ rejection_code = code.to_s
156
+ unless rejection_code.match?(/\A[a-z][a-z0-9_]*\z/)
157
+ raise ArgumentError, "rejection code must contain lowercase letters, digits, and underscores"
158
+ end
159
+
160
+ raise Rejected.new(code: rejection_code, message:, details:)
161
+ end
162
+
163
+ # @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
154
164
  def emit(name, on_success: nil, on_failure: nil, **arguments)
155
165
  validate_effect_callback!(on_success)
156
166
  validate_effect_callback!(on_failure)
@@ -162,9 +172,10 @@ module SolidObjects
162
172
  ).tap do |intent|
163
173
  effect_intents << intent
164
174
  end
175
+ nil
165
176
  end
166
177
 
167
- # @rbs (Symbol | String, at: Time, ?every: Numeric?, ?missed: Symbol | String, arguments: Hash[Symbol | String, untyped]) -> ReminderIntent
178
+ # @rbs (Symbol | String, at: Time, ?every: Numeric?, ?missed: Symbol | String, arguments: Hash[Symbol | String, untyped]) -> nil
168
179
  def schedule(name, at:, every: nil, missed: :latest, arguments: {})
169
180
  interval_seconds = every&.to_f
170
181
  if interval_seconds && !interval_seconds.positive?
@@ -184,11 +195,13 @@ module SolidObjects
184
195
  ).tap do |intent|
185
196
  reminder_intents << intent
186
197
  end
198
+ nil
187
199
  end
188
200
 
189
- # @rbs (Reference, Symbol | String, ?available_at: Time?, ?idempotency_key: String?, **untyped) -> OutboundMessageIntent
201
+ # @rbs (Reference, Symbol | String, ?available_at: Time?, ?idempotency_key: String?, **untyped) -> nil
190
202
  def send_to(reference, message_name, available_at: nil, idempotency_key: nil, **arguments)
191
203
  stage_outbound_message(reference, message_name, arguments, available_at:, idempotency_key:)
204
+ nil
192
205
  end
193
206
 
194
207
  # @rbs (Symbol | String, Hash[String, untyped]) -> untyped
@@ -0,0 +1,57 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class CallerProcess
5
+ # @rbs @mutex: Thread::Mutex
6
+ # @rbs @process_id: Integer?
7
+ # @rbs @registry: ProcessRegistry?
8
+
9
+ # @rbs () -> void
10
+ def initialize
11
+ @mutex = Thread::Mutex.new
12
+ @process_id = nil
13
+ @registry = nil
14
+ end
15
+
16
+ # @rbs () -> ProcessRegistry
17
+ def process_registry
18
+ mutex.synchronize do
19
+ reset_after_fork
20
+ register unless reusable_registry?
21
+ registry.heartbeat
22
+ registry
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :mutex, :registry
29
+
30
+ # @rbs () -> void
31
+ def reset_after_fork
32
+ return if @process_id == ::Process.pid
33
+
34
+ @process_id = ::Process.pid
35
+ @registry = nil
36
+ end
37
+
38
+ # @rbs () -> bool
39
+ def reusable_registry?
40
+ return false unless registry&.process_record
41
+
42
+ registry.process_record.reload.shutdown_state == "running"
43
+ rescue ActiveRecord::RecordNotFound
44
+ false
45
+ end
46
+
47
+ # @rbs () -> ProcessRegistry
48
+ def register
49
+ @registry = ProcessRegistry.new
50
+ registry.register(
51
+ kind: "caller",
52
+ metadata: { execution: "synchronous" }
53
+ )
54
+ registry
55
+ end
56
+ end
57
+ end
@@ -1,6 +1,7 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  require "solid_objects/mailbox"
4
+ require "solid_objects/synchronous_invocation"
4
5
 
5
6
  module SolidObjects
6
7
  class Client
@@ -12,7 +13,7 @@ module SolidObjects
12
13
  end
13
14
 
14
15
  # @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped) -> MessageReference
15
- def tell(reference, message_name, arguments, available_at: nil, idempotency_key: nil, authorization_context: nil)
16
+ def async(reference, message_name, arguments, available_at: nil, idempotency_key: nil, authorization_context: nil)
16
17
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
17
18
  message_symbol = message_name.to_sym
18
19
  raise UnknownMessage, "unknown message #{message_name.inspect}" unless actor_class.definition.messages.key?(message_symbol)
@@ -28,14 +29,14 @@ module SolidObjects
28
29
  reference,
29
30
  message_name,
30
31
  arguments,
31
- kind: "tell",
32
+ kind: "async",
32
33
  available_at:,
33
34
  idempotency_key:
34
35
  )
35
36
  end
36
37
 
37
38
  # @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped) -> untyped
38
- def ask(reference, message_name, arguments, timeout:, idempotency_key: nil, authorization_context: nil)
39
+ def sync(reference, message_name, arguments, timeout:, idempotency_key: nil, authorization_context: nil)
39
40
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
40
41
  message_symbol = message_name.to_sym
41
42
  query = actor_class.definition.queries.key?(message_symbol)
@@ -53,10 +54,10 @@ module SolidObjects
53
54
  reference,
54
55
  message_name,
55
56
  arguments,
56
- kind: "ask",
57
+ kind: "sync",
57
58
  idempotency_key:
58
59
  )
59
- wait_for_result(message_reference, timeout:)
60
+ SynchronousInvocation.new.call(message_reference, timeout:)
60
61
  end
61
62
 
62
63
  # @rbs (Reference, ?authorization_context: untyped) -> bool
@@ -117,37 +118,5 @@ module SolidObjects
117
118
 
118
119
  raise Unauthorized, "actor destruction is not authorized"
119
120
  end
120
-
121
- # @rbs (MessageReference, timeout: Numeric) -> untyped
122
- def wait_for_result(message_reference, timeout:)
123
- deadline = monotonic_now + timeout.to_f
124
-
125
- loop do
126
- message = Message.uncached { Message.find(message_reference.id) }
127
- return message.result if message.completed?
128
-
129
- if message.dead?
130
- raise MessageFailed.new(
131
- "actor message failed permanently",
132
- message_id: message.id,
133
- details: message.error || {}
134
- )
135
- end
136
-
137
- remaining = deadline - monotonic_now
138
- raise AskTimeout, "actor request timed out after #{timeout} seconds" unless remaining.positive?
139
-
140
- SolidObjects.wake_up.wait(
141
- timeout: [ remaining, SolidObjects.configuration.ask_polling_interval ].min
142
- )
143
- end
144
- rescue ActiveRecord::RecordNotFound
145
- raise ActorDestroyed, "actor was destroyed while waiting for its result"
146
- end
147
-
148
- # @rbs () -> Float
149
- def monotonic_now
150
- ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
151
- end
152
121
  end
153
122
  end
@@ -4,7 +4,7 @@ module SolidObjects
4
4
  class Configuration
5
5
  # @rbs @table_name_prefix: String
6
6
  # @rbs @polling_interval: Float
7
- # @rbs @ask_polling_interval: Float
7
+ # @rbs @sync_polling_interval: Float
8
8
  # @rbs @lease_duration: Float
9
9
  # @rbs @lease_renewal_interval: Float
10
10
  # @rbs @idle_deactivation_timeout: Float
@@ -37,7 +37,7 @@ module SolidObjects
37
37
 
38
38
  attr_accessor :table_name_prefix,
39
39
  :polling_interval,
40
- :ask_polling_interval,
40
+ :sync_polling_interval,
41
41
  :lease_duration,
42
42
  :lease_renewal_interval,
43
43
  :idle_deactivation_timeout,
@@ -72,7 +72,7 @@ module SolidObjects
72
72
  def initialize
73
73
  @table_name_prefix = "solid_objects_"
74
74
  @polling_interval = 0.1
75
- @ask_polling_interval = 0.05
75
+ @sync_polling_interval = 0.05
76
76
  @lease_duration = 30.0
77
77
  @lease_renewal_interval = 10.0
78
78
  @idle_deactivation_timeout = 30.0
@@ -138,7 +138,7 @@ module SolidObjects
138
138
  def positive_values
139
139
  {
140
140
  polling_interval:,
141
- ask_polling_interval:,
141
+ sync_polling_interval:,
142
142
  lease_duration:,
143
143
  lease_renewal_interval:,
144
144
  max_messages_per_activation_pass:,
@@ -21,6 +21,7 @@ module SolidObjects
21
21
 
22
22
  initializer "solid_objects.helpers" do
23
23
  ActiveSupport.on_load(:action_view) do
24
+ require_relative "../../app/helpers/solid_objects/actor_helper"
24
25
  include SolidObjects::ActorHelper
25
26
  end
26
27
  end
@@ -40,7 +40,23 @@ module SolidObjects
40
40
  class ActorDestroyed < LostActivation
41
41
  end
42
42
 
43
- class AskTimeout < Error
43
+ class SyncTimeout < Error
44
+ end
45
+
46
+ class Rejected < Error
47
+ # @rbs @code: String
48
+ # @rbs @details: Hash[String, untyped]
49
+ # @rbs @message_id: Integer?
50
+
51
+ attr_reader :code, :details, :message_id
52
+
53
+ # @rbs (code: String | Symbol, message: String, ?details: Hash[String | Symbol, untyped], ?message_id: Integer?) -> void
54
+ def initialize(code:, message:, details: {}, message_id: nil)
55
+ @code = code.to_s
56
+ @details = Serialization.dump(details)
57
+ @message_id = message_id
58
+ super(message)
59
+ end
44
60
  end
45
61
 
46
62
  class MessageFailed < Error
@@ -33,6 +33,11 @@ module SolidObjects
33
33
  true
34
34
  rescue LostActivation
35
35
  raise
36
+ rescue Rejected => rejection
37
+ activation.restore_state(state_before) if state_before
38
+ actor.discard_intents
39
+ reject_message(rejection)
40
+ false
36
41
  rescue => error
37
42
  activation.restore_state(state_before) if state_before
38
43
  actor.discard_intents
@@ -51,7 +56,7 @@ module SolidObjects
51
56
 
52
57
  # @rbs (Hash[String, untyped]) -> void
53
58
  def ensure_query_did_not_mutate_state!(state_before)
54
- return unless message.message_kind == "ask"
59
+ return unless message.message_kind == "sync"
55
60
  return unless actor.class.definition.queries.key?(message.message_name.to_sym)
56
61
  return if actor.state.to_h == state_before
57
62
 
@@ -72,7 +77,7 @@ module SolidObjects
72
77
  max_bytes: SolidObjects.configuration.max_state_bytes
73
78
  )
74
79
  serialized_result = Serialization.dump(
75
- (message.message_kind == "ask") ? result : nil,
80
+ (message.message_kind == "sync") ? result : nil,
76
81
  max_bytes: SolidObjects.configuration.max_result_bytes
77
82
  )
78
83
  effect_intents = actor.drain_effect_intents
@@ -235,11 +240,46 @@ module SolidObjects
235
240
  SolidObjects.wake_up.signal
236
241
  end
237
242
 
243
+ # @rbs (Rejected) -> void
244
+ def reject_message(rejection)
245
+ rejection_data = Serialization.dump(
246
+ {
247
+ "code" => rejection.code,
248
+ "message" => rejection.message.to_s.byteslice(0, 8_192),
249
+ "details" => rejection.details
250
+ },
251
+ max_bytes: SolidObjects.configuration.max_result_bytes
252
+ )
253
+
254
+ activation.lease.fenced_transaction do |instance|
255
+ claimed_message = matching_claim!
256
+ locked_message = Message.lock.find(message.id)
257
+ now = SolidObjects.database_adapter.database_now
258
+ instance.update!(last_used_at: now)
259
+ locked_message.update!(
260
+ result: nil,
261
+ rejection: rejection_data,
262
+ error: nil,
263
+ rejected_at: now,
264
+ completed_at: now
265
+ )
266
+ claimed_message.destroy!
267
+ end
268
+
269
+ SolidObjects.instrument(
270
+ :"message.rejected",
271
+ **instrumentation_payload,
272
+ code: rejection.code
273
+ )
274
+ SolidObjects.wake_up.signal
275
+ end
276
+
238
277
  # @rbs () -> ClaimedMessage
239
278
  def matching_claim!
240
279
  ClaimedMessage.lock.find_by!(
241
280
  message_id: message.id,
242
281
  process_id: activation.lease.owner_id,
282
+ activation_token: activation.lease.activation_token,
243
283
  activation_generation: activation.lease.generation
244
284
  )
245
285
  rescue ActiveRecord::RecordNotFound
@@ -3,19 +3,21 @@
3
3
  module SolidObjects
4
4
  class Lease
5
5
  class << self
6
- # @rbs (instance: Instance, owner_id: String, now: Time, ?duration: Numeric, database_adapter: DatabaseAdapter) -> Lease?
6
+ # @rbs (instance: Instance, owner_id: String, activation_token: String, now: Time, ?duration: Numeric, database_adapter: DatabaseAdapter) -> Lease?
7
7
  def claim(
8
8
  instance:,
9
9
  owner_id:,
10
+ activation_token:,
10
11
  now:,
11
12
  database_adapter:, duration: SolidObjects.configuration.lease_duration
12
13
  )
13
- return if held_by_another_live_owner?(instance, owner_id, now)
14
+ return if held_by_another_live_owner?(instance, activation_token, now)
14
15
 
15
16
  generation = instance.activation_generation + 1
16
17
  expires_at = now + duration
17
18
  instance.update!(
18
19
  activation_owner_id: owner_id,
20
+ activation_token:,
19
21
  activation_expires_at: expires_at,
20
22
  activation_generation: generation,
21
23
  activated_at: now,
@@ -24,23 +26,25 @@ module SolidObjects
24
26
  new(
25
27
  instance_id: instance.id,
26
28
  owner_id:,
29
+ activation_token:,
27
30
  generation:,
28
31
  expires_at:,
29
32
  database_adapter:
30
33
  )
31
34
  end
32
35
 
33
- # @rbs (instance_id: Integer, owner_id: String, ?duration: Numeric, ?database_adapter: DatabaseAdapter) -> Lease?
36
+ # @rbs (instance_id: Integer, owner_id: String, ?activation_token: String, ?duration: Numeric, ?database_adapter: DatabaseAdapter) -> Lease?
34
37
  def acquire(
35
38
  instance_id:,
36
39
  owner_id:,
40
+ activation_token: SecureRandom.uuid,
37
41
  duration: SolidObjects.configuration.lease_duration,
38
42
  database_adapter: SolidObjects.database_adapter
39
43
  )
40
44
  lease = database_adapter.transaction do
41
45
  instance = Instance.lock.find(instance_id)
42
46
  now = database_adapter.database_now
43
- claim(instance:, owner_id:, now:, duration:, database_adapter:)
47
+ claim(instance:, owner_id:, activation_token:, now:, duration:, database_adapter:)
44
48
  end
45
49
 
46
50
  if lease
@@ -58,9 +62,9 @@ module SolidObjects
58
62
  private
59
63
 
60
64
  # @rbs (Instance, String, Time) -> bool
61
- def held_by_another_live_owner?(instance, owner_id, now)
65
+ def held_by_another_live_owner?(instance, activation_token, now)
62
66
  instance.activation_owner_id.present? &&
63
- instance.activation_owner_id != owner_id &&
67
+ instance.activation_token != activation_token &&
64
68
  instance.activation_expires_at.present? &&
65
69
  instance.activation_expires_at > now
66
70
  end
@@ -68,16 +72,18 @@ module SolidObjects
68
72
 
69
73
  # @rbs @instance_id: Integer
70
74
  # @rbs @owner_id: String
75
+ # @rbs @activation_token: String
71
76
  # @rbs @generation: Integer
72
77
  # @rbs @expires_at: Time
73
78
  # @rbs @database_adapter: DatabaseAdapter
74
79
 
75
- attr_reader :instance_id, :owner_id, :generation, :expires_at
80
+ attr_reader :instance_id, :owner_id, :activation_token, :generation, :expires_at
76
81
 
77
- # @rbs (instance_id: Integer, owner_id: String, generation: Integer, expires_at: Time, database_adapter: DatabaseAdapter) -> void
78
- def initialize(instance_id:, owner_id:, generation:, expires_at:, database_adapter:)
82
+ # @rbs (instance_id: Integer, owner_id: String, activation_token: String, generation: Integer, expires_at: Time, database_adapter: DatabaseAdapter) -> void
83
+ def initialize(instance_id:, owner_id:, activation_token:, generation:, expires_at:, database_adapter:)
79
84
  @instance_id = instance_id
80
85
  @owner_id = owner_id
86
+ @activation_token = activation_token
81
87
  @generation = generation
82
88
  @expires_at = expires_at
83
89
  @database_adapter = database_adapter
@@ -95,6 +101,7 @@ module SolidObjects
95
101
  self.class.new(
96
102
  instance_id:,
97
103
  owner_id:,
104
+ activation_token:,
98
105
  generation:,
99
106
  expires_at: renewed_expiration,
100
107
  database_adapter:
@@ -117,7 +124,11 @@ module SolidObjects
117
124
  next false unless instance
118
125
  next false unless owned_generation?(instance)
119
126
 
120
- instance.update!(activation_owner_id: nil, activation_expires_at: nil)
127
+ instance.update!(
128
+ activation_owner_id: nil,
129
+ activation_token: nil,
130
+ activation_expires_at: nil
131
+ )
121
132
  true
122
133
  end
123
134
 
@@ -159,6 +170,7 @@ module SolidObjects
159
170
  # @rbs (Instance) -> bool
160
171
  def owned_generation?(instance)
161
172
  instance.activation_owner_id == owner_id &&
173
+ instance.activation_token == activation_token &&
162
174
  instance.activation_generation == generation
163
175
  end
164
176
 
@@ -36,6 +36,7 @@ module SolidObjects
36
36
  # @rbs () -> String
37
37
  def status
38
38
  message = Message.find(id)
39
+ return "rejected" if message.rejected?
39
40
  return "completed" if message.completed?
40
41
  return "dead" if message.dead?
41
42
  return "claimed" if message.claimed?
@@ -22,9 +22,13 @@ module SolidObjects
22
22
  SolidObjects.database_adapter.transaction do
23
23
  Instance.where(activation_owner_id: process_record.id).update_all(
24
24
  activation_owner_id: nil,
25
+ activation_token: nil,
25
26
  activation_expires_at: nil
26
27
  )
27
- ClaimedMessage.where(process_id: process_record.id).update_all(process_id: nil)
28
+ ClaimedMessage.where(process_id: process_record.id).update_all(
29
+ process_id: nil,
30
+ activation_token: nil
31
+ )
28
32
  Effect.where(claimed_by: process_record.id).update_all(
29
33
  status: "pending",
30
34
  claimed_by: nil,
@@ -16,19 +16,20 @@ module SolidObjects
16
16
  freeze
17
17
  end
18
18
 
19
- # @rbs (Symbol | String, ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped, **untyped) -> (MessageReference | Actor::OutboundMessageIntent)
20
- def tell(message_name, available_at: nil, idempotency_key: nil, authorization_context: nil, **arguments)
19
+ # @rbs (Symbol | String, ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped, **untyped) -> MessageReference?
20
+ def async(message_name, available_at: nil, idempotency_key: nil, authorization_context: nil, **arguments)
21
21
  if (actor = Context.current_actor)
22
- return actor.stage_outbound_message(
22
+ actor.stage_outbound_message(
23
23
  self,
24
24
  message_name,
25
25
  arguments,
26
26
  available_at:,
27
27
  idempotency_key:
28
28
  )
29
+ return
29
30
  end
30
31
 
31
- SolidObjects.client.tell(
32
+ SolidObjects.client.async(
32
33
  self,
33
34
  message_name,
34
35
  arguments,
@@ -39,10 +40,10 @@ module SolidObjects
39
40
  end
40
41
 
41
42
  # @rbs (Symbol | String, ?timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped, **untyped) -> untyped
42
- def ask(message_name, timeout: 5.seconds, idempotency_key: nil, authorization_context: nil, **arguments)
43
+ def sync(message_name, timeout: 5.seconds, idempotency_key: nil, authorization_context: nil, **arguments)
43
44
  raise ActorCallCycle, "actors cannot synchronously wait for another actor" if Context.current_actor
44
45
 
45
- SolidObjects.client.ask(
46
+ SolidObjects.client.sync(
46
47
  self,
47
48
  message_name,
48
49
  arguments,
@@ -60,8 +61,7 @@ module SolidObjects
60
61
  # @rbs (Symbol, *untyped, **untyped) -> untyped
61
62
  def method_missing(name, *arguments, **keywords)
62
63
  return super if arguments.any? || block_given?
63
- return tell(name, **keywords) if actor_message?(name)
64
- return Serialization.readonly_copy(ask(name, **keywords)) if actor_query?(name)
64
+ return sync(name, **keywords) if actor_message?(name) || actor_query?(name)
65
65
 
66
66
  super
67
67
  end
@@ -0,0 +1,93 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "solid_objects/activation_manager"
4
+ require "solid_objects/lease_renewer"
5
+
6
+ module SolidObjects
7
+ class SynchronousInvocation
8
+ # @rbs (MessageReference, timeout: Numeric) -> untyped
9
+ def call(message_reference, timeout:)
10
+ deadline = monotonic_now + timeout.to_f
11
+
12
+ loop do
13
+ message = load_message(message_reference)
14
+ return completed_result(message) if message.completed? || message.dead?
15
+
16
+ remaining = deadline - monotonic_now
17
+ unless remaining.positive?
18
+ raise SyncTimeout, "actor invocation timed out after #{timeout} seconds"
19
+ end
20
+
21
+ processed = assist(message, deadline:)
22
+ wait(remaining) if processed.zero?
23
+ end
24
+ rescue ActiveRecord::RecordNotFound
25
+ raise ActorDestroyed, "actor was destroyed while waiting for its result"
26
+ end
27
+
28
+ private
29
+
30
+ # @rbs (MessageReference) -> Message
31
+ def load_message(message_reference)
32
+ Message.uncached { Message.find(message_reference.id) }
33
+ end
34
+
35
+ # @rbs (Message) -> untyped
36
+ def completed_result(message)
37
+ raise_rejection(message) if message.rejected?
38
+ if message.dead?
39
+ raise MessageFailed.new(
40
+ "actor message failed permanently",
41
+ message_id: message.id,
42
+ details: message.error || {}
43
+ )
44
+ end
45
+
46
+ Serialization.readonly_copy(message.result)
47
+ end
48
+
49
+ # @rbs (Message) -> bot
50
+ def raise_rejection(message)
51
+ rejection = message.rejection
52
+ raise Rejected.new(
53
+ code: rejection.fetch("code"),
54
+ message: rejection.fetch("message"),
55
+ details: rejection.fetch("details"),
56
+ message_id: message.id
57
+ )
58
+ end
59
+
60
+ # @rbs (Message, deadline: Float) -> Integer
61
+ def assist(message, deadline:)
62
+ process_registry = SolidObjects.caller_process.process_registry
63
+ activation = ActivationManager
64
+ .new(owner_id: process_registry.process_record.id)
65
+ .claim(instance_id: message.instance_id)
66
+ return 0 unless activation
67
+
68
+ LeaseRenewer.new(
69
+ activation:,
70
+ process_registry:
71
+ ).around do
72
+ activation.drain_until(message_id: message.id, deadline:)
73
+ end
74
+ ensure
75
+ if activation
76
+ activation.yield_ready_messages if activation.pass_exhausted?
77
+ activation.deactivate
78
+ end
79
+ end
80
+
81
+ # @rbs (Numeric) -> void
82
+ def wait(remaining)
83
+ SolidObjects.wake_up.wait(
84
+ timeout: [ remaining, SolidObjects.configuration.sync_polling_interval ].min
85
+ )
86
+ end
87
+
88
+ # @rbs () -> Float
89
+ def monotonic_now
90
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
91
+ end
92
+ end
93
+ end
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/solid_objects.rb CHANGED
@@ -78,6 +78,12 @@ module SolidObjects
78
78
  @client ||= Client.new
79
79
  end
80
80
 
81
+ # @rbs () -> CallerProcess
82
+ def caller_process
83
+ require "solid_objects/caller_process"
84
+ @caller_process ||= CallerProcess.new
85
+ end
86
+
81
87
  # @rbs () -> DeadLetterManager
82
88
  def dead_letters
83
89
  @dead_letters ||= DeadLetterManager.new
@@ -100,6 +106,7 @@ module SolidObjects
100
106
  @client = nil
101
107
  @database_adapter = nil
102
108
  @wake_up = nil
109
+ @caller_process = nil
103
110
  @effect_registry = EffectRegistry.new
104
111
  @dead_letters = nil
105
112
  end
@@ -22,6 +22,9 @@ module SolidObjects
22
22
  # @rbs () -> Integer
23
23
  def drain: () -> Integer
24
24
 
25
+ # @rbs (message_id: Integer, deadline: Float) -> Integer
26
+ def drain_until: (message_id: Integer, deadline: Float) -> Integer
27
+
25
28
  # @rbs () -> bool
26
29
  def ready?: () -> bool
27
30
 
@@ -50,6 +53,9 @@ module SolidObjects
50
53
 
51
54
  attr_reader actor_class: untyped
52
55
 
56
+ # @rbs (?message_id: Integer?, ?deadline: Float?) -> Integer
57
+ def drain_messages: (?message_id: Integer?, ?deadline: Float?) -> Integer
58
+
53
59
  # @rbs (Instance) -> Actor
54
60
  def build_actor: (Instance) -> Actor
55
61
 
@@ -12,12 +12,18 @@ module SolidObjects
12
12
  # @rbs () -> Activation?
13
13
  def claim_next: () -> Activation?
14
14
 
15
+ # @rbs (instance_id: Integer) -> Activation?
16
+ def claim: (instance_id: Integer) -> Activation?
17
+
15
18
  private
16
19
 
17
20
  attr_reader owner_id: untyped
18
21
 
19
22
  attr_reader database_adapter: untyped
20
23
 
24
+ # @rbs (Array[Integer]) -> Activation?
25
+ def claim_from: (Array[Integer]) -> Activation?
26
+
21
27
  # @rbs (Time) -> Array[Integer]
22
28
  def candidate_instance_ids: (Time) -> Array[Integer]
23
29