使用单例来管理发出相似请求的多个类

时间:2019-02-01 18:53:59

标签: python concurrency singleton aiohttp

我有一个应用程序,它向Web API发出数以百万计的请求,目前我的工作方式有效,但效果不佳,而且笨重。我现在正在重写它,但是基本上,我有多个类需要向API发出请求,并且它们需要同时运行。 API约束只允许我创建~10 requests per second per API token,但是我可以创建很多tokens

顺便说一句,几个月前,我确实向this very similair but less involved提出了一个问题,此后这个项目一直处于停滞状态,而且自问这个问题以来,我学到了很多东西。所以看看你自己的判断力哈哈

目前,(旧方法)我已经为每个类分配了自己的令牌集以供使用。就我的想法而言,这对于我的情况而言效率低下,为什么不让所有类都调用一个管理所有令牌的Singleton,这样就不会经常使用它们并对每个人执行所有请求,然后在响应中使用回调

我一直在进行一些主要的挖掘和阅读工作,在经历了许多头痛之后,我在以下资源之间拼凑了一个可以工作的原型:

所以这是我想出的:

import json
import uvloop
import aiohttp
import AsyncLeakyBucket

from pprint import pprint
from aiohttp import ClientSession
from itertools import cycle

loop = uvloop.new_event_loop()

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class API(type, metaclass=Singleton):

    def __init__(self, base_url):
        super(API, self).__init__()
        self._tokens = ('list', 'of', 'tokens')
        num_tokens = len(self._tokens)
        self._tokens = cycle(self._tokens)
        self._base_url = base_url
        num_concurrent_per_token = 10
        total_concurrent = num_concurrent_per_token*num_tokens
        self._bucket = AsyncLeakyBucket(10*total_concurrent)
        self._queue = asyncio.PriorityQueue()

        asyncio.run(_start_workers(total_concurrent))

    async def _start_workers(self, num):
        session = asyncio.get_event_loop().run_until_complete(aiohttp.ClientSession())
        loop.run_until_complete(asyncio.gather(
            *(_get_requests(session, queue) for _ in range(num))
        ))

    async def _get_request(self, session, queue):
        api_headers = {
            'Accept': "application/json",
            'Authorization': "Auth {}"
        }
        while True:
            url_ext, callback = await queue.get()

            with self._bucket:
                try:
                    async with session.get(f"{self._base_url}{url_ext}",
                                           headers=api_headers.format(next(self.tokens))) as response:
                        status = response.status
                        data = await response.read()
                        try:
                            data = json.loads(r)
                        except json.decoder.JSONDecodeError:
                            print(f"Error loading {data} as JSON")
                            #return
                except aiohttp.ClientError as e:
                    print(f"Error retrieving request: {e}")
                    #return

            queue.task_done()
            asyncio.create_task(callback(data))

    async def get(self, url_extension, callback, pri=1):
        await self._queue.put(priority, (url_ext, callback))

async def request_done(data):
    pprint(data)

a = API('https://jsonplaceholder.typicode.com/')

for num in range(500):
    a.get(f"/comments/{num}", request_done)

我遇到错误,无法测试此代码的其余部分:

Traceback (most recent call last):
  File "api.py", line 73, in <module>
    a = API('https://jsonplaceholder.typicode.com/')
  File "api.py", line 15, in __call__
    cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
TypeError: type.__new__() takes exactly 3 arguments (1 given)

从字面上看,我对Singleton和线程的了解有限,因此整个班级都写了这堂课,现在我被困住了,不想把它弄得太深。我所知道的并不是很多,因此希望代码和我的解释足以使我理解我的意图。

顺便说一句,我只是使用https://jsonplaceholder.typicode.com/来测试基本功能,因为您可能会说其他API需要标头和完全不同的地址...

一如既往,在此先感谢您,并希望能收到社区的反馈。如果有任何疑问,我在这里!

0 个答案:

没有答案