atlas_rb 1.27.0 → 1.27.1
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/.version +1 -1
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +38 -0
- data/lib/atlas_rb/errors.rb +41 -0
- data/lib/atlas_rb/faraday_helper.rb +5 -0
- data/lib/atlas_rb/health.rb +78 -0
- data/lib/atlas_rb/middleware/raise_on_backend_unavailable.rb +49 -0
- data/lib/atlas_rb.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c1b0723d54145c8be9868a1849502cf80adbc1a30b2f7b1ee39aadbfa1a2f3b
|
|
4
|
+
data.tar.gz: 7ab2bb5a662b02dd48275e645f9c6b3c1226d6bc995231766bd5a4252f17b25a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64f43d675ad66dac85657c0460702f53f95c99760e7a44ce6f0dec019e375f41b197c214ca3dc4d89a94a1e0b829fa9d377fd4dd9432fe68cdc53d9aa490e38e
|
|
7
|
+
data.tar.gz: dfc0ade69d92b8b100698091f6ec22d90eb428769bb61aebdc79a9e842840036bc45875bc330531be335d5a11459c823ff13a1ccd78e1b143b11009f083ca54b
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.27.
|
|
1
|
+
1.27.1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.27.1
|
|
4
|
+
|
|
5
|
+
Requires an Atlas that serves `GET /health` and the `backend_unavailable`
|
|
6
|
+
envelope.
|
|
7
|
+
|
|
8
|
+
### Added — `AtlasRb::Health.read`
|
|
9
|
+
|
|
10
|
+
Reads Atlas's readiness report (`GET /health`). Returns the parsed body on a
|
|
11
|
+
`200` or a `503`, because a `503` there is an answer. Raises only on
|
|
12
|
+
transport failure, or `ResourceError` when the answer is not the report. No
|
|
13
|
+
credential, a 5-second deadline, no retry, and a fresh socket per call.
|
|
14
|
+
|
|
15
|
+
### Added — `AtlasRb::BackendUnavailableError`
|
|
16
|
+
|
|
17
|
+
Raised on every binding when Atlas answers `503` with
|
|
18
|
+
`error: "backend_unavailable"`. Carries `dependency` (`"database"`,
|
|
19
|
+
`"index"` or `"storage"`) and `retry_after`. A sibling of
|
|
20
|
+
`ReadOnlyModeError`, not a `ResourceError`. A bodyless `503` from a proxy
|
|
21
|
+
still passes through.
|
|
22
|
+
|
|
3
23
|
## 1.27.0
|
|
4
24
|
|
|
5
25
|
### Removed — `Resource.class_for` and `Resource::TYPE_MAP`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -819,6 +819,44 @@ A `503` from a reverse proxy while Atlas restarts carries no JSON body and is
|
|
|
819
819
|
a different condition with a different remedy, so it passes through as a plain
|
|
820
820
|
response rather than raising.
|
|
821
821
|
|
|
822
|
+
### Health and outages
|
|
823
|
+
|
|
824
|
+
`AtlasRb::Health.read` returns Atlas's readiness report. It sends no
|
|
825
|
+
credential, because Atlas serves `GET /health` unauthenticated:
|
|
826
|
+
|
|
827
|
+
```ruby
|
|
828
|
+
report = AtlasRb::Health.read
|
|
829
|
+
report["status"] # => "ok" / "degraded" / "unavailable"
|
|
830
|
+
report["database"] # => "ok" / "unavailable"
|
|
831
|
+
report["index"] # => "ok" / "unavailable"
|
|
832
|
+
report["storage"] # => "ok" / "unavailable"
|
|
833
|
+
report["cache"] # => "ok" / "unavailable" / "disabled"
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Atlas answers `503` when the database, the index or the storage is down. That
|
|
837
|
+
is an answer, so `read` returns it like a `200`. It raises
|
|
838
|
+
`Faraday::ConnectionFailed` or `Faraday::TimeoutError` only when nothing
|
|
839
|
+
answers, and `AtlasRb::ResourceError` when something answers that is not the
|
|
840
|
+
report. It has a 5-second deadline (`read(timeout: 3)` to change it) and no
|
|
841
|
+
retry, so a probe answers inside its poller's own deadline.
|
|
842
|
+
|
|
843
|
+
When an ordinary call meets a dependency outage, Atlas answers `503` with
|
|
844
|
+
`error: "backend_unavailable"`, and every binding raises:
|
|
845
|
+
|
|
846
|
+
```ruby
|
|
847
|
+
begin
|
|
848
|
+
AtlasRb::Work.find("w-789")
|
|
849
|
+
rescue AtlasRb::BackendUnavailableError => e
|
|
850
|
+
e.dependency # => "database" / "index" / "storage"
|
|
851
|
+
e.retry_after # => 30
|
|
852
|
+
end
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
It is not a `ResourceError`, which hosts treat as a refusal of the request.
|
|
856
|
+
|
|
857
|
+
A streaming download (`Blob.content`, `Blob.version_content`) cannot raise
|
|
858
|
+
this: its body has already gone to your block. Check the returned `status`.
|
|
859
|
+
|
|
822
860
|
## End-to-end example
|
|
823
861
|
|
|
824
862
|
JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -420,6 +420,47 @@ module AtlasRb
|
|
|
420
420
|
end
|
|
421
421
|
end
|
|
422
422
|
|
|
423
|
+
# Raised when Atlas cannot reach one of its own dependencies: an HTTP `503`
|
|
424
|
+
# whose body carries `error: "backend_unavailable"` and a `dependency` key
|
|
425
|
+
# naming `"database"`, `"index"` or `"storage"`.
|
|
426
|
+
#
|
|
427
|
+
# A sibling of {ReadOnlyModeError}, and deliberately NOT a {ResourceError}.
|
|
428
|
+
# Hosts commonly treat a `ResourceError` as a refusal of the request (Cerberus's
|
|
429
|
+
# `Authorizable` renders it as one), and an outage is no statement about the
|
|
430
|
+
# caller or the resource. Without this the outage reaches the host as the
|
|
431
|
+
# generic `ResourceError` a `5xx` read raises, indistinguishable from a bug.
|
|
432
|
+
#
|
|
433
|
+
# rescue AtlasRb::BackendUnavailableError => e
|
|
434
|
+
# render "outage", status: :service_unavailable
|
|
435
|
+
# response.headers["Retry-After"] = e.retry_after.to_s if e.retry_after
|
|
436
|
+
#
|
|
437
|
+
# A bodyless `503` from a proxy is not this; it keeps reaching the caller as
|
|
438
|
+
# it did before. Poll {AtlasRb::Health.read} to see every dependency at once.
|
|
439
|
+
class BackendUnavailableError < Error
|
|
440
|
+
# @return [String, nil] the envelope's `error` discriminator
|
|
441
|
+
# (`"backend_unavailable"`).
|
|
442
|
+
attr_reader :code
|
|
443
|
+
|
|
444
|
+
# @return [String, nil] which dependency failed: `"database"`, `"index"` or
|
|
445
|
+
# `"storage"`.
|
|
446
|
+
attr_reader :dependency
|
|
447
|
+
|
|
448
|
+
# @return [Integer, nil] seconds to wait before retrying, from Atlas's
|
|
449
|
+
# `Retry-After` response header.
|
|
450
|
+
attr_reader :retry_after
|
|
451
|
+
|
|
452
|
+
# @param message [String, nil] human-readable description.
|
|
453
|
+
# @param code [String, nil] the envelope's `error` discriminator.
|
|
454
|
+
# @param dependency [String, nil] the dependency that failed.
|
|
455
|
+
# @param retry_after [Integer, nil] seconds from the `Retry-After` header.
|
|
456
|
+
def initialize(message=nil, code: nil, dependency: nil, retry_after: nil)
|
|
457
|
+
@code = code
|
|
458
|
+
@dependency = dependency
|
|
459
|
+
@retry_after = retry_after
|
|
460
|
+
super(message || "Atlas cannot reach its #{dependency || "backend"}")
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
423
464
|
# Raised when the transport has no way to authenticate a relay request:
|
|
424
465
|
# neither `ATLAS_JWT` (BYO-JWT mode) nor a signing key
|
|
425
466
|
# ({AtlasRb.config#assertion_signing_key}, relay-signing mode) is configured.
|
|
@@ -408,11 +408,16 @@ module AtlasRb
|
|
|
408
408
|
# above: a maintenance window refuses writes on every path and a `503`
|
|
409
409
|
# reaches neither of them. On all three shapes, because a write that
|
|
410
410
|
# slips past it silently unwraps nil and reports success.
|
|
411
|
+
# - {Middleware::RaiseOnBackendUnavailable} — path-independent for the same
|
|
412
|
+
# reason: a dependency outage can answer any request on any shape. It
|
|
413
|
+
# must run before the read guard, or an outage on a read becomes the
|
|
414
|
+
# generic {AtlasRb::ResourceError} a host treats as a refusal.
|
|
411
415
|
def error_middleware(builder, reads:)
|
|
412
416
|
builder.use AtlasRb::Middleware::RaiseOnReadError if reads
|
|
413
417
|
builder.use AtlasRb::Middleware::RaiseOnStaleResource
|
|
414
418
|
builder.use AtlasRb::Middleware::RaiseOnResourceError
|
|
415
419
|
builder.use AtlasRb::Middleware::RaiseOnReadOnlyMode
|
|
420
|
+
builder.use AtlasRb::Middleware::RaiseOnBackendUnavailable
|
|
416
421
|
end
|
|
417
422
|
|
|
418
423
|
# Retry an idempotent read that failed in transport, in exactly one layer.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# Atlas's readiness report (`GET /health`): whether it can reach its database,
|
|
5
|
+
# its Solr index, its storage and its cache.
|
|
6
|
+
#
|
|
7
|
+
# Atlas answers `200` when every required dependency answers and `503` when
|
|
8
|
+
# one does not, with the same body either way:
|
|
9
|
+
#
|
|
10
|
+
# { "status" => "degraded", "database" => "ok", "index" => "unavailable",
|
|
11
|
+
# "storage" => "ok", "cache" => "ok" }
|
|
12
|
+
#
|
|
13
|
+
# The endpoint is unauthenticated, so this sends no credential, and works
|
|
14
|
+
# without a signing key or `ATLAS_JWT` configured.
|
|
15
|
+
#
|
|
16
|
+
# Not to be confused with `GET /up`, the liveness route the orchestrator
|
|
17
|
+
# polls; it has no binding.
|
|
18
|
+
class Health
|
|
19
|
+
extend AtlasRb::FaradayHelper
|
|
20
|
+
|
|
21
|
+
# Seconds to wait for a socket to open.
|
|
22
|
+
OPEN_TIMEOUT = 2
|
|
23
|
+
|
|
24
|
+
# Seconds to wait for the report. Atlas bounds each of its own probes, so a
|
|
25
|
+
# slower answer means Atlas itself is struggling. Short enough to answer
|
|
26
|
+
# inside a monitor's own deadline (Nagios's `check_http` defaults to 10s).
|
|
27
|
+
READ_TIMEOUT = 5
|
|
28
|
+
|
|
29
|
+
# Read the report.
|
|
30
|
+
#
|
|
31
|
+
# A `503` here is an answer, not an error, so it is returned like a `200`;
|
|
32
|
+
# read `status` off the result. There is no retry: a probe must answer
|
|
33
|
+
# faster than its poller times out, and a retry would hide the very
|
|
34
|
+
# flakiness it exists to show.
|
|
35
|
+
#
|
|
36
|
+
# @param timeout [Numeric] seconds to wait for the report.
|
|
37
|
+
# @return [AtlasRb::Mash] `status` (`"ok"` / `"degraded"` /
|
|
38
|
+
# `"unavailable"`), and `database`, `index`, `storage` (`"ok"` /
|
|
39
|
+
# `"unavailable"`) and `cache` (`"ok"` / `"unavailable"` / `"disabled"`).
|
|
40
|
+
# @raise [Faraday::ConnectionFailed, Faraday::TimeoutError] when Atlas
|
|
41
|
+
# does not answer at all.
|
|
42
|
+
# @raise [AtlasRb::ResourceError] when something answers that is not
|
|
43
|
+
# Atlas's report: a `404` from an Atlas that predates the endpoint, or a
|
|
44
|
+
# proxy's `502` with an HTML body.
|
|
45
|
+
def self.read(timeout: READ_TIMEOUT)
|
|
46
|
+
resp = health_connection(timeout).get("/health")
|
|
47
|
+
report = parse_report(resp)
|
|
48
|
+
return AtlasRb::Mash.new(report) if report
|
|
49
|
+
|
|
50
|
+
raise AtlasRb::ResourceError.new("#{request_target(resp)} → #{resp.status}: #{resp.body}", response: resp)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# A fresh socket per call rather than the shared pool. With no retry, a
|
|
57
|
+
# pooled socket the server had closed would report a healthy Atlas as
|
|
58
|
+
# down; a probe runs far too rarely for the handshake to matter.
|
|
59
|
+
def health_connection(timeout)
|
|
60
|
+
Faraday.new(url: ENV.fetch("ATLAS_URL", nil)) do |f|
|
|
61
|
+
f.options.open_timeout = OPEN_TIMEOUT
|
|
62
|
+
f.options.timeout = timeout
|
|
63
|
+
instrument(f)
|
|
64
|
+
f.adapter :net_http
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_report(resp)
|
|
69
|
+
return nil unless [200, 503].include?(resp.status)
|
|
70
|
+
|
|
71
|
+
body = JSON.parse(resp.body.to_s)
|
|
72
|
+
body.is_a?(Hash) && body.key?("status") ? body : nil
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
module Middleware
|
|
5
|
+
# Translates Atlas's dependency-outage answer into a typed Ruby exception.
|
|
6
|
+
#
|
|
7
|
+
# When Atlas cannot reach its database, its Solr index or its storage, it
|
|
8
|
+
# answers `503 Service Unavailable` with the discriminator
|
|
9
|
+
# `error: "backend_unavailable"`, a `dependency` key and a `Retry-After`
|
|
10
|
+
# header. This middleware keys on the **status + discriminator pair** and
|
|
11
|
+
# raises {AtlasRb::BackendUnavailableError}.
|
|
12
|
+
#
|
|
13
|
+
# Path-independent, like {RaiseOnReadOnlyMode}: an outage can answer any
|
|
14
|
+
# request. A 503 without Atlas's envelope — a proxy while Atlas restarts —
|
|
15
|
+
# passes through untouched, because that is a different condition.
|
|
16
|
+
class RaiseOnBackendUnavailable < Faraday::Middleware
|
|
17
|
+
# The discriminator Atlas renders. An exact-match wire contract.
|
|
18
|
+
CODE = "backend_unavailable"
|
|
19
|
+
|
|
20
|
+
# @param env [Faraday::Env] the completed response environment.
|
|
21
|
+
# @raise [AtlasRb::BackendUnavailableError] on a 503 whose body carries
|
|
22
|
+
# `error: "backend_unavailable"`.
|
|
23
|
+
# @return [void]
|
|
24
|
+
def on_complete(env)
|
|
25
|
+
return unless env.status == 503
|
|
26
|
+
|
|
27
|
+
body = parse_json(env.body)
|
|
28
|
+
return unless body.is_a?(Hash) && body["error"] == CODE
|
|
29
|
+
|
|
30
|
+
raise AtlasRb::BackendUnavailableError.new(
|
|
31
|
+
body["message"],
|
|
32
|
+
code: body["error"],
|
|
33
|
+
dependency: body["dependency"],
|
|
34
|
+
retry_after: env.response_headers&.[]("retry-after")&.to_i
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_json(body)
|
|
41
|
+
return body if body.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
JSON.parse(body.to_s)
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "atlas_rb/middleware/raise_on_read_error"
|
|
|
21
21
|
require_relative "atlas_rb/middleware/raise_on_stale_resource"
|
|
22
22
|
require_relative "atlas_rb/middleware/raise_on_resource_error"
|
|
23
23
|
require_relative "atlas_rb/middleware/raise_on_read_only_mode"
|
|
24
|
+
require_relative "atlas_rb/middleware/raise_on_backend_unavailable"
|
|
24
25
|
require_relative "atlas_rb/faraday_helper"
|
|
25
26
|
require_relative "atlas_rb/mash"
|
|
26
27
|
require_relative "atlas_rb/authentication"
|
|
@@ -46,6 +47,7 @@ require_relative "atlas_rb/system/token"
|
|
|
46
47
|
require_relative "atlas_rb/system/work"
|
|
47
48
|
require_relative "atlas_rb/audit_event"
|
|
48
49
|
require_relative "atlas_rb/maintenance"
|
|
50
|
+
require_relative "atlas_rb/health"
|
|
49
51
|
|
|
50
52
|
# Ruby client for the Atlas API — Northeastern University's institutional
|
|
51
53
|
# digital repository.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atlas_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.27.
|
|
4
|
+
version: 1.27.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -170,8 +170,10 @@ files:
|
|
|
170
170
|
- lib/atlas_rb/errors.rb
|
|
171
171
|
- lib/atlas_rb/faraday_helper.rb
|
|
172
172
|
- lib/atlas_rb/file_set.rb
|
|
173
|
+
- lib/atlas_rb/health.rb
|
|
173
174
|
- lib/atlas_rb/maintenance.rb
|
|
174
175
|
- lib/atlas_rb/mash.rb
|
|
176
|
+
- lib/atlas_rb/middleware/raise_on_backend_unavailable.rb
|
|
175
177
|
- lib/atlas_rb/middleware/raise_on_read_error.rb
|
|
176
178
|
- lib/atlas_rb/middleware/raise_on_read_only_mode.rb
|
|
177
179
|
- lib/atlas_rb/middleware/raise_on_resource_error.rb
|