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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b93be9173755a967ce4c1f1d38205b8803bc175ec1d029f860c6620807553bca
4
- data.tar.gz: 74ff753691ea46be3ac391cea6b178673a8fb4cd7e0b6ea2861b9646d138e710
3
+ metadata.gz: 25e18f304c5b420719930533c91b34cc2df76504e5a6d28504491b42c267e2e9
4
+ data.tar.gz: 99b6acaf7c8dc69e730ebe81a4de17eea39f72ff32e681da08130cdafd2181f2
5
5
  SHA512:
6
- metadata.gz: 4533c807575899944c7626e716585018309a55311d659b0e2b3fa5005b0ab5b825bec2ef89a0b3d9108277ddac28cc985c75c6047ded18542c2d4dceeace66f1
7
- data.tar.gz: d7a5148cba59e40aaf3190b8147800e4777882ff08c31e9245ede3f289eff1eedb2c43dfe034587bf52cf2b23518b2d6cdbe4435e90235c71418d433c1d9b16c
6
+ metadata.gz: 79f945a9dacd76714a5d2a229931858d0346428b9459a6dfa98b0a5a363a1b043ff12c29643cc3c3f0d1679930d9d7056c29d02813a18863594e796ce667ad7d
7
+ data.tar.gz: 8b61b6a9f38a5ff2e52d5a37d7979ed18fd0d49f06f8ae3ecf4a07bba8683cc7cefc76b17ec68a3e4a32654644a313e2d6e70c24666de9d1f93654720233ecdd
@@ -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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EndPointBlank
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -36,6 +36,7 @@ module EndPointBlank
36
36
  body: truncate(normalize_body(body)),
37
37
  sent_at: Time.now.utc.iso8601(3),
38
38
  route: route,
39
+ method: request&.request_method,
39
40
  data: data,
40
41
  source_application_environment_id: source_application_environment_id
41
42
  }
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.1
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-08 00:00:00.000000000 Z
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