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 ec6fceec3b escalate plugin: handle origin not reachable (#11847)
ec6fceec3b is described below

commit ec6fceec3bec8844b462edff61ab55a2dd8b131d
Author: Brian Neradt <[email protected]>
AuthorDate: Thu Dec 12 16:56:19 2024 -0600

    escalate plugin: handle origin not reachable (#11847)
    
    Update the escalate plugin to handle dispatching to the failover server
    if the original server is down.
    
    This also adds a new autest for the escalate plugin.
    
    Fixes: #11836
---
 plugins/escalate/escalate.cc                       | 102 ++++++++------
 .../pluginTest/escalate/escalate.test.py           | 130 ++++++++++++++++++
 .../escalate/escalate_failover.replay.yaml         | 126 ++++++++++++++++++
 .../escalate/escalate_original.replay.yaml         | 147 +++++++++++++++++++++
 4 files changed, 468 insertions(+), 37 deletions(-)

diff --git a/plugins/escalate/escalate.cc b/plugins/escalate/escalate.cc
index ba0f394d29..0f1104ffff 100644
--- a/plugins/escalate/escalate.cc
+++ b/plugins/escalate/escalate.cc
@@ -30,6 +30,9 @@
 #include <iterator>
 #include <map>
 
+#include "swoc/IPEndpoint.h"
+#include "swoc/TextView.h"
+
 // Constants and some declarations
 
 const char PLUGIN_NAME[] = "escalate";
@@ -72,12 +75,19 @@ struct EscalationState {
 char *
 MakeEscalateUrl(TSMBuffer mbuf, TSMLoc url, const char *host, size_t host_len, 
int &url_len)
 {
-  char *url_str = nullptr;
-
+  swoc::TextView   input_host_view{host, host_len};
+  std::string_view host_view;
+  std::string_view port_view;
+  swoc::IPEndpoint::tokenize(input_host_view, &host_view, &port_view);
   // Update the request URL with the new Host to try.
-  TSUrlHostSet(mbuf, url, host, host_len);
-  url_str = TSUrlStringGet(mbuf, url, &url_len);
-  Dbg(dbg_ctl, "Setting new URL to %.*s", url_len, url_str);
+  TSUrlHostSet(mbuf, url, host_view.data(), host_view.size());
+  if (port_view.size()) {
+    int const port_int = swoc::svtou(port_view);
+    TSUrlPortSet(mbuf, url, port_int);
+    Dbg(dbg_ctl, "Setting port to %d", port_int);
+  }
+  char *url_str = TSUrlStringGet(mbuf, url, &url_len);
+  Dbg(dbg_ctl, "Setting new URL from configured %.*s to %.*s", (int)host_len, 
host, url_len, url_str);
 
   return url_str;
 }
@@ -88,58 +98,76 @@ MakeEscalateUrl(TSMBuffer mbuf, TSMLoc url, const char 
*host, size_t host_len, i
 static int
 EscalateResponse(TSCont cont, TSEvent event, void *edata)
 {
-  TSHttpTxn                                      txn = 
static_cast<TSHttpTxn>(edata);
-  EscalationState                               *es  = 
static_cast<EscalationState *>(TSContDataGet(cont));
-  EscalationState::StatusMapType::const_iterator entry;
-  TSMBuffer                                      mbuf;
-  TSMLoc                                         hdrp, url;
-  TSHttpStatus                                   status;
-  char                                          *url_str = nullptr;
-  int                                            url_len, tries;
-
-  TSAssert(event == TS_EVENT_HTTP_READ_RESPONSE_HDR);
-
-  // First, we need the server response ...
-  if (TS_SUCCESS != TSHttpTxnServerRespGet(txn, &mbuf, &hdrp)) {
-    goto no_action;
+  TSHttpTxn        txn = static_cast<TSHttpTxn>(edata);
+  EscalationState *es  = static_cast<EscalationState *>(TSContDataGet(cont));
+  TSMBuffer        mbuf;
+  TSMLoc           hdrp, url;
+
+  TSAssert(event == TS_EVENT_HTTP_READ_RESPONSE_HDR || event == 
TS_EVENT_HTTP_SEND_RESPONSE_HDR);
+  bool const processing_connection_error = (event == 
TS_EVENT_HTTP_SEND_RESPONSE_HDR);
+
+  if (processing_connection_error) {
+    TSServerState const state = TSHttpTxnServerStateGet(txn);
+    if (state == TS_SRVSTATE_CONNECTION_ALIVE) {
+      // There is no connection error, so nothing to do.
+      TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+      return TS_EVENT_NONE;
+    }
   }
 
-  tries = TSHttpTxnRedirectRetries(txn);
+  int const tries = TSHttpTxnRedirectRetries(txn);
   if (0 != tries) { // ToDo: Future support for more than one retry-URL
-    goto no_action;
+    Dbg(dbg_ctl, "Not pursuing failover due previous redirect already, num 
tries: %d", tries);
+    TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+    return TS_EVENT_NONE;
+  }
+
+  int ret = 0;
+  if (processing_connection_error) {
+    ret = TSHttpTxnClientRespGet(txn, &mbuf, &hdrp);
+  } else {
+    ret = TSHttpTxnServerRespGet(txn, &mbuf, &hdrp);
+  }
+  if (TS_SUCCESS != ret) {
+    TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+    return TS_EVENT_NONE;
   }
-  Dbg(dbg_ctl, "This is try %d, proceeding", tries);
 
   // Next, the response status ...
-  status = TSHttpHdrStatusGet(mbuf, hdrp);
-  TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdrp); // Don't need this any more
+  TSHttpStatus const status = TSHttpHdrStatusGet(mbuf, hdrp);
+  TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdrp);
 
-  // See if we have an escalation retry config for this response code
-  entry = es->status_map.find(static_cast<unsigned>(status));
+  // See if we have an escalation retry config for this response code.
+  auto const entry = es->status_map.find(status);
   if (entry == es->status_map.end()) {
-    goto no_action;
+    TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+    return TS_EVENT_NONE;
   }
-
-  Dbg(dbg_ctl, "Found an entry for HTTP status %u", 
static_cast<unsigned>(status));
-  if (EscalationState::RETRY_URL == entry->second.type) {
-    url_str = TSstrdup(entry->second.target.c_str());
-    url_len = entry->second.target.size();
+  EscalationState::RetryInfo const &retry_info = entry->second;
+
+  Dbg(dbg_ctl, "Handling failover redirect for HTTP status %d", status);
+  char const *url_str = nullptr;
+  int         url_len = 0;
+  if (EscalationState::RETRY_URL == retry_info.type) {
+    url_str = TSstrdup(retry_info.target.c_str());
+    url_len = retry_info.target.size();
     Dbg(dbg_ctl, "Setting new URL to %.*s", url_len, url_str);
-  } else if (EscalationState::RETRY_HOST == entry->second.type) {
+  } else if (EscalationState::RETRY_HOST == retry_info.type) {
     if (es->use_pristine) {
       if (TS_SUCCESS == TSHttpTxnPristineUrlGet(txn, &mbuf, &url)) {
-        url_str = MakeEscalateUrl(mbuf, url, entry->second.target.c_str(), 
entry->second.target.size(), url_len);
+        url_str = MakeEscalateUrl(mbuf, url, retry_info.target.c_str(), 
retry_info.target.size(), url_len);
         TSHandleMLocRelease(mbuf, TS_NULL_MLOC, url);
       }
     } else {
       if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &mbuf, &hdrp)) {
         if (TS_SUCCESS == TSHttpHdrUrlGet(mbuf, hdrp, &url)) {
-          url_str = MakeEscalateUrl(mbuf, url, entry->second.target.c_str(), 
entry->second.target.size(), url_len);
+          url_str = MakeEscalateUrl(mbuf, url, retry_info.target.c_str(), 
retry_info.target.size(), url_len);
         }
         // Release the request MLoc
         TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdrp);
       }
     }
+    Dbg(dbg_ctl, "Setting host URL to %.*s", url_len, url_str);
   }
 
   // Now update the Redirect URL, if set
@@ -147,8 +175,7 @@ EscalateResponse(TSCont cont, TSEvent event, void *edata)
     TSHttpTxnRedirectUrlSet(txn, url_str, url_len); // Transfers ownership
   }
 
-// Set the transaction free ...
-no_action:
+  // Set the transaction free ...
   TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
   return TS_EVENT_NONE;
 }
@@ -229,5 +256,6 @@ TSRemapDoRemap(void *instance, TSHttpTxn txn, 
TSRemapRequestInfo * /* rri */)
   EscalationState *es = static_cast<EscalationState *>(instance);
 
   TSHttpTxnHookAdd(txn, TS_HTTP_READ_RESPONSE_HDR_HOOK, es->cont);
+  TSHttpTxnHookAdd(txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, es->cont);
   return TSREMAP_NO_REMAP;
 }
