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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +81 -0
- data/app/controllers/solid_objects/components_controller.rb +3 -18
- data/benchmark/support.rb +47 -5
- data/docs/benchmarks.md +19 -2
- data/docs/local-testing.md +91 -0
- data/docs/realtime.md +74 -6
- data/docs/roadmap.md +56 -36
- data/exe/solid_objects +12 -1
- data/lib/generators/solid_objects/templates/solid_objects.rb +8 -0
- data/lib/solid_objects/actor_channel.rb +50 -10
- data/lib/solid_objects/callable_keywords.rb +29 -0
- data/lib/solid_objects/component_subscriptions.rb +19 -11
- data/lib/solid_objects/configuration.rb +13 -0
- data/lib/solid_objects/database_adapter.rb +37 -0
- data/lib/solid_objects/database_adapters/mysql.rb +28 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +15 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +5 -0
- data/lib/solid_objects/doctor.rb +16 -0
- data/lib/solid_objects/payload_broadcast.rb +29 -1
- data/lib/solid_objects/supervisor.rb +76 -0
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/wake_up_adapters/redis.rb +183 -0
- data/lib/solid_objects.rb +2 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +0 -8
- data/sig/generated/lib/solid_objects/actor_channel.rbs +19 -0
- data/sig/generated/lib/solid_objects/callable_keywords.rbs +16 -0
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +5 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
- data/sig/generated/lib/solid_objects/database_adapter.rbs +17 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +11 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
- data/sig/generated/lib/solid_objects/doctor.rbs +3 -0
- data/sig/generated/lib/solid_objects/payload_broadcast.rbs +13 -0
- data/sig/generated/lib/solid_objects/supervisor.rbs +32 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +96 -0
- 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
|
|
|
@@ -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.
|
|
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
|