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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +67 -37
- data/app/models/solid_objects/claimed_message.rb +2 -0
- data/app/models/solid_objects/message.rb +6 -1
- data/benchmark/support.rb +10 -15
- data/benchmark/{ask_latency.rb → sync_latency.rb} +1 -1
- data/db/migrate/20260805000000_create_solid_objects_tables.rb +15 -3
- data/docs/adr/0006-at-least-once-delivery.md +1 -1
- data/docs/adr/0008-actor-communication.md +4 -1
- data/docs/adr/0011-wake-up-strategy.md +13 -4
- data/docs/architecture.md +51 -20
- data/docs/benchmarks.md +9 -9
- data/docs/correctness.md +28 -9
- data/docs/database-schema.md +15 -7
- data/docs/development.md +3 -3
- data/docs/implementation-plan.md +22 -16
- data/docs/operations.md +3 -3
- data/docs/research/solid_queue.md +2 -1
- data/docs/roadmap.md +5 -2
- data/docs/security.md +4 -2
- data/lib/solid_objects/activation.rb +35 -19
- data/lib/solid_objects/activation_manager.rb +22 -6
- data/lib/solid_objects/actor.rb +16 -3
- data/lib/solid_objects/caller_process.rb +57 -0
- data/lib/solid_objects/client.rb +6 -37
- data/lib/solid_objects/configuration.rb +4 -4
- data/lib/solid_objects/engine.rb +1 -0
- data/lib/solid_objects/errors.rb +17 -1
- data/lib/solid_objects/executor.rb +42 -2
- data/lib/solid_objects/lease.rb +22 -10
- data/lib/solid_objects/message_reference.rb +1 -0
- data/lib/solid_objects/process_registry.rb +5 -1
- data/lib/solid_objects/reference.rb +8 -8
- data/lib/solid_objects/synchronous_invocation.rb +93 -0
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +7 -0
- data/sig/generated/lib/solid_objects/activation.rbs +6 -0
- data/sig/generated/lib/solid_objects/activation_manager.rbs +6 -0
- data/sig/generated/lib/solid_objects/actor.rbs +9 -6
- data/sig/generated/lib/solid_objects/caller_process.rbs +32 -0
- data/sig/generated/lib/solid_objects/client.rbs +2 -8
- data/sig/generated/lib/solid_objects/configuration.rbs +2 -2
- data/sig/generated/lib/solid_objects/errors.rbs +18 -1
- data/sig/generated/lib/solid_objects/executor.rbs +3 -0
- data/sig/generated/lib/solid_objects/lease.rbs +14 -10
- data/sig/generated/lib/solid_objects/reference.rbs +3 -3
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +28 -0
- data/sig/generated/lib/solid_objects.rbs +3 -0
- data/sig/generated/models/solid_objects/message.rbs +3 -0
- metadata +8 -8
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -150,7 +150,17 @@ module SolidObjects
|
|
|
150
150
|
Context.current_message
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
# @rbs (Symbol | String,
|
|
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]) ->
|
|
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) ->
|
|
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
|
data/lib/solid_objects/client.rb
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
|
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: "
|
|
57
|
+
kind: "sync",
|
|
57
58
|
idempotency_key:
|
|
58
59
|
)
|
|
59
|
-
|
|
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 @
|
|
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
|
-
:
|
|
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
|
-
@
|
|
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
|
-
|
|
141
|
+
sync_polling_interval:,
|
|
142
142
|
lease_duration:,
|
|
143
143
|
lease_renewal_interval:,
|
|
144
144
|
max_messages_per_activation_pass:,
|
data/lib/solid_objects/engine.rb
CHANGED
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -40,7 +40,23 @@ module SolidObjects
|
|
|
40
40
|
class ActorDestroyed < LostActivation
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
class
|
|
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 == "
|
|
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 == "
|
|
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
|
data/lib/solid_objects/lease.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
65
|
+
def held_by_another_live_owner?(instance, activation_token, now)
|
|
62
66
|
instance.activation_owner_id.present? &&
|
|
63
|
-
instance.
|
|
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!(
|
|
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
|
|
|
@@ -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(
|
|
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) ->
|
|
20
|
-
def
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
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
|
|