You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,16 +9,16 @@ A search runs through a small, explicit pipeline — each stage is a separate, t
9
9
1.**Validate** — query params are checked before any network call: `query` is required, `count` is clamped (≤100), `lang`/`country` must be ISO two-letter codes, `from`/`to` must parse as ISO 8601, and `sortBy` ∈ {`publishedAt`, `relevance`}. Bad input fails fast with `400` instead of wasting an upstream call or quota.
10
10
2.**Cache** — parameters are normalized into a deterministic key and read through a pluggable store (in-memory by default, Redis when `REDIS_URL` is set). Cache failures are logged/metriced but do not fail article requests; identical in-flight misses in the same process share one upstream call.
11
11
3.**Upstream** — on a miss, GNews is called with a hard timeout; transport/provider failures surface as `502`, and repeated failures open a short circuit that returns `503` without amplifying the outage.
5.**Respond** — legacy endpoints return raw article arrays, while `/api/v1/*` returns `{ data, meta }` envelopes with request/cache metadata and structured error bodies.
14
14
15
15
Full diagram and component notes live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
16
16
17
17
## Production-oriented features
18
18
19
19
-**Security:**[Helmet](https://helmetjs.github.io/) headers, configurable rate limiting, optional `TRUST_PROXY` for correct client IPs behind a load balancer, optional **`CLIENT_API_KEYS`** + `X-API-Key` on `/api/*`.
20
-
-**Reliability:** Upstream HTTP timeouts, response validation, `502` for provider/transport failures, provider circuit breaker with `503` short-circuiting, graceful shutdown on `SIGTERM` / `SIGINT`.
21
-
-**Observability:** JSON logs via [Pino](https://getpino.io/), `x-request-id`, **`GET /metrics`** ([Prometheus](https://prometheus.io/) text format), cache hit/miss/error/coalescing + upstream latency/circuit metrics, and optional **OpenTelemetry** traces to OTLP (`OTEL_EXPORTER_OTLP_*`).
20
+
-**Reliability:** Upstream HTTP timeouts, response validation, stale-on-error cache fallback, `502` for provider/transport failures, provider circuit breaker with `503` short-circuiting, graceful shutdown on `SIGTERM` / `SIGINT`.
21
+
-**Observability:** JSON logs via [Pino](https://getpino.io/), `x-request-id`, **`GET /metrics`** ([Prometheus](https://prometheus.io/) text format), cache hit/miss/stale/error/coalescing + upstream latency/circuit metrics, and optional **OpenTelemetry** traces to OTLP (`OTEL_EXPORTER_OTLP_*`).
22
22
-**Kubernetes-style probes:**`GET /health` (liveness), `GET /ready` (readiness when the API key is configured).
23
23
-**Supply chain:**`npm audit` in CI; **SPDX SBOM** artifacts; Docker builds with **SBOM + provenance**; **dependency review** on PRs; optional **SLSA-style lockfile attestation** on `main`; lockfile-only installs.
24
24
-**Contract:** OpenAPI at **`GET /openapi.yaml`** (also on disk as [docs/openapi.yaml](docs/openapi.yaml)).
@@ -95,7 +95,7 @@ Base path: `/api`. Machine-readable schema: **`GET /openapi.yaml`** · source fi
95
95
|`GET`|`/health`| Liveness: `{ "status": "ok", "uptime": number }`. |
96
96
|`GET`|`/ready`| Readiness; `503` if `GNEWS_API_KEY` missing (non-test). |
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+10-4Lines changed: 10 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,8 +24,8 @@ flowchart LR
24
24
### A search, end to end
25
25
26
26
The cache is an optimization, not a dependency: hits skip the provider, misses are
27
-
coalesced per process, and cache failures fall through to the upstream rather than failing
28
-
the request.
27
+
coalesced per process, cache failures fall through to the upstream rather than failing
28
+
the request, and stale copies can serve reads when the provider fails.
29
29
30
30
```mermaid
31
31
sequenceDiagram
@@ -44,8 +44,12 @@ sequenceDiagram
44
44
else miss (read fail falls through here too)
45
45
Svc->>Svc: coalesce identical in-flight misses
46
46
Svc->>GNews: provider adapter: GET /api/v4/search (timeout)
47
-
GNews-->>Svc: normalized articles (502 on provider/transport error)
47
+
GNews-->>Svc: normalized articles
48
48
Svc->>Cache: set(key, articles, TTL 600s)
49
+
Svc->>Cache: set(stale key, articles, longer TTL)
50
+
else upstream failure and stale copy exists
51
+
Svc->>Cache: get(stale key)
52
+
Cache-->>Svc: stale articles
49
53
end
50
54
Svc-->>API: articles
51
55
API-->>Client: 200 + results (legacy) or { data, meta } (v1)
@@ -86,10 +90,12 @@ Article arrays are stored per normalized search key with a **600-second** TTL (`
86
90
87
91
The service treats the cache as a quota and latency optimization, not as a hard dependency. Read failures fall through to the upstream provider, write failures return the upstream response without caching it, and both paths emit warning logs plus cache error metrics. Within a single process, concurrent misses for the same normalized key are coalesced so only the first request calls the provider.
88
92
93
+
Successful upstream searches write both a fresh cache key and a longer-lived stale key. The fresh key protects latency and quota under normal conditions; the stale key is only read after an upstream failure. Versioned responses expose this through `meta.cache=stale`.
94
+
89
95
## Provider Circuit Breaker
90
96
91
97
The GNews provider adapter tracks consecutive provider failures. After `UPSTREAM_CIRCUIT_FAILURE_THRESHOLD` failures, it opens a cooldown window (`UPSTREAM_CIRCUIT_COOLDOWN_MS`) and returns `503` without making another upstream call. After the cooldown, one request is allowed through; success closes the circuit, while another failure opens it again.
92
98
93
99
## Metrics
94
100
95
-
`src/metrics/register.ts` exports a single Prometheus registry used by `/metrics`. HTTP middleware records response counts, while `newsService` and the provider adapter record cache hits/misses/errors/coalesced misses, upstream request outcomes, upstream latency buckets, and circuit breaker events.
101
+
`src/metrics/register.ts` exports a single Prometheus registry used by `/metrics`. HTTP middleware records response counts, while `newsService` and the provider adapter record cache hits/misses/errors/coalesced/stale misses, upstream request outcomes, upstream latency buckets, and circuit breaker events.
Copy file name to clipboardExpand all lines: docs/OPERATIONS.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,6 +10,7 @@
10
10
|`LOG_LEVEL`|`info` (non-test) | Pino level (`trace`–`fatal`, or `silent`). |
11
11
|`GNEWS_BASE_URL`|`https://gnews.io/api/v4`| Upstream provider base URL. Override only for local integration tests, benchmarks, or compatible provider mocks. |
|`STALE_CACHE_TTL_SEC`|`3600`| Longer-lived stale article cache TTL used only as an upstream-failure fallback (min effective value `>600`, max `86400`). |
13
14
|`UPSTREAM_CIRCUIT_FAILURE_THRESHOLD`|`3`| Consecutive provider failures before the circuit opens. |
14
15
|`UPSTREAM_CIRCUIT_COOLDOWN_MS`|`30000`| How long to short-circuit provider calls after the circuit opens (max `300000`). |
15
16
|`SHUTDOWN_TIMEOUT_MS`|`10000`| Force-exit if `server.close` does not finish. |
@@ -40,6 +41,7 @@
40
41
- Cache keys include normalized search parameters: query, count, `lang`, `country`, `from`, `to`, and `sortBy`.
41
42
- Cache reads/writes are non-fatal for article searches. If the cache backend is unavailable, the service logs a warning, increments cache error metrics, falls through to GNews on read failure, and still returns the upstream response on write failure.
42
43
- Identical in-flight misses are coalesced per process, so concurrent requests for the same normalized search wait on one upstream provider request.
44
+
- Successful searches are also written to a longer-lived stale cache key. If a later fresh miss hits an upstream failure and stale data is available, `/api/v1/*` returns `meta.cache=stale` with a `200` response instead of surfacing the provider outage.
43
45
44
46
On shutdown the server closes the Redis connection when that backend was used.
45
47
@@ -50,13 +52,13 @@ On shutdown the server closes the Redis connection when that backend was used.
|`news_cache_events_total`| `result=hit|miss|error|coalesced` | Cache lookup and in-flight coalescing behavior for article searches. |
54
-
|`news_cache_errors_total`| `operation=get|set` | Cache backend errors that were tolerated by falling through to upstream or returning an uncached upstream response. |
55
+
|`news_cache_events_total`| `result=hit|miss|error|coalesced|stale` | Cache lookup, stale fallback, and in-flight coalescing behavior for article searches. |
56
+
|`news_cache_errors_total`| `operation=get|set|get_stale|set_stale` | Cache backend errors that were tolerated by falling through to upstream, returning an uncached upstream response, or skipping stale fallback. |
|`news_upstream_circuit_events_total`| `event=opened|short_circuit|half_open|closed` | Provider circuit breaker state transitions and short-circuited requests. |
58
60
59
-
Use cache hit rate and coalesced miss counts to understand quota protection, cache error metrics to detect Redis/backend trouble, upstream latency/error metrics to separate provider trouble from local API trouble, and circuit events to see when repeated provider failures are being shed locally.
61
+
Use cache hit rate and coalesced miss counts to understand quota protection, stale counts to see when provider trouble is being hidden by cached data, cache error metrics to detect Redis/backend trouble, upstream latency/error metrics to separate provider trouble from local API trouble, and circuit events to see when repeated provider failures are being shed locally.
0 commit comments