bidi2pdf 0.1.17 → 0.1.19
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 +32 -2
- data/README.md +111 -0
- data/lib/bidi2pdf/bidi/commands/browsing_context_get_tree.rb +16 -0
- data/lib/bidi2pdf/bidi/commands/cdp_send_command.rb +25 -0
- data/lib/bidi2pdf/bidi/commands.rb +2 -0
- data/lib/bidi2pdf/bidi/session.rb +53 -24
- data/lib/bidi2pdf/chrome_sweeper/inspector.rb +117 -0
- data/lib/bidi2pdf/chrome_sweeper/settings.rb +80 -0
- data/lib/bidi2pdf/chrome_sweeper.rb +398 -0
- data/lib/bidi2pdf/chromedriver_api.rb +72 -0
- data/lib/bidi2pdf/cli/session_commands.rb +82 -0
- data/lib/bidi2pdf/cli.rb +43 -0
- data/lib/bidi2pdf/dsl.rb +2 -1
- data/lib/bidi2pdf/launcher.rb +2 -1
- data/lib/bidi2pdf/session_registry/heartbeat.rb +86 -0
- data/lib/bidi2pdf/session_registry.rb +77 -13
- data/lib/bidi2pdf/session_sweeper.rb +5 -3
- data/lib/bidi2pdf/session_warmer.rb +74 -7
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +2 -0
- data/sig/bidi2pdf/bidi/commands/browsing_context_get_tree.rbs +12 -0
- data/sig/bidi2pdf/bidi/commands/cdp_send_command.rbs +23 -0
- data/sig/bidi2pdf/bidi/session.rbs +19 -3
- data/sig/bidi2pdf/chrome_sweeper/inspector.rbs +61 -0
- data/sig/bidi2pdf/chrome_sweeper/settings.rbs +35 -0
- data/sig/bidi2pdf/chrome_sweeper.rbs +220 -0
- data/sig/bidi2pdf/chromedriver_api.rbs +44 -0
- data/sig/bidi2pdf/cli/session_commands.rbs +35 -0
- data/sig/bidi2pdf/cli.rbs +6 -0
- data/sig/bidi2pdf/session_registry/heartbeat.rbs +50 -0
- data/sig/bidi2pdf/session_registry.rbs +46 -9
- data/sig/bidi2pdf/session_sweeper.rbs +2 -1
- data/sig/bidi2pdf/session_warmer.rbs +42 -2
- data/sig/bidi2pdf/version.rbs +1 -1
- metadata +17 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# Closes Chrome sessions a shared chromedriver holds that nobody will close any more - left behind
|
|
3
|
+
# by a process that died, or stuck in a tab that never returns - and keeps the number of sessions
|
|
4
|
+
# below a limit. chromedriver keeps every session (a whole Chrome) until someone deletes it.
|
|
5
|
+
#
|
|
6
|
+
# Which sessions it looks at (+scope+):
|
|
7
|
+
# - +:recorded+ (default) - only sessions a SessionRegistry proves were opened through bidi2pdf.
|
|
8
|
+
# - +:all+ - every session on that chromedriver (+GET /sessions+), aged by its first tab. Only for
|
|
9
|
+
# a chromedriver the application owns: on a shared one it closes other clients' old sessions.
|
|
10
|
+
#
|
|
11
|
+
# What it closes, in this order - never a session of +own_sessions+, never one whose lease a live
|
|
12
|
+
# process keeps renewing (SessionRegistry#hold - a render in flight or a warm spare of another
|
|
13
|
+
# worker), and never one younger than +min_age+, not even over the limit:
|
|
14
|
+
# 1. sessions older than +orphan_age+;
|
|
15
|
+
# 2. sessions that failed +unresponsive_checks+ checks in a row (no answer, or a renderer burning
|
|
16
|
+
# CPU at full speed between two sweeps - an endless loop);
|
|
17
|
+
# 3. while more sessions exist than +max_sessions+, the oldest remaining ones. When that is not
|
|
18
|
+
# enough, the sweep reports +limit_exceeded+ and closes nothing more.
|
|
19
|
+
#
|
|
20
|
+
# Run it once (#sweep!, ChromeSweeper.sweep!) - e.g. when the application suspects a leak - or
|
|
21
|
+
# periodically (+interval+, #start/#stop). As a last resort when a render fails for lack of
|
|
22
|
+
# resources, #with_retry sweeps under +pressure+ - every session nobody holds and older than
|
|
23
|
+
# +min_age+ goes - and tries once more. One sweep at a time, across processes too (a lock file
|
|
24
|
+
# next to the registry). Fail-open: a sweep never raises; errors land in the Result. See Settings
|
|
25
|
+
# for every setting and its default.
|
|
26
|
+
#
|
|
27
|
+
# @example A periodic sweeper for an application's own chromedriver
|
|
28
|
+
# sweeper = Bidi2pdf::ChromeSweeper.new("http://remote-chrome:3000/session", scope: :all,
|
|
29
|
+
# max_sessions: :auto, pids_limit: 1024, interval: 60)
|
|
30
|
+
# sweeper.start
|
|
31
|
+
class ChromeSweeper
|
|
32
|
+
@session_url: untyped
|
|
33
|
+
|
|
34
|
+
@settings: untyped
|
|
35
|
+
|
|
36
|
+
@own_sessions: untyped
|
|
37
|
+
|
|
38
|
+
@clock: untyped
|
|
39
|
+
|
|
40
|
+
@registry: untyped
|
|
41
|
+
|
|
42
|
+
@api: untyped
|
|
43
|
+
|
|
44
|
+
@inspector: untyped
|
|
45
|
+
|
|
46
|
+
@mutex: untyped
|
|
47
|
+
|
|
48
|
+
@wakeup: untyped
|
|
49
|
+
|
|
50
|
+
@failures: untyped
|
|
51
|
+
|
|
52
|
+
@first_seen: untyped
|
|
53
|
+
|
|
54
|
+
@cpu_samples: untyped
|
|
55
|
+
|
|
56
|
+
@thread: untyped
|
|
57
|
+
|
|
58
|
+
extend Forwardable
|
|
59
|
+
|
|
60
|
+
# One closed (or, in a dry run, would-be closed) session. +why+: :orphan, :unresponsive,
|
|
61
|
+
# :over_limit or :pressure.
|
|
62
|
+
Closed: untyped
|
|
63
|
+
|
|
64
|
+
# What one sweep saw and did.
|
|
65
|
+
Result: untyped
|
|
66
|
+
|
|
67
|
+
# A renderer that used at least this share of the wall-clock time between two sweeps counts as
|
|
68
|
+
# stuck in a loop.
|
|
69
|
+
BUSY_CPU_SHARE: ::Float
|
|
70
|
+
|
|
71
|
+
# Errors #with_retry sweeps and retries on by default: a session chromedriver refused, and a
|
|
72
|
+
# connection or command that died or timed out - what a Chrome out of memory or pids looks like.
|
|
73
|
+
# A command Chrome answered with an error (CmdError) is the page's problem, not a resource one.
|
|
74
|
+
RETRYABLE: untyped
|
|
75
|
+
|
|
76
|
+
# The bookkeeping of one sweep.
|
|
77
|
+
Sweep: untyped
|
|
78
|
+
|
|
79
|
+
attr_reader session_url: untyped
|
|
80
|
+
|
|
81
|
+
attr_reader settings: untyped
|
|
82
|
+
|
|
83
|
+
attr_reader registry: untyped
|
|
84
|
+
|
|
85
|
+
# One-shot sweep with a throw-away sweeper. Give it a +check_interval+ for the unresponsive rule
|
|
86
|
+
# to apply - see #sweep!.
|
|
87
|
+
#
|
|
88
|
+
# @return [Result]
|
|
89
|
+
def self.sweep!: (untyped session_url, ?reason: ::Symbol, ?check_interval: untyped?, ?pressure: bool, **untyped) -> untyped
|
|
90
|
+
|
|
91
|
+
# #with_retry with a throw-away sweeper.
|
|
92
|
+
def self.with_retry: (untyped session_url, ?retry_on: untyped, **untyped) { (?) -> untyped } -> untyped
|
|
93
|
+
|
|
94
|
+
# @param session_url [String] chromedriver's new-session URL (".../session").
|
|
95
|
+
# @param registry [SessionRegistry, nil] defaults to the one for +session_url+ in +registry_dir+.
|
|
96
|
+
# @param own_sessions [#call] returns the ids of the caller's live sessions - never touched.
|
|
97
|
+
# @param api [ChromedriverApi, nil], inspector [Inspector, nil], clock [#call] - for tests.
|
|
98
|
+
# @param settings see Settings.
|
|
99
|
+
# @raise [InvalidConfigError] for a setting out of range.
|
|
100
|
+
def initialize: (untyped session_url, ?registry: untyped?, ?registry_dir: untyped?, ?own_sessions: untyped, ?api: untyped?, ?inspector: untyped?, ?clock: untyped, **untyped settings) -> void
|
|
101
|
+
|
|
102
|
+
# Every session in scope, inspected (age, tabs, responsive) - for diagnostics; closes nothing.
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Inspector::SessionInfo>]
|
|
105
|
+
def sessions: () -> untyped
|
|
106
|
+
|
|
107
|
+
# Runs the block; when it fails with a resource error (+retry_on+: a callable or a list of error
|
|
108
|
+
# classes, RETRYABLE by default), sweeps under pressure and runs it once more. The block must
|
|
109
|
+
# be safe to run twice - it gets a new session the second time. Other errors, and a second
|
|
110
|
+
# failure, propagate unchanged.
|
|
111
|
+
def with_retry: (?retry_on: untyped) { () -> untyped } -> untyped
|
|
112
|
+
|
|
113
|
+
# Inspects every session in scope and counts its checks, like a sweep, but closes nothing.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<Inspector::SessionInfo>]
|
|
116
|
+
def observe: () -> untyped
|
|
117
|
+
|
|
118
|
+
# Sweeps once. Skipped (Result#skipped) when another sweep - in this or another process - is
|
|
119
|
+
# still running.
|
|
120
|
+
#
|
|
121
|
+
# A session counts as hung only after +unresponsive_checks+ failed checks, one per sweep - a
|
|
122
|
+
# one-shot sweep never gets there. With +check_interval+ it first checks
|
|
123
|
+
# +unresponsive_checks - 1+ times, +check_interval+ seconds apart (#observe), so a single call
|
|
124
|
+
# applies every rule. The periodic thread doesn't need that: its sweeps are the checks.
|
|
125
|
+
#
|
|
126
|
+
# @param reason [Symbol] why - reported in the notification (:manual, :periodic, :create_failed…).
|
|
127
|
+
# Under +pressure+ - the last resort when a render failed - every eligible session is closed:
|
|
128
|
+
# nobody holds it, and it is older than +min_age+; +orphan_age+, the unresponsive checks and the
|
|
129
|
+
# limit no longer matter.
|
|
130
|
+
#
|
|
131
|
+
# @param check_interval [Numeric, nil] seconds between those checks; nil sweeps right away.
|
|
132
|
+
# @param pressure [Boolean] close everything nobody holds that is past +min_age+.
|
|
133
|
+
# @return [Result]
|
|
134
|
+
def sweep!: (?reason: ::Symbol, ?check_interval: untyped?, ?pressure: bool) -> untyped
|
|
135
|
+
|
|
136
|
+
# Starts the periodic sweep thread (needs an +interval+). Idempotent.
|
|
137
|
+
def start: () -> self
|
|
138
|
+
|
|
139
|
+
# Stops the periodic sweep thread and waits for a sweep in progress to finish.
|
|
140
|
+
def stop: () -> (nil | untyped)
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def run: (untyped reason, untyped started, untyped pressure) -> untyped
|
|
145
|
+
|
|
146
|
+
# A config error raises; a failed check only means the sweep has fewer checks to go on.
|
|
147
|
+
def check_first: (untyped check_interval) -> untyped
|
|
148
|
+
|
|
149
|
+
def observe_then_wait: (untyped check_interval) -> untyped
|
|
150
|
+
|
|
151
|
+
def sweep_sessions: (untyped pressure) -> ::Array[untyped]
|
|
152
|
+
|
|
153
|
+
# In-scope sessions not owned by the caller, not held by a live process and not known to be
|
|
154
|
+
# younger than min_age, with the registry time when there is one. None of the others is even
|
|
155
|
+
# attached to - a render in flight is left alone entirely.
|
|
156
|
+
def candidates: (untyped entries) -> untyped
|
|
157
|
+
|
|
158
|
+
def live_sessions: () -> untyped
|
|
159
|
+
|
|
160
|
+
def live_info: (untyped id, untyped recorded_at) -> untyped
|
|
161
|
+
|
|
162
|
+
def retryable?: (untyped retry_on, untyped error) -> untyped
|
|
163
|
+
|
|
164
|
+
def in_scope: (untyped entries, untyped recorded) -> untyped
|
|
165
|
+
|
|
166
|
+
def observe_entries: (untyped entries) -> untyped
|
|
167
|
+
|
|
168
|
+
# Sessions chromedriver no longer has are dropped from the registry and from this sweeper's own
|
|
169
|
+
# tracking - most sessions end normally, so a long-running sweeper would otherwise keep every id
|
|
170
|
+
# it ever saw.
|
|
171
|
+
def forget_gone: (untyped entries) -> untyped
|
|
172
|
+
|
|
173
|
+
def forget_tracking: (untyped id) -> untyped
|
|
174
|
+
|
|
175
|
+
# Counts failed checks per session across sweeps, and ages a session by when this sweeper first
|
|
176
|
+
# saw it when nothing better is known.
|
|
177
|
+
def track: (untyped info) -> untyped
|
|
178
|
+
|
|
179
|
+
def count_check: (untyped info) -> untyped
|
|
180
|
+
|
|
181
|
+
def busy?: (untyped info) -> (false | untyped)
|
|
182
|
+
|
|
183
|
+
def decide: (untyped eligible, untyped sweep) -> untyped
|
|
184
|
+
|
|
185
|
+
def close_reason: (untyped info, untyped sweep) -> (:orphan | untyped)
|
|
186
|
+
|
|
187
|
+
def note_unresponsive: (untyped info, untyped failed, untyped sweep) -> untyped
|
|
188
|
+
|
|
189
|
+
def enforce_limit: (untyped eligible, untyped sweep) -> (nil | untyped)
|
|
190
|
+
|
|
191
|
+
def over_limit?: (untyped sweep) -> untyped
|
|
192
|
+
|
|
193
|
+
def oldest_open: (untyped eligible, untyped sweep) -> untyped
|
|
194
|
+
|
|
195
|
+
def limit_exceeded: (untyped sweep) -> untyped
|
|
196
|
+
|
|
197
|
+
# Adds the session to the sweep's closed list unless chromedriver refused to close it.
|
|
198
|
+
def close_session: (untyped info, untyped why, untyped sweep) -> untyped
|
|
199
|
+
|
|
200
|
+
def announce: (untyped closed) -> untyped
|
|
201
|
+
|
|
202
|
+
def gone_after_delete?: (untyped id) -> (false | true)
|
|
203
|
+
|
|
204
|
+
def report: (untyped result) -> untyped
|
|
205
|
+
|
|
206
|
+
def sweep_payload: (untyped result) -> untyped
|
|
207
|
+
|
|
208
|
+
def result: (untyped reason, untyped started, ?sweep: untyped, ?inspected: untyped, ?skipped: bool) -> untyped
|
|
209
|
+
|
|
210
|
+
# A lock file next to the registry, so processes sharing it never sweep at the same time. When
|
|
211
|
+
# the file cannot be created at all the sweep still runs (fail-open, like the registry itself).
|
|
212
|
+
def with_lock_file: () { (untyped) -> untyped } -> untyped
|
|
213
|
+
|
|
214
|
+
def open_lock_file: () -> untyped
|
|
215
|
+
|
|
216
|
+
def now: () -> untyped
|
|
217
|
+
|
|
218
|
+
def monotonic: () -> untyped
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# The two chromedriver HTTP endpoints session cleanup needs, next to the BiDi protocol: the
|
|
3
|
+
# non-standard +GET /sessions+ (every session on that chromedriver - WebDriver itself has no way to
|
|
4
|
+
# list them, BiDi neither) and +DELETE /session/{id}+ (ends a session and its Chrome even when that
|
|
5
|
+
# Chrome no longer answers BiDi).
|
|
6
|
+
#
|
|
7
|
+
# Built from the new-session URL every other part of bidi2pdf is configured with
|
|
8
|
+
# ("http://host:3000/session").
|
|
9
|
+
class ChromedriverApi
|
|
10
|
+
@session_url: untyped
|
|
11
|
+
|
|
12
|
+
@timeout: untyped
|
|
13
|
+
|
|
14
|
+
@http: untyped
|
|
15
|
+
|
|
16
|
+
# One entry of +GET /sessions+: chromedriver gives no creation time, no owner, and no hint
|
|
17
|
+
# whether a client is still connected (checked against chromedriver 153/154).
|
|
18
|
+
Entry: untyped
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIMEOUT: 30
|
|
21
|
+
|
|
22
|
+
attr_reader session_url: untyped
|
|
23
|
+
|
|
24
|
+
# @param session_url [String] chromedriver's new-session URL (".../session").
|
|
25
|
+
# @param http [#call, nil] (method, url) -> [status, body]; injectable for tests.
|
|
26
|
+
def initialize: (untyped session_url, ?http: untyped?, ?timeout: untyped) -> void
|
|
27
|
+
|
|
28
|
+
# @return [Array<Entry>] every session chromedriver holds right now.
|
|
29
|
+
def sessions: () -> untyped
|
|
30
|
+
|
|
31
|
+
# Ends a session. chromedriver answers 404 ("invalid session id") for one it no longer has.
|
|
32
|
+
#
|
|
33
|
+
# @return [Symbol] :closed, :gone or :failed
|
|
34
|
+
def delete_session: (untyped id) -> untyped
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# "http://host:3000/session" -> "http://host:3000/sessions"; a chromedriver started with a
|
|
39
|
+
# --url-base keeps its prefix.
|
|
40
|
+
def sessions_url: () -> ::String
|
|
41
|
+
|
|
42
|
+
def net_http: (untyped method, untyped url) -> untyped
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
class CLI < Thor
|
|
3
|
+
# Output of `bidi2pdf sessions` and `bidi2pdf sweep` (ChromeSweeper). Never prints a tab URL or
|
|
4
|
+
# any page content - only ids, ages, tab counts and what was done.
|
|
5
|
+
module SessionCommands
|
|
6
|
+
# CLI flag => ChromeSweeper setting, for the ones that are only passed when given.
|
|
7
|
+
SWEEP_FLAGS: { older_than: :orphan_age, max_sessions: :max_sessions, min_age: :min_age, unresponsive_checks: :unresponsive_checks }
|
|
8
|
+
|
|
9
|
+
# Settings that are counts - Thor hands every numeric over as a Float.
|
|
10
|
+
COUNT_SETTINGS: ::Array[:max_sessions | :unresponsive_checks]
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def chrome_sweeper: () -> untyped
|
|
15
|
+
|
|
16
|
+
def sweep_flags: () -> untyped
|
|
17
|
+
|
|
18
|
+
# --check-interval 0 means no checks before the sweep.
|
|
19
|
+
def run_sweep: () -> untyped
|
|
20
|
+
|
|
21
|
+
def session_hash: (untyped info) -> { id: untyped, age: untyped, source: untyped, tabs: untyped, responsive: untyped, live: untyped }
|
|
22
|
+
|
|
23
|
+
def print_sessions: (untyped infos) -> untyped
|
|
24
|
+
|
|
25
|
+
# A live session is not attached to, so there is nothing to say about its tabs.
|
|
26
|
+
def session_state: (untyped info) -> ("live (a running process holds it)" | untyped)
|
|
27
|
+
|
|
28
|
+
def sweep_hash: (untyped result) -> { reason: untyped, sessions: untyped, dry_run: untyped, skipped: untyped, closed: untyped, unresponsive: untyped, limit: untyped, limit_exceeded: untyped, errors: untyped }
|
|
29
|
+
|
|
30
|
+
def print_sweep: (untyped result) -> untyped
|
|
31
|
+
|
|
32
|
+
def print_sweep_problems: (untyped result) -> untyped
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/sig/bidi2pdf/cli.rbs
CHANGED
|
@@ -13,6 +13,8 @@ module Bidi2pdf
|
|
|
13
13
|
|
|
14
14
|
include JsonOutput
|
|
15
15
|
|
|
16
|
+
include SessionCommands
|
|
17
|
+
|
|
16
18
|
def self.exit_on_failure?: () -> true
|
|
17
19
|
|
|
18
20
|
def render: () -> (nil | untyped)
|
|
@@ -28,6 +30,10 @@ module Bidi2pdf
|
|
|
28
30
|
# below is what makes `bidi2pdf run recipe.yml` dispatch here.
|
|
29
31
|
def run_recipe: (untyped recipe_path) -> untyped
|
|
30
32
|
|
|
33
|
+
def sessions: () -> untyped
|
|
34
|
+
|
|
35
|
+
def sweep: () -> untyped
|
|
36
|
+
|
|
31
37
|
def template: () -> untyped
|
|
32
38
|
|
|
33
39
|
private
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
class SessionRegistry
|
|
3
|
+
# Keeps the leases of every session this process holds fresh: one thread per process renews
|
|
4
|
+
# them in their registry files every +interval+ seconds, so a sweeper in another process (a
|
|
5
|
+
# second Puma worker, a job worker sharing the registry directory) can tell a live session
|
|
6
|
+
# from one whose process died. The thread runs only while something is held.
|
|
7
|
+
#
|
|
8
|
+
# Fork-aware: a child process (Puma forks its workers) starts with nothing held and without the
|
|
9
|
+
# parent's thread - the parent's sessions are the parent's to renew.
|
|
10
|
+
module Heartbeat
|
|
11
|
+
self.@mutex: untyped
|
|
12
|
+
|
|
13
|
+
self.@held: untyped
|
|
14
|
+
|
|
15
|
+
self.@pid: untyped
|
|
16
|
+
|
|
17
|
+
self.@thread: untyped
|
|
18
|
+
|
|
19
|
+
DEFAULT_INTERVAL: untyped
|
|
20
|
+
|
|
21
|
+
# @return [Numeric] seconds between renewals - keep it well below the sweeper's lease_ttl.
|
|
22
|
+
attr_writer self.interval: untyped
|
|
23
|
+
|
|
24
|
+
def self.interval: () -> untyped
|
|
25
|
+
|
|
26
|
+
# The TTL this process writes into its leases: three missed renewals and it is taken for dead.
|
|
27
|
+
def self.lease_ttl: () -> untyped
|
|
28
|
+
|
|
29
|
+
def self.hold: (untyped registry, untyped session_id) -> untyped
|
|
30
|
+
|
|
31
|
+
def self.release: (untyped registry, untyped session_id) -> untyped
|
|
32
|
+
|
|
33
|
+
# @return [Array<String>] the session ids this process holds in +registry+'s file.
|
|
34
|
+
def self.held_ids: (untyped registry) -> untyped
|
|
35
|
+
|
|
36
|
+
# Renews every held lease now.
|
|
37
|
+
def self.beat!: (?now: untyped) -> untyped
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.beat_loop: () -> untyped
|
|
42
|
+
|
|
43
|
+
def self.held: () -> untyped
|
|
44
|
+
|
|
45
|
+
def self.reset_after_fork: () -> (nil | untyped)
|
|
46
|
+
|
|
47
|
+
def self.synchronize: () { (?) -> untyped } -> untyped
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
module Bidi2pdf
|
|
2
|
-
# Remembers which sessions
|
|
3
|
-
#
|
|
4
|
-
# process
|
|
5
|
-
#
|
|
6
|
-
#
|
|
2
|
+
# Remembers which sessions bidi2pdf opened on a shared (remote) chromedriver, when, by which
|
|
3
|
+
# process, and whether that process is still alive - in a small JSON file per chromedriver URL - so
|
|
4
|
+
# a process started later, or a ChromeSweeper, can find and close the ones a process that died
|
|
5
|
+
# without a clean shutdown left behind (see SessionSweeper). chromedriver itself keeps a session
|
|
6
|
+
# until someone deletes it, and it drops any tag we could attach to one (a custom capability is
|
|
7
|
+
# accepted but echoed nowhere - checked against chromedriver 153), hence a file.
|
|
8
|
+
#
|
|
9
|
+
# Liveness is a lease: a session the process #hold s is renewed by its Heartbeat every
|
|
10
|
+
# Heartbeat.interval seconds, and every entry carries its own TTL - the time the owner promises to
|
|
11
|
+
# renew it within (three heartbeats) - so a sweeper in another process never has to guess how
|
|
12
|
+
# often that owner renews. A session whose lease is fresh belongs to a live process - a render in
|
|
13
|
+
# flight or a warm spare - and no sweeper closes it; one whose lease ran out belongs to a dead
|
|
14
|
+
# process. #record alone writes an entry nobody renews.
|
|
7
15
|
#
|
|
8
16
|
# Everything here is best effort and fail-open: a directory that is not writable, a locked-out or
|
|
9
17
|
# corrupt file only switches orphan tracking off for this process (one warning, one
|
|
10
18
|
# +session_warmer.registry_unavailable.bidi2pdf+ notification) - rendering never depends on it.
|
|
11
19
|
# The file is created 0600 and every change happens under an exclusive lock, so processes sharing
|
|
12
|
-
# the directory (
|
|
20
|
+
# the directory (Puma workers, a job worker, a spec run) can record at the same time.
|
|
13
21
|
class SessionRegistry
|
|
14
22
|
@path: untyped
|
|
15
23
|
|
|
@@ -17,16 +25,42 @@ module Bidi2pdf
|
|
|
17
25
|
|
|
18
26
|
FILE_PREFIX: "bidi2pdf-sessions-"
|
|
19
27
|
|
|
28
|
+
# Seconds a lease stays fresh without a renewal when its entry carries no TTL of its own (written
|
|
29
|
+
# by bidi2pdf 0.1.18) - matches that version's fixed 20 s heartbeat.
|
|
30
|
+
DEFAULT_LEASE_TTL: 60
|
|
31
|
+
|
|
20
32
|
attr_reader path: untyped
|
|
21
33
|
|
|
34
|
+
def self.owner: () -> ::String
|
|
35
|
+
|
|
22
36
|
def initialize: (untyped session_url, ?dir: untyped?) -> void
|
|
23
37
|
|
|
24
|
-
# Records a session the caller
|
|
25
|
-
def
|
|
38
|
+
# Records a session the caller opened and keeps its lease fresh while this process lives.
|
|
39
|
+
def hold: (untyped session_id) -> (false | untyped)
|
|
40
|
+
|
|
41
|
+
# Stops renewing the lease but keeps the entry - for a session this process could not close, so
|
|
42
|
+
# a sweeper takes it once the lease ran out.
|
|
43
|
+
def release: (untyped session_id) -> (untyped | nil)
|
|
44
|
+
|
|
45
|
+
# Writes an entry, leased from +created_at+ on for +ttl+ seconds (nil: the reader's default) but
|
|
46
|
+
# not renewed (see #hold).
|
|
47
|
+
# Times are stored as fractions of a second: a whole-second timestamp could cost a short lease
|
|
48
|
+
# (0.5 s heartbeats: 1.5 s) up to a second of its life. Entries of whole seconds still read.
|
|
49
|
+
def record: (untyped session_id, ?created_at: untyped, ?ttl: untyped?) -> (false | untyped)
|
|
26
50
|
|
|
27
51
|
# Forgets a session the caller closed itself (or found gone).
|
|
28
52
|
def forget: (untyped session_id) -> (false | untyped)
|
|
29
53
|
|
|
54
|
+
# Renews the leases of +session_ids+ that are still recorded, for +ttl+ seconds when given.
|
|
55
|
+
def renew: (untyped session_ids, ?at: untyped, ?ttl: untyped?) -> untyped
|
|
56
|
+
|
|
57
|
+
# Every recorded session: { id => opened at (epoch seconds) }.
|
|
58
|
+
def recorded: () -> untyped
|
|
59
|
+
|
|
60
|
+
# The recorded session ids whose lease is still fresh at +now+ - sessions of a live process. An
|
|
61
|
+
# entry's own TTL wins; +ttl+ is only for entries without one.
|
|
62
|
+
def leased: (?now: untyped, ?ttl: untyped) -> untyped
|
|
63
|
+
|
|
30
64
|
# The recorded session ids opened at or before +cutoff+ (epoch seconds).
|
|
31
65
|
def recorded_before: (untyped cutoff) -> untyped
|
|
32
66
|
|
|
@@ -36,9 +70,12 @@ module Bidi2pdf
|
|
|
36
70
|
|
|
37
71
|
def update: () { (untyped) -> untyped } -> untyped
|
|
38
72
|
|
|
39
|
-
# A corrupt or foreign file counts as empty rather than as an error
|
|
73
|
+
# A corrupt or foreign file counts as empty rather than as an error; an entry of the first
|
|
74
|
+
# format (just the creation time) has no lease.
|
|
40
75
|
def parse: (untyped text) -> untyped
|
|
41
76
|
|
|
77
|
+
def normalize: (untyped entry) -> ({ "created_at" => untyped } | untyped)
|
|
78
|
+
|
|
42
79
|
def unavailable!: (untyped error) -> (nil | untyped)
|
|
43
80
|
end
|
|
44
81
|
end
|
|
@@ -4,7 +4,8 @@ module Bidi2pdf
|
|
|
4
4
|
# whole Chrome each until someone deletes them. Run once when a warmer starts.
|
|
5
5
|
#
|
|
6
6
|
# Only recorded sessions are ever touched - another tool's sessions on the same chromedriver never
|
|
7
|
-
# are
|
|
7
|
+
# are, and neither is one whose lease a live process keeps renewing (SessionRegistry#hold). A
|
|
8
|
+
# recorded session that is already gone ("invalid session id" / 404) is just forgotten.
|
|
8
9
|
# Fail-open: any error is logged and ends the sweep; it never raises.
|
|
9
10
|
class SessionSweeper
|
|
10
11
|
@session_url: untyped
|
|
@@ -42,8 +42,12 @@ module Bidi2pdf
|
|
|
42
42
|
|
|
43
43
|
@reaper_wakeup: untyped
|
|
44
44
|
|
|
45
|
+
@own_sessions: untyped
|
|
46
|
+
|
|
45
47
|
@registry: untyped
|
|
46
48
|
|
|
49
|
+
@sweeper: untyped
|
|
50
|
+
|
|
47
51
|
@reaper: untyped
|
|
48
52
|
|
|
49
53
|
# Configuration for the session warmer.
|
|
@@ -64,6 +68,10 @@ module Bidi2pdf
|
|
|
64
68
|
|
|
65
69
|
@registry_dir: untyped
|
|
66
70
|
|
|
71
|
+
@sweeper: untyped
|
|
72
|
+
|
|
73
|
+
@retry_refused_sessions: untyped
|
|
74
|
+
|
|
67
75
|
# @return [Integer] Number of Chrome slots to keep pre-warmed.
|
|
68
76
|
attr_accessor size: untyped
|
|
69
77
|
|
|
@@ -101,6 +109,17 @@ module Bidi2pdf
|
|
|
101
109
|
# Dir.tmpdir. Every process that should clean up after the others must share it.
|
|
102
110
|
attr_accessor registry_dir: untyped
|
|
103
111
|
|
|
112
|
+
# @return [Hash, nil] Options for a ChromeSweeper on the remote chromedriver (+scope+,
|
|
113
|
+
# +orphan_age+, +min_age+, +unresponsive_checks+, +max_sessions+, +pids_limit+, +interval+,
|
|
114
|
+
# ...), or +nil+ (default) for none. This warmer's own sessions are never touched by it; with
|
|
115
|
+
# an +interval+ it sweeps in the background, and a session that fails to start triggers one
|
|
116
|
+
# pressure sweep and one retry (see +retry_refused_sessions+). Remote mode only.
|
|
117
|
+
attr_accessor sweeper: untyped
|
|
118
|
+
|
|
119
|
+
# @return [Boolean] With a +sweeper+: when chromedriver refuses a new session, sweep under
|
|
120
|
+
# pressure and try once more. Default true; false leaves a refused session to the caller.
|
|
121
|
+
attr_accessor retry_refused_sessions: untyped
|
|
122
|
+
|
|
104
123
|
DEFAULT_MAX_IDLE_AGE: 300
|
|
105
124
|
|
|
106
125
|
def initialize: () -> void
|
|
@@ -119,6 +138,8 @@ module Bidi2pdf
|
|
|
119
138
|
def validate_max_idle_age!: () -> (nil | untyped)
|
|
120
139
|
|
|
121
140
|
def validate_orphan_age!: () -> (nil | untyped)
|
|
141
|
+
|
|
142
|
+
def validate_sweeper!: () -> (nil | untyped)
|
|
122
143
|
end
|
|
123
144
|
|
|
124
145
|
# Configures the warmer and eagerly (re)creates the singleton, warming config.size slots right
|
|
@@ -149,6 +170,10 @@ module Bidi2pdf
|
|
|
149
170
|
# Checks out a slot, yields a fresh tab for one render, then retires the slot.
|
|
150
171
|
def self.with_tab: () { (?) -> untyped } -> untyped
|
|
151
172
|
|
|
173
|
+
# Sweeps the remote chromedriver with the configured sweeper; nil when there is none (or no
|
|
174
|
+
# warmer yet - this never creates one).
|
|
175
|
+
def self.sweep!: (?reason: ::Symbol) -> untyped
|
|
176
|
+
|
|
152
177
|
# Retires every currently-warm spare and resets the singleton.
|
|
153
178
|
def self.shutdown: () -> untyped
|
|
154
179
|
|
|
@@ -166,6 +191,11 @@ module Bidi2pdf
|
|
|
166
191
|
|
|
167
192
|
def initialize: (untyped config, ?slot_factory: untyped?) -> void
|
|
168
193
|
|
|
194
|
+
# Sweeps the remote chromedriver now - for an application that suspects leaked sessions.
|
|
195
|
+
#
|
|
196
|
+
# @return [ChromeSweeper::Result, nil] nil when no sweeper is configured.
|
|
197
|
+
def sweep!: (?reason: ::Symbol) -> untyped
|
|
198
|
+
|
|
169
199
|
# Checks out a slot, creates an isolated UserContext/Window/Tab for one render, yields the tab,
|
|
170
200
|
# then unconditionally closes those resources and retires the underlying slot.
|
|
171
201
|
def with_tab: () { (untyped) -> untyped } -> untyped
|
|
@@ -184,8 +214,18 @@ module Bidi2pdf
|
|
|
184
214
|
|
|
185
215
|
def create_slot: () -> untyped
|
|
186
216
|
|
|
187
|
-
#
|
|
188
|
-
#
|
|
217
|
+
# A chromedriver out of room for another Chrome refuses the session; with a sweeper, close
|
|
218
|
+
# everything nobody holds (a pressure sweep) and try once more.
|
|
219
|
+
def new_slot: () -> untyped
|
|
220
|
+
|
|
221
|
+
def start_background_work: () -> untyped
|
|
222
|
+
|
|
223
|
+
def build_sweeper: () -> (nil | untyped)
|
|
224
|
+
|
|
225
|
+
# Remote mode: every session is leased, so no sweeper in any process takes a warm spare or a
|
|
226
|
+
# render in flight, and a later start (or a sweeper) can close the ones this process leaves
|
|
227
|
+
# behind if it dies uncleanly. The start-up sweep itself needs the orphan age - see
|
|
228
|
+
# #sweep_leftovers.
|
|
189
229
|
def build_registry: () -> (nil | untyped)
|
|
190
230
|
|
|
191
231
|
def sweep_leftovers: () -> (nil | untyped)
|
data/sig/bidi2pdf/version.rbs
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bidi2pdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dieter S.
|
|
@@ -429,11 +429,13 @@ files:
|
|
|
429
429
|
- lib/bidi2pdf/bidi/commands/browser_create_user_context.rb
|
|
430
430
|
- lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb
|
|
431
431
|
- lib/bidi2pdf/bidi/commands/browsing_context_close.rb
|
|
432
|
+
- lib/bidi2pdf/bidi/commands/browsing_context_get_tree.rb
|
|
432
433
|
- lib/bidi2pdf/bidi/commands/browsing_context_navigate.rb
|
|
433
434
|
- lib/bidi2pdf/bidi/commands/browsing_context_print.rb
|
|
434
435
|
- lib/bidi2pdf/bidi/commands/cancel_auth.rb
|
|
435
436
|
- lib/bidi2pdf/bidi/commands/capture_screenshot.rb
|
|
436
437
|
- lib/bidi2pdf/bidi/commands/cdp_get_session.rb
|
|
438
|
+
- lib/bidi2pdf/bidi/commands/cdp_send_command.rb
|
|
437
439
|
- lib/bidi2pdf/bidi/commands/create_tab.rb
|
|
438
440
|
- lib/bidi2pdf/bidi/commands/create_window.rb
|
|
439
441
|
- lib/bidi2pdf/bidi/commands/get_user_contexts.rb
|
|
@@ -463,9 +465,14 @@ files:
|
|
|
463
465
|
- lib/bidi2pdf/bidi/session.rb
|
|
464
466
|
- lib/bidi2pdf/bidi/user_context.rb
|
|
465
467
|
- lib/bidi2pdf/bidi/web_socket_dispatcher.rb
|
|
468
|
+
- lib/bidi2pdf/chrome_sweeper.rb
|
|
469
|
+
- lib/bidi2pdf/chrome_sweeper/inspector.rb
|
|
470
|
+
- lib/bidi2pdf/chrome_sweeper/settings.rb
|
|
471
|
+
- lib/bidi2pdf/chromedriver_api.rb
|
|
466
472
|
- lib/bidi2pdf/chromedriver_manager.rb
|
|
467
473
|
- lib/bidi2pdf/cli.rb
|
|
468
474
|
- lib/bidi2pdf/cli/json_output.rb
|
|
475
|
+
- lib/bidi2pdf/cli/session_commands.rb
|
|
469
476
|
- lib/bidi2pdf/diagnose.rb
|
|
470
477
|
- lib/bidi2pdf/dsl.rb
|
|
471
478
|
- lib/bidi2pdf/error_codes.rb
|
|
@@ -488,6 +495,7 @@ files:
|
|
|
488
495
|
- lib/bidi2pdf/result_collector.rb
|
|
489
496
|
- lib/bidi2pdf/schema.rb
|
|
490
497
|
- lib/bidi2pdf/session_registry.rb
|
|
498
|
+
- lib/bidi2pdf/session_registry/heartbeat.rb
|
|
491
499
|
- lib/bidi2pdf/session_runner.rb
|
|
492
500
|
- lib/bidi2pdf/session_sweeper.rb
|
|
493
501
|
- lib/bidi2pdf/session_warmer.rb
|
|
@@ -529,11 +537,13 @@ files:
|
|
|
529
537
|
- sig/bidi2pdf/bidi/commands/browser_create_user_context.rbs
|
|
530
538
|
- sig/bidi2pdf/bidi/commands/browser_remove_user_context.rbs
|
|
531
539
|
- sig/bidi2pdf/bidi/commands/browsing_context_close.rbs
|
|
540
|
+
- sig/bidi2pdf/bidi/commands/browsing_context_get_tree.rbs
|
|
532
541
|
- sig/bidi2pdf/bidi/commands/browsing_context_navigate.rbs
|
|
533
542
|
- sig/bidi2pdf/bidi/commands/browsing_context_print.rbs
|
|
534
543
|
- sig/bidi2pdf/bidi/commands/cancel_auth.rbs
|
|
535
544
|
- sig/bidi2pdf/bidi/commands/capture_screenshot.rbs
|
|
536
545
|
- sig/bidi2pdf/bidi/commands/cdp_get_session.rbs
|
|
546
|
+
- sig/bidi2pdf/bidi/commands/cdp_send_command.rbs
|
|
537
547
|
- sig/bidi2pdf/bidi/commands/create_tab.rbs
|
|
538
548
|
- sig/bidi2pdf/bidi/commands/create_window.rbs
|
|
539
549
|
- sig/bidi2pdf/bidi/commands/get_user_contexts.rbs
|
|
@@ -563,9 +573,14 @@ files:
|
|
|
563
573
|
- sig/bidi2pdf/bidi/session.rbs
|
|
564
574
|
- sig/bidi2pdf/bidi/user_context.rbs
|
|
565
575
|
- sig/bidi2pdf/bidi/web_socket_dispatcher.rbs
|
|
576
|
+
- sig/bidi2pdf/chrome_sweeper.rbs
|
|
577
|
+
- sig/bidi2pdf/chrome_sweeper/inspector.rbs
|
|
578
|
+
- sig/bidi2pdf/chrome_sweeper/settings.rbs
|
|
579
|
+
- sig/bidi2pdf/chromedriver_api.rbs
|
|
566
580
|
- sig/bidi2pdf/chromedriver_manager.rbs
|
|
567
581
|
- sig/bidi2pdf/cli.rbs
|
|
568
582
|
- sig/bidi2pdf/cli/json_output.rbs
|
|
583
|
+
- sig/bidi2pdf/cli/session_commands.rbs
|
|
569
584
|
- sig/bidi2pdf/diagnose.rbs
|
|
570
585
|
- sig/bidi2pdf/dsl.rbs
|
|
571
586
|
- sig/bidi2pdf/error_codes.rbs
|
|
@@ -588,6 +603,7 @@ files:
|
|
|
588
603
|
- sig/bidi2pdf/result_collector.rbs
|
|
589
604
|
- sig/bidi2pdf/schema.rbs
|
|
590
605
|
- sig/bidi2pdf/session_registry.rbs
|
|
606
|
+
- sig/bidi2pdf/session_registry/heartbeat.rbs
|
|
591
607
|
- sig/bidi2pdf/session_runner.rbs
|
|
592
608
|
- sig/bidi2pdf/session_sweeper.rbs
|
|
593
609
|
- sig/bidi2pdf/session_warmer.rbs
|