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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +111 -0
  4. data/lib/bidi2pdf/bidi/commands/browsing_context_get_tree.rb +16 -0
  5. data/lib/bidi2pdf/bidi/commands/cdp_send_command.rb +25 -0
  6. data/lib/bidi2pdf/bidi/commands.rb +2 -0
  7. data/lib/bidi2pdf/bidi/session.rb +53 -24
  8. data/lib/bidi2pdf/chrome_sweeper/inspector.rb +117 -0
  9. data/lib/bidi2pdf/chrome_sweeper/settings.rb +80 -0
  10. data/lib/bidi2pdf/chrome_sweeper.rb +398 -0
  11. data/lib/bidi2pdf/chromedriver_api.rb +72 -0
  12. data/lib/bidi2pdf/cli/session_commands.rb +82 -0
  13. data/lib/bidi2pdf/cli.rb +43 -0
  14. data/lib/bidi2pdf/dsl.rb +2 -1
  15. data/lib/bidi2pdf/launcher.rb +2 -1
  16. data/lib/bidi2pdf/session_registry/heartbeat.rb +86 -0
  17. data/lib/bidi2pdf/session_registry.rb +77 -13
  18. data/lib/bidi2pdf/session_sweeper.rb +5 -3
  19. data/lib/bidi2pdf/session_warmer.rb +74 -7
  20. data/lib/bidi2pdf/version.rb +1 -1
  21. data/lib/bidi2pdf.rb +2 -0
  22. data/sig/bidi2pdf/bidi/commands/browsing_context_get_tree.rbs +12 -0
  23. data/sig/bidi2pdf/bidi/commands/cdp_send_command.rbs +23 -0
  24. data/sig/bidi2pdf/bidi/session.rbs +19 -3
  25. data/sig/bidi2pdf/chrome_sweeper/inspector.rbs +61 -0
  26. data/sig/bidi2pdf/chrome_sweeper/settings.rbs +35 -0
  27. data/sig/bidi2pdf/chrome_sweeper.rbs +220 -0
  28. data/sig/bidi2pdf/chromedriver_api.rbs +44 -0
  29. data/sig/bidi2pdf/cli/session_commands.rbs +35 -0
  30. data/sig/bidi2pdf/cli.rbs +6 -0
  31. data/sig/bidi2pdf/session_registry/heartbeat.rbs +50 -0
  32. data/sig/bidi2pdf/session_registry.rbs +46 -9
  33. data/sig/bidi2pdf/session_sweeper.rbs +2 -1
  34. data/sig/bidi2pdf/session_warmer.rbs +42 -2
  35. data/sig/bidi2pdf/version.rbs +1 -1
  36. 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 a SessionWarmer opened on a shared (remote) chromedriver, and when, in a
3
- # small JSON file per chromedriver URL - so a process started later can find and close the ones a
4
- # process that died without a clean shutdown left behind (see SessionSweeper). chromedriver itself
5
- # keeps a session until someone deletes it, and it drops any tag we could attach to one (a custom
6
- # capability is accepted but echoed nowhere - checked against chromedriver 153), hence a file.
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 (a dev server, a job worker, a spec run) can record at the same time.
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 just opened.
25
- def record: (untyped session_id, ?created_at: untyped) -> (false | untyped)
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. A recorded session that is already gone ("invalid session id" / 404) is just forgotten.
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
- # Remote mode with an orphan age: sessions are recorded, so a later start can close the ones
188
- # this process leaves behind if it dies uncleanly - and this start closes others' leftovers.
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)
@@ -1,3 +1,3 @@
1
1
  module Bidi2pdf
2
- VERSION: "0.1.17"
2
+ VERSION: "0.1.19"
3
3
  end
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.17
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