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

bnolsen 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 9534d955aa API for Next Hop Strategy rebind during a transaction 
(#12512)
9534d955aa is described below

commit 9534d955aa78657d311c7170aae394c561d6bcc6
Author: Brian Olsen <[email protected]>
AuthorDate: Mon Oct 20 16:18:53 2025 -0600

    API for Next Hop Strategy rebind during a transaction (#12512)
    
    * Create ts API functions to change a transaction strategy.
    
    This adds 'c' friendly api calls to get and set strategies,
    to get the name of a given strategy and
    to get a strategy from the strategy factory by name.
    
    Also includes additions to header_rewrite, regex_remap and lua plugins.
    
    * in plugins allow 'null' name to clear txn strategy
    
    * add an inkapi test for strategy get and set
    
    * add test for setting bad strategy
    
    ---------
    
    Co-authored-by: Brian Olsen <[email protected]>
---
 doc/admin-guide/plugins/header_rewrite.en.rst      |  13 +
 doc/admin-guide/plugins/lua.en.rst                 |  44 +++
 doc/admin-guide/plugins/regex_remap.en.rst         |   1 +
 .../functions/TSHttpNextHopStrategyNameGet.en.rst  |  49 +++
 .../TSHttpTxnNextHopNamedStrategyGet.en.rst        |  52 ++++
 .../functions/TSHttpTxnNextHopStrategyGet.en.rst   |  49 +++
 .../functions/TSHttpTxnNextHopStrategySet.en.rst   |  56 ++++
 include/proxy/http/HttpTransact.h                  |  10 +-
 include/proxy/http/remap/NextHopStrategyFactory.h  |   7 +-
 include/proxy/http/remap/UrlMapping.h              |   6 +-
 include/ts/ts.h                                    |  61 ++++
 plugins/header_rewrite/conditions.cc               |   9 +
 plugins/header_rewrite/factory.cc                  |   2 +
 plugins/header_rewrite/operators.cc                |  47 +++
 plugins/header_rewrite/operators.h                 |  19 ++
 plugins/header_rewrite/statement.cc                |   2 +
 plugins/header_rewrite/statement.h                 |   1 +
 plugins/lua/ts_lua_http.cc                         |  66 +++++
 plugins/regex_remap/regex_remap.cc                 |  22 ++
 src/api/InkAPI.cc                                  |  53 ++++
 src/api/InkAPITest.cc                              |  32 +-
 src/proxy/http/HttpSM.cc                           |   5 +-
 src/proxy/http/HttpTransact.cc                     |  73 ++---
 src/proxy/http/remap/NextHopStrategyFactory.cc     |  28 +-
 src/proxy/http/remap/RemapProcessor.cc             |  15 +-
 .../remap/unit-tests/test_NextHopConsistentHash.cc |  40 +--
 .../remap/unit-tests/test_NextHopRoundRobin.cc     |  28 +-
 .../unit-tests/test_NextHopStrategyFactory.cc      |  26 +-
 .../strategies/strategies_plugins.test.py          | 330 +++++++++++++++++++++
 29 files changed, 1019 insertions(+), 127 deletions(-)

diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst 
b/doc/admin-guide/plugins/header_rewrite.en.rst
index 1711337964..e7f8c74769 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -647,6 +647,7 @@ are supported::
 
     %{NEXT-HOP:HOST} Name of the current selected parent.
     %{NEXT-HOP:PORT} Port of the current selected parent.
+    %{NEXT-HOP:STRATEGY} Name of the current strategy (can be "" if not using 
a strategy)
 
 Note that the ``<part>`` of NEXT-HOP will likely not be available unless
 an origin server connection is attempted at which point it will available
@@ -1083,6 +1084,18 @@ if necessary.
 The header's ``<value>`` may be a literal string, or take advantage of
 `String concatenations`_ to calculate a dynamic value for the header.
 
+set-next-hop-strategy
+~~~~~~~~~~~~~~~~~~~~~
+::
+
+  set-next-hop-strategy <name>
+
+Replaces/Sets the current next hop parent selection strategy with
+the matching strategy specified in `strategies.yaml`
+
+Setting to "null" removes the current strategy which will fall back
+to other methods (ie: parent.config or remap to url).
+
 set-redirect
 ~~~~~~~~~~~~
 ::
diff --git a/doc/admin-guide/plugins/lua.en.rst 
b/doc/admin-guide/plugins/lua.en.rst
index d065d3d1e3..aba1ff7db3 100644
--- a/doc/admin-guide/plugins/lua.en.rst
+++ b/doc/admin-guide/plugins/lua.en.rst
@@ -1930,6 +1930,50 @@ Here is an example:
 
 `TOP <#ts-lua-plugin>`_
 
+ts.http.get_next_hop_strategy
+-----------------------------
+**syntax:** *ts.http.get_next_hop_strategy()*
+
+**context:** function @ TS_LUA_HOOK_READ_REQUEST_HDR or do_remap()
+
+**description** Returns the name of the current next hop selection strategy, 
or nil string if no strategy is in use.
+
+Here is an example:
+
+::
+
+    function do_remap()
+                 local strategy = ts.http.get_next_hop_strategy()
+                       ts.debug("Using strategy: " .. strategy)
+               end
+
+`TOP <#ts-lua-plugin>`_
+
+ts.http.set_next_hop_strategy
+-----------------------------
+**syntax:** *ts.http.set_next_hop_strategy(str)*
+
+**context:** function @ TS_LUA_HOOK_READ_REQUEST_HDR or do_remap()
+
+**description** Looks for the named strategy and sets the current
+transaction to use that strategy.
+
+Use empty string or "null" to clear the strategy and fall back to
+parent.config or the remap to url.
+
+Here is an example:
+
+::
+
+    function do_remap()
+                 local uri = ts.client_request.get_uri()
+                       if uri == "otherhost" then
+                         ts.http.set_next_hop_strategy("otherhost")
+                       end
+               end
+
+`TOP <#ts-lua-plugin>`_
+
 ts.sha256
 ---------
 **syntax:** *digest = ts.sha256(str)*
diff --git a/doc/admin-guide/plugins/regex_remap.en.rst 
b/doc/admin-guide/plugins/regex_remap.en.rst
index 547309faca..b309ca0726 100644
--- a/doc/admin-guide/plugins/regex_remap.en.rst
+++ b/doc/admin-guide/plugins/regex_remap.en.rst
@@ -143,6 +143,7 @@ remap.config. The following options are available ::
 
     @caseless                   - Make regular expressions case insensitive
     @lowercase_substitutions    - Turn on (enable) lower case substitutions
+               @strategy                   - Specify a strategy from 
strategies.yaml. "null" or "" will clear the strategy.
 
 
 This can be useful to force a particular response for some URLs, e.g. ::
diff --git 
a/doc/developer-guide/api/functions/TSHttpNextHopStrategyNameGet.en.rst 
b/doc/developer-guide/api/functions/TSHttpNextHopStrategyNameGet.en.rst
new file mode 100644
index 0000000000..273b636fb8
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSHttpNextHopStrategyNameGet.en.rst
@@ -0,0 +1,49 @@
+.. 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.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSHttpNextHopStrategyNameGet
+****************************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: char const* TSHttpNextHopStrategyNameGet(void const* strategy)
+
+Description
+===========
+
+Gets the name associated with the provided strategy.
+This may be nullptr indicating that parent.config is in use.
+
+.. note::
+
+   This returned pointer must not be freed and the contents must not
+   be changed.
+   Strategy pointers held by plugins will become invalid when ATS
+   configs are reloaded and should be reset with :func:`TSRemapNewInstance`
+
+See Also
+========
+
+:func:`TSHttpTxnNextHopStrategyGet`
diff --git 
a/doc/developer-guide/api/functions/TSHttpTxnNextHopNamedStrategyGet.en.rst 
b/doc/developer-guide/api/functions/TSHttpTxnNextHopNamedStrategyGet.en.rst
new file mode 100644
index 0000000000..949d92346d
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSHttpTxnNextHopNamedStrategyGet.en.rst
@@ -0,0 +1,52 @@
+.. 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.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSHttpTxnNextHopNamedStrategyGet
+********************************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: void const* TSHttpTxnNextHopNamedStrategyGet(TSHttpTxn txnp, 
const char *name)
+
+Description
+===========
+
+Gets a pointer to the specified :arg:`name` NextHopSelectionStrategy.
+This may be nullptr indicating that no strategy exists with the given name.
+
+This function uses the transaction :arg:`txnp` to get access to the
+NextHopStrategyFactory associated with the current configuration.
+
+.. note::
+
+   This returned pointer must not be freed and the contents must not
+   be changed.
+   Strategy pointers held by plugins will become invalid when ATS
+   configs are reloaded and should be reset with :func:`TSRemapNewInstance`
+
+See Also
+========
+
+:func:`TSHttpTxnNextHopStrategySet`
diff --git 
a/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategyGet.en.rst 
b/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategyGet.en.rst
new file mode 100644
index 0000000000..c609511bbe
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategyGet.en.rst
@@ -0,0 +1,49 @@
+.. 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.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSHttpTxnNextHopStrategyGet
+***************************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: void const* TSHttpTxnNextHopStrategyGet(TSHttpTxn txnp)
+
+Description
+===========
+
+Gets a pointer to the current transaction :arg:`txnp` NextHopSelectionStrategy.
+This may be nullptr indicating that parent.config is in use.
+
+.. note::
+
+   This strategy pointer must not be freed and the contents must not
+   be changed.
+   Strategy pointers held by plugins will become invalid when ATS
+   configs are reloaded and should be reset with :func:`TSRemapNewInstance`
+
+See Also
+========
+
+:func:`TSHttpTxnNextHopStrategySet`
diff --git 
a/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategySet.en.rst 
b/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategySet.en.rst
new file mode 100644
index 0000000000..33ca4b6266
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSHttpTxnNextHopStrategySet.en.rst
@@ -0,0 +1,56 @@
+.. 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.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSHttpTxnNextHopNameGet
+***********************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: void TSHttpTxnNextHopStrategySet(TSHttpTxn txnp, void const* 
strategy)
+
+Description
+===========
+
+Sets the next hop strategy for the transaction :arg:`txnp`
+This :arg:`strategy` pointer must be a valid strategy and can be
+nullptr to indicate that parent.config will be used instead.
+
+Plugins can get a strategy by name by calling
+:func:`TSHttpTxnNextHopStrategyGet` to get the current transaction's
+active strategy or :func:`TSHttpTxnNextHopNamedStrategyGet` to
+look up a strategy by name using the transaction's pointer to the
+NextHopStrategyFactory strategy database.
+
+.. note::
+
+   This strategy pointer must not be freed and the contents must not
+   be changed.
+   Strategy pointers held by plugins will become invalid when ATS
+   configs are reloaded and should be reset with :func:`TSRemapNewInstance`
+
+See Also
+========
+
+:func:`TSHttpTxnNextHopStrategyGet`, :func:`TSHttpTxnNextHopNamedStrategyGet`.
diff --git a/include/proxy/http/HttpTransact.h 
b/include/proxy/http/HttpTransact.h
index 62a44da904..01bcb8182e 100644
--- a/include/proxy/http/HttpTransact.h
+++ b/include/proxy/http/HttpTransact.h
@@ -723,11 +723,11 @@ public:
     //  able to defer some work in building the request
     TransactFunc_t pending_work = nullptr;
 
-    HttpRequestData                           request_data;
-    ParentConfigParams                       *parent_params     = nullptr;
-    std::shared_ptr<NextHopSelectionStrategy> next_hop_strategy = nullptr;
-    ParentResult                              parent_result;
-    CacheControlResult                        cache_control;
+    HttpRequestData           request_data;
+    ParentConfigParams       *parent_params     = nullptr;
+    NextHopSelectionStrategy *next_hop_strategy = nullptr;
+    ParentResult              parent_result;
+    CacheControlResult        cache_control;
 
     StateMachineAction_t next_action                      = 
StateMachineAction_t::UNDEFINED; // out
     StateMachineAction_t api_next_action                  = 
StateMachineAction_t::UNDEFINED; // out
diff --git a/include/proxy/http/remap/NextHopStrategyFactory.h 
b/include/proxy/http/remap/NextHopStrategyFactory.h
index 4822460d91..aae01e3d1b 100644
--- a/include/proxy/http/remap/NextHopStrategyFactory.h
+++ b/include/proxy/http/remap/NextHopStrategyFactory.h
@@ -45,7 +45,10 @@ public:
   NextHopStrategyFactory() = delete;
   NextHopStrategyFactory(const char *file);
   ~NextHopStrategyFactory();
-  std::shared_ptr<NextHopSelectionStrategy> strategyInstance(const char *name);
+
+  // The lifetime of this is attached to UrlRewrite which is reference
+  // counted and attached to an HttpSM.
+  NextHopSelectionStrategy *strategyInstance(const char *name) const;
 
   bool strategies_loaded;
 
@@ -53,5 +56,5 @@ private:
   std::string fn;
   void        loadConfigFile(const std::string &file, std::stringstream &doc, 
std::unordered_set<std::string> &include_once);
   void        createStrategy(const std::string &name, const NHPolicyType 
policy_type, ts::Yaml::Map &node);
-  std::unordered_map<std::string, std::shared_ptr<NextHopSelectionStrategy>> 
_strategies;
+  std::unordered_map<std::string, NextHopSelectionStrategy *> _strategies;
 };
diff --git a/include/proxy/http/remap/UrlMapping.h 
b/include/proxy/http/remap/UrlMapping.h
index a35b66f8c1..dabab07118 100644
--- a/include/proxy/http/remap/UrlMapping.h
+++ b/include/proxy/http/remap/UrlMapping.h
@@ -112,9 +112,9 @@ public:
   bool              ip_allow_check_enabled_p = false;
   acl_filter_rule  *filter                   = nullptr; // acl filtering 
(linked list of rules)
   LINK(url_mapping, link);                              // For use with the 
main Queue linked list holding all the mapping
-  std::shared_ptr<NextHopSelectionStrategy> strategy = nullptr;
-  std::string                               remapKey;
-  std::atomic<uint64_t>                     _hitCount = 0; // counter can 
overflow
+  NextHopSelectionStrategy *strategy = nullptr;
+  std::string               remapKey;
+  std::atomic<uint64_t>     _hitCount = 0; // counter can overflow
 
   int
   getRank() const
diff --git a/include/ts/ts.h b/include/ts/ts.h
index 7bfab14982..8ea727082f 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -1579,6 +1579,67 @@ void TSHttpTxnErrorBodySet(TSHttpTxn txnp, char *buf, 
size_t buflength, char *mi
 */
 char *TSHttpTxnErrorBodyGet(TSHttpTxn txnp, size_t *buflength, char 
**mimetype);
 
+/**
+    Sets the Transaction's Next Hop Parent Strategy.
+    Calling this after TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK will
+    result in bad behavior.
+
+    You can get this strategy pointer by calling TSHttpTxnParentStrategyGet().
+
+    @param txnp HTTP transaction whose parent strategy to set.
+    @param pointer to the given strategy.
+
+ */
+void TSHttpTxnNextHopStrategySet(TSHttpTxn txnp, void const *strategy);
+
+/**
+    Retrieves a pointer to the current next hop selection strategy.
+    This value may be a nullptr due to:
+      - parent proxying not enabled
+      - no parent selection strategy (using parent.config)
+
+    @param txnp HTTP transaction whose next hop strategy to get.
+
+ */
+void const *TSHttpTxnNextHopStrategyGet(TSHttpTxn txnp);
+
+/**
+    Returns either null pointer or null terminated pointer to name.
+                DO NOT FREE.
+
+    This value may be a nullptr due to:
+      - parent proxying not enabled
+      - no parent selection strategy (using parent.config)
+
+    @param txnp HTTP transaction whose next hop strategy to get.
+
+ */
+char const *TSHttpNextHopStrategyNameGet(void const *strategy);
+
+/**
+    Retrieves a pointer to the named strategy in the strategy table.
+    Returns nullptr if no strategy is set.
+    This uses the current transaction's state machine to get
+    access to UrlRewrite's NextHopStrategyFactory.
+
+    @param txnp HTTP transaction which holds the strategy table.
+    @param name of the strategy to look up.
+
+ */
+void const *TSHttpTxnNextHopNamedStrategyGet(TSHttpTxn txnp, const char *name);
+
+/**
+    Sets the parent proxy name and port. The string hostname is copied
+    into the TSHttpTxn; you can modify or delete the string after
+    calling TSHttpTxnParentProxySet().
+
+    @param txnp HTTP transaction whose parent proxy to set.
+    @param hostname parent proxy host name string.
+    @param port parent proxy port to set.
+
+ */
+void TSHttpTxnParentProxySet(TSHttpTxn txnp, const char *hostname, int port);
+
 /**
     Retrieves the parent proxy hostname and port, if parent
     proxying is enabled. If parent proxying is not enabled,
diff --git a/plugins/header_rewrite/conditions.cc 
b/plugins/header_rewrite/conditions.cc
index dcb44831ed..11357033e2 100644
--- a/plugins/header_rewrite/conditions.cc
+++ b/plugins/header_rewrite/conditions.cc
@@ -1538,6 +1538,15 @@ ConditionNextHop::append_value(std::string &s, const 
Resources &res)
     Dbg(pi_dbg_ctl, "Appending '%d' to evaluation value", port);
     s.append(std::to_string(port));
   } break;
+  case NEXT_HOP_STRATEGY: {
+    char const *const name = TSHttpNextHopStrategyNameGet(res.state.txnp);
+    if (nullptr != name) {
+      Dbg(pi_dbg_ctl, "Appending '%s' to evaluation value", name);
+      s.append(name);
+    } else {
+      Dbg(pi_dbg_ctl, "NextHopStrategyName is empty");
+    }
+  } break;
   default:
     TSReleaseAssert(!"All cases should have been handled");
     break;
diff --git a/plugins/header_rewrite/factory.cc 
b/plugins/header_rewrite/factory.cc
index 3317b49b33..9e0499f060 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -89,6 +89,8 @@ operator_factory(const std::string &op)
     o = new OperatorSetStateInt16();
   } else if (op == "set-effective-address") {
     o = new OperatorSetEffectiveAddress();
+  } else if (op == "set-next-hop-strategy") {
+    o = new OperatorSetNextHopStrategy();
   } else {
     TSError("[%s] Unknown operator: %s", PLUGIN_NAME, op.c_str());
     return nullptr;
diff --git a/plugins/header_rewrite/operators.cc 
b/plugins/header_rewrite/operators.cc
index 4ac0e245b0..e1a9683d3c 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -1627,3 +1627,50 @@ OperatorSetEffectiveAddress::exec(const Resources &res) 
const
 
   return true;
 }
+
+// OperatorSetNextHopStrategy
+void
+OperatorSetNextHopStrategy::initialize(Parser &p)
+{
+  Operator::initialize(p);
+
+  _value.set_value(p.get_arg(), this);
+  Dbg(pi_dbg_ctl, "OperatorSetNextHopStrategy::initialie: %s", 
_value.get_value().c_str());
+}
+
+void
+OperatorSetNextHopStrategy::initialize_hooks()
+{
+  add_allowed_hook(TS_HTTP_READ_REQUEST_HDR_HOOK);
+  add_allowed_hook(TS_REMAP_PSEUDO_HOOK);
+}
+
+bool
+OperatorSetNextHopStrategy::exec(const Resources &res) const
+{
+  if (!res.state.txnp) {
+    TSError("[%s] OperatorSetNextHopStrategy() failed. Transaction is null", 
PLUGIN_NAME);
+  }
+
+  auto const txnp = res.state.txnp;
+
+  std::string value;
+  _value.append_value(value, res);
+
+  // Setting an empty strategy clears it for either parent.config or remap to
+  if ("null" == value || value.empty()) {
+    Dbg(pi_dbg_ctl, "Clearing strategy");
+    TSHttpTxnNextHopStrategySet(txnp, nullptr);
+    return true;
+  }
+
+  void const *const stratptr = TSHttpTxnNextHopNamedStrategyGet(txnp, 
value.c_str());
+  if (nullptr == stratptr) {
+    TSWarning("[%s] Failed to get strategy '%s'", PLUGIN_NAME, value.c_str());
+  } else {
+    Dbg(pi_dbg_ctl, "   Setting strategy '%s'", value.c_str());
+    TSHttpTxnNextHopStrategySet(txnp, stratptr);
+  }
+
+  return true;
+}
diff --git a/plugins/header_rewrite/operators.h 
b/plugins/header_rewrite/operators.h
index 4ff4f0edcb..a0b825d3d0 100644
--- a/plugins/header_rewrite/operators.h
+++ b/plugins/header_rewrite/operators.h
@@ -651,3 +651,22 @@ protected:
 private:
   Value _value;
 };
+
+class OperatorSetNextHopStrategy : public Operator
+{
+public:
+  OperatorSetNextHopStrategy() { Dbg(dbg_ctl, "Calling CTOR for 
OperatorSetNextHopStrategy"); }
+
+  // noncopyable
+  OperatorSetNextHopStrategy(const OperatorSetNextHopStrategy &) = delete;
+  void operator=(const OperatorSetNextHopStrategy &)             = delete;
+
+  void initialize(Parser &p) override;
+
+protected:
+  void initialize_hooks() override;
+  bool exec(const Resources &res) const override;
+
+private:
+  Value _value;
+};
diff --git a/plugins/header_rewrite/statement.cc 
b/plugins/header_rewrite/statement.cc
index ca840d7a9b..57eab9d62f 100644
--- a/plugins/header_rewrite/statement.cc
+++ b/plugins/header_rewrite/statement.cc
@@ -118,6 +118,8 @@ Statement::parse_next_hop_qualifier(const std::string &q) 
const
     qual = NEXT_HOP_HOST;
   } else if (q == "PORT") {
     qual = NEXT_HOP_PORT;
+  } else if (q == "STRATEGY") {
+    qual = NEXT_HOP_STRATEGY;
   } else {
     TSError("[%s] Invalid NextHop() qualifier: %s", PLUGIN_NAME, q.c_str());
   }
diff --git a/plugins/header_rewrite/statement.h 
b/plugins/header_rewrite/statement.h
index 28e7195128..aac03d878a 100644
--- a/plugins/header_rewrite/statement.h
+++ b/plugins/header_rewrite/statement.h
@@ -63,6 +63,7 @@ enum NextHopQualifiers {
   NEXT_HOP_NONE,
   NEXT_HOP_HOST,
   NEXT_HOP_PORT,
+  NEXT_HOP_STRATEGY,
 };
 
 // NOW data
diff --git a/plugins/lua/ts_lua_http.cc b/plugins/lua/ts_lua_http.cc
index b631206bb2..b1bc7c104a 100644
--- a/plugins/lua/ts_lua_http.cc
+++ b/plugins/lua/ts_lua_http.cc
@@ -75,6 +75,10 @@ static int ts_lua_http_set_cache_url(lua_State *L);
 static int ts_lua_http_get_cache_lookup_url(lua_State *L);
 static int ts_lua_http_set_cache_lookup_url(lua_State *L);
 static int ts_lua_http_redo_cache_lookup(lua_State *L);
+
+static int ts_lua_http_get_next_hop_strategy(lua_State *L);
+static int ts_lua_http_set_next_hop_strategy(lua_State *L);
+
 static int ts_lua_http_get_parent_proxy(lua_State *L);
 static int ts_lua_http_set_parent_proxy(lua_State *L);
 static int ts_lua_http_get_parent_selection_url(lua_State *L);
@@ -180,6 +184,12 @@ ts_lua_inject_http_cache_api(lua_State *L)
   lua_pushcfunction(L, ts_lua_http_redo_cache_lookup);
   lua_setfield(L, -2, "redo_cache_lookup");
 
+  lua_pushcfunction(L, ts_lua_http_get_next_hop_strategy);
+  lua_setfield(L, -2, "get_next_hop_strategy");
+
+  lua_pushcfunction(L, ts_lua_http_set_next_hop_strategy);
+  lua_setfield(L, -2, "set_next_hop_strategy");
+
   lua_pushcfunction(L, ts_lua_http_get_parent_proxy);
   lua_setfield(L, -2, "get_parent_proxy");
 
@@ -517,6 +527,62 @@ ts_lua_http_redo_cache_lookup(lua_State *L)
   return 0;
 }
 
+static int
+ts_lua_http_get_next_hop_strategy(lua_State *L)
+{
+  char const      *name = nullptr;
+  ts_lua_http_ctx *http_ctx;
+
+  GET_HTTP_CONTEXT(http_ctx, L);
+
+  void const *const stratptr = TSHttpTxnNextHopStrategyGet(http_ctx->txnp);
+  if (nullptr != stratptr) {
+    name = TSHttpNextHopStrategyNameGet(stratptr);
+  }
+
+  if (name == nullptr) {
+    lua_pushnil(L);
+  } else {
+    lua_pushstring(L, name);
+  }
+
+  return 1;
+}
+
+static int
+ts_lua_http_set_next_hop_strategy(lua_State *L)
+{
+  int n = 0;
+
+  ts_lua_http_ctx *http_ctx;
+
+  GET_HTTP_CONTEXT(http_ctx, L);
+
+  n = lua_gettop(L);
+
+  if (n == 1) {
+    const char *name = nullptr;
+    size_t      name_len;
+
+    name = luaL_checklstring(L, 1, &name_len);
+    if (0 == name_len || "null" == std::string_view(name)) {
+      Dbg(dbg_ctl, "Clearning strategy (use parent.config)");
+      TSHttpTxnNextHopStrategySet(http_ctx->txnp, nullptr);
+    } else {
+      void const *const stratptr = 
TSHttpTxnNextHopNamedStrategyGet(http_ctx->txnp, name);
+      if (nullptr == stratptr) {
+        TSError("[ts_lua][%s] Failed get next hop strategy name '%s'", 
__FUNCTION__, name);
+      } else {
+        TSHttpTxnNextHopStrategySet(http_ctx->txnp, stratptr);
+      }
+    }
+  } else {
+    return luaL_error(L, "incorrect # of arguments for set_parent_proxy, 
receiving %d instead of 1", n);
+  }
+
+  return 0;
+}
+
 static int
 ts_lua_http_get_parent_proxy(lua_State *L)
 {
diff --git a/plugins/regex_remap/regex_remap.cc 
b/plugins/regex_remap/regex_remap.cc
index 2c9e5db29a..be57f75998 100644
--- a/plugins/regex_remap/regex_remap.cc
+++ b/plugins/regex_remap/regex_remap.cc
@@ -226,6 +226,11 @@ public:
   {
     return _lowercase_substitutions;
   }
+  inline std::string const &
+  strategy() const
+  {
+    return _strategy;
+  }
 
   // Hold an overridable configurations
   struct Override {
@@ -263,6 +268,8 @@ private:
   int _connect_timeout     = -1;
   int _dns_timeout         = -1;
 
+  std::string _strategy = {};
+
   Override *_first_override = nullptr;
   int       _sub_pos[MAX_SUBS];
   int       _sub_ix[MAX_SUBS];
@@ -309,6 +316,8 @@ RemapRegex::initialize(const std::string &reg, const 
std::string &sub, const std
       _options |= PCRE_CASELESS;
     } else if (opt.compare(start, 23, "lowercase_substitutions") == 0) {
       _lowercase_substitutions = true;
+    } else if (opt.compare(start, 8, "strategy") == 0) {
+      _strategy = opt_val;
     } else if (opt_val.size() <= 0) {
       // All other options have a required value
       TSError("[%s] Malformed options: %s", PLUGIN_NAME, opt.c_str());
@@ -971,6 +980,19 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, 
TSRemapRequestInfo *rri)
         Dbg(dbg_ctl, "Setting DNS timeout to %d", re->dns_timeout_option());
         TSHttpTxnDNSTimeoutSet(txnp, re->dns_timeout_option());
       }
+      auto const &strat = re->strategy();
+      if (strat.empty() || "null" == strat) {
+        Dbg(dbg_ctl, "Clearing strategy (use parent.config)");
+        TSHttpTxnNextHopStrategySet(txnp, nullptr);
+      } else {
+        void const *const stratptr = TSHttpTxnNextHopNamedStrategyGet(txnp, 
strat.c_str());
+        if (nullptr == stratptr) {
+          Dbg(dbg_ctl, "No strategy found with name '%s'", strat.c_str());
+        } else {
+          Dbg(dbg_ctl, "Setting strategy to %s", strat.c_str());
+          TSHttpTxnNextHopStrategySet(txnp, stratptr);
+        }
+      }
       bool lowercase_substitutions = false;
       if (re->lowercase_substitutions_option() == true) {
         Dbg(dbg_ctl, "Setting lowercasing substitutions on");
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index 6c6f34b37e..f1869503f0 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -4991,6 +4991,59 @@ TSHttpTxnServerRequestBodySet(TSHttpTxn txnp, char *buf, 
int64_t buflength)
   s->internal_msg_buffer_fast_allocator_size = -1;
 }
 
+void const *
+TSHttpTxnNextHopStrategyGet(TSHttpTxn txnp)
+{
+  sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS);
+
+  auto sm = reinterpret_cast<HttpSM const *>(txnp);
+
+  return static_cast<void *>(sm->t_state.next_hop_strategy);
+}
+
+void
+TSHttpTxnNextHopStrategySet(TSHttpTxn txnp, void const *stratptr)
+{
+  sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS);
+  // null strategy falls back to parent.config
+  // sdk_assert(sdk_sanity_check_null_ptr(strategy) == TS_SUCCESS);
+
+  auto sm       = reinterpret_cast<HttpSM *>(txnp);
+  auto strategy = reinterpret_cast<NextHopSelectionStrategy const *>(stratptr);
+
+  sm->t_state.next_hop_strategy = const_cast<NextHopSelectionStrategy 
*>(strategy);
+}
+
+char const *
+TSHttpNextHopStrategyNameGet(void const *stratptr)
+{
+  char const *name = nullptr;
+  if (nullptr != stratptr) {
+    auto strategy = reinterpret_cast<NextHopSelectionStrategy const 
*>(stratptr);
+    name          = strategy->strategy_name.c_str();
+  }
+
+  return name;
+}
+
+void const *
+TSHttpTxnNextHopNamedStrategyGet(TSHttpTxn txnp, const char *name)
+{
+  sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS);
+  sdk_assert(sdk_sanity_check_null_ptr((void *)name) == TS_SUCCESS);
+
+  auto sm = reinterpret_cast<HttpSM const *>(txnp);
+
+  sdk_assert(sdk_sanity_check_null_ptr((void *)sm->m_remap) == TS_SUCCESS);
+  sdk_assert(sdk_sanity_check_null_ptr((void *)sm->m_remap->strategyFactory) 
== TS_SUCCESS);
+
+  // HttpSM has a reference count handle to UrlRewrite which has a
+  // pointer to NextHopStrategyFactory
+  NextHopSelectionStrategy const *const strat = 
sm->m_remap->strategyFactory->strategyInstance(name);
+
+  return static_cast<void const *>(strat);
+}
+
 TSReturnCode
 TSHttpTxnParentProxyGet(TSHttpTxn txnp, const char **hostname, int *port)
 {
diff --git a/src/api/InkAPITest.cc b/src/api/InkAPITest.cc
index a1dc169a74..521474bedd 100644
--- a/src/api/InkAPITest.cc
+++ b/src/api/InkAPITest.cc
@@ -3062,6 +3062,7 @@ struct SocketTest {
   bool            test_next_hop_ip_get;
   bool            test_next_hop_name_get;
   bool            test_next_hop_port_get;
+  bool            test_next_hop_strategy_get;
   bool            test_client_protocol_stack_get;
   bool            test_client_protocol_stack_contains;
 
@@ -3156,6 +3157,28 @@ checkHttpTxnClientProtocolStackContains(SocketTest 
*test, void *data)
   return TS_EVENT_CONTINUE;
 }
 
+// This func is called by us from mytest_handler to check for 
TSHttpTxnNextStrategyGet
+static int
+checkHttpTxnNextHopStrategyGet(SocketTest *test, void *data)
+{
+  TSHttpTxn txnp = static_cast<TSHttpTxn>(data);
+
+  // this is an invalid pointer but the contents don't matter for this test.
+  void const *const exp = reinterpret_cast<void *>(0x01);
+
+  void const *const strategy = TSHttpTxnNextHopStrategyGet(txnp);
+  if (strategy == exp) {
+    test->test_next_hop_strategy_get = true;
+    SDK_RPRINT(test->regtest, "TSHttpTxnNextHopStrategyGet", "TestCase1", 
TC_PASS, "ok");
+  } else {
+    test->test_next_hop_strategy_get = false;
+    SDK_RPRINT(test->regtest, "TSHttpTxnNextHopStrategyGet", "TestCase1", 
TC_FAIL, "Value's Mismatch [expected '%jx', got '%jx'",
+               exp, strategy);
+  }
+
+  return TS_EVENT_CONTINUE;
+}
+
 // This func is called by us from mytest_handler to check for 
TSHttpTxnNextHopIPGet
 static int
 checkHttpTxnNextHopIPGet(SocketTest *test, void *data)
@@ -3485,6 +3508,11 @@ mytest_handler(TSCont contp, TSEvent event, void *data)
       test->hook_mask |= 2;
     }
     TSHttpTxnCntlSet(static_cast<TSHttpTxn>(data), 
TS_HTTP_CNTL_SKIP_REMAPPING, true);
+
+    // Set the strategy pointer here
+    // this is an invalid pointer but the contents don't matter for this test.
+    TSHttpTxnNextHopStrategySet(static_cast<TSHttpTxn>(data), (void *)0x01);
+
     checkHttpTxnClientReqGet(test, data);
 
     TSHttpTxnReenable(static_cast<TSHttpTxn>(data), TS_EVENT_HTTP_CONTINUE);
@@ -3501,6 +3529,7 @@ mytest_handler(TSCont contp, TSEvent event, void *data)
 
     checkHttpTxnClientIPGet(test, data);
     checkHttpTxnServerIPGet(test, data);
+    checkHttpTxnNextHopStrategyGet(test, data);
 
     TSHttpTxnReenable(static_cast<TSHttpTxn>(data), TS_EVENT_HTTP_CONTINUE);
     test->reenable_mask |= 8;
@@ -3591,7 +3620,7 @@ mytest_handler(TSCont contp, TSEvent event, void *data)
           (test->test_client_remote_port_get != true) || 
(test->test_client_req_get != true) ||
           (test->test_client_resp_get != true) || (test->test_server_ip_get != 
true) || (test->test_server_req_get != true) ||
           (test->test_server_resp_get != true) || (test->test_next_hop_ip_get 
!= true) || (test->test_next_hop_name_get != true) ||
-          (test->test_next_hop_port_get != true)) {
+          (test->test_next_hop_port_get != true) || 
(test->test_next_hop_strategy_get != true)) {
         *(test->pstatus) = REGRESSION_TEST_FAILED;
       }
       // transaction is over. clean up.
@@ -3635,6 +3664,7 @@ 
EXCLUSIVE_REGRESSION_TEST(SDK_API_HttpHookAdd)(RegressionTest *test, int /* atyp
   socktest->test_next_hop_ip_get          = false;
   socktest->test_next_hop_name_get        = false;
   socktest->test_next_hop_port_get        = false;
+  socktest->test_next_hop_strategy_get    = false;
   socktest->magic                         = MAGIC_ALIVE;
   TSContDataSet(cont, socktest);
 
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 2978adfbb2..95d677a390 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -314,8 +314,9 @@ HttpSM::init(bool from_early_data)
   // Added to skip dns if the document is in cache. DNS will be forced if 
there is a ip based ACL in
   // cache control or parent.config or if the doc_in_cache_skip_dns is 
disabled or if http caching is disabled
   // TODO: This probably doesn't honor this as a per-transaction overridable 
config.
-  t_state.force_dns = (ip_rule_in_CacheControlTable() || 
t_state.parent_params->parent_table->ipMatch ||
-                       !(t_state.txn_conf->doc_in_cache_skip_dns) || 
!(t_state.txn_conf->cache_http));
+  t_state.force_dns =
+    (ip_rule_in_CacheControlTable() || (nullptr != t_state.parent_params && 
t_state.parent_params->parent_table->ipMatch) ||
+     !(t_state.txn_conf->doc_in_cache_skip_dns) || 
!(t_state.txn_conf->cache_http));
 
   SET_HANDLER(&HttpSM::main_handler);
 
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index df4727cd46..e8d53be3f3 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -128,13 +128,12 @@ extern HttpBodyFactory *body_factory;
 inline static bool
 bypass_ok(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
   if (s->response_action.handled) {
     return s->response_action.action.goDirect;
-  } else if (mp && mp->strategy) {
+  } else if (nullptr != s->next_hop_strategy) {
     // remap strategies do not support the TSHttpTxnParentProxySet API.
-    return mp->strategy->go_direct;
-  } else if (s->parent_params) {
+    return s->next_hop_strategy->go_direct;
+  } else if (nullptr != s->parent_params) {
     return s->parent_result.bypass_ok();
   }
   return false;
@@ -145,13 +144,11 @@ bypass_ok(HttpTransact::State *s)
 inline static bool
 is_api_result(HttpTransact::State *s)
 {
-  bool         r  = false;
-  url_mapping *mp = s->url_map.getMapping();
-
-  if (mp && mp->strategy) {
+  bool r = false;
+  if (nullptr != s->next_hop_strategy) {
     // remap strategies do not support the TSHttpTxnParentProxySet API.
     r = false;
-  } else if (s->parent_params) {
+  } else if (nullptr != s->parent_params) {
     r = s->parent_result.is_api_result();
   }
   return r;
@@ -184,12 +181,11 @@ numParents(HttpTransact::State *s)
 inline static bool
 parent_is_proxy(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
   if (s->response_action.handled) {
     return s->response_action.action.parentIsProxy;
-  } else if (mp && mp->strategy) {
-    return mp->strategy->parent_is_proxy;
-  } else if (s->parent_params) {
+  } else if (nullptr != s->next_hop_strategy) {
+    return s->next_hop_strategy->parent_is_proxy;
+  } else if (nullptr != s->parent_params) {
     return s->parent_result.parent_is_proxy();
   }
   return false;
@@ -211,7 +207,6 @@ retry_type(HttpTransact::State *s)
 inline static void
 findParent(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
   Metrics::Counter::increment(http_rsb.parent_count);
   if (s->response_action.handled) {
     s->parent_result.hostname = s->response_action.action.hostname;
@@ -224,9 +219,9 @@ findParent(HttpTransact::State *s)
     } else {
       s->parent_result.result = ParentResultType::FAIL;
     }
-  } else if (mp && mp->strategy) {
-    mp->strategy->findNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine));
-  } else if (s->parent_params) {
+  } else if (nullptr != s->next_hop_strategy) {
+    
s->next_hop_strategy->findNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine));
+  } else if (nullptr != s->parent_params) {
     s->parent_params->findParent(&s->request_data, &s->parent_result, 
s->txn_conf->parent_fail_threshold,
                                  s->txn_conf->parent_retry_time);
   }
@@ -237,8 +232,6 @@ findParent(HttpTransact::State *s)
 inline static void
 markParentDown(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
-
   TxnDbg(dbg_ctl_http_trans, "enable_parent_timeout_markdowns: %d, 
disable_parent_markdowns: %d",
          s->txn_conf->enable_parent_timeout_markdowns, 
s->txn_conf->disable_parent_markdowns);
 
@@ -258,10 +251,10 @@ markParentDown(HttpTransact::State *s)
 
   if (s->response_action.handled) {
     // Do nothing. If a plugin handled the response, let it handle markdown.
-  } else if (mp && mp->strategy) {
-    mp->strategy->markNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine), 
s->parent_result.hostname, s->parent_result.port,
-                              NHCmd::MARK_DOWN);
-  } else if (s->parent_params) {
+  } else if (nullptr != s->next_hop_strategy) {
+    
s->next_hop_strategy->markNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine),
 s->parent_result.hostname,
+                                      s->parent_result.port, NHCmd::MARK_DOWN);
+  } else if (nullptr != s->parent_params) {
     s->parent_params->markParentDown(&s->parent_result, 
s->txn_conf->parent_fail_threshold, s->txn_conf->parent_retry_time);
   }
 }
@@ -271,13 +264,12 @@ markParentDown(HttpTransact::State *s)
 inline static void
 markParentUp(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
   if (s->response_action.handled) {
     // Do nothing. If a plugin handled the response, let it handle markdown
-  } else if (mp && mp->strategy) {
-    mp->strategy->markNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine), 
s->parent_result.hostname, s->parent_result.port,
-                              NHCmd::MARK_UP);
-  } else if (s->parent_params) {
+  } else if (nullptr != s->next_hop_strategy) {
+    
s->next_hop_strategy->markNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine),
 s->parent_result.hostname,
+                                      s->parent_result.port, NHCmd::MARK_UP);
+  } else if (nullptr != s->parent_params) {
     s->parent_params->markParentUp(&s->parent_result);
   }
 }
@@ -287,12 +279,11 @@ markParentUp(HttpTransact::State *s)
 inline static bool
 parentExists(HttpTransact::State *s)
 {
-  url_mapping *mp = s->url_map.getMapping();
   if (s->response_action.handled) {
     return s->response_action.action.nextHopExists;
-  } else if (mp && mp->strategy) {
-    return 
mp->strategy->nextHopExists(reinterpret_cast<TSHttpTxn>(s->state_machine));
-  } else if (s->parent_params) {
+  } else if (nullptr != s->next_hop_strategy) {
+    return 
s->next_hop_strategy->nextHopExists(reinterpret_cast<TSHttpTxn>(s->state_machine));
+  } else if (nullptr != s->parent_params) {
     return s->parent_params->parentExists(&s->request_data);
   } else {
     return false;
@@ -306,7 +297,6 @@ nextParent(HttpTransact::State *s)
 {
   TxnDbg(dbg_ctl_parent_down, "connection to parent %s failed, conn_state: %s, 
request to origin: %s", s->parent_result.hostname,
          HttpDebugNames::get_server_state_name(s->current.state), 
s->request_data.get_host());
-  url_mapping *mp = s->url_map.getMapping();
   Metrics::Counter::increment(http_rsb.parent_count);
   if (s->response_action.handled) {
     s->parent_result.hostname = s->response_action.action.hostname;
@@ -319,10 +309,10 @@ nextParent(HttpTransact::State *s)
     } else {
       s->parent_result.result = ParentResultType::FAIL;
     }
-  } else if (mp && mp->strategy) {
+  } else if (nullptr != s->next_hop_strategy) {
     // NextHop only has a findNextHop() function.
-    mp->strategy->findNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine));
-  } else if (s->parent_params) {
+    
s->next_hop_strategy->findNextHop(reinterpret_cast<TSHttpTxn>(s->state_machine));
+  } else if (nullptr != s->parent_params) {
     s->parent_params->nextParent(&s->request_data, &s->parent_result, 
s->txn_conf->parent_fail_threshold,
                                  s->txn_conf->parent_retry_time);
   }
@@ -404,12 +394,11 @@ response_is_retryable(HttpTransact::State *s, HTTPStatus 
response_code)
   if (s->response_action.handled) {
     return s->response_action.action.responseIsRetryable ? 
ParentRetry_t::SIMPLE : ParentRetry_t::NONE;
   }
-  const url_mapping *mp = s->url_map.getMapping();
-  if (mp && mp->strategy) {
-    return mp->strategy->responseIsRetryable(s->state_machine->sm_id, 
s->current, response_code);
+  if (nullptr != s->next_hop_strategy) {
+    return s->next_hop_strategy->responseIsRetryable(s->state_machine->sm_id, 
s->current, response_code);
   }
 
-  if (s->parent_params && 
!s->parent_result.response_is_retryable(s->parent_result.retry_type(), 
response_code)) {
+  if (nullptr != s->parent_params && 
!s->parent_result.response_is_retryable(s->parent_result.retry_type(), 
response_code)) {
     return ParentRetry_t::NONE;
   }
   const ParentRetry_t s_retry_type = retry_type(s);
@@ -6516,8 +6505,8 @@ HttpTransact::process_quick_http_filter(State *s, int 
method)
   }
 
   // if the "ip_allow" named filter is deactivated in the remap.config, then 
don't modify anything
-  url_mapping *mp = s->url_map.getMapping();
-  if (mp && !mp->ip_allow_check_enabled_p) {
+  url_mapping const *const mp = s->url_map.getMapping();
+  if (nullptr != mp && !mp->ip_allow_check_enabled_p) {
     return;
   }
 
diff --git a/src/proxy/http/remap/NextHopStrategyFactory.cc 
b/src/proxy/http/remap/NextHopStrategyFactory.cc
index 12bad353f6..b0e6ba97f8 100644
--- a/src/proxy/http/remap/NextHopStrategyFactory.cc
+++ b/src/proxy/http/remap/NextHopStrategyFactory.cc
@@ -124,16 +124,16 @@ done:
 NextHopStrategyFactory::~NextHopStrategyFactory()
 {
   NH_Dbg(NH_DBG_CTL, "destroying NextHopStrategyFactory");
+
+  for (auto &[_, ptr] : _strategies) {
+    delete ptr;
+  }
 }
 
 void
 NextHopStrategyFactory::createStrategy(const std::string &name, const 
NHPolicyType policy_type, ts::Yaml::Map &node)
 {
-  std::shared_ptr<NextHopSelectionStrategy> strat;
-  std::shared_ptr<NextHopRoundRobin>        strat_rr;
-  std::shared_ptr<NextHopConsistentHash>    strat_chash;
-
-  strat = strategyInstance(name.c_str());
+  NextHopSelectionStrategy const *strat = strategyInstance(name.c_str());
   if (strat != nullptr) {
     NH_Note("A strategy named '%s' has already been loaded and another will 
not be created.", name.data());
     node.bad();
@@ -145,26 +145,28 @@ NextHopStrategyFactory::createStrategy(const std::string 
&name, const NHPolicyTy
     case NHPolicyType::FIRST_LIVE:
     case NHPolicyType::RR_STRICT:
     case NHPolicyType::RR_IP:
-    case NHPolicyType::RR_LATCHED:
-      strat_rr = std::make_shared<NextHopRoundRobin>(name, policy_type, node);
+    case NHPolicyType::RR_LATCHED: {
+      NextHopRoundRobin *const strat_rr = new NextHopRoundRobin(name, 
policy_type, node);
       _strategies.emplace(std::make_pair(std::string(name), strat_rr));
       break;
-    case NHPolicyType::CONSISTENT_HASH:
-      strat_chash = std::make_shared<NextHopConsistentHash>(name, policy_type, 
node);
+    }
+    case NHPolicyType::CONSISTENT_HASH: {
+      NextHopConsistentHash *const strat_chash = new 
NextHopConsistentHash(name, policy_type, node);
       _strategies.emplace(std::make_pair(std::string(name), strat_chash));
       break;
+    }
     default: // handles ParentRR_t::UNDEFINED, no strategy is added
       break;
     };
   } catch (std::exception &ex) {
-    strat.reset();
+    ;
   }
 }
 
-std::shared_ptr<NextHopSelectionStrategy>
-NextHopStrategyFactory::strategyInstance(const char *name)
+NextHopSelectionStrategy *
+NextHopStrategyFactory::strategyInstance(const char *name) const
 {
-  std::shared_ptr<NextHopSelectionStrategy> ps_strategy;
+  NextHopSelectionStrategy *ps_strategy = nullptr;
 
   if (!strategies_loaded) {
     NH_Error("no strategy configurations were defined, see definitions in '%s' 
file", fn.c_str());
diff --git a/src/proxy/http/remap/RemapProcessor.cc 
b/src/proxy/http/remap/RemapProcessor.cc
index a345507ffc..e14f8c1217 100644
--- a/src/proxy/http/remap/RemapProcessor.cc
+++ b/src/proxy/http/remap/RemapProcessor.cc
@@ -166,14 +166,11 @@ RemapProcessor::finish_remap(HttpTransact::State *s, 
UrlRewrite *table)
   referer_info *ri;
 
   map = s->url_map.getMapping();
-  if (!map) {
+  if (nullptr == map) {
+    Dbg(dbg_ctl_url_rewrite, "Could not find corresponding url_mapping for 
this transaction");
     return false;
   }
 
-  // if there is a configured next hop strategy, make it available in the 
state.
-  if (map->strategy) {
-    s->next_hop_strategy = map->strategy;
-  }
   // Do fast ACL filtering (it is safe to check map here)
   table->PerformACLFiltering(s, map);
 
@@ -310,7 +307,7 @@ RemapProcessor::perform_remap(Continuation *cont, 
HttpTransact::State *s)
   url_mapping   *map            = s->url_map.getMapping();
   host_hdr_info *hh_info        = &(s->hh_info);
 
-  if (!map) {
+  if (nullptr == map) {
     Error("Could not find corresponding url_mapping for this transaction %p", 
s);
     Dbg(dbg_ctl_url_rewrite, "Could not find corresponding url_mapping for 
this transaction");
     ink_assert(!"this should never happen -- call setup_for_remap first");
@@ -318,6 +315,12 @@ RemapProcessor::perform_remap(Continuation *cont, 
HttpTransact::State *s)
     return ACTION_RESULT_DONE;
   }
 
+  // if there is a configured next hop strategy, make it available in the 
state.
+  if (nullptr != map->strategy) {
+    Dbg(dbg_ctl_url_rewrite, "Setting next-hop strategy to %s", 
map->strategy->strategy_name.c_str());
+    s->next_hop_strategy = map->strategy;
+  }
+
   RemapPlugins plugins(s, request_url, request_header, hh_info);
 
   while (!plugins.run_single_remap()) {
diff --git a/src/proxy/http/remap/unit-tests/test_NextHopConsistentHash.cc 
b/src/proxy/http/remap/unit-tests/test_NextHopConsistentHash.cc
index 1c586dc4e5..5e582b9306 100644
--- a/src/proxy/http/remap/unit-tests/test_NextHopConsistentHash.cc
+++ b/src/proxy/http/remap/unit-tests/test_NextHopConsistentHash.cc
@@ -49,9 +49,8 @@ SCENARIO("Testing NextHopConsistentHash class, using policy 
'consistent_hash'",
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
     // load the configuration strtegies.
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("consistent-hash-1");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("consistent-hash-1");
 
     WHEN("the config is loaded.")
     {
@@ -188,9 +187,8 @@ SCENARIO("Testing NextHopConsistentHash class (all 
firstcalls), using policy 'co
 
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("consistent-hash-1");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("consistent-hash-1");
 
     WHEN("the config is loaded.")
     {
@@ -298,9 +296,8 @@ SCENARIO("Testing NextHop ignore_self_detect false", 
"[NextHopConsistentHash]")
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
     // load the configuration strtegies.
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("ignore-self-detect-false");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("ignore-self-detect-false");
 
     HostStatus &hs = HostStatus::instance();
     hs.setHostStatus("localhost", TSHostStatus::TS_HOST_STATUS_DOWN, 0, 
Reason::SELF_DETECT);
@@ -348,9 +345,8 @@ SCENARIO("Testing NextHop ignore_self_detect true", 
"[NextHopConsistentHash]")
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
     // load the configuration strtegies.
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("ignore-self-detect-true");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("ignore-self-detect-true");
 
     HostStatus &hs = HostStatus::instance();
     hs.setHostStatus("localhost", TSHostStatus::TS_HOST_STATUS_DOWN, 0, 
Reason::SELF_DETECT);
@@ -398,9 +394,8 @@ SCENARIO("Testing NextHopConsistentHash same host different 
port markdown", "[Ne
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
     // load the configuration strtegies.
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("same-host-different-port");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("same-host-different-port");
 
     WHEN("the config is loaded.")
     {
@@ -466,9 +461,8 @@ SCENARIO("Testing NextHopConsistentHash hash_string 
override", "[NextHopConsiste
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
     // load the configuration strtegies.
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("hash-string-override");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("hash-string-override");
 
     WHEN("the config is loaded.")
     {
@@ -526,9 +520,8 @@ SCENARIO("Testing NextHopConsistentHash class (alternating 
rings), using policy
 
   GIVEN("Loading the consistent-hash-tests.yaml config for 'consistent_hash' 
tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
-    strategy = nhf.strategyInstance("consistent-hash-2");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR 
"/consistent-hash-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("consistent-hash-2");
 
     WHEN("the config is loaded.")
     {
@@ -645,9 +638,8 @@ SCENARIO("Testing NextHopConsistentHash using a peering 
ring_mode.")
 
   GIVEN("Loading the peering.yaml config for 'consistent_hash' tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR "/peering.yaml");
-    strategy = nhf.strategyInstance("peering-group-1");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR "/peering.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("peering-group-1");
 
     WHEN("the config is loaded.")
     {
diff --git a/src/proxy/http/remap/unit-tests/test_NextHopRoundRobin.cc 
b/src/proxy/http/remap/unit-tests/test_NextHopRoundRobin.cc
index 2102902f12..2fc1875501 100644
--- a/src/proxy/http/remap/unit-tests/test_NextHopRoundRobin.cc
+++ b/src/proxy/http/remap/unit-tests/test_NextHopRoundRobin.cc
@@ -45,9 +45,8 @@ SCENARIO("Testing NextHopRoundRobin class, using policy 
'rr-strict'", "[NextHopR
 
   GIVEN("Loading the round-robin-tests.yaml config for round robin 'rr-strict' 
tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/round-robin-tests.yaml");
-    strategy = nhf.strategyInstance("rr-strict-exhaust-ring");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR "/round-robin-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("rr-strict-exhaust-ring");
 
     WHEN("the config is loaded.")
     {
@@ -170,9 +169,8 @@ SCENARIO("Testing NextHopRoundRobin class, using policy 
'first-live'", "[NextHop
 
   GIVEN("Loading the round-robin-tests.yaml config for round robin 
'first-live' tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/round-robin-tests.yaml");
-    strategy = nhf.strategyInstance("first-live");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR "/round-robin-tests.yaml");
+    NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("first-live");
 
     WHEN("the config is loaded.")
     {
@@ -239,13 +237,12 @@ SCENARIO("Testing NextHopRoundRobin class, using policy 
'rr-ip'", "[NextHopRound
 
   GIVEN("Loading the round-robin-tests.yaml config for round robin 'rr-ip' 
tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/round-robin-tests.yaml");
-    strategy        = nhf.strategyInstance("rr-ip");
-    sockaddr_in sa1 = {};
-    sockaddr_in sa2 = {};
-    sa1.sin_port    = 10000;
-    sa1.sin_family  = AF_INET;
+    NextHopStrategyFactory          nhf(TS_SRC_DIR "/round-robin-tests.yaml");
+    NextHopSelectionStrategy *const strategy = nhf.strategyInstance("rr-ip");
+    sockaddr_in                     sa1      = {};
+    sockaddr_in                     sa2      = {};
+    sa1.sin_port                             = 10000;
+    sa1.sin_family                           = AF_INET;
     REQUIRE(inet_pton(AF_INET, "192.168.1.1", &(sa1.sin_addr)) == 1);
     sa2.sin_port   = 10001;
     sa2.sin_family = AF_INET;
@@ -327,9 +324,8 @@ SCENARIO("Testing NextHopRoundRobin class, using policy 
'latched'", "[NextHopRou
 
   GIVEN("Loading the round-robin-tests.yaml config for round robin 'latched' 
tests.")
   {
-    std::shared_ptr<NextHopSelectionStrategy> strategy;
-    NextHopStrategyFactory                    nhf(TS_SRC_DIR 
"/round-robin-tests.yaml");
-    strategy = nhf.strategyInstance("latched");
+    NextHopStrategyFactory          nhf(TS_SRC_DIR "/round-robin-tests.yaml");
+    NextHopSelectionStrategy *const strategy = nhf.strategyInstance("latched");
 
     WHEN("the config is loaded.")
     {
diff --git a/src/proxy/http/remap/unit-tests/test_NextHopStrategyFactory.cc 
b/src/proxy/http/remap/unit-tests/test_NextHopStrategyFactory.cc
index 490e389ccb..9ad20006a5 100644
--- a/src/proxy/http/remap/unit-tests/test_NextHopStrategyFactory.cc
+++ b/src/proxy/http/remap/unit-tests/test_NextHopStrategyFactory.cc
@@ -62,7 +62,7 @@ SCENARIO("factory tests loading yaml configs", "[loadConfig]")
     {
       THEN("Expect that these results for 'strategy-1'")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("strategy-1");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("strategy-1");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->parent_is_proxy == true);
         CHECK(strategy->max_simple_retries == 1);
@@ -70,7 +70,7 @@ SCENARIO("factory tests loading yaml configs", "[loadConfig]")
 
         // down cast here using the stored pointer so that I can verify the 
hash_key was set
         // properly.
-        NextHopConsistentHash *ptr = static_cast<NextHopConsistentHash 
*>(strategy.get());
+        NextHopConsistentHash *const ptr = static_cast<NextHopConsistentHash 
*>(strategy);
         REQUIRE(ptr != nullptr);
         CHECK(ptr->hash_key == NHHashKeyType::CACHE_HASH_KEY);
 
@@ -147,7 +147,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("Expect that these results for 'strategy-2'")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("strategy-2");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("strategy-2");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_STRICT);
         CHECK(strategy->go_direct == true);
@@ -234,7 +234,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("Expect that these results for 'strategy-3'")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("strategy-3");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("strategy-3");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_IP);
         CHECK(strategy->go_direct == true);
@@ -309,7 +309,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("Expect that these results for 'strategy-4'")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("strategy-4");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("strategy-4");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_LATCHED);
         CHECK(strategy->go_direct == true);
@@ -375,7 +375,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("expect the following details.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-north");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-north");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->parent_is_proxy == false);
         CHECK(strategy->max_simple_retries == 2);
@@ -454,7 +454,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-south");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-south");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_LATCHED);
         CHECK(strategy->parent_is_proxy == false);
@@ -534,7 +534,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-east");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-east");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::FIRST_LIVE);
         CHECK(strategy->parent_is_proxy == false);
@@ -614,7 +614,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-west");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-west");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_STRICT);
         CHECK(strategy->go_direct == true);
@@ -693,7 +693,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-midwest");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-midwest");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::CONSISTENT_HASH);
         CHECK(strategy->parent_is_proxy == false);
@@ -701,7 +701,7 @@ SCENARIO("factory tests loading yaml configs", 
"[loadConfig]")
 
         // I need to down cast here using the stored pointer so that I can 
verify that
         // the hash_key was set properly.
-        NextHopConsistentHash *ptr = static_cast<NextHopConsistentHash 
*>(strategy.get());
+        NextHopConsistentHash *const ptr = static_cast<NextHopConsistentHash 
*>(strategy);
         REQUIRE(ptr != nullptr);
         CHECK(ptr->hash_key == NHHashKeyType::URL_HASH_KEY);
 
@@ -797,7 +797,7 @@ SCENARIO("factory tests loading yaml configs from a 
directory", "[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-north");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-north");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->parent_is_proxy == false);
         CHECK(strategy->max_simple_retries == 2);
@@ -879,7 +879,7 @@ SCENARIO("factory tests loading yaml configs from a 
directory", "[loadConfig]")
     {
       THEN("expect the following results.")
       {
-        std::shared_ptr<NextHopSelectionStrategy> strategy = 
nhf.strategyInstance("mid-tier-south");
+        NextHopSelectionStrategy *const strategy = 
nhf.strategyInstance("mid-tier-south");
         REQUIRE(strategy != nullptr);
         CHECK(strategy->policy_type == NHPolicyType::RR_LATCHED);
         CHECK(strategy->parent_is_proxy == false);
diff --git a/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py 
b/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py
new file mode 100644
index 0000000000..6a8b1a549b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py
@@ -0,0 +1,330 @@
+'''
+'''
+#  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
+
+Test.Summary = '''
+Combined header_rewrite/regex_remap/tslua strategies tests
+'''
+
+# Test description:
+# Preload the cache with the entire asset to be range requested.
+# Reload remap rule with slice plugin
+# Request content through the slice plugin
+
+Test.SkipUnless(
+    Condition.PluginExists('header_rewrite.so'),
+    Condition.PluginExists('regex_remap.so'),
+    Condition.PluginExists('tslua.so'),
+)
+Test.ContinueOnFail = False
+
+dns = Test.MakeDNServer("dns")
+
+origins = []
+num_origins = 3
+for ind in range(num_origins):
+    name = f"nh{ind}"
+    origin = Test.MakeOriginServer(name, options={"--verbose": ""})
+    request_header = {
+        "headers": f"GET / HTTP/1.1\r\nHost: origin\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": "",
+    }
+    response_header = {
+        "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": "",
+    }
+    origin.addResponse("sessionfile.log", request_header, response_header)
+    request_header = {
+        "headers": "GET /path HTTP/1.1\r\nHost: origin\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": "",
+    }
+    response_header = {
+        "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: 
{name}\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": name,
+    }
+    origin.addResponse("sessionfile.log", request_header, response_header)
+    request_header = {
+        "headers": f"GET /path/{name} HTTP/1.1\r\nHost: origin\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": "",
+    }
+    response_header = {
+        "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: 
{name}\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": name,
+    }
+    origin.addResponse("sessionfile.log", request_header, response_header)
+    origin.ReturnCode = 0
+    origins.append(origin)
+    dns.addRecords(records={name: ["127.0.0.1"]})
+
+# Define ATS and configure
+ts = Test.MakeATSProcess("ts", enable_cache=False)
+ts.ReturnCode = 0
+ts.Disk.records_config.update(
+    {
+        'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}",
+        'proxy.config.dns.resolv_conf': "NULL",
+        'proxy.config.http.cache.http': 0,
+        "proxy.config.http.insert_response_via_str": 1,
+        'proxy.config.http.uncacheable_requests_bypass_parent': 0,
+        'proxy.config.http.no_dns_just_forward_to_parent': 1,
+        'proxy.config.http.parent_proxy.mark_down_hostdb': 0,
+        'proxy.config.http.parent_proxy.self_detect': 0,
+        'proxy.config.diags.debug.enabled': 1,
+        'proxy.config.diags.debug.tags': 
"url_rewrite|next_hop|dns|parent|regex_remap|header_rewrite|tslua|http|hostdb",
+    })
+
+ts.Disk.MakeConfigFile("hdr_rw.config").AddLines(
+    [
+        "cond %{REMAP_PSEUDO_HOOK}",
+        'cond %{CLIENT-HEADER:Strategy} ="" [NOT]',
+        "set-next-hop-strategy %{CLIENT-HEADER:Strategy}",
+    ])
+ts.Disk.MakeConfigFile("regex_remap.config").AddLines(
+    [
+        "/nh0 http://origin/path @strategy=nh1",
+        '/nh1 http://origin/path @strategy=',
+        "/nh2 http://origin/path @strategy=nh0",
+        "/nemo http://origin/path @strategy=nemo",
+    ])
+ts.Disk.MakeConfigFile("strategies.lua").AddLines(
+    [
+        'function do_remap()',
+        ' local uri = ts.client_request.get_uri()',
+        ' if uri:find("nh0") then',
+        '  ts.http.set_next_hop_strategy("nh1")',
+        ' elseif uri:find("nh1") then',
+        '  ts.http.set_next_hop_strategy("")',
+        ' elseif uri:find("nh2") then',
+        '  ts.http.set_next_hop_strategy("nh0")',
+        ' elseif uri:find("nemo") then',
+        '  ts.http.set_next_hop_strategy("nemo")',
+        ' end',
+        ' ts.client_request.set_uri("path")',
+        ' return 0',
+        'end',
+    ])
+
+# parent.config
+ts.Disk.parent_config.AddLines(
+    [f'dest_domain=. parent="nh2:{origins[2].Variables.Port}" 
round_robin=false go_direct=false parent_is_proxy=false'])
+
+# build strategies.yaml file
+ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", 
typename="ats:config")
+
+s = ts.Disk.strategies
+s.AddLine("groups:")
+for ind in range(num_origins - 1):
+    name = f"nh{ind}"
+    s.AddLines(
+        [
+            f"  - &g{ind}",
+            f"    - host: {name}",
+            f"      protocol:",
+            f"      - scheme: http",
+            f"        port: {origins[ind].Variables.Port}",
+            f"      weight: 1.0",
+        ])
+
+s.AddLine("strategies:")
+
+# third ts_nh
+for ind in range(num_origins - 1):
+    s.AddLines(
+        [
+            f"  - strategy: nh{ind}",
+            f"    policy: consistent_hash",
+            f"    hash_key: path",
+            f"    go_direct: false",
+            f"    parent_is_proxy: false",
+            f"    ignore_self_detect: true",
+            f"    groups:",
+            f"      - *g{ind}",
+            f"    scheme: http",
+        ])
+
+ts.Disk.remap_config.AddLines(
+    [
+        "map http://nh0_hr http://origin @strategy=nh0 
@plugin=header_rewrite.so @pparam=hdr_rw.config",
+        "map http://nh1_hr http://origin @strategy=nh1 
@plugin=header_rewrite.so @pparam=hdr_rw.config",
+        "map http://nh2_hr http://origin @plugin=header_rewrite.so 
@pparam=hdr_rw.config",
+        "map http://nh0_rr http://origin @strategy=nh0 @plugin=regex_remap.so 
@pparam=regex_remap.config",
+        "map http://nh1_rr http://origin @strategy=nh1 @plugin=regex_remap.so 
@pparam=regex_remap.config",
+        "map http://nh2_rr http://origin @plugin=regex_remap.so 
@pparam=regex_remap.config",
+        "map http://nh0_lua http://origin @strategy=nh0 @plugin=tslua.so 
@pparam=strategies.lua",
+        "map http://nh1_lua http://origin @strategy=nh1 @plugin=tslua.so 
@pparam=strategies.lua",
+        "map http://nh2_lua http://origin @plugin=tslua.so 
@pparam=strategies.lua",
+    ])
+
+# Tests
+
+# stdout for body, stderr for headers
+curl_and_args = '-s -o /dev/stdout -D /dev/stderr -x 
localhost:{}'.format(ts.Variables.port)
+
+# header rewrite
+
+# 0 - nh0 default request
+tr = Test.AddTestRun("nh0_hr straight through request")
+ps = tr.Processes.Default
+for ind in range(num_origins):
+    origin = origins[ind]
+    ps.StartBefore(origin, ready=When.PortOpen(origin.Variables.Port))
+    tr.StillRunningAfter = origin
+ps.StartBefore(dns)
+ps.StartBefore(Test.Processes.ts)
+tr.MakeCurlCommand(curl_and_args + " http://nh0_hr/path";, ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 1 - nh1_hr default request
+tr = Test.AddTestRun("nh1_hr straight through request")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + " http://nh1_hr/path";, ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1")
+tr.StillRunningAfter = ts
+
+# 2 - nh2_hr default request
+tr = Test.AddTestRun("nh2_hr straight through request")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + " http://nh2_hr/path";, ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2")
+tr.StillRunningAfter = ts
+
+# 3 switch strategies
+tr = Test.AddTestRun("nh0_hr switch to nh1")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nh1"', 
ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 4 strategy to parent.config
+tr = Test.AddTestRun("nh1_hr switch to parent.config")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh1_hr/path -H "Strategy: null"', 
ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 5 parent.config strategy to strategy
+tr = Test.AddTestRun("nh2_hr switch to nh0")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh2_hr/path -H "Strategy: nh0"', 
ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 6 try to switch to non existent strategy
+tr = Test.AddTestRun("nh0_hr switch to nemo (fail)")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nemo"', 
ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# regex_remap
+
+# 7 switch strategies
+tr = Test.AddTestRun("nh0_rr switch to nh1")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nh0', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 8 strategy to parent.config
+tr = Test.AddTestRun("nh1_rr switch to parent.config")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh1_rr/nh1', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 9 parent.config strategy to strategy
+tr = Test.AddTestRun("nh2_rr switch to nh0")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh2_rr/nh2', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 10 switch strategies (fail)
+tr = Test.AddTestRun("nh0_rr switch to nemo")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nemo', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# tslua
+
+# 11 switch strategies
+tr = Test.AddTestRun("nh0_lua switch to nh1")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nh0', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 12 strategy to parent.config
+tr = Test.AddTestRun("nh1_lua switch to parent.config")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh1_lua/nh1', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 13 parent.config strategy to strategy
+tr = Test.AddTestRun("nh2_lua switch to nh0")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh2_lua/nh2', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# 14 switch strategies, fail
+tr = Test.AddTestRun("nh0_lua switch to nemo")
+ps = tr.Processes.Default
+tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nemo', ts=ts)
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0")
+tr.StillRunningAfter = ts
+tr.StillRunnerAfter = dns
+
+# Overriding the built in ERROR check since we expect some ERROR messages
+ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests 
are failure tests")

Reply via email to