Skip to content

cache: centralize dispatch and preserve response bodies - #4370

Closed
larry-dalmeida wants to merge 7 commits into
larry-almeida-zse-cache-metrics-prfrom
larry-almeida-zse-cache-dispatch-pr
Closed

larry-dalmeida wants to merge 7 commits into
larry-almeida-zse-cache-metrics-prfrom
larry-almeida-zse-cache-dispatch-pr

Conversation

@larry-dalmeida

@larry-dalmeida larry-dalmeida commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary of changes

Share request preparation between cold fetches and revalidation, preserving backend URL, Host, RequestURI and pipeline fallbacks. Prevent sharing Vary-star responses and preserve bypass bodies until delivery.

Direct dispatch bypasses later proxy filters; required filter ordering is documented and tested.

How to Verify

Shared setup

mkdir skipper-review && cd skipper-review
git clone https://github.com/zalando/skipper.git skipper && cd skipper
for PR in 4369 4370 4371 4372 4384 4385 4386 4387; do
  git fetch origin pull/$PR/head:pr-$PR
  git worktree add ../skipper-pr-$PR pr-$PR
done
cd ..
git clone https://github.com/larry-dalmeida/skipper-cache-e2e-tests.git
cd skipper-cache-e2e-tests
make verify SKIPPER_CHECKOUT=../skipper-pr-4387

make verify starts or reuses Minikube, the mock origin and the stack; runs the smoke and coalescing checks; and validates supported dashboard queries. It prints a report path and exits non-zero on failure. For outage evidence, run make chaos SKIPPER_CHECKOUT=../skipper-pr-4387 NAME=origin-stale; it includes its own timed load. Requirements: Python 3.9+, Make, Bash, Git, curl, kubectl, Minikube, Docker and Go compatible with Skipper's go.mod. Free ports: 8080, 8081, 6379, 4318, 9091, 9999, 9911 (8089 for endless load). Dynamic URLs: .runtime/urls.txt; stop with make stop.

Validated on #4387 (d767f636): smoke: 6,398 requests, 0 failures; origin-stale: 155 failed revalidations across 77 routes. Smoke metric checks: 42 PASS, 2 fault-only NOT EXERCISED, 0 FAIL.

Focused tests

go -C ../skipper-pr-4370 test -race -count=1 ./filters/cache -run '^(TestProxy_CacheBypassBodyOwnership|TestCacheFilter_BypassBodyReadError|TestCacheFilter_DispatchSelection|TestCacheFilter_PrepareDispatch_NoMutation|TestCacheFilter_Coalesce_PipelineFallback|TestProxy_Coalesce_PipelineFallback|TestCacheFilter_DispatchParity|TestCacheFilter_Coalesce_VaryStar_NotShared|TestCacheFilter_ColdMissCoalescing|TestRevalidationDispatch_RFCModeIgnoresBackendUrl|TestRevalidationDispatch_ForceModeUsesBackendUrl|TestProxy_Coalesce_RealFetch_ConcurrentColdMiss_SingleBackendCall|TestCacheFilter_Coalesce_LeaderNeverRefetches|TestCacheFilter_Coalesce_VaryMismatch_IndependentFetch|TestCacheFilter_Coalesce_ForceMode_SharesDespitePrivate|TestProxy_ContentfulCache_ResponseHeaderOrder|TestProxy_ContentfulCache_RequestRewriteOrder)$'

Expected: 17 focused tests pass; make verify confirms coalescing (cold burst: 1 origin call; warm follow-up: 0 extra; three Vary: * requests: 3 calls). Bypass-body tests cover no-store/Vary: *, not 405/500. #4370 mishandles Vary: Accept-Language on cold misses; #4371 fixes it. Direct origin fetches lack separate backend metrics/access-log records; ingress serve metrics/client access logging remain. cache.reval_backend_dispatch counts refreshes only; #4387 adds dispatch-labelled origin duration.

Checklist

Rollback Plan

Revert dependent PRs first, then this PR. No cache data-format migration is required.

References & Additional Context

Depends on #4369; followed by #4371 and #4372.

E2E Testing Results

E2E runs exercising dispatch under load and origin failure. Full results: larry-dalmeida/skipper-cache-e2e-tests/results

Test Scenarios:

Scenario Build Requests Failures Status
Smoke bda674b (top of stack) 6,306 0% ✓ Pass
Origin Stale bda674b (top of stack) 50,870 33.3% (503s while origin was failing) ✓ Pass
Smoke 8a80d62e + local changes 6,270 0% ✓ Pass
Origin Stale 8a80d62e + local changes 44,288 26.7% (503s while origin was failing) ✓ Pass
Valkey Kill 8a80d62e + local changes 47,653 0% ✓ Pass

Validation Panels:

