solid_objects 0.14.6 → 0.15.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +1 -0
  4. data/Rakefile +1 -1
  5. data/app/models/solid_objects/effect_recovery.rb +10 -0
  6. data/benchmark/support.rb +3 -0
  7. data/db/migrate/20260915000000_add_solid_objects_effect_recoveries.rb +17 -0
  8. data/docs/architecture.md +48 -0
  9. data/docs/development.md +110 -0
  10. data/docs/effect-recovery.md +203 -0
  11. data/docs/operations.md +6 -0
  12. data/docs/reminders.md +8 -0
  13. data/docs/roadmap.md +11 -0
  14. data/examples/at_least_once/boot.rb +3 -1
  15. data/lib/solid_objects/actor.rb +43 -4
  16. data/lib/solid_objects/actor_signatures.rb +80 -0
  17. data/lib/solid_objects/database_adapter.rb +13 -0
  18. data/lib/solid_objects/doctor.rb +1 -0
  19. data/lib/solid_objects/effect_executor.rb +18 -15
  20. data/lib/solid_objects/effect_payload.rb +42 -0
  21. data/lib/solid_objects/effect_recovery_coordinator.rb +143 -0
  22. data/lib/solid_objects/executor.rb +14 -2
  23. data/lib/solid_objects/process_heartbeat.rb +64 -0
  24. data/lib/solid_objects/process_pruner.rb +1 -1
  25. data/lib/solid_objects/process_registry.rb +9 -6
  26. data/lib/solid_objects/test_helper.rb +1 -0
  27. data/lib/solid_objects/version.rb +1 -1
  28. data/lib/solid_objects.rb +3 -0
  29. data/sig/generated/lib/solid_objects/actor.rbs +40 -6
  30. data/sig/generated/lib/solid_objects/actor_signatures.rbs +24 -0
  31. data/sig/generated/lib/solid_objects/database_adapter.rbs +3 -0
  32. data/sig/generated/lib/solid_objects/effect_payload.rbs +23 -0
  33. data/sig/generated/lib/solid_objects/effect_recovery_coordinator.rbs +41 -0
  34. data/sig/generated/lib/solid_objects/process_heartbeat.rbs +38 -0
  35. data/sig/generated/models/solid_objects/effect_recovery.rbs +6 -0
  36. data/sig/public/effect_payload.rbs +33 -0
  37. metadata +15 -2
@@ -3,6 +3,8 @@
3
3
  module SolidObjects
4
4
  class Actor
5
5
  class EffectIntent < Data
6
+ attr_reader effect_id(): untyped
7
+
6
8
  attr_reader name(): untyped
7
9
 
8
10
  attr_reader arguments(): untyped
@@ -11,12 +13,31 @@ module SolidObjects
11
13
 
12
14
  attr_reader failure_operation(): untyped
13
15
 
14
- def self.new: (untyped name, untyped arguments, untyped success_operation, untyped failure_operation) -> instance
15
- | (name: untyped, arguments: untyped, success_operation: untyped, failure_operation: untyped) -> instance
16
+ attr_reader recovery_operation(): untyped
17
+
18
+ attr_reader status_operation(): untyped
19
+
20
+ attr_reader recovery_timeout(): untyped
21
+
22
+ def self.new: (untyped effect_id, untyped name, untyped arguments, untyped success_operation, untyped failure_operation, untyped recovery_operation, untyped status_operation, untyped recovery_timeout) -> instance
23
+ | (effect_id: untyped, name: untyped, arguments: untyped, success_operation: untyped, failure_operation: untyped, recovery_operation: untyped, status_operation: untyped, recovery_timeout: untyped) -> instance
16
24
 
17
- def self.members: () -> [ :name, :arguments, :success_operation, :failure_operation ]
25
+ def self.members: () -> [ :effect_id, :name, :arguments, :success_operation, :failure_operation, :recovery_operation, :status_operation, :recovery_timeout ]
18
26
 
