html2rss 0.32.2 → 0.32.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d37540b2f300af8a1db631e16cf6cabf4cfa76069000a71c7723737225bd6c3
|
|
4
|
+
data.tar.gz: bd8f656ae629587336257d790da183c759b9c709c1ba411e5c0041156d35dd6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 802b88abad9bbadfa6753a5c62694a66c6eec3774e84d85191beabdfdd2bc8df3e37fdf325bc14c1d6fda0b90528aca2484df15d0d985b6e328c07f4c27a49b9
|
|
7
|
+
data.tar.gz: 827d398c34732c10600ced1002525702392448fcb4e8b585f7c27048c8fae38e9c2652307705c8a91e82fba295dbbfa8bfb0cc4dc3a71c4d70cdf9773722a4cf
|
|
@@ -35,6 +35,10 @@ These abort immediately (`AutoFallback::NON_FALLBACK_ERRORS`): unknown strategy,
|
|
|
35
35
|
|
|
36
36
|
Retries share the feed's request/session policy. Pin a concrete strategy when you need a single-hop budget profile.
|
|
37
37
|
|
|
38
|
+
### Terminal fallback outcome
|
|
39
|
+
|
|
40
|
+
A hop that returns zero items does **not** erase an earlier terminal transport failure (`RequestTimedOut` or `BlockedSurfaceDetected`). `AutoFallback` remembers that error across later hops and, when no later hop yields items, re-raises it with `attempts` attached. A later hop that extracts items still succeeds. Two zero-item hops with no terminal transport error still raise `NoFeedItemsExtracted`. `html2rss-web` `ErrorClassifier` maps the raised error to HTTP/cache policy; it does not invent a second cache rule for empty extraction.
|
|
41
|
+
|
|
38
42
|
## Success signals
|
|
39
43
|
|
|
40
44
|
- `Html2rss::RequestService::Response` includes transport metadata for the strategy that produced it.
|
|
@@ -27,16 +27,26 @@ module Html2rss
|
|
|
27
27
|
RequestService::RedirectLimitReached
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
|
+
# Transport failures preserved across a later hop that extracts zero items.
|
|
31
|
+
# Re-raised from finalize_failure so web maps them to uncacheable gateway/blocked
|
|
32
|
+
# decisions instead of EXTRACTION_EMPTY.
|
|
33
|
+
TERMINAL_TRANSPORT_ERRORS = [
|
|
34
|
+
RequestService::RequestTimedOut,
|
|
35
|
+
RequestService::BlockedSurfaceDetected
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
30
38
|
##
|
|
31
39
|
# Mutable run state for one auto-fallback chain: attempts plus selected result.
|
|
32
40
|
class AttemptState
|
|
33
|
-
attr_reader :attempts, :result, :last_response, :last_error, :entry_resolution
|
|
41
|
+
attr_reader :attempts, :result, :last_response, :last_error, :entry_resolution,
|
|
42
|
+
:terminal_transport_error
|
|
34
43
|
|
|
35
44
|
def initialize
|
|
36
45
|
@attempts = []
|
|
37
46
|
@result = nil
|
|
38
47
|
@last_response = nil
|
|
39
48
|
@last_error = nil
|
|
49
|
+
@terminal_transport_error = nil
|
|
40
50
|
@entry_resolution = nil
|
|
41
51
|
@resolution_tried = false
|
|
42
52
|
@aborted = false
|
|
@@ -76,6 +86,9 @@ module Html2rss
|
|
|
76
86
|
attempt[:transport_meta] = meta if meta&.any?
|
|
77
87
|
@attempts << attempt
|
|
78
88
|
@last_error = error
|
|
89
|
+
return unless TERMINAL_TRANSPORT_ERRORS.any? { |klass| error.is_a?(klass) }
|
|
90
|
+
|
|
91
|
+
@terminal_transport_error = error
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
# @param strategy [Symbol] strategy that returned a response
|
|
@@ -85,7 +98,7 @@ module Html2rss
|
|
|
85
98
|
def record_items(strategy:, items_count:, transport_meta: nil)
|
|
86
99
|
@last_error = nil
|
|
87
100
|
attempt = { strategy:, items_count:, error_class: nil }
|
|
88
|
-
attempt[:transport_meta] = transport_meta if transport_meta
|
|
101
|
+
attempt[:transport_meta] = transport_meta if transport_meta&.any?
|
|
89
102
|
@attempts << attempt
|
|
90
103
|
end
|
|
91
104
|
|
|
@@ -183,7 +196,9 @@ module Html2rss
|
|
|
183
196
|
|
|
184
197
|
# rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize -- extract + resolution delegate
|
|
185
198
|
def process_response(response:, strategy:, next_strategy:, request_session:, state:)
|
|
186
|
-
articles, dedup_dropped, admission_drops =
|
|
199
|
+
articles, dedup_dropped, admission_drops = pipeline.deduplicated_articles(
|
|
200
|
+
config:, response:, request_session:
|
|
201
|
+
)
|
|
187
202
|
state.remember_response(response)
|
|
188
203
|
items_count = articles.size
|
|
189
204
|
state.record_items(strategy:, items_count:, transport_meta: response.transport_meta)
|
|
@@ -204,7 +219,7 @@ module Html2rss
|
|
|
204
219
|
state:)
|
|
205
220
|
end
|
|
206
221
|
|
|
207
|
-
if
|
|
222
|
+
if DETERMINISTIC_HTTP_STATUSES.member?(response&.status)
|
|
208
223
|
state.abort!
|
|
209
224
|
Log.debug("#{self.class}: deterministic HTTP #{response.status}; aborting auto fallback")
|
|
210
225
|
return
|
|
@@ -213,14 +228,6 @@ module Html2rss
|
|
|
213
228
|
log_info_fallback_zero_items(strategy:, next_strategy:, response:) if next_strategy
|
|
214
229
|
end
|
|
215
230
|
|
|
216
|
-
def deterministic_status?(response)
|
|
217
|
-
DETERMINISTIC_HTTP_STATUSES.member?(response&.status)
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def articles_for(response:, request_session:)
|
|
221
|
-
pipeline.deduplicated_articles(config:, response:, request_session:)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
231
|
# rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength -- success kwargs + structured selected log
|
|
225
232
|
def record_success(response:, strategy:, articles:, dedup_dropped:, admission_drops:, state:)
|
|
226
233
|
attempt_count = state.attempts.size
|
|
@@ -243,57 +250,46 @@ module Html2rss
|
|
|
243
250
|
end
|
|
244
251
|
|
|
245
252
|
def finalize_failure(state)
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
error = failure_error(state)
|
|
254
|
+
if error
|
|
248
255
|
error.attempts = state.attempts if error.is_a?(Html2rss::Error)
|
|
249
256
|
raise error
|
|
250
257
|
end
|
|
251
258
|
|
|
259
|
+
response = state.last_response
|
|
252
260
|
raise NoFeedItemsExtracted.new(
|
|
253
261
|
attempts: state.attempts,
|
|
254
|
-
surface_category: surface_category_for(
|
|
262
|
+
surface_category: (PageRecon.surface_category_for(response:, url: response.url) if response)
|
|
255
263
|
)
|
|
256
264
|
end
|
|
257
265
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
# Prefer a remembered timeout/blocked-surface over a later zero-item hop so
|
|
267
|
+
# callers do not cache EXTRACTION_EMPTY for a transport failure.
|
|
268
|
+
def failure_error(state)
|
|
269
|
+
return state.terminal_transport_error if state.terminal_transport_error
|
|
261
270
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def surface_category_for(response)
|
|
266
|
-
return unless response
|
|
271
|
+
error = state.last_error
|
|
272
|
+
return if error.nil? || error.is_a?(RequestService::BotasaurusConfigurationError)
|
|
267
273
|
|
|
268
|
-
|
|
274
|
+
error
|
|
269
275
|
end
|
|
270
276
|
|
|
271
|
-
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
|
|
277
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- shared payload then info/warn dispatch
|
|
272
278
|
def log_fallback_error(strategy:, next_strategy:, error:, request_session:)
|
|
273
279
|
host = request_session.url.host
|
|
274
280
|
detail = "host=#{host} elapsed=#{format('%.3f', budget.elapsed_seconds)}s " \
|
|
275
281
|
"budget_remaining=#{budget_remaining_label}"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Log.warn({
|
|
288
|
-
event_name: 'auto_fallback.error',
|
|
289
|
-
host:,
|
|
290
|
-
strategy: strategy.to_s,
|
|
291
|
-
next_strategy: next_strategy.to_s,
|
|
292
|
-
error_class: error.class.name,
|
|
293
|
-
message: "#{self.class}: auto fallback #{strategy} -> #{next_strategy} " \
|
|
294
|
-
"after error=#{error.class} #{detail}"
|
|
295
|
-
}.to_json)
|
|
296
|
-
end
|
|
282
|
+
timed_out = error.is_a?(RequestService::RequestTimedOut)
|
|
283
|
+
payload = {
|
|
284
|
+
event_name: timed_out ? 'auto_fallback.timeout' : 'auto_fallback.error',
|
|
285
|
+
host:,
|
|
286
|
+
strategy: strategy.to_s,
|
|
287
|
+
next_strategy: next_strategy.to_s,
|
|
288
|
+
error_class: error.class.name,
|
|
289
|
+
message: "#{self.class}: auto fallback #{strategy} -> #{next_strategy} " \
|
|
290
|
+
"after #{timed_out ? "timeout=#{error.class}" : "error=#{error.class}"} #{detail}"
|
|
291
|
+
}.to_json
|
|
292
|
+
timed_out ? Log.info(payload) : Log.warn(payload)
|
|
297
293
|
Log.debug("#{self.class}: strategy=#{strategy} error=#{error.class}: #{error.message} #{detail}")
|
|
298
294
|
end
|
|
299
295
|
|
|
@@ -21,9 +21,9 @@ module Html2rss
|
|
|
21
21
|
# combined with ping! on a reused closed selector that never decrements retries,
|
|
22
22
|
# that overflows the stack and kills Falcon workers.
|
|
23
23
|
RETRY_AFTER_SECONDS = 0.25
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# Base-session retries plugin defaults (stack-overflow guard for ping reconnects).
|
|
25
|
+
# Per-request +session.with+ sets +max_retries: 0+; Ruby owns TimeoutError retries
|
|
26
|
+
# against the monotonic deadline so HTTPX cannot stack read timeouts past the cap.
|
|
27
27
|
# Do not reload +:fiber_concurrency+: a reused closed selector made +ping!+
|
|
28
28
|
# skip decrementing retries and recurse until +SystemStackError+.
|
|
29
29
|
#
|
|
@@ -54,19 +54,19 @@ module Html2rss
|
|
|
54
54
|
def perform_execute
|
|
55
55
|
deadline = request_deadline
|
|
56
56
|
reset_redirect_tracking!
|
|
57
|
-
raw_response =
|
|
57
|
+
raw_response = request_with_deadline_retries(deadline:)
|
|
58
58
|
build_response(raw_response)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
private
|
|
62
62
|
|
|
63
|
-
# Caps one
|
|
64
|
-
#
|
|
63
|
+
# Caps one strategy execute (Ruby retries included) so a hanging :default tier
|
|
64
|
+
# cannot consume the full shared auto-fallback budget.
|
|
65
65
|
#
|
|
66
66
|
# @return [Float] monotonic deadline
|
|
67
67
|
def request_deadline
|
|
68
68
|
budget_timeout = ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
|
|
69
|
-
|
|
69
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) + [budget_timeout, attempt_timeout_ceiling].min
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# @return [Float] connect + read ceiling for a single attempt
|
|
@@ -81,8 +81,30 @@ module Html2rss
|
|
|
81
81
|
@current_url = request_url
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
# Retries HTTPX timeouts in Ruby while the shared monotonic deadline has time left.
|
|
85
|
+
# First try consumes the budget slot; retries do not.
|
|
86
|
+
def request_with_deadline_retries(deadline:)
|
|
87
|
+
state = { retries_remaining: MAX_RETRIES, consume_budget: true }
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
request_with_terminal_redirect_retry(deadline:, consume_budget: state[:consume_budget])
|
|
91
|
+
rescue HTTPX::TimeoutError => error
|
|
92
|
+
prepare_deadline_retry!(deadline:, state:, error:)
|
|
93
|
+
retry
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def prepare_deadline_retry!(deadline:, state:, error:)
|
|
98
|
+
raise error if state[:retries_remaining].zero?
|
|
99
|
+
|
|
100
|
+
remaining_timeout_seconds(deadline)
|
|
101
|
+
state[:retries_remaining] -= 1
|
|
102
|
+
state[:consume_budget] = false
|
|
103
|
+
reset_redirect_tracking!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def request_with_terminal_redirect_retry(deadline:, consume_budget: true)
|
|
107
|
+
raw_response = execute_http_request(deadline:, consume_budget:)
|
|
86
108
|
return raw_response unless redirect_limit_reached?(raw_response)
|
|
87
109
|
|
|
88
110
|
unless terminal_redirect_retryable?(raw_response)
|
|
@@ -164,6 +186,7 @@ module Html2rss
|
|
|
164
186
|
session.with(session_options(deadline))
|
|
165
187
|
end
|
|
166
188
|
|
|
189
|
+
# Plugin retries stay off per request; Ruby owns deadline-bounded TimeoutError retries.
|
|
167
190
|
def session_options(deadline)
|
|
168
191
|
remaining = remaining_timeout_seconds(deadline)
|
|
169
192
|
{
|
|
@@ -173,7 +196,7 @@ module Html2rss
|
|
|
173
196
|
max_response_body_size: ctx.policy.max_response_bytes,
|
|
174
197
|
resolver_class: :system,
|
|
175
198
|
redirect_on: redirect_callback,
|
|
176
|
-
**RETRY_OPTIONS
|
|
199
|
+
**RETRY_OPTIONS.merge(max_retries: 0)
|
|
177
200
|
}
|
|
178
201
|
end
|
|
179
202
|
|
|
@@ -219,16 +242,12 @@ module Html2rss
|
|
|
219
242
|
end
|
|
220
243
|
|
|
221
244
|
def remaining_timeout_seconds(deadline)
|
|
222
|
-
remaining = deadline -
|
|
245
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
223
246
|
raise RequestTimedOut, 'Request timed out' if remaining <= 0
|
|
224
247
|
|
|
225
248
|
remaining
|
|
226
249
|
end
|
|
227
250
|
|
|
228
|
-
def monotonic_now
|
|
229
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
251
|
def timeout_error?(error)
|
|
233
252
|
error.is_a?(HTTPX::TimeoutError) || super
|
|
234
253
|
end
|
data/lib/html2rss/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.32.
|
|
4
|
+
version: 0.32.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -478,7 +478,7 @@ licenses:
|
|
|
478
478
|
- MIT
|
|
479
479
|
metadata:
|
|
480
480
|
allowed_push_host: https://rubygems.org
|
|
481
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.32.
|
|
481
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.32.3
|
|
482
482
|
rubygems_mfa_required: 'true'
|
|
483
483
|
rdoc_options: []
|
|
484
484
|
require_paths:
|