These performance metrics demonstrate that centralized dispatch maintains efficiency and reliability across normal and degraded conditions:

  • Request Rate - Dispatch throughput under sustained load (bda674b smoke), ramping to ~54 req/s with zero failures. Illustrates that centralizing request preparation between cold fetches and revalidation does not introduce bottlenecks or contention. Smooth, stable throughput under high load proves the shared preparation pathway is efficient and reliable.
    Request Rate

  • Origin Latency (p95) - Origin fetch latency during the same smoke run, ~9.5–10 ms p95 consistently across all response statuses (200, 304, 404, 503). Validates that preserving bypass bodies and response state does not introduce latency overhead or unnecessary re-reads. Consistent latency across different status codes confirms that centralized dispatch handles diverse request/response paths without performance degradation.
    Origin Latency p95

  • Origin Responses by Status - Response distribution at the mock origin (bda674b origin-stale scenario): 503s peak at ~25 req/s while origin fails, 200s and 304s drop accordingly, then both recover cleanly. Demonstrates proper routing and fallback behavior during origin degradation. With centralized dispatch preserving pipeline fallbacks, the system correctly handles revalidation and bypass under failure conditions—304 responses occur for revalidation, and cache-serve + bypass paths maintain isolation even when origin is unavailable.
    Origin Responses by Status

coalesce() clones ctx.Request() (a server-received request, which
carries RequestURI and a relative URL) and passes it straight to
f.fetch (http.Client.Do). The client unconditionally rejects any
request with RequestURI set, so the leader's fetch inside the
singleflight closure fails on every single cold miss. coalesce()
then returns without calling ctx.Serve(), silently falling through
to skipper's normal backend-proxy pipeline instead - which resolves
the backend correctly on its own, so client-visible correctness is
unaffected and the bug has been invisible.

The real effect: singleflight cold-miss coalescing has never
actually worked. Concurrent requests to the same cold key each
independently fall through to the normal path instead of being
deduplicated to one origin request - origin-request amplification
under concurrent load. Verified locally: 10 concurrent requests to a
fresh cold key produced 10 origin hits before this fix, 1 after.

Fix mirrors the already-correct pattern a few hundred lines down in
this same file, in doRevalidate's direct-dispatch branch: resolve
ctx.BackendUrl(), set req.URL.Scheme/Host, clear req.RequestURI.

Fixing only that regressed TestProxy_L1Cache_VaryStar_NeverCached:
storeDecision() (coalesce()'s leader path only calls this one, not
Response()'s separate storage path) was missing the Vary: * never-
cache check Response() already has, and had simply never been
exercised before now - coalesce()'s fetch always crashed first.
Added the same check to storeDecision(), the one choke point both
callers share.

Added a regression test using a real httptest backend + proxytest,
not a stub: every existing Coalesce* test in this file stubs f.fetch
directly, which bypasses net/http's client-side RequestURI
validation entirely and cannot catch this class of bug. Confirmed
the new test fails (10 backend calls, not 1) with the fix reverted.

Pre-existing since the cache() filter's original commit (#3991),
through #4033, #4315, #4344 - not introduced by any of those.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Share validated request preparation between cold-miss coalescing and background revalidation. Preserve filtered direct requests and outgoing Host, replay original requests for RFC and unresolved backends, and report invalid backend URLs before pipeline fallback.

Use singleflight join barriers for deterministic concurrent tests, cover real-client dispatch and dynamic routing, and prohibit sharing Vary-star responses.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Buffer independent nonstoreable follower responses before closing origin streams. Preserve response metadata and count body-read failures as bypass errors without serving incomplete payloads.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Reproduce the cache-first Host and path contract violation through a real proxy, and verify that Host/prefix rewrites before cache preserve cold and warm behavior. Document the required ordering without changing the executor or external deployment routes.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Demonstrate that a response filter after cache is skipped on direct cold, warm, and stale paths. Verify moving it before cache applies client Cache-Control without mutating stored origin directives. Runtime and deployment sources unchanged.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
@zalando-robot

Copy link
Copy Markdown

Docker image "registry-write.opensource.zalan.do/teapot/skipper:d8f1d2e9be3439ffcd91eeba5f0bf80c79403578" is not based on an approved base image. Any production deployment relying on this image will be blocked.

To create a compliant Docker image of your application, you must reference an allowed Docker image as its base image in your Dockerfile. This base image must come from the Zalando Container Registry namespace library and use a recommended version as listed in the documentation.

Signed-off-by: Larry D Almeida <hello@larrydalmeida.com>
@zalando-robot

Copy link
Copy Markdown

Docker image "registry-write.opensource.zalan.do/teapot/skipper:064c34b21d23000eaab4ee3e009059861e95740b" is not based on an approved base image. Any production deployment relying on this image will be blocked.

To create a compliant Docker image of your application, you must reference an allowed Docker image as its base image in your Dockerfile. This base image must come from the Zalando Container Registry namespace library and use a recommended version as listed in the documentation.

@larry-dalmeida

Copy link
Copy Markdown
Collaborator Author

Superseded by #4394. Changes consolidated into five focused commits with published e2e evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants