startback 1.2.4 → 2.1.0

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.
data/README.md CHANGED
@@ -14,10 +14,53 @@ Currently,
14
14
 
15
15
  ## Public API
16
16
 
17
- This gem uses semantics versioning and has reached it's 1.0 version. The public
18
- API is defined as follows:
17
+ This gem uses semantic versioning. The public API is defined as follows:
19
18
 
20
19
  * All ruby classes, require path, constructor arguments, and public methods.
21
20
 
22
21
  * The `enspirit/startback:api` and `enspirit/startback:web` docker images and
23
22
  main `CMD`.
23
+
24
+ Upgrading across a major version? See [UPGRADING.md](UPGRADING.md).
25
+
26
+ ## Supported rubies
27
+
28
+ CI runs the suite on **Ruby 3.2, 3.3, 3.4 and 4.0** -- the whole range
29
+ `required_ruby_version` allows. Docker images are released for 3.4 and 4.0.
30
+
31
+ ## Running the tests
32
+
33
+ make tests
34
+
35
+ The `Startback::Event::Bus::Bunny::Async` specs need a real RabbitMQ broker --
36
+ mocking bunny would only assert that Startback calls the methods Startback
37
+ calls. Start one and point the suite at it:
38
+
39
+ make rabbitmq.up
40
+ export STARTBACK_BUS_BUNNY_ASYNC_URL=amqp://guest:guest@localhost:5672
41
+ make tests
42
+ make rabbitmq.down
43
+
44
+ Without a broker those specs **skip**, and the suite is still green. CI sets
45
+ `STARTBACK_SPEC_REQUIRE_BUNNY=1`, which turns "no broker" into a failure, so
46
+ that a broken service container cannot quietly take the coverage away.
47
+
48
+ ## Docker images
49
+
50
+ docker pull enspirit/startback:api # ruby 3.4
51
+ docker pull enspirit/startback:web # ruby 3.4, plus nodejs and yarn
52
+
53
+ The tags that name no ruby version -- `:api`, `:api-2.1.0`, `:api-2.1` -- are
54
+ built with `DEFAULT_MRI_VERSION`, currently **3.4**. Every ruby version listed
55
+ in `RELEASE_MRI_VERSIONS` is also reachable by name:
56
+
57
+ docker pull enspirit/startback:api-ruby4.0
58
+ docker pull enspirit/startback:api-2.1.0-ruby4.0
59
+
60
+ Both variables live at the bottom of the [Makefile](Makefile). Adding a ruby
61
+ version to the release matrix means listing it there and in the
62
+ `ruby-version` matrix of the tests and release-images workflows.
63
+
64
+ `make images` builds and pushes one ruby version (`MRI_VERSION`, defaulting to
65
+ `DEFAULT_MRI_VERSION`); `make images.all` walks the whole matrix, as the
66
+ release workflow does with one job per version.
data/UPGRADING.md ADDED
@@ -0,0 +1,460 @@
1
+ # Upgrading Startback
2
+
3
+ ## From 2.0.x to 2.1.0
4
+
5
+ **There is next to nothing to do.** Startback's API is unchanged and the
6
+ event bus upgrade needs no broker-side migration. One thing does move: the
7
+ un-suffixed docker tags go from Ruby 3.3 to Ruby 3.4. This section exists so
8
+ you know *why*, and so you can spot the one thing that might bite you later.
9
+
10
+ | | |
11
+ |---|---|
12
+ | Ruby | Unchanged, still `>= 3.2`. Ruby 4.0 is now supported and tested. |
13
+ | Docker images | `enspirit/startback:api` and `:web` move from Ruby 3.3 to **Ruby 3.4**. Ruby 4.0 is opt-in by name. |
14
+ | Event bus | Durable topology now, adopted automatically. **But see the RabbitMQ 4.3 wall below.** |
15
+
16
+ ---
17
+
18
+ ### The one thing to know: RabbitMQ 4.3
19
+
20
+ This is the only item here with a deadline, and it is not really about
21
+ Startback.
22
+
23
+ `queue_options` used to default to `{}`, declaring a *transient
24
+ non-exclusive* queue. RabbitMQ deprecated that and flips it to denied in 4.3:
25
+
26
+ | RabbitMQ | transient non-exclusive queues | Startback <= 2.0 bus |
27
+ |---|---|---|
28
+ | 4.0, 4.1, 4.2 | permitted | works |
29
+ | **4.3+** | **denied** | **`listen` receives nothing, then `Timeout::Error`** |
30
+
31
+ So on 4.2 or earlier nothing is on fire today -- but 4.3 is a wall you hit
32
+ whether or not you upgrade Startback. 2.1.0 is what gets you over it: the
33
+ exchange and queue are now declared `durable: true`.
34
+
35
+ ### Why the durable switch costs you nothing
36
+
37
+ AMQP refuses to redeclare an exchange or queue with different properties. On
38
+ a broker that has been up continuously since an older Startback declared its
39
+ topology, the new durable declaration is rejected with
40
+ `PRECONDITION_FAILED`. Startback now **adopts** what is already there rather
41
+ than failing on it, logging:
42
+
43
+ ```
44
+ Adopting an existing fanout whose properties differ from the requested ones.
45
+ It will be declared as requested after the next broker restart.
46
+ ```
47
+
48
+ So deploy in any order, with or without restarting the broker. There is
49
+ nothing to drain and nothing to delete: a transient queue holds no durable
50
+ state, and does not survive a broker restart in the first place. You become
51
+ durable by yourself the next time the broker restarts.
52
+
53
+ Restarting the broker before deploying gets you there immediately, but it is
54
+ an option, not a requirement.
55
+
56
+ Applications already passing their own `queue_options`/`fanout_options` are
57
+ unaffected: explicit options still win.
58
+
59
+ ### Two bus bugs fixed, in case you saw them
60
+
61
+ Both predate 2.1.0 and neither announced itself. If you have ever seen the
62
+ bus "just stop" until a restart, this is likely why:
63
+
64
+ * **A dead channel was cached forever.** A channel-level error closes the
65
+ channel, and the bus kept one per thread without checking it was still
66
+ open. One such error broke the bus for that thread permanently -- every
67
+ later `emit` failing with `cannot use a closed channel`, for *any* event
68
+ type. Since `emit` runs inside `stop_errors`, the application kept
69
+ returning 200s while dropping every event.
70
+
71
+ * **Declaring could unsubscribe your listeners.** A rejected declaration
72
+ closes the channel it happened on, which was the shared one carrying your
73
+ consumers. Topology is now probed on a scratch channel.
74
+
75
+ ### Bus listeners still receive a String
76
+
77
+ Not a change, but now documented and pinned by a spec, because it bites
78
+ people moving a listener between busses:
79
+
80
+ ```ruby
81
+ bus.listen("My::Event::Type", "my-processor") do |body|
82
+ # Bus::Memory::Async hands over a Startback::Event here.
83
+ # Bus::Bunny::Async hands over the raw JSON String.
84
+ event = Startback::Event.json(body, nil)
85
+ end
86
+ ```
87
+
88
+ ### Docker images, if you build on them
89
+
90
+ `enspirit/startback:api` and `:web` are built from `DEFAULT_MRI_VERSION`,
91
+ now **Ruby 3.4**. 2.0.0 published them from Ruby 3.3, so tracking those tags
92
+ moves you one ruby minor version -- not a major, and 3.3 reaches end of life
93
+ in March 2027. Ruby 4.0 stays opt-in, asked for by name:
94
+
95
+ ```dockerfile
96
+ FROM enspirit/startback:api-ruby4.0 # tracks 2.x on ruby 4.0
97
+ FROM enspirit/startback:api-2.1.0-ruby4.0 # pinned
98
+ ```
99
+
100
+ The `web` target now installs **nodejs 22** instead of 20, node 20 being end
101
+ of life since April 2026. Applications pinning a node version in their own
102
+ layer are unaffected.
103
+
104
+ **Moving your own application to Ruby 4.0** is a separate exercise, and worth
105
+ doing separately. `benchmark`, `logger` and `ostruct` stop being default gems
106
+ there: if your code requires them without declaring them, add them to your
107
+ Gemfile. Startback already declares all three for itself.
108
+
109
+ ### Checklist
110
+
111
+ - [ ] Nothing, unless you are heading for RabbitMQ 4.3 -- in which case 2.1.0
112
+ is what you need, and it is enough
113
+ - [ ] `:api` / `:web` move from Ruby 3.3 to 3.4. 2.1.0 publishes no Ruby 3.3
114
+ image: ask for `-ruby4.0` if you want 4.0, or stay on
115
+ `:api-2.0.0-ruby3.3` if you are not ready to leave 3.3
116
+
117
+ ---
118
+
119
+ ## From 1.2.x to 2.0.0
120
+
121
+ **Startback's own API has not changed.** Every class, require path, constructor
122
+ argument and public method behaves as it did in 1.2.x. What changed is the
123
+ dependency floor: Sinatra 4, and therefore Rack 3, are now required, and the
124
+ other dependencies moved to their latest major.
125
+
126
+ So almost everything below is about *your* application code meeting Rack 3 and
127
+ Sinatra 4, not about Startback. Each section is written as: what you will see,
128
+ why, and what to do.
129
+
130
+ Rough budget: a small API service usually needs **two changes** -- setting
131
+ `RACK_ENV`, and lowercasing any response triples it builds by hand. The rest
132
+ depends on what you use.
133
+
134
+ ---
135
+
136
+ ## Before you start
137
+
138
+ | | |
139
+ |---|---|
140
+ | Ruby | **>= 3.2** is now enforced by the gemspec. 3.1 is end of life. |
141
+ | webspicy | **Must move to 1.x.** See [webspicy](#10-webspicy-must-move-to-1x) -- this one fails to install, it does not degrade quietly. |
142
+ | Everything else | Installs fine; behaviour changes are listed below. |
143
+
144
+ Start with:
145
+
146
+ ```sh
147
+ bundle update startback
148
+ bundle exec rake test # or whatever runs your suite
149
+ ```
150
+
151
+ Most failures will be issue 1 or issue 2.
152
+
153
+ ---
154
+
155
+ ## 1. Every request returns `403 Host not permitted`
156
+
157
+ **You will see** every request failing with status 403 and a `text/plain` body
158
+ reading `Host not permitted` -- in your test suite first, and in local
159
+ development if you reach the app through anything other than `localhost`.
160
+
161
+ **Why.** Sinatra 4.1 added `Rack::Protection::HostAuthorization` (for
162
+ CVE-2024-21510). In the `development` environment it only accepts `localhost`,
163
+ `*.localhost`, `*.test` and IP literals as `Host`. `development` is the
164
+ environment Sinatra picks when **neither `RACK_ENV` nor `APP_ENV` is set**,
165
+ which is the common case in test suites and docker-compose.
166
+
167
+ Test suites are hit systematically because `Rack::Test` sends requests to
168
+ `example.org`, and so does webspicy's `RackTestClient`.
169
+
170
+ **Production is not affected**: outside `development`, the permitted list is
171
+ empty, which means "allow everything".
172
+
173
+ **Fix, for test suites** -- set the environment before Sinatra is loaded, i.e.
174
+ at the very top of `spec_helper.rb` (or your webspicy `config.rb`), *above* the
175
+ `require`s:
176
+
177
+ ```ruby
178
+ ENV['RACK_ENV'] ||= 'test'
179
+
180
+ require 'startback'
181
+ ```
182
+
183
+ **Fix, for local development behind a custom hostname** -- either set
184
+ `RACK_ENV` in your docker-compose/`.env`, or declare the hosts:
185
+
186
+ ```ruby
187
+ class MyApi < Startback::Web::Api
188
+ set :host_authorization, { permitted_hosts: ['.my-app.internal', '.localhost'] }
189
+ end
190
+ ```
191
+
192
+ A leading dot matches subdomains. Passing an empty list disables the check
193
+ entirely -- reasonable for a service that only ever sits behind a trusted
194
+ reverse proxy, but it is opting out of a CVE fix, so do it deliberately.
195
+
196
+ ---
197
+
198
+ ## 2. A response header appears twice, or a middleware stops seeing it
199
+
200
+ **You will see** responses carrying, say, both `Cache-Control` and
201
+ `cache-control` with different values; or a middleware that used to read a
202
+ header no longer finding it; or a caching proxy behaving oddly.
203
+
204
+ **Why.** The Rack 3 SPEC states that response header keys *"must not contain
205
+ uppercase ASCII characters (A-Z)"*. Rack 3 middleware therefore looks headers
206
+ up in lowercase. A triple you build by hand with `"Content-Type"` is a
207
+ different key from the `"content-type"` everything else uses, so instead of
208
+ overriding, it coexists.
209
+
210
+ Nothing raises. This is a silent behaviour change, which is what makes it worth
211
+ hunting for deliberately.
212
+
213
+ **Fix.** Lowercase the header names in any response triple your code builds:
214
+
215
+ ```ruby
216
+ # before
217
+ [200, { "Content-Type" => "application/json" }, [body]]
218
+
219
+ # after
220
+ [200, { "content-type" => "application/json" }, [body]]
221
+ ```
222
+
223
+ Grep for it:
224
+
225
+ ```sh
226
+ grep -rnE '"(Content-Type|Cache-Control|Location|Content-Length|X-[A-Za-z-]+)"\s*=>' app lib
227
+ ```
228
+
229
+ You do **not** need to change:
230
+
231
+ * `content_type :json` and friends inside a Sinatra route -- Sinatra normalizes.
232
+ * Reading headers from a response object (`response['Content-Type']`) --
233
+ `Rack::Headers` is case-insensitive on read.
234
+ * Startback's own middlewares. `AutoCaching`, `CorsHeaders`, `HealthCheck`,
235
+ `Shield` and `CatchAll` were all fixed in this release; `AutoCaching` and
236
+ `CorsHeaders` had exactly this duplication bug.
237
+
238
+ ---
239
+
240
+ ## 3. `undefined method 'each' for an instance of String`
241
+
242
+ **You will see** that error, or a blank response body.
243
+
244
+ **Why.** Rack 3 requires a response body to respond to `each` or `call`. A bare
245
+ String is no longer a valid body.
246
+
247
+ **Fix.** Wrap it:
248
+
249
+ ```ruby
250
+ # before
251
+ [404, { "content-type" => "text/plain" }, "NotFound"]
252
+
253
+ # after
254
+ [404, { "content-type" => "text/plain" }, ["NotFound"]]
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 4. `uninitialized constant` for a Rack 2 class
260
+
261
+ Rack 3 removed a number of constants. If your app or a third-party middleware
262
+ uses one, it fails at load time:
263
+
264
+ | Removed | Use instead |
265
+ |---|---|
266
+ | `Rack::Utils::HeaderHash` | `Rack::Headers` |
267
+ | `Rack::File` | `Rack::Files` |
268
+ | `Rack::Session::Cookie` | the `rack-session` gem (Sinatra already depends on it) |
269
+ | `Rack::Handler` | `Rackup::Handler`, from the `rackup` gem |
270
+
271
+ If the failure comes from a gem rather than your code, check whether it has a
272
+ Rack 3 compatible release. This is the most common reason an upgrade stalls,
273
+ and it is nothing Startback can shield you from.
274
+
275
+ Sinatra 4 also dropped the `IndifferentHash` initializer, disabled
276
+ `session_hijacking` protection by default, and removed
277
+ `Rack::Protection::EncryptedCookie` (cookies are still encrypted, by
278
+ `Rack::Session::Cookie`). And if you start the server by running the app file
279
+ directly rather than through `config.ru` + puma, you now need the `rackup` gem
280
+ in your Gemfile.
281
+
282
+ ---
283
+
284
+ ## 5. Puma: lifecycle hooks renamed, and a new default bind
285
+
286
+ Puma goes from 6 to 8, crossing two majors. Startback never loads puma itself
287
+ -- it ships it for you -- so nothing here is detectable by Startback's tests.
288
+
289
+ **Puma 7 renamed every lifecycle hook.** If your `puma.rb` uses the old names
290
+ they are simply not called, silently:
291
+
292
+ | Before | After |
293
+ |---|---|
294
+ | `on_worker_boot` | `before_worker_boot` |
295
+ | `on_worker_shutdown` | `before_worker_shutdown` |
296
+ | `on_restart` | `before_restart` |
297
+ | `on_booted` | `after_booted` |
298
+ | `on_stopped` | `after_stopped` |
299
+ | `on_refork` | `before_refork` |
300
+ | `on_thread_start` | `before_thread_start` |
301
+
302
+ This matters most for database connection handling, which is usually exactly
303
+ what those hooks do.
304
+
305
+ **Puma 7 also** made `preload_app!` the default in clustered mode, and requires
306
+ a config instance to be `clamp`-ed before values are read.
307
+
308
+ **Puma 8** changed the default production bind from `0.0.0.0` to `::` when an
309
+ IPv6 interface is available. In a container that publishes ports over IPv4
310
+ only, this can make the service unreachable. Bind explicitly if you care:
311
+
312
+ ```ruby
313
+ # puma.rb
314
+ bind 'tcp://0.0.0.0:3000'
315
+ ```
316
+
317
+ **Not ready?** `gem 'puma', '~> 6.0'` in your Gemfile. Startback accepts
318
+ `>= 6.0.2, < 9.0`.
319
+
320
+ ---
321
+
322
+ ## 6. `undefined method 'fast_generate' for module JSON`
323
+
324
+ **Why.** json 3 removed `JSON.fast_generate`.
325
+
326
+ **Fix.** `JSON.generate`. It is the same output; `fast_generate` only skipped
327
+ the circular-reference check.
328
+
329
+ Startback used it internally in `Security::RateLimiter` and
330
+ `Caching::EntityCache#encode_key`, and both now use `JSON.generate`. **The
331
+ generated strings are identical**, so cache entries and rate-limit counters
332
+ survive the upgrade -- no cache flush needed.
333
+
334
+ ---
335
+
336
+ ## 7. jwt 2 to 3
337
+
338
+ Only relevant if your application uses JWT; Startback ships the gem but never
339
+ loads it. jwt 3 is a real break:
340
+
341
+ * RSA keys must be **at least 2048 bits**. Shorter keys now raise.
342
+ * Base64 decoding follows RFC 4648 strictly; tolerantly-encoded tokens that
343
+ used to decode now fail.
344
+ * The payload cannot be read before the signature is verified.
345
+ * `HS512256` is gone.
346
+ * Custom algorithms must include `JWT::JWA::SigningAlgorithm`.
347
+ * Since 3.3: if you rescue `JWT::DecodeError`, `JWT::IncorrectAlgorithm` or
348
+ `ArgumentError` **around `JWT.encode`**, rescue `JWT::EncodeError` instead.
349
+ Decoding is unaffected.
350
+
351
+ Read jwt's own `UPGRADING.md` before taking it. **Not ready?**
352
+ `gem 'jwt', '~> 2.1'`. Startback accepts `>= 2.1, < 4.0`.
353
+
354
+ ---
355
+
356
+ ## 8. finitio 0.12 to 1.0: check your `.fio` schemas
357
+
358
+ Two removals affect schemas, and one of them changes meaning silently:
359
+
360
+ * `Fixnum` and `Bignum` are gone from `finitio/data`. Use `Integer`. This one
361
+ fails loudly.
362
+ * **`FalseClass` was a bug and is now fixed.** It used to be an alias of
363
+ `.TrueClass`, so it accepted `true` and rejected `false`. If a schema of
364
+ yours worked around that -- writing `FalseClass` where it meant a *true*
365
+ value -- it now means the opposite.
366
+
367
+ Grep before upgrading:
368
+
369
+ ```sh
370
+ grep -rn "Fixnum\|Bignum\|FalseClass" --include=*.fio .
371
+ ```
372
+
373
+ **Not ready?** `gem 'finitio', '~> 0.12'`. Startback accepts `>= 0.12, < 2.0`.
374
+
375
+ ---
376
+
377
+ ## 9. bunny 2 to 3, if you use the event bus
378
+
379
+ Applies to `Startback::Event::Bus::Bunny::Async` only.
380
+
381
+ * Versioned delivery tags are removed.
382
+ * Passive declarations (`passive: true`) are no longer replayed by topology
383
+ recovery.
384
+ * The `openssl` gem >= 3.3 is now required, which means a native build --
385
+ watch slim/alpine images.
386
+
387
+ **Heads up on coverage:** Startback's test matrix has no RabbitMQ, so the Bunny
388
+ bus is upgraded but *unverified by the suite*. If you use it, exercise it in a
389
+ staging environment rather than trusting the green build. *(Fixed in 2.1.0 --
390
+ and it found a RabbitMQ 4.3 incompatibility. See the 2.1.0 section above.)*
391
+
392
+ **Not ready?** `gem 'bunny', '~> 2.14'`. Startback accepts `>= 2.14, < 4.0`.
393
+
394
+ ---
395
+
396
+
397
+ ## 10. webspicy must move to 1.x
398
+
399
+ **You will see** `bundle install` fail outright:
400
+
401
+ ```
402
+ Because every version of startback depends on rack-robustness >= 2.0, < 3.0
403
+ and webspicy >= 0.25.0 depends on rack-robustness >= 1.2, < 2.0,
404
+ every version of startback is incompatible with webspicy >= 0.25.0.
405
+ ```
406
+
407
+ **Why.** Every webspicy 0.27.x release caps `finitio < 0.13`, `http < 6.0` and
408
+ `rack-robustness < 2.0`. webspicy 1.0 widened all three.
409
+
410
+ **Fix.** `gem 'webspicy', '>= 1.0', '< 2.0'`.
411
+
412
+ There is no way around this one, and no quiet degradation: bundler refuses to
413
+ resolve.
414
+
415
+ Coming from 0.26 or earlier, note that webspicy validates unstructured
416
+ response bodies against `output_schema` since 0.27, where it used to skip
417
+ them. A content-negotiating endpoint may need its schema widened accordingly
418
+ (e.g. `[Todo]|Csv` for one serving both JSON and CSV).
419
+
420
+ ---
421
+
422
+ ## Opting out, gem by gem
423
+
424
+ Startback deliberately declares **wide ranges** for the dependencies it does
425
+ not use itself, so you can stay on an older major while still upgrading
426
+ Startback. Add the pin to your own Gemfile:
427
+
428
+ | Gem | Startback accepts | Pin to stay put |
429
+ |---|---|---|
430
+ | puma | `>= 6.0.2, < 9.0` | `gem 'puma', '~> 6.0'` |
431
+ | jwt | `>= 2.1, < 4.0` | `gem 'jwt', '~> 2.1'` |
432
+ | http | `>= 5.0, < 7.0` | `gem 'http', '~> 5.0'` |
433
+ | bunny | `>= 2.14, < 4.0` | `gem 'bunny', '~> 2.14'` |
434
+ | finitio | `>= 0.12, < 2.0` | `gem 'finitio', '~> 0.12'` |
435
+ | json | `>= 2.6, < 4.0` | `gem 'json', '~> 2.6'` |
436
+
437
+ These cannot be opted out of, because Startback's own code depends on them:
438
+
439
+ | Gem | Required |
440
+ |---|---|
441
+ | sinatra | `>= 4.0, < 5.0` -- the middlewares use `Rack::Headers`, Rack 3 only |
442
+ | rack-robustness | `>= 2.0, < 3.0` -- `Shield` and `CatchAll` subclass it |
443
+ | Ruby | `>= 3.2` |
444
+
445
+ ---
446
+
447
+ ## Checklist
448
+
449
+ - [ ] Ruby >= 3.2
450
+ - [ ] `RACK_ENV` set in the test suite, at the top of `spec_helper.rb`
451
+ - [ ] Response triples built by hand use lowercase header names
452
+ - [ ] Response bodies are arrays, not bare Strings
453
+ - [ ] No `Rack::Utils::HeaderHash` / `Rack::File` / `Rack::Handler` left, in
454
+ your code or your gems
455
+ - [ ] `puma.rb` lifecycle hooks renamed; bind address checked
456
+ - [ ] No `JSON.fast_generate` left
457
+ - [ ] `.fio` schemas grepped for `Fixnum`, `Bignum`, `FalseClass`
458
+ - [ ] webspicy on 1.x
459
+ - [ ] jwt / bunny reviewed, or pinned
460
+ - [ ] Bunny event bus exercised somewhere real, since CI does not cover it
@@ -144,10 +144,10 @@ module Startback
144
144
 
145
145
  # Encodes a context free key to an actual cache key.
146
146
  #
147
- # Default implementation uses JSON.fast_generate but MAY be
147
+ # Default implementation uses JSON.generate but MAY be
148
148
  # overriden.
149
149
  def encode_key(context_free_key)
150
- JSON.fast_generate(context_free_key)
150
+ JSON.generate(context_free_key)
151
151
  end
152
152
 
153
153
  # Returns whether `cached` entity seems fresh enough to