end_point_blank 0.2.1 → 0.2.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/HARDENING_REPORT.md +210 -0
- data/lib/end_point_blank/version.rb +1 -1
- data/lib/end_point_blank/writers/response_writer.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25e18f304c5b420719930533c91b34cc2df76504e5a6d28504491b42c267e2e9
|
|
4
|
+
data.tar.gz: 99b6acaf7c8dc69e730ebe81a4de17eea39f72ff32e681da08130cdafd2181f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79f945a9dacd76714a5d2a229931858d0346428b9459a6dfa98b0a5a363a1b043ff12c29643cc3c3f0d1679930d9d7056c29d02813a18863594e796ce667ad7d
|
|
7
|
+
data.tar.gz: 8b61b6a9f38a5ff2e52d5a37d7979ed18fd0d49f06f8ae3ecf4a07bba8683cc7cefc76b17ec68a3e4a32654644a313e2d6e70c24666de9d1f93654720233ecdd
|
data/HARDENING_REPORT.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Hardening report — end_point_blank (Rails/Ruby client lib) — P2 #14
|
|
2
|
+
|
|
3
|
+
Branch: `harden-timeouts-bounded-queue` (off `master`, not pushed)
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Bring this lib's reliability posture in line with the Elixir sibling lib,
|
|
8
|
+
which was hardened with per-attempt HTTP timeouts (connect 3s / read 5s)
|
|
9
|
+
and a bounded, drop-oldest background send queue (cap 1000, throttled
|
|
10
|
+
warning on drop).
|
|
11
|
+
|
|
12
|
+
## Gap 1 — Excon calls with no timeout
|
|
13
|
+
|
|
14
|
+
### Every `Excon.` call site in the lib (confirmed via `grep -rn "Excon\." lib/`)
|
|
15
|
+
|
|
16
|
+
| File | Line | Call |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `lib/end_point_blank/commands/http.rb` | 23 | `Excon.post` (shared fire-and-forget POST helper, used by `DirectWriter`) |
|
|
19
|
+
| `lib/end_point_blank/commands/generate_access_token.rb` | 20 | `Excon.post` (access-token exchange) |
|
|
20
|
+
| `lib/end_point_blank/commands/endpoint_update.rb` | 30 | `Excon.post` (endpoint/route registration) |
|
|
21
|
+
|
|
22
|
+
All three are the only `Excon.*` calls in the lib. No other `Excon.new`/etc. exist.
|
|
23
|
+
|
|
24
|
+
### Fix
|
|
25
|
+
|
|
26
|
+
Added a single shared constant in `lib/end_point_blank/commands/http.rb`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
CONNECT_TIMEOUT = 3
|
|
30
|
+
READ_TIMEOUT = 5
|
|
31
|
+
TIMEOUT_OPTIONS = { connect_timeout: CONNECT_TIMEOUT, read_timeout: READ_TIMEOUT }.freeze
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `Http.post` now passes `**TIMEOUT_OPTIONS` directly.
|
|
35
|
+
- `generate_access_token.rb` and `endpoint_update.rb` each `require_relative 'http'`
|
|
36
|
+
and splat `**EndPointBlank::Commands::Http::TIMEOUT_OPTIONS` into their own
|
|
37
|
+
`Excon.post` calls, so the values live in exactly one place.
|
|
38
|
+
|
|
39
|
+
### Timeout error handling
|
|
40
|
+
|
|
41
|
+
`Excon::Error::Timeout` is a subclass of `Excon::Error` (verified against the
|
|
42
|
+
installed excon 1.5.0 source), so:
|
|
43
|
+
|
|
44
|
+
- `Http.post`'s existing `rescue Excon::Error => e` already covers timeouts —
|
|
45
|
+
it retries up to `MAX_ATTEMPTS` (3) then logs and returns `nil`. No caller
|
|
46
|
+
ever sees an exception.
|
|
47
|
+
- `generate_access_token.rb`'s existing `rescue => e` (StandardError) already
|
|
48
|
+
covered timeouts too.
|
|
49
|
+
- `endpoint_update.rb` had a **latent bug**: `rescue Excon::Error::Socket,
|
|
50
|
+
Excon::Error::Connection => e`. `Excon::Error::Connection` **does not
|
|
51
|
+
exist** in excon 1.5.0 (only `Excon::Error::Socket`, `Excon::Error::Timeout`,
|
|
52
|
+
etc. are defined under `Excon::Error`). Because Ruby resolves rescue-clause
|
|
53
|
+
constants lazily, this clause would silently work until an exception was
|
|
54
|
+
actually raised inside the `begin` block, at which point evaluating
|
|
55
|
+
`Excon::Error::Connection` would raise `NameError` and crash the caller —
|
|
56
|
+
i.e. a network blip or a timeout during endpoint registration would have
|
|
57
|
+
thrown `NameError` instead of being handled gracefully. Fixed by rescuing
|
|
58
|
+
`Excon::Error` broadly (matching the pattern in `http.rb`), which now also
|
|
59
|
+
correctly covers `Excon::Error::Timeout`.
|
|
60
|
+
|
|
61
|
+
### Incidental fix required to exercise the above in tests
|
|
62
|
+
|
|
63
|
+
`lib/end_point_blank/authorization.rb` calls `Base64.encode64` for the Basic
|
|
64
|
+
auth header path but never `require 'base64'`. On Ruby 3.4 `base64` was
|
|
65
|
+
removed from default gems, so this raised `NameError` / `LoadError` the
|
|
66
|
+
moment that code path executed (which is exactly the path exercised in the
|
|
67
|
+
`endpoint_update` timeout test, since it needs an `Authorization.header` call
|
|
68
|
+
with no cached token). Fixed by adding `require 'base64'` in
|
|
69
|
+
`authorization.rb` and declaring `spec.add_dependency "base64"` in the
|
|
70
|
+
gemspec (ran `bundle install`, updating `Gemfile.lock`). This was a real
|
|
71
|
+
reliability bug independent of this task — any Basic-auth call path in this
|
|
72
|
+
gem on Ruby >= 3.4 without `base64` explicitly in the app's own Gemfile would
|
|
73
|
+
have crashed.
|
|
74
|
+
|
|
75
|
+
## Gap 2 — unbounded background queue
|
|
76
|
+
|
|
77
|
+
`lib/end_point_blank/writers/delayed_writer.rb` used a plain `Queue.new`
|
|
78
|
+
(unbounded). During an intake outage, the 2 background drain threads block
|
|
79
|
+
on failed/slow POSTs and the queue grows without bound → OOM risk.
|
|
80
|
+
|
|
81
|
+
### Fix
|
|
82
|
+
|
|
83
|
+
`Queue` has no `max` option, so the bound is implemented explicitly:
|
|
84
|
+
|
|
85
|
+
- `MAX_QUEUE_SIZE = 1000`.
|
|
86
|
+
- `enqueue` (public API, accepts a single payload or an array, unchanged
|
|
87
|
+
signature) now routes each item through `enqueue_one`, which is guarded by
|
|
88
|
+
a `Mutex` (`enqueue_mutex`) so the check-then-drop-then-push sequence is
|
|
89
|
+
atomic across concurrent producer threads (the request/response/log/error
|
|
90
|
+
writer call paths).
|
|
91
|
+
- If `queue.size >= MAX_QUEUE_SIZE` when a new item arrives, the **oldest**
|
|
92
|
+
item is dropped first (`pop_additional`, a non-blocking `Queue#pop(true)`,
|
|
93
|
+
which removes from the front of the FIFO `Queue`), then the new item is
|
|
94
|
+
pushed. Net effect: queue never exceeds 1000, newest items win.
|
|
95
|
+
- Drop warnings are throttled: `note_drop` tracks a drop counter and only
|
|
96
|
+
logs (via `log_warning`, which prefers `::Rails.logger.warn` and falls
|
|
97
|
+
back to `Kernel#warn` when Rails isn't loaded) at most once per
|
|
98
|
+
`WARN_THROTTLE_SECONDS` (30s), with the message including how many items
|
|
99
|
+
were dropped since the last warning — so a sustained outage doesn't itself
|
|
100
|
+
become a logging flood.
|
|
101
|
+
- The existing 2-thread drain model (`start_threads`) is unchanged in
|
|
102
|
+
structure; internal `@queue` access was switched to a `queue` reader
|
|
103
|
+
(`@queue ||= Queue.new`) so both the drain threads and the new bounded
|
|
104
|
+
`enqueue_one` share the same lazily-initialized queue, and so tests can
|
|
105
|
+
instantiate a bare object that mixes in `DelayedWriter` and drive
|
|
106
|
+
`enqueue`/`queue` without needing to spin up the background threads.
|
|
107
|
+
|
|
108
|
+
## Sinatra coverage
|
|
109
|
+
|
|
110
|
+
Confirmed: `end_point_blank_sinatra/Gemfile` depends on
|
|
111
|
+
`gem 'end_point_blank', path: '../end_point_blank_rails'` and
|
|
112
|
+
`end_point_blank_sinatra/app.rb` does `require 'end_point_blank'`, which is
|
|
113
|
+
this gem's single entry point (`lib/end_point_blank.rb`) that requires every
|
|
114
|
+
file touched here (`commands/http`, `commands/generate_access_token`,
|
|
115
|
+
`commands/endpoint_update`, `writers/delayed_writer`, `authorization`).
|
|
116
|
+
There is no Sinatra-specific fork of any of this code. **Sinatra is
|
|
117
|
+
covered.**
|
|
118
|
+
|
|
119
|
+
## Tests added (TDD: written first, confirmed red against the pre-fix code, then made green)
|
|
120
|
+
|
|
121
|
+
- `spec/commands_http_spec.rb` — asserts `Excon.post` is called with
|
|
122
|
+
`connect_timeout`/`read_timeout` matching the shared constants; asserts a
|
|
123
|
+
raised `Excon::Error::Timeout` is retried `MAX_ATTEMPTS` times, logged, and
|
|
124
|
+
returns `nil` without raising.
|
|
125
|
+
- `spec/generate_access_token_spec.rb` — asserts the same timeout options
|
|
126
|
+
reach `Excon.post` for the access-token exchange; asserts a timeout
|
|
127
|
+
doesn't raise.
|
|
128
|
+
- `spec/endpoint_update_spec.rb` — asserts the same timeout options reach
|
|
129
|
+
`Excon.post` for endpoint registration; asserts a timeout doesn't raise
|
|
130
|
+
and is logged via `Rails.logger.warn` (this is the test that caught both
|
|
131
|
+
the `Excon::Error::Connection` NameError bug and the missing
|
|
132
|
+
`require 'base64'`).
|
|
133
|
+
- `spec/delayed_writer_spec.rb` — mixes `DelayedWriter` into a bare test
|
|
134
|
+
class (without starting background threads, so the queue can be inspected
|
|
135
|
+
synchronously): asserts the queue never exceeds `MAX_QUEUE_SIZE`, asserts
|
|
136
|
+
the oldest items are dropped first (FIFO order preserved for survivors),
|
|
137
|
+
asserts normal FIFO enqueue/dequeue when under the cap, asserts array-form
|
|
138
|
+
`enqueue` also respects the bound, and asserts `log_warning` is called
|
|
139
|
+
only **once** across 50 drops (throttling), not once per drop.
|
|
140
|
+
|
|
141
|
+
All four spec files were confirmed to fail (9 of 11 new examples red) against
|
|
142
|
+
the original code before implementing the fix, then confirmed green after.
|
|
143
|
+
|
|
144
|
+
## Test evidence
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
$ bundle exec rspec
|
|
148
|
+
...
|
|
149
|
+
49 examples, 0 failures
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Also ran the project's actual CI commands directly:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
$ ./build.sh # bundle install — passes
|
|
156
|
+
$ ./test.sh # bundle exec rspec — 49 examples, 0 failures
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`bundle exec ruby -Ilib -e "require 'end_point_blank'"` loads cleanly and
|
|
160
|
+
`EndPointBlank::Commands::Http::TIMEOUT_OPTIONS` /
|
|
161
|
+
`EndPointBlank::Writers::DelayedWriter::MAX_QUEUE_SIZE` are inspectable at
|
|
162
|
+
the top level, confirming the constants are reachable as documented.
|
|
163
|
+
|
|
164
|
+
`rubocop` was also run for hygiene (not part of CI — `ci.yml` only runs
|
|
165
|
+
`./test.sh`, i.e. rspec). It reports 46 offenses across the 5 touched files
|
|
166
|
+
vs. 43 pre-existing on the same files before this change — the 3 new ones
|
|
167
|
+
are `Metrics/AbcSize`/`Metrics/MethodLength` on `start_threads`, whose body
|
|
168
|
+
is unchanged except swapping `@queue` for the new `queue` reader method; all
|
|
169
|
+
newly-added methods (`enqueue_one`, `drop_oldest_and_note`, `note_drop`,
|
|
170
|
+
`warn_dropped_items`, `log_warning`) were kept short enough to avoid adding
|
|
171
|
+
further offenses. None of this blocks CI.
|
|
172
|
+
|
|
173
|
+
## Files changed
|
|
174
|
+
|
|
175
|
+
- `lib/end_point_blank/commands/http.rb` — shared timeout constants/helper,
|
|
176
|
+
applied to `Http.post`.
|
|
177
|
+
- `lib/end_point_blank/commands/generate_access_token.rb` — applies shared
|
|
178
|
+
timeout options to its `Excon.post`.
|
|
179
|
+
- `lib/end_point_blank/commands/endpoint_update.rb` — applies shared timeout
|
|
180
|
+
options to its `Excon.post`; fixed the broken `Excon::Error::Connection`
|
|
181
|
+
rescue clause to `rescue Excon::Error` (now also catches `Timeout`).
|
|
182
|
+
- `lib/end_point_blank/writers/delayed_writer.rb` — bounded, drop-oldest,
|
|
183
|
+
throttled-warning queue.
|
|
184
|
+
- `lib/end_point_blank/authorization.rb` — `require 'base64'` (incidental
|
|
185
|
+
fix, needed for the Basic-auth path to work at all on Ruby 3.4, and
|
|
186
|
+
required to exercise the `endpoint_update` timeout test end-to-end).
|
|
187
|
+
- `end_point_blank.gemspec`, `Gemfile.lock` — declare `base64` as an
|
|
188
|
+
explicit dependency (Ruby 3.4 removed it from default gems).
|
|
189
|
+
- New specs: `spec/commands_http_spec.rb`, `spec/generate_access_token_spec.rb`,
|
|
190
|
+
`spec/endpoint_update_spec.rb`, `spec/delayed_writer_spec.rb`.
|
|
191
|
+
|
|
192
|
+
## Concerns / follow-ups (not fixed here, out of scope for P2 #14)
|
|
193
|
+
|
|
194
|
+
- The queue bound is a soft/approximate guarantee under heavy concurrent
|
|
195
|
+
enqueue: the mutex only guards the *decision* to drop before push, so with
|
|
196
|
+
many producer threads racing, the queue could very briefly be examined by
|
|
197
|
+
more than one thread between the `pop`/`push` pair in rare interleavings
|
|
198
|
+
is prevented by the mutex — but a concurrent **drain** thread popping at
|
|
199
|
+
the same moment only ever shrinks the queue, never grows it past the cap.
|
|
200
|
+
In practice the bound holds exactly given the mutex around all enqueue-side
|
|
201
|
+
mutation.
|
|
202
|
+
- `note_drop`/`@last_drop_warning_at` state lives on the writer singleton
|
|
203
|
+
instance (`RequestWriter`, `ResponseWriter`, `ExceptionWriter`,
|
|
204
|
+
`LogWriter` each get their own independent throttle window since they each
|
|
205
|
+
`include DelayedWriter` separately) — this is intentional (matches
|
|
206
|
+
per-queue behavior) but worth knowing if someone expects a single global
|
|
207
|
+
throttle across all four queues.
|
|
208
|
+
- Rubocop was not run as part of CI before this change and still isn't;
|
|
209
|
+
pre-existing style debt (43 offenses) was left alone except where it was
|
|
210
|
+
directly in code I touched and cheap to avoid growing.
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: end_point_blank
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert A. Lasch
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-07-
|
|
10
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: excon
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- ".rspec"
|
|
92
92
|
- ".rubocop.yml"
|
|
93
93
|
- ".ruby-version"
|
|
94
|
+
- HARDENING_REPORT.md
|
|
94
95
|
- LICENSE
|
|
95
96
|
- README.md
|
|
96
97
|
- Rakefile
|