Use of current limiting function of Django REST framework

  • 2021-11-13 08:19:43
  • OfStack

Directory body begins
1. Current Limiting in DRF
2. Advanced Configuration of Current Limiting
3. Analysis of current limiting ideas
4. Source code analysis
5. Other considerations
References

Beginning of the text

Let's talk about the concept of current limiting first. The earliest contact with this concept is at the front end. The real business scenario is that when you enter words in the search box to search, you don't want to call the back-end interface every character you enter, but you really call the interface after a pause. This function is necessary to reduce the pressure of front-end request and rendering, and at the same time reduce the pressure of back-end interface access. The code for the front-end-like functionality is as follows:


//  Example of front-end function current limiting 
function throttle(fn, delay) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
            return;
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null;
        }, delay)
    }
}

However, the current limiting at the back end is similar to that at the front end in purpose, but the implementation will be different. Let's look at the current limiting at DRF.

1. Current Limiting in DRF

Project configuration


# demo/settings.py

REST_FRAMEWORK = {
    # ...
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
         'rest_framework.throttling.ScopedRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '10/day',
        'user': '2/day'
    },
}

# article/views.py

#  Based on ViewSet Current limiting of 
class ArticleViewSet(viewsets.ModelViewSet, ExceptionMixin):
    """
     That allows users to view or edit API Path. 
    """
    queryset = Article.objects.all()
    #  Use default user current limiting 
    throttle_classes = (UserRateThrottle,)
    serializer_class = ArticleSerializer

#  Based on view Current limiting of 
@throttle_classes([UserRateThrottle])

Because the user I configured can only request twice a day, an exception of 429 Too Many Requests will be given after the third request, and the specific exception information is that the next available time is 86398 seconds.

2. Advanced Configuration of Current Limiting

The current limiting configuration demonstrated above is applicable to the current limiting of users. For example, if I continue to visit another user, I still have two opportunities.


$ curl -H 'Accept: application/json; indent=4' -u root:root   http://127.0.0.1:8000/api/article/1/ 
{
    "id": 1,
    "creator": "admin",
    "tag": " Modern poetry ",
    "title": " If ",
    "content": " This life   Never think of you again \n Except for \n Except in some \n A night wet with tears   If \n If you will "
}

Three kinds of current limiting classes under 1 are introduced respectively

AnonRateThrottle applies to any user restrictions on interface access UserRateThrottle applies to restrictions on interface access after authentication is requested ScopedRateThrottle applies to restrictions on access to multiple interfaces

Therefore, three different classes are suitable for different business scenarios, and the specific use is selected according to different business scenarios. The expected effect can be achieved by configuring the frequency corresponding to scope.

3. Analysis of current limiting ideas

Imagine 1. If you code to realize this requirement, how should you realize it?

In fact, this function is not difficult, the core parameters are time, number of times, the scope of use, the following demonstration of the limit on the number of function calls.


from functools import wraps

TOTAL_RATE = 2

FUNC_SCOPE = ['test', 'test1']


def rate_count(func):
    func_num = {
        #  Note that function names cannot be duplicated 
        func.__name__: 0
    }

    @wraps(func)
    def wrapper():
        if func.__name__ in FUNC_SCOPE:
            if func_num[func.__name__] >= TOTAL_RATE:
                raise Exception(f"{func.__name__} Function calls exceed the set number of times ")
            result = func()
            func_num[func.__name__] += 1
            print(f"  Function  {func.__name__}  The number of calls is:  {func_num[func.__name__]}")
            return result
        else:
            #  Functions that are not limited by count are not limited 
            return func()

    return wrapper


@rate_count
def test1():
    pass


@rate_count
def test2():
    print("test2")
    pass


if __name__ == "__main__":
    try:
        test2()
        test2()
        test1()
        test1()
        test1()
    except Exception as e:
        print(e)
    test2()
    test2()
    
"""
test2
test2
  Function  test1  The number of calls is:  1
  Function  test1  The number of calls is:  2
test1 Function calls exceed the set number of times 
test2
test2
"""

Here, the monitoring of the number of function calls is realized, and the functions that can use this function are set. Throw an exception when the number of function calls exceeds the set threshold for a long time. It's just that there is no time limit here.

4. Source code analysis

Just now, I analyzed how to implement the limit on the number of function calls, which may be one point complicated for one request. Let's take a look at how DRF implements it:


class SimpleRateThrottle(BaseThrottle):
   
    # ......
    
    def allow_request(self, request, view):
        """
        Implement the check to see if the request should be throttled.

        On success calls `throttle_success`.
        On failure calls `throttle_failure`.
        """
        if self.rate is None:
            return True

        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        #  Change the cache of request times according to the limit of setting time 
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        #  The core logic is to judge the number of requests here 
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()
        return self.throttle_success()
    
    # ......
    
class UserRateThrottle(SimpleRateThrottle):
    """
    Limits the rate of API calls that may be made by a given user.

    The user id will be used as a unique cache key if the user is
    authenticated.  For anonymous requests, the IP address of the request will
    be used.
    """
    scope = 'user'

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            #  Considering that the user is not authenticated,   And  AnonRateThrottle  Medium  key 1 To 
            ident = self.get_ident(request)
        #  Constructs the cached according to the set scope  key
        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

To sum up:

The core judgment logic is still to obtain the number of calls per user in the cache, and judge whether it exceeds the set threshold according to the range and time. The design of cache key is different for different types of current limiting, and the default key is REMOTE_ADDR in request.

5. Other considerations

Because the implementation here uses caching, it should be noted that the caching service of System 1 needs to be configured in the case of multi-instance deployment (the default caching is Django memory-based implementation). Restarting the cache service may cause the existing count to be cleared. If there is a strong business logic need, please implement the current limiting logic yourself. If it is a custom user table, you need to override the logic of get_cache_key in the cache. If it is necessary to statistically analyze the current limiting situation of users, it is also necessary to redesign the current limiting logic. The logic of current restriction should be used with caution in production environment, because it will restrict users from using products and is not friendly to users.

References

DRF current limiting
Django cache

The above is the use of Django REST framework current limiting function in detail, more information about Django REST framework current limiting function, please pay attention to other related articles on this site!


Related articles: