solid_objects 0.8.0 → 0.10.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/app/controllers/solid_objects/components_controller.rb +3 -18
  4. data/benchmark/support.rb +47 -5
  5. data/docs/benchmarks.md +19 -2
  6. data/docs/local-testing.md +91 -0
  7. data/docs/realtime.md +74 -6
  8. data/docs/roadmap.md +56 -36
  9. data/exe/solid_objects +12 -1
  10. data/lib/generators/solid_objects/templates/solid_objects.rb +8 -0
  11. data/lib/solid_objects/actor_channel.rb +50 -10
  12. data/lib/solid_objects/callable_keywords.rb +29 -0
  13. data/lib/solid_objects/component_subscriptions.rb +19 -11
  14. data/lib/solid_objects/configuration.rb +13 -0
  15. data/lib/solid_objects/database_adapter.rb +37 -0
  16. data/lib/solid_objects/database_adapters/mysql.rb +28 -0
  17. data/lib/solid_objects/database_adapters/postgresql.rb +15 -0
  18. data/lib/solid_objects/database_adapters/sqlite.rb +5 -0
  19. data/lib/solid_objects/doctor.rb +16 -0
  20. data/lib/solid_objects/payload_broadcast.rb +29 -1
  21. data/lib/solid_objects/supervisor.rb +76 -0
  22. data/lib/solid_objects/version.rb +1 -1
  23. data/lib/solid_objects/wake_up_adapters/redis.rb +183 -0
  24. data/lib/solid_objects.rb +2 -0
  25. data/sig/generated/controllers/solid_objects/components_controller.rbs +0 -8
  26. data/sig/generated/lib/solid_objects/actor_channel.rbs +19 -0
  27. data/sig/generated/lib/solid_objects/callable_keywords.rbs +16 -0
  28. data/sig/generated/lib/solid_objects/component_subscriptions.rbs +5 -0
  29. data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
  30. data/sig/generated/lib/solid_objects/database_adapter.rbs +17 -0
  31. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +11 -0
  32. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
  33. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
  34. data/sig/generated/lib/solid_objects/doctor.rbs +3 -0
  35. data/sig/generated/lib/solid_objects/payload_broadcast.rbs +13 -0
  36. data/sig/generated/lib/solid_objects/supervisor.rbs +32 -0
  37. data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +96 -0
  38. metadata +6 -1
@@ -0,0 +1,183 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "timeout"
4
+
5
+ module SolidObjects
6
+ module WakeUpAdapters
7
+ # Wakes runtime roles across processes using Redis publish/subscribe.
8
+ #
9
+ # MySQL has no notification primitive, so this is the cross-process option
10
+ # for applications that cannot use PostgreSQL notifications. It is optional
11
+ # in every sense: the `redis` gem is not a dependency of this gem, and the
12
+ # polling interval remains the upper bound, so a missed or failed
13
+ # notification costs latency rather than correctness.
14
+ class Redis
15
+ CHANNEL = "solid_objects_wake_up"
16
+ FAILED_WAIT_INTERVAL = 0.05
17
+ SUBSCRIBE_TIMEOUT = 5.0
18
+
19
+ # @rbs @channel: String
20
+ # @rbs @url: String?
21
+ # @rbs @client: untyped
22
+ # @rbs @mutex: Thread::Mutex
23
+ # @rbs @condition: Thread::ConditionVariable
24
+ # @rbs @subscriber: Thread?
25
+ # @rbs @subscription: untyped
26
+ # @rbs @signalled: Integer
27
+
28
+ attr_reader :channel
29
+
30
+ # @rbs (?channel: String, ?url: String?, ?client: untyped) -> void
31
+ def initialize(channel: CHANNEL, url: nil, client: nil)
32
+ @channel = channel
33
+ @url = url
34
+ @client = client
35
+ @mutex = Thread::Mutex.new
36
+ @condition = Thread::ConditionVariable.new
37
+ @subscriber = nil
38
+ @subscription = nil
39
+ @signalled = 0
40
+ validate_client!
41
+ end
42
+
43
+ # @rbs () -> bool
44
+ def signal
45
+ publisher.publish(channel, "1")
46
+ true
47
+ rescue => error
48
+ instrument_failure(:signal, error)
49
+ false
50
+ end
51
+
52
+ # The counter is snapshotted before subscribing, and re-checked before
53
+ # blocking, so a signal delivered while this caller was still getting
54
+ # ready is observed rather than absorbed into the new baseline.
55
+ # @rbs (timeout: Numeric) -> bool
56
+ def wait(timeout:)
57
+ signalled = mutex.synchronize { @signalled }
58
+ return paced_failure(timeout) unless listen
59
+
60
+ mutex.synchronize do
61
+ return true unless @signalled == signalled
62
+
63
+ condition.wait(mutex, timeout.to_f)
64
+ @signalled != signalled
65
+ end
66
+ end
67
+
68
+ # Redis delivers to a subscribed connection only, and a subscribed
69
+ # connection cannot serve other callers, so one background subscription
70
+ # per process fans out to every waiting role in memory. Subscribing
71
+ # eagerly also closes the window where a signal sent during startup would
72
+ # be missed.
73
+ # @rbs () -> bool
74
+ def listen
75
+ mutex.synchronize do
76
+ return true if @subscriber&.alive?
77
+
78
+ ready = Queue.new
79
+ @subscriber = Thread.new { subscribe_loop(ready) }
80
+ Timeout.timeout(SUBSCRIBE_TIMEOUT) { ready.pop } == :subscribed
81
+ end
82
+ rescue => error
83
+ instrument_failure(:listen, error)
84
+ false
85
+ end
86
+
87
+ # @rbs () -> bool
88
+ def stop
89
+ subscriber = mutex.synchronize do
90
+ thread = @subscriber
91
+ @subscriber = nil
92
+ thread
93
+ end
94
+ return false unless subscriber
95
+
96
+ disconnect(@subscription)
97
+ subscriber.join(SUBSCRIBE_TIMEOUT)
98
+ subscriber.kill if subscriber.alive?
99
+ true
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :mutex, :condition, :url
105
+
106
+ # @rbs (Queue) -> void
107
+ def subscribe_loop(ready)
108
+ connection = build_client
109
+ @subscription = connection
110
+ connection.subscribe(channel) do |on|
111
+ on.subscribe { ready << :subscribed }
112
+ on.message { broadcast }
113
+ end
114
+ rescue => error
115
+ instrument_failure(:subscribe, error)
116
+ ready << :failed
117
+ end
118
+
119
+ # @rbs () -> void
120
+ def broadcast
121
+ mutex.synchronize do
122
+ @signalled += 1
123
+ condition.broadcast
124
+ end
125
+ end
126
+
127
+ # @rbs (Numeric) -> bool
128
+ def paced_failure(timeout)
129
+ pace_after_failure(timeout)
130
+ false
131
+ end
132
+
133
+ # @rbs () -> untyped
134
+ def publisher
135
+ @publisher ||= build_client
136
+ end
137
+
138
+ # @rbs () -> untyped
139
+ def build_client
140
+ return @client.call if @client.respond_to?(:call)
141
+
142
+ require "redis"
143
+ url ? ::Redis.new(url:) : ::Redis.new
144
+ rescue LoadError
145
+ raise ArgumentError,
146
+ "the redis gem is required for SolidObjects::WakeUpAdapters::Redis"
147
+ end
148
+
149
+ # @rbs () -> void
150
+ def validate_client!
151
+ return if @client.nil? || @client.respond_to?(:call)
152
+
153
+ raise ArgumentError, "client must respond to call and return a Redis client"
154
+ end
155
+
156
+ # @rbs (untyped) -> void
157
+ def disconnect(connection)
158
+ connection&.close
159
+ rescue
160
+ nil
161
+ end
162
+
163
+ # @rbs (Numeric) -> void
164
+ def pace_after_failure(timeout)
165
+ interval = [ timeout.to_f, FAILED_WAIT_INTERVAL ].min
166
+ return unless interval.positive?
167
+
168
+ sleep interval
169
+ end
170
+
171
+ # @rbs (Symbol, Exception) -> void
172
+ def instrument_failure(operation, error)
173
+ SolidObjects.instrument(
174
+ :"wake_up.failed",
175
+ adapter: "redis",
176
+ operation: operation.to_s,
177
+ error_class: error.class.name,
178
+ error_message: error.message
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
data/lib/solid_objects.rb CHANGED
@@ -13,6 +13,7 @@ require "securerandom"
13
13
  require "solid_objects/version"
14
14
  require "solid_objects/errors"
15
15
  require "solid_objects/sync_deadline"
16
+ require "solid_objects/callable_keywords"
16
17
  require "solid_objects/configuration"
17
18
  require "solid_objects/instrumentation"
18
19
  require "solid_objects/log_subscriber"
@@ -47,6 +48,7 @@ require "solid_objects/actor_channel"
47
48
  require "solid_objects/action_cable_broadcast_adapter"
48
49
  require "solid_objects/wake_up"
49
50
  require "solid_objects/wake_up_adapters/postgresql"
51
+ require "solid_objects/wake_up_adapters/redis"
50
52
  require "solid_objects/wake_up_adapters"
51
53
  require "solid_objects/effect_registry"
52
54
  require "solid_objects/commit_action_registry"
@@ -27,14 +27,6 @@ module SolidObjects
27
27
  # @rbs (Array[ComponentRegistration]) -> untyped
28
28
  def component_authorization_context: (Array[ComponentRegistration]) -> untyped
29
29
 
30
- # A lambda answers `parameters` directly; a callable object answers it
31
- # through its `call` method.
32
- # @rbs (untyped) -> bool
33
- def accepts_registrations?: (untyped) -> bool
34
-
35
- # @rbs (untyped) -> Array[[ Symbol, Symbol ]]
36
- def callable_parameters: (untyped) -> Array[[ Symbol, Symbol ]]
37
-
38
30
  # @rbs (ComponentRegistration) -> Hash[Symbol, untyped]
39
31
  def registration_payload: (ComponentRegistration) -> Hash[Symbol, untyped]
40
32
 
@@ -21,6 +21,25 @@ module SolidObjects
21
21
  # @rbs (ActorSnapshot) -> void
22
22
  def transmit_state_payloads: (ActorSnapshot) -> void
23
23
 
24
+ # A payload is one subscriber's view of one name. Letting it raise through
25
+ # here would reject the subscription or abandon the rest of a broadcast, so
26
+ # a failure is confined to the payload that caused it and reported. The
27
+ # exception message is deliberately not instrumented: a payload block reads
28
+ # actor state, so its message is the one place subscriber state could leak
29
+ # into logs.
30
+ #
31
+ # Returns whether this revision was settled for the name. An unauthorized
32
+ # payload is settled: the decision is stable, so retrying it would only
33
+ # re-deliver its authorized siblings.
34
+ # @rbs (ActorSnapshot, String) -> bool
35
+ def transmit_state_payload: (ActorSnapshot, String) -> bool
36
+
37
+ # Resolves the Cable connection to whatever the application uses as an
38
+ # authorization subject, so a payload block and `authorize_query` see the
39
+ # same object a controller render would pass.
40
+ # @rbs (String) -> untyped
41
+ def payload_authorization_context: (String) -> untyped
42
+
24
43
  # @rbs (ActorSnapshot) -> bool
25
44
  def newer_payload_revision?: (ActorSnapshot) -> bool
26
45
 
@@ -0,0 +1,16 @@
1
+ # Generated from lib/solid_objects/callable_keywords.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ # Authorization context resolvers gained keywords after applications had
5
+ # already written them, so a resolver is called with what it declared it
6
+ # accepts rather than with everything the caller could offer.
7
+ module CallableKeywords
8
+ # @rbs (untyped, Symbol) -> bool
9
+ def self.accepts?: (untyped, Symbol) -> bool
10
+
11
+ # A lambda answers `parameters` directly; a callable object answers it
12
+ # through its `call` method.
13
+ # @rbs (untyped) -> Array[[ Symbol, Symbol ]]
14
+ private def self.parameters: (untyped) -> Array[[ Symbol, Symbol ]]
15
+ end
16
+ end
@@ -31,6 +31,11 @@ module SolidObjects
31
31
 
32
32
  attr_reader revisions: untyped
33
33
 
34
+ # Live invalidations and reconnect replays share this, so a reconnecting
35
+ # client pays the same number of requests a connected one does.
36
+ # @rbs (Array[ComponentRegistration], Integer, Integer) -> Array[String]
37
+ def refresh_streams: (Array[ComponentRegistration], Integer, Integer) -> Array[String]
38
+
34
39
  # @rbs (ComponentRegistration, Integer, Integer) -> void
35
40
  def record_revision: (ComponentRegistration, Integer, Integer) -> void
36
41
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Configuration
5
- @shutdown_timeout: Float
6
-
7
5
  @supervisor_monitor_interval: Float
8
6
 
7
+ @retention_interval: Float
8
+
9
9
  @dead_process_cleanup_interval: Float
10
10
 
11
11
  @message_retention: Numeric
@@ -40,6 +40,8 @@ module SolidObjects
40
40
 
41
41
  @component_authorization_context: Proc
42
42
 
43
+ @payload_authorization_context: Proc
44
+
43
45
  @authorize_message: Proc
44
46
 
45
47
  @authorize_query: Proc
@@ -86,6 +88,8 @@ module SolidObjects
86
88
 
87
89
  @process_alive_threshold: Float
88
90
 
91
+ @shutdown_timeout: Float
92
+
89
93
  attr_accessor table_name_prefix: untyped
90
94
 
91
95
  attr_accessor polling_interval: untyped
@@ -126,6 +130,8 @@ module SolidObjects
126
130
 
127
131
  attr_accessor supervisor_monitor_interval: untyped
128
132
 
133
+ attr_accessor retention_interval: untyped
134
+
129
135
  attr_accessor dead_process_cleanup_interval: untyped
130
136
 
131
137
  attr_accessor message_retention: untyped
@@ -160,6 +166,8 @@ module SolidObjects
160
166
 
161
167
  attr_accessor component_authorization_context: untyped
162
168
 
169
+ attr_accessor payload_authorization_context: untyped
170
+
163
171
  attr_accessor authorize_message: untyped
164
172
 
165
173
  attr_accessor authorize_query: untyped
@@ -16,6 +16,23 @@ module SolidObjects
16
16
  # @rbs (untyped) -> void
17
17
  def initialize: (untyped) -> void
18
18
 
19
+ # The oldest server the adapter has been exercised against. Reported rather
20
+ # than enforced: refusing to boot on an untested server would be a worse
21
+ # failure than running on one.
22
+ # @rbs () -> Gem::Version?
23
+ def minimum_server_version: () -> Gem::Version?
24
+
25
+ # @rbs () -> Gem::Version
26
+ def server_version: () -> Gem::Version
27
+
28
+ # One observed version decides both the status and the message. Reading it
29
+ # again could let a transient failure replace an already determined result.
30
+ # @rbs (?Gem::Version?) -> Array[String]
31
+ def unsupported_server_reasons: (?Gem::Version?) -> Array[String]
32
+
33
+ # @rbs () -> Array[String]
34
+ def additional_server_reasons: () -> Array[String]
35
+
19
36
  # @rbs () -> bool
20
37
  def supports_skip_locked?: () -> bool
21
38
 
@@ -9,6 +9,17 @@ module SolidObjects
9
9
  # @rbs () -> String
10
10
  def claim_lock: () -> String
11
11
 
12
+ # A non-transactional engine would silently break fenced commits, so the
13
+ # storage engine is verified rather than assumed.
14
+ # @rbs () -> Array[String]
15
+ def additional_server_reasons: () -> Array[String]
16
+
17
+ # @rbs () -> Array[String]
18
+ def non_innodb_tables: () -> Array[String]
19
+
20
+ # @rbs () -> Gem::Version?
21
+ def minimum_server_version: () -> Gem::Version?
22
+
12
23
  # @rbs () -> String
13
24
  def current_time_expression: () -> String
14
25
 
@@ -3,6 +3,14 @@
3
3
  module SolidObjects
4
4
  module DatabaseAdapters
5
5
  class Postgresql < DatabaseAdapter
6
+ # @rbs () -> Gem::Version?
7
+ def minimum_server_version: () -> Gem::Version?
8
+
9
+ # PostgreSQL reports a packed integer, 170010 for 17.10, so comparing it
10
+ # directly would make every server look newer than any minimum.
11
+ # @rbs () -> Gem::Version
12
+ def server_version: () -> Gem::Version
13
+
6
14
  # @rbs () -> bool
7
15
  def supports_skip_locked?: () -> bool
8
16
 
@@ -11,6 +11,9 @@ module SolidObjects
11
11
 
12
12
  LOCK_RETRY_CONDITION: untyped
13
13
 
14
+ # @rbs () -> Gem::Version?
15
+ def minimum_server_version: () -> Gem::Version?
16
+
14
17
  # @rbs () -> String
15
18
  def current_time_expression: () -> String
16
19
 
@@ -72,6 +72,9 @@ module SolidObjects
72
72
  # @rbs () -> Check
73
73
  def check_authorization: () -> Check
74
74
 
75
+ # @rbs () -> Check
76
+ def check_database_server: () -> Check
77
+
75
78
  # @rbs () -> Check
76
79
  def check_runtime: () -> Check
77
80
 
@@ -29,6 +29,19 @@ module SolidObjects
29
29
  # @rbs (ActorDefinition::Handler) -> untyped
30
30
  def rendered_payload: (ActorDefinition::Handler) -> untyped
31
31
 
32
+ # The block runs against the actor instance, like every other block in the
33
+ # actor DSL, and still receives the actor and the resolved authorization
34
+ # context as arguments, so blocks written to the documented signature are
35
+ # unaffected.
36
+ # @rbs (ActorDefinition::Handler) -> untyped
37
+ def evaluated_payload: (ActorDefinition::Handler) -> untyped
38
+
39
+ # Distinguishes a block that relied on the old class-level receiver from an
40
+ # ordinary typo, so the one behaviour change reports itself instead of
41
+ # surfacing as an unexplained NameError.
42
+ # @rbs (NameError[untyped]) -> bool
43
+ def class_level_receiver?: (NameError[untyped]) -> bool
44
+
32
45
  # @rbs () -> void
33
46
  def authorize!: () -> void
34
47
  end
@@ -2,8 +2,12 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Supervisor
5
+ MAXIMUM_RETENTION_BACKOFF_DOUBLINGS: ::Integer
6
+
5
7
  @lifecycle: Thread::Mutex
6
8
 
9
+ @retention: Thread?
10
+
7
11
  @cleaned_up_at: Float
8
12
 
9
13
  @started: bool
@@ -52,6 +56,34 @@ module SolidObjects
52
56
  # @rbs (Thread?) -> String?
53
57
  def thread_error: (Thread?) -> String?
54
58
 
59
+ # Retention gets its own thread rather than sharing the monitor's. A large
60
+ # backlog or a lock wait can make a pass slow, and role replacement must not
61
+ # wait behind housekeeping.
62
+ # @rbs () -> void
63
+ def retention_loop: () -> void
64
+
65
+ # Sleeping the whole interval would make shutdown wait out an hour-long
66
+ # nap, so the pause is taken in short steps that notice a stop request.
67
+ # @rbs (Integer) -> void
68
+ def wait_for_next_retention: (Integer) -> void
69
+
70
+ # Every actor call writes a durable message row, so retention that is only
71
+ # configured and never run leaves those rows to grow without bound. The
72
+ # supervisor runs it rather than requiring every application to schedule
73
+ # its own job.
74
+ # @rbs () -> void
75
+ def prune_expired_records: () -> void
76
+
77
+ # A transient lock or connection error must not defer retention for the
78
+ # whole interval, so a failed pass retries at monitor cadence. The pause
79
+ # then doubles per consecutive failure, capped by the interval, so a
80
+ # database that stays down is not polled once a second forever.
81
+ # @rbs (Integer) -> Float
82
+ def retention_pause: (Integer) -> Float
83
+
84
+ # @rbs () -> void
85
+ def stop_retention: () -> void
86
+
55
87
  # @rbs () -> void
56
88
  def cleanup_dead_processes: () -> void
57
89
 
@@ -0,0 +1,96 @@
1
+ # Generated from lib/solid_objects/wake_up_adapters/redis.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ module WakeUpAdapters
5
+ # Wakes runtime roles across processes using Redis publish/subscribe.
6
+ #
7
+ # MySQL has no notification primitive, so this is the cross-process option
8
+ # for applications that cannot use PostgreSQL notifications. It is optional
9
+ # in every sense: the `redis` gem is not a dependency of this gem, and the
10
+ # polling interval remains the upper bound, so a missed or failed
11
+ # notification costs latency rather than correctness.
12
+ class Redis
13
+ CHANNEL: ::String
14
+
15
+ FAILED_WAIT_INTERVAL: ::Float
16
+
17
+ SUBSCRIBE_TIMEOUT: ::Float
18
+
19
+ @signalled: Integer
20
+
21
+ @subscription: untyped
22
+
23
+ @subscriber: Thread?
24
+
25
+ @condition: Thread::ConditionVariable
26
+
27
+ @mutex: Thread::Mutex
28
+
29
+ @client: untyped
30
+
31
+ @url: String?
32
+
33
+ @channel: String
34
+
35
+ attr_reader channel: untyped
36
+
37
+ # @rbs (?channel: String, ?url: String?, ?client: untyped) -> void
38
+ def initialize: (?channel: String, ?url: String?, ?client: untyped) -> void
39
+
40
+ # @rbs () -> bool
41
+ def signal: () -> bool
42
+
43
+ # The counter is snapshotted before subscribing, and re-checked before
44
+ # blocking, so a signal delivered while this caller was still getting
45
+ # ready is observed rather than absorbed into the new baseline.
46
+ # @rbs (timeout: Numeric) -> bool
47
+ def wait: (timeout: Numeric) -> bool
48
+
49
+ # Redis delivers to a subscribed connection only, and a subscribed
50
+ # connection cannot serve other callers, so one background subscription
51
+ # per process fans out to every waiting role in memory. Subscribing
52
+ # eagerly also closes the window where a signal sent during startup would
53
+ # be missed.
54
+ # @rbs () -> bool
55
+ def listen: () -> bool
56
+
57
+ # @rbs () -> bool
58
+ def stop: () -> bool
59
+
60
+ private
61
+
62
+ attr_reader mutex: untyped
63
+
64
+ attr_reader condition: untyped
65
+
66
+ attr_reader url: untyped
67
+
68
+ # @rbs (Queue) -> void
69
+ def subscribe_loop: (Queue) -> void
70
+
71
+ # @rbs () -> void
72
+ def broadcast: () -> void
73
+
74
+ # @rbs (Numeric) -> bool
75
+ def paced_failure: (Numeric) -> bool
76
+
77
+ # @rbs () -> untyped
78
+ def publisher: () -> untyped
79
+
80
+ # @rbs () -> untyped
81
+ def build_client: () -> untyped
82
+
83
+ # @rbs () -> void
84
+ def validate_client!: () -> void
85
+
86
+ # @rbs (untyped) -> void
87
+ def disconnect: (untyped) -> void
88
+
89
+ # @rbs (Numeric) -> void
90
+ def pace_after_failure: (Numeric) -> void
91
+
92
+ # @rbs (Symbol, Exception) -> void
93
+ def instrument_failure: (Symbol, Exception) -> void
94
+ end
95
+ end
96
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson
@@ -319,6 +319,7 @@ files:
319
319
  - docs/development.md
320
320
  - docs/fit.md
321
321
  - docs/implementation-plan.md
322
+ - docs/local-testing.md
322
323
  - docs/migrating-existing-state.md
323
324
  - docs/operations.md
324
325
  - docs/realtime.md
@@ -353,6 +354,7 @@ files:
353
354
  - lib/solid_objects/application_actor_loader.rb
354
355
  - lib/solid_objects/application_write_guard.rb
355
356
  - lib/solid_objects/broadcast_executor.rb
357
+ - lib/solid_objects/callable_keywords.rb
356
358
  - lib/solid_objects/caller_process.rb
357
359
  - lib/solid_objects/cli.rb
358
360
  - lib/solid_objects/client.rb
@@ -405,6 +407,7 @@ files:
405
407
  - lib/solid_objects/wake_up.rb
406
408
  - lib/solid_objects/wake_up_adapters.rb
407
409
  - lib/solid_objects/wake_up_adapters/postgresql.rb
410
+ - lib/solid_objects/wake_up_adapters/redis.rb
408
411
  - lib/solid_objects/worker.rb
409
412
  - lib/tasks/solid_objects_tasks.rake
410
413
  - sig/generated/controllers/solid_objects/application_controller.rbs
@@ -426,6 +429,7 @@ files:
426
429
  - sig/generated/lib/solid_objects/application_actor_loader.rbs
427
430
  - sig/generated/lib/solid_objects/application_write_guard.rbs
428
431
  - sig/generated/lib/solid_objects/broadcast_executor.rbs
432
+ - sig/generated/lib/solid_objects/callable_keywords.rbs
429
433
  - sig/generated/lib/solid_objects/caller_process.rbs
430
434
  - sig/generated/lib/solid_objects/cli.rbs
431
435
  - sig/generated/lib/solid_objects/client.rbs
@@ -478,6 +482,7 @@ files:
478
482
  - sig/generated/lib/solid_objects/wake_up.rbs
479
483
  - sig/generated/lib/solid_objects/wake_up_adapters.rbs
480
484
  - sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs
485
+ - sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs
481
486
  - sig/generated/lib/solid_objects/worker.rbs
482
487
  - sig/generated/models/solid_objects/broadcast.rbs
483
488
  - sig/generated/models/solid_objects/claimed_message.rbs