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
80 changes: 40 additions & 40 deletions docs/features/ratelimiting.rst
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
Rate Limiting
=============
Thanks to `@catcherwong article <http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/>`_ for inspiring me to finally write this documentation.
Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much.
OK so to get rate limiting working for a ReRoute you need to add the following json to it.
.. code-block:: json
"RateLimitOptions": {
"ClientWhitelist": [],
"EnableRateLimiting": true,
"Period": "1s",
"PeriodTimespan": 1,
"Limit": 1
}
ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting.
EnableRateLimiting - This value specifies enable endpoint rate limiting.
Period - This value specifies the period, such as 1s, 5m, 1h,1d and so on.
PeriodTimespan - This value specifies that we can retry after a certain number of seconds.
Limit - This value specifies the maximum number of requests that a client can make in a defined period.
You can also set the following in the GlobalConfiguration part of ocelot.json
.. code-block:: json
"RateLimitOptions": {
"DisableRateLimitHeaders": false,
"QuotaExceededMessage": "Customize Tips!",
"HttpStatusCode": 999,
"ClientIdHeader" : "Test"
}
DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled.
QuotaExceededMessage - This value specifies the exceeded message.
HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs.
ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId"
Rate Limiting
=============

Thanks to `@catcherwong article <http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/>`_ for inspiring me to finally write this documentation.

Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much.

OK so to get rate limiting working for a ReRoute you need to add the following json to it.

.. code-block:: json

"RateLimitOptions": {
"ClientWhitelist": [],
"EnableRateLimiting": true,
"Period": "1s",
"PeriodTimespan": 1,
"Limit": 1
}

ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting.
EnableRateLimiting - This value specifies enable endpoint rate limiting.
Period - This value specifies the period that the limit applies to, such as 1s, 5m, 1h,1d and so on. If you make more requests in the period than the limit allows then you need to wait for PeriodTimespan to elapse before you make another request.
PeriodTimespan - This value specifies that we can retry after a certain number of seconds.
Limit - This value specifies the maximum number of requests that a client can make in a defined period.

You can also set the following in the GlobalConfiguration part of ocelot.json

.. code-block:: json

"RateLimitOptions": {
"DisableRateLimitHeaders": false,
"QuotaExceededMessage": "Customize Tips!",
"HttpStatusCode": 999,
"ClientIdHeader" : "Test"
}

DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled.
QuotaExceededMessage - This value specifies the exceeded message.
HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs.
ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId"
296 changes: 148 additions & 148 deletions src/Ocelot/RateLimit/RateLimitCore.cs
Original file line number Diff line number Diff line change
@@ -1,148 +1,148 @@
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class RateLimitCore
{
private readonly IRateLimitCounterHandler _counterHandler;
private static readonly object _processLocker = new object();
public RateLimitCore(IRateLimitCounterHandler counterStore)
{
_counterHandler = counterStore;
}
public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1);
var rule = option.RateLimitRule;
var counterId = ComputeCounterKey(requestIdentity, option);
// serial reads and writes
lock (_processLocker)
{
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
// entry has not expired
if (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period) >= DateTime.UtcNow)
{
// increment request count
var totalRequests = entry.Value.TotalRequests + 1;
// deep copy
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
}
}
}
if (counter.TotalRequests > rule.Limit)
{
var retryAfter = RetryAfterFrom(counter.Timestamp, rule);
if (retryAfter > 0)
{
var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan);
_counterHandler.Set(counterId, counter, expirationTime);
}
else
{
_counterHandler.Remove(counterId);
}
}
else
{
var expirationTime = ConvertToTimeSpan(rule.Period);
_counterHandler.Set(counterId, counter, expirationTime);
}
return counter;
}
public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime)
{
var counterId = ComputeCounterKey(requestIdentity, option);
var rule = option.RateLimitRule;
// stores: id (string) - timestamp (datetime) - total_requests (long)
_counterHandler.Set(counterId, counter, expirationTime);
}
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var rule = option.RateLimitRule;
RateLimitHeaders headers = null;
var counterId = ComputeCounterKey(requestIdentity, option);
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
headers = new RateLimitHeaders(context, rule.Period,
(rule.Limit - entry.Value.TotalRequests).ToString(),
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
);
}
else
{
headers = new RateLimitHeaders(context,
rule.Period,
rule.Limit.ToString(),
(DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
}
return headers;
}
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
var idBytes = Encoding.UTF8.GetBytes(key);
byte[] hashBytes;
using (var algorithm = SHA1.Create())
{
hashBytes = algorithm.ComputeHash(idBytes);
}
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
}
public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
{
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds);
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
return retryAfter;
}
public TimeSpan ConvertToTimeSpan(string timeSpan)
{
var l = timeSpan.Length - 1;
var value = timeSpan.Substring(0, l);
var type = timeSpan.Substring(l, 1);
switch (type)
{
case "d":
return TimeSpan.FromDays(double.Parse(value));
case "h":
return TimeSpan.FromHours(double.Parse(value));
case "m":
return TimeSpan.FromMinutes(double.Parse(value));
case "s":
return TimeSpan.FromSeconds(double.Parse(value));
default:
throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
}
}
}
}
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Ocelot.RateLimit
{
public class RateLimitCore
{
private readonly IRateLimitCounterHandler _counterHandler;
private static readonly object _processLocker = new object();

public RateLimitCore(IRateLimitCounterHandler counterStore)
{
_counterHandler = counterStore;
}

public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1);
var rule = option.RateLimitRule;

var counterId = ComputeCounterKey(requestIdentity, option);

// serial reads and writes
lock (_processLocker)
{
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
// entry has not expired
if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow)
{
// increment request count
var totalRequests = entry.Value.TotalRequests + 1;

// deep copy
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
}
}
}

if (counter.TotalRequests > rule.Limit)
{
var retryAfter = RetryAfterFrom(counter.Timestamp, rule);
if (retryAfter > 0)
{
var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan);
_counterHandler.Set(counterId, counter, expirationTime);
}
else
{
_counterHandler.Remove(counterId);
}
}
else
{
var expirationTime = ConvertToTimeSpan(rule.Period);
_counterHandler.Set(counterId, counter, expirationTime);
}

return counter;
}

public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime)
{
var counterId = ComputeCounterKey(requestIdentity, option);
var rule = option.RateLimitRule;

// stores: id (string) - timestamp (datetime) - total_requests (long)
_counterHandler.Set(counterId, counter, expirationTime);
}

public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var rule = option.RateLimitRule;
RateLimitHeaders headers = null;
var counterId = ComputeCounterKey(requestIdentity, option);
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
headers = new RateLimitHeaders(context, rule.Period,
(rule.Limit - entry.Value.TotalRequests).ToString(),
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
);
}
else
{
headers = new RateLimitHeaders(context,
rule.Period,
rule.Limit.ToString(),
(DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
}

return headers;
}

public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";

var idBytes = Encoding.UTF8.GetBytes(key);

byte[] hashBytes;

using (var algorithm = SHA1.Create())
{
hashBytes = algorithm.ComputeHash(idBytes);
}

return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
}

public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
{
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds);
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
return retryAfter;
}

public TimeSpan ConvertToTimeSpan(string timeSpan)
{
var l = timeSpan.Length - 1;
var value = timeSpan.Substring(0, l);
var type = timeSpan.Substring(l, 1);

switch (type)
{
case "d":
return TimeSpan.FromDays(double.Parse(value));
case "h":
return TimeSpan.FromHours(double.Parse(value));
case "m":
return TimeSpan.FromMinutes(double.Parse(value));
case "s":
return TimeSpan.FromSeconds(double.Parse(value));
default:
throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
}
}
}
}
Loading