15

It has to be trivial, but I just cannot get through it. I have to limit amount of tasks (let's say connections, emails sent or clicks in the button) per amount of time. So e.g. I can send 1000 emails per hour.

How can I do that in c#? I don't know and don't care how much time each operation will take. I just want to make sure that for last hour, only 1000 will be executed.

IamDeveloper
  • 5,156
  • 6
  • 34
  • 50
  • In what environment? A web app, a windows service, a WCF service? – Jamiec Oct 11 '11 at 15:32
  • 1
    Is it a problem if you burn through all 1000 in the first minute? – Anthony Pegram Oct 11 '11 at 15:32
  • Last hour, meaning on the hour, or a rolling hour? – George Duckett Oct 11 '11 at 15:34
  • asp.net. I suppose last hour, rolling hour could break the requirement. So if the first action started at 01:00 , till 02:00 there can be only 1000 operations. And till 03:00, 3000 only. – IamDeveloper Oct 11 '11 at 15:38
  • If you literally implement it the way you describe it then you'll need to record the method call times for the past 1000 calls or one hour, which ever is less. That's expensive and horribly unintuitive to the user who can never guess when she's close to the limit. The cheap and intuitive way is to simply reset the counter to 0 at the start of an hour on the clock. – Hans Passant Oct 11 '11 at 15:46
  • What do you want to do if somebody tries to execute it more than 1000 times? Should it buffer them and run them when it gets quota back or should it just in some way fail? – Chris Oct 11 '11 at 15:49
  • @Hans: I wouldn't have thought a collection of up to 1000 datetimes would be that horrible or expensive. And we don't know who the consumer of this method is so whether feedback is needed or not. – Chris Oct 11 '11 at 15:51
  • @Chris: wait till it's again possible to allow them. Don't buffer, let's say sleep on current thread. – IamDeveloper Oct 11 '11 at 15:54
  • Well, a thousand datetimes is actually horribly more than one short. Besides, is it still a thousand a year from now? – Hans Passant Oct 11 '11 at 15:57
  • see this answer: http://stackoverflow.com/questions/4085055/how-would-you-limit-the-number-of-operations-per-second/4085164#4085164 –  May 06 '13 at 11:36

6 Answers6

23
 class EventLimiter
 {
    Queue<DateTime> requestTimes;
    int maxRequests;
    TimeSpan timeSpan;

    public EventLimiter(int maxRequests, TimeSpan timeSpan)
    {
        this.maxRequests = maxRequests;
        this.timeSpan = timeSpan;
        requestTimes = new Queue<DateTime>(maxRequests);
    }

    private void SynchronizeQueue()
    {
        while ((requestTimes.Count > 0) && (requestTimes.Peek().Add(timeSpan) < DateTime.UtcNow))
            requestTimes.Dequeue();
    }

    public bool CanRequestNow()
    {
        SynchronizeQueue();
        return requestTimes.Count < maxRequests;
    }

    public void EnqueueRequest()
    {
        while (!CanRequestNow())               
            Thread.Sleep(requestTimes.Peek().Add(timeSpan).Subtract(DateTime.UtcNow));
            // Was: System.Threading.Thread.Sleep(1000);

        requestTimes.Enqueue(DateTime.UtcNow);
    }
 }
Eric J.
  • 147,927
  • 63
  • 340
  • 553
BlueMonkMN
  • 25,079
  • 9
  • 80
  • 146
  • 1
    Heh -- looks like I just coded George's suggestion for a rolling window. – BlueMonkMN Oct 11 '11 at 15:54
  • 2
    presumably you could do that thread.sleep in a slightly nicer way by peeking at the top item in teh queue to find out when it would expire since that will be the next time the queue will get smaller... – Chris Oct 11 '11 at 15:58
  • Now this is really nice & simple ! – IamDeveloper Oct 11 '11 at 15:59
  • This will not work as expected transitioning in and out of daylight savings time. Use DateTime.UtcNow to avoid that problem. – Eric J. Feb 19 '16 at 00:29
  • @BlueMonkMN: Missed that one (it scrolled off page for me). Yes, it should. Very nice answer by the way. – Eric J. Feb 19 '16 at 18:30
  • 1
    i think you should check the Thread.Sleep timespan value is not negative. otherwise it will block the request. – Chocoboboy May 13 '18 at 14:31
4

Assuming a rolling hour window:

Maintain a list of when actions were done.

Each time you want to do your action, remove all in the list not within the hour.

If there are fewer than 1000 then do the action and add a record to your list.


Assuming hourly:

Create a proxy method and a variable that is incremented for every action, and reduced to zero on the hour.

Do your action if the counter is < 1000.

George Duckett
  • 31,770
  • 9
  • 95
  • 162
3

The above solution looked fine. Here is my trimmed down version:

public class EmailRateHelper
{
    private int _requestsPerInterval;
    private Queue<DateTime> _history;
    private TimeSpan _interval;

    public EmailRateHelper()
        : this(30, new TimeSpan(0, 1, 0)) { }

    public EmailRateHelper(int requestsPerInterval, TimeSpan interval)
    {
        _requestsPerInterval = requestsPerInterval;
        _history = new Queue<DateTime>();
        _interval = interval;
    }

    public void SleepAsNeeded()
    {
        DateTime now = DateTime.Now;

        _history.Enqueue(now);

        if (_history.Count >= _requestsPerInterval)
        {
            var last = _history.Dequeue();                
            TimeSpan difference = now - last;

            if (difference < _interval)
            {
                System.Threading.Thread.Sleep(_interval - difference);
            }
        }
    }
}
PoLáKoSz
  • 355
  • 1
  • 6
  • 7
Leoric2
  • 31
  • 1
2

You can use Rx extensions (How to use the new BufferWithTimeOrCount in Rx that returns IObservable<IObservable<T>> instead of IObservable<IList<T>>), but I would implement the buffering manually by adding an appropriate proxy object.

Community
  • 1
  • 1
Vlad
  • 35,022
  • 6
  • 77
  • 199
  • I would agree that you need a proxy but a buffer sounds like a bad idea since it would delay the execution of things to at least some extent since that is what buffers do. For example the buffer with time or count would prevent execution of the method until either the time is up or a given number of requests has been received. Not good if you want things to run as quick as possible subject to the given constraints. – Chris Oct 11 '11 at 15:54
  • @vittore: What Rx code would be good for this? I've not used much Rx so have no idea what rate limiting functionality they provide... – Chris Oct 11 '11 at 15:55
  • @Chris: anyway you are going to delay the execution, at least when there are too many calls in a short period of time. So the client code has to expect the calls to come later. – Vlad Oct 11 '11 at 15:57
  • 1
    @Chris: [here](http://stackoverflow.com/questions/4505529/rx-iobservable-buffering-to-smooth-out-bursts-of-events) is one more example. – Vlad Oct 11 '11 at 15:58
  • True. I guess you do need a buffer but I'd have thought avoiding stuff waiting in it when possible would be preferable. If you use something like bufferwithtimeorcount and set the time to an hour and a count of 1000 then you are potentially making an action wait an hour when it could easily be executed immediately. Also depending on processing time you might not want 1000 calls to kick off all at once. – Chris Oct 11 '11 at 16:01
  • @Chris: the second example seems to behave better from this POV. – Vlad Oct 11 '11 at 16:02
1

You may also consider storing {action, time, user} information in a database and get number of actions in a last hour fomr the DB (or similar persisted storager) if you need to handle Application pool restarts / crashes. Otherwise clever user may circumvent your in-memory protection with overloading your server.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
1

You can create a persistent counter for every user. Every time you receive a request (for sending an email) you need to check the value of the counter and the date of the counter creation.

  • If the count is greater than the limit you refuse the request
  • If the date is older than an hour you reset the counter and set the new creation date
  • If the date is correct and the count is under the limit you increase the counter

Only in the last two cases the request is executed.

Massimo Zerbini
  • 3,125
  • 22
  • 22