Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat responses to collapsed requests as fresh #1927

Closed
8 changes: 8 additions & 0 deletions doc/release-notes/release-7.sgml.in
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ This section gives an account of those changes in three categories:
<tag>external_acl_type</tag>
<p>Removed <em>%IDENT</em> format code with Ident protocol support.

<tag>collapsed_forwarding</tag>
<p>Squid no longer revalidates responses to collapsed requests, treating
all such responses as fresh. This change follows IETF HTTP Working Group
advice (in an HTTP gray area) and prevents arguably excessive freshness
checks for responses to collapsed requests. This change does not prevent
freshness checks for responses that were, at the time of a hit request,
either fully cached or still receiving response body bytes.

</descrip>

<sect1>Removed directives<label id="removeddirectives">
Expand Down
4 changes: 4 additions & 0 deletions src/StoreClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class StoreEntry;
class ACLFilledChecklist;
class LogTags;

// TODO: Merge store_client into StoreClient.
/// a storeGetPublic*() caller
class StoreClient: public Acl::ChecklistFiller
{
Expand All @@ -58,6 +59,9 @@ class StoreClient: public Acl::ChecklistFiller
bool mayInitiateCollapsing() const { return onCollapsingPath(); }
/// whether Squid configuration allows collapsing for this transaction
bool onCollapsingPath() const;

/// whether startCollapsingOn() was called and returned true
mutable bool didCollapse = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single clientReplyContext::didCollapse member cannot distinguish which request (initial or refresh) got collapsed. Code "works" because the new member is ignored by refresh transactions -- they do not consult refreshCheckHTTP() -- but this ambiguity is a design defect.

Fixing this requires merging store_client class into StoreClient class (I added a TODO about that) while refactoring clientReplyContext to move initial and refresh fetching code into two distinct/dedicated classes, each derived from the merged StoreClient class (instead of current complex/error-prone saveState() and restoreState() hacks that use the same classes for both fetches). That complex refactoring is outside this small PR scope, but I hope to be able to do it in the foreseeable future.

};

#if USE_DELAY_POOLS
Expand Down
9 changes: 8 additions & 1 deletion src/cf.data.pre
Original file line number Diff line number Diff line change
Expand Up @@ -6438,7 +6438,9 @@ LOC: Config.accessList.sendHit
DOC_START
Responses denied by this directive will not be served from the cache
(but may still be cached, see store_miss). This directive has no
effect on the responses it allows and on the cached objects.
effect on the responses it allows and on the cached objects. This
directive is applied to both regular from-cache responses and responses
reused by collapsed requests (see collapsed_forwarding).

Please see the "cache" directive for a summary of differences among
store_miss, send_hit, and cache directives.
Expand Down Expand Up @@ -7225,6 +7227,11 @@ DOC_START
requests hitting a stale cached object. Revalidation collapsing
is currently disabled for Squid instances containing SMP-aware
disk or memory caches and for Vary-controlled cached objects.

A response reused by the collapsed request is deemed fresh in that
request processing context -- Squid does not apply refresh_pattern and
internal freshness validation checks to collapsed transactions. Squid
does apply send_hit rules.
DOC_END