19
- def members: () -> [ :name, :arguments, :success_operation, :failure_operation ]
27
+ def members: () -> [ :effect_id, :name, :arguments, :success_operation, :failure_operation, :recovery_operation, :status_operation, :recovery_timeout ]
28
+ end
29
+
30
+ class EffectRecoveryIntent < Data
31
+ attr_reader effect_id(): untyped
32
+
33
+ attr_reader request_id(): untyped
34
+
35
+ def self.new: (untyped effect_id, untyped request_id) -> instance
36
+ | (effect_id: untyped, request_id: untyped) -> instance
37
+
38
+ def self.members: () -> [ :effect_id, :request_id ]
39
+
40
+ def members: () -> [ :effect_id, :request_id ]
20
41
  end
21
42
 
22
43
  class CommitActionIntent < Data
@@ -139,6 +160,8 @@ module SolidObjects
139
160
 
140
161
  @commit_action_intents: Array[CommitActionIntent]
141
162
 
163
+ @effect_recovery_intents: Array[EffectRecoveryIntent]
164
+
142
165
  @effect_intents: Array[EffectIntent]
143
166
 
144
167
  @state: State
@@ -158,8 +181,11 @@ module SolidObjects
158
181
  # @rbs (Symbol | String, String, ?details: Hash[String | Symbol, untyped]) -> bot
159
182
  def reject: (Symbol | String, String, ?details: Hash[String | Symbol, untyped]) -> bot
160
183
 
