This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a96d5056d Preserve the cache action while dispatching cache write 
events (#13487)
6a96d5056d is described below

commit 6a96d5056debcb5499496fb8ede73fcf901398d0
Author: Brian Neradt <[email protected]>
AuthorDate: Thu Aug 6 10:38:05 2026 -0500

    Preserve the cache action while dispatching cache write events (#13487)
    
    A transaction that loses the cache write lock and schedules a retry
    hands HttpSM a reusable captive action owned by HttpCacheSM. When the
    retry fires, HttpSM::state_cache_open_write() assigns the result of
    adjust_thread() to pending_action before releasing that delivered
    action. The callback is normally already on the correct thread, so
    adjust_thread() returns nullptr, and assigning nullptr to a
    PendingAction cancels whatever it was holding. The transaction thereby
    cancels its own captive action, and the cache read that the retry
    immediately issues comes back on an action already marked cancelled.
    Debug builds abort on the resulting assertion in HttpCacheSM, which is
    how this was found in production; release builds instead take the
    cancelled early return, drop a valid cache callback, and stall the
    transaction until it times out.
    
    This patch clears the delivered action before the thread adjustment
    rather than after it. Clearing first is safe because the cache action
    has already called back, and it means a genuine reschedule installs its
    event as the new pending action instead of canceling a captive action
    that is still in use.
    
    This also adds an autest in which two transactions contend for the
    cache write lock with read-while-writer disabled, so the loser's write
    retry delivers a synchronous cache read callback. That test aborts
    reliably on an unpatched debug build.
---
 src/proxy/http/HttpSM.cc                           |   8 +-
 .../cache/cache-write-retry-callback.test.py       |  25 ++++
 .../replay/cache-write-retry-callback.replay.yaml  | 134 +++++++++++++++++++++
 3 files changed, 165 insertions(+), 2 deletions(-)

diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 64173d0f3c..1ffa2d8b5b 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -2533,6 +2533,12 @@ HttpSM::state_cache_open_write(int event, void *data)
 {
   STATE_ENTER(state_cache_open_write, event);
 
+  // The cache action has already delivered this callback, so drop it before 
any
+  // thread adjustment below can assign over it. Assigning to pending_action
+  // cancels whatever it holds, and canceling the cache SM's reusable captive
+  // action here would break every cache operation this transaction makes 
later.
+  pending_action.clear_if_action_is(reinterpret_cast<Action *>(data));
+
   // Make sure we are on the "right" thread
   if (_ua.get_txn()) {
     pending_action = _ua.get_txn()->adjust_thread(this, event, data);
@@ -2544,8 +2550,6 @@ HttpSM::state_cache_open_write(int event, void *data)
     ink_release_assert(vc && vc->thread == this_ethread());
   }
 
-  pending_action.clear_if_action_is(reinterpret_cast<Action *>(data));
-
   ATS_PROBE1(milestone_cache_open_write_end, sm_id);
   milestones[TS_MILESTONE_CACHE_OPEN_WRITE_END] = ink_get_hrtime();
   pending_action                                = nullptr;
diff --git a/tests/gold_tests/cache/cache-write-retry-callback.test.py 
b/tests/gold_tests/cache/cache-write-retry-callback.test.py
new file mode 100644
index 0000000000..28e3e2ec64
--- /dev/null
+++ b/tests/gold_tests/cache/cache-write-retry-callback.test.py
@@ -0,0 +1,25 @@
+'''
+Verify a cache write retry does not cancel the cache read that follows it.
+'''
+#  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 = '''
+Verify a transaction that loses the cache write lock and retries can still
+receive the cache read callback that its retry triggers.
+'''
+
+Test.ATSReplayTest(replay_file="replay/cache-write-retry-callback.replay.yaml")
diff --git 
a/tests/gold_tests/cache/replay/cache-write-retry-callback.replay.yaml 
b/tests/gold_tests/cache/replay/cache-write-retry-callback.replay.yaml
new file mode 100644
index 0000000000..9e3627803b
--- /dev/null
+++ b/tests/gold_tests/cache/replay/cache-write-retry-callback.replay.yaml
@@ -0,0 +1,134 @@
+#  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.
+#
+# Two transactions request the same uncached object. The first takes the cache
+# write lock and holds it while its origin response is outstanding. The second
+# loses the write lock, and its scheduled retry hands a cache read event back 
to
+# HttpSM. HttpSM used to cancel its own cache action while dispatching that
+# event, which aborted debug builds on the canceled action assertion in
+# HttpCacheSM and dropped the callback in release builds.
+#
+# Read-while-writer is disabled so that the contending cache read fails
+# immediately rather than waiting for the writer, which is what makes the
+# callback arrive synchronously from within the write retry.
+
+meta:
+  version: "1.0"
+
+autest:
+  description: 'Verify a cache write retry preserves the cache read callback 
that follows it'
+  dns:
+    name: 'dns-write-retry-callback'
+
+  server:
+    name: 'origin-write-retry-callback'
+
+  client:
+    name: 'client-write-retry-callback'
+    process_config:
+      # The two sessions below have to overlap in time, so the client must not
+      # serialize them onto a single thread.
+      other_args: '--thread-limit 4'
+
+  ats:
+    name: 'ts-write-retry-callback'
+    process_config:
+      enable_cache: true
+
+    records_config:
+      proxy.config.diags.debug.enabled: 1
+      proxy.config.diags.debug.tags: 'http_cache|http_trans'
+      # READ_RETRY: retry the cache read when the write lock is lost.
+      proxy.config.http.cache.open_write_fail_action: 5
+      proxy.config.http.cache.max_open_write_retries: 1
+      proxy.config.http.cache.max_open_write_retry_timeout: 0
+      proxy.config.http.cache.max_open_read_retries: 2
+      proxy.config.http.cache.open_read_retry_time: 250
+      proxy.config.cache.enable_read_while_writer: 0
+
+    remap_config:
+      - from: "http://example.com/";
+        to: "http://backend.example.com:{SERVER_HTTP_PORT}/";
+
+    log_validation:
+      traffic_out:
+        excludes:
+          - expression: "[Ff]atal|failed assertion"
+            description: "Verify ATS does not abort while retrying the cache 
write"
+        contains:
+          - expression: "falling back to read retry"
+            description: "Verify the contending transaction retried its cache 
read after losing the write lock"
+          - expression: "READ_RETRY cache read failed, bypassing cache"
+            description: "Verify the retried cache read was delivered and 
handled"
+
+sessions:
+  # Take the cache write lock and hold it for the duration of this slow origin
+  # response.
+  - transactions:
+      - client-request:
+          method: GET
+          version: '1.1'
+          url: /contended-object
+          headers:
+            fields:
+              - [uuid, cache-writer]
+              - [Host, example.com]
+
+        server-response:
+          delay: 3s
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 16]
+              - [Cache-Control, "max-age=300"]
+              - [X-Response, writer]
+
+        proxy-response:
+          status: 200
+          headers:
+            fields:
+              - [X-Response, {value: writer, as: equal}]
+
+  # Lose the cache write lock to the session above. Both write attempts fail
+  # while that writer owns the lock, so the scheduled write retry delivers a
+  # cache read event to HttpSM, which issues another cache read. That read also
+  # fails, and the transaction proxies to the origin without caching.
+  - transactions:
+      - client-request:
+          delay: 200ms
+          method: GET
+          version: '1.1'
+          url: /contended-object
+          headers:
+            fields:
+              - [uuid, cache-contender]
+              - [Host, example.com]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 16]
+              - [Cache-Control, "max-age=300"]
+              - [X-Response, contender]
+
+        proxy-response:
+          status: 200
+          headers:
+            fields:
+              - [X-Response, {value: contender, as: equal}]

Reply via email to