8000 rolling window rate limiter by caoilainnl · Pull Request #25423 · ccxt/ccxt · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

rolling window rate limiter #25423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
99b74e6
rolling window rate limiter
caoilainnl Jun 2, 2025
d08ed7a
rollingWindowRateLimiter for other languages
caoilainnl Jun 2, 2025
c21347f
C# rate limiter fix
caoilainnl Jun 10, 2025
eb26af7
python rate limiter fix
caoilainnl Jun 10, 2025
2f29eaf
rollingWindowLoop fixes
caoilainnl Jun 10, 2025
55e493d
ratelimiter go fixes
caoilainnl Jun 10, 2025
e0a6de7
go throttler is lock safe
caoilainnl Jun 10, 2025
aad1d09
go rate limiter fix
caoilainnl Jun 11, 2025
01d61f7
Merge branch 'master' into rolling-window-rate-limiter
caoilainnl Jun 11, 2025
1b3b430
Merge branch 'master' into rolling-window-rate-limiter
caoilainnl Jun 12, 2025
0d554a1
Update cs/ccxt/base/Exchange.BaseMethods.cs
ttodua Jun 13, 2025
ef9cf93
Update cs/ccxt/base/Exchange.BaseMethods.cs
ttodua Jun 13, 2025
b760ac6
Update cs/ccxt/base/Exchange.BaseMethods.cs
ttodua Jun 13, 2025
6f70fa8
Update cs/ccxt/base/Exchange.BaseMethods.cs
ttodua Jun 13, 2025
70ae641
Update cs/ccxt/base/Throttler.cs
ttodua Jun 13, 2025
26f2c6c
move out of lock
ttodua Jun 13, 2025
9b4e539
Merge branch 'rolling-window-rate-limiter' of github.com:caoilainnl/c…
ttodua Jun 13, 2025
87a6264
tokens > cost
ttodua Jun 13, 2025
833b96e
updates
ttodua Jun 13, 2025
718f665
Update cs/ccxt/base/Throttler.cs
ttodua Jun 14, 2025
28e1081
Update cs/ccxt/base/Throttler.cs
ttodua Jun 14, 2025
3f108d0
var change
ttodua Jun 14, 2025
e7be89c
rev go formatting
ttodua Jun 14, 2025
f0abc26
Merge branch 'master' into rolling-window-rate-limiter
caoilainnl Jun 16, 2025
7dcc605
Update ts/src/base/functions/throttle.ts
caoilainnl Jun 16, 2025
dac6a77
Update cs/ccxt/base/Throttler.cs
caoilainnl Jun 16, 2025
97bbd32
removed lock from leaky bucket loop
caoilainnl Jun 16, 2025
573861f
Merge branch 'rolling-window-rate-limiter' of https://github.com/caoi…
caoilainnl Jun 16, 2025
fdabf94
throttler config typed object
caoilainnl Jun 16, 2025
8290df6
rev
ttodua Jun 16, 2025
d88fabf
min
ttodua Jun 16, 2025
09498f5
spacing
ttodua Jun 16, 2025
eca3391
ts simplify
ttodua Jun 16, 2025
c0df892
php clauses
ttodua Jun 16, 2025
361340d
use sleep in php
ttodua Jun 16, 2025
35a4b53
Merge branch 'master' into rolling-window-rate-limiter
ttodua Jun 16, 2025
587c69b
-
ttodua Jun 16, 2025
d9f82ad
-
ttodua Jun 16, 2025
868dbe5
simplify php
ttodua Jun 16, 2025
19a90e0
optimizations - ts, php, py
ttodua Jun 16, 2025
1fe42b8
Update cs/ccxt/base/Throttler.cs
caoilainnl Jun 16, 2025
c967782
Update cs/ccxt/base/Throttler.cs
caoilainnl Jun 16, 2025
1bee5c4
Update cs/ccxt/base/Throttler.cs
caoilainnl Jun 16, 2025
a2db733
Update cs/ccxt/base/Throttler.cs
caoilainnl Jun 16, 2025
b1e81f0
throttler updates
caoilainnl Jun 16, 2025
5beff86
sleep fix
ttodua Jun 17, 2025
3cab749
Merge branch 'rolling-window-rate-limiter' of github.com:caoilainnl/c…
ttodua Jun 17, 2025
0e6588d
Merge branch 'master' into rolling-window-rate-limiter
ttodua Jun 19, 2025
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
6 changes: 6 additions & 0 deletions cs/ccxt/base/Exchange.Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public partial class Exchange
public string userAgent { get; set; }
public bool verbose { get; set; } = true;
public bool enableRateLimit { get; set; } = true;
public string rateLimiterAlgorithm {get; set; } = "leakyBucket"; // rollingWindow or leakyBucket
public int maxLimiterRequests {get; set; } = 1000;
public int rollingWindowSize {get; set; } = 60000;
public long lastRestRequestTimestamp { get; set; } = 0;
public string url { get; set; } = "";

