Как обновленные лимиты на payload serverless func (Vercel) скрасили мой вечер
Был у меня небольшой проект, который больше полугода лежит и ждет свою аудиторию, проект про генерацию метаданных для подкаста из, собственно, небольшого медиа файла (до 25мб). Все сделал, сдал, работает. Вчера пришла бага, что 13мб файл не грузится, стал изучать. Сам проект по сути ai-proxy (3х уровневый) над клиентом, деплой на Vercel (проще развернуть там для первичной оценки востребованности). Поэтому это никакой не selfhosted, сервера нет, есть только serverless, через которые я хожу в ai через ai-sdk (gpt-4o-mini, whisper-1, dall-e-3)
Поймал
serverless func payload too large (>4.5mb), раньше либо все работало, либо не тестировали на средних аудио (10мин), уже не вспомню, начал разбираться, как можно это обойти, использовать внешний стор не хотелось по причине опять же, не self-hosted решения, бекенд я уже умею на nest.js писать, но с деплоями не разбирался (да и не хочется пока), решил попробовать сжатие. Благо Cursor я уже расчехлил, он настроил мне агрессивное сжатие для аудио через OfflineAudioContext (моно-канал, low sample rate 8000Hz, 8bit вместо 16), если и с таким сжатием не канает, взял pako, выбрал уровень сжатия 9 🤯.
serverless func payload победил, но теперь из-за того что я отправляю файл в compressed формате, надо бы и заголовки кастомные добавить (как минимум для сохранения типа файла, как максимум для названия), потом еще дописать decode логику на сервере, логика вокруг ai ждет от меня File, окей, все еще несложно, но теперь я уперся в
serverless func max time invocation (max 60s на hobby). Decoding стал занимать некоторое время, из-за чего на всю ai-цепочку оставалось меньше времени. Все предельно ясно, надо разбить одну serverless func на несколько.
Этим я и занялся, сначала вынес декодинг в отдельный енд поинт: api/decode, тут я получал сжатый payload, получал X-Custom-Headers, собирал из этого исходный файл. Но как теперь пошарить результат api/decode с api/generate-metadata? Вернуть тяжелый файл и кинуть его снова с клиента - бессмысленно, упремся снова в large payload. Сделаем simple cache!
Написал мапу, в нее добавлял файлы и expiredAt (а-ля свой редис), написал cleanup-ы, которые дергал во время инвокейшенов serverless func, но если их нет, как чистить кеш? Таймауты не сработают, если серверлесс функция уже завершилась. Можно удалять файл после процессинга, подумал я.
Стал проверять, обнаружил интересный баг, каждую сессию первая загрузка не может найти файл в мапе(cache), последующие отрабатывают. Cold start? Нет, дело в том, что serverless func инвокаются в изолированных контекстах, поэтому пошерить стейт между ними нельзя (мой simple redis cache представлял из себя export const map, который не всегда шерился между разными серверлесс функциями).
Штош, посмотрим на @vercel/kv, которые, оказывается уже переезжают на маркетплейс. Нашел там redis + upstash redis. На первом лимит стора на фри тарифе 30мб, что дает мне предел в 6 пользователей, как-то вяло, да? 🤡 На втором лимит 10к запросов к инстансу в день, выглядит что надо, развернул (2секи), подключаю, тестирую, payload too large (free tier 1mb).
Окей, раз нельзя пошерить мапу(кеш), начнем сохранять файлы в /tmp. Для этого я начал после api/decode выдавать подписанный секретом токен, в токене была информация о пути к файлу, токен можно было верифицировать при получении на api/generate-metadata и избегать ушлых хакеров, которые решат получить транскрибции моих внутренних файлов сервера(или серверлесса? 🤡)
Но и это оказалось не финишной прямой, тяжелый файл - более долгая обработка, пришлось и ai-слои раздробить на api/transcribe, api/generate-metadata, api/generate-prompt, api/generate-image. Но теперь UX сильно лучше, разбив на более явные процессы (среди которых, напомню, если сжатие на клиенте), я смог отображать прогресс более четко
пять с половиной часов пролетели за секунду