[Python] Throttling Async Functions in Python Asyncio

Discussão em 'Python' iniciado por Stack, Novembro 30, 2024.

  1. Stack

    Stack Membro Participativo

    I have a list of awaitables that I want to pass to the asyncio.AbstractEventLoop but I need to throttle the requests to a third party API.

    I would like to avoid something that waits to pass the future to the loop because in the meantime I block my loop waiting. What options do I have? Semaphores and ThreadPools will limit how many are running concurrently, but that's not my problem. I need to throttle my requests to 100/sec, but it doesn't matter how long it takes to complete the request.

    This is a very concise (non)working example using the standard library, that demonstrates the problem. This is supposed to throttle at 100/sec but throttles at 116.651/sec. What's the best way to throttle the scheduling of an asynchronous request in asyncio?

    Working code:

    import asyncio
    from threading import Lock

    class PTBNL:

    def __init__(self):
    self._req_id_seq = 0
    self._futures = {}
    self._results = {}
    self.token_bucket = TokenBucket()
    self.token_bucket.set_rate(100)

    def run(self, *awaitables):

    loop = asyncio.get_event_loop()

    if not awaitables:
    loop.run_forever()
    elif len(awaitables) == 1:
    return loop.run_until_complete(*awaitables)
    else:
    future = asyncio.gather(*awaitables)
    return loop.run_until_complete(future)

    def sleep(self, secs) -> True:

    self.run(asyncio.sleep(secs))
    return True

    def get_req_id(self) -> int:

    new_id = self._req_id_seq
    self._req_id_seq += 1
    return new_id

    def start_req(self, key):

    loop = asyncio.get_event_loop()
    future = loop.create_future()
    self._futures[key] = future
    return future

    def end_req(self, key, result=None):

    future = self._futures.pop(key, None)
    if future:
    if result is None:
    result = self._results.pop(key, [])
    if not future.done():
    future.set_result(result)

    def req_data(self, req_id, obj):
    # Do Some Work Here
    self.req_data_end(req_id)
    pass

    def req_data_end(self, req_id):
    print(req_id, " has ended")
    self.end_req(req_id)

    async def req_data_async(self, obj):

    req_id = self.get_req_id()
    future = self.start_req(req_id)

    self.req_data(req_id, obj)

    await future
    return future.result()

    async def req_data_batch_async(self, contracts):

    futures = []
    FLAG = False

    for contract in contracts:
    req_id = self.get_req_id()
    future = self.start_req(req_id)
    futures.append(future)

    nap = self.token_bucket.consume(1)

    if FLAG is False:
    FLAG = True
    start = asyncio.get_event_loop().time()

    asyncio.get_event_loop().call_later(nap, self.req_data, req_id, contract)

    await asyncio.gather(*futures)
    elapsed = asyncio.get_event_loop().time() - start

    return futures, len(contracts)/elapsed

    class TokenBucket:

    def __init__(self):
    self.tokens = 0
    self.rate = 0
    self.last = asyncio.get_event_loop().time()
    self.lock = Lock()

    def set_rate(self, rate):
    with self.lock:
    self.rate = rate
    self.tokens = self.rate

    def consume(self, tokens):
    with self.lock:
    if not self.rate:
    return 0

    now = asyncio.get_event_loop().time()
    lapse = now - self.last
    self.last = now
    self.tokens += lapse * self.rate

    if self.tokens > self.rate:
    self.tokens = self.rate

    self.tokens -= tokens

    if self.tokens >= 0:
    return 0
    else:
    return -self.tokens / self.rate


    if __name__ == '__main__':

    asyncio.get_event_loop().set_debug(True)
    app = PTBNL()

    objs = [obj for obj in range(500)]

    l,t = app.run(app.req_data_batch_async(objs))

    print(l)
    print(t)


    Edit: I've added a simple example of TrottleTestApp here using semaphores, but still can't throttle the execution:

    import asyncio
    import time


    class ThrottleTestApp:

    def __init__(self):
    self._req_id_seq = 0
    self._futures = {}
    self._results = {}
    self.sem = asyncio.Semaphore()

    async def allow_requests(self, sem):
    """Permit 100 requests per second; call
    loop.create_task(allow_requests())
    at the beginning of the program to start this routine. That call returns
    a task handle that can be canceled to end this routine.

    asyncio.Semaphore doesn't give us a great way to get at the value other
    than accessing sem._value. We do that here, but creating a wrapper that
    adds a current_value method would make this cleaner"""

    while True:
    while sem._value < 100: sem.release()
    await asyncio.sleep(1) # Or spread more evenly
    # with a shorter sleep and
    # increasing the value less

    async def do_request(self, req_id, obj):
    await self.sem.acquire()

    # this is the work for the request
    self.req_data(req_id, obj)

    def run(self, *awaitables):

    loop = asyncio.get_event_loop()

    if not awaitables:
    loop.run_forever()
    elif len(awaitables) == 1:
    return loop.run_until_complete(*awaitables)
    else:
    future = asyncio.gather(*awaitables)
    return loop.run_until_complete(future)

    def sleep(self, secs: [float]=0.02) -> True:

    self.run(asyncio.sleep(secs))
    return True

    def get_req_id(self) -> int:

    new_id = self._req_id_seq
    self._req_id_seq += 1
    return new_id

    def start_req(self, key):

    loop = asyncio.get_event_loop()
    future = loop.create_future()
    self._futures[key] = future
    return future

    def end_req(self, key, result=None):

    future = self._futures.pop(key, None)
    if future:
    if result is None:
    result = self._results.pop(key, [])
    if not future.done():
    future.set_result(result)

    def req_data(self, req_id, obj):
    # This is the method that "does" something
    self.req_data_end(req_id)
    pass

    def req_data_end(self, req_id):

    print(req_id, " has ended")
    self.end_req(req_id)

    async def req_data_batch_async(self, objs):

    futures = []
    FLAG = False

    for obj in objs:
    req_id = self.get_req_id()
    future = self.start_req(req_id)
    futures.append(future)

    if FLAG is False:
    FLAG = True
    start = time.time()

    self.do_request(req_id, obj)

    await asyncio.gather(*futures)
    elapsed = time.time() - start
    print("Roughly %s per second" % (len(objs)/elapsed))

    return futures


    if __name__ == '__main__':

    asyncio.get_event_loop().set_debug(True)
    app = ThrottleTestApp()

    objs = [obj for obj in range(10000)]

    app.run(app.req_data_batch_async(objs))

    Continue reading...

Compartilhe esta Página