Expand Down Expand Up @@ -292,5 +295,8 @@ void initializeProperties(dict userConfig = null)
this.newUpdates = SafeValue(extendedProperties, "newUpdates") as bool? ?? true;
this.accounts = SafeValue(extendedProperties, "accounts") as List<object>;
this.features = SafeValue(extendedProperties, "features", features) as dict;
this.rateLimiterAlgorithm = SafeString(extendedProperties, "rateLimiterAlgorithm", "leakyBucket");
this.maxLimiterRequests = (int)(SafeInteger(extendedProperties, "maxLimiterRequests") ?? 1000);
this.rollingWindowSize = (int)(SafeInteger(extendedProperties, "rollingWindowSize") ?? 60000);
}
}
144 changes: 113 additions & 31 deletions cs/ccxt/base/Throttler.cs
8000
Original file line numberDiff line number Diff line change
Expand Up @@ -3,30 +3,49 @@
namespace ccxt;

using dict = Dictionary<string, object>;
public class Throttler

public class ThrottlerConfig
{
public double RefillRate { get; set; } = 1.0;
public double Delay { get; set; } = 0.001;
public double Cost { get; set; } = 1.0;
public double Tokens { get; set; } = 0.0;
public int MaxLimiterRequests { get; set; } = 2000;
public double Capacity { get; set; } = 1.0;
public string Algorithm { get; set; } = "leakyBucket";
public double RateLimit { get; set; } = 0.0;
public double WindowSize { get; set; } = 60000.0;
public double MaxWeight { get; set; }
}

private dict config = new dict();
public class Throttler
{
private ThrottlerConfig config = new ThrottlerConfig();
private Queue<(Task, double)> queue = new Queue<(Task, double)>();
private readonly object queueLock = new object();

private bool running = false;
private List<(long timestamp, double cost)> timestamps = new List<(long, double)>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosmiei what about having ConcurrentList here instead of the regular list? or SlimList even?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have to look into this more, I've read a little bit about it and it looks like right now, timestamps is single-threaded, so a plain List is fine (and slightly faster). Switching to a concurrent collection should only happen if the list is to run outside rollingWindowLoop() as well (which I don't think it ever would)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosmiei what about having ConcurrentList here instead of the regular list? or SlimList even?

I think it's better to use a regular list. I haven't run into concurrency issues using it yet, and it looks like a plain List is slightly faster

Copy link
Author
@caoilainnl caoilainnl Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is needed, I created thread safe timestamps on caoilainnl@d1feae0 , I don't think it is though because theres only 1 iteration of the rollingWindowLoop executing at a time, and the timestamp list isn't accessed anywhere else (other than NewThrottler, which is only called once)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosmiei do you think a thread safe timestamp list would be useful?


public Throttler(dict config)
public Throttler(Dictionary<string, object> configInput)
{
this.config = new Dictionary<string, object>()
// Convert the dictionary input to our typed config
if (configInput != null)
{
{"refillRate",1.0},
{"delay", 0.001},
{"cost", 1.0},
{"tokens", 0},
{"maxCapacity", 2000},
{"capacity", 1.0},
};
this.config = extend(this.config, config);

config.RefillRate = configInput.TryGetValue("refillRate", out var refillRate) ? Convert.ToDouble(refillRate) : config.RefillRate;
config.Delay = configInput.TryGetValue("delay", out var delay) ? Convert.ToDouble(delay) : config.Delay;
config.Cost = configInput.TryGetValue("cost", out var cost) ? Convert.ToDouble(cost) : config.Cost;
config.Tokens = configInput.TryGetValue("tokens", out var tokens) ? Convert.ToDouble(tokens) : config.Tokens;
config.MaxLimiterRequests = configInput.TryGetValue("maxLimiterRequests", out var maxRequests) ? Convert.ToInt32(maxRequests) : config.MaxLimiterRequests;
config.Capacity = configInput.TryGetValue("capacity", out var capacity) ? Convert.ToDouble(capacity) : config.Capacity;
config.Algorithm = configInput.TryGetValue("algorithm", out var algorithm) ? Convert.ToString(algorithm) : config.Algorithm;
config.RateLimit = configInput.TryGetValue("rateLimit", out var rateLimit) ? Convert.ToDouble(rateLimit) : config.RateLimit;
config.WindowSize = configInput.TryGetValue("windowSize", out var windowSize) ? Convert.ToDouble(windowSize) : config.WindowSize;
config.MaxWeight = configInput.TryGetValue("maxWeight", out var maxWeight) ? Convert.ToDouble(maxWeight) : config.MaxWeight;
}
}

