monkrb 0.17.0 → 0.19.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 +133 -0
- data/README.md +6 -5
- data/exe/monk +45 -7
- data/lib/monk/jobs/adapters/pg.rb +272 -0
- data/lib/monk/jobs/args.rb +43 -0
- data/lib/monk/jobs/claim.rb +7 -0
- data/lib/monk/jobs/errors.rb +29 -0
- data/lib/monk/jobs/job.rb +117 -0
- data/lib/monk/jobs/runtime.rb +239 -0
- data/lib/monk/jobs/worker.rb +94 -0
- data/lib/monk/jobs.rb +229 -0
- data/lib/monk/live/client/monk_live.js +2 -2
- data/lib/monk/live/client/protocol.js +5 -0
- data/lib/monk/live.rb +10 -0
- data/lib/monk/mail/errors.rb +7 -0
- data/lib/monk/mail/later.rb +53 -0
- data/lib/monk/persistence/pg.rb +21 -1
- data/lib/monk/scaffold.rb +369 -70
- data/lib/monk/templates/auth/app/mailers/app_mailer.rb +22 -0
- data/lib/monk/templates/auth/config/auth.rb +7 -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 +6 -0
- 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/app/jobs/hello_job.rb +9 -0
- data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +28 -0
- data/lib/monk/templates/jobs/bin/jobs +18 -0
- data/lib/monk/templates/jobs/config/jobs.rb +15 -0
- data/lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.down.sql +3 -0
- data/lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.up.sql +48 -0
- 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 +14 -2
- 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/Dockerfile +3 -1
- data/lib/monk/templates/postgres/bin/console +4 -2
- data/lib/monk/version.rb +1 -1
- data/lib/monk/websocket/errors.rb +11 -0
- data/lib/monk/websocket/listeners.rb +23 -0
- data/lib/monk/websocket/pg_fanout.rb +55 -20
- data/lib/monk/websocket/redis_fanout.rb +57 -14
- data/lib/monk/websocket/registry.rb +6 -0
- data/lib/monk/websocket/server.rb +77 -17
- metadata +32 -7
- /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/live/{views → app/views}/live/_hits.erb +0 -0
|
@@ -8,6 +8,11 @@ export const MAX_DELAY = 30000;
|
|
|
8
8
|
// Reconnect backoff: double, capped.
|
|
9
9
|
export const nextDelay = (delay) => Math.min(delay * 2, MAX_DELAY);
|
|
10
10
|
|
|
11
|
+
// The wait actually used: 50%..150% of the backoff delay. When the WS process
|
|
12
|
+
// restarts, every open page loses its socket at once; without jitter they all
|
|
13
|
+
// reconnect, and all refetch their page (resync), at the same instant.
|
|
14
|
+
export const jittered = (delay, random = Math.random) => Math.floor(delay * (0.5 + random()));
|
|
15
|
+
|
|
11
16
|
const MODES = ["morph", "replace", "append", "prepend", "remove"];
|
|
12
17
|
|
|
13
18
|
// data-live-topic values ("a:1 b:2") -> a sorted, de-duplicated topic list.
|
data/lib/monk/live.rb
CHANGED
|
@@ -62,6 +62,16 @@ module Monk
|
|
|
62
62
|
@max_topics = max_topics
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Boot-time, main Ractor, in the WS process only (bin/websocket_server):
|
|
66
|
+
# starts the configured registry relaying other processes' updates
|
|
67
|
+
# to this process's sockets. A publish-only process (bin/server,
|
|
68
|
+
# bin/jobs) never calls it, so it opens no subscriber connection.
|
|
69
|
+
def listen!
|
|
70
|
+
registry || raise(NotConfiguredError,
|
|
71
|
+
"Monk::Live isn't configured -- call Monk::Live.configure(registry: ...) before listen!",)
|
|
72
|
+
registry.listen!
|
|
73
|
+
end
|
|
74
|
+
|
|
65
75
|
# Boot-time, main Ractor. See Monk::Live::Policy for the semantics:
|
|
66
76
|
# deny by default, first matching rule wins, anonymous subjects need
|
|
67
77
|
# `anonymous: true`. The block must be Ractor-shareable.
|
data/lib/monk/mail/errors.rb
CHANGED
|
@@ -24,5 +24,12 @@ module Monk
|
|
|
24
24
|
# refusing it. The transport's own exception is the #cause.
|
|
25
25
|
class DeliveryError < StandardError
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# A DeliveryError the server made final: an SMTP 5xx (no such mailbox,
|
|
29
|
+
# relaying refused) or refused credentials. Raised by
|
|
30
|
+
# Monk::Mail::DeliveryJob (require "monk/mail/later") so the job isn't
|
|
31
|
+
# retried; still a DeliveryError, so a rescue of that catches it too.
|
|
32
|
+
class PermanentDeliveryError < DeliveryError
|
|
33
|
+
end
|
|
27
34
|
end
|
|
28
35
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require_relative "../mail"
|
|
2
|
+
require_relative "../jobs"
|
|
3
|
+
|
|
4
|
+
module Monk
|
|
5
|
+
module Mail
|
|
6
|
+
# Sends one message from a job process (docs/adr/0014-mail-from-jobs-and-login-links-created-in-the-job.md).
|
|
7
|
+
# Enqueued by Monk::Mail.deliver_later, never directly: its argument
|
|
8
|
+
# is the message's fields, already checked and with from: resolved.
|
|
9
|
+
class DeliveryJob < Monk::Job
|
|
10
|
+
queue "mailers"
|
|
11
|
+
never_retry PermanentDeliveryError
|
|
12
|
+
|
|
13
|
+
# The SMTP failures that are final, by class name so this file never
|
|
14
|
+
# loads net/smtp itself (the app's Gemfile decides that, ADR 0012):
|
|
15
|
+
# a 5xx reply, a malformed command, refused credentials. Everything
|
|
16
|
+
# else -- a 4xx "try later", a refused connection, TLS, a timeout --
|
|
17
|
+
# is worth retrying.
|
|
18
|
+
PERMANENT_SMTP_ERRORS = %w[Net::SMTPFatalError Net::SMTPSyntaxError Net::SMTPAuthenticationError].freeze
|
|
19
|
+
|
|
20
|
+
def self.perform(fields)
|
|
21
|
+
Monk::Mail.deliver(**fields.transform_keys(&:to_sym))
|
|
22
|
+
rescue DeliveryError => e
|
|
23
|
+
raise unless PERMANENT_SMTP_ERRORS.include?(e.cause&.class&.name)
|
|
24
|
+
|
|
25
|
+
raise PermanentDeliveryError, e.message
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# deliver, later: the same arguments, plus enqueue's wait:, at: and
|
|
31
|
+
# conn: (the last one enqueues inside the caller's transaction, so the
|
|
32
|
+
# email commits or rolls back with the app's own writes). Returns the
|
|
33
|
+
# job's id. The message is built -- and so checked -- here, before
|
|
34
|
+
# anything is enqueued: bad input raises InvalidMessageError in the
|
|
35
|
+
# caller, exactly as deliver would, rather than failing later in the
|
|
36
|
+
# job process. Render HTML before calling (html: Monk::Mail.render(...)):
|
|
37
|
+
# the job stores the finished message and only sends it.
|
|
38
|
+
#
|
|
39
|
+
# Monk::Mail.deliver_later(to: order[:email], subject: "Your receipt", text: receipt_text)
|
|
40
|
+
def deliver_later(to:, subject:, text: nil, html: nil, reply_to: nil, from: nil, wait: nil, at: nil, conn: nil)
|
|
41
|
+
raise NotConfiguredError, "call Monk::Mail.configure(url:, from:) before Monk::Mail.deliver_later" unless config
|
|
42
|
+
|
|
43
|
+
from ||= config[:from]
|
|
44
|
+
if from.nil?
|
|
45
|
+
raise InvalidMessageError, "no from: given, and Monk::Mail.configure has no default from: (MAIL_FROM)"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
message = Message.new(from: from, to: to, subject: subject, text: text, html: html, reply_to: reply_to)
|
|
49
|
+
DeliveryJob.enqueue(message.to_h.transform_keys(&:to_s), wait: wait, at: at, conn: conn)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/monk/persistence/pg.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "json"
|
|
1
2
|
require "pg"
|
|
2
3
|
|
|
3
4
|
require_relative "../persistence"
|
|
@@ -16,12 +17,31 @@ module Monk
|
|
|
16
17
|
module Pg
|
|
17
18
|
extend Monk::Persistence::Registry
|
|
18
19
|
|
|
20
|
+
# json/jsonb columns, decoded with plain JSON.parse. pg's own
|
|
21
|
+
# PG::TextDecoder::JSON (1.6.3) calls JSON.parse(string, quirks_mode:
|
|
22
|
+
# true), and json 3 removed that keyword, so with pg's default every
|
|
23
|
+
# json/jsonb read raised ArgumentError. json 3 parses a bare scalar
|
|
24
|
+
# ("7", "\"text\"") without it.
|
|
25
|
+
class JsonDecoder < PG::SimpleDecoder
|
|
26
|
+
def decode(string, _tuple = nil, _field = nil)
|
|
27
|
+
JSON.parse(string)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# pg's default result types, with JsonDecoder for json and jsonb.
|
|
32
|
+
# Built once and made shareable, so every Ractor's #connect can use
|
|
33
|
+
# it rather than building its own.
|
|
34
|
+
RESULT_TYPES = Ractor.make_shareable(
|
|
35
|
+
PG::BasicTypeRegistry.new.register_default_types
|
|
36
|
+
.tap { |types| %w[json jsonb].each { |name| types.register_type(0, name, nil, JsonDecoder) } },
|
|
37
|
+
)
|
|
38
|
+
|
|
19
39
|
class << self
|
|
20
40
|
private
|
|
21
41
|
|
|
22
42
|
def connect(**opts)
|
|
23
43
|
conn = PG.connect(**opts)
|
|
24
|
-
conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)
|
|
44
|
+
conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn, registry: RESULT_TYPES)
|
|
25
45
|
conn
|
|
26
46
|
end
|
|
27
47
|
|