Skip to content

Commit bb34d9e

Browse files
authored
Fix retry logic for TSHttpTxnServerAddrSet (issue #12611) (#12733)
When a plugin uses TSHttpTxnServerAddrSet() to set a server address and the connection fails, ATS should retry by calling the OS_DNS hook again to allow the plugin to provide an alternative address. However, the retry logic was missing handling for the USE_API case. Changes: - Added retry handling for ResolveInfo::OS_Addr::USE_API in handle_response_from_server() - Clears dns_info.resolved_p to trigger OS_DNS hook re-execution - Resets os_addr_style to TRY_DEFAULT to allow normal flow - Clears server_request to allow it to be rebuilt for new destination - Added api_server_addr_set_retried flag to prevent infinite retry loops - Added test plugin and autest to verify the fix
1 parent 163056e commit bb34d9e

File tree

6 files changed

+274
-4
lines changed

6 files changed

+274
-4
lines changed

doc/developer-guide/api/functions/TSHttpTxnServerAddrSet.en.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ Notes
5151
=====
5252

5353
If |TS| is configured to retry connections to origin servers and
54-
:func:`TSHttpTxnServerAddrGet` has been called, |TS| will return
55-
to TS_HTTP_OS_DNS_HOOK so to let the plugin set a different server
56-
address. Plugins should be prepared for TS_HTTP_OS_DNS_HOOK and any
57-
subsequent hooks to be called multiple times.
54+
:func:`TSHttpTxnServerAddrSet` has been called, |TS| will return
55+
to TS_HTTP_OS_DNS_HOOK once to let the plugin set a different server
56+
address. Plugins should be prepared for TS_HTTP_OS_DNS_HOOK to be
57+
called one additional time on connection failure.
5858

5959
Once a plugin calls :func:`TSHttpTxnServerAddrGet` any prior DNS
6060
resolution results are lost. The plugin should use

include/proxy/http/HttpTransact.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ class HttpTransact
683683
bool api_server_request_body_set = false;
684684
bool api_req_cacheable = false;
685685
bool api_resp_cacheable = false;
686+
bool api_server_addr_set_retried = false;
686687
bool reverse_proxy = false;
687688
bool url_remap_success = false;
688689
bool api_skip_all_remapping = false;

src/proxy/http/HttpTransact.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3770,6 +3770,18 @@ HttpTransact::handle_response_from_server(State *s)
37703770
// families - that is locked in by the client source address.
37713771
ats_force_order_by_family(s->current.server->dst_addr.family(), s->my_txn_conf().host_res_data.order);
37723772
return CallOSDNSLookup(s);
3773+
} else if (ResolveInfo::OS_Addr::USE_API == s->dns_info.os_addr_style && !s->api_server_addr_set_retried) {
3774+
// Plugin set the server address via TSHttpTxnServerAddrSet(). Clear resolution
3775+
// state to allow the OS_DNS hook to be called again, giving the plugin a chance
3776+
// to set a different server address for retry (issue #12611).
3777+
// Only retry once to avoid infinite loops if the plugin keeps setting failing addresses.
3778+
s->api_server_addr_set_retried = true;
3779+
s->dns_info.resolved_p = false;
3780+
s->dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_DEFAULT;
3781+
// Clear the server request so it can be rebuilt for the new destination
3782+
s->hdr_info.server_request.destroy();
3783+
TxnDbg(dbg_ctl_http_trans, "Retrying with plugin-set address, returning to OS_DNS hook");
3784+
return CallOSDNSLookup(s);
37733785
} else {
37743786
if ((s->txn_conf->connect_attempts_rr_retries > 0) &&
37753787
((s->current.retry_attempts.get() + 1) % s->txn_conf->connect_attempts_rr_retries == 0)) {

tests/gold_tests/pluginTest/tsapi/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ add_autest_plugin(test_TSHttpTxnServerAddrSet test_TSHttpTxnServerAddrSet.cc)
2020
add_autest_plugin(test_TSHttpSsnInfo test_TSHttpSsnInfo.cc)
2121
add_autest_plugin(test_TSVConnPPInfo test_TSVConnPPInfo.cc)
2222
add_autest_plugin(test_TSHttpTxnVerifiedAddr test_TSHttpTxnVerifiedAddr.cc)
23+
add_autest_plugin(test_TSHttpTxnServerAddrSet_retry test_TSHttpTxnServerAddrSet_retry.cc)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* Test plugin to reproduce issue #12611
21+
*
22+
* This plugin sets server addresses via TSHttpTxnServerAddrSet() in the OS_DNS hook.
23+
* On first call, it sets a non-routable address that will fail to connect.
24+
* On retry (if OS_DNS is called again), it sets a working address.
25+
*
26+
* BUG: On master, the OS_DNS hook is NOT called again on retry, so the connection
27+
* keeps failing with the bad address.
28+
*/
29+
30+
#include <ts/ts.h>
31+
32+
#include <arpa/inet.h>
33+
#include <netinet/in.h>
34+
#include <sys/socket.h>
35+
36+
#include <cstdint>
37+
#include <cstdlib>
38+
39+
namespace
40+
{
41+
42+
DbgCtl dbg_ctl{"test_TSHttpTxnServerAddrSet_retry"};
43+
44+
// Transaction argument index for per-transaction call count
45+
int g_txn_arg_idx = -1;
46+
47+
/** Get the OS_DNS call count for this transaction */
48+
int
49+
get_txn_call_count(TSHttpTxn txnp)
50+
{
51+
return static_cast<int>(reinterpret_cast<intptr_t>(TSUserArgGet(txnp, g_txn_arg_idx)));
52+
}
53+
54+
/** Set the OS_DNS call count for this transaction */
55+
void
56+
set_txn_call_count(TSHttpTxn txnp, int count)
57+
{
58+
TSUserArgSet(txnp, g_txn_arg_idx, reinterpret_cast<void *>(static_cast<intptr_t>(count)));
59+
}
60+
61+
/** Handler for OS_DNS hook */
62+
int
63+
handle_os_dns(TSCont /* cont */, TSEvent event, void *edata)
64+
{
65+
if (event != TS_EVENT_HTTP_OS_DNS) {
66+
TSError("[TSHttpTxnServerAddrSet_retry] Unexpected event in OS_DNS handler: %d", event);
67+
return TS_ERROR;
68+
}
69+
70+
TSHttpTxn txnp = (TSHttpTxn)edata;
71+
72+
// Increment per-transaction call count
73+
int call_count = get_txn_call_count(txnp) + 1;
74+
set_txn_call_count(txnp, call_count);
75+
76+
Dbg(dbg_ctl, "OS_DNS hook called, count=%d", call_count);
77+
TSError("[TSHttpTxnServerAddrSet_retry] OS_DNS hook called, count=%d", call_count);
78+
79+
// First call: set a bad address (192.0.2.1 is TEST-NET-1, non-routable)
80+
// Second call: set a good address (localhost)
81+
const char *addr_str;
82+
int port;
83+
84+
if (call_count == 1) {
85+
addr_str = "192.0.2.1"; // Non-routable - will fail
86+
port = 80;
87+
TSError("[TSHttpTxnServerAddrSet_retry] Attempt 1: Setting BAD address %s:%d (will fail)", addr_str, port);
88+
} else {
89+
addr_str = "127.0.0.1"; // Localhost - should work
90+
port = 8080;
91+
TSError("[TSHttpTxnServerAddrSet_retry] Attempt %d: Setting GOOD address %s:%d (should work)", call_count, addr_str, port);
92+
}
93+
94+
// Create sockaddr
95+
struct sockaddr_in sa;
96+
sa.sin_family = AF_INET;
97+
sa.sin_port = htons(port);
98+
if (inet_pton(AF_INET, addr_str, &sa.sin_addr) != 1) {
99+
TSError("[TSHttpTxnServerAddrSet_retry] Invalid address %s", addr_str);
100+
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
101+
return TS_ERROR;
102+
}
103+
104+
// Set the server address
105+
if (TSHttpTxnServerAddrSet(txnp, reinterpret_cast<struct sockaddr const *>(&sa)) != TS_SUCCESS) {
106+
TSError("[TSHttpTxnServerAddrSet_retry] Failed to set server address to %s:%d", addr_str, port);
107+
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
108+
return TS_ERROR;
109+
}
110+
111+
Dbg(dbg_ctl, "Set server address to %s:%d", addr_str, port);
112+
113+
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
114+
return TS_SUCCESS;
115+
}
116+
117+
/** Handler for TXN_CLOSE hook - report results */
118+
int
119+
handle_txn_close(TSCont /* cont */, TSEvent event, void *edata)
120+
{
121+
if (event != TS_EVENT_HTTP_TXN_CLOSE) {
122+
TSError("[TSHttpTxnServerAddrSet_retry] Unexpected event in TXN_CLOSE handler: %d", event);
123+
return TS_ERROR;
124+
}
125+
126+
TSHttpTxn txnp = (TSHttpTxn)edata;
127+
128+
// Get the per-transaction call count
129+
int call_count = get_txn_call_count(txnp);
130+
131+
TSError("[TSHttpTxnServerAddrSet_retry] Transaction closing. OS_DNS was called %d time(s)", call_count);
132+
133+
if (call_count == 1) {
134+
TSError("[TSHttpTxnServerAddrSet_retry] *** BUG CONFIRMED: OS_DNS hook was only called ONCE. "
135+
"Plugin could not retry with different address. This is issue #12611. ***");
136+
} else if (call_count >= 2) {
137+
TSError("[TSHttpTxnServerAddrSet_retry] SUCCESS: OS_DNS hook was called %d times. "
138+
"Plugin was able to retry with different address.",
139+
call_count);
140+
}
141+
142+
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
143+
return TS_SUCCESS;
144+
}
145+
146+
} // anonymous namespace
147+
148+
void
149+
TSPluginInit(int /* argc */, char const * /* argv */[])
150+
{
151+
Dbg(dbg_ctl, "Initializing plugin to reproduce issue #12611");
152+
TSError("[TSHttpTxnServerAddrSet_retry] Plugin initialized - will test TSHttpTxnServerAddrSet retry behavior");
153+
154+
TSPluginRegistrationInfo info;
155+
info.plugin_name = "test_TSHttpTxnServerAddrSet_retry";
156+
info.vendor_name = "Apache Software Foundation";
157+
info.support_email = "[email protected]";
158+
TSReleaseAssert(TSPluginRegister(&info) == TS_SUCCESS);
159+
160+
// Reserve a transaction argument slot for per-transaction call count
161+
TSReleaseAssert(TSUserArgIndexReserve(TS_USER_ARGS_TXN, "test_TSHttpTxnServerAddrSet_retry", "OS_DNS call count",
162+
&g_txn_arg_idx) == TS_SUCCESS);
163+
164+
TSCont os_dns_cont = TSContCreate(handle_os_dns, nullptr);
165+
TSHttpHookAdd(TS_HTTP_OS_DNS_HOOK, os_dns_cont);
166+
167+
TSCont close_cont = TSContCreate(handle_txn_close, nullptr);
168+
TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, close_cont);
169+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
'''
17+
Test to reproduce issue #12611 - retry fails when TSHttpTxnServerAddrSet is used
18+
'''
19+
20+
import os
21+
from ports import get_port
22+
23+
Test.Summary = '''
24+
Reproduce issue #12611: OS_DNS hook is not called again on retry when using TSHttpTxnServerAddrSet
25+
'''
26+
27+
plugin_name = "test_TSHttpTxnServerAddrSet_retry"
28+
29+
30+
class TestIssue12611:
31+
"""Reproduce issue #12611: retry fails when TSHttpTxnServerAddrSet is used."""
32+
33+
def _configure_dns(self, tr: 'TestRun') -> 'Process':
34+
"""Configure the DNS process for a TestRun."""
35+
self._dns = tr.MakeDNServer("dns", default=['127.0.0.1'])
36+
return self._dns
37+
38+
def _configure_traffic_server(self, tr: 'TestRun') -> 'Process':
39+
"""Configure the Traffic Server process for a TestRun."""
40+
self._ts = tr.MakeATSProcess("ts", enable_cache=False)
41+
ts = self._ts
42+
43+
plugin_path = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so')
44+
ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'])
45+
ts.Disk.plugin_config.AddLine(f"{plugin_path}")
46+
47+
ts.Disk.records_config.update(
48+
{
49+
'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}',
50+
'proxy.config.dns.resolv_conf': 'NULL',
51+
'proxy.config.diags.debug.enabled': 1,
52+
'proxy.config.diags.debug.tags': 'http|dns|test_TSHttpTxnServerAddrSet_retry',
53+
# Enable retries so we can see if OS_DNS is called multiple times
54+
'proxy.config.http.connect_attempts_max_retries': 3,
55+
'proxy.config.http.connect_attempts_timeout': 1,
56+
})
57+
58+
# Remap to a nonexisting server - the plugin will set addresses
59+
bogus_port = get_port(ts, "bogus_port")
60+
ts.Disk.remap_config.AddLine(f'map / http://non.existent.server.com:{bogus_port}')
61+
62+
def run(self):
63+
"""Configure the TestRun."""
64+
tr = Test.AddTestRun("Reproduce issue #12611")
65+
self._configure_dns(tr)
66+
self._configure_traffic_server(tr)
67+
68+
# Make a simple request - it should fail since first address is non-routable
69+
# but the plugin should log whether OS_DNS was called multiple times
70+
tr.Processes.Default.Command = f'curl -vs --connect-timeout 5 http://127.0.0.1:{self._ts.Variables.port}/ -o /dev/null 2>&1; true'
71+
tr.Processes.Default.ReturnCode = 0
72+
73+
tr.Processes.Default.StartBefore(self._dns)
74+
tr.Processes.Default.StartBefore(self._ts)
75+
76+
# Override the default diags.log error check - we use TSError() for test output
77+
# Using '=' instead of '+=' replaces the default "no errors" check
78+
self._ts.Disk.diags_log.Content = Testers.ContainsExpression("OS_DNS hook called, count=1", "First OS_DNS call logged")
79+
80+
# This message indicates the fix works - OS_DNS was called multiple times
81+
# Test will FAIL on master (bug exists), PASS after fix is applied
82+
self._ts.Disk.diags_log.Content += Testers.ContainsExpression(
83+
"SUCCESS: OS_DNS hook was called", "Plugin was able to retry with different address")
84+
85+
86+
test = TestIssue12611()
87+
test.run()

0 commit comments

Comments
 (0)