blob: e308e73a4bb7bc40c4830b3c640384a2189556ad [file] [log] [blame]
/** @file
A brief file description
@section license License
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 <optional>
#include <yaml-cpp/yaml.h>
#include <tsutil/YamlCfg.h>
#include "iocore/utils/Machine.h"
#include "proxy/http/HttpSM.h"
#include "proxy/http/remap/NextHopSelectionStrategy.h"
DbgCtl NH_DBG_CTL{"next_hop"};
// ring mode strings
constexpr std::string_view alternate_rings = "alternate_ring";
constexpr std::string_view exhaust_rings = "exhaust_ring";
constexpr std::string_view peering_rings = "peering_ring";
// health check strings
constexpr std::string_view active_health_check = "active";
constexpr std::string_view passive_health_check = "passive";
constexpr const char *policy_strings[] = {"NHPolicyType::UNDEFINED", "NHPolicyType::FIRST_LIVE", "NHPolicyType::RR_STRICT",
"NHPolicyType::RR_IP", "NHPolicyType::RR_LATCHED", "NHPolicyType::CONSISTENT_HASH"};
NextHopSelectionStrategy::NextHopSelectionStrategy(const std::string_view &name, const NHPolicyType &policy, ts::Yaml::Map &n)
: strategy_name(name), policy_type(policy)
{
NH_Dbg(NH_DBG_CTL, "NextHopSelectionStrategy calling constructor");
NH_Dbg(NH_DBG_CTL, "Using a selection strategy of type %s", policy_strings[static_cast<int>(policy)]);
std::string self_host;
bool self_host_used = false;
try {
// scheme is optional, and strategies with no scheme will match hosts with no scheme
if (n["scheme"]) {
auto scheme_val = n["scheme"].Scalar();
if (scheme_val == "http") {
scheme = NHSchemeType::HTTP;
} else if (scheme_val == "https") {
scheme = NHSchemeType::HTTPS;
} else {
NH_Note("Invalid scheme '%s' for strategy '%s', setting to NONE", scheme_val.c_str(), strategy_name.c_str());
}
}
// go_direct config.
if (n["go_direct"]) {
go_direct = n["go_direct"].as<bool>();
}
// parent_is_proxy config.
if (n["parent_is_proxy"]) {
parent_is_proxy = n["parent_is_proxy"].as<bool>();
}
// ignore_self_detect
if (n["ignore_self_detect"]) {
ignore_self_detect = n["ignore_self_detect"].as<bool>();
}
if (n["cache_peer_result"]) {
cache_peer_result = n["cache_peer_result"].as<bool>();
}
if (n["host_override"]) {
host_override = n["host_override"].as<bool>();
}
if (n["use_pristine"]) {
use_pristine = n["use_pristine"].as<bool>();
}
// failover node.
YAML::Node failover_node_n = n["failover"];
if (failover_node_n) {
ts::Yaml::Map failover_node{failover_node_n};
if (failover_node["ring_mode"]) {
auto ring_mode_val = failover_node["ring_mode"].Scalar();
if (ring_mode_val == alternate_rings) {
ring_mode = NHRingMode::ALTERNATE_RING;
} else if (ring_mode_val == exhaust_rings) {
ring_mode = NHRingMode::EXHAUST_RING;
} else if (ring_mode_val == peering_rings) {
ring_mode = NHRingMode::PEERING_RING;
YAML::Node self_node = failover_node["self"];
if (self_node) {
self_host = self_node.Scalar();
NH_Dbg(NH_DBG_CTL, "%s is self", self_host.c_str());
}
} else {
ring_mode = NHRingMode::ALTERNATE_RING;
NH_Note("Invalid 'ring_mode' value, '%s', for the strategy named '%s', using default '%s'.", ring_mode_val.c_str(),
strategy_name.c_str(), alternate_rings.data());
}
}
if (failover_node["max_simple_retries"]) {
max_simple_retries = failover_node["max_simple_retries"].as<int>();
}
if (failover_node["max_unavailable_retries"]) {
max_unavailable_retries = failover_node["max_unavailable_retries"].as<int>();
}
// response codes for simple retry.
YAML::Node resp_codes_node;
if (failover_node["response_codes"]) {
resp_codes_node = failover_node["response_codes"];
if (resp_codes_node.Type() != YAML::NodeType::Sequence) {
NH_Error("Error in the response_codes definition for the strategy named '%s', skipping response_codes.",
strategy_name.c_str());
} else {
for (auto &&k : resp_codes_node) {
auto code = k.as<int>();
if (code > 300 && code < 599) {
resp_codes.add(code);
} else {
NH_Note("Skipping invalid response code '%d' for the strategy named '%s'.", code, strategy_name.c_str());
}
}
resp_codes.sort();
}
}
YAML::Node markdown_codes_node;
if (failover_node["markdown_codes"]) {
markdown_codes_node = failover_node["markdown_codes"];
if (markdown_codes_node.Type() != YAML::NodeType::Sequence) {
NH_Error("Error in the markdown_codes definition for the strategy named '%s', skipping markdown_codes.",
strategy_name.c_str());
} else {
for (auto &&k : markdown_codes_node) {
auto code = k.as<int>();
if (code > 300 && code < 599) {
markdown_codes.add(code);
} else {
NH_Note("Skipping invalid markdown response code '%d' for the strategy named '%s'.", code, strategy_name.c_str());
}
}
markdown_codes.sort();
}
}
YAML::Node health_check_node;
if (failover_node["health_check"]) {
health_check_node = failover_node["health_check"];
if (health_check_node.Type() != YAML::NodeType::Sequence) {
NH_Error("Error in the health_check definition for the strategy named '%s', skipping health_checks.",
strategy_name.c_str());
} else {
for (auto it = health_check_node.begin(); it != health_check_node.end(); ++it) {
auto health_check = it->as<std::string>();
if (health_check.compare(active_health_check) == 0) {
health_checks.active = true;
}
if (health_check.compare(passive_health_check) == 0) {
health_checks.passive = true;
}
}
}
}
failover_node.done();
}
// parse and load the host data
YAML::Node groups_node = n["groups"];
if (groups_node) {
// a groups list is required.
if (groups_node.Type() != YAML::NodeType::Sequence) {
throw std::invalid_argument("Invalid groups definition, expected a sequence, '" + strategy_name + "' cannot be loaded.");
} else {
Machine *mach = Machine::instance();
HostStatus &h_stat = HostStatus::instance();
uint32_t grp_size = groups_node.size();
if (grp_size > MAX_GROUP_RINGS) {
NH_Note("the groups list exceeds the maximum of %d for the strategy '%s'. Only the first %d groups will be configured.",
MAX_GROUP_RINGS, strategy_name.c_str(), MAX_GROUP_RINGS);
groups = MAX_GROUP_RINGS;
} else {
groups = groups_node.size();
}
// resize the hosts vector.
host_groups.reserve(groups);
// loop through the groups
for (unsigned int grp = 0; grp < groups; ++grp) {
YAML::Node hosts_list = groups_node[grp];
// a list of hosts is required.
if (hosts_list.Type() != YAML::NodeType::Sequence) {
throw std::invalid_argument("Invalid hosts definition, expected a sequence, '" + strategy_name + "' cannot be loaded.");
} else {
// loop through the hosts list.
std::vector<std::shared_ptr<HostRecord>> hosts_inner;
for (unsigned int hst = 0; hst < hosts_list.size(); ++hst) {
std::shared_ptr<HostRecord> host_rec = std::make_shared<HostRecord>(hosts_list[hst].as<HostRecordCfg>());
host_rec->group_index = grp;
host_rec->host_index = hst;
if ((self_host == host_rec->hostname) || mach->is_self(host_rec->hostname.c_str())) {
if (ring_mode == NHRingMode::PEERING_RING && grp != 0) {
throw std::invalid_argument("self host (" + self_host +
") can only appear in first host group for peering ring mode");
}
h_stat.setHostStatus(host_rec->hostname.c_str(), TSHostStatus::TS_HOST_STATUS_DOWN, 0, Reason::SELF_DETECT);
host_rec->self = true;
self_host_used = true;
}
hosts_inner.push_back(std::move(host_rec));
num_parents++;
}
passive_health.insert(hosts_inner);
host_groups.push_back(std::move(hosts_inner));
}
}
}
}
if (!self_host.empty() && !self_host_used) {
throw std::invalid_argument("self host (" + self_host + ") does not appear in the first (peer) group");
}
} catch (std::exception &ex) {
throw std::invalid_argument("Error parsing the strategy named '" + strategy_name + "' due to '" + ex.what() +
"', this strategy will be ignored.");
}
if (ring_mode == NHRingMode::PEERING_RING) {
if (groups == 1) {
if (!go_direct) {
throw std::invalid_argument("ring mode '" + std::string(peering_rings) +
"' go_direct must be true when there is only one host group");
}
} else if (groups != 2) {
throw std::invalid_argument(
"ring mode '" + std::string(peering_rings) +
"' requires two host groups (peering group and an upstream group), or a single peering group with go_direct");
}
if (policy_type != NHPolicyType::CONSISTENT_HASH) {
throw std::invalid_argument("ring mode '" + std::string(peering_rings) +
"' is only implemented for a 'consistent_hash' policy");
}
}
}
void
NextHopSelectionStrategy::markNextHop(TSHttpTxn txnp, const char *hostname, const int port, const NHCmd status, void *ih,
const time_t now)
{
return passive_health.markNextHop(txnp, hostname, port, status, ih, now);
}
void
NextHopSelectionStrategy::setHostHeader(TSHttpTxn txnp, const char *hostname)
{
if (host_override && nullptr != hostname) {
HttpSM *sm = reinterpret_cast<HttpSM *>(txnp);
sm->t_state.hdr_info.client_request.value_set(static_cast<std::string_view>(MIME_FIELD_HOST), hostname);
NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] overriding host header with parent %s", sm->sm_id, hostname);
}
}
bool
NextHopSelectionStrategy::nextHopExists(TSHttpTxn txnp, void * /* ih ATS_UNUSED */)
{
HttpSM *sm = reinterpret_cast<HttpSM *>(txnp);
int64_t sm_id = sm->sm_id;
for (uint32_t gg = 0; gg < groups; gg++) {
for (auto &hh : host_groups[gg]) {
HostRecord *p = hh.get();
if (p->available.load()) {
NH_Dbg(NH_DBG_CTL,
"[%" PRIu64 "] found available next hop %s (this is NOT necessarily the parent which will be selected, just the "
"first available parent found)",
sm_id, p->hostname.c_str());
return true;
}
}
}
return false;
}
ParentRetry_t
NextHopSelectionStrategy::responseIsRetryable(int64_t sm_id, HttpTransact::CurrentInfo &current_info, HTTPStatus response_code)
{
unsigned sa = current_info.simple_retry_attempts;
unsigned ua = current_info.unavailable_server_retry_attempts;
NH_Dbg(NH_DBG_CTL,
"[%" PRIu64 "] response_code %d, simple_retry_attempts: %d max_simple_retries: %d, unavailable_server_retry_attempts: "
"%d, max_unavailable_retries: %d",
sm_id, static_cast<int>(response_code), sa, this->max_simple_retries, ua, max_unavailable_retries);
if (this->resp_codes.contains(static_cast<short>(response_code)) && sa < this->max_simple_retries && sa < this->num_parents) {
NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] response code %d is retryable, returning ParentRetry_t::SIMPLE", sm_id,
static_cast<int>(response_code));
return ParentRetry_t::SIMPLE;
}
if (this->markdown_codes.contains(static_cast<short>(response_code)) && ua < this->max_unavailable_retries &&
ua < this->num_parents) {
NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] response code %d is retryable, returning ParentRetry_t::UNAVAILABLE_SERVER", sm_id,
static_cast<int>(response_code));
return ParentRetry_t::UNAVAILABLE_SERVER;
}
NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] response code %d is not retryable, returning ParentRetry_t::NONE", sm_id,
static_cast<int>(response_code));
return ParentRetry_t::NONE;
}
namespace YAML
{
template <> struct convert<HostRecordCfg> {
static bool
decode(const Node &node, HostRecordCfg &nh)
{
ts::Yaml::Map map{node};
ts::Yaml::Map *mmap{&map};
std::optional<ts::Yaml::Map> mergeable_map;
// check for YAML merge tag.
YAML::Node mergeable_map_n = map["<<"];
if (mergeable_map_n) {
mergeable_map.emplace(mergeable_map_n);
mmap = &mergeable_map.value();
}
// lookup the hostname
if ((*mmap)["host"]) {
nh.hostname = (*mmap)["host"].Scalar();
} else {
throw std::invalid_argument("Invalid host definition, missing host name.");
}
// lookup the port numbers supported by this host.
YAML::Node proto = (*mmap)["protocol"];
if (proto.Type() != YAML::NodeType::Sequence) {
throw std::invalid_argument("Invalid host protocol definition, expected a sequence.");
} else {
for (auto &&ii : proto) {
const YAML::Node &protocol_node = ii;
std::shared_ptr<NHProtocol> pr = std::make_shared<NHProtocol>(protocol_node.as<NHProtocol>());
nh.protocols.push_back(std::move(pr));
}
}
// get the host's weight, allowing override of weight in merged map
YAML::Node weight = map["weight"];
if (mmap != &map) {
// weight must always be looked up in the merged map, even if overridden, so it's presence will not
// cause an exception when mmap->done() is called
YAML::Node w = (*mmap)["weight"];
if (!weight) {
weight = w;
}
}
if (weight) {
nh.weight = weight.as<float>();
} else {
NH_Note("No weight is defined for the host '%s', using default 1.0", nh.hostname.data());
nh.weight = 1.0;
}
// get the host's optional hash_string
YAML::Node hash{(*mmap)["hash_string"]};
if (hash) {
nh.hash_string = hash.Scalar();
}
map.done();
if (mmap != &map) {
mmap->done();
}
return true;
}
};
template <> struct convert<NHProtocol> {
static bool
decode(const Node &node, NHProtocol &nh)
{
ts::Yaml::Map map{node};
// scheme is optional, and strategies with no scheme will match hosts with no scheme
if (map["scheme"]) {
const auto scheme_val = map["scheme"].Scalar();
if (scheme_val == "http") {
nh.scheme = NHSchemeType::HTTP;
} else if (scheme_val == "https") {
nh.scheme = NHSchemeType::HTTPS;
} else {
NH_Note("Invalid scheme '%s' for protocol, setting to NONE", scheme_val.c_str());
}
}
if (map["port"]) {
nh.port = map["port"].as<int>();
if (nh.port <= 0 || nh.port > 65535) {
throw YAML::ParserException(map["port"].Mark(), "port number must be in (inclusive) range 1 - 65,536");
}
} else {
if (nh.port == 0) {
throw YAML::ParserException(map["port"].Mark(), "no port defined must be in (inclusive) range 1 - 65,536");
}
}
if (map["health_check_url"]) {
nh.health_check_url = map["health_check_url"].Scalar();
}
map.done();
return true;
}
};
}; // namespace YAML
// namespace YAML