relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.2
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/README.adoc +57 -1
- data/bin/console +0 -1
- data/lib/relaton/3gpp/bibliography.rb +82 -7
- data/lib/relaton/3gpp/data_fetcher.rb +51 -3
- data/lib/relaton/3gpp/docidentifier.rb +114 -0
- data/lib/relaton/3gpp/item.rb +6 -0
- data/lib/relaton/3gpp/parser.rb +1 -1
- data/lib/relaton/3gpp/processor.rb +4 -1
- data/lib/relaton/3gpp.rb +5 -1
- data/lib/relaton/adobe/bibdata.rb +8 -0
- data/lib/relaton/adobe/bibitem.rb +8 -0
- data/lib/relaton/adobe/bibliography.rb +92 -0
- data/lib/relaton/adobe/docidentifier.rb +49 -0
- data/lib/relaton/adobe/doctype.rb +14 -0
- data/lib/relaton/adobe/ext.rb +32 -0
- data/lib/relaton/adobe/item.rb +15 -0
- data/lib/relaton/adobe/item_base.rb +18 -0
- data/lib/relaton/adobe/item_data.rb +6 -0
- data/lib/relaton/adobe/processor.rb +45 -0
- data/lib/relaton/adobe/util.rb +8 -0
- data/lib/relaton/adobe.rb +37 -0
- data/lib/relaton/bib/model/address.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +24 -9
- data/lib/relaton/bib/model/localized_string.rb +1 -1
- data/lib/relaton/bib/model/structured_identifier.rb +10 -9
- data/lib/relaton/bib/sanitizer.rb +202 -6
- data/lib/relaton/bib.rb +0 -2
- data/lib/relaton/bipm/bibliography.rb +159 -10
- data/lib/relaton/bipm/data_fetcher.rb +26 -2
- data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
- data/lib/relaton/bipm/id_parser.rb +5 -4
- data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
- data/lib/relaton/bipm/processor.rb +2 -2
- data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
- data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
- data/lib/relaton/bipm.rb +7 -1
- data/lib/relaton/bsi/bibliography.rb +115 -43
- data/lib/relaton/bsi/hit.rb +14 -0
- data/lib/relaton/bsi/hit_collection.rb +15 -16
- data/lib/relaton/bsi/model/docidentifier.rb +99 -1
- data/lib/relaton/bsi/processor.rb +1 -0
- data/lib/relaton/calconnect/bibliography.rb +12 -14
- data/lib/relaton/calconnect/data_fetcher.rb +77 -9
- data/lib/relaton/calconnect/docidentifier.rb +80 -0
- data/lib/relaton/calconnect/hit_collection.rb +65 -57
- data/lib/relaton/calconnect/model/item.rb +7 -0
- data/lib/relaton/calconnect/processor.rb +7 -1
- data/lib/relaton/calconnect.rb +11 -1
- data/lib/relaton/ccsds/data/fetcher.rb +17 -12
- data/lib/relaton/ccsds/data/parser.rb +1 -1
- data/lib/relaton/ccsds/hit_collection.rb +6 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
- data/lib/relaton/ccsds/model/item.rb +2 -0
- data/lib/relaton/cen/bibliography.rb +75 -47
- data/lib/relaton/cen/hit.rb +16 -1
- data/lib/relaton/cen/hit_collection.rb +59 -11
- data/lib/relaton/cen/model/docidentifier.rb +92 -1
- data/lib/relaton/cen/processor.rb +13 -8
- data/lib/relaton/cen/scraper.rb +13 -5
- data/lib/relaton/cen.rb +1 -0
- data/lib/relaton/cie/data_fetcher.rb +215 -30
- data/lib/relaton/cie/processor.rb +3 -1
- data/lib/relaton/cie/scrapper.rb +15 -2
- data/lib/relaton/cie.rb +2 -1
- data/lib/relaton/core/data_fetcher.rb +150 -3
- data/lib/relaton/core/governor.rb +320 -0
- data/lib/relaton/core/pacer.rb +134 -0
- data/lib/relaton/core/processor.rb +19 -0
- data/lib/relaton/core/request_error.rb +14 -0
- data/lib/relaton/core.rb +3 -0
- data/lib/relaton/db/registry.rb +41 -1
- data/lib/relaton/doi/crossref.rb +19 -2
- data/lib/relaton/doi/parser.rb +109 -15
- data/lib/relaton/easc/bibdata.rb +8 -0
- data/lib/relaton/easc/bibitem.rb +8 -0
- data/lib/relaton/easc/bibliography.rb +95 -0
- data/lib/relaton/easc/docidentifier.rb +100 -0
- data/lib/relaton/easc/doctype.rb +14 -0
- data/lib/relaton/easc/ext.rb +44 -0
- data/lib/relaton/easc/item.rb +13 -0
- data/lib/relaton/easc/item_base.rb +18 -0
- data/lib/relaton/easc/item_data.rb +6 -0
- data/lib/relaton/easc/processor.rb +46 -0
- data/lib/relaton/easc/util.rb +8 -0
- data/lib/relaton/easc.rb +35 -0
- data/lib/relaton/ecma/bibliography.rb +93 -25
- data/lib/relaton/ecma/data_fetcher.rb +71 -12
- data/lib/relaton/ecma/docidentifier.rb +124 -0
- data/lib/relaton/ecma/item.rb +2 -0
- data/lib/relaton/ecma/memento_parser.rb +1 -1
- data/lib/relaton/ecma/page_fetcher.rb +15 -3
- data/lib/relaton/ecma/parser_common.rb +2 -2
- data/lib/relaton/ecma/processor.rb +4 -1
- data/lib/relaton/ecma/standard_parser.rb +2 -2
- data/lib/relaton/ecma.rb +10 -1
- data/lib/relaton/etsi/bibliography.rb +67 -2
- data/lib/relaton/etsi/data_fetcher.rb +43 -4
- data/lib/relaton/etsi/processor.rb +3 -1
- data/lib/relaton/etsi.rb +2 -1
- data/lib/relaton/gb/bibliography.rb +55 -29
- data/lib/relaton/gb/docidentifier.rb +58 -9
- data/lib/relaton/gb/processor.rb +3 -0
- data/lib/relaton/gb/scraper.rb +27 -10
- data/lib/relaton/gost/bibdata.rb +8 -0
- data/lib/relaton/gost/bibitem.rb +8 -0
- data/lib/relaton/gost/bibliography.rb +107 -0
- data/lib/relaton/gost/docidentifier.rb +80 -0
- data/lib/relaton/gost/doctype.rb +16 -0
- data/lib/relaton/gost/ext.rb +46 -0
- data/lib/relaton/gost/item.rb +15 -0
- data/lib/relaton/gost/item_base.rb +18 -0
- data/lib/relaton/gost/item_data.rb +6 -0
- data/lib/relaton/gost/processor.rb +49 -0
- data/lib/relaton/gost/util.rb +8 -0
- data/lib/relaton/gost.rb +36 -0
- data/lib/relaton/iala/bibdata.rb +8 -0
- data/lib/relaton/iala/bibitem.rb +8 -0
- data/lib/relaton/iala/bibliography.rb +146 -0
- data/lib/relaton/iala/docidentifier.rb +89 -0
- data/lib/relaton/iala/doctype.rb +18 -0
- data/lib/relaton/iala/ext.rb +32 -0
- data/lib/relaton/iala/item.rb +21 -0
- data/lib/relaton/iala/item_base.rb +18 -0
- data/lib/relaton/iala/item_data.rb +6 -0
- data/lib/relaton/iala/processor.rb +43 -0
- data/lib/relaton/iala/relation.rb +7 -0
- data/lib/relaton/iala/util.rb +8 -0
- data/lib/relaton/iala.rb +35 -0
- data/lib/relaton/iana/bibliography.rb +67 -14
- data/lib/relaton/iana/data_fetcher.rb +35 -5
- data/lib/relaton/iana/processor.rb +3 -1
- data/lib/relaton/iana.rb +12 -1
- data/lib/relaton/iec/data_fetcher.rb +7 -1
- data/lib/relaton/iec/hit_collection.rb +1 -1
- data/lib/relaton/iec/model/docidentifier.rb +9 -5
- data/lib/relaton/iec/model/ext.rb +2 -2
- data/lib/relaton/iec/processor.rb +1 -0
- data/lib/relaton/ieee/bibliography.rb +25 -3
- data/lib/relaton/ieee/data_fetcher.rb +158 -17
- data/lib/relaton/ieee/idams_parser.rb +18 -11
- data/lib/relaton/ieee/processor.rb +4 -1
- data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
- data/lib/relaton/ieee.rb +2 -1
- data/lib/relaton/ietf/data_fetcher.rb +295 -12
- data/lib/relaton/ietf/processor.rb +7 -3
- data/lib/relaton/ietf/rfc/entry.rb +39 -3
- data/lib/relaton/ietf/scraper.rb +69 -36
- data/lib/relaton/ietf.rb +4 -1
- data/lib/relaton/iho/bibliography.rb +1 -1
- data/lib/relaton/iho/docidentifier.rb +1 -1
- data/lib/relaton/index/file_io.rb +11 -11
- data/lib/relaton/index/file_storage.rb +6 -1
- data/lib/relaton/index/pool.rb +6 -1
- data/lib/relaton/index/shard_source.rb +201 -0
- data/lib/relaton/index/type.rb +63 -12
- data/lib/relaton/index.rb +2 -1
- data/lib/relaton/iso/bibliography.rb +20 -15
- data/lib/relaton/iso/data_fetcher.rb +3 -3
- data/lib/relaton/iso/data_parser.rb +17 -3
- data/lib/relaton/iso/hit_collection.rb +27 -15
- data/lib/relaton/iso/item_data.rb +22 -0
- data/lib/relaton/iso/model/docidentifier.rb +24 -12
- data/lib/relaton/iso/processor.rb +1 -0
- data/lib/relaton/iso/scraper.rb +19 -3
- data/lib/relaton/itu/bibliography.rb +9 -4
- data/lib/relaton/itu/data_crawler_r.rb +664 -0
- data/lib/relaton/itu/data_fetcher.rb +496 -50
- data/lib/relaton/itu/data_merge_r.rb +149 -0
- data/lib/relaton/itu/data_parser_r.rb +163 -89
- data/lib/relaton/itu/data_parser_t.rb +228 -0
- data/lib/relaton/itu/family_cache.rb +177 -0
- data/lib/relaton/itu/governor.rb +56 -0
- data/lib/relaton/itu/hit.rb +9 -3
- data/lib/relaton/itu/hit_collection.rb +258 -86
- data/lib/relaton/itu/model/docidentifier.rb +67 -1
- data/lib/relaton/itu/model/structured_identifier.rb +19 -0
- data/lib/relaton/itu/processor.rb +10 -4
- data/lib/relaton/itu/pubid.rb +27 -5
- data/lib/relaton/itu/recommendation_fields.rb +334 -0
- data/lib/relaton/itu/recommendation_parser.rb +18 -149
- data/lib/relaton/itu/scraper.rb +13 -3
- data/lib/relaton/itu.rb +2 -1
- data/lib/relaton/jcgm/bibdata.rb +8 -0
- data/lib/relaton/jcgm/bibitem.rb +8 -0
- data/lib/relaton/jcgm/bibliography.rb +97 -0
- data/lib/relaton/jcgm/data_fetcher.rb +81 -0
- data/lib/relaton/jcgm/docidentifier.rb +102 -0
- data/lib/relaton/jcgm/doctype.rb +12 -0
- data/lib/relaton/jcgm/ext.rb +23 -0
- data/lib/relaton/jcgm/item.rb +20 -0
- data/lib/relaton/jcgm/item_base.rb +18 -0
- data/lib/relaton/jcgm/item_data.rb +6 -0
- data/lib/relaton/jcgm/meetings_parser.rb +175 -0
- data/lib/relaton/jcgm/processor.rb +71 -0
- data/lib/relaton/jcgm/relation.rb +9 -0
- data/lib/relaton/jcgm/structured_identifier.rb +40 -0
- data/lib/relaton/jcgm/util.rb +8 -0
- data/lib/relaton/jcgm.rb +24 -0
- data/lib/relaton/jis/bibliography.rb +8 -10
- data/lib/relaton/jis/data_fetcher.rb +21 -19
- data/lib/relaton/jis/docidentifier.rb +104 -5
- data/lib/relaton/jis/hit.rb +18 -23
- data/lib/relaton/jis/hit_collection.rb +19 -18
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -3
- data/lib/relaton/logger/channels/gh_issue.rb +78 -13
- data/lib/relaton/nist/data_fetcher.rb +63 -13
- data/lib/relaton/nist/docidentifier.rb +165 -0
- data/lib/relaton/nist/item.rb +2 -0
- data/lib/relaton/nist/item_base.rb +16 -0
- data/lib/relaton/nist/mods_parser.rb +38 -12
- data/lib/relaton/nist/processor.rb +2 -1
- data/lib/relaton/nist/relation.rb +3 -0
- data/lib/relaton/nist/scraper.rb +6 -3
- data/lib/relaton/oasis/bibliography.rb +147 -6
- data/lib/relaton/oasis/data_fetcher.rb +41 -5
- data/lib/relaton/oasis/data_parser_utils.rb +37 -3
- data/lib/relaton/oasis/docidentifier.rb +54 -0
- data/lib/relaton/oasis/item.rb +3 -0
- data/lib/relaton/oasis/processor.rb +7 -1
- data/lib/relaton/oasis.rb +14 -1
- data/lib/relaton/ogc/data_fetcher.rb +23 -2
- data/lib/relaton/ogc/docidentifier.rb +105 -0
- data/lib/relaton/ogc/hit_collection.rb +78 -3
- data/lib/relaton/ogc/processor.rb +2 -1
- data/lib/relaton/ogc.rb +5 -1
- data/lib/relaton/oiml/bibliography.rb +90 -15
- data/lib/relaton/oiml/docidentifier.rb +18 -3
- data/lib/relaton/omg/docidentifier.rb +67 -0
- data/lib/relaton/omg/item.rb +1 -0
- data/lib/relaton/omg/processor.rb +1 -0
- data/lib/relaton/omg/scraper.rb +61 -16
- data/lib/relaton/omg.rb +1 -0
- data/lib/relaton/plateau/bibliography.rb +10 -3
- data/lib/relaton/plateau/data_fetcher.rb +25 -2
- data/lib/relaton/plateau/handbook_parser.rb +8 -1
- data/lib/relaton/plateau/hit.rb +10 -2
- data/lib/relaton/plateau/hit_collection.rb +31 -11
- data/lib/relaton/plateau/processor.rb +3 -1
- data/lib/relaton/plateau/technical_report_parser.rb +8 -1
- data/lib/relaton/plateau.rb +2 -1
- data/lib/relaton/sdo/config.rb +34 -0
- data/lib/relaton/sdo/fetcher.rb +52 -0
- data/lib/relaton/sdo/logo.rb +95 -0
- data/lib/relaton/sdo/name.rb +26 -0
- data/lib/relaton/sdo/organization.rb +71 -0
- data/lib/relaton/sdo/store.rb +49 -0
- data/lib/relaton/sdo.rb +29 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +132 -12
- data/lib/relaton/w3c/data_fetcher.rb +194 -16
- data/lib/relaton/w3c/data_parser.rb +3 -3
- data/lib/relaton/w3c/docidentifier.rb +48 -0
- data/lib/relaton/w3c/governor.rb +32 -0
- data/lib/relaton/w3c/item.rb +3 -0
- data/lib/relaton/w3c/pubid.rb +12 -0
- data/lib/relaton/w3c/safe_realize.rb +110 -21
- data/lib/relaton/w3c.rb +12 -1
- data/lib/relaton/xsf/bibliography.rb +61 -1
- data/lib/relaton/xsf/data_fetcher.rb +55 -5
- data/lib/relaton/xsf/docidentifier.rb +46 -0
- data/lib/relaton/xsf/hit_collection.rb +31 -3
- data/lib/relaton/xsf/item.rb +6 -0
- data/lib/relaton/xsf/processor.rb +1 -0
- data/lib/relaton/xsf.rb +5 -1
- data/lib/relaton.rb +42 -0
- metadata +135 -24
- data/lib/relaton/ieee/pub_id.rb +0 -161
- data/lib/relaton/index/id_number.rb +0 -30
|
@@ -3,7 +3,6 @@ require "w3c_api"
|
|
|
3
3
|
require_relative "../w3c"
|
|
4
4
|
require_relative "safe_realize"
|
|
5
5
|
require_relative "data_parser"
|
|
6
|
-
require_relative "pubid"
|
|
7
6
|
|
|
8
7
|
module Relaton
|
|
9
8
|
module W3c
|
|
@@ -27,6 +26,48 @@ module Relaton
|
|
|
27
26
|
# (rate-limit/connection) before giving up and aborting the crawl.
|
|
28
27
|
PAGE_FETCH_ATTEMPTS = 3
|
|
29
28
|
|
|
29
|
+
# How many resources may be lost to rate limiting before the dataset is
|
|
30
|
+
# considered too incomplete to save. Deliberately low: crawls are bimodal
|
|
31
|
+
# — a healthy run loses none (Aug 17 2026: 0), a rate-limited one loses
|
|
32
|
+
# hundreds (Aug 19 2026: 1,412) — so there is no middle ground to protect.
|
|
33
|
+
# Genuinely broken resources (permanent skips from 404s/5xx) are counted
|
|
34
|
+
# separately and stay tolerated.
|
|
35
|
+
DEFAULT_MAX_THROTTLED_LOSSES = 25
|
|
36
|
+
|
|
37
|
+
# Retry policy pushed into w3c_api. Its defaults spend ~31s (1+2+4+8+16)
|
|
38
|
+
# inside lutaml-hal before a 429 ever reaches us, which just delays the
|
|
39
|
+
# shared back-pressure that actually works; this compresses the same chain
|
|
40
|
+
# to ~11s (0.5+1+2+4+4) so the Governor can open its pool-wide cooldown
|
|
41
|
+
# while the ban window is still young.
|
|
42
|
+
#
|
|
43
|
+
# `max_retries` deliberately stays at 5. lutaml-hal applies one retry count
|
|
44
|
+
# to both 429 and 5xx, and unlike a 429 a 5xx is still a *permanent* skip
|
|
45
|
+
# here — cutting its attempts would blacklist a burst of resources on a
|
|
46
|
+
# brief upstream wobble that the old policy rode out, and `skipped` is not
|
|
47
|
+
# counted by guard_throttle_budget, so that loss would be invisible.
|
|
48
|
+
#
|
|
49
|
+
# `max_retry_after` matches `max_delay` because a server-instructed wait
|
|
50
|
+
# must not outlive the compressed chain either. Left unset, lutaml-hal's
|
|
51
|
+
# DEFAULT_MAX_RETRY_AFTER (300 s) applies, and a large Retry-After on a
|
|
52
|
+
# 429 parks one thread inside the HTTP layer for up to 5 x 300 s with the
|
|
53
|
+
# governor hearing nothing — the very thing the compression prevents. The
|
|
54
|
+
# hint is not discarded, it moves to where it belongs: Governor.retry_after
|
|
55
|
+
# reads it off the same error and uses it as the FLOOR of the pool-wide
|
|
56
|
+
# cooldown, under the governor's own 900 s cap.
|
|
57
|
+
UPSTREAM_RATE_LIMITING = {
|
|
58
|
+
max_retries: 5, base_delay: 0.5, max_delay: 4.0, backoff_factor: 2.0,
|
|
59
|
+
max_retry_after: 4.0
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# Ceiling on resources dropped to rate limiting before the crawl refuses
|
|
63
|
+
# to save. Tunable via env var for a deliberately partial run; 0 is a
|
|
64
|
+
# meaningful setting (tolerate nothing), so it is accepted here unlike in
|
|
65
|
+
# the governor's duration knobs.
|
|
66
|
+
def self.max_throttled_losses
|
|
67
|
+
value = ENV["RELATON_W3C_MAX_THROTTLED"].to_s.strip
|
|
68
|
+
value.match?(/\A\d+\z/) ? value.to_i : DEFAULT_MAX_THROTTLED_LOSSES
|
|
69
|
+
end
|
|
70
|
+
|
|
30
71
|
# Number of fetch_spec worker threads. Tunable via env var so CI or
|
|
31
72
|
# local runs can dial it up for speed or down to lighten load on
|
|
32
73
|
# api.w3.org (or for debugging).
|
|
@@ -52,8 +93,14 @@ module Relaton
|
|
|
52
93
|
@interrupted = false
|
|
53
94
|
end
|
|
54
95
|
|
|
96
|
+
# The index is pubid-backed: `pubid_class:` makes Relaton::Index sort the
|
|
97
|
+
# rows by `id.root.number` on save and serialize each id to its
|
|
98
|
+
# `_type: pubid:w3c:*` hash. Without it a v1-shaped file would be written
|
|
99
|
+
# under a v2 name, with no error and no narrowing.
|
|
55
100
|
def index
|
|
56
|
-
@index ||= Relaton::Index.find_or_create(
|
|
101
|
+
@index ||= Relaton::Index.find_or_create(
|
|
102
|
+
:W3C, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::W3c::Identifier
|
|
103
|
+
)
|
|
57
104
|
end
|
|
58
105
|
|
|
59
106
|
def log_error(msg)
|
|
@@ -75,6 +122,8 @@ module Relaton
|
|
|
75
122
|
# of everything fetched so far is saved rather than the run being lost.
|
|
76
123
|
#
|
|
77
124
|
def fetch(_source = nil)
|
|
125
|
+
SafeRealize.reset!
|
|
126
|
+
configure_upstream_rate_limiting
|
|
78
127
|
n_workers = self.class.concurrency
|
|
79
128
|
queue = SizedQueue.new(n_workers * 4)
|
|
80
129
|
workers = Array.new(n_workers) { spawn_worker(queue) }
|
|
@@ -89,13 +138,52 @@ module Relaton
|
|
|
89
138
|
n_workers.times { queue << nil } # poison pills
|
|
90
139
|
workers.each(&:join)
|
|
91
140
|
end
|
|
92
|
-
|
|
141
|
+
if @interrupted
|
|
142
|
+
# Ctrl-C means "give me what you have", so the completeness guards
|
|
143
|
+
# below are deliberately not applied to an interrupted crawl.
|
|
144
|
+
Util.warn "Crawl interrupted — saving progress collected so far."
|
|
145
|
+
else
|
|
146
|
+
guard_rate_limited
|
|
147
|
+
guard_throttle_budget
|
|
148
|
+
end
|
|
93
149
|
index.save
|
|
94
150
|
end
|
|
95
151
|
|
|
96
152
|
report_errors
|
|
97
153
|
end
|
|
98
154
|
|
|
155
|
+
# Abort when the governor concluded we are banned rather than briefly
|
|
156
|
+
# throttled. Everything fetched so far is discarded on purpose: a partial
|
|
157
|
+
# dataset must never be saved (crawler.rb wipes data/ first, so saving one
|
|
158
|
+
# commits mass deletions).
|
|
159
|
+
def guard_rate_limited
|
|
160
|
+
return unless SafeRealize.governor.exhausted?
|
|
161
|
+
|
|
162
|
+
# #give_up_rounds, not #throttle_rounds: the latter is live and a
|
|
163
|
+
# straggler success resets it, which is how a give-up at round 5 got
|
|
164
|
+
# reported as "after 2 consecutive backoffs".
|
|
165
|
+
rounds = SafeRealize.governor.give_up_rounds ||
|
|
166
|
+
SafeRealize.governor.throttle_rounds
|
|
167
|
+
raise CrawlIncompleteError,
|
|
168
|
+
"crawl is rate-limited: gave up after " \
|
|
169
|
+
"#{rounds} consecutive backoffs " \
|
|
170
|
+
"(#{SafeRealize.governor.throttle_count} rate-limited responses); " \
|
|
171
|
+
"refusing to save a partial dataset"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The per-resource counterpart to guard_complete_pagination: pagination can
|
|
175
|
+
# complete cleanly while rate limiting has quietly hollowed out the
|
|
176
|
+
# documents behind it.
|
|
177
|
+
def guard_throttle_budget
|
|
178
|
+
lost = SafeRealize.throttled.size
|
|
179
|
+
budget = self.class.max_throttled_losses
|
|
180
|
+
return if lost <= budget
|
|
181
|
+
|
|
182
|
+
raise CrawlIncompleteError,
|
|
183
|
+
"#{lost} resources were dropped to rate limiting (budget #{budget}); " \
|
|
184
|
+
"refusing to save a partial dataset"
|
|
185
|
+
end
|
|
186
|
+
|
|
99
187
|
#
|
|
100
188
|
# Page through the specifications index, feeding each spec (paired with
|
|
101
189
|
# its embedded page) to the worker queue. Returns early when interrupted.
|
|
@@ -113,11 +201,11 @@ module Relaton
|
|
|
113
201
|
loop do
|
|
114
202
|
page = specs
|
|
115
203
|
page.links.specifications.each do |spec|
|
|
116
|
-
break if
|
|
204
|
+
break if stopping?
|
|
117
205
|
|
|
118
206
|
queue << [spec, page]
|
|
119
207
|
end
|
|
120
|
-
break if
|
|
208
|
+
break if stopping?
|
|
121
209
|
|
|
122
210
|
last_page = page.page
|
|
123
211
|
break unless page.next?
|
|
@@ -133,6 +221,11 @@ module Relaton
|
|
|
133
221
|
# `break`ing prevents a rate-limit blip from silently truncating the
|
|
134
222
|
# dataset: a partial crawl must never be saved/committed.
|
|
135
223
|
unless next_page
|
|
224
|
+
# When the governor gave up, the page didn't fail — we stopped
|
|
225
|
+
# asking. Let guard_rate_limited report that with the accurate
|
|
226
|
+
# reason instead of dressing it up as a pagination fault.
|
|
227
|
+
break if SafeRealize.governor.exhausted?
|
|
228
|
+
|
|
136
229
|
raise CrawlIncompleteError,
|
|
137
230
|
"specifications pagination stopped at page #{page.page}: " \
|
|
138
231
|
"failed to fetch page #{page.page + 1}"
|
|
@@ -141,11 +234,29 @@ module Relaton
|
|
|
141
234
|
specs = next_page
|
|
142
235
|
end
|
|
143
236
|
|
|
144
|
-
return if
|
|
237
|
+
return if stopping?
|
|
145
238
|
|
|
146
239
|
guard_complete_pagination(last_page, expected_pages)
|
|
147
240
|
end
|
|
148
241
|
|
|
242
|
+
# Reasons to stop feeding the queue: a Ctrl-C (save what we have) or the
|
|
243
|
+
# governor concluding the crawl is banned (abort). Both need the producer
|
|
244
|
+
# to stop; #fetch decides which of the two it was.
|
|
245
|
+
def stopping?
|
|
246
|
+
@interrupted || SafeRealize.governor.exhausted?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# The mid-spec back-out, and deliberately NOT #stopping?. The two stop
|
|
250
|
+
# reasons want opposite things of a spec already in flight:
|
|
251
|
+
#
|
|
252
|
+
# - Ctrl-C means "give me what you have". The index IS saved, so the
|
|
253
|
+
# in-flight spec must finish, or a half-parsed document is written.
|
|
254
|
+
# - A latched give-up means the run is lost — guard_rate_limited raises
|
|
255
|
+
# and nothing is saved — and the host has banned us. Abandon at once.
|
|
256
|
+
def abandoned?
|
|
257
|
+
SafeRealize.governor.exhausted?
|
|
258
|
+
end
|
|
259
|
+
|
|
149
260
|
# Defense in depth: even when no page fetch raised, make sure pagination
|
|
150
261
|
# actually reached the last page the API advertised. Catches truncation
|
|
151
262
|
# modes other than a failed fetch (e.g. a `next` link that goes missing).
|
|
@@ -164,6 +275,10 @@ module Relaton
|
|
|
164
275
|
# parent_resource serves the spec from embedded data (no HTTP request).
|
|
165
276
|
spec = realize(unrealized_spec, parent_resource: page)
|
|
166
277
|
return unless spec
|
|
278
|
+
# The latch can land while this spec is in flight. Nothing this crawl
|
|
279
|
+
# produces is saved any more, so stop before the parse and the write
|
|
280
|
+
# too, not only before the requests.
|
|
281
|
+
return if abandoned?
|
|
167
282
|
|
|
168
283
|
local_errors = Hash.new(true)
|
|
169
284
|
save_doc DataParser.parse(spec, local_errors)
|
|
@@ -209,14 +324,41 @@ module Relaton
|
|
|
209
324
|
if @files.include?(file)
|
|
210
325
|
Util.warn "File #{file} already exists. Document: #{bib.docnumber}" if warn_duplicate
|
|
211
326
|
else
|
|
212
|
-
|
|
213
|
-
index.add_or_update pubid.to_hash, file
|
|
327
|
+
index_primary bib.docidentifier&.detect(&:primary), file
|
|
214
328
|
@files << file
|
|
215
329
|
end
|
|
216
330
|
File.write file, serialize(bib), encoding: "UTF-8"
|
|
217
331
|
end
|
|
218
332
|
end
|
|
219
333
|
|
|
334
|
+
#
|
|
335
|
+
# Add a document's primary id to the index.
|
|
336
|
+
#
|
|
337
|
+
# Every W3C id is expected to parse. One that does not is recorded in
|
|
338
|
+
# `@errors`, so the inherited `report_errors` logs it at ERROR and raises
|
|
339
|
+
# a tracked GitHub issue at the end of the crawl, and the row is skipped
|
|
340
|
+
# rather than indexed unparsed: `Relaton::Index` rejects the WHOLE index
|
|
341
|
+
# if a single row fails to deserialize, and its sort calls
|
|
342
|
+
# `.root.number` on every id. The data file is still written, so the
|
|
343
|
+
# document is unindexed, never lost.
|
|
344
|
+
#
|
|
345
|
+
# Keyed by the ID rather than the output path: the id is the defect, the
|
|
346
|
+
# file is only where it landed, and one broken id reaching several files
|
|
347
|
+
# must still report once.
|
|
348
|
+
#
|
|
349
|
+
# @param [Docidentifier, nil] docid the document's primary identifier
|
|
350
|
+
# @param [String] file path the document was written to
|
|
351
|
+
#
|
|
352
|
+
def index_primary(docid, file)
|
|
353
|
+
if docid&.pubid
|
|
354
|
+
index.add_or_update docid.pubid, file
|
|
355
|
+
return
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
id = docid&.content || file
|
|
359
|
+
@errors[id.to_s] = "Unparseable primary id `#{id}` was not indexed (#{file})"
|
|
360
|
+
end
|
|
361
|
+
|
|
220
362
|
def to_xml(bib)
|
|
221
363
|
bib.to_xml(bibdata: true)
|
|
222
364
|
end
|
|
@@ -262,15 +404,30 @@ module Relaton
|
|
|
262
404
|
# embedded_data is populated. Transient 403/5xx/connection failures are
|
|
263
405
|
# already retried upstream (w3c_api/lutaml-hal), but losing an index page
|
|
264
406
|
# drops every spec on it, so retry a few more times here with backoff to
|
|
265
|
-
# ride out a brief
|
|
266
|
-
#
|
|
267
|
-
#
|
|
407
|
+
# ride out a brief wobble. Rate limiting is handled separately, by the
|
|
408
|
+
# governor. Returns nil once the attempts are exhausted (or the crawl is
|
|
409
|
+
# declared banned); the caller turns that into a CrawlIncompleteError so
|
|
410
|
+
# the crawl aborts instead of committing a truncated dataset.
|
|
268
411
|
def fetch_specifications_page(number)
|
|
269
412
|
attempt = 0
|
|
270
413
|
begin
|
|
271
|
-
|
|
272
|
-
client.specifications(embed: true, page: number)
|
|
414
|
+
SafeRealize.governor.wait
|
|
415
|
+
page = client.specifications(embed: true, page: number)
|
|
416
|
+
SafeRealize.governor.succeeded!
|
|
417
|
+
page
|
|
418
|
+
rescue *Governor::THROTTLE_ERRORS => e
|
|
419
|
+
# How long to keep trying a rate-limited page is the governor's call,
|
|
420
|
+
# not this counter's: PAGE_FETCH_ATTEMPTS would give up after ~3
|
|
421
|
+
# minutes and report a *pagination* fault, hiding the real reason and
|
|
422
|
+
# pre-empting the escalation ladder. Retry until the governor declares
|
|
423
|
+
# the crawl banned; #wait above paces each attempt, and returns
|
|
424
|
+
# immediately once it has, so this cannot spin.
|
|
425
|
+
log_error "Rate-limited fetching specifications page #{number}: #{e.message}"
|
|
426
|
+
return nil if SafeRealize.governor.throttled!(retry_after: Governor.retry_after(e))
|
|
427
|
+
|
|
428
|
+
retry
|
|
273
429
|
rescue Lutaml::Hal::Error, Faraday::Error => e
|
|
430
|
+
attempt += 1
|
|
274
431
|
log_error "Failed to fetch specifications page #{number} " \
|
|
275
432
|
"(attempt #{attempt}/#{PAGE_FETCH_ATTEMPTS}): " \
|
|
276
433
|
"#{e.class}: #{e.message}"
|
|
@@ -282,12 +439,33 @@ module Relaton
|
|
|
282
439
|
end
|
|
283
440
|
end
|
|
284
441
|
|
|
442
|
+
# Point w3c_api's rate limiter at UPSTREAM_RATE_LIMITING. This mutates
|
|
443
|
+
# w3c_api's process-wide singleton and is not restored afterwards — the
|
|
444
|
+
# crawler is a standalone script, and a shorter inner retry is the right
|
|
445
|
+
# setting for any caller that has the governor in front of it anyway.
|
|
446
|
+
# Best-effort: the setter is w3c_api's only public knob and is not part of
|
|
447
|
+
# a stable contract, so losing it must not fail the run.
|
|
448
|
+
def configure_upstream_rate_limiting
|
|
449
|
+
hal = W3cApi::Hal.instance
|
|
450
|
+
return unless hal.respond_to?(:configure_rate_limiting)
|
|
451
|
+
|
|
452
|
+
# w3c_api >= 0.3.3 cascades this through reset_client -> rebuild_register
|
|
453
|
+
# itself, so the register no longer goes on serving the old policy.
|
|
454
|
+
# Do NOT add a reset_register call after this: rebuild_register
|
|
455
|
+
# re-registers eagerly on purpose (Link#realize raises when the name is
|
|
456
|
+
# absent from lutaml-hal's GlobalRegister), and tearing it back down
|
|
457
|
+
# would break realize for every model fetched before this point.
|
|
458
|
+
hal.configure_rate_limiting(UPSTREAM_RATE_LIMITING)
|
|
459
|
+
rescue StandardError => e
|
|
460
|
+
Util.warn "Could not configure w3c_api rate limiting: #{e.class}: #{e.message}"
|
|
461
|
+
end
|
|
462
|
+
|
|
285
463
|
def spawn_worker(queue)
|
|
286
464
|
Thread.new do
|
|
287
465
|
while (item = queue.pop)
|
|
288
|
-
# Once
|
|
289
|
-
#
|
|
290
|
-
next if
|
|
466
|
+
# Once stopping, drain the queue without processing so the producer
|
|
467
|
+
# unblocks and the pool reaches its poison pills quickly.
|
|
468
|
+
next if stopping?
|
|
291
469
|
|
|
292
470
|
spec, page = item
|
|
293
471
|
begin
|
|
@@ -126,11 +126,11 @@ module Relaton
|
|
|
126
126
|
#
|
|
127
127
|
# Parse docidentifier
|
|
128
128
|
#
|
|
129
|
-
# @return [Array<
|
|
129
|
+
# @return [Array<Docidentifier>] docidentifier
|
|
130
130
|
#
|
|
131
131
|
def parse_docid
|
|
132
132
|
id = pub_id(doc_uri)
|
|
133
|
-
[
|
|
133
|
+
[Docidentifier.new(type: "W3C", content: id, primary: true)]
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
#
|
|
@@ -371,7 +371,7 @@ module Relaton
|
|
|
371
371
|
Bib::Organization.new(
|
|
372
372
|
name: [Bib::TypedLocalizedString.new(content: "World Wide Web Consortium")],
|
|
373
373
|
abbreviation: Bib::LocalizedString.new(content: "W3C"),
|
|
374
|
-
uri: Bib::Uri.new(content: "https://www.w3.org"),
|
|
374
|
+
uri: [Bib::Uri.new(content: "https://www.w3.org")],
|
|
375
375
|
)
|
|
376
376
|
end
|
|
377
377
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module W3c
|
|
3
|
+
# A W3C document identifier that carries its parsed pubid alongside the
|
|
4
|
+
# source string.
|
|
5
|
+
#
|
|
6
|
+
# `DataParser#pub_id` builds `"W3C REC-xml-names-20091208"` — publisher
|
|
7
|
+
# prefix included — which is exactly the form
|
|
8
|
+
# `Pubid::W3c::Identifier.parse` accepts, so nothing has to synthesize or
|
|
9
|
+
# strip a prefix. `DataFetcher#index_primary` takes `#pubid` straight from
|
|
10
|
+
# here to build the `index-v2` rows.
|
|
11
|
+
#
|
|
12
|
+
# Follows the IHO shape rather than the ISO one: `content=` calls `super`
|
|
13
|
+
# first, so `content` keeps the source string verbatim and serialization is
|
|
14
|
+
# unchanged. ISO re-renders `content` from the pubid, which W3C has no need
|
|
15
|
+
# of and which would alter its published output.
|
|
16
|
+
class Docidentifier < Bib::Docidentifier
|
|
17
|
+
attr_reader :pubid
|
|
18
|
+
|
|
19
|
+
def content=(value)
|
|
20
|
+
super
|
|
21
|
+
@pubid = value && parse(value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# W3C identifiers carry no part, date-as-component or all-parts notion, so
|
|
25
|
+
# the three mutators `Bib::ItemData` broadcasts over every docidentifier
|
|
26
|
+
# are no-ops here rather than the inherited NotImplementedError.
|
|
27
|
+
def remove_part!; end
|
|
28
|
+
|
|
29
|
+
def remove_date!; end
|
|
30
|
+
|
|
31
|
+
def to_all_parts!; end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# An identifier that does not parse is a data defect, so it is reported at
|
|
36
|
+
# ERROR — never at WARN. It does not raise: an already-published record
|
|
37
|
+
# still has to deserialize and render. The crawl escalates the same
|
|
38
|
+
# failure into a tracked GitHub issue (see
|
|
39
|
+
# `DataFetcher#index_primary` and `Core::DataFetcher#report_errors`).
|
|
40
|
+
def parse(value)
|
|
41
|
+
::Pubid::W3c::Identifier.parse value.to_s
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
Util.error "Failed to parse pubid `#{value}`: #{e.message}"
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/hal"
|
|
4
|
+
require_relative "../core/governor"
|
|
5
|
+
|
|
6
|
+
module Relaton
|
|
7
|
+
module W3c
|
|
8
|
+
#
|
|
9
|
+
# W3C binding of {Relaton::Core::Governor}.
|
|
10
|
+
#
|
|
11
|
+
# The ladder (60 s → 900 s), the per-round escalation, the jitter, the
|
|
12
|
+
# latched give-up and the Retry-After parsing all live in core, where the
|
|
13
|
+
# ITU-R crawler shares them. What is W3C-specific is exactly two things:
|
|
14
|
+
# which errors mean "rate-limited", and which env vars tune the durations.
|
|
15
|
+
#
|
|
16
|
+
class Governor < Relaton::Core::Governor
|
|
17
|
+
# Errors that mean "you are being rate-limited", as opposed to "this
|
|
18
|
+
# resource is broken". api.w3.org signals throttling with **403 as well as
|
|
19
|
+
# 429** — w3c_api's whole faraday-retry layer is built around 403 being the
|
|
20
|
+
# W3C rate-limit signal — so a 403 must never be recorded as a permanently
|
|
21
|
+
# broken resource either. Requires lutaml-hal >= 0.2.5, which gave 403 its
|
|
22
|
+
# own ForbiddenError class (before that it arrived as a bare Error).
|
|
23
|
+
THROTTLE_ERRORS = [
|
|
24
|
+
::Lutaml::Hal::TooManyRequestsError,
|
|
25
|
+
::Lutaml::Hal::ForbiddenError,
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# RELATON_W3C_THROTTLE_BASE / _MAX / _GIVEUP.
|
|
29
|
+
ENV_PREFIX = "RELATON_W3C"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/relaton/w3c/item.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "item_data"
|
|
2
2
|
require_relative "ext"
|
|
3
|
+
require_relative "docidentifier"
|
|
3
4
|
|
|
4
5
|
module Relaton
|
|
5
6
|
module W3c
|
|
@@ -7,6 +8,8 @@ module Relaton
|
|
|
7
8
|
model ItemData
|
|
8
9
|
|
|
9
10
|
attribute :ext, Ext
|
|
11
|
+
attribute :docidentifier, Docidentifier, collection: true,
|
|
12
|
+
initialize_empty: true
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
end
|
data/lib/relaton/w3c/pubid.rb
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
module Relaton
|
|
2
2
|
module W3c
|
|
3
|
+
# The legacy `index-v1` row shape.
|
|
4
|
+
#
|
|
5
|
+
# This is no longer on any lookup or crawl path in this gem: the flavor
|
|
6
|
+
# indexes and searches on `Pubid::W3c::Identifier` (see
|
|
7
|
+
# `Relaton::W3c::Docidentifier` and `INDEXFILE`). It is retained public
|
|
8
|
+
# only so `relaton-data-w3c`'s crawler can keep emitting the bespoke
|
|
9
|
+
# `index-v1` that released relaton v2 consumers read — the same reason
|
|
10
|
+
# `Relaton::Bipm::Id` was retained.
|
|
11
|
+
#
|
|
12
|
+
# Do not add call sites. Its decomposition differs from pubid's: it splits
|
|
13
|
+
# `year` from `date` and keeps a `/`-suffix in `suff`, where pubid folds
|
|
14
|
+
# both into the slug.
|
|
3
15
|
class PubId
|
|
4
16
|
PARTS = %i[code stage type year date suff].freeze
|
|
5
17
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "concurrent/map"
|
|
2
|
+
require_relative "governor"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
4
5
|
module W3c
|
|
@@ -7,10 +8,22 @@ module Relaton
|
|
|
7
8
|
# resources that failed terminally and returns nil for them — so one broken
|
|
8
9
|
# link doesn't abort the crawl and isn't re-fetched on every reference.
|
|
9
10
|
#
|
|
10
|
-
# Transient failures are retried upstream: w3c_api retries
|
|
11
|
+
# Transient failures are retried upstream first: w3c_api retries 403 (the
|
|
11
12
|
# W3C rate-limit signal) and connection/timeout errors, and lutaml-hal
|
|
12
|
-
# retries 429 and 5xx. By the time an error surfaces here
|
|
13
|
+
# retries 429 and 5xx. By the time an error surfaces here the resource is
|
|
14
|
+
# either genuinely broken (skipped for the rest of the run) or we are still
|
|
15
|
+
# being rate-limited (handed to the Governor).
|
|
16
|
+
#
|
|
17
|
+
# That distinction is the point: a 429 or 403 means "come back later", not
|
|
18
|
+
# "this document does not exist". Recording one as a permanent skip is what
|
|
19
|
+
# silently cost the Aug-2026 relaton-data-w3c crawl 1,412 documents.
|
|
13
20
|
module SafeRealize
|
|
21
|
+
# How many times a rate-limited realize is re-attempted, each behind the
|
|
22
|
+
# governor's shared cooldown, before the resource is written off for this
|
|
23
|
+
# run. Three spans the escalating cooldowns without letting one unlucky
|
|
24
|
+
# href monopolise a worker.
|
|
25
|
+
THROTTLE_ATTEMPTS = 3
|
|
26
|
+
|
|
14
27
|
# Hrefs that failed terminally — one map shared by every includer
|
|
15
28
|
# (DataFetcher and DataParser) since a broken resource is broken for the
|
|
16
29
|
# whole crawl. Initialized eagerly (at load, single-threaded) so the
|
|
@@ -18,8 +31,26 @@ module Relaton
|
|
|
18
31
|
# Concurrent::Map then handles the concurrent reads/writes.
|
|
19
32
|
@skipped = Concurrent::Map.new
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
# Hrefs lost to rate limiting rather than to a broken resource. Kept apart
|
|
35
|
+
# from @skipped because they measure how incomplete the crawl is, not a
|
|
36
|
+
# property of the resource: DataFetcher refuses to save an index once too
|
|
37
|
+
# many pile up.
|
|
38
|
+
@throttled = Concurrent::Map.new
|
|
39
|
+
|
|
40
|
+
# Shared back-pressure for the whole crawl (see Governor).
|
|
41
|
+
@governor = Governor.new
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
attr_reader :skipped, :throttled
|
|
45
|
+
attr_accessor :governor
|
|
46
|
+
|
|
47
|
+
# Start a crawl (or a spec example) from a clean slate. The governor is
|
|
48
|
+
# reset in place, so a reference taken before the reset stays live.
|
|
49
|
+
def reset!
|
|
50
|
+
@skipped.clear
|
|
51
|
+
@throttled.clear
|
|
52
|
+
@governor.reset!
|
|
53
|
+
end
|
|
23
54
|
end
|
|
24
55
|
|
|
25
56
|
# @param parent_resource [Object, nil] the index/page the link came from.
|
|
@@ -27,30 +58,88 @@ module Relaton
|
|
|
27
58
|
# payload lets the link realize from memory instead of issuing an HTTP
|
|
28
59
|
# request. nil (the default) preserves the plain remote-fetch behavior.
|
|
29
60
|
def realize(obj, parent_resource: nil)
|
|
61
|
+
# Once the governor has latched its give-up the run is already lost
|
|
62
|
+
# (DataFetcher#guard_rate_limited raises and nothing is saved) and the
|
|
63
|
+
# host has just banned us, so every further request only extends the
|
|
64
|
+
# ban into the next run. #wait alone stops WAITING, not asking: the
|
|
65
|
+
# 2026-08-31 crawl spent its last 11 minutes firing at a banned host
|
|
66
|
+
# because the per-spec fan-out had no back-pressure of its own.
|
|
67
|
+
#
|
|
68
|
+
# This is the one choke point every request in the crawl passes
|
|
69
|
+
# through, so one check covers DataFetcher#fetch_versions and all
|
|
70
|
+
# seven of DataParser's realize sites. They already tolerate nil —
|
|
71
|
+
# that is the SafeRealize contract.
|
|
72
|
+
return nil if SafeRealize.governor.exhausted?
|
|
73
|
+
|
|
30
74
|
href = resolve_href(obj)
|
|
31
75
|
return nil if SafeRealize.skipped.key?(href)
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
attempt = 0
|
|
78
|
+
begin
|
|
79
|
+
attempt += 1
|
|
80
|
+
SafeRealize.governor.wait
|
|
81
|
+
realized = obj.realize(parent_resource: parent_resource)
|
|
82
|
+
# Only a realize that actually went to the network says anything about
|
|
83
|
+
# the limiter. With a parent_resource, lutaml-hal serves the object
|
|
84
|
+
# straight out of the page's `_embedded` payload and never issues a
|
|
85
|
+
# request (link.rb: "Priority 1: check embedded content first") — and
|
|
86
|
+
# that is the common case, one per specification. Reporting those as
|
|
87
|
+
# successes would clear the cooldown and reset the escalation ladder
|
|
88
|
+
# between every pair of version-history 429s, so the governor could
|
|
89
|
+
# never reach its give-up threshold: exactly the 60s re-trip loop this
|
|
90
|
+
# class exists to break.
|
|
91
|
+
SafeRealize.governor.succeeded! unless parent_resource
|
|
92
|
+
# The document made it into the dataset after all, so it must stop
|
|
93
|
+
# counting against the crawl's throttle budget.
|
|
94
|
+
SafeRealize.throttled.delete(href)
|
|
95
|
+
realized
|
|
96
|
+
rescue *Governor::THROTTLE_ERRORS => e
|
|
97
|
+
retry if record_throttle(href, e, attempt)
|
|
98
|
+
nil
|
|
99
|
+
rescue Lutaml::Hal::ConnectionError, Lutaml::Hal::TimeoutError, Faraday::Error, Net::OpenTimeout => e
|
|
100
|
+
# Network-level failure (already retried by w3c_api). The resource itself
|
|
101
|
+
# is fine, so don't skip it permanently — a later reference can try again.
|
|
102
|
+
Util.warn "Failed to realize object: #{href}, error: #{e.message}"
|
|
103
|
+
nil
|
|
104
|
+
rescue Lutaml::Hal::NotFoundError
|
|
105
|
+
Util.warn "Object not found: #{href}"
|
|
106
|
+
SafeRealize.skipped[href] = true
|
|
107
|
+
nil
|
|
108
|
+
rescue Lutaml::Hal::Error => e
|
|
109
|
+
# Definitive upstream error (5xx, 400/401, …) already retried by
|
|
110
|
+
# w3c_api / lutaml-hal. Skip the broken/unavailable resource rather than
|
|
111
|
+
# re-hitting it for every link that references it. 5xx stays here
|
|
112
|
+
# deliberately: the handful of persistent per-resource 500s a healthy
|
|
113
|
+
# crawl sees are broken records, not throttling, and routing them
|
|
114
|
+
# through the governor would open a pool-wide cooldown for each one.
|
|
115
|
+
Util.warn "Skipping #{href}, upstream error after retries: #{e.message}"
|
|
116
|
+
SafeRealize.skipped[href] = true
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
50
119
|
end
|
|
51
120
|
|
|
52
121
|
private
|
|
53
122
|
|
|
123
|
+
#
|
|
124
|
+
# Record a rate-limited attempt and decide whether to try again.
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] true to retry — the caller re-enters #realize's begin
|
|
127
|
+
# block, whose #wait sits out the shared cooldown first
|
|
128
|
+
#
|
|
129
|
+
def record_throttle(href, error, attempt)
|
|
130
|
+
gave_up = SafeRealize.governor.throttled!(
|
|
131
|
+
retry_after: Governor.retry_after(error),
|
|
132
|
+
)
|
|
133
|
+
# Once the governor has declared the crawl rate-limited there is nothing
|
|
134
|
+
# to gain from more attempts — the run is about to abort anyway.
|
|
135
|
+
return true if !gave_up && attempt < THROTTLE_ATTEMPTS
|
|
136
|
+
|
|
137
|
+
Util.warn "Throttled out: #{href}, still rate-limited after " \
|
|
138
|
+
"#{attempt} attempt(s): #{error.message}"
|
|
139
|
+
SafeRealize.throttled[href] = true
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
54
143
|
def resolve_href(obj)
|
|
55
144
|
obj.href || obj.links.self.href
|
|
56
145
|
end
|
data/lib/relaton/w3c.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "pubid"
|
|
2
|
+
require "pubid/w3c"
|
|
1
3
|
require "relaton/bib"
|
|
2
4
|
require "relaton/index"
|
|
3
5
|
require_relative "version"
|
|
@@ -6,10 +8,19 @@ require_relative "w3c/item"
|
|
|
6
8
|
require_relative "w3c/bibitem"
|
|
7
9
|
require_relative "w3c/bibdata"
|
|
8
10
|
require_relative "w3c/bibliography"
|
|
11
|
+
# Not used by this gem any more; loaded so relaton-data-w3c's crawler can
|
|
12
|
+
# require "relaton/w3c" and still reach the legacy index-v1 row shape.
|
|
13
|
+
require_relative "w3c/pubid"
|
|
9
14
|
|
|
10
15
|
module Relaton
|
|
11
16
|
module W3c
|
|
12
|
-
|
|
17
|
+
# The runtime index is the pubid-backed `index-v2`, whose rows are
|
|
18
|
+
# `Pubid::W3c::Identifier` leaves serialized as `_type: pubid:w3c:*`, built
|
|
19
|
+
# and read via `pubid_class: ::Pubid::W3c::Identifier`. The bespoke
|
|
20
|
+
# `index-v1` is relaton-v2 legacy and is no longer produced here —
|
|
21
|
+
# `relaton-data-w3c`'s crawler emits it from the retained public
|
|
22
|
+
# `Relaton::W3c::PubId`.
|
|
23
|
+
INDEXFILE = "index-v2".freeze
|
|
13
24
|
|
|
14
25
|
class Error < StandardError; end
|
|
15
26
|
|