diff --git a/tests/gold_tests/pluginTest/escalate/escalate.test.py 
b/tests/gold_tests/pluginTest/escalate/escalate.test.py
new file mode 100644
index 0000000000..68c4e7a915
--- /dev/null
+++ b/tests/gold_tests/pluginTest/escalate/escalate.test.py
@@ -0,0 +1,130 @@
+'''
+'''
+#  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.
+
+import os
+from ports import get_port
+
+Test.Summary = '''
+Test the escalate plugin.
+'''
+
+Test.SkipUnless(Condition.PluginExists('escalate.so'))
+
+
+class EscalateTest:
+    """
+    Test the escalate plugin.
+    """
+
+    _replay_original_file: str = 'escalate_original.replay.yaml'
+    _replay_failover_file: str = 'escalate_failover.replay.yaml'
+
+    def __init__(self):
+        '''Configure the test run.'''
+        tr = Test.AddTestRun('Test escalate plugin.')
+        self._setup_dns(tr)
+        self._setup_servers(tr)
+        self._setup_ts(tr)
+        self._setup_client(tr)
+
+    def _setup_dns(self, tr: 'Process') -> None:
+        '''Set up the DNS server.
+
+        :param tr: The test run to add the DNS server to.
+        '''
+        self._dns = tr.MakeDNServer(f"dns", default='127.0.0.1')
+
+    def _setup_servers(self, tr: 'Process') -> None:
+        '''Set up the origin and failover servers.
+
+        :param tr: The test run to add the servers to.
+        '''
+        tr.Setup.Copy(self._replay_original_file)
+        tr.Setup.Copy(self._replay_failover_file)
+        self._server_origin = tr.AddVerifierServerProcess(f"server_origin", 
self._replay_original_file)
+        self._server_failover = 
tr.AddVerifierServerProcess(f"server_failover", self._replay_failover_file)
+
+        self._server_origin.Streams.All += Testers.ContainsExpression(
+            'uuid: GET', "Verify the origin server received the GET request.")
+        self._server_origin.Streams.All += Testers.ContainsExpression(
+            'uuid: GET_chunked', "Verify the origin server GET request for 
chunked content.")
+        self._server_origin.Streams.All += Testers.ContainsExpression(
+            'uuid: GET_failed', "Verify the origin server received the GET 
request that it returns a 502 with.")
+        self._server_origin.Streams.All += Testers.ExcludesExpression(
+            'uuid: GET_down_origin', "Verify the origin server did not receive 
the down origin request.")
+
+        self._server_failover.Streams.All += Testers.ContainsExpression(
+            'uuid: GET_failed', "Verify the failover server received the 
failed GET request.")
+        self._server_failover.Streams.All += Testers.ContainsExpression(
+            'uuid: GET_down_origin', "Verify the failover server received the 
GET request for the down origin.")
+
+        self._server_failover.Streams.All += Testers.ExcludesExpression(
+            'x-request: first', "Verify the failover server did not receive 
the GET request.")
+        self._server_failover.Streams.All += Testers.ExcludesExpression(
+            'uuid: GET_chunked', "Verify the failover server did not receive 
the GET request for chunked content.")
+
+    def _setup_ts(self, tr: 'Process') -> None:
+        '''Set up Traffic Server.
+
+        :param tr: The test run to add Traffic Server to.
+        '''
+        self._ts = tr.MakeATSProcess(f"ts", enable_cache=False)
+        # Select a port that is guaranteed to not be used at the moment.
+        dead_port = get_port(self._ts, "dead_port")
+        self._ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'http|escalate',
+                'proxy.config.dns.nameservers': 
f'127.0.0.1:{self._dns.Variables.Port}',
+                'proxy.config.dns.resolv_conf': 'NULL',
+                'proxy.config.http.redirect.actions': 'self:follow',
+                'proxy.config.http.number_of_redirections': 4,
+            })
+        self._ts.Disk.remap_config.AddLines(
+            [
+                f'map http://origin.server.com 
http://backend.origin.server.com:{self._server_origin.Variables.http_port} '
+                f'@plugin=escalate.so 
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port}',
+
+                # Now create remap entries for the multiplexed hosts: one that
+                # verifies HTTP, and another that verifies HTTPS.
+                f'map http://down_origin.server.com 
http://backend.down_origin.server.com:{dead_port} '
+                f'@plugin=escalate.so 
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} 
',
+            ])
+
+    def _setup_client(self, tr: 'Process') -> None:
+        '''Set up the client.
+
+        :param tr: The test run to add the client to.
+        '''
+        client = tr.AddVerifierClientProcess(f"client", 
self._replay_original_file, http_ports=[self._ts.Variables.port])
+        client.StartBefore(self._dns)
+        client.StartBefore(self._server_origin)
+        client.StartBefore(self._server_failover)
+        client.StartBefore(self._ts)
+
+        client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify 
there were no errors in the replay.')
+        client.Streams.All += Testers.ExcludesExpression('400 Bad', 'Verify 
none of the 400 responses make it to the client.')
+        client.Streams.All += Testers.ExcludesExpression('502 Bad', 'Verify 
none of the 502 responses make it to the client.')
+        client.Streams.All += Testers.ExcludesExpression('500 Internal', 
'Verify none of the 500 responses make it to the client.')
+        client.Streams.All += Testers.ContainsExpression('x-response: first', 
'Verify that the first response was received.')
+        client.Streams.All += Testers.ContainsExpression('x-response: second', 
'Verify that the second response was received.')
+        client.Streams.All += Testers.ContainsExpression('x-response: third', 
'Verify that the third response was received.')
+        client.Streams.All += Testers.ContainsExpression('x-response: fourth', 
'Verify that the fourth response was received.')
+
+
+EscalateTest()
diff --git a/tests/gold_tests/pluginTest/escalate/escalate_failover.replay.yaml 
b/tests/gold_tests/pluginTest/escalate/escalate_failover.replay.yaml
new file mode 100644
index 0000000000..3adffddeda
--- /dev/null
+++ b/tests/gold_tests/pluginTest/escalate/escalate_failover.replay.yaml
@@ -0,0 +1,126 @@
+#  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.
+
+meta:
+  version: "1.0"
+
+sessions:
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, first ]
+        - [ uuid, GET ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: first, as: equal } ]
+
+    server-response:
+      # The failover server should not receive this request since the original
+      # server should handle it.
+      status: 400
+      reason: Bad Request
+      headers:
+        fields:
+        - [ Content-Length, 0 ]
+
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_chunked
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, second ]
+        - [ uuid, GET_chunked ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: second, as: equal } ]
+
+    server-response:
+      # The failover server should not receive this request since the original
+      # server should handle it.
+      status: 400
+      reason: Bad Request
+      headers:
+        fields:
+        - [ Content-Length, 0 ]
+
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_failed
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, third ]
+        - [ uuid, GET_failed ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: third, as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 320000 ]
+        - [ X-Response, third ]
+
+  # This will not make it to the origin server since the Host is set to a
+  # non-responsive server. But the failover server should reply with a 200 OK.
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_down
+      headers:
+        fields:
+        - [ Host, down_origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, fourth ]
+        - [ uuid, GET_down_origin ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: fourth, as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Transfer-Encoding, chunked ]
+        - [ X-Response, fourth ]
+      content:
+        size: 320000
diff --git a/tests/gold_tests/pluginTest/escalate/escalate_original.replay.yaml 
b/tests/gold_tests/pluginTest/escalate/escalate_original.replay.yaml
new file mode 100644
index 0000000000..898464bee2
--- /dev/null
+++ b/tests/gold_tests/pluginTest/escalate/escalate_original.replay.yaml
@@ -0,0 +1,147 @@
+#  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.
+
+meta:
+  version: "1.0"
+
+sessions:
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, first ]
+        - [ uuid, GET ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: first, as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 320000 ]
+        - [ X-Response, first ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: first, as: equal } ]
+
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_chunked
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, second ]
+        - [ uuid, GET_chunked ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: second, as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Transfer-Encoding, chunked ]
+        - [ X-Response, second ]
+      content:
+        size: 320000
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: second, as: equal } ]
+
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_failed
+      headers:
+        fields:
+        - [ Host, origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, third ]
+        - [ uuid, GET_failed ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: third, as: equal } ]
+
+    server-response:
+      status: 502
+      reason: Bad Gateway
+      headers:
+        fields:
+        - [ Content-Length, 0 ]
+
+    proxy-response:
+      # The failover server should reply with a 200 OK.
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: third, as: equal } ]
+
+  # This will not make it to the origin server since the Host is set to a
+  # non-responsive server. But the failover server should reply with a 200 OK.
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /path/get_down
+      headers:
+        fields:
+        - [ Host, down_origin.server.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, fourth ]
+        - [ uuid, GET_down_origin ]
+
+    proxy-request:
+      method: "GET"
+      headers:
+        fields:
+        - [ X-Request, { value: fourth, as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 320000 ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: fourth, as: equal } ]

Reply via email to