monkrb 0.18.0 → 0.20.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 +166 -0
- data/README.md +2 -2
- data/lib/monk/base.rb +20 -0
- data/lib/monk/jobs/adapters/pg.rb +3 -1
- data/lib/monk/live/client/monk_live.js +2 -2
- data/lib/monk/live/client/protocol.js +5 -0
- data/lib/monk/live/policy.rb +39 -4
- data/lib/monk/live/session.rb +7 -0
- data/lib/monk/live.rb +15 -3
- data/lib/monk/persistence/errors.rb +32 -0
- data/lib/monk/persistence/pg.rb +92 -1
- data/lib/monk/persistence/pool.rb +513 -0
- data/lib/monk/persistence.rb +196 -7
- data/lib/monk/scaffold.rb +155 -78
- data/lib/monk/templates/auth/app/mailers/app_mailer.rb +22 -0
- data/lib/monk/templates/auth/config/auth.rb +13 -20
- data/lib/monk/templates/base/app/app.rb +9 -0
- data/lib/monk/templates/base/app/broadcasts/.keep +0 -0
- data/lib/monk/templates/base/app/helpers/.keep +0 -0
- data/lib/monk/templates/base/app/jobs/.keep +0 -0
- data/lib/monk/templates/base/app/mailers/.keep +0 -0
- data/lib/monk/templates/base/app/models/.keep +0 -0
- data/lib/monk/templates/base/app/presenters/.keep +0 -0
- data/lib/monk/templates/base/bin/websocket_server +17 -2
- data/lib/monk/templates/base/config/load.rb +18 -0
- data/lib/monk/templates/base/config.ru +3 -12
- data/lib/monk/templates/base/public/js/app.js +1 -1
- data/lib/monk/templates/jobs/{jobs → app/jobs}/send_login_link.rb +2 -1
- data/lib/monk/templates/jobs/bin/jobs +5 -13
- data/lib/monk/templates/jobs/config/jobs.rb +2 -4
- data/lib/monk/templates/live/{config.ru → app/app.rb} +1 -6
- data/lib/monk/templates/live/{views → app/views}/index.erb +2 -2
- data/lib/monk/templates/live/bin/websocket_server +23 -3
- data/lib/monk/templates/live/config/live.rb +1 -1
- data/lib/monk/templates/live/config/live_pg.rb +1 -1
- data/lib/monk/templates/mail/config/mail.rb +2 -2
- data/lib/monk/templates/postgres/bin/console +4 -2
- data/lib/monk/templates/postgres/db/migrate/.keep +0 -0
- data/lib/monk/version.rb +1 -1
- data/lib/monk/websocket/connection.rb +25 -6
- data/lib/monk/websocket/errors.rb +11 -0
- data/lib/monk/websocket/listeners.rb +47 -0
- data/lib/monk/websocket/pg_fanout.rb +189 -31
- data/lib/monk/websocket/redis_fanout.rb +125 -33
- data/lib/monk/websocket/registry.rb +30 -0
- data/lib/monk/websocket/server.rb +128 -29
- data/lib/monk/websocket.rb +2 -0
- data/lib/monk.rb +10 -0
- metadata +21 -9
- /data/lib/monk/templates/auth/{views → app/views}/mail/magic_link.erb +0 -0
- /data/lib/monk/templates/base/{views → app/views}/index.erb +0 -0
- /data/lib/monk/templates/base/{views → app/views}/layouts/app.erb +0 -0
- /data/lib/monk/templates/jobs/{jobs → app/jobs}/hello_job.rb +0 -0
- /data/lib/monk/templates/live/{views → app/views}/live/_hits.erb +0 -0
data/lib/monk/persistence.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "freeze_hooks"
|
|
2
|
+
require_relative "persistence/pool"
|
|
2
3
|
|
|
3
4
|
module Monk
|
|
4
5
|
# Backend-agnostic persistence. Concrete backends (e.g.
|
|
@@ -8,6 +9,16 @@ module Monk
|
|
|
8
9
|
# can seal every backend actually in use without needing to know their
|
|
9
10
|
# names.
|
|
10
11
|
module Persistence
|
|
12
|
+
# Closes every connection this Ractor opened, in every backend. A
|
|
13
|
+
# Ractor that ends leaves its connections to the garbage collector,
|
|
14
|
+
# which may not run for a long time; one that opened any calls this
|
|
15
|
+
# as it finishes (Monk::WebSocket::Server.serve, for a socket's).
|
|
16
|
+
def self.disconnect_all
|
|
17
|
+
# A copy: each backend's disconnect_all removes its own key.
|
|
18
|
+
backends = Ractor.current[:monk_persistence]&.keys || []
|
|
19
|
+
backends.each(&:disconnect_all)
|
|
20
|
+
end
|
|
21
|
+
|
|
11
22
|
# Shared by every backend module: per-Ractor connection lifecycle, a
|
|
12
23
|
# registry of named configs, and boot-time shareability sealing. A
|
|
13
24
|
# backend `extend`s this and implements #connect(**opts) /
|
|
@@ -27,6 +38,14 @@ module Monk
|
|
|
27
38
|
configs[name] = opts
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
# The options a database was registered with, for something that
|
|
42
|
+
# opens its own connection to it (PgFanout's LISTEN).
|
|
43
|
+
def connection_options(name)
|
|
44
|
+
configs.fetch(name) do
|
|
45
|
+
raise Monk::UnknownPersistenceError, "no database registered as #{name.inspect}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
30
49
|
# Registered connection names (e.g. :primary) -- Monk.boot's log
|
|
31
50
|
# line reads this to report which backends an app actually uses.
|
|
32
51
|
def names
|
|
@@ -37,6 +56,76 @@ module Monk
|
|
|
37
56
|
entry(name).conn
|
|
38
57
|
end
|
|
39
58
|
|
|
59
|
+
# With options, declares a named pool (docs/history/plan-pg-pool.md):
|
|
60
|
+
# `size` worker Ractors, each with its own connection to the
|
|
61
|
+
# registered database `db`, that run calls sent from any other
|
|
62
|
+
# Ractor. Nothing connects until #start_pools! runs, in the process
|
|
63
|
+
# that uses it. Every process loads the declarations
|
|
64
|
+
# (config/persistence.rb), next to #register.
|
|
65
|
+
#
|
|
66
|
+
# Without options, returns the started pool, from any Ractor.
|
|
67
|
+
# Raises if it was never declared, or declared but not started in
|
|
68
|
+
# this process.
|
|
69
|
+
def pool(name, **options)
|
|
70
|
+
return declare_pool(name, **options) unless options.empty?
|
|
71
|
+
|
|
72
|
+
handle = @running && @running[name]
|
|
73
|
+
return handle if handle
|
|
74
|
+
|
|
75
|
+
pool_config(name)
|
|
76
|
+
raise Monk::PoolNotStartedError,
|
|
77
|
+
"pool #{name.inspect} is declared but not started in this process -- call " \
|
|
78
|
+
"#{self}.start_pools!(#{name.inspect}) at boot in the process that uses it"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Starts declared pools in this process: each pool's dispatcher and
|
|
82
|
+
# `size` workers, each worker with its own connection. Returns once
|
|
83
|
+
# every worker has connected; raises Monk::PoolStartError if any
|
|
84
|
+
# can't, and starts nothing for that pool. Main Ractor only, at boot,
|
|
85
|
+
# after every register and pool declaration: it freezes the
|
|
86
|
+
# registry, since the workers read it. A pool already started is
|
|
87
|
+
# left alone.
|
|
88
|
+
def start_pools!(*names)
|
|
89
|
+
unless Ractor.current == Ractor.main
|
|
90
|
+
raise ArgumentError, "#{self}.start_pools! runs in the main Ractor, at boot"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
freeze_registry!
|
|
94
|
+
names.each { |name| start_pool(name) unless @running&.key?(name) }
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Hook: makes a copy of an exception fit to leave a pool's worker for
|
|
99
|
+
# another Ractor, by dropping what can't be copied. A backend whose
|
|
100
|
+
# errors carry nothing like that keeps this default.
|
|
101
|
+
def make_portable(_error) = nil
|
|
102
|
+
|
|
103
|
+
# This Ractor's own connections to this backend, closed and
|
|
104
|
+
# forgotten. A pool worker runs it as it stops; the next checkout in
|
|
105
|
+
# this Ractor reconnects.
|
|
106
|
+
def disconnect_all
|
|
107
|
+
Ractor.current[:monk_persistence]&.delete(self)&.each_value do |e|
|
|
108
|
+
disconnect(e.conn)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The pools started in this process, name => size (Monk.boot's line).
|
|
115
|
+
def running_pools
|
|
116
|
+
(@running || {}).transform_values { |handle| handle.config.size }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A declared pool's options (a frozen Pool::Config).
|
|
120
|
+
def pool_config(name)
|
|
121
|
+
pools.fetch(name) do
|
|
122
|
+
raise Monk::UnknownPoolError,
|
|
123
|
+
"pool #{name.inspect} was never declared -- declare it once at boot with " \
|
|
124
|
+
"#{self}.pool(#{name.inspect}, size: #{Pool::DEFAULTS[:size]}); " \
|
|
125
|
+
"pool(#{name.inspect}) with no options only looks one up"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
40
129
|
def checkout(name, timeout: DEFAULT_CHECKOUT_TIMEOUT)
|
|
41
130
|
e = entry(name)
|
|
42
131
|
token = e.slot.pop(timeout: timeout)
|
|
@@ -46,9 +135,18 @@ module Monk
|
|
|
46
135
|
"(checkout held longer than #{timeout}s)"
|
|
47
136
|
end
|
|
48
137
|
|
|
138
|
+
# Checked here, before the block, and never by re-running it: Monk
|
|
139
|
+
# can't tell whether a failed block's writes reached the server,
|
|
140
|
+
# so a drop in the middle of a block fails that block, and the
|
|
141
|
+
# next checkout finds the connection dead and revives it. A
|
|
142
|
+
# revive that can't reach the server raises from here.
|
|
143
|
+
revive(e.conn) unless alive?(e.conn)
|
|
49
144
|
yield e.conn
|
|
50
145
|
ensure
|
|
51
|
-
|
|
146
|
+
if token
|
|
147
|
+
release(e.conn)
|
|
148
|
+
e.slot << true
|
|
149
|
+
end
|
|
52
150
|
end
|
|
53
151
|
|
|
54
152
|
# Called from Base#freeze! (Seam B), via Monk.freeze_hooks.
|
|
@@ -61,17 +159,16 @@ module Monk
|
|
|
61
159
|
# the value (not the module) fixes it, the same way it did there.
|
|
62
160
|
def freeze_registry!
|
|
63
161
|
@configs = Ractor.make_shareable(configs)
|
|
162
|
+
@pools = Ractor.make_shareable(pools)
|
|
64
163
|
end
|
|
65
164
|
|
|
66
165
|
# Test-only: drops all registered configs and this Ractor's cached
|
|
67
166
|
# connections. Not part of the app-facing API.
|
|
68
167
|
def reset!
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
rescue StandardError
|
|
72
|
-
end
|
|
73
|
-
Ractor.current[:monk_persistence] = {}
|
|
168
|
+
stop_pools
|
|
169
|
+
disconnect_all
|
|
74
170
|
@configs = {}
|
|
171
|
+
@pools = {}
|
|
75
172
|
end
|
|
76
173
|
|
|
77
174
|
private
|
|
@@ -80,8 +177,87 @@ module Monk
|
|
|
80
177
|
@configs ||= {}
|
|
81
178
|
end
|
|
82
179
|
|
|
180
|
+
def pools
|
|
181
|
+
@pools ||= {}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The started pools' dispatchers and inboxes, which only this (the
|
|
185
|
+
# main) Ractor uses: to stop them. Kept apart from @running, the
|
|
186
|
+
# frozen handles every Ractor reads.
|
|
187
|
+
def pool_ractors
|
|
188
|
+
@pool_ractors ||= {}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def start_pool(name)
|
|
192
|
+
ready = Ractor::Port.new
|
|
193
|
+
dispatcher = Ractor.new(self, pool_config(name), ready, name: "monk-pool-#{name}") do |backend, config, r|
|
|
194
|
+
Monk::Persistence::Pool::Dispatcher.run(backend, config, r)
|
|
195
|
+
end
|
|
196
|
+
status, value = ready.receive
|
|
197
|
+
unless status == :ok
|
|
198
|
+
dispatcher.join
|
|
199
|
+
raise Monk::PoolStartError, "pool #{name.inspect} couldn't connect its workers: #{value}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
pool_ractors[name] = { dispatcher: dispatcher, inbox: value }
|
|
203
|
+
handle = Pool::Handle.new(config: pool_config(name), inbox: value)
|
|
204
|
+
@running = Ractor.make_shareable((@running || {}).merge(name => handle))
|
|
205
|
+
ensure
|
|
206
|
+
ready&.close
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Test-only path (reset!). The dispatcher stops its workers, each
|
|
210
|
+
# finishing the call in hand, and answers every caller still waiting.
|
|
211
|
+
def stop_pools
|
|
212
|
+
pool_ractors.each_key { |name| stop_pool(name) }
|
|
213
|
+
@running = nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def stop_pool(name)
|
|
217
|
+
stopping = pool_ractors.delete(name)
|
|
218
|
+
return unless stopping
|
|
219
|
+
|
|
220
|
+
send_stop(stopping[:inbox], [:stop])
|
|
221
|
+
stopping[:dispatcher].join
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def send_stop(port, message)
|
|
225
|
+
port.send(message)
|
|
226
|
+
rescue Ractor::ClosedError
|
|
227
|
+
nil # already ended
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def declare_pool(name, **options)
|
|
231
|
+
unknown = options.keys - Pool::DEFAULTS.keys
|
|
232
|
+
raise ArgumentError, "unknown pool option(s) #{unknown.join(", ")} for #{name.inspect}" if unknown.any?
|
|
233
|
+
raise ArgumentError, "pool #{name.inspect} is already declared" if pools.key?(name)
|
|
234
|
+
|
|
235
|
+
config = Pool::Config.new(name: name, **Pool::DEFAULTS, **options)
|
|
236
|
+
validate_pool!(config)
|
|
237
|
+
pools[name] = Ractor.make_shareable(config)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def validate_pool!(config)
|
|
241
|
+
unless configs.key?(config.db)
|
|
242
|
+
raise Monk::UnknownPersistenceError,
|
|
243
|
+
"pool #{config.name.inspect} uses db: #{config.db.inspect}, but no database is registered as " \
|
|
244
|
+
"#{config.db.inspect} -- call #{self}.register(#{config.db.inspect}, ...) before declaring the pool"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
{ size: Integer, queue: Integer, timeout: Numeric }.each do |option, type|
|
|
248
|
+
value = config.public_send(option)
|
|
249
|
+
next if value.is_a?(type) && value.positive?
|
|
250
|
+
|
|
251
|
+
raise ArgumentError,
|
|
252
|
+
"pool #{config.name.inspect}: #{option}: must be a positive #{type}, got #{value.inspect}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# This Ractor's connections, by backend, then by name: so
|
|
257
|
+
# Persistence.disconnect_all can find every backend this Ractor
|
|
258
|
+
# used, from any Ractor.
|
|
83
259
|
def ractor_local
|
|
84
|
-
Ractor.current[:monk_persistence] ||= {}
|
|
260
|
+
(Ractor.current[:monk_persistence] ||= {})[self] ||= {}
|
|
85
261
|
end
|
|
86
262
|
|
|
87
263
|
def entry(name)
|
|
@@ -108,6 +284,19 @@ module Monk
|
|
|
108
284
|
def disconnect(conn)
|
|
109
285
|
raise NotImplementedError, "#{self} must implement #disconnect(conn)"
|
|
110
286
|
end
|
|
287
|
+
|
|
288
|
+
# Optional hooks around #checkout's block. A backend that can't tell
|
|
289
|
+
# a dead connection from a live one keeps these defaults.
|
|
290
|
+
|
|
291
|
+
# Whether conn can take a query. Cheap: runs before every checkout.
|
|
292
|
+
def alive?(_conn) = true
|
|
293
|
+
|
|
294
|
+
# Reconnects a connection #alive? said was dead; raises if it can't.
|
|
295
|
+
def revive(_conn) = nil
|
|
296
|
+
|
|
297
|
+
# Runs after every checkout's block, however it ended. Must not
|
|
298
|
+
# raise: it runs in an ensure, and would hide the block's exception.
|
|
299
|
+
def release(_conn) = nil
|
|
111
300
|
end
|
|
112
301
|
end
|
|
113
302
|
end
|