From 48ed94a7f08e803c67a272c78da70f80a24e86e3 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 17 Sep 2026 14:53:30 -0700 Subject: [PATCH 1/3] Fix cache_fill range request detection and a Content-Length config gap The range header lookup in bgFetchAllowed() had a misplaced closing paren: TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), header.size() == TS_SUCCESS) TSMimeHdrFieldFind() returns a TSMLoc rather than a TSReturnCode, so the comparison belonged on the call, not on the length argument. Because TS_SUCCESS is 0, "header.size() == TS_SUCCESS" evaluated to false and the plugin searched for a zero length field name, which never matches. That left hasRangeHdrs false for every request and broke both options that depend on it: --range-req-only=true suppressed the background fetch even for real range requests, and --cache-range-req=false never suppressed anything. Pass the name length and test the returned handle, and release the handles the lookup hands back, which the broken call never produced. Also reject a Content-Length condition with no size value, the way background_fetch already does. Without the check an empty value leaves both the parsed and remaining views empty, the length comparison succeeds, and a "<= 0" rule is installed from a line that specified no size at all. Adds an autest covering both directions of the --range-req-only decision: a range request fills the cache, a plain request does not. --- plugins/experimental/cache_fill/configs.cc | 9 +- .../cache_fill_range_req_only.test.py | 145 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py diff --git a/plugins/experimental/cache_fill/configs.cc b/plugins/experimental/cache_fill/configs.cc index c9d1b7b3ff9..cf09a61a384 100644 --- a/plugins/experimental/cache_fill/configs.cc +++ b/plugins/experimental/cache_fill/configs.cc @@ -168,6 +168,10 @@ BgFetchConfig::readConfig(const char *config_file) continue; } ++cfg_value; // Drop leading character. + if (cfg_value.empty()) { + TSError("[%s] missing Content-Length size value, skipping config value", PLUGIN_NAME); + continue; + } swoc::TextView parsed; auto n = swoc::svtou(cfg_value, &parsed); if (parsed.size() != cfg_value.size()) { @@ -213,11 +217,14 @@ BgFetchConfig::bgFetchAllowed(TSHttpTxn txnp) const if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { bool hasRangeHdrs = false; for (auto const &header : FILTER_HEADERS) { - if (TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), header.size() == TS_SUCCESS)) { + TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), static_cast(header.size())); + if (field_loc != TS_NULL_MLOC) { + TSHandleMLocRelease(bufp, hdr_loc, field_loc); hasRangeHdrs = true; break; } } + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); if (!hasRangeHdrs && _range_req_only) { Dbg(dbg_ctl, "_range_req_only=true; This transaction is not a range request"); return false; diff --git a/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py b/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py new file mode 100644 index 00000000000..9b2a670ff80 --- /dev/null +++ b/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py @@ -0,0 +1,145 @@ +''' +Test the cache_fill plugin's --range-req-only option +''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Test.Summary = ''' +cache_fill --range-req-only=true fills the cache for a range request and +leaves a non-range request alone. +''' + +Test.SkipUnless( + Condition.PluginExists('cache_fill.so'), + Condition.PluginExists('xdebug.so'), +) +# Skip until cache_fill supports UDS +Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) +Test.ContinueOnFail = True +Test.testName = "cache_fill_range_req_only" + + +class CacheFillRangeReqOnlyTest: + ''' + --range-req-only=true means "only background fill when the client sent a + range request". That decision is made by looking for a Range (or + conditional) header on the client request, so the test drives both sides of + it: a range request must fill, a plain request must not. + ''' + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + self.curl_and_args = '-s -D /dev/stdout -v -x localhost:{} -H "x-debug: x-cache,x-cache-key"'.format(self.ts.Variables.port) + + def addCacheableResponse(self, path, etag): + req = { + "headers": "GET {} HTTP/1.1\r\n".format(path) + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=300\r\n" + "Date: Tue, 17 Sep 2026 00:00:00 GMT\r\n" + + "Last-Modified: Mon, 16 Sep 2026 00:00:00 GMT\r\n" + "Connection: close\r\n" + 'Etag: {}\r\n'.format(etag), + "timestamp": "1469733493.993", + "body": "hello hello" + } + self.server.addResponse("sessionlog.json", req, res) + + def setUpOriginServer(self): + self.server = Test.MakeOriginServer("server") + # Distinct paths that are not a prefix of one another, so a remap rule + # cannot match the wrong one. + self.addCacheableResponse("/fill_on_range", "994324f6-78f6bc3e8d639") + self.addCacheableResponse("/skip_when_plain", "772102f4-56f4bc1e6d417") + + def setUpTS(self): + self.ts = Test.MakeATSProcess("ts") + + self.ts.Disk.remap_config.AddLines( + [ + 'map http://www.example.com/fill_on_range http://127.0.0.1:{}/fill_on_range'.format(self.server.Variables.Port) + + ' @plugin=cache_fill.so @pparam=--range-req-only=true', + 'map http://www.example.com/skip_when_plain http://127.0.0.1:{}/skip_when_plain'.format( + self.server.Variables.Port) + ' @plugin=cache_fill.so @pparam=--range-req-only=true', + ]) + + self.ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') + + self.ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_fill', + }) + + def test_rangeRequestPrimesCache(self): + # A range request is what --range-req-only asks for, so the background + # fetch should run and pull the whole object into cache. The client + # still sees the origin's 200 on this first pass. + tr = Test.AddTestRun("Range request is a miss and starts a background fill") + ps = tr.Processes.Default + ps.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) + ps.StartBefore(Test.Processes.ts) + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "expected the origin's 200") + tr.StillRunningAfter = self.ts + + def test_rangeRequestFilledCache(self): + # The fill above must have happened: the object is now served from + # cache, and ATS synthesizes the 206 from the cached full body. This is + # the assertion that fails when the Range header lookup is broken. + tr = Test.AddTestRun("Second range request is served from the filled cache") + ps = tr.Processes.Default + tr.DelayStart = 2 # let the background fetch finish writing to cache + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache: hit-fresh", "expected a cache hit from the background fill") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Partial Content", "expected 206 from the cached object") + ps.Streams.stdout.Content += Testers.ContainsExpression( + "Content-Range: bytes 0-4/11", "expected Content-Range: bytes 0-4/11") + tr.StillRunningAfter = self.ts + + def test_plainRequestIsAMiss(self): + tr = Test.AddTestRun("Non-range request is a miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/skip_when_plain', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = self.ts + + def test_plainRequestDidNotFillCache(self): + # The other direction of the same decision: --range-req-only=true means + # a plain request must NOT be filled, so this stays a miss. Without + # this run, a fix that simply always fills would still look correct. + tr = Test.AddTestRun("Non-range request did not background fill") + ps = tr.Processes.Default + tr.DelayStart = 2 # same window the fill would have had + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/skip_when_plain', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected no background fill for a plain request") + tr.StillRunningAfter = self.ts + + def run(self): + self.test_rangeRequestPrimesCache() + self.test_rangeRequestFilledCache() + self.test_plainRequestIsAMiss() + self.test_plainRequestDidNotFillCache() + + +CacheFillRangeReqOnlyTest().run() From 4024204c632bccd711b852b1bcfb2c533abd1c32 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 17 Sep 2026 15:00:56 -0700 Subject: [PATCH 2/3] cache_fill autest: drop the stale Date and assert the decision, not cache state The fixed Date in the past against max-age=300 made every stored object immediately stale, so the cache-hit assertions could not pass; the origin supplies a current Date on its own. The non-range case also cannot be observed through cache state, because a plain cacheable response is stored by ordinary proxy caching whether or not the plugin background fills. Assert on the plugin's own logged decision instead, which is what distinguishes the two. --- .../cache_fill_range_req_only.test.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py b/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py index 9b2a670ff80..62cc3d7e49d 100644 --- a/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py +++ b/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py @@ -53,8 +53,7 @@ def addCacheableResponse(self, path, etag): } res = { "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=300\r\n" + "Date: Tue, 17 Sep 2026 00:00:00 GMT\r\n" + - "Last-Modified: Mon, 16 Sep 2026 00:00:00 GMT\r\n" + "Connection: close\r\n" + 'Etag: {}\r\n'.format(etag), + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=300\r\n" + "Connection: close\r\n" + 'Etag: {}\r\n'.format(etag), "timestamp": "1469733493.993", "body": "hello hello" } @@ -123,23 +122,24 @@ def test_plainRequestIsAMiss(self): ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") tr.StillRunningAfter = self.ts - def test_plainRequestDidNotFillCache(self): - # The other direction of the same decision: --range-req-only=true means - # a plain request must NOT be filled, so this stays a miss. Without - # this run, a fix that simply always fills would still look correct. - tr = Test.AddTestRun("Non-range request did not background fill") - ps = tr.Processes.Default - tr.DelayStart = 2 # same window the fill would have had - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/skip_when_plain', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected no background fill for a plain request") - tr.StillRunningAfter = self.ts + def check_plainRequestWasDeclined(self): + # The other direction of the same decision. Cache state cannot show + # this: a plain cacheable response is stored by ordinary proxy caching + # whether or not the plugin background fills, so both outcomes look the + # same from the client. Assert on the plugin's own decision instead. + # Under a fix that simply always filled, this line would never appear. + self.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "_range_req_only=true; This transaction is not a range request", + "expected the plain request to be declined as a non-range request") + # ...and the range request must have been accepted, not declined. + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "scheduling background fetch", "expected the range request to schedule a background fetch") def run(self): self.test_rangeRequestPrimesCache() self.test_rangeRequestFilledCache() self.test_plainRequestIsAMiss() - self.test_plainRequestDidNotFillCache() + self.check_plainRequestWasDeclined() CacheFillRangeReqOnlyTest().run() From 36008c30489baad0a6a1f25e9f6fa443a1e1da27 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 17 Sep 2026 16:44:45 -0700 Subject: [PATCH 3/3] cache_fill autest: fold the range-option tests into cache_fill.test.py Group the range-option coverage with the existing cache_fill test rather than keeping a separate file, in its own ATS process: the existing one loads cache_fill globally with default options, and a global instance hooks every transaction, which would background fill these paths regardless of the per-remap options and make the negative cases vacuous. Add the --cache-range-req=false case. No test in the tree set either option before this, and the same range header lookup drives both, so that branch had no coverage. Under the broken lookup hasRangeHdrs was always false and the branch never ran, which makes it an independent check on the same fix. --- .../pluginTest/cache_fill/cache_fill.test.py | 137 +++++++++++++++++ .../cache_fill_range_req_only.test.py | 145 ------------------ 2 files changed, 137 insertions(+), 145 deletions(-) delete mode 100644 tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py diff --git a/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py b/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py index 5584a602695..29647d98a29 100644 --- a/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py +++ b/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py @@ -229,4 +229,141 @@ def run(self): self.runTraffic() +class CacheFillRangeReqOnlyTest: + ''' + Cover the two options that hinge on detecting a range request: + --range-req-only=true (fill ONLY for a range request) and + --cache-range-req=false (do NOT fill for a range request). + + This runs in its own ATS process rather than sharing the one above, + because that one loads cache_fill globally with default options. A global + instance hooks every transaction, which would background fill these paths + regardless of the per-remap options and make the negative cases vacuous. + ''' + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + self.curl_and_args = '-s -D /dev/stdout -v -x localhost:{} -H "x-debug: x-cache,x-cache-key"'.format(self.ts.Variables.port) + + def addCacheableResponse(self, path, etag): + req = { + "headers": "GET {} HTTP/1.1\r\n".format(path) + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # No explicit Date: a fixed date in the past plus max-age makes the + # stored object stale on arrival, and every hit assertion would fail. + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=300\r\n" + "Connection: close\r\n" + 'Etag: {}\r\n'.format(etag), + "timestamp": "1469733493.993", + "body": "hello hello" + } + self.server.addResponse("sessionlog.json", req, res) + + def setUpOriginServer(self): + self.server = Test.MakeOriginServer("server-range") + # Distinct paths that are not a prefix of one another, so a remap rule + # cannot match the wrong one. + self.addCacheableResponse("/fill_on_range", "994324f6-78f6bc3e8d639") + self.addCacheableResponse("/skip_when_plain", "772102f4-56f4bc1e6d417") + self.addCacheableResponse("/decline_on_range", "883213f5-67f5bc2e7d528") + + def setUpTS(self): + self.ts = Test.MakeATSProcess("ts-range") + + port = self.server.Variables.Port + self.ts.Disk.remap_config.AddLines( + [ + 'map http://www.example.com/fill_on_range http://127.0.0.1:{}/fill_on_range'.format(port) + + ' @plugin=cache_fill.so @pparam=--range-req-only=true', + 'map http://www.example.com/skip_when_plain http://127.0.0.1:{}/skip_when_plain'.format(port) + + ' @plugin=cache_fill.so @pparam=--range-req-only=true', + 'map http://www.example.com/decline_on_range http://127.0.0.1:{}/decline_on_range'.format(port) + + ' @plugin=cache_fill.so @pparam=--cache-range-req=false', + ]) + + self.ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') + + self.ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_fill', + }) + + def test_rangeRequestPrimesCache(self): + # A range request is what --range-req-only asks for, so the background + # fetch should run and pull the whole object into cache. The client + # still sees the origin's 200 on this first pass. + tr = Test.AddTestRun("range-req-only: range request starts a background fill") + ps = tr.Processes.Default + ps.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) + ps.StartBefore(self.ts) + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "expected the origin's 200") + tr.StillRunningAfter = self.ts + + def test_rangeRequestFilledCache(self): + # The fill above must have happened: the object is now served from + # cache, and ATS synthesizes the 206 from the cached full body. This is + # the assertion that fails when the range header lookup is broken. + tr = Test.AddTestRun("range-req-only: second range request is served from the filled cache") + ps = tr.Processes.Default + tr.DelayStart = 2 # let the background fetch finish writing to cache + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache: hit-fresh", "expected a cache hit from the background fill") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Partial Content", "expected 206 from the cached object") + ps.Streams.stdout.Content += Testers.ContainsExpression( + "Content-Range: bytes 0-4/11", "expected Content-Range: bytes 0-4/11") + tr.StillRunningAfter = self.ts + + def test_plainRequestIsAMiss(self): + tr = Test.AddTestRun("range-req-only: non-range request is a miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/skip_when_plain', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = self.ts + + def test_cacheRangeReqFalseDeclinesRange(self): + # The mirror option: --cache-range-req=false must decline the fill for a + # range request. Under the broken lookup hasRangeHdrs was always false, + # so this branch never ran either -- which makes this case an + # independent check on the same fix. + tr = Test.AddTestRun("cache-range-req=false: range request is declined") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/decline_on_range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = self.ts + + def check_pluginDecisions(self): + # Cache state cannot show the negative cases: a plain cacheable response + # is stored by ordinary proxy caching whether or not the plugin + # background fills, so both outcomes look the same from the client. + # Assert on the plugin's own decisions instead. Under a fix that simply + # always filled, neither decline line would ever appear. + self.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "_range_req_only=true; This transaction is not a range request", + "expected the plain request to be declined as a non-range request") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "_cache_range_req=false; This transaction is a range request", + "expected the range request to be declined when --cache-range-req=false") + # ...and the --range-req-only range request must have been accepted. + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "scheduling background fetch", "expected the range request to schedule a background fetch") + + def run(self): + self.test_rangeRequestPrimesCache() + self.test_rangeRequestFilledCache() + self.test_plainRequestIsAMiss() + self.test_cacheRangeReqFalseDeclinesRange() + self.check_pluginDecisions() + + CacheFillTest().run() +CacheFillRangeReqOnlyTest().run() diff --git a/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py b/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py deleted file mode 100644 index 62cc3d7e49d..00000000000 --- a/tests/gold_tests/pluginTest/cache_fill/cache_fill_range_req_only.test.py +++ /dev/null @@ -1,145 +0,0 @@ -''' -Test the cache_fill plugin's --range-req-only option -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -Test.Summary = ''' -cache_fill --range-req-only=true fills the cache for a range request and -leaves a non-range request alone. -''' - -Test.SkipUnless( - Condition.PluginExists('cache_fill.so'), - Condition.PluginExists('xdebug.so'), -) -# Skip until cache_fill supports UDS -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True -Test.testName = "cache_fill_range_req_only" - - -class CacheFillRangeReqOnlyTest: - ''' - --range-req-only=true means "only background fill when the client sent a - range request". That decision is made by looking for a Range (or - conditional) header on the client request, so the test drives both sides of - it: a range request must fill, a plain request must not. - ''' - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - self.curl_and_args = '-s -D /dev/stdout -v -x localhost:{} -H "x-debug: x-cache,x-cache-key"'.format(self.ts.Variables.port) - - def addCacheableResponse(self, path, etag): - req = { - "headers": "GET {} HTTP/1.1\r\n".format(path) + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - res = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=300\r\n" + "Connection: close\r\n" + 'Etag: {}\r\n'.format(etag), - "timestamp": "1469733493.993", - "body": "hello hello" - } - self.server.addResponse("sessionlog.json", req, res) - - def setUpOriginServer(self): - self.server = Test.MakeOriginServer("server") - # Distinct paths that are not a prefix of one another, so a remap rule - # cannot match the wrong one. - self.addCacheableResponse("/fill_on_range", "994324f6-78f6bc3e8d639") - self.addCacheableResponse("/skip_when_plain", "772102f4-56f4bc1e6d417") - - def setUpTS(self): - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.remap_config.AddLines( - [ - 'map http://www.example.com/fill_on_range http://127.0.0.1:{}/fill_on_range'.format(self.server.Variables.Port) + - ' @plugin=cache_fill.so @pparam=--range-req-only=true', - 'map http://www.example.com/skip_when_plain http://127.0.0.1:{}/skip_when_plain'.format( - self.server.Variables.Port) + ' @plugin=cache_fill.so @pparam=--range-req-only=true', - ]) - - self.ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') - - self.ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_fill', - }) - - def test_rangeRequestPrimesCache(self): - # A range request is what --range-req-only asks for, so the background - # fetch should run and pull the whole object into cache. The client - # still sees the origin's 200 on this first pass. - tr = Test.AddTestRun("Range request is a miss and starts a background fill") - ps = tr.Processes.Default - ps.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) - ps.StartBefore(Test.Processes.ts) - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "expected the origin's 200") - tr.StillRunningAfter = self.ts - - def test_rangeRequestFilledCache(self): - # The fill above must have happened: the object is now served from - # cache, and ATS synthesizes the 206 from the cached full body. This is - # the assertion that fails when the Range header lookup is broken. - tr = Test.AddTestRun("Second range request is served from the filled cache") - ps = tr.Processes.Default - tr.DelayStart = 2 # let the background fetch finish writing to cache - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/fill_on_range -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache: hit-fresh", "expected a cache hit from the background fill") - ps.Streams.stdout.Content += Testers.ContainsExpression("206 Partial Content", "expected 206 from the cached object") - ps.Streams.stdout.Content += Testers.ContainsExpression( - "Content-Range: bytes 0-4/11", "expected Content-Range: bytes 0-4/11") - tr.StillRunningAfter = self.ts - - def test_plainRequestIsAMiss(self): - tr = Test.AddTestRun("Non-range request is a miss") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/skip_when_plain', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - tr.StillRunningAfter = self.ts - - def check_plainRequestWasDeclined(self): - # The other direction of the same decision. Cache state cannot show - # this: a plain cacheable response is stored by ordinary proxy caching - # whether or not the plugin background fills, so both outcomes look the - # same from the client. Assert on the plugin's own decision instead. - # Under a fix that simply always filled, this line would never appear. - self.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "_range_req_only=true; This transaction is not a range request", - "expected the plain request to be declined as a non-range request") - # ...and the range request must have been accepted, not declined. - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "scheduling background fetch", "expected the range request to schedule a background fetch") - - def run(self): - self.test_rangeRequestPrimesCache() - self.test_rangeRequestFilledCache() - self.test_plainRequestIsAMiss() - self.check_plainRequestWasDeclined() - - -CacheFillRangeReqOnlyTest().run()