Есть ли способ получить доступ к контексту выполнения, не основанному на потоках, в Python? - PullRequest
0 голосов
/ 07 мая 2020

Я поддерживаю модуль Python, который собирает различные метрики выполнения функций и отправляет их во внутренний поток журнала. Реализация выглядит примерно так:

from metrics_module import collect_metrics

@collect_metrics
def foo(*args, **kwargs):
    response = requests.get("https://www.example.com")
    # some more time-consuming computations

Помимо записи времени выполнения декорированной функции и сбора информации, содержащейся во входящих аргументах, collect_metrics также исправляет библиотеку requests для сбора HTTP-ответа серверной части. раз. Это исправление происходит во время импорта:

def collect_metrics(fn):

    # patches requests to collect backend response times
    patch_all()

    @wraps(fn)
    def wrap(*args, **kwargs):
        # collect some things
        result = fn(*args, **kwargs)
        # collect more things
        return result

    return wrap

После исправления модуля requests все вызовы requests.request будут добавлять данные запроса и ответа в список, содержащийся в объекте глобального словаря metrics. Затем этот объект jsonified и отправляется в поток журнала.

metrics = {"backends": [], "foo": None}

Из-за глобального характера объекта metrics я вижу проблемы, когда collect_metrics используется в многопоточной среде . например,

with concurrent.futures.ThreadPoolExecutor() as executor:
    executor.submit(foo, bar=bar)
    executor.submit(foo, bar=bar)

Сначала я подумал о назначении каждому потоку его собственного объекта метрик с помощью threading.local(), но быстро понял, что это не сработает, когда requests.request вызывается в другом дочернем потоке. например,

@collect_metrics
def foo(*args, **kwargs):
    with concurrent.futures.ThreadPoolExecutor() as executor:
        executor.submit(requests.get, url="https://www.example.com")
        executor.submit(requests.get, url="https://www.example.com")

Тогда казалось, что более простым решением было бы создать «идентификатор контекста запроса», используя uuid, который затем можно было бы передать функции patch_all, чтобы исправленная версия из requests.request может использовать его для выполнения поиска в объекте global_metrics, чтобы найти правильный словарь метрик для изменения.

global_metrics = {
    "1029804a-3b62-4015-a7fa-d1ee1fb28ab2": {"backends": [], "foo": None},
    "854fa162-bc24-4f0b-9be5-e679a65e263c": {"backends": [], "foo": None},
}

В этом случае исправление должно происходить во время каждого выполнения (что далеки от идеала), и поскольку исправление модуля requests происходит глобально, каждый вызов patch_all будет мешать другим потокам, использующим их собственные исправленные версии (каждая из которых содержит соответствующий «идентификатор контекста запроса»).

Вопрос: Есть ли способ реализовать исправление модуля requests, которое позволило бы исправлению определять текущий контекст запроса и, следовательно, правильный объект metrics для записи?

Я подумал о том, чтобы сохранить исправленную версию для каждого «идентификатора контекста запроса», но это кажется немного громоздким ... Одно что может быть примечательно, так это то, что мы впервые заметили эту проблему во время теста производительности приложения Flask. Проблема может быть воспроизведена путем простого вызова функции в нескольких потоках, поэтому тот факт, что используется Flask, может не иметь значения.

...