private async Task loop()
private async Task leakyBucketLoop()
{
var lastTimestamp = milliseconds();
while (this.running)
Expand All @@ -40,49 +59,112 @@ private async Task loop()
var first = this.queue.Peek();
var task = first.Item1;
var cost = first.Item2;
var tokensAsString = Convert.ToString(this.config["tokens"], CultureInfo.InvariantCulture);
var floatTokens = double.Parse(tokensAsString, CultureInfo.InvariantCulture);
var floatTokens = this.config.Tokens;
if (floatTokens >= 0)
{
this.config["tokens"] = floatTokens - cost;
await Task.Delay(0);
this.config.Tokens = floatTokens - cost;
if (task != null)
{
if (task.Status == TaskStatus.Created)
{
task.Start();
}
}
this.queue.Dequeue();

if (this.queue.Count == 0)
await Task.Delay(0);
lock (queueLock)
{
this.running = false;
this.queue.Dequeue();
if (this.queue.Count == 0)
{
this.running = false;
}
}
}
else
{
await Task.Delay((int)((double)this.config["delay"] * 1000));
await Task.Delay((int)(this.config.Delay * 1000));
var current = milliseconds();
var elapsed = current - lastTimestamp;
lastTimestamp = current;
var tokens = (double)this.config["tokens"] + ((double)this.config["refillRate"] * elapsed);
this.config["tokens"] = Math.Min(tokens, (int)this.config["capacity"]);
var tokens = (double)this.config.Tokens + ((double)this.config.RefillRate * elapsed);
this.config.Tokens = Math.Min(tokens, (double)this.config.Capacity);
}
}

}

public async Task<Task> throttle(object cost2)
private async Task rollingWindowLoop()
{
while (this.running)
{
lock (queueLock)
{
if (this.queue.Count == 0)
{
this.running = false;
continue;
}
}
(Task, double) first;
lock (queueLock)
{
first = this.queue.Peek();
}
var task = first.Item1;
var cost = first.Item2;
var now = milliseconds();
var windowSize = this.config.WindowSize;
timestamps.RemoveAll(t => now - t.timestamp >= windowSize);
var totalCost = timestamps.Sum(t => t.cost);
if (totalCost + cost <= this.config.MaxWeight)
{
timestamps.Add((now, cost));
if (task != null && task.Status == TaskStatus.Created)
{
task.Start();
}
await Task.Delay(0);
lock (queueLock)
{
this.queue.Dequeue();
if (this.queue.Count == 0) this.running = false;
}
}
else
{
var earliest = timestamps[0].timestamp;
var waitTime = (earliest + windowSize) - now;
if (waitTime > 0)
{
await Task.Delay((int)waitTime);
}
}
}
}

var cost = (cost2 != null) ? Convert.ToDouble(cost2) : Convert.ToDouble(this.config["cost"]);
if (this.queue.Count > (int)this.config["maxCapacity"])
private async Task loop()
{
if (this.config.Algorithm == "leakyBucket")
{
throw new Exception("throttle queue is over maxCapacity (" + this.config["maxCapacity"].ToString() + "), see https://github.com/ccxt/ccxt/issues/11645#issuecomment-1195695526");
await leakyBucketLoop();
}
else
{
await rollingWindowLoop();
}
}

public async Task<Task> throttle(object cost2)
{
var cost = (cost2 != null) ? Convert.ToDouble(cost2) : Convert.ToDouble(this.config.Cost);
var t = new Task(() => { });
this.queue.Enqueue((t, cost));
lock (queueLock)
{
if (this.queue.Count > (int)this.config.MaxLimiterRequests)
{
throw new Exception("throttle queue is over maxLimiterRequests (" + this.config.MaxLimiterRequests.ToString() + "), see https://github.com/ccxt/ccxt/issues/11645#issuecomment-1195695526");
}
this.queue.Enqueue((t, cost));
}
if (!this.running)
{
this.running = true;
Expand Down
3 changes: 3 additions & 0 deletions go/v4/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type Exchange struct {
Timeout int64
MAX_VALUE float64
RateLimit float64
RateLimiterAlgorithm string // rollingWindow or leakyBucket
MaxLimiterRequests int
RollingWindowSize int
TokenBucket map[string]interface{}
Throttler Throttler
NewUpdates bool
Expand Down
3 changes: 3 additions & 0 deletions go/v4/exchange_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ func (this *Exchange) initializeProperties(extendedProperties map[string]interfa
}
this.EnableRateLimit = SafeValue(extendedProperties, "enableRateLimit", true).(bool)
this.RateLimit = SafeFloat(extendedProperties, "rateLimit", -1).(float64)
this.RateLimiterAlgorithm = SafeString(extendedProperties, "rateLimiterAlgorithm", "leakyBucket").(string)
this.MaxLimiterRequests = int(SafeInteger(extendedProperties, "maxLimiterRequests", 1000).(int64))
this.RollingWindowSize = int(SafeInteger(extendedProperties, "rollingWindowSize", 60000).(int64))
// this.status = SafeValue(extendedProperties, "status",map[string]interface{}{}).(map[string]interface{})
this.PrecisionMode = int(SafeInteger(extendedProperties, "precisionMode", this.PrecisionMode).(int64))
this.PaddingMode = int(SafeInteger(extendedProperties, "paddingMode", this.PaddingMode).(int64))
Expand Down
Loading
Loading
0