Skip to content

Test background_fetch rule matching - #13563

Open
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:test-background-fetch-rules
Open

Test background_fetch rule matching#13563
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:test-background-fetch-rules

Conversation

@bneradt

@bneradt bneradt commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Add an AuTest covering the Content-Length threshold and wildcard request-header exclusion rules. Include an unrestricted path as a positive control so the test confirms the background fetch path is active.

Add an AuTest covering the Content-Length threshold and wildcard request-header exclusion rules. Include an unrestricted path as a positive control so the test confirms the background fetch path is active.
Copilot AI lite review requested due to automatic review settings August 18, 2026 20:27
@bneradt bneradt added this to the 11.0.0 milestone Aug 18, 2026
@bneradt bneradt self-assigned this Aug 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cmcfarlen cmcfarlen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the new test against the plugin's actual rule evaluation. The wildcard case is solid, but two of the three scenarios don't verify what they're meant to — including the positive control, which currently can't observe a background fetch at all.

1. The positive control never exercises the background fetch path

MakeOriginServer defaults to lookup_key='{PATH}' (microserver.test.ext:31), and microserver stores entries with a plain dict assignment:

glb.replayDict[key] = response_header      # microserver.py:156

_configure_server() registers two responses for path /allowed — the 206 from the _add_range_response loop, then the explicit 200 — so the second overwrites the first. Every request to /allowed gets the 200 OK with the full 10-byte body, including the client's Range: bytes=0-4 request.

That matters because background_fetch only engages on a partial response:

// background_fetch.cc:536
if (TS_HTTP_STATUS_PARTIAL_CONTENT == status || (config->allow304() && TS_HTTP_STATUS_NOT_MODIFIED == status)) {

A 200 means no background fetch is ever started, so the "unrestricted path" demonstrates nothing about the plugin, and the 200 OK assertion passes for the wrong reason — it's asserting the origin ignored the Range header.

Two changes needed:

  • Key the origin so the range and non-range entries for /allowed can coexist. microserver supports {PATH}, {HOST}, {URL} and {%Field} (servers.py:139-160), so Test.MakeOriginServer("server", lookup_key='{PATH}{%Range}') works, and the allowed run then expects 206 Partial Content like the others.

  • Assert the positive signal. There's a debug line for exactly this:

    // background_fetch.cc:393
    Dbg(Bg_dbg_ctl, "Starting background fetch, replaying:");

    Without it, nothing in the test distinguishes "background fetch ran" from "the plugin did nothing," which is the whole point of having a control.

2. The Content-Length rule has no discriminating assertion

Both exclusion runs emit the same generic line:

// configs.cc:209
Dbg(Bg_dbg_ctl, "found %s rule match", r._exclude ? "exclude" : "include");

So Testers.ContainsExpression(r"found exclude rule match") is satisfied by the wildcard case on its own. If the Content-Length threshold stopped matching — a parse regression, an operator change, a comparison flip — this test still passes. The 206 Partial Content status assertion doesn't help either: the client gets a 206 whether or not the background fetch was suppressed.

The wildcard rule is uniquely pinned, via "Found X-Skip-Bg wild card" — that half is well done. The Content-Length rule, which is first in the PR title and summary, is the untested one.

Cheapest improvement is to also assert the parse line, which at least catches a config-syntax regression:

// configs.cc:174
"adding background_fetch content length rule {} for {}: {}"

A stronger version would add a fourth remap with exclude Content-Length >1000 — which should not match a 5-byte response — and assert a background fetch does start for it. That gives you a matched pair around the threshold instead of a single one-sided check.

Also worth a comment in the test: <1000 parses to LESS_THAN_OR_EQUAL (configs.cc:151), not strict less-than. The config syntax reads like <, so the boundary behavior is surprising, and a test is the natural place to record it.

3. StillRunningAfter assigned twice drops the first check

tr.StillRunningAfter = self._ts
tr.StillRunningAfter = self._server

TesterSet.Assign replaces the whole list:

def Assign(self, value):
    self._testers = [self._create_tester(value)]   # testerset.py:47
def Add(self, value):
    self._testers += [self._create_tester(value)]

= routes to Assign, += to Add. So only _server is verified still running after each run — if traffic_server crashes mid-test, the run passes. Same in _wait_for_wildcard_exclusion().

This is a pre-existing pattern in the tree (160 .test.py files do it), so not something to hold this PR on, and you're the one converting all of it to pytest where the read-only expectation objects make it impossible. But since this file is new, += on the second line costs nothing.

4. The waiter waits on the earlier of the two lines

_wait_for_wildcard_exclusion() blocks on "Found X-Skip-Bg wild card", which is logged in check_value (rules.cc:118) and returns true, after which check_field_configured's caller logs "found exclude rule match" (configs.cc:209). So the line the waiter confirms is flushed is emitted before the other asserted line. The narrow flush race the waiter exists to close is still open for "found exclude rule match".

Waiting on "found exclude rule match" instead covers both, since it's the later of the two.

Smaller items

  • Test.SkipUnless(Condition.PluginExists('background_fetch.so'),) has a stray trailing comma inside the call.
  • The tester description "wildcard request header rule should match by value" is slightly off — * matches presence, any value; check_value short-circuits before reading the value at all. "should match on presence regardless of value" is closer.
  • _add_curl_run derives the path from the host with host.split(".")[0], which is neat but means the path and hostname can't be varied independently. A path parameter would read more plainly, especially if the >1000 case above gets added.
  • The 206 responses pair Content-Range: bytes 0-4/10 with Content-Length: 5 and a 5-byte body, which is consistent — worth keeping in mind that the /10 total is what makes a background fetch worthwhile, so it shouldn't drift from the 200 response's Content-Length: 10.

The structure itself is good — class-based, one config file per rule type, one remap per scenario, and per-host isolation so the rules can't interfere. It's the assertions that need to become specific enough to fail when the rules break.

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.

3 participants