NAME: collapsed_forwarding_access
Expand Down
2 changes: 1 addition & 1 deletion src/client_side_reply.cc
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ clientReplyContext::cacheHit(const StoreIOBuffer result)
http->updateLoggingTags(LOG_TCP_MISS);
processMiss();
return;
} else if (!r->flags.internal && refreshCheckHTTP(e, r)) {
} else if (!r->flags.internal && !didCollapse && refreshCheckHTTP(e, r)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current implementation requires this refreshCheckHTTP() and other indirect refreshCheck() callers to remember to check StoreClient::didCollapse member. We should embed that check, so that it cannot be forgotten! Doing so requires adding a pure virtual StoreClient::refreshCheck() method and moving Foo-specific refreshCheckFoo() functions into its overrides. The latter, in turn, requires some refresh.h API refactoring. All that is outside this small PR scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about adding a TODO or XXX to that effect? PR comments are bound to be forgotten

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about adding a TODO or XXX to that effect?

Done in commit 9fc1ed5.

PR comments are bound to be forgotten

Yes, but a much bigger problem, in my experience, is that C++ comments like that often trigger negative votes and associated overheads, especially in areas where the existing code is quite messed up, obscuring underlying problems and proper solutions. C++ comments are also bound to become stale and, hence, misleading. That is why I only added two minimal C++ TODO/XXX comments and kept the rest of the discussion to PR comments. Let's hope it is going to be different this time.

debugs(88, 5, "clientCacheHit: in refreshCheck() block");
/*
* We hold a stale copy; it needs to be validated
Expand Down
4 changes: 2 additions & 2 deletions src/htcp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -981,10 +981,10 @@ htcpSpecifier::checkHit()
debugs(31, 3, "NO; public object not found");
} else if (!e->validToSend()) {
debugs(31, 3, "NO; entry not valid to send" );
} else if (refreshCheckHTCP(e, checkHitRequest.getRaw())) {
debugs(31, 3, "NO; cached response is stale");
} else if (e->hittingRequiresCollapsing() && !startCollapsingOn(*e, false)) {
debugs(31, 3, "NO; prohibited CF hit: " << *e);
} else if (!didCollapse && refreshCheckHTCP(e, checkHitRequest.getRaw())) {
debugs(31, 3, "NO; cached response is stale");
Comment on lines -984 to +987
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git diff does not make this obvious, but this PR reorders HTCP and ICP freshness check steps to use didCollapse member (set during startCollapsingOn() step) for avoiding refreshCheck*() step. This reordering should not have a significant effect in most cases because hittingRequiresCollapsing() quickly returns false for most transactions. In rare/exceptional collapsed forwarding cases, the new computation order may be slower (both checks may perform configuration-dependent ACL or ACL-like computations), but it is a fair price to pay for avoiding revalidation transactions in those cases.

} else {
debugs(31, 3, "YES!?");
hit = e;
Expand Down
4 changes: 2 additions & 2 deletions src/icp_v2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ ICPState::confirmAndPrepHit(const StoreEntry &e) const
if (!e.validToSend())
return false;

if (!Config.onoff.icp_hit_stale && refreshCheckICP(&e, request))
if (e.hittingRequiresCollapsing() && !startCollapsingOn(e, false))
return false;

if (e.hittingRequiresCollapsing() && !startCollapsingOn(e, false))
if (!Config.onoff.icp_hit_stale && !didCollapse && refreshCheckICP(&e, request))
return false;

return true;
Expand Down
5 changes: 4 additions & 1 deletion src/refresh.cc
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ refreshIsCachable(const StoreEntry * entry)
* minimum_expiry_time seconds delta (defaults to 60 seconds), to
* avoid objects which expire almost immediately, and which can't
* be refreshed.
*
* No hittingRequiresCollapsing() or didCollapse concerns here: This
* incoming response is fresh now, but we want to check whether it can be
* refreshed Config.minimum_expiry_time seconds later.
*/
int reason = refreshCheck(entry, nullptr, Config.minimum_expiry_time);
++ refreshCounts[rcStore].total;
Expand Down Expand Up @@ -574,7 +578,6 @@ refreshCheckHTTP(const StoreEntry * entry, HttpRequest * request)
++ refreshCounts[rcHTTP].total;
++ refreshCounts[rcHTTP].status[reason];
request->flags.staleIfHit = refreshIsStaleIfHit(reason);
// TODO: Treat collapsed responses as fresh but second-hand.
return (Config.onoff.offline || reason < 200) ? 0 : 1;
}

Expand Down
1 change: 1 addition & 0 deletions src/store_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ StoreClient::startCollapsingOn(const StoreEntry &e, const bool doingRevalidation
tags->collapsingHistory.otherCollapses++;
}

didCollapse = true;
debugs(85, 5, e << " doingRevalidation=" << doingRevalidation);
return true;
}
Expand Down
4 changes: 4 additions & 0 deletions src/store_digest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ storeDigestAddable(const StoreEntry * e)
}

/* still here? check staleness */
// Include hittingRequiresCollapsing() entries: They are fresh _now_, but we
// check future freshness. They should not have enough info to judge future
// freshness since we are still waiting for their response headers, but
// admins might configure Squid to consider such entries fresh anyway.
/* Note: We should use the time of the next rebuild, not (cur_time+period) */
if (refreshCheckDigest(e, Config.digest.rebuild_period)) {
debugs(71, 6, "storeDigestAdd: entry expires within " << Config.digest.rebuild_period << " secs, ignoring");
Expand Down
2 changes: 2 additions & 0 deletions src/urn.cc
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ urnHandleReply(void *data, StoreIOBuffer result)
return;
}

// XXX: Missing reply freshness checks (e.g., calling refreshCheckHTTP()).

urls = urnParseReply(urnState->parsingBuffer.toSBuf(), urnState->request->method);

if (!urls) { /* unknown URN error */
Expand Down
1 change: 1 addition & 0 deletions test-suite/test-functionality.sh
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ main() {
upgrade-protocols
cache-response
proxy-collapsed-forwarding
hit-revalidation
busy-restart
truncated-responses
malformed-request
Expand Down