Transcript
00:00:00Привет, большое спасибо, что присоединились к нам сегодня.
00:00:02Я Пранит из команды разработки воркфлоу в Vercel.
00:00:05Привет, я Нейт, тоже из команды воркфлоу.
00:00:08Нейт, мы с тобой в этой команде с самого начала,
00:00:12и из всего, что мы выпустили за последние шесть месяцев,
00:00:15хуки и вебхуки — одна из моих любимых функций,
00:00:18и именно об этом ты пришел сегодня рассказать.
00:00:21Хуки и вебхуки тоже моя любимая функция.
00:00:23Они невероятно мощные, и я покажу несколько демо, чтобы объяснить почему.
00:00:28Первое демо — это то, с чем мы все, вероятно, знакомы: магические ссылки.
00:00:33Магическая ссылка — это форма входа. Вы вводите свой email, получаете письмо,
00:00:40и когда вы нажимаете на ссылку, вы авторизуетесь в сервисе.
00:00:44Да, и если я правильно помню, в Vercel — еще до того, как он стал Vercel,
00:00:48когда он еще назывался Zeit, магические ссылки были единственным способом входа,
00:00:52и в то время мы сами построили всю эту систему.
00:00:56Верно, и у меня до сих пор ПТСР.
00:01:01Потому что без воркфлоу внедрение такой системы гораздо сложнее, чем кажется на первый взгляд.
00:01:08Логика оказывается разбросана по нескольким файлам.
00:01:12Нужно подключать базу данных для отслеживания состояния, и все быстро превращается в хаос.
00:01:19Да, я уже думал о том, как бы я это структурировал и какую базу данных использовал,
00:01:24потому что это похоже на классическую проблему, которую я решал раньше.
00:01:28Так что да, мне очень интересно посмотреть, как это выглядит.
00:01:30Да, чтобы продемонстрировать болевые точки, о которых я говорю,
00:01:38я начал с реализации «традиционной» версии магической ссылки без воркфлоу.
00:01:43Итак, здесь задействованы три эндпоинта.
00:01:47Первый — когда отправляется форма входа,
00:01:50он должен создать сессию и сохранить её в базе данных, например в Redis.
00:01:57Нужно реализовать TTL, нельзя оставлять данные висеть вечно, их нужно удалять.
00:02:06Затем отправка письма: она может сорваться, вход не сработает, и это будет неприятно.
00:02:14Верно, а потом вам нужен крон-джоб или стажер, чтобы чистить базу данных.
00:02:19Возможно, тем стажером в то время был я.
00:02:22Но есть второй эндпоинт — тот, который срабатывает, когда пользователь кликает по ссылке в письме.
00:02:28Он должен запросить базу данных и восстановить состояние, созданное в первом эндпоинте.
00:02:36И мы уже получаем настоящий «спагетти-код».
00:02:38Когда я представлял, как это будет выглядеть, этот код кажется таким знакомым, я бы и сам так сделал.
00:02:48Мы видим, что всё быстро усложняется, хотя концепция очень проста.
00:02:54Давайте посмотрим, как реализовать эту функцию с помощью Workflow.
00:02:59Реализация магической ссылки с использованием Workflow SDK выглядит примерно так.
00:03:05Мы видим нашу функцию с директивой useWorkflow, что означает, что это функция воркфлоу.
00:03:11И самое первое, что мы делаем — вызываем функцию createWebhook из пакета воркфлоу.
00:03:18Также мы используем опцию respondWithManual, что означает: наша функция воркфлоу будет отвечать за отправку HTTP-ответа на запрос к вебхуку.
00:03:36Это для того, чтобы можно было сделать редирект после входа?
00:03:40Да, если в функции воркфлоу есть информация, необходимая для определения типа ответа.
00:03:51Как и в первом эндпоинте, мы отправляем письмо для входа. Это функция useStep.
00:03:57Так что если что-то пойдет не так, Workflow SDK автоматически сделает повторные попытки.
00:04:03Аспект отказоустойчивости уже дает преимущество перед традиционным подходом.
00:04:10То есть sendLoginEmails — это шаг, и если отправка письма не удалась, вы повторяете её с тем же URL вебхука.
00:04:21И если мы посмотрим сюда, это очень интересный паттерн.
00:04:26Мы используем promise.race с ожиданием (sleep) в пять минут.
00:04:30Это возможно, потому что объект вебхука реализует промис.
00:04:35Чтобы дождаться запроса к этому вебхуку, вы просто пишете await Webhook.
00:04:40Или, как здесь, через race. Это круто, я ожидал, что у вебхука будет какой-то аргумент с таймаутом.
00:04:50Но мне нравится, что всё стало чище: чтобы сделать таймаут, вы просто моделируете гонку между вебхуком и ожиданием.
00:04:58Кажется, с этим можно сделать гораздо больше. Возможно, запустить гонку между двумя вебхуками.
00:05:02Мало что можно сделать, когда у тебя всего пара аргументов в функции.
00:05:06Но тот факт, что это просто промис и можно использовать promise.race против sleep или другого шага...
00:05:12Мне обожаю этот паттерн. Голова идет кругом от того, что можно на этом построить.
00:05:16Верно, в этом и прелесть примитивов, которые предлагает Workflow SDK.
00:05:21Все представлено в виде промисов.
00:05:23Так что стандартные паттерны JavaScript, такие как await promise.race, просто работают.
00:05:28И еще один момент: здесь нет Redis. Нет базы данных.
00:05:33В традиционном примере мы использовали TTL в Redis для реализации таймаута.
00:05:41А здесь мы используем примитив воркфлоу — sleep.
00:05:44И никакого стажера, который должен вычищать грязную базу данных после.
00:05:50Это лучшая часть.
00:05:51И вы видите, что воркфлоу отвечает на публичный запрос редиректом на страницу успешного входа.
00:05:59Затем он получает информацию о пользователе, чтобы вернуть её клиенту, инициировавшему вход.
00:06:07И это весь наш воркфлоу. Реализация магической ссылки заняла 50 строк кода.
00:06:12Невероятно. Можно увидеть это в действии?
00:06:17Вот демо магической ссылки. Я просто введу свой email.
00:06:24Наш воркфлоу запустился и отправил письмо. И вебхук просто ждет.
00:06:31На самом деле наш воркфлоу сейчас приостановлен. То есть потребляется ноль ресурсов, пока мы ждем клика по ссылке.
00:06:41О, а как это выглядит в Vercel? Можно посмотреть на запущенный процесс?
00:06:47Хорошо, письмо пришло. Но прежде чем я кликну, давайте взглянем на мониторинг.
00:06:52Я знаю, что перескакиваю, но мне нравится, что мы на это смотрим.
00:06:57Итак, мы видим, что наш запуск здесь, он начался 40 секунд назад.
00:07:02Если мы заглянем внутрь, увидим стандартные функции мониторинга воркфлоу.
00:07:08Мы видим входные данные: мой email, который я ввел в форму.
00:07:13И что интересно, мы видим, что наш хук просто находится в ожидании.
00:07:17Ты сказал, что сейчас ничего не исполняется. Это просто мониторинг, но никто реально не сидит и не ждет моего клика.
00:07:25Именно. Хук ждет, sleep спит, и ни то, ни другое не потребляет вычислительных мощностей.
00:07:39Но мы видим наш хук, и если помните, они оба участвуют в promise.race.
00:07:46Так что один из них должен завершиться первым, чтобы воркфлоу продолжился.
00:07:50Нажимаю на ссылку... и мы видим редирект на страницу успеха, как и было прописано в логике.
00:07:59И если я вернусь к форме входа...
00:08:01Верно, а в панели управления процесс тоже должен быть завершен.
00:08:05Точно. Наш воркфлоу завершился.
00:08:08И видно, что таймер тоже остановился, как только хук «выиграл».
00:08:11Да, мы внедрили магическую ссылку примерно в 50 строках кода.
00:08:17Это очень изящно. Здорово видеть, как схема работы магической ссылки на доске...
00:08:27совпадает с тем, как это выглядит в коде — шаги в коде в точности повторяют логику.
00:08:34Никакой промежуточной базы данных, никаких лишних API-роутов. Код читается очень легко.
00:08:41Я думаю, это самый мощный аспект Workflow SDK — возможность структурировать логику приложения естественно, а не подстраивать её под инфраструктуру.
00:08:59Верно. И мне нравится идея с вебхуками здесь — само название дает новый взгляд на них.
00:09:07Это просто эфемерный URL, который можно создать и на котором можно приостановиться.
00:09:10Это хороший переход, потому что я думаю о том, что мы в Vercel создаем много агентов.
00:09:16Мы делаем агентов для Slack и GitHub, и часто подписываемся на их вебхуки, верно?
00:09:23Каждый раз, когда появляется новый комментарий в PR, мы хотим запустить агента Vercel на основе события от GitHub.
00:09:31Можно ли использовать вебхуки Workflow, чтобы подписываться на события от GitHub, например?
00:09:36Для вебхуков от Slack или GitHub обычно нужно зайти в панель управления и вручную настроить статический URL обратного вызова.
00:09:49Верно. Там нельзя создавать одноразовые URL, как мы сделали с email.
00:09:54Верно. Функция createWebhook более высокого уровня, она генерирует уникальный случайный URL,
00:10:04который привязан к одному конкретному запуску воркфлоу.
00:10:07А роут для GitHub или Slack может быть связан с любым количеством запусков.
00:10:14Точно. Нужно заранее сконфигурировать что-то одно, хотя пул-реквестов много, все они идут на один эндпоинт.
00:10:20Чтобы реализовать это с помощью Workflow SDK, мы спустимся на уровень ниже и используем примитив hook.
00:10:28У меня как раз есть демо, чтобы показать это.
00:10:31Давайте посмотрим.
00:10:32Окей. Это бот «Время историй» (storytime bot).
00:10:35Это самое первое приложение, которое я написал на Workflow SDK чуть больше года назад.
00:10:40Работает оно так: вы вводите команду /storytime, и создается новый тред.
00:10:47Каждый тред представлен отдельным запуском воркфлоу.
00:10:52Когда мы открываем тред, мы видим, что ИИ начал историю, а вы, я или кто угодно в канале можем её продолжить.
00:11:05И ИИ поможет нам довести её до финала.
00:11:09Окей. У Луны есть магическое семя, что дальше? Она его сажает.
00:11:13Мы видим, что работа пошла.
00:11:17Что дальше? Что-то магическое.
00:11:20Наша история закончена, вот финал, и сейчас еще сгенерируется картинка.
00:11:26Но мы к этому вернемся.
00:11:28Мне уже очень любопытно, я ожидал один запрос к вебхуку, но их было как минимум два, ведь было два сообщения.
00:11:35Очень интересно посмотреть на код.
00:11:38Хорошо. Это функция воркфлоу для нашего бота.
00:11:44Она принимает ID канала — канала нашего бота.
00:11:50В неё можно передать некоторые настройки.
00:11:53Но интересно то, что здесь есть массив messages. Если вы знакомы с AI SDK, это формат данных для хранения переписки с ИИ.
00:12:04В типичном боте для Slack вам пришлось бы хранить это в базе данных и при каждом событии вебхука (новом сообщении) восстанавливать состояние и искать переписку в базе.
00:12:23Но здесь происходит не это. Это просто массив внутри вашей функции.
00:12:27Да. Я усмехнулся вначале, потому что увидел комментарий в коде: «Смотри, мам, никаких очередей или баз данных».
00:12:34И здесь нет импорта базы данных. Вы импортируете только Workflow.
00:12:40И возвращаясь к сообщениям: это легко пропустить, но у вас просто есть переменная final story, куда, как я понимаю, добавляются сообщения,
00:12:55и в итоге получается строка. Но база данных не нужна. Похоже, что «let» здесь и есть ваша база данных.
00:13:04Да, «let — это ваша база данных» — отличное выражение, надо его закрепить.
00:13:10Возможно, я его у тебя и украл, но...
00:13:14Интересный момент, ради которого мы здесь: функция hook. Мы создаем хук, но в отличие от примера с магической ссылкой, здесь мы передаем токен.
00:13:28Это строка, содержащая идентификатор, уникальный для данного запуска воркфлоу.
00:13:35TS — это ID треда. Так что эта строка — токен, который однозначно определяет этот запуск.
00:13:44Когда мы посмотрим на код роута вебхука, мы увидим, что данные от Slack содержат всё необходимое, чтобы детерминированно воссоздать этот ID.
00:13:58В этом и заключается магия того, как вебхук сопоставляется с конкретным запуском воркфлоу.
00:14:04Да, мне было интересно, ведь для магической ссылки создавались новые URL, а в Slack-боте нельзя давать новый URL для каждого треда.
00:14:17То есть вы получаете сообщение и вычисляете тот же токен на стороне возобновления.
00:14:29Воркфлоу может ждать этот токен, а вы конструируете его из данных сообщения, чтобы продолжить выполнение.
00:14:37Именно. Slack-бот был настроен один раз через панель управления Slack, где нужно указать статический URL.
00:14:50Поэтому низкоуровневый примитив hook здесь подходит лучше — мы можем динамически воссоздать токен.
00:14:59Давайте быстро взглянем на роут вебхука, там на самом деле не так много кода.
00:15:07Главное — как мы воссоздаем токен из данных, присланных Slack.
00:15:13Затем мы вызываем функцию resume, и это возобновляет именно тот самый запуск воркфлоу.
00:15:20Это очень круто. Я так понимаю, с вебхуками вы делаете примерно то же самое.
00:15:28Вебхук — это просто случайный токен и эндпоинт, который его разрешает?
00:15:35Да, разница лишь в том, что для функции вебхука вам не нужно прописывать API-роут в своем коде.
00:15:44Workflow SDK сам реализует дефолтный роут для этой функции.
00:15:50Но в остальном — это сгенерированный токен, уникальный для одного запуска.
00:15:55Но в данном случае у нас есть хук с токеном, и этот хук может получать данные несколько раз.
00:16:06Это отличается от примера с магической ссылкой, где срабатывание было однократным.
00:16:11Здесь мы хотим, чтобы хук срабатывал на каждое новое сообщение в треде Slack.
00:16:17Для этого используется синтаксис for await в JavaScript, обычный для асинхронных итераторов.
00:16:25В данном случае мы получаем несколько порций данных от вебхука Slack через наш хук.
00:16:33Это так круто. Я никогда не находил хорошего применения... Я люблю асинхронные итераторы и генераторы, когда-то даже делал доклад об этом.
00:16:42Но они всегда были хороши только для демо, я не знал, как их реально использовать.
00:16:46А здесь это выглядит просто как цикл.
00:16:50Но вместо перебора фиксированного набора элементов, вы используете for await на хуке — и цикл идеально ложится на задачу.
00:17:01Всё внутри цикла соответствует одному сообщению пользователя.
00:17:05Это отличный способ мышления: новое сообщение вызывает новую итерацию цикла, всё встает в очередь и продолжается.
00:17:12Прелесть в том, что на каждой итерации, пока мы ждем нового сообщения, вычислительные ресурсы вообще не тратятся.
00:17:22Воркфлоу полностью приостановлен, и следующее сообщение может прийти через минуты, дни или никогда — это не проблема.
00:17:33То есть в том канале могут быть треды, где запуск просто висит неделями в ожидании ответа?
00:17:42Это очень круто.
00:17:43И возвращаясь к массиву messages: теперь мы просто модифицируем массив.
00:17:48Добавляем новое сообщение — и это и есть наше изменение «базы данных», потому что массив — локальная переменная.
00:17:57Обалденно. И я вижу, вы используете promise.all для параллельного выполнения шагов.
00:18:03Код выглядит очень чисто для каждого сообщения в Slack.
00:18:08Мне нравится, что именно так я бы моделировал задачу на каком-нибудь хакатоне.
00:18:12Просто записать, что должно происходить при каждом сообщении.
00:18:16Да, модель с promise.all — это обычные функции useStep, которые мы запускаем параллельно.
00:18:23Например, добавление реакции на сообщение в Slack нужно для быстрой обратной связи пользователю.
00:18:32И одновременно мы запускаем ИИ, чтобы он продолжал генерировать историю.
00:18:39Мне было бы очень интересно посмотреть мониторинг, когда будет возможность. Представляю, как эти процессы запускаются одновременно.
00:18:49У нас есть мониторинг для «Времени историй».
00:18:52Он завершен, так что нужно будет проверить ту картинку.
00:18:56Вот наш хук.
00:18:58И здесь интересно то, что у нас два события получения данных хуком.
00:19:05Они соответствуют двум сообщениям, которые я отправил в тред Slack.
00:19:10Мониторинг позволяет увидеть конкретные данные, которые были переданы в хук.
00:19:14О, это очень удобно.
00:19:16То есть это данные от Slack. И вам не нужно было их отдельно логировать, они просто отображаются как события в панели управления.
00:19:25Верно. И каждый раз при получении данных выполнение воркфлоу продолжается, и шаги идут дальше.
00:19:34И в конце мы получаем результат генерации картинки для истории.
00:19:40Вот так работает бот «Время историй».
00:19:42Действительно здорово.
00:19:43Здорово увидеть вебхуки и в магических ссылках, и использование низкоуровневых хуков в цикле для обработки множества событий.
00:19:54Это очень круто.
00:19:55Кажется, эта модель идеально подходит для взаимодействия человека с системой через вебхуки.
00:20:02А для чего еще можно использовать хуки?
00:20:05О, определенно есть еще варианты.
00:20:06Последнее демо, которое я подготовил, использует похожий паттерн.
00:20:17Но здесь мы используем вебхук, чтобы передать выполнение кода куда-то еще и ждать завершения вычислений на другой стороне.
00:20:32Пока воркфлоу приостановлен, сторонний сервис вызывает наш вебхук, и мы завершаем логику приложения.
00:20:41В этом примере мы возьмем Vercel Sandbox для длительной операции, например, конвертации файла через FFmpeg.
00:20:51Итак, вот наш воркфлоу конвертации через FFmpeg.
00:20:56Первым делом мы создаем Vercel Sandbox.
00:21:00Интересно, что NPM-пакет Sandbox предоставляет функции, которые внутри себя уже используют useStep.
00:21:09Таким образом, эта операция фактически является Шагом (Step).
00:21:12То есть вы можете выпустить свой NPM-пакет.
00:21:15Sandbox по сути просто поставляется как пакет с директивой use Step внутри этой функции.
00:21:21Поэтому, когда вы импортируете и используете его в рабочем процессе, он автоматически делает Sandbox Шагом.
00:21:29Но это не значит, что вы не можете использовать Sandbox для создания чего-то вне рабочего процесса.
00:21:32Что произойдет, если вызвать это без рабочего процесса?
00:21:35Если вы понимаете, что директива — это просто строка, то при выполнении без компилятора рабочих процессов...
00:21:47эта строка ничего не делает. Так что это просто работает.
00:21:49Добавление use Step в ваши NPM-пакеты отлично работает и без Workflow SDK.
00:21:55А как только вы используете эту функцию внутри Workflow SDK, вы сразу получаете преимущества отказоустойчивости.
00:22:03Итак, Sandbox просто выполняет типичные вещи.
00:22:07Он устанавливает FFmpeg, так как по умолчанию он недоступен.
00:22:11Затем загружает файл по указанному нами URL.
00:22:14И каждый из этих запусков сейчас тоже является отдельным Шагом?
00:22:17Да, они запускают отдельные команды в Sandbox, и это Шаги. Мы увидим их в панели мониторинга.
00:22:29И затем мы возвращаемся к вызову create-webhook, который вы можете помнить по демо с магической ссылкой.
00:22:36Но в данном случае мы просто передадим этот URL вебхука в наш Bash-скрипт, который запустим в Sandbox.
00:22:43Что здесь происходит: мы запустим FFmpeg и сконвертируем файл в формат, запрошенный в интерфейсе.
00:22:53И когда всё будет готово, Bash-скрипт выполнит cURL-запрос к нашему URL обратного вызова из вебхука.
00:22:59И как только этот cURL-запрос поступит, логика нашего рабочего процесса возобновится.
00:23:04Понятно. Это круто. Я заглянул немного вперед и заметил оператор AND в этом запуске.
00:23:11То есть вы запускаете скрипт в фоновом режиме, потому что такой Шаг FFmpeg может занять много времени.
00:23:17Вам не нужно, чтобы Шаг просто висел и ждал завершения.
00:23:20Верно. Эта строка прямо здесь запускает наш скрипт конвертации FFmpeg в фоновом режиме.
00:23:28Затем функция рабочего процесса приостанавливается, и мы ждем возобновления через вебхук.
00:23:34И я снова вижу Promise.race с часовым ожиданием. Это отличный паттерн.
00:23:40Да, на этот раз процесс конвертации FFmpeg может занять длительное время.
00:23:46Это может быть очень большой медиафайл, поэтому в данном случае мы устанавливаем таймаут в один час.
00:23:51И это абсолютно нормально. В рабочем процессе можно ожидать практически неограниченное время.
00:23:56И опять же, пока мы ждем возобновления вебхука, вычислительные ресурсы вообще не потребляются.
00:24:01Мы можем это увидеть? Можем посмотреть, как это работает? У нас есть демо?
00:24:04Да.
00:24:05Это немного забавный пример.
00:24:07О, я сразу узнал кролика Big Buck Bunny. Это из Blender.
00:24:12Да, я помню, как смотрел эти видео, когда давным-давно изучал Blender.
00:24:16Ого, я тебе завидую.
00:24:19Итак, мы вставили URL медиафайла. В данном случае мы просто извлечем из него аудиодорожку.
00:24:26Как только мы нажимаем кнопку, запускается рабочий процесс, и мы можем перейти в панель мониторинга.
00:24:33А вот и он. Мы видим создание нашего Sandbox.
00:24:37И он возвращает экземпляр Sandbox. Довольно круто.
00:24:42Это потому, что в рабочем процессе всё должно быть сериализуемым.
00:24:46Но, как вы сказали, Sandbox поддерживает сериализацию, поэтому они отображаются в рабочем процессе.
00:24:53Верно. Пакет Vercel Sandbox содержит класс Sandbox, в котором реализованы функции сериализации рабочего процесса.
00:25:03Поэтому всё отлично работает в нашей панели мониторинга.
00:25:06То есть это может любой пакет, верно? Любой класс может реализовать те же символы и директивы use Step.
00:25:17Да, именно так. Мы видим, что наш хук был вызван обратно через 20 секунд.
00:25:25На этот раз конвертация прошла быстрее, так как файл небольшой, но это могло занять любое время.
00:25:31Видно, что после создания и инициализации Sandbox был создан хук, который мы передали для запуска FFmpeg.
00:25:43И когда работа завершилась, мы получили ответ (payload) от нашего Sandbox.
00:25:48Это тот самый cURL из bash-скрипта. Он выполняет команду, а затем через cURL завершает вебхук.
00:25:57Верно. Sandbox закончил работу и передает управление обратно нашему рабочему процессу.
00:26:04Теперь я думаю об этом так: вы запускаете Шаг, он выполняет код в фоне и продолжает процесс.
00:26:13Но Hook и Webhook кажутся чем-то более низкоуровневым. Можно просто создать токен или URL и ждать чего угодно.
00:26:21Это может быть магическая ссылка для человека, email, Sandbox или любое другое вычисление.
00:26:27Рабочий процесс просто замирает со всем своим состоянием до наступления события. Это как бы глубже, чем Step.
00:26:34Да. Я рассматриваю Webhook и Hook как способ передачи внешних данных в ваш рабочий процесс.
00:26:42Для меня Step — это способ приостановить процесс, дождаться завершения вычислений и возобновить его.
00:26:50Но Hook и Webhook действительно кажутся более базовыми, потому что вы создаете токен или URL для отправки куда угодно.
00:27:01Это может быть человек, электронная почта или, например, другой рабочий процесс.
00:27:05И когда это завершается, родительский рабочий процесс просыпается и продолжает ровно с того же места.
00:27:12Так что это в каком-то смысле уровень ниже, чем Step. Способ приостановки для любого внешнего действия.
00:27:19Да. Мне нравится думать о Hook как о способе приостановки процесса в ожидании внешних данных.
00:27:31Это очень круто. Наше время вышло, но эти демо снова подтвердили, почему Hook — моя любимая функция.
00:27:42Здорово. Рад, что вам понравилось.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video