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
5 changes: 4 additions & 1 deletion configs/envoy_service_to_service.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@
"filters": [
{"type": "decoder", "name": "rate_limit",
"config": {
"domain": "envoy_service_to_service"
"domain": "envoy_service_to_service",
"actions": [
{"type": "service_to_service"}
]
}
},
{"type": "both", "name": "grpc_http1_bridge", "config": {}},
Expand Down
50 changes: 39 additions & 11 deletions docs/configuration/http_filters/rate_limit_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,59 @@ Global rate limiting :ref:`architecture overview <arch_overview_rate_limit>`.

The HTTP rate limit filter will call the rate limit service when the request's route has the
*global* property set in the :ref:`rate limit configuration
<config_http_conn_man_route_table_route_rate_limit>`. If the rate limit service is called, the
following descriptors are sent:
<config_http_conn_man_route_table_route_rate_limit>`.

* ("to_cluster", "<:ref:`route target cluster <config_http_conn_man_route_table_route_cluster>`>")
* ("to_cluster", "<:ref:`route target cluster <config_http_conn_man_route_table_route_cluster>`>"),
("from_cluster", "<local service cluster>")

<local service cluster> is derived from the :option:`--service-cluster` option.

If the rate limit service is called, and the response for either of the above descriptors is over
limit, a 429 response is returned.
If the rate limit service is called, and the response for any of the descriptors is over limit, a
429 response is returned.

.. code-block:: json

{
"type": "decoder",
"name": "rate_limit",
"config": {
"domain": "..."
"domain": "...",
"actions": []
}
}

domain
*(required, string)* The rate limit domain to use when calling the rate limit service.

actions
*(required, array)* An array of rate limiting actions to perform. Multiple actions can be
specified. The supported action types are documented below.

Actions
-------

.. code-block:: json

{
"type": "..."
}

type
*(required, string) The type of rate limit action to perform. The currently supported action
type is *service_to_service*.

Service to service
^^^^^^^^^^^^^^^^^^

.. code-block:: json

{
"type": "service_to_service"
}

The following descriptors are sent:

* ("to_cluster", "<:ref:`route target cluster <config_http_conn_man_route_table_route_cluster>`>")
* ("to_cluster", "<:ref:`route target cluster <config_http_conn_man_route_table_route_cluster>`>"),
("from_cluster", "<local service cluster>")

<local service cluster> is derived from the :option:`--service-cluster` option.

Statistics
----------

Expand Down
80 changes: 50 additions & 30 deletions source/common/http/filter/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,78 @@
#include "common/http/headers.h"

namespace Http {
namespace RateLimit {

const Http::HeaderMapImpl RateLimitFilter::TOO_MANY_REQUESTS_HEADER{
const Http::HeaderMapImpl Filter::TOO_MANY_REQUESTS_HEADER{
{Http::Headers::get().Status, std::to_string(enumToInt(Code::TooManyRequests))}};

RateLimitFilterConfig::RateLimitFilterConfig(const Json::Object& config,
const std::string& local_service_cluster,
Stats::Store& stats_store, Runtime::Loader& runtime)
void ServiceToServiceAction::populateDescriptors(const Router::RouteEntry& route,
std::vector<::RateLimit::Descriptor>& descriptors,
FilterConfig& config) {
// We limit on 2 dimensions.
// 1) All calls to the given cluster.
// 2) Calls to the given cluster and from this cluster.
// The service side configuration can choose to limit on 1 or both of the above.
descriptors.push_back({{{"to_cluster", route.clusterName()}}});
descriptors.push_back(
{{{"to_cluster", route.clusterName()}, {"from_cluster", config.localServiceCluster()}}});
}

FilterConfig::FilterConfig(const Json::Object& config, const std::string& local_service_cluster,
Stats::Store& stats_store, Runtime::Loader& runtime)
: domain_(config.getString("domain")), local_service_cluster_(local_service_cluster),
stats_store_(stats_store), runtime_(runtime) {}
stats_store_(stats_store), runtime_(runtime) {
for (const Json::Object& action : config.getObjectArray("actions")) {
std::string type = action.getString("type");
if (type == "service_to_service") {
actions_.emplace_back(new ServiceToServiceAction());
} else {
throw EnvoyException(fmt::format("unknown http rate limit filter action '{}'", type));
}
}
}

FilterHeadersStatus RateLimitFilter::decodeHeaders(HeaderMap& headers, bool) {
FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) {
if (!config_->runtime().snapshot().featureEnabled("ratelimit.http_filter_enabled", 100)) {
return FilterHeadersStatus::Continue;
}

const Router::RouteEntry* route = callbacks_->routeTable().routeForRequest(headers);
if (route && route->rateLimitPolicy().doGlobalLimiting()) {
cluster_stat_prefix_ = fmt::format("cluster.{}.", route->clusterName());
cluster_ratelimit_stat_prefix_ = fmt::format("{}ratelimit.", cluster_stat_prefix_);

// We limit on 2 dimensions.
// 1) All calls to the given cluster.
// 2) Calls to the given cluster and from this cluster.
// The service side configuration can choose to limit on 1 or both of the above.
// NOTE: In the future we might add more things such as the path of the request.
std::vector<RateLimit::Descriptor> descriptors = {
{{{"to_cluster", route->clusterName()}}},
{{{"to_cluster", route->clusterName()}, {"from_cluster", config_->localServiceCluster()}}}};

state_ = State::Calling;
initiating_call_ = true;
client_->limit(*this, config_->domain(), descriptors);
initiating_call_ = false;
std::vector<::RateLimit::Descriptor> descriptors;
for (const ActionPtr& action : config_->actions()) {
action->populateDescriptors(*route, descriptors, *config_);
}

if (!descriptors.empty()) {
cluster_stat_prefix_ = fmt::format("cluster.{}.", route->clusterName());
cluster_ratelimit_stat_prefix_ = fmt::format("{}ratelimit.", cluster_stat_prefix_);

state_ = State::Calling;
initiating_call_ = true;
client_->limit(*this, config_->domain(), descriptors);
initiating_call_ = false;
}
}

return (state_ == State::Calling || state_ == State::Responded)
? FilterHeadersStatus::StopIteration
: FilterHeadersStatus::Continue;
}

FilterDataStatus RateLimitFilter::decodeData(Buffer::Instance&, bool) {
FilterDataStatus Filter::decodeData(Buffer::Instance&, bool) {
ASSERT(state_ != State::Responded);
return state_ == State::Calling ? FilterDataStatus::StopIterationAndBuffer
: FilterDataStatus::Continue;
}

FilterTrailersStatus RateLimitFilter::decodeTrailers(HeaderMap&) {
FilterTrailersStatus Filter::decodeTrailers(HeaderMap&) {
ASSERT(state_ != State::Responded);
return state_ == State::Calling ? FilterTrailersStatus::StopIteration
: FilterTrailersStatus::Continue;
}

void RateLimitFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) {
void Filter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) {
callbacks_ = &callbacks;
callbacks.addResetStreamCallback([this]() -> void {
if (state_ == State::Calling) {
Expand All @@ -70,17 +89,17 @@ void RateLimitFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& ca
});
}

void RateLimitFilter::complete(RateLimit::LimitStatus status) {
void Filter::complete(::RateLimit::LimitStatus status) {
state_ = State::Complete;

switch (status) {
case RateLimit::LimitStatus::OK:
case ::RateLimit::LimitStatus::OK:
config_->stats().counter(cluster_ratelimit_stat_prefix_ + "ok").inc();
break;
case RateLimit::LimitStatus::Error:
case ::RateLimit::LimitStatus::Error:
config_->stats().counter(cluster_ratelimit_stat_prefix_ + "error").inc();
break;
case RateLimit::LimitStatus::OverLimit:
case ::RateLimit::LimitStatus::OverLimit:
config_->stats().counter(cluster_ratelimit_stat_prefix_ + "over_limit").inc();
Http::CodeUtility::ResponseStatInfo info{config_->stats(), cluster_stat_prefix_,
TOO_MANY_REQUESTS_HEADER, true, EMPTY_STRING,
Expand All @@ -89,7 +108,7 @@ void RateLimitFilter::complete(RateLimit::LimitStatus status) {
break;
}

if (status == RateLimit::LimitStatus::OverLimit &&
if (status == ::RateLimit::LimitStatus::OverLimit &&
config_->runtime().snapshot().featureEnabled("ratelimit.http_filter_enforcing", 100)) {
state_ = State::Responded;
Http::HeaderMapPtr response_headers{new HeaderMapImpl(TOO_MANY_REQUESTS_HEADER)};
Expand All @@ -99,4 +118,5 @@ void RateLimitFilter::complete(RateLimit::LimitStatus status) {
}
}

} // RateLimit
} // Http
55 changes: 46 additions & 9 deletions source/common/http/filter/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,50 @@
#include "common/json/json_loader.h"

namespace Http {
namespace RateLimit {

class FilterConfig;

/**
* Generic rate limit action that the filter performs.
*/
class Action {
public:
virtual ~Action() {}

/**
* Potentially populate the descriptor array with new descriptors to query.
* @param route supplies the target route for the request.
* @param descriptors supplies the descriptor array to optionally fill.
* @param config supplies the filter configuration.
*/
virtual void populateDescriptors(const Router::RouteEntry& route,
std::vector<::RateLimit::Descriptor>& descriptors,
FilterConfig& config) PURE;
};

typedef std::unique_ptr<Action> ActionPtr;

/**
* Action for service to service rate limiting.
*/
class ServiceToServiceAction : public Action {
public:
// Action
void populateDescriptors(const Router::RouteEntry& route,
std::vector<::RateLimit::Descriptor>& descriptors,
FilterConfig& config) override;
};

/**
* Global configuration for the HTTP rate limit filter.
*/
class RateLimitFilterConfig {
class FilterConfig {
public:
RateLimitFilterConfig(const Json::Object& config, const std::string& local_service_cluster,
Stats::Store& stats_store, Runtime::Loader& runtime);
FilterConfig(const Json::Object& config, const std::string& local_service_cluster,
Stats::Store& stats_store, Runtime::Loader& runtime);

const std::vector<ActionPtr>& actions() { return actions_; }
const std::string& domain() { return domain_; }
const std::string& localServiceCluster() { return local_service_cluster_; }
Runtime::Loader& runtime() { return runtime_; }
Expand All @@ -27,17 +62,18 @@ class RateLimitFilterConfig {
const std::string local_service_cluster_;
Stats::Store& stats_store_;
Runtime::Loader& runtime_;
std::vector<ActionPtr> actions_;
};

typedef std::shared_ptr<RateLimitFilterConfig> RateLimitFilterConfigPtr;
typedef std::shared_ptr<FilterConfig> FilterConfigPtr;

/**
* HTTP rate limit filter. Depending on the route configuration, this filter calls the global
* rate limiting service before allowing further filter iteration.
*/
class RateLimitFilter : public StreamDecoderFilter, public RateLimit::RequestCallbacks {
class Filter : public StreamDecoderFilter, public ::RateLimit::RequestCallbacks {
public:
RateLimitFilter(RateLimitFilterConfigPtr config, RateLimit::ClientPtr&& client)
Filter(FilterConfigPtr config, ::RateLimit::ClientPtr&& client)
: config_(config), client_(std::move(client)) {}

// Http::StreamDecoderFilter
Expand All @@ -47,20 +83,21 @@ class RateLimitFilter : public StreamDecoderFilter, public RateLimit::RequestCal
void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) override;

// RateLimit::RequestCallbacks
void complete(RateLimit::LimitStatus status) override;
void complete(::RateLimit::LimitStatus status) override;

private:
enum class State { NotStarted, Calling, Complete, Responded };

static const Http::HeaderMapImpl TOO_MANY_REQUESTS_HEADER;

RateLimitFilterConfigPtr config_;
RateLimit::ClientPtr client_;
FilterConfigPtr config_;
::RateLimit::ClientPtr client_;
StreamDecoderFilterCallbacks* callbacks_{};
bool initiating_call_{};
State state_{State::NotStarted};
std::string cluster_ratelimit_stat_prefix_;
std::string cluster_stat_prefix_;
};

} // RateLimit
} // Http
4 changes: 2 additions & 2 deletions source/server/config/http/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ class RateLimitFilterConfig : public HttpFilterConfigFactory {
return nullptr;
}

Http::RateLimitFilterConfigPtr filter_config(new Http::RateLimitFilterConfig(
Http::RateLimit::FilterConfigPtr filter_config(new Http::RateLimit::FilterConfig(
config, server.options().serviceClusterName(), server.stats(), server.runtime()));
return [filter_config, &server](Http::FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterPtr{new Http::RateLimitFilter(
callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterPtr{new Http::RateLimit::Filter(
filter_config, server.rateLimitClient(std::chrono::milliseconds(20)))});
};
}
Expand Down
Loading