161
- # @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
162
- def emit: (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
184
+ # @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, ?on_recovery: Symbol | String?, ?on_status: Symbol | String?, ?recovery_timeout: Numeric?, **untyped) -> effect_handle
185
+ def emit: (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, ?on_recovery: Symbol | String?, ?on_status: Symbol | String?, ?recovery_timeout: Numeric?, **untyped) -> effect_handle
186
+
187
+ # @rbs (effect_handle) -> nil
188
+ def request_effect_recovery: (effect_handle) -> nil
163
189
 
164
190
  # @rbs () -> OperationDispatcher
165
191
  def transmit: () -> OperationDispatcher
@@ -216,6 +242,9 @@ module SolidObjects
216
242
  # @rbs () -> Array[EffectIntent]
217
243
  def drain_effect_intents: () -> Array[EffectIntent]
218
244
 
245
+ # @rbs () -> Array[EffectRecoveryIntent]
246
+ def drain_effect_recovery_intents: () -> Array[EffectRecoveryIntent]
247
+
219
248
  # @rbs () -> Array[CommitActionIntent]
220
249
  def drain_commit_action_intents: () -> Array[CommitActionIntent]
221
250
 
@@ -232,6 +261,8 @@ module SolidObjects
232
261
 
233
262
  attr_reader effect_intents: untyped
234
263
 
264
+ attr_reader effect_recovery_intents: untyped
265
+
235
266
  attr_reader commit_action_intents: untyped
236
267
 
237
268
  attr_reader reminder_intents: untyped
@@ -246,5 +277,8 @@ module SolidObjects
246
277
 
247
278
  # @rbs (Symbol | String?) -> void
248
279
  def validate_effect_callback!: (Symbol | String?) -> void
280
+
281
+ # @rbs (timeout: Numeric?, operation: String | Symbol?) -> void
282
+ def validate_recovery_timeout!: (timeout: Numeric?, operation: String | Symbol?) -> void
249
283
  end
250
284
  end
@@ -0,0 +1,24 @@
1
+ # Generated from lib/solid_objects/actor_signatures.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class ActorSignatures
5
+ # @rbs (actors: Array[Class], signatures: Array[String]) -> String
6
+ def self.generate: (actors: Array[Class], signatures: Array[String]) -> String
7
+
8
+ @builder: untyped
9
+
10
+ # @rbs (signatures: Array[String]) -> void
11
+ def initialize: (signatures: Array[String]) -> void
12
+
13
+ # @rbs (actors: Array[Class]) -> String
14
+ def generate: (actors: Array[Class]) -> String
15
+
16
+ private
17
+
18
+ # @rbs (untyped) -> String
19
+ def actor_signature: (untyped) -> String
20
+
21
+ # @rbs (untyped, String, Symbol) -> untyped
22
+ def staged_type: (untyped, String, Symbol) -> untyped
23
+ end
24
+ end
@@ -55,6 +55,9 @@ module SolidObjects
55
55
  # @rbs () -> Time
56
56
  def database_now: () -> Time
57
57
 
58
+ # @rbs () -> Time
59
+ def database_clock_now: () -> Time
60
+
58
61
  # @rbs () { () -> untyped } -> untyped
59
62
  def with_lock_retry: () { () -> untyped } -> untyped
60
63
 
@@ -0,0 +1,23 @@
1
+ # Generated from lib/solid_objects/effect_payload.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ module EffectPayload
5
+ # @rbs [Arguments] (effect_id: String, arguments: Arguments) -> effect_retired_payload[Arguments]
6
+ def self.retired: [Arguments] (effect_id: String, arguments: Arguments) -> effect_retired_payload[Arguments]
7
+
8
+ # @rbs [Arguments, Result] (effect_id: String, arguments: Arguments, result: Result) -> effect_completed_recovery_payload[Arguments, Result]
9
+ def self.recovery_completed: [Arguments, Result] (effect_id: String, arguments: Arguments, result: Result) -> effect_completed_recovery_payload[Arguments, Result]
10
+
11
+ # @rbs (effect_id: String, outcome: effect_observation_outcome) -> effect_observation_payload
12
+ def self.recovery_observation: (effect_id: String, outcome: effect_observation_outcome) -> effect_observation_payload
13
+
14
+ # @rbs [Arguments, Result] (effect_id: String, arguments: Arguments, result: Result) -> effect_success_payload[Arguments, Result]
15
+ def self.success: [Arguments, Result] (effect_id: String, arguments: Arguments, result: Result) -> effect_success_payload[Arguments, Result]
16
+
17
+ # @rbs [Arguments] (effect_id: String, arguments: Arguments, error: effect_error) -> effect_failure_payload[Arguments]
18
+ def self.failure: [Arguments] (effect_id: String, arguments: Arguments, error: effect_error) -> effect_failure_payload[Arguments]
19
+
20
+ # @rbs (Exception) -> effect_error
21
+ def self.error: (Exception) -> effect_error
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # Generated from lib/solid_objects/effect_recovery_coordinator.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ module EffectRecoveryOutcome
5
+ RETIRED: "retired"
6
+
7
+ DEFERRED: "deferred"
8
+
9
+ PENDING: "pending"
10
+
11
+ COMPLETED: "completed"
12
+
13
+ DEAD: "dead"
14
+
15
+ ALREADY_RETIRED: "already_retired"
16
+
17
+ MISSING: "missing"
18
+ end
19
+
20
+ class EffectRecoveryCoordinator
21
+ # @rbs (instance: Instance, intents: Array[Actor::EffectRecoveryIntent]) -> void
22
+ def check: (instance: Instance, intents: Array[Actor::EffectRecoveryIntent]) -> void
23
+
24
+ # @rbs () -> void
25
+ def recover_available: () -> void
26
+
27
+ private
28
+
29
+ # @rbs () -> ActiveRecord::Relation[EffectRecovery]
30
+ def recovery_candidates: () -> ActiveRecord::Relation[EffectRecovery]
31
+
32
+ # @rbs (instance: Instance, intent: Actor::EffectRecoveryIntent, recovery: EffectRecovery, effect: Effect?, owners: Hash[String, Process], now: Time) -> void
33
+ def check_one: (instance: Instance, intent: Actor::EffectRecoveryIntent, recovery: EffectRecovery, effect: Effect?, owners: Hash[String, Process], now: Time) -> void
34
+
35
+ # @rbs (effect: Effect?, recovery: EffectRecovery, owners: Hash[String, Process], now: Time) -> String
36
+ def observe: (effect: Effect?, recovery: EffectRecovery, owners: Hash[String, Process], now: Time) -> String
37
+
38
+ # @rbs (instance: Instance, effect: Effect, recovery: EffectRecovery, now: Time) -> Message
39
+ def retire: (instance: Instance, effect: Effect, recovery: EffectRecovery, now: Time) -> Message
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # Generated from lib/solid_objects/process_heartbeat.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class ProcessHeartbeat
5
+ @process_registry: ProcessRegistry
6
+
7
+ @mutex: Thread::Mutex
8
+
9
+ @condition: Thread::ConditionVariable
10
+
11
+ @stopped: bool
12
+
13
+ @thread: Thread?
14
+
15
+ # @rbs (process_registry: ProcessRegistry) -> void
16
+ def initialize: (process_registry: ProcessRegistry) -> void
17
+
18
+ # @rbs () -> void
19
+ def start: () -> void
20
+
21
+ # @rbs () -> void
22
+ def stop: () -> void
23
+
24
+ private
25
+
26
+ attr_reader process_registry: untyped
27
+
28
+ attr_reader mutex: untyped
29
+
30
+ attr_reader condition: untyped
31
+
32
+ # @rbs (Exception) -> void
33
+ def report_failure: (Exception) -> void
34
+
35
+ # @rbs () -> bool
36
+ def wait_for_interval: () -> bool
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ # Generated from app/models/solid_objects/effect_recovery.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class EffectRecovery < Record
5
+ end
6
+ end
@@ -0,0 +1,33 @@
1
+ module SolidObjects
2
+ type effect_handle = { "effect_id" => String }
3
+
4
+ type effect_retired_payload[Arguments] = {
5
+ "effect_id" => String, "arguments" => Arguments, "outcome" => "retired"
6
+ }
7
+
8
+ type effect_completed_recovery_payload[Arguments, Result] = {
9
+ "effect_id" => String, "arguments" => Arguments, "outcome" => "completed", "result" => Result
10
+ }
11
+
12
+ type effect_observation_outcome = "deferred" | "pending" | "dead" | "already_retired" | "missing"
13
+ type effect_observation_payload = { "effect_id" => String, "outcome" => effect_observation_outcome }
14
+ type effect_recovery_payload[Arguments, Result] = effect_retired_payload[Arguments] | effect_completed_recovery_payload[Arguments, Result] | effect_observation_payload
15
+
16
+ type effect_error = {
17
+ "class" => String?,
18
+ "message" => String,
19
+ "backtrace" => Array[String]
20
+ }
21
+
22
+ type effect_failure_payload[Arguments] = {
23
+ "effect_id" => String,
24
+ "arguments" => Arguments,
25
+ "error" => effect_error
26
+ }
27
+
28
+ type effect_success_payload[Arguments, Result] = {
29
+ "effect_id" => String,
30
+ "arguments" => Arguments,
31
+ "result" => Result
32
+ }
33
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.6
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-12 00:00:00.000000000 Z
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actioncable
@@ -303,6 +303,7 @@ files:
303
303
  - app/models/solid_objects/claimed_message.rb
304
304
  - app/models/solid_objects/dead_letter.rb
305
305
  - app/models/solid_objects/effect.rb
306
+ - app/models/solid_objects/effect_recovery.rb
306
307
  - app/models/solid_objects/instance.rb
307
308
  - app/models/solid_objects/message.rb
308
309
  - app/models/solid_objects/process.rb
@@ -330,6 +331,7 @@ files:
330
331
  - db/migrate/20260805000000_create_solid_objects_tables.rb
331
332
  - db/migrate/20260806000000_add_state_revision_to_solid_objects_instances.rb
332
333
  - db/migrate/20260813000000_rename_message_dispatch_columns.rb
334
+ - db/migrate/20260915000000_add_solid_objects_effect_recoveries.rb
333
335
  - docs/adr/0001-postgresql-backend.md
334
336
  - docs/adr/0002-jsonb-actor-state.md
335
337
  - docs/adr/0003-mailbox-ordering.md
@@ -350,6 +352,7 @@ files:
350
352
  - docs/dashboard.md
351
353
  - docs/database-schema.md
352
354
  - docs/development.md
355
+ - docs/effect-recovery.md
353
356
  - docs/fit.md
354
357
  - docs/implementation-plan.md
355
358
  - docs/local-testing.md
@@ -389,6 +392,7 @@ files:
389
392
  - lib/solid_objects/actor_channel.rb
390
393
  - lib/solid_objects/actor_definition.rb
391
394
  - lib/solid_objects/actor_registry.rb
395
+ - lib/solid_objects/actor_signatures.rb
392
396
  - lib/solid_objects/actor_snapshot.rb
393
397
  - lib/solid_objects/actor_view.rb
394
398
  - lib/solid_objects/administration.rb
@@ -416,6 +420,8 @@ files:
416
420
  - lib/solid_objects/doctor.rb
417
421
  - lib/solid_objects/dom_identity.rb
418
422
  - lib/solid_objects/effect_executor.rb
423
+ - lib/solid_objects/effect_payload.rb
424
+ - lib/solid_objects/effect_recovery_coordinator.rb
419
425
  - lib/solid_objects/effect_registry.rb
420
426
  - lib/solid_objects/engine.rb
421
427
  - lib/solid_objects/errors.rb
@@ -431,6 +437,7 @@ files:
431
437
  - lib/solid_objects/operation_dispatcher.rb
432
438
  - lib/solid_objects/payload_broadcast.rb
433
439
  - lib/solid_objects/polling_backoff.rb
440
+ - lib/solid_objects/process_heartbeat.rb
434
441
  - lib/solid_objects/process_pruner.rb
435
442
  - lib/solid_objects/process_registry.rb
436
443
  - lib/solid_objects/reference.rb
@@ -478,6 +485,7 @@ files:
478
485
  - sig/generated/lib/solid_objects/actor_channel.rbs
479
486
  - sig/generated/lib/solid_objects/actor_definition.rbs
480
487
  - sig/generated/lib/solid_objects/actor_registry.rbs
488
+ - sig/generated/lib/solid_objects/actor_signatures.rbs
481
489
  - sig/generated/lib/solid_objects/actor_snapshot.rbs
482
490
  - sig/generated/lib/solid_objects/actor_view.rbs
483
491
  - sig/generated/lib/solid_objects/administration.rbs
@@ -505,6 +513,8 @@ files:
505
513
  - sig/generated/lib/solid_objects/doctor.rbs
506
514
  - sig/generated/lib/solid_objects/dom_identity.rbs
507
515
  - sig/generated/lib/solid_objects/effect_executor.rbs
516
+ - sig/generated/lib/solid_objects/effect_payload.rbs
517
+ - sig/generated/lib/solid_objects/effect_recovery_coordinator.rbs
508
518
  - sig/generated/lib/solid_objects/effect_registry.rbs
509
519
  - sig/generated/lib/solid_objects/engine.rbs
510
520
  - sig/generated/lib/solid_objects/errors.rbs
@@ -520,6 +530,7 @@ files:
520
530
  - sig/generated/lib/solid_objects/operation_dispatcher.rbs
521
531
  - sig/generated/lib/solid_objects/payload_broadcast.rbs
522
532
  - sig/generated/lib/solid_objects/polling_backoff.rbs
533
+ - sig/generated/lib/solid_objects/process_heartbeat.rbs
523
534
  - sig/generated/lib/solid_objects/process_pruner.rbs
524
535
  - sig/generated/lib/solid_objects/process_registry.rbs
525
536
  - sig/generated/lib/solid_objects/reference.rbs
@@ -555,12 +566,14 @@ files:
555
566
  - sig/generated/models/solid_objects/claimed_message.rbs
556
567
  - sig/generated/models/solid_objects/dead_letter.rbs
557
568
  - sig/generated/models/solid_objects/effect.rbs
569
+ - sig/generated/models/solid_objects/effect_recovery.rbs
558
570
  - sig/generated/models/solid_objects/instance.rbs
559
571
  - sig/generated/models/solid_objects/message.rbs
560
572
  - sig/generated/models/solid_objects/process.rbs
561
573
  - sig/generated/models/solid_objects/ready_message.rbs
562
574
  - sig/generated/models/solid_objects/record.rbs
563
575
  - sig/generated/models/solid_objects/reminder.rbs
576
+ - sig/public/effect_payload.rbs
564
577
  - sig/support/framework.rbs
565
578
  - web/assets/javascripts/application.js
566
579
  - web/assets/javascripts/charts.js