Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ message StatefulSession {
config.core.v3.TypedExtensionConfig session_state = 1;

// Determines whether the HTTP request must be strictly routed to the requested destination. When set to ``true``,
// if the requested destination is unavailable, Envoy will return a 503 status code. The default value is ``false``,
// which allows Envoy to fall back to its load balancing mechanism. In this case, if the requested destination is not
// found, the request will be routed according to the load balancing algorithm.
// if the requested destination is not found in the set of available endpoints, Envoy will return a status code
// determined by ``status_on_strict_destination_not_found``. If the destination exists but is unhealthy, Envoy will
// always return ``503`` regardless of ``status_on_strict_destination_not_found``. The default value is ``false``,
// which allows Envoy to fall back to its load balancing mechanism and route the request according to the load
// balancing algorithm.
bool strict = 2;

// Optional stat prefix. If specified, the filter will emit statistics in the
Expand All @@ -38,6 +40,12 @@ message StatefulSession {
// Per-route configuration overrides do not support statistics and will not emit stats even if this field is set
// in the per-route config.
string stat_prefix = 3;

// The HTTP status code to return when ``strict`` mode is enabled and the requested destination
// is not found in the set of available endpoints. This does not apply when the destination exists
// but is unhealthy. This field has no effect when ``strict`` is set to ``false`` and will be
// ignored. Defaults to ``503`` (Service Unavailable) if not specified or set to ``0``.
uint32 status_on_strict_destination_not_found = 4;
}

message StatefulSessionPerRoute {
Expand Down
9 changes: 9 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,15 @@ new_features:
Added support for clear route cache in the :ref:`set_filter_state http filter <config_http_filters_set_filter_state>`. When
``clear_route_cache`` is set, the filter will clear the route cache for the current request after applying filter state updates.
This is necessary if the route configuration may depend on the filter state values set.
- area: stateful_session
change: |
Added :ref:`status_on_strict_destination_not_found
<envoy_v3_api_field_extensions.filters.http.stateful_session.v3.StatefulSession.status_on_strict_destination_not_found>`
to the :ref:`stateful session filter <config_http_filters_stateful_session>`. When
:ref:`strict <envoy_v3_api_field_extensions.filters.http.stateful_session.v3.StatefulSession.strict>`
mode is enabled and the requested destination is not found in the set of available endpoints, Envoy
will return the configured HTTP status code instead of the default ``503``. This does not apply when
the destination exists but is unhealthy.
- area: tcp_proxy
change: |
Propagate upstream TCP RST to downstream when detected close type is RemoteReset.
Expand Down
18 changes: 13 additions & 5 deletions envoy/upstream/load_balancer.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "envoy/common/optref.h"
#include "envoy/common/pure.h"
#include "envoy/config/cluster/v3/cluster.pb.h"
#include "envoy/http/codes.h"
#include "envoy/network/transport_socket.h"
#include "envoy/router/router.h"
#include "envoy/stream_info/stream_info.h"
Expand All @@ -30,14 +31,14 @@ namespace Upstream {
using ClusterProto = envoy::config::cluster::v3::Cluster;

/*
* A handle to allow cancelation of asynchronous host selection.
* A handle to allow cancellation of asynchronous host selection.
* If chooseHost returns a HostSelectionResponse with an AsyncHostSelectionHandle
* handle, and the endpoint does not wish to receive onAsyncHostSelction call,
* handle, and the endpoint does not wish to receive onAsyncHostSelection call,
* it must call cancel() on the provided handle.
*
* Please note that the AsyncHostSelectionHandle may be deleted after the
* cancel() call. It is up to the implemention of the asynchronous load balancer
* to ensure the cancelation state persists until the load balancer checks it.
* cancel() call. It is up to the implementation of the asynchronous load balancer
* to ensure the cancellation state persists until the load balancer checks it.
*/
class AsyncHostSelectionHandle {
public:
Expand All @@ -53,7 +54,7 @@ class AsyncHostSelectionHandle {
* load balancing, returns an AsyncHostSelectionHandle handle.
*
* If it returns a AsyncHostSelectionHandle handle, the load balancer guarantees an
* eventual call to LoadBalancerContext::onAsyncHostSelction unless
* eventual call to LoadBalancerContext::onAsyncHostSelection unless
* AsyncHostSelectionHandle::cancel is called.
*/
struct HostSelectionResponse {
Expand All @@ -66,6 +67,9 @@ struct HostSelectionResponse {
// Optional details if host selection fails (empty string implies no details).
std::string details;
std::unique_ptr<AsyncHostSelectionHandle> cancelable;
// Optional HTTP status code to use when host selection fails because the strict override
// destination is missing from available endpoints. If not set, defaults to 503.
absl::optional<Http::Code> failure_status;
};

/**
Expand Down Expand Up @@ -156,6 +160,10 @@ class LoadBalancerContext {
// If strict and no valid host is found, the load balancer should return nullptr.
// If not strict, the load balancer will select another host if the target host is not valid.
bool strict{false};
// The HTTP status code to return when strict mode is enabled and the target host
// is not found in the set of available endpoints. Does not apply when the host
// exists but is unhealthy. Defaults to 503 (ServiceUnavailable).
Http::Code status_on_strict_destination_not_found{Http::Code::ServiceUnavailable};
};
/**
* Returns the host the load balancer should select directly. If the expected host exists and
Expand Down
24 changes: 14 additions & 10 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
// well as handling unsupported asynchronous host selection by treating it
// as host selection failure and calling sendNoHealthyUpstreamResponse.
continueDecodeHeaders(cluster, headers, end_stream, std::move(host_selection_response.host),
host_selection_response.details);
host_selection_response.details, host_selection_response.failure_status);
return Http::FilterHeadersStatus::StopIteration;
}

Expand Down Expand Up @@ -756,13 +756,14 @@ void Filter::onAsyncHostSelection(Upstream::HostConstSharedPtr&& host, std::stri
bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster,
Http::RequestHeaderMap& headers, bool end_stream,
Upstream::HostConstSharedPtr&& selected_host,
absl::string_view host_selection_details) {
absl::string_view host_selection_details,
absl::optional<Http::Code> failure_status) {
callbacks_->streamInfo().downstreamTiming().setValue(
"envoy.router.host_selection_end_ms", callbacks_->dispatcher().timeSource().monotonicTime());

std::unique_ptr<GenericConnPool> generic_conn_pool = createConnPool(*cluster, selected_host);
if (!generic_conn_pool) {
sendNoHealthyUpstreamResponse(host_selection_details);
sendNoHealthyUpstreamResponse(host_selection_details, failure_status);
return false;
}
Upstream::HostDescriptionConstSharedPtr host = generic_conn_pool->host();
Expand Down Expand Up @@ -952,14 +953,16 @@ std::unique_ptr<GenericConnPool> Filter::createConnPool(Upstream::ThreadLocalClu
callbacks_->streamInfo().protocol(), this, *message);
}

void Filter::sendNoHealthyUpstreamResponse(absl::string_view optional_details) {
void Filter::sendNoHealthyUpstreamResponse(absl::string_view optional_details,
absl::optional<Http::Code> failure_status) {
const Http::Code status_code = failure_status.value_or(Http::Code::ServiceUnavailable);
callbacks_->streamInfo().setResponseFlag(StreamInfo::CoreResponseFlag::NoHealthyUpstream);
chargeUpstreamCode(Http::Code::ServiceUnavailable, {}, false);
chargeUpstreamCode(status_code, {}, false);
absl::string_view details = optional_details.empty()
? StreamInfo::ResponseCodeDetails::get().NoHealthyUpstream
: optional_details;
callbacks_->sendLocalReply(Http::Code::ServiceUnavailable, "no healthy upstream", modify_headers_,
absl::nullopt, details);
callbacks_->sendLocalReply(status_code, "no healthy upstream", modify_headers_, absl::nullopt,
details);
}

uint64_t Filter::calculateEffectiveBufferLimit() const {
Expand Down Expand Up @@ -2241,7 +2244,7 @@ void Filter::doRetry(bool can_send_early_data, bool can_use_http3, TimeoutRetry
// as host selection failure).
continueDoRetry(can_send_early_data, can_use_http3, is_timeout_retry,
std::move(host_selection_response.host), *cluster,
host_selection_response.details);
host_selection_response.details, host_selection_response.failure_status);
}

ENVOY_STREAM_LOG(debug, "Handling asynchronous host selection for retry\n", *callbacks_);
Expand All @@ -2259,12 +2262,13 @@ void Filter::doRetry(bool can_send_early_data, bool can_use_http3, TimeoutRetry
void Filter::continueDoRetry(bool can_send_early_data, bool can_use_http3,
TimeoutRetry is_timeout_retry, Upstream::HostConstSharedPtr&& host,
Upstream::ThreadLocalCluster& cluster,
absl::string_view host_selection_details) {
absl::string_view host_selection_details,
absl::optional<Http::Code> failure_status) {
callbacks_->streamInfo().downstreamTiming().setValue(
"envoy.router.host_selection_end_ms", callbacks_->dispatcher().timeSource().monotonicTime());
std::unique_ptr<GenericConnPool> generic_conn_pool = createConnPool(cluster, host);
if (!generic_conn_pool) {
sendNoHealthyUpstreamResponse(host_selection_details);
sendNoHealthyUpstreamResponse(host_selection_details, failure_status);
cleanup();
return;
}
Expand Down
9 changes: 6 additions & 3 deletions source/common/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ class Filter : Logger::Loggable<Logger::Id::router>,

bool continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster, Http::RequestHeaderMap& headers,
bool end_stream, Upstream::HostConstSharedPtr&& host,
absl::string_view host_selection_details = {});
absl::string_view host_selection_details = {},
absl::optional<Http::Code> failure_status = absl::nullopt);

Http::FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override;
Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap& trailers) override;
Expand Down Expand Up @@ -603,7 +604,8 @@ class Filter : Logger::Loggable<Logger::Id::router>,
// if a "good" response comes back and we return downstream, so there is no point in waiting
// for the remaining upstream requests to return.
void resetOtherUpstreams(UpstreamRequest& upstream_request);
void sendNoHealthyUpstreamResponse(absl::string_view details);
void sendNoHealthyUpstreamResponse(absl::string_view details,
absl::optional<Http::Code> failure_status = absl::nullopt);
bool setupRedirect(const Http::ResponseHeaderMap& headers);
bool convertRequestHeadersForInternalRedirect(Http::RequestHeaderMap& downstream_headers,
const Http::ResponseHeaderMap& upstream_headers,
Expand All @@ -614,7 +616,8 @@ class Filter : Logger::Loggable<Logger::Id::router>,
void doRetry(bool can_send_early_data, bool can_use_http3, TimeoutRetry is_timeout_retry);
void continueDoRetry(bool can_send_early_data, bool can_use_http3, TimeoutRetry is_timeout_retry,
Upstream::HostConstSharedPtr&& host, Upstream::ThreadLocalCluster& cluster,
absl::string_view host_selection_details);
absl::string_view host_selection_details,
absl::optional<Http::Code> failure_status = absl::nullopt);
void updateStatsOnNoRetry(RetryStatus retry_status);
void updateStatsOnDoRetry(RetryState::DoRetryType do_retry_type);

Expand Down
30 changes: 20 additions & 10 deletions source/common/upstream/cluster_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2064,13 +2064,13 @@ void ClusterManagerImpl::ThreadLocalClusterManagerImpl::httpConnPoolIsIdle(
HostSelectionResponse ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::chooseHost(
LoadBalancerContext* context) {
auto cross_priority_host_map = priority_set_.crossPriorityHostMap();
auto host_and_strict_mode = HostUtility::selectOverrideHost(cross_priority_host_map.get(),
override_host_statuses_, context);
if (host_and_strict_mode.first != nullptr) {
return {std::move(host_and_strict_mode.first)};
auto override_result = HostUtility::selectOverrideHost(cross_priority_host_map.get(),
override_host_statuses_, context);
if (override_result.host != nullptr) {
return {std::move(override_result.host)};
}

if (!host_and_strict_mode.second) {
if (!override_result.strict) {
Upstream::HostSelectionResponse host_selection = lb_->chooseHost(context);
if (host_selection.host || host_selection.cancelable) {
return host_selection;
Expand All @@ -2082,16 +2082,26 @@ HostSelectionResponse ClusterManagerImpl::ThreadLocalClusterManagerImpl::Cluster

cluster_info_->trafficStats()->upstream_cx_none_healthy_.inc();
ENVOY_LOG(debug, "no healthy host");
return {nullptr};
HostSelectionResponse response{nullptr};
// Only apply the custom failure status when the destination is missing from
// available endpoints, not when it exists but is unhealthy.
if (override_result.status == HostUtility::OverrideHostSelectionStatus::NotFound) {
if (context != nullptr) {
if (auto override_host = context->overrideHostToSelect(); override_host.has_value()) {
response.failure_status = override_host->status_on_strict_destination_not_found;
}
}
}
return response;
}

HostConstSharedPtr ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::peekAnotherHost(
LoadBalancerContext* context) {
auto cross_priority_host_map = priority_set_.crossPriorityHostMap();
auto host_and_strict_mode = HostUtility::selectOverrideHost(cross_priority_host_map.get(),
override_host_statuses_, context);
if (host_and_strict_mode.first != nullptr) {
return std::move(host_and_strict_mode.first);
auto override_result = HostUtility::selectOverrideHost(cross_priority_host_map.get(),
override_host_statuses_, context);
if (override_result.host != nullptr) {
return std::move(override_result.host);
}
// TODO(wbpcode): should we do strict mode check of override host here?
return lb_->peekAnotherHost(context);
Expand Down
18 changes: 9 additions & 9 deletions source/common/upstream/host_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -137,39 +137,39 @@ HostUtility::HostStatusSet HostUtility::createOverrideHostStatus(
return override_host_status;
}

std::pair<HostConstSharedPtr, bool> HostUtility::selectOverrideHost(const HostMap* host_map,
HostStatusSet status,
LoadBalancerContext* context) {
HostUtility::OverrideHostSelectionResult
HostUtility::selectOverrideHost(const HostMap* host_map, HostStatusSet status,
LoadBalancerContext* context) {
if (context == nullptr) {
return {nullptr, false};
return {};
}

OptRef<const Upstream::LoadBalancerContext::OverrideHost> override_host =
context->overrideHostToSelect();
if (!override_host.has_value()) {
return {nullptr, false};
return {};
}

const bool strict_mode = override_host->strict;

if (host_map == nullptr) {
return {nullptr, strict_mode};
return {nullptr, strict_mode, OverrideHostSelectionStatus::NotFound};
}

auto host_iter = host_map->find(override_host->host);

// The override host cannot be found in the host map.
if (host_iter == host_map->end()) {
return {nullptr, strict_mode};
return {nullptr, strict_mode, OverrideHostSelectionStatus::NotFound};
}

HostConstSharedPtr host = host_iter->second;
ASSERT(host != nullptr);

if (status[static_cast<uint32_t>(host->healthStatus())]) {
return {host, strict_mode};
return {host, strict_mode, OverrideHostSelectionStatus::Success};
}
return {nullptr, strict_mode};
return {nullptr, strict_mode, OverrideHostSelectionStatus::Unhealthy};
}

void HostUtility::forEachHostMetric(
Expand Down
26 changes: 23 additions & 3 deletions source/common/upstream/host_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,33 @@ class HostUtility {
// A utility function to create override host status from lb config.
static HostStatusSet createOverrideHostStatus(const CommonLbConfigProto& common_config);

// Status of override host selection.
enum class OverrideHostSelectionStatus {
// Host was successfully selected.
Success,
// Host was not found in the host map.
NotFound,
// Host was found but is not healthy.
Unhealthy,
};

// Result of attempting to select an override host from the host map.
struct OverrideHostSelectionResult {
// The selected host, or nullptr if selection failed.
HostConstSharedPtr host;
// Whether strict mode was requested for the override.
bool strict{false};
// The status of the override host selection.
OverrideHostSelectionStatus status{OverrideHostSelectionStatus::Success};
};

/**
* A utility function to select override host from host map according to load balancer context.
*
* @return pair<HostConstSharedPtr, bool> the first element is the selected host and the second
* element is a boolean indicating whether the host should be selected strictly or not.
* @return OverrideHostSelectionResult containing the selected host, whether strict mode was
* requested, and the reason for the selection outcome.
*/
static std::pair<HostConstSharedPtr, bool>
static OverrideHostSelectionResult
selectOverrideHost(const HostMap* host_map, HostStatusSet status, LoadBalancerContext* context);

// Iterate over all per-endpoint metrics, for clusters with `per_endpoint_stats` enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class EmptySessionStateFactory : public Envoy::Http::SessionStateFactory {
StatefulSessionConfig::StatefulSessionConfig(const ProtoConfig& config,
Server::Configuration::GenericFactoryContext& context,
const std::string& stats_prefix, Stats::Scope& scope)
: strict_(config.strict()) {
: strict_(config.strict()),
status_on_strict_destination_not_found_(
config.strict() && config.status_on_strict_destination_not_found() != 0
? static_cast<Http::Code>(config.status_on_strict_destination_not_found())
: Http::Code::ServiceUnavailable) {
// Only construct stats if stat_prefix is explicitly set.
if (!config.stat_prefix().empty()) {
const std::string final_prefix =
Expand Down Expand Up @@ -83,7 +87,8 @@ Http::FilterHeadersStatus StatefulSession::decodeHeaders(Http::RequestHeaderMap&

if (auto upstream_address = session_state_->upstreamAddress(); upstream_address.has_value()) {
decoder_callbacks_->setUpstreamOverrideHost(Upstream::LoadBalancerContext::OverrideHost{
std::string(upstream_address.value()), effective_config_->isStrict()});
std::string(upstream_address.value()), effective_config_->isStrict(),
effective_config_->statusOnMissingStrictDestination()});
}
return Http::FilterHeadersStatus::Continue;
}
Expand Down
Loading
Loading