0:00
[БЕЗ_ЗВУКА] В
этом видео мы продолжим говорить про middleware и рассмотрим,
каким образом через middleware можно организовать тайминги каких-либо операций,
происходящих внутри запроса.
Для этого мы обратимся к уже известному вам пакету «контекст»,
ранее мы его рассматривали только в аспекте отмены запросов,
однако это не единственная его возможность.
Второй его возможностью является возможность
нести в себе некие значения, то есть context value.
Причем эти значения будут копироваться при,
точнее, контекст будет копироваться при перезаписи.
Ну, то есть это такой tread safe local storage,
но для конкретной области видимости, то есть для вашего запроса.
Рассмотрим в коде, что это такое, что это значит.
Рассмотрим функцию main, тут ничего сложного нет,
мы делаем multiplex запросов, регистрируем через обработчик,
регистрируем там один middleware и начинаем запрос.
Вроде ничего сложного.
Но прежде чем рассматривать тайминг middleware, посмотрим, что в load posts.
Итак, сначала мы получаем контекст,
мы получили контекст из нашего запроса, request,
теперь я написал небольшую функцию, которая будет нам эмулировать работу.
В нее мы передаем контекст первым параметром, контекст всегда
должен передаваться первым параметром, это такое соглашение, и какое-то имя работы.
Я вызываю несколько функций, чтобы сделать в виде нескольких работ,
тут я делаю небольшой тайм без emulate, я расскажу про это дальше,
зачем, и еще что-то, и вывожу, что request done, все.
То есть единственное отличие от разработчиков, которых вы видели до этого,
является то, что я тут получаю в начале контекст и дальше с этим контекстом
работаю, передавая его просто в функцию, даже не делая какие-то операции.
Теперь что внутри emulateWork.
Ну тоже все очень просто.
Внутри есть функция trackContextTimings, куда я передаю, собственно,
мой контекст, имя работы, которую я туда передаю, и время начала запроса.
Я напоминаю, что в defer аргумент вычисляется в момент удаления defer,
то есть это время выполнится вот здесь,
посчитается, а выполнится запрос после sleep.
Окей.
Вроде бы мы что-то сделали, что-то посчитали,
далее я вам продемонстрирую результат, а потом мы углубимся в детали middleware.
Итак, результат, вот мой запрос, posts 42.
Посмотрите, я вывожу на экран всю работу,
которая там была, то есть вот мой checkCashe, loadPosts,
причем их было вызвано аж три раза, Sidebar, Comments, и всего времени,
когда выполнялся запрос общий, всего времени, которое я посчитал,
используя эту функцию, trackContextTimings,
и еще какое-то unknown время, которое занимает почти десять миллисекунд.
Это как раз у нас вот это время.
То есть то время, которое не покрыто вот этим вот общим таймингом.
То есть штука полезная, мы можем смотреть,
либо какая из функций начала работать дольше либо в какой функции затык,
либо собирать по этому общую статистику, строить графики и
смотреть в реальном времени, что происходит с нашей программой.
Вдруг там какой-то внешний сервис начал тупить либо после нашего обновления
мы стали делать больше запросов куда-то, либо они стали дольше.
Окей.
С пользой понятно.
Это важная вещь.
Теперь посмотрим, как оно реализовано внутри.
Начнем мы с timingMiddleware.
Так.
Сначала я получаю контекст.
Что я делаю?
У меня в запросе есть контекст.
Давайте я лучше напишу вот так.
В моем запросе есть контекст,
в том, что приходит, и request.
Его вы можете получить через функцию context.
Теперь далее я добавляю еще какое-либо значение в него,
то есть я говорю context withValue, указываю ключ,
по которому я хочу к этому значению в дальнейшем обращаться,
и указываю свою структуру,
ее детали сейчас не очень важны, указываю в общем какую-то структуру.
Вот.
Что происходит?
У нас создался новый индекс контекста,
в котором появилось новое значение, которое лежит по timing scale.
При этом старый контекст не изменился, это copy on right, Окей,
теперь в defer я делаю logContextTimings, куда опять передаю этот самый контекст,
url и текущее время, точнее, время начала запроса.
Ну дальше я продолжаю обслуживать наш запрос.
То есть это middleware и я оберну свою функцию в контекст.
Таким образом, когда ко мне контекст приходит, уже моя функция начинает
выполняться, там уже есть контекст с value timings.
Вроде бы понятно, мы что-то куда-то присвоили,
что-то есть, как это читается?
Посмотрим trackContextTimings.
Хорошо.
Я получаю уже свое значение, которое я туда получил.
Поскольку это хранилище такое универсальное,
то хранится там все в виде пустого интерфейса.
Поэтому я, используя мой ключ, я получаю
value и сразу же пытаюсь его преобразовать к своему типу данных ContextTimings.
И проверяю.
Если вдруг там нет этого ContextTiming или я
его не смог преобразовать либо там лежит что-то другое с другим типом данных,
кто-то задумал положить, ну я короче просто выхожу из функции.
В противном случае я смотрю, сколько прошло
времени от того, что меня передали, лочусь, внутри у меня там мапка,
мапка не гарутинобезопасный тип данных, поэтому мне нужно залочиться,
чтобы что-то изменить, в defer сразу unlock делаю, ну и там смотрю дальше.
Если у меня уже такая метрика есть, я там плюсую время и количество,
если нет, я создаю ее в map.
Вот.
То есть что, я просто плюсую время.
Это в trackContextTimings.
То есть что я тут делаю, еще раз.
У меня есть абсолютно любая функция, в которой есть контекст, значит,
внутри этой функции я могу вызвать track ContextTimings, передать туда
этот контекст, имя метрики, которую я хочу считать, и время начала, и она засчитает.
Если у меня там этого значения нет,
если нет ContextTiming, где-то не инициализирован, или еще что-то,
ничего страшного не случится, потому что я сразу же выйду.
На самом деле это один из таких недостатков контекста.
Почему?
Потому что все хранится внутри как пустой интерфейс,
то есть нету у компа time-проверок.
Вы всегда должны будете проверять кассу пустого интерфейса к нужному вам
типу данных, и нет какой-то общей документации,
то есть если у нас есть какая-то структура жесткая, там понятно, что мы туда кладем,
что там делается, поэтому там можно даже сгенерировать документацию,
а с ContextValue нет ничего.
Да, он резиновый, это хорошо, но вот он создает такое параллельное api.
Это большой минус.
То есть с контекстом нужно быть очень осторожным
и передавать туда только те переменные, хранить там только те значения,
которые нужны вам в течение всего срока жизни вашего запроса.
То есть instance-логгера туда передавать не надо,
коннект к базе данных туда тоже передавать не надо.
Вот тайминги — понятно.
Например, сессию или requestID или userID там тоже можно хранить.
Однако ни в коем случае нельзя через контекст передавать сами
параметры функции.
То есть нельзя делать вот так: ctx
: = ctx WithValue,
например, post_id, допустим, 42.
Вот так делать нельзя.
И потом вызывать типа getPosts.
Вот так делать ни в коем случае нельзя.
Это очень плохой тон.
Это создает вам абсолютное недокументированное параллельное API,
которое не проверяется в compile time и чревато огромным количеством ошибок.
Поэтому забудьте, что вы можете передавать так параметры функции.
Туда передаются только какие-то общие значения.
Если вдруг вы думаете, что нужно залогировать
что-то, вы можете сделать там ctxlog,
чтобы request id доставать ctx,
some thing happened.
Вот, то есть не передавайте никогда
через контекст параметры функции, не делайте параллельным API.
Еще раз: контекст нужен для хранения общих переменных на протяжении вашего запроса.
В моем случае это некое хранилище таймингов, которые я в конце вывожу.
Ладно.
Мы посмотрели, каким образом мы добавляем в тайминг.
То есть мы получили нашу структуру заплюсовали туда и все.
Теперь вернемся к функции Middleware.
Изначально мы тут создали запрос,
создали контекст с таймингами, а дальше мы в defer его попытаемся слогировать.
Рассмотрим это логирование.
Опять-таки мы проверяем эти тайминги.
Опять проверяем, что они там есть и они действительно скастовались к тому типу
данных, которые нам нужны.
Теперь смотрим время начала запроса,
и тут я уже начинаю итерироваться по всем таймингам.
Тут я плюсую к duration и пишу строку,
собственно, что это был за тайминг,
сколько его количество было и сколько времени он занимал.
Теперь пишу, сколько всего времени это заняло,
сколько было учтено времени и сколько не учтено.
И потом вывожу это на экран.
Я не делаю это через плюсование, конкатенацию строк,
типа так: log + =,
потому что это не очень эффективно, я и так Sprintfm пользуюсь,
а я сразу пишу просто в буфер.
Итак, еще раз: вот код, который мне выводится после запроса.
То, что мне выводит после запроса.
Вот, собственно, результат.
То есть я проитерировался main timing, вывел все, что у меня там было.
Теперь я вывел total, то есть сколько запрос работал всего.
Я вывел учтенное время, то есть то время,
которое было покрыто этими таймингами, контекстными, и вывел unknown.
unknown — это неучтенное время, и я не знаю, на что оно было потрачено.
Помните, вот здесь вот я делал time.Sleep?
Вот это как раз оно, вот это как раз неучтенное время.
Вот так работает Context Value.
Еще раз я вам напомню: Context Value предназначен для того,
чтобы хранить в request scope что-то.
То есть хранить какое-то значение на протяжении жизни вашего запроса.
В нем не надо хранить то,
что не уничтожается после того, как запрос отработает.
То есть хранить подключение к базе данных там не надо.
Также через Context Value нельзя ни в коем случае передавать
параметры в функцию, то есть почему?
Потому что Context Value работает через пустой интерфейс,
значит, он создает недокументированное runtime api,
которое нельзя проверить в compile time.
То есть если у вас есть четкая структура, вы знаете,
что сюда можно присвоить то, туда се, и есть четкие проверки.
А с Context Value нельзя, вам нужно все это проверять руками.
В этом плане это неудобно, однако контекст все-таки вещь нужная,
и если использовать его с умом, он будет вам в помощь.
Если нет, у вас будет все плохо.
Context Value используется много где, несмотря на то, что некоторые говорят,
что использовать его вообще не надо, он есть, его можно применять с пользой.
Если вдруг вам совсем не хватает базовых функциональностей контекста,
вы все-таки хотите хранить там что-то общее, лучше сделайте свой контекст.
И храните там не через Value,
который runtime,
который динамический через пустой интерфейс, а как поле в структуре.
Также контекст никогда не надо присваивать уже в какую-то готовую структуру, разве
что это ваш отдельный контекст, в который вы его присваиваете через композицию.
То есть контекст всегда просто контекст, он не часть структуры,
он всегда передается как первый аргумент в функцию,
контекст не должен быть параллельным API.
Я надеюсь, что вы будете использовать его с пользой.