solid_objects 0.12.1 → 0.13.1
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 +60 -0
- data/README.md +69 -13
- data/benchmark/idle_polling.rb +98 -0
- data/benchmark/support.rb +2 -0
- data/docs/adr/0009-realtime-updates.md +5 -1
- data/docs/adr/0011-wake-up-strategy.md +6 -0
- data/docs/architecture.md +5 -4
- data/docs/authorization.md +11 -4
- data/docs/benchmarks.md +38 -0
- data/docs/correctness.md +3 -3
- data/docs/dashboard.md +200 -0
- data/docs/database-schema.md +5 -4
- data/docs/development.md +1 -0
- data/docs/operations.md +24 -0
- data/docs/realtime.md +22 -14
- data/docs/roadmap.md +32 -11
- data/docs/security.md +7 -7
- data/examples/application/README.md +5 -5
- data/examples/application/app/actors/chat_room_actor.rb +1 -1
- data/examples/application/app/actors/shopping_cart_actor.rb +2 -2
- data/lib/solid_objects/actor.rb +1 -1
- data/lib/solid_objects/broadcast_executor.rb +35 -4
- data/lib/solid_objects/configuration.rb +4 -0
- data/lib/solid_objects/effect_executor.rb +34 -3
- data/lib/solid_objects/polling_backoff.rb +45 -0
- data/lib/solid_objects/process_registry.rb +51 -0
- data/lib/solid_objects/reminder_scheduler.rb +35 -4
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/wake_up.rb +36 -4
- data/lib/solid_objects/wake_up_adapters/postgresql.rb +6 -0
- data/lib/solid_objects/wake_up_adapters/redis.rb +25 -3
- data/lib/solid_objects/web/action.rb +109 -0
- data/lib/solid_objects/web/application.rb +239 -0
- data/lib/solid_objects/web/csrf_protection.rb +130 -0
- data/lib/solid_objects/web/helpers.rb +227 -0
- data/lib/solid_objects/web/paginator.rb +64 -0
- data/lib/solid_objects/web/route.rb +55 -0
- data/lib/solid_objects/web/router.rb +46 -0
- data/lib/solid_objects/web/statistics.rb +78 -0
- data/lib/solid_objects/web.rb +222 -0
- data/lib/solid_objects/worker.rb +39 -3
- data/lib/solid_objects.rb +2 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +6 -2
- data/sig/generated/lib/solid_objects/effect_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/polling_backoff.rbs +29 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +15 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +7 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +19 -2
- data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +3 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +17 -2
- data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
- data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
- data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
- data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
- data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
- data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
- data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
- data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
- data/sig/generated/lib/solid_objects/web.rbs +129 -0
- data/sig/generated/lib/solid_objects/worker.rbs +7 -0
- data/web/assets/javascripts/application.js +118 -0
- data/web/assets/javascripts/charts.js +190 -0
- data/web/assets/stylesheets/application.css +527 -0
- data/web/views/_messages.erb +29 -0
- data/web/views/_navigation.erb +15 -0
- data/web/views/_paging.erb +17 -0
- data/web/views/_status_filter.erb +8 -0
- data/web/views/_summary.erb +39 -0
- data/web/views/broadcasts.erb +35 -0
- data/web/views/dashboard.erb +128 -0
- data/web/views/dead_letter.erb +40 -0
- data/web/views/dead_letters.erb +42 -0
- data/web/views/effects.erb +35 -0
- data/web/views/instance.erb +139 -0
- data/web/views/instances.erb +49 -0
- data/web/views/layout.erb +22 -0
- data/web/views/mailbox.erb +35 -0
- data/web/views/message.erb +34 -0
- data/web/views/processes.erb +37 -0
- data/web/views/reminders.erb +37 -0
- metadata +58 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "rack"
|
|
8
|
+
require "rack/builder"
|
|
9
|
+
require "rack/static"
|
|
10
|
+
|
|
11
|
+
require "solid_objects"
|
|
12
|
+
require "solid_objects/web/route"
|
|
13
|
+
require "solid_objects/web/router"
|
|
14
|
+
require "solid_objects/web/statistics"
|
|
15
|
+
require "solid_objects/web/paginator"
|
|
16
|
+
require "solid_objects/web/helpers"
|
|
17
|
+
require "solid_objects/web/action"
|
|
18
|
+
require "solid_objects/web/csrf_protection"
|
|
19
|
+
require "solid_objects/web/application"
|
|
20
|
+
|
|
21
|
+
module SolidObjects
|
|
22
|
+
# An operator dashboard for the actor runtime, served as a Rack application:
|
|
23
|
+
#
|
|
24
|
+
# Rails.application.routes.draw do
|
|
25
|
+
# mount SolidObjects::Web => "/solid_objects"
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# It is deliberately not loaded by `require "solid_objects"`. A worker
|
|
29
|
+
# process must not carry a web stack, and an application that never mounts
|
|
30
|
+
# the dashboard must not pay for it.
|
|
31
|
+
#
|
|
32
|
+
# Every page asks `configuration.authorize_administration` first. That block
|
|
33
|
+
# denies by default, so a mount alone exposes nothing until an application
|
|
34
|
+
# states who may read it.
|
|
35
|
+
class Web
|
|
36
|
+
ROOT = File.expand_path("../../web", __dir__)
|
|
37
|
+
VIEWS = File.join(ROOT, "views")
|
|
38
|
+
ASSETS = File.join(ROOT, "assets")
|
|
39
|
+
|
|
40
|
+
NONCE_KEY = "solid_objects.content_security_policy_nonce"
|
|
41
|
+
CSRF_TOKEN_KEY = "solid_objects.csrf_token"
|
|
42
|
+
NONCE_BYTES = 16
|
|
43
|
+
ASSET_CACHE_SECONDS = 86_400
|
|
44
|
+
TEMPLATE_NAME = /\A_?[a-z][a-z0-9_]*\z/
|
|
45
|
+
|
|
46
|
+
# The dashboard charts need a charting library, and this one is fetched
|
|
47
|
+
# from a public CDN with a subresource integrity hash, so a compromised CDN
|
|
48
|
+
# cannot substitute other code. A deployment with no outbound network
|
|
49
|
+
# access should vendor the file and point `chart_library_url` at it, or set
|
|
50
|
+
# that to nil to render the dashboard without charts.
|
|
51
|
+
CHART_LIBRARY_URL = "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"
|
|
52
|
+
CHART_LIBRARY_INTEGRITY = "sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"
|
|
53
|
+
|
|
54
|
+
DEFAULT_TABS = {
|
|
55
|
+
"Dashboard" => "/",
|
|
56
|
+
"Instances" => "/instances",
|
|
57
|
+
"Mailbox" => "/mailbox",
|
|
58
|
+
"Reminders" => "/reminders",
|
|
59
|
+
"Effects" => "/effects",
|
|
60
|
+
"Broadcasts" => "/broadcasts",
|
|
61
|
+
"Dead letters" => "/dead_letters",
|
|
62
|
+
"Processes" => "/processes"
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
LOCK = Mutex.new
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
# A path below the mount serves a vendored copy; an absolute URL is
|
|
69
|
+
# fetched from that host and is named in the content security policy.
|
|
70
|
+
# nil renders the dashboard without charts.
|
|
71
|
+
# @rbs @chart_library_url: String?
|
|
72
|
+
# @rbs @chart_library_integrity: String?
|
|
73
|
+
attr_writer :chart_library_url, :chart_library_integrity
|
|
74
|
+
|
|
75
|
+
# @rbs () -> String?
|
|
76
|
+
def chart_library_url
|
|
77
|
+
defined?(@chart_library_url) ? @chart_library_url : CHART_LIBRARY_URL
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs () -> String?
|
|
81
|
+
def chart_library_integrity
|
|
82
|
+
defined?(@chart_library_integrity) ? @chart_library_integrity : CHART_LIBRARY_INTEGRITY
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @rbs () -> bool
|
|
86
|
+
def charts?
|
|
87
|
+
!chart_library_url.nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The origin the content security policy has to allow. A vendored copy
|
|
91
|
+
# served from the mount has none, so the policy stays at 'self'.
|
|
92
|
+
# @rbs () -> String?
|
|
93
|
+
def chart_library_origin
|
|
94
|
+
url = chart_library_url
|
|
95
|
+
return nil unless url&.include?("//")
|
|
96
|
+
|
|
97
|
+
uri = URI.parse(url)
|
|
98
|
+
return nil unless uri.scheme && uri.host
|
|
99
|
+
|
|
100
|
+
"#{uri.scheme}://#{uri.host}"
|
|
101
|
+
rescue URI::InvalidURIError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @rbs () -> Hash[String, String]
|
|
106
|
+
def tabs
|
|
107
|
+
@tabs ||= DEFAULT_TABS.dup
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Searched in order, so an extension directory added first wins over the
|
|
111
|
+
# packaged one and an application can replace a single page.
|
|
112
|
+
# @rbs () -> Array[String]
|
|
113
|
+
def views
|
|
114
|
+
@views ||= [ VIEWS ]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @rbs () -> Array[[Array[untyped], Proc?]]
|
|
118
|
+
def middlewares
|
|
119
|
+
@middlewares ||= []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The built stack is memoized, so a middleware added after the first
|
|
123
|
+
# request would otherwise be dropped without a word.
|
|
124
|
+
# @rbs (*untyped) ?{ () -> untyped } -> void
|
|
125
|
+
def use(*arguments, &block)
|
|
126
|
+
middlewares << [ arguments, block ]
|
|
127
|
+
LOCK.synchronize { @application = nil }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Adds pages to the dashboard. The extension receives the application
|
|
131
|
+
# class and declares its own routes on it, which means its routes carry
|
|
132
|
+
# an authorization policy like every other route.
|
|
133
|
+
#
|
|
134
|
+
# @rbs (untyped, tab: String, path: String, ?views: String?) -> void
|
|
135
|
+
def register(extension, tab:, path:, views: nil)
|
|
136
|
+
if views
|
|
137
|
+
self.views.unshift(views)
|
|
138
|
+
# A template compiled before this call resolved against the old
|
|
139
|
+
# search path, so a replacement view would never be reached.
|
|
140
|
+
LOCK.synchronize { @templates = {} }
|
|
141
|
+
end
|
|
142
|
+
tabs[tab] = path
|
|
143
|
+
extension.registered(Application)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @rbs (Symbol) -> untyped
|
|
147
|
+
def template(name)
|
|
148
|
+
LOCK.synchronize do
|
|
149
|
+
templates[name] ||= ERB.new(File.read(template_path(name)), trim_mode: "-")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @rbs () -> void
|
|
154
|
+
def reset!
|
|
155
|
+
LOCK.synchronize { @templates = {} }
|
|
156
|
+
@tabs = nil
|
|
157
|
+
@views = nil
|
|
158
|
+
@middlewares = nil
|
|
159
|
+
@application = nil
|
|
160
|
+
remove_instance_variable(:@chart_library_url) if defined?(@chart_library_url)
|
|
161
|
+
remove_instance_variable(:@chart_library_integrity) if defined?(@chart_library_integrity)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
165
|
+
def call(env)
|
|
166
|
+
application.call(env)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs () -> Web
|
|
170
|
+
def application
|
|
171
|
+
LOCK.synchronize { @application ||= new }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
177
|
+
def templates
|
|
178
|
+
@templates ||= {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# A template name never comes from a request, and this keeps it that way
|
|
182
|
+
# rather than trusting every future caller to know it.
|
|
183
|
+
# @rbs (Symbol) -> String
|
|
184
|
+
def template_path(name)
|
|
185
|
+
raise ArgumentError, "invalid template name" unless name.to_s.match?(TEMPLATE_NAME)
|
|
186
|
+
|
|
187
|
+
found = views.lazy.map { |directory| File.join(directory, "#{name}.erb") }.find { |path| File.exist?(path) }
|
|
188
|
+
found || raise(ArgumentError, "no template named #{name}")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
193
|
+
def call(env)
|
|
194
|
+
env[NONCE_KEY] = SecureRandom.base64(NONCE_BYTES)
|
|
195
|
+
app.call(env)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @rbs () -> untyped
|
|
199
|
+
def app
|
|
200
|
+
@app ||= build
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# @rbs () -> untyped
|
|
206
|
+
def build
|
|
207
|
+
assets = ASSETS
|
|
208
|
+
extra = self.class.middlewares
|
|
209
|
+
|
|
210
|
+
::Rack::Builder.new do
|
|
211
|
+
use ::Rack::Static,
|
|
212
|
+
urls: [ "/stylesheets", "/javascripts" ],
|
|
213
|
+
root: assets,
|
|
214
|
+
cascade: true,
|
|
215
|
+
header_rules: [ [ :all, { "cache-control" => "private, max-age=#{ASSET_CACHE_SECONDS}" } ] ]
|
|
216
|
+
extra.each { |arguments, block| use(*arguments, &block) }
|
|
217
|
+
use CsrfProtection
|
|
218
|
+
run Application.new
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/lib/solid_objects/worker.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "solid_objects/activation_manager"
|
|
|
5
5
|
require "solid_objects/activation"
|
|
6
6
|
require "solid_objects/executor"
|
|
7
7
|
require "solid_objects/lease_renewer"
|
|
8
|
+
require "solid_objects/polling_backoff"
|
|
8
9
|
|
|
9
10
|
module SolidObjects
|
|
10
11
|
class Worker
|
|
@@ -13,6 +14,7 @@ module SolidObjects
|
|
|
13
14
|
# @rbs @activations: Hash[Integer, Activation]
|
|
14
15
|
# @rbs @stopped: bool
|
|
15
16
|
# @rbs @shutdown_requested: bool
|
|
17
|
+
# @rbs @polling_backoff: PollingBackoff
|
|
16
18
|
|
|
17
19
|
# @rbs (?process_registry: ProcessRegistry) -> void
|
|
18
20
|
def initialize(process_registry: ProcessRegistry.new)
|
|
@@ -22,6 +24,23 @@ module SolidObjects
|
|
|
22
24
|
@activations = {}
|
|
23
25
|
@stopped = false
|
|
24
26
|
@shutdown_requested = false
|
|
27
|
+
@polling_backoff = PollingBackoff.new(
|
|
28
|
+
minimum_interval: [
|
|
29
|
+
SolidObjects.configuration.polling_interval,
|
|
30
|
+
SolidObjects.configuration.lease_renewal_interval
|
|
31
|
+
].min,
|
|
32
|
+
maximum_interval: [
|
|
33
|
+
SolidObjects.configuration.idle_polling_interval,
|
|
34
|
+
SolidObjects.configuration.lease_renewal_interval
|
|
35
|
+
].min,
|
|
36
|
+
on_change: ->(transition) do
|
|
37
|
+
SolidObjects.instrument(
|
|
38
|
+
:"polling.interval_changed",
|
|
39
|
+
role: "actors",
|
|
40
|
+
**transition
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
)
|
|
25
44
|
end
|
|
26
45
|
|
|
27
46
|
# @rbs () -> Integer
|
|
@@ -70,11 +89,23 @@ module SolidObjects
|
|
|
70
89
|
|
|
71
90
|
# @rbs () -> void
|
|
72
91
|
def run
|
|
92
|
+
ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
|
|
93
|
+
|
|
73
94
|
until shutdown_requested?
|
|
95
|
+
wake_up = SolidObjects.wake_up
|
|
96
|
+
watch = wake_up.respond_to?(:watch) ? wake_up.watch : wake_up
|
|
74
97
|
processed = run_once
|
|
75
|
-
|
|
98
|
+
if processed.positive?
|
|
99
|
+
polling_backoff.reset(:work)
|
|
100
|
+
next
|
|
101
|
+
end
|
|
76
102
|
|
|
77
|
-
|
|
103
|
+
notified = watch.wait(timeout: current_polling_interval)
|
|
104
|
+
if notified == false
|
|
105
|
+
polling_backoff.record_idle
|
|
106
|
+
else
|
|
107
|
+
polling_backoff.reset(:wake_up)
|
|
108
|
+
end
|
|
78
109
|
end
|
|
79
110
|
ensure
|
|
80
111
|
stop
|
|
@@ -107,9 +138,14 @@ module SolidObjects
|
|
|
107
138
|
@shutdown_requested
|
|
108
139
|
end
|
|
109
140
|
|
|
141
|
+
# @rbs () -> Float
|
|
142
|
+
def current_polling_interval
|
|
143
|
+
polling_backoff.current_interval
|
|
144
|
+
end
|
|
145
|
+
|
|
110
146
|
private
|
|
111
147
|
|
|
112
|
-
attr_reader :process_registry, :activation_manager, :activations
|
|
148
|
+
attr_reader :process_registry, :activation_manager, :activations, :polling_backoff
|
|
113
149
|
|
|
114
150
|
# @rbs () -> Activation?
|
|
115
151
|
def cached_ready_activation
|
data/lib/solid_objects.rb
CHANGED
|
@@ -52,6 +52,7 @@ require "solid_objects/wake_up"
|
|
|
52
52
|
require "solid_objects/wake_up_adapters/postgresql"
|
|
53
53
|
require "solid_objects/wake_up_adapters/redis"
|
|
54
54
|
require "solid_objects/wake_up_adapters"
|
|
55
|
+
require "solid_objects/polling_backoff"
|
|
55
56
|
require "solid_objects/effect_registry"
|
|
56
57
|
require "solid_objects/commit_action_registry"
|
|
57
58
|
require "solid_objects/lease"
|
|
@@ -153,6 +154,7 @@ module SolidObjects
|
|
|
153
154
|
|
|
154
155
|
# @rbs () -> void
|
|
155
156
|
def reset!
|
|
157
|
+
ProcessRegistry.reset_polling_warning! if defined?(ProcessRegistry)
|
|
156
158
|
@configuration = Configuration.new
|
|
157
159
|
@registry = ActorRegistry.new
|
|
158
160
|
@client = nil
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class BroadcastExecutor
|
|
5
|
+
@polling_backoff: PollingBackoff
|
|
6
|
+
|
|
5
7
|
@shutdown_requested: bool
|
|
6
8
|
|
|
7
9
|
@stopped: bool
|
|
@@ -31,12 +33,17 @@ module SolidObjects
|
|
|
31
33
|
# @rbs () -> bool
|
|
32
34
|
def shutdown_requested?: () -> bool
|
|
33
35
|
|
|
36
|
+
# @rbs () -> Float
|
|
37
|
+
def current_polling_interval: () -> Float
|
|
38
|
+
|
|
34
39
|
private
|
|
35
40
|
|
|
36
41
|
attr_reader process_registry: untyped
|
|
37
42
|
|
|
38
43
|
attr_reader database_adapter: untyped
|
|
39
44
|
|
|
45
|
+
attr_reader polling_backoff: untyped
|
|
46
|
+
|
|
40
47
|
# @rbs () -> Broadcast?
|
|
41
48
|
def claim_next: () -> Broadcast?
|
|
42
49
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Configuration
|
|
5
|
+
@table_name_prefix: String
|
|
6
|
+
|
|
5
7
|
@process_alive_threshold: Float
|
|
6
8
|
|
|
7
9
|
@shutdown_timeout: Float
|
|
@@ -56,10 +58,10 @@ module SolidObjects
|
|
|
56
58
|
|
|
57
59
|
@authorize_administration: Proc
|
|
58
60
|
|
|
59
|
-
@table_name_prefix: String
|
|
60
|
-
|
|
61
61
|
@polling_interval: Float
|
|
62
62
|
|
|
63
|
+
@idle_polling_interval: Float
|
|
64
|
+
|
|
63
65
|
@sync_polling_interval: Float
|
|
64
66
|
|
|
65
67
|
@lease_duration: Float
|
|
@@ -94,6 +96,8 @@ module SolidObjects
|
|
|
94
96
|
|
|
95
97
|
attr_accessor polling_interval: untyped
|
|
96
98
|
|
|
99
|
+
attr_accessor idle_polling_interval: untyped
|
|
100
|
+
|
|
97
101
|
attr_accessor sync_polling_interval: untyped
|
|
98
102
|
|
|
99
103
|
attr_accessor lease_duration: untyped
|
|
@@ -23,6 +23,8 @@ module SolidObjects
|
|
|
23
23
|
class EffectExecutor
|
|
24
24
|
ACTOR_MESSAGE_EFFECT: ::String
|
|
25
25
|
|
|
26
|
+
@polling_backoff: PollingBackoff
|
|
27
|
+
|
|
26
28
|
@shutdown_requested: bool
|
|
27
29
|
|
|
28
30
|
@stopped: bool
|
|
@@ -52,12 +54,17 @@ module SolidObjects
|
|
|
52
54
|
# @rbs () -> bool
|
|
53
55
|
def shutdown_requested?: () -> bool
|
|
54
56
|
|
|
57
|
+
# @rbs () -> Float
|
|
58
|
+
def current_polling_interval: () -> Float
|
|
59
|
+
|
|
55
60
|
private
|
|
56
61
|
|
|
57
62
|
attr_reader process_registry: untyped
|
|
58
63
|
|
|
59
64
|
attr_reader database_adapter: untyped
|
|
60
65
|
|
|
66
|
+
attr_reader polling_backoff: untyped
|
|
67
|
+
|
|
61
68
|
# @rbs () -> Effect?
|
|
62
69
|
def claim_next: () -> Effect?
|
|
63
70
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/polling_backoff.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class PollingBackoff
|
|
5
|
+
@minimum_interval: Float
|
|
6
|
+
|
|
7
|
+
@maximum_interval: Float
|
|
8
|
+
|
|
9
|
+
@current_interval: Float
|
|
10
|
+
|
|
11
|
+
@on_change: Proc?
|
|
12
|
+
|
|
13
|
+
attr_reader current_interval: untyped
|
|
14
|
+
|
|
15
|
+
# @rbs (minimum_interval: Numeric, maximum_interval: Numeric, ?on_change: Proc?) -> void
|
|
16
|
+
def initialize: (minimum_interval: Numeric, maximum_interval: Numeric, ?on_change: Proc?) -> void
|
|
17
|
+
|
|
18
|
+
# @rbs () -> void
|
|
19
|
+
def record_idle: () -> void
|
|
20
|
+
|
|
21
|
+
# @rbs (:work | :wake_up) -> void
|
|
22
|
+
def reset: (:work | :wake_up) -> void
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# @rbs (Float, :idle | :work | :wake_up) -> void
|
|
27
|
+
def change: (Float, :idle | :work | :wake_up) -> void
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -8,6 +8,21 @@ module SolidObjects
|
|
|
8
8
|
# @rbs (Process, ?now: Time) -> bool
|
|
9
9
|
def self.deregister: (Process, ?now: Time) -> bool
|
|
10
10
|
|
|
11
|
+
# @rbs () -> void
|
|
12
|
+
def self.warn_if_polling_is_only_cross_process_wake_up: () -> void
|
|
13
|
+
|
|
14
|
+
# @rbs () -> void
|
|
15
|
+
def self.reset_polling_warning!: () -> void
|
|
16
|
+
|
|
17
|
+
# @rbs () -> bool
|
|
18
|
+
private def self.another_live_process?: () -> bool
|
|
19
|
+
|
|
20
|
+
# @rbs () -> bool
|
|
21
|
+
private def self.polling_warning_emitted?: () -> bool
|
|
22
|
+
|
|
23
|
+
# @rbs () -> Mutex
|
|
24
|
+
private def self.polling_warning_mutex: () -> Mutex
|
|
25
|
+
|
|
11
26
|
# @rbs (Process, Time) -> void
|
|
12
27
|
private def self.cleanup_process: (Process, Time) -> void
|
|
13
28
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class ReminderScheduler
|
|
5
|
+
@polling_backoff: PollingBackoff
|
|
6
|
+
|
|
5
7
|
@shutdown_requested: bool
|
|
6
8
|
|
|
7
9
|
@stopped: bool
|
|
@@ -31,12 +33,17 @@ module SolidObjects
|
|
|
31
33
|
# @rbs () -> bool
|
|
32
34
|
def shutdown_requested?: () -> bool
|
|
33
35
|
|
|
36
|
+
# @rbs () -> Float
|
|
37
|
+
def current_polling_interval: () -> Float
|
|
38
|
+
|
|
34
39
|
private
|
|
35
40
|
|
|
36
41
|
attr_reader process_registry: untyped
|
|
37
42
|
|
|
38
43
|
attr_reader database_adapter: untyped
|
|
39
44
|
|
|
45
|
+
attr_reader polling_backoff: untyped
|
|
46
|
+
|
|
40
47
|
# @rbs (now: Time?) -> Reminder?
|
|
41
48
|
def claim_next: (now: Time?) -> Reminder?
|
|
42
49
|
|
|
@@ -2,18 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class WakeUp
|
|
5
|
+
class Watch
|
|
6
|
+
@wake_up: WakeUp
|
|
7
|
+
|
|
8
|
+
@generation: Integer
|
|
9
|
+
|
|
10
|
+
# @rbs (WakeUp, Integer) -> void
|
|
11
|
+
def initialize: (WakeUp, Integer) -> void
|
|
12
|
+
|
|
13
|
+
# @rbs (timeout: Numeric) -> bool
|
|
14
|
+
def wait: (timeout: Numeric) -> bool
|
|
15
|
+
end
|
|
16
|
+
|
|
5
17
|
@mutex: Mutex
|
|
6
18
|
|
|
7
19
|
@condition: Thread::ConditionVariable
|
|
8
20
|
|
|
21
|
+
@generation: Integer
|
|
22
|
+
|
|
9
23
|
# @rbs () -> void
|
|
10
24
|
def initialize: () -> void
|
|
11
25
|
|
|
12
26
|
# @rbs () -> void
|
|
13
27
|
def signal: () -> void
|
|
14
28
|
|
|
15
|
-
# @rbs (
|
|
16
|
-
def
|
|
29
|
+
# @rbs () -> Watch
|
|
30
|
+
def watch: () -> Watch
|
|
31
|
+
|
|
32
|
+
# @rbs (timeout: Numeric, ?generation: Integer?) -> bool
|
|
33
|
+
def wait: (timeout: Numeric, ?generation: Integer?) -> bool
|
|
17
34
|
|
|
18
35
|
private
|
|
19
36
|
|
|
@@ -31,6 +31,9 @@ module SolidObjects
|
|
|
31
31
|
# @rbs (timeout: Numeric) -> bool
|
|
32
32
|
def wait: (timeout: Numeric) -> bool
|
|
33
33
|
|
|
34
|
+
# @rbs () -> self
|
|
35
|
+
def watch: () -> self
|
|
36
|
+
|
|
34
37
|
# Starts listening before a caller blocks, so a notification sent between
|
|
35
38
|
# startup and the first wait is not missed.
|
|
36
39
|
# @rbs () -> bool
|
|
@@ -10,6 +10,18 @@ module SolidObjects
|
|
|
10
10
|
# polling interval remains the upper bound, so a missed or failed
|
|
11
11
|
# notification costs latency rather than correctness.
|
|
12
12
|
class Redis
|
|
13
|
+
class Watch
|
|
14
|
+
@adapter: Redis
|
|
15
|
+
|
|
16
|
+
@generation: Integer
|
|
17
|
+
|
|
18
|
+
# @rbs (Redis, Integer) -> void
|
|
19
|
+
def initialize: (Redis, Integer) -> void
|
|
20
|
+
|
|
21
|
+
# @rbs (timeout: Numeric) -> bool
|
|
22
|
+
def wait: (timeout: Numeric) -> bool
|
|
23
|
+
end
|
|
24
|
+
|
|
13
25
|
CHANNEL: ::String
|
|
14
26
|
|
|
15
27
|
FAILED_WAIT_INTERVAL: ::Float
|
|
@@ -43,8 +55,11 @@ module SolidObjects
|
|
|
43
55
|
# The counter is snapshotted before subscribing, and re-checked before
|
|
44
56
|
# blocking, so a signal delivered while this caller was still getting
|
|
45
57
|
# ready is observed rather than absorbed into the new baseline.
|
|
46
|
-
# @rbs (timeout: Numeric) -> bool
|
|
47
|
-
def wait: (timeout: Numeric) -> bool
|
|
58
|
+
# @rbs (timeout: Numeric, ?generation: Integer?) -> bool
|
|
59
|
+
def wait: (timeout: Numeric, ?generation: Integer?) -> bool
|
|
60
|
+
|
|
61
|
+
# @rbs () -> Watch
|
|
62
|
+
def watch: () -> Watch
|
|
48
63
|
|
|
49
64
|
# Redis delivers to a subscribed connection only, and a subscribed
|
|
50
65
|
# connection cannot serve other callers, so one background subscription
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/action.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# One request. A route handler runs inside an instance of this class, so a
|
|
6
|
+
# handler and the template it renders share the same helpers and the same
|
|
7
|
+
# request.
|
|
8
|
+
class Action
|
|
9
|
+
include Helpers
|
|
10
|
+
|
|
11
|
+
@response_status: Integer
|
|
12
|
+
|
|
13
|
+
@layout_rendered: bool
|
|
14
|
+
|
|
15
|
+
@request: untyped
|
|
16
|
+
|
|
17
|
+
@captures: Hash[Symbol, String?]
|
|
18
|
+
|
|
19
|
+
@route: Route
|
|
20
|
+
|
|
21
|
+
@env: Hash[String, untyped]
|
|
22
|
+
|
|
23
|
+
attr_reader env: untyped
|
|
24
|
+
|
|
25
|
+
attr_reader route: untyped
|
|
26
|
+
|
|
27
|
+
attr_reader response_status: untyped
|
|
28
|
+
|
|
29
|
+
# @rbs (env: Hash[String, untyped], route: Route) -> void
|
|
30
|
+
def initialize: (env: Hash[String, untyped], route: Route) -> void
|
|
31
|
+
|
|
32
|
+
# Sets the status a rendered page is served with. `halt` and `redirect`
|
|
33
|
+
# stop the handler, so a page that must render its own body and still
|
|
34
|
+
# report a failure needs this instead.
|
|
35
|
+
# @rbs (Integer) -> void
|
|
36
|
+
def status: (Integer) -> void
|
|
37
|
+
|
|
38
|
+
# @rbs () -> untyped
|
|
39
|
+
def request: () -> untyped
|
|
40
|
+
|
|
41
|
+
# @rbs () -> Hash[untyped, untyped]?
|
|
42
|
+
def session: () -> Hash[untyped, untyped]?
|
|
43
|
+
|
|
44
|
+
# @rbs (Symbol) -> String?
|
|
45
|
+
def route_params: (Symbol) -> String?
|
|
46
|
+
|
|
47
|
+
# @rbs (String) -> untyped
|
|
48
|
+
def url_params: (String) -> untyped
|
|
49
|
+
|
|
50
|
+
# @rbs () -> untyped
|
|
51
|
+
def call: () -> untyped
|
|
52
|
+
|
|
53
|
+
# The layout is rendered once per request. The flag is raised before the
|
|
54
|
+
# page body runs so a partial the body renders returns its own fragment
|
|
55
|
+
# rather than a second whole page. The layout reads the page it wraps
|
|
56
|
+
# from `locals.fetch(:content)`.
|
|
57
|
+
# @rbs (Symbol, ?Hash[Symbol, untyped]) -> String
|
|
58
|
+
def erb: (Symbol, ?Hash[Symbol, untyped]) -> String
|
|
59
|
+
|
|
60
|
+
# @rbs (Integer, ?String) -> void
|
|
61
|
+
def halt: (Integer, ?String) -> void
|
|
62
|
+
|
|
63
|
+
# @rbs (String) -> void
|
|
64
|
+
def redirect: (String) -> void
|
|
65
|
+
|
|
66
|
+
# @rbs (untyped) -> void
|
|
67
|
+
def json: (untyped) -> void
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# `locals` is a local variable of this method, so a template reads it by
|
|
72
|
+
# name, and every helper is reachable because the template runs against
|
|
73
|
+
# this object.
|
|
74
|
+
# @rbs (untyped, Hash[Symbol, untyped]) -> String
|
|
75
|
+
def evaluate: (untyped, Hash[Symbol, untyped]) -> String
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/application.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# The pages. Each route states the administration policy it needs, and
|
|
6
|
+
# `call` asks that policy before the handler runs, so a page cannot read
|
|
7
|
+
# the actor tables on behalf of an unauthorized request.
|
|
8
|
+
class Application
|
|
9
|
+
extend Router
|
|
10
|
+
|
|
11
|
+
SCRIPT_PLACEHOLDER: ::String
|
|
12
|
+
|
|
13
|
+
CONTENT_SECURITY_POLICY: untyped
|
|
14
|
+
|
|
15
|
+
MAILBOX_MEMBERSHIPS: untyped
|
|
16
|
+
|
|
17
|
+
RECENT_LIMIT: ::Integer
|
|
18
|
+
|
|
19
|
+
CHART_TYPE_LIMIT: ::Integer
|
|
20
|
+
|
|
21
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
22
|
+
def call: (Hash[String, untyped]) -> Array[untyped]
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# @rbs (Action, untyped) -> Array[untyped]
|
|
27
|
+
def respond: (Action, untyped) -> Array[untyped]
|
|
28
|
+
|
|
29
|
+
# @rbs (Action) -> bool
|
|
30
|
+
def authorized?: (Action) -> bool
|
|
31
|
+
|
|
32
|
+
# @rbs (Hash[String, untyped]) -> Hash[String, String]
|
|
33
|
+
def page_headers: (Hash[String, untyped]) -> Hash[String, String]
|
|
34
|
+
|
|
35
|
+
# The chart host is named only when one is configured, so a deployment
|
|
36
|
+
# that vendors the library or turns charts off never advertises a third
|
|
37
|
+
# party origin it does not use.
|
|
38
|
+
# @rbs (Hash[String, untyped]) -> String
|
|
39
|
+
def content_security_policy: (Hash[String, untyped]) -> String
|
|
40
|
+
|
|
41
|
+
# The cascade header lets a host application serve its own 404 for a path
|
|
42
|
+
# below the mount that the dashboard does not define.
|
|
43
|
+
# @rbs () -> Array[untyped]
|
|
44
|
+
def not_found: () -> Array[untyped]
|
|
45
|
+
|
|
46
|
+
# @rbs () -> Array[untyped]
|
|
47
|
+
def forbidden: () -> Array[untyped]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|