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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time" # Time.parse, for an HTTP-date Retry-After
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Core
|
|
7
|
+
#
|
|
8
|
+
# Process-wide, thread-safe rate-limit back-pressure for a crawl.
|
|
9
|
+
#
|
|
10
|
+
# Promoted here from `Relaton::W3c::Governor`, which is now a thin binding
|
|
11
|
+
# of it, when the ITU-R crawler became the second flavor to need
|
|
12
|
+
# cross-thread throttling. What stays with a flavor is only which errors
|
|
13
|
+
# mean "throttled" (THROTTLE_ERRORS / .throttle?) and which env vars tune it
|
|
14
|
+
# (ENV_PREFIX); the ladder, the jitter, the give-up latch and the
|
|
15
|
+
# Retry-After parsing are shared.
|
|
16
|
+
#
|
|
17
|
+
# The problem it solves, as first met on api.w3.org: that host sits behind
|
|
18
|
+
# Cloudflare, whose rate limiting is bimodal — a run either never trips it
|
|
19
|
+
# or trips it and stays banned for hours. Before this
|
|
20
|
+
# existed, each of the crawl's workers reacted to its own 429s in isolation
|
|
21
|
+
# with a backoff capped at ~31s — far too short to outlast the ban, and the
|
|
22
|
+
# retries themselves kept the limiter engaged. The Aug-2026 crawl spent 4h56m
|
|
23
|
+
# doing nothing but re-tripping the limiter, permanently discarding 1,412
|
|
24
|
+
# documents, before it aborted.
|
|
25
|
+
#
|
|
26
|
+
# The governor gives the whole worker pool one shared cooldown. When any
|
|
27
|
+
# thread sees a 429 it opens (or extends) that cooldown; every other thread
|
|
28
|
+
# observes it in #wait. Cooldowns escalate per *round* — 60s, 120s, ... to a
|
|
29
|
+
# 15-minute cap — and after GIVE_UP_AFTER consecutive rounds without a single
|
|
30
|
+
# success the crawl is declared rate-limited and gives up (~15 min) so CI
|
|
31
|
+
# fails fast instead of grinding for five hours.
|
|
32
|
+
#
|
|
33
|
+
# `clock:` and `sleeper:` are injectable so specs exercise the real
|
|
34
|
+
# arithmetic without wall-clock waits.
|
|
35
|
+
#
|
|
36
|
+
class Governor
|
|
37
|
+
# First cooldown, in seconds. Deliberately minute-scale: a Cloudflare ban
|
|
38
|
+
# window outlasts any second-scale backoff.
|
|
39
|
+
BASE_COOLDOWN = 60
|
|
40
|
+
|
|
41
|
+
# Ceiling for a single cooldown, including a server-supplied Retry-After
|
|
42
|
+
# (which must not be able to park a worker for an hour).
|
|
43
|
+
MAX_COOLDOWN = 900
|
|
44
|
+
|
|
45
|
+
# Consecutive throttle rounds with no intervening success before the crawl
|
|
46
|
+
# is considered banned. With the ladder above that is ~15 min of cooldown
|
|
47
|
+
# (60+120+240+480 s; the fifth round latches before its own wait).
|
|
48
|
+
GIVE_UP_AFTER = 5
|
|
49
|
+
|
|
50
|
+
# Fraction of the cooldown added as random jitter, so the workers do not
|
|
51
|
+
# all resume on the same instant and immediately re-trip the limiter.
|
|
52
|
+
JITTER_FRACTION = 0.1
|
|
53
|
+
|
|
54
|
+
# Which errors mean "you are being rate-limited", as opposed to "this
|
|
55
|
+
# resource is broken". Core ships none: the distinction is the one thing
|
|
56
|
+
# only the flavor knows, and getting it wrong in either direction is
|
|
57
|
+
# expensive — a broken resource routed here opens a pool-wide cooldown
|
|
58
|
+
# for one bad record, and a throttle routed the other way gets recorded
|
|
59
|
+
# as permanent data loss (which is exactly what the W3C crawl did before
|
|
60
|
+
# this class existed). Subclasses override the constant, or #throttle?
|
|
61
|
+
# when the signal is a status code rather than an exception class.
|
|
62
|
+
THROTTLE_ERRORS = [].freeze
|
|
63
|
+
|
|
64
|
+
# Prefix for the three duration knobs, so two flavors crawling in one
|
|
65
|
+
# process cannot share a cooldown ladder by accident. A subclass sets its
|
|
66
|
+
# own ("RELATON_W3C", "RELATON_ITU"); the suffixes are fixed.
|
|
67
|
+
ENV_PREFIX = "RELATON"
|
|
68
|
+
|
|
69
|
+
# Whether this error means the server is rate-limiting us.
|
|
70
|
+
#
|
|
71
|
+
# Class membership is the common case. A flavor whose server signals
|
|
72
|
+
# throttling with a *status code* on a shared exception class (ITU: 503 on
|
|
73
|
+
# /rec) overrides this instead of the constant.
|
|
74
|
+
#
|
|
75
|
+
# @param error [StandardError]
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def self.throttle?(error)
|
|
78
|
+
self::THROTTLE_ERRORS.any? { |klass| error.is_a? klass }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
attr_reader :base, :max, :give_up_after, :env_prefix
|
|
82
|
+
|
|
83
|
+
def initialize(base: nil, max: nil, give_up_after: nil,
|
|
84
|
+
clock: nil, sleeper: nil, jitter: nil, env_prefix: nil)
|
|
85
|
+
@env_prefix = env_prefix || self.class::ENV_PREFIX
|
|
86
|
+
# Remembered so #reset! can re-read the env knobs for the next crawl
|
|
87
|
+
# without discarding values a caller passed explicitly.
|
|
88
|
+
@overrides = { base: base, max: max, give_up_after: give_up_after }
|
|
89
|
+
load_limits
|
|
90
|
+
@clock = clock || -> { Time.now }
|
|
91
|
+
@sleeper = sleeper || ->(secs) { sleep secs }
|
|
92
|
+
@jitter = jitter || ->(secs) { Kernel.rand * secs * self.class::JITTER_FRACTION }
|
|
93
|
+
@mutex = Mutex.new
|
|
94
|
+
@cooldown_until = nil
|
|
95
|
+
@penalty = nil
|
|
96
|
+
@rounds = 0
|
|
97
|
+
@events = 0
|
|
98
|
+
@abandoned = false
|
|
99
|
+
@abandoned_rounds = nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#
|
|
103
|
+
# Block until the shared cooldown expires. Called before every request.
|
|
104
|
+
#
|
|
105
|
+
# The mutex is never held across the sleep — the deadline is read under
|
|
106
|
+
# the lock and the sleeping happens outside it, so a cooling-down worker
|
|
107
|
+
# cannot block another thread from recording its own throttle.
|
|
108
|
+
#
|
|
109
|
+
# @return [Numeric] seconds actually slept (0.0 when there was nothing to
|
|
110
|
+
# wait for, including once the crawl has given up)
|
|
111
|
+
#
|
|
112
|
+
def wait
|
|
113
|
+
slept = 0.0
|
|
114
|
+
waited_for = nil
|
|
115
|
+
loop do
|
|
116
|
+
# Once we have given up there is nothing left to wait for, and stalling
|
|
117
|
+
# here would delay the shutdown by up to a full cooldown.
|
|
118
|
+
deadline, remaining = @mutex.synchronize do
|
|
119
|
+
gave_up? ? [nil, 0.0] : [@cooldown_until, cooldown_remaining]
|
|
120
|
+
end
|
|
121
|
+
break unless remaining.positive?
|
|
122
|
+
# Re-check after sleeping so a cooldown *extended* mid-round (a later
|
|
123
|
+
# Retry-After asking for longer) also holds back the workers already
|
|
124
|
+
# sleeping — they would otherwise wake on the stale deadline and
|
|
125
|
+
# re-trip the limiter. Looping only on a later deadline keeps a stalled
|
|
126
|
+
# clock from spinning here.
|
|
127
|
+
break if waited_for && deadline && deadline <= waited_for
|
|
128
|
+
|
|
129
|
+
waited_for = deadline
|
|
130
|
+
delay = remaining + @jitter.call(remaining)
|
|
131
|
+
@sleeper.call(delay)
|
|
132
|
+
slept += delay
|
|
133
|
+
end
|
|
134
|
+
slept
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
#
|
|
138
|
+
# Record a 429 and open/extend the shared cooldown.
|
|
139
|
+
#
|
|
140
|
+
# @param retry_after [Integer, nil] seconds from the response's Retry-After
|
|
141
|
+
#
|
|
142
|
+
# @return [Boolean] true when the crawl should give up
|
|
143
|
+
#
|
|
144
|
+
def throttled!(retry_after: nil)
|
|
145
|
+
@mutex.synchronize do
|
|
146
|
+
now = @clock.call
|
|
147
|
+
@events += 1
|
|
148
|
+
if @cooldown_until.nil? || now >= @cooldown_until
|
|
149
|
+
open_round(now, retry_after)
|
|
150
|
+
elsif retry_after
|
|
151
|
+
# Mid-round, the server may still ask for longer than we planned.
|
|
152
|
+
extend_round(now + capped(retry_after))
|
|
153
|
+
end
|
|
154
|
+
# Giving up latches. Otherwise a straggler worker succeeding after the
|
|
155
|
+
# producer already stopped paginating would clear @rounds and let the
|
|
156
|
+
# crawl save the truncated index it just decided to abandon.
|
|
157
|
+
#
|
|
158
|
+
# @rounds is snapshotted at the same moment, because #succeeded!
|
|
159
|
+
# resets it: read afterwards it names whatever has re-accumulated
|
|
160
|
+
# since, not the threshold that was crossed. The abort message is
|
|
161
|
+
# the only place an operator learns why the crawl stopped.
|
|
162
|
+
unless @abandoned
|
|
163
|
+
@abandoned = @rounds >= @give_up_after
|
|
164
|
+
@abandoned_rounds = @rounds if @abandoned
|
|
165
|
+
end
|
|
166
|
+
@abandoned
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
#
|
|
171
|
+
# Record a successful request: the limiter has released us.
|
|
172
|
+
#
|
|
173
|
+
def succeeded!
|
|
174
|
+
@mutex.synchronize do
|
|
175
|
+
@rounds = 0
|
|
176
|
+
@cooldown_until = nil
|
|
177
|
+
# Decay rather than reset, so a limiter that keeps flapping still
|
|
178
|
+
# climbs the ladder instead of restarting at `base` every time.
|
|
179
|
+
@penalty = @penalty && @penalty > @base ? [@penalty / 2.0, @base].max : nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @return [Boolean] give_up_after consecutive rounds passed with no success
|
|
184
|
+
def exhausted?
|
|
185
|
+
@mutex.synchronize { gave_up? }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Clear all state for a fresh crawl. Resets in place rather than handing
|
|
189
|
+
# back a new object so callers (and specs) holding this instance keep
|
|
190
|
+
# observing the live governor.
|
|
191
|
+
def reset!
|
|
192
|
+
@mutex.synchronize do
|
|
193
|
+
load_limits
|
|
194
|
+
@cooldown_until = nil
|
|
195
|
+
@penalty = nil
|
|
196
|
+
@rounds = 0
|
|
197
|
+
@events = 0
|
|
198
|
+
@abandoned = false
|
|
199
|
+
@abandoned_rounds = nil
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [Integer] consecutive throttle rounds since the last success
|
|
204
|
+
def throttle_rounds
|
|
205
|
+
@mutex.synchronize { @rounds }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The consecutive-round count as it stood when the give-up latched, for
|
|
209
|
+
# the abort message. Distinct from #throttle_rounds, which stays LIVE
|
|
210
|
+
# because a crawler logs it per retry as the current round (see
|
|
211
|
+
# Relaton::Itu::DataCrawlerR) and #succeeded! resets it.
|
|
212
|
+
#
|
|
213
|
+
# @return [Integer, nil] nil while the crawl is still running
|
|
214
|
+
def give_up_rounds
|
|
215
|
+
@mutex.synchronize { @abandoned_rounds }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [Integer] every 429 observed, for reporting
|
|
219
|
+
def throttle_count
|
|
220
|
+
@mutex.synchronize { @events }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
#
|
|
224
|
+
# Seconds requested by a rate-limited response's Retry-After header.
|
|
225
|
+
#
|
|
226
|
+
# lutaml-hal >= 0.2.5 carries a `{ status:, headers: }` hash on every error
|
|
227
|
+
# mapped from a response (nil for locally raised ones), so anything without
|
|
228
|
+
# one yields nil.
|
|
229
|
+
#
|
|
230
|
+
# @param error [StandardError]
|
|
231
|
+
#
|
|
232
|
+
# @return [Integer, nil]
|
|
233
|
+
#
|
|
234
|
+
def self.retry_after(error)
|
|
235
|
+
return nil unless error.respond_to?(:response)
|
|
236
|
+
|
|
237
|
+
response = error.response
|
|
238
|
+
headers = response.is_a?(Hash) ? (response[:headers] || response["headers"]) : nil
|
|
239
|
+
return nil unless headers.respond_to?(:[])
|
|
240
|
+
|
|
241
|
+
seconds_from headers["retry-after"] || headers["Retry-After"]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
#
|
|
245
|
+
# Retry-After is either delta-seconds or an HTTP-date (RFC 9110). The date
|
|
246
|
+
# form is parsed properly rather than by scanning digits, which would pick
|
|
247
|
+
# up the day-of-month and back off for a wildly wrong duration; a date
|
|
248
|
+
# already in the past yields 0, i.e. "no extra floor".
|
|
249
|
+
#
|
|
250
|
+
def self.seconds_from(value)
|
|
251
|
+
str = value.to_s.strip
|
|
252
|
+
return nil if str.empty?
|
|
253
|
+
return str.to_i if str.match?(/\A\d+\z/)
|
|
254
|
+
|
|
255
|
+
begin
|
|
256
|
+
[(Time.parse(str) - Time.now).ceil, 0].max
|
|
257
|
+
rescue ArgumentError
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# A blank or non-positive override is treated as unset.
|
|
263
|
+
def self.env_int(name, default)
|
|
264
|
+
value = ENV[name].to_s.strip
|
|
265
|
+
return default if value.empty?
|
|
266
|
+
|
|
267
|
+
parsed = value.to_i
|
|
268
|
+
parsed.positive? ? parsed : default
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
# "<PREFIX>_THROTTLE_<SUFFIX>", the one thing ENV_PREFIX exists for.
|
|
274
|
+
def env_name(suffix)
|
|
275
|
+
"#{@env_prefix}_THROTTLE_#{suffix}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Read the duration knobs, letting explicit constructor args win over the
|
|
279
|
+
# environment. Re-read on #reset! so the env vars behave like a flavor's
|
|
280
|
+
# sibling knobs, which are class methods evaluated per crawl rather than
|
|
281
|
+
# frozen when the flavor was autoloaded.
|
|
282
|
+
def load_limits
|
|
283
|
+
@base = @overrides[:base] || self.class.env_int(env_name("BASE"), self.class::BASE_COOLDOWN)
|
|
284
|
+
@max = @overrides[:max] || self.class.env_int(env_name("MAX"), self.class::MAX_COOLDOWN)
|
|
285
|
+
@give_up_after = @overrides[:give_up_after] ||
|
|
286
|
+
self.class.env_int(env_name("GIVEUP"), self.class::GIVE_UP_AFTER)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Start a new escalation step. Retry-After raises the floor but never
|
|
290
|
+
# lowers our own ladder — the server's hint is a minimum, not a discount.
|
|
291
|
+
def open_round(now, retry_after)
|
|
292
|
+
@rounds += 1
|
|
293
|
+
ladder = @penalty ? [@penalty * 2, @max].min : @base
|
|
294
|
+
hint = retry_after ? capped(retry_after) : 0
|
|
295
|
+
@penalty = [ladder, hint].max
|
|
296
|
+
@cooldown_until = now + @penalty
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def extend_round(deadline)
|
|
300
|
+
@cooldown_until = deadline if deadline > @cooldown_until
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def cooldown_remaining
|
|
304
|
+
return 0.0 unless @cooldown_until
|
|
305
|
+
|
|
306
|
+
[@cooldown_until - @clock.call, 0.0].max
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def capped(seconds)
|
|
310
|
+
[seconds, @max].min
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Callers must already hold the mutex. Latched by #throttled! — never
|
|
314
|
+
# recomputed from @rounds, which #succeeded! resets.
|
|
315
|
+
def gave_up?
|
|
316
|
+
@abandoned
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Core
|
|
5
|
+
#
|
|
6
|
+
# A process-wide, thread-safe request pacer: the whole worker pool between
|
|
7
|
+
# them starts **at most one request per `gap` seconds**.
|
|
8
|
+
#
|
|
9
|
+
# It exists because the two natural alternatives are both wrong for a
|
|
10
|
+
# crawler that has to be polite to one host:
|
|
11
|
+
#
|
|
12
|
+
# * `sleep delay` before each request (what `Relaton::Itu::DataCrawlerR`
|
|
13
|
+
# did) **adds** the delay to the server's own latency. At ITU's measured
|
|
14
|
+
# ~1.1 s response time a 1 s delay yields one request every ~2.1 s, so a
|
|
15
|
+
# ~7k-request crawl spends ~2 h of a ~4 h run doing nothing at all — and
|
|
16
|
+
# the crawl is pinned to one thread, because the pacing *is* the loop.
|
|
17
|
+
# * a per-worker gap (`Relaton::Cie::DataFetcher::Pacing`) lets N workers
|
|
18
|
+
# issue N requests per gap. That is right for CIE, where the point is to
|
|
19
|
+
# let one struggling worker slow itself without dragging the others down,
|
|
20
|
+
# and wrong here, where the budget belongs to the host.
|
|
21
|
+
#
|
|
22
|
+
# This reserves a *slot* instead: each caller takes the next free instant on
|
|
23
|
+
# a shared timeline and sleeps only until it arrives. The server's latency
|
|
24
|
+
# therefore counts toward the gap rather than being added on top, so the
|
|
25
|
+
# rate the host sees is exactly `1 / gap` no matter how many workers there
|
|
26
|
+
# are — and workers exist only to hide latency, never to raise the rate.
|
|
27
|
+
#
|
|
28
|
+
# The mutex is held for the reservation and **never across the sleep**, so a
|
|
29
|
+
# waiting worker cannot stop another from reserving.
|
|
30
|
+
#
|
|
31
|
+
# Idle time does not bank up slots: a reservation never starts earlier than
|
|
32
|
+
# "now", so a stall elsewhere in the crawl (a governor cooldown, a slow
|
|
33
|
+
# merge) cannot be repaid afterwards as a burst — which is exactly the burst
|
|
34
|
+
# that would re-trip the limiter that caused the stall.
|
|
35
|
+
#
|
|
36
|
+
# `clock:` and `sleeper:` are injectable so specs exercise the arithmetic
|
|
37
|
+
# without wall-clock waits (mirroring `Relaton::Core::Governor`).
|
|
38
|
+
#
|
|
39
|
+
class Pacer
|
|
40
|
+
# How the gap is spent.
|
|
41
|
+
#
|
|
42
|
+
# :slot (default) the gap is the minimum interval between request
|
|
43
|
+
# *starts*, shared by the pool, so the host's own latency counts
|
|
44
|
+
# toward it. Peak rate 1/gap regardless of worker count.
|
|
45
|
+
# :fixed sleep the whole gap before every request, whatever else is
|
|
46
|
+
# happening. This is what a plain `sleep delay` loop did, kept
|
|
47
|
+
# as a rollback knob: with one worker it reproduces the old
|
|
48
|
+
# request pattern exactly — one request per (gap + latency).
|
|
49
|
+
DEFAULT_MODE = :slot
|
|
50
|
+
|
|
51
|
+
# @param gap [Numeric] minimum seconds between request *starts*. Zero
|
|
52
|
+
# disables pacing entirely (used by specs and by cassette replay).
|
|
53
|
+
# @param mode [Symbol] :slot or :fixed, see DEFAULT_MODE
|
|
54
|
+
# @param clock [#call, nil] returns monotonic seconds
|
|
55
|
+
# @param sleeper [#call, nil] receives seconds to sleep
|
|
56
|
+
def initialize(gap:, mode: DEFAULT_MODE, clock: nil, sleeper: nil)
|
|
57
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
58
|
+
@sleeper = sleeper || ->(secs) { sleep secs }
|
|
59
|
+
@mutex = Mutex.new
|
|
60
|
+
@gap = coerce_gap(gap)
|
|
61
|
+
@mode = mode == :fixed ? :fixed : DEFAULT_MODE
|
|
62
|
+
@next = nil
|
|
63
|
+
@requests = 0
|
|
64
|
+
@slept = 0.0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :gap, :mode
|
|
68
|
+
|
|
69
|
+
# @return [Integer] requests paced so far, for the run summary
|
|
70
|
+
def requests
|
|
71
|
+
@mutex.synchronize { @requests }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Float] total seconds spent waiting, for the run summary
|
|
75
|
+
def slept
|
|
76
|
+
@mutex.synchronize { @slept }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param secs [Numeric] the new minimum gap; takes effect on the next
|
|
80
|
+
# reservation. A negative value is clamped to zero.
|
|
81
|
+
def gap=(secs)
|
|
82
|
+
@mutex.synchronize { @gap = coerce_gap(secs) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Clear the timeline and the counters, so the next caller goes out
|
|
86
|
+
# immediately. For specs, and for a crawl starting a fresh phase.
|
|
87
|
+
def reset!
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
@next = nil
|
|
90
|
+
@requests = 0
|
|
91
|
+
@slept = 0.0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# Reserve the next slot and block until it arrives.
|
|
97
|
+
#
|
|
98
|
+
# @return [Float] seconds actually slept (0.0 when the slot was already
|
|
99
|
+
# due, which is the common case once the host is slower than the gap)
|
|
100
|
+
#
|
|
101
|
+
def wait
|
|
102
|
+
delay = @mode == :fixed ? @mutex.synchronize { @gap } : (reserve - @clock.call)
|
|
103
|
+
@mutex.synchronize { @requests += 1 }
|
|
104
|
+
return 0.0 unless delay.positive?
|
|
105
|
+
|
|
106
|
+
@sleeper.call delay
|
|
107
|
+
@mutex.synchronize { @slept += delay }
|
|
108
|
+
delay
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# Claim the next instant on the shared timeline and advance it. Held under
|
|
114
|
+
# the lock; the waiting happens outside.
|
|
115
|
+
#
|
|
116
|
+
# @return [Float] the instant this caller may start its request
|
|
117
|
+
def reserve
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
now = @clock.call
|
|
120
|
+
# `max(now)` is what stops an idle stretch from banking up slots: the
|
|
121
|
+
# timeline catches up to the present rather than paying out a burst.
|
|
122
|
+
@next = @next.nil? || @next < now ? now : @next
|
|
123
|
+
reserved = @next
|
|
124
|
+
@next += @gap
|
|
125
|
+
reserved
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def coerce_gap(secs)
|
|
130
|
+
[secs.to_f, 0.0].max
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -13,6 +13,25 @@ module Relaton
|
|
|
13
13
|
# @return [Array<String>]
|
|
14
14
|
attr_reader :datasets
|
|
15
15
|
|
|
16
|
+
# Global document-ID prefixes this flavor owns, e.g. BSI => %w[BS BSI DD …].
|
|
17
|
+
# The source of truth is **pubid**: a flavor sets @pubid_flavor to its Pubid
|
|
18
|
+
# module name (e.g. :Iso) in #initialize, and the prefixes are read from
|
|
19
|
+
# `Pubid::<Flavor>.prefixes` — the SDO's own leading identifier tokens,
|
|
20
|
+
# including non-obvious ones (BSI `DD`) and joint forms (`ISO/IEC`). Flavors
|
|
21
|
+
# with no pubid backing fall back to the single canonical @prefix. Loaded
|
|
22
|
+
# lazily (pubid is only required on first call) and memoized. Feeds the
|
|
23
|
+
# global prefix register (Relaton.prefix_flavor). (relaton-db#103)
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<String>]
|
|
26
|
+
def prefixes
|
|
27
|
+
@prefixes ||= if @pubid_flavor
|
|
28
|
+
require "pubid"
|
|
29
|
+
::Pubid.const_get(@pubid_flavor).prefixes
|
|
30
|
+
else
|
|
31
|
+
Array(prefix)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
16
35
|
def initialize
|
|
17
36
|
raise "This is an abstract class!"
|
|
18
37
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
# Transport failure while fetching remote data; `Relaton::Db#net_retry`
|
|
3
|
+
# retries it. It depends on nothing, so `relaton/index` requires this file
|
|
4
|
+
# alone and does not load the rest of `relaton/core`.
|
|
5
|
+
#
|
|
6
|
+
# The class is named `Relaton::RequestError`, not `Relaton::Core::...`: an
|
|
7
|
+
# error raised without a message uses the class name as its message, and
|
|
8
|
+
# relaton-cli prints that message.
|
|
9
|
+
class RequestError < StandardError; end
|
|
10
|
+
|
|
11
|
+
module Core
|
|
12
|
+
RequestError = Relaton::RequestError
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/relaton/core.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "relaton/logger"
|
|
2
2
|
require_relative "version"
|
|
3
|
+
require_relative "core/request_error"
|
|
3
4
|
require_relative "core/array_wrapper"
|
|
4
5
|
require_relative "core/hash_keys_sybolizer"
|
|
5
6
|
require_relative "core/date_parser"
|
|
@@ -7,3 +8,5 @@ require_relative "core/processor"
|
|
|
7
8
|
require_relative "core/data_fetcher"
|
|
8
9
|
require_relative "core/hit_collection"
|
|
9
10
|
require_relative "core/workers_pool"
|
|
11
|
+
require_relative "core/pacer"
|
|
12
|
+
require_relative "core/governor"
|
data/lib/relaton/db/registry.rb
CHANGED
|
@@ -11,7 +11,8 @@ module Relaton
|
|
|
11
11
|
relaton/bsi relaton/cen relaton/iana relaton/3gpp
|
|
12
12
|
relaton/oasis relaton/doi relaton/jis relaton/xsf
|
|
13
13
|
relaton/ccsds relaton/etsi relaton/isbn relaton/plateau
|
|
14
|
-
relaton/oiml
|
|
14
|
+
relaton/oiml relaton/jcgm relaton/easc relaton/gost relaton/adobe
|
|
15
|
+
relaton/iala
|
|
15
16
|
].freeze
|
|
16
17
|
|
|
17
18
|
include Singleton
|
|
@@ -107,8 +108,47 @@ module Relaton
|
|
|
107
108
|
nil
|
|
108
109
|
end
|
|
109
110
|
|
|
111
|
+
#
|
|
112
|
+
# Global prefix register (relaton-db#103): all processors that own the
|
|
113
|
+
# given global prefix, matched case-insensitively and exactly (unlike
|
|
114
|
+
# #class_by_ref, which matches a prefix at the start of a full reference).
|
|
115
|
+
# Ordered by registration order (SUPPORTED_GEMS), so results are
|
|
116
|
+
# deterministic. Lazy: never dereferences a flavor constant.
|
|
117
|
+
#
|
|
118
|
+
# @param prefix [String]
|
|
119
|
+
# @return [Array<Relaton::Core::Processor>]
|
|
120
|
+
#
|
|
121
|
+
def processors_by_prefix(prefix)
|
|
122
|
+
key = prefix.to_s.strip.upcase
|
|
123
|
+
processors.values.select do |processor|
|
|
124
|
+
processor.prefixes.any? { |pref| pref.upcase == key }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#
|
|
129
|
+
# Flavor modules (e.g. Relaton::Iso) that own the given global prefix.
|
|
130
|
+
# NOTE: dereferencing the returned module forces that flavor's lazy load.
|
|
131
|
+
# Callers that must stay lazy should use #processors_by_prefix instead.
|
|
132
|
+
#
|
|
133
|
+
# @param prefix [String]
|
|
134
|
+
# @return [Array<Module>]
|
|
135
|
+
#
|
|
136
|
+
def flavors_by_prefix(prefix)
|
|
137
|
+
processors_by_prefix(prefix).map { |processor| flavor_module(processor) }
|
|
138
|
+
end
|
|
139
|
+
|
|
110
140
|
private
|
|
111
141
|
|
|
142
|
+
# The flavor namespace for a processor: the module enclosing its class.
|
|
143
|
+
# Relaton::Iso::Processor -> Relaton::Iso. Derived from the class name so
|
|
144
|
+
# there's no @short string-munging and no 3gpp special case.
|
|
145
|
+
#
|
|
146
|
+
# @param processor [Relaton::Core::Processor]
|
|
147
|
+
# @return [Module]
|
|
148
|
+
def flavor_module(processor)
|
|
149
|
+
Object.const_get processor.class.name.split("::")[0..-2].join("::")
|
|
150
|
+
end
|
|
151
|
+
|
|
112
152
|
def gem_to_module_path(gem_name)
|
|
113
153
|
gem_name.split("/").map do |part|
|
|
114
154
|
part.capitalize.sub("3gpp", "ThreeGpp")
|
data/lib/relaton/doi/crossref.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Relaton
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
n += 1
|
|
52
|
-
sleep resp.response
|
|
52
|
+
sleep backoff(resp.response, n)
|
|
53
53
|
rescue Mechanize::ResponseCodeError => e
|
|
54
54
|
return nil if e.response_code == "404"
|
|
55
55
|
|
|
@@ -58,10 +58,27 @@ module Relaton
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
n += 1
|
|
61
|
-
sleep e.page.response
|
|
61
|
+
sleep backoff(e.page.response, n)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
#
|
|
66
|
+
# Seconds to wait before retry n. Crossref sends X-Rate-Limit-Interval as
|
|
67
|
+
# "1s", but a throttled response can carry no rate-limit headers at all —
|
|
68
|
+
# the 429s seen here had only Date/Content-Length/Connection. Without the
|
|
69
|
+
# floor that degenerates to `sleep 0`, i.e. hammering the endpoint that
|
|
70
|
+
# just asked us to slow down.
|
|
71
|
+
#
|
|
72
|
+
# @param [Hash, nil] headers The response headers.
|
|
73
|
+
# @param [Integer] num The attempt number.
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer] Delay in seconds, at least 1.
|
|
76
|
+
#
|
|
77
|
+
def backoff(headers, num)
|
|
78
|
+
interval = headers.to_h["x-rate-limit-interval"].to_s[/\d+/].to_i
|
|
79
|
+
[interval, 1].max * num
|
|
80
|
+
end
|
|
81
|
+
|
|
65
82
|
def agent
|
|
66
83
|
@agent ||= Mechanize.new do |a|
|
|
67
84
|
a.user_agent = USER_AGENT
|