Содержание урока · 10 разделов
- chromedp таймаут: дедлайн в контексте
- Прогрев: первый Run без дедлайна
- Ретраи: что повторять, а что чинить
- Классификация ошибок устойчивого обхода
- Смерть браузера: заметить и восстановиться
- chromedp.Cancel: закрыть браузер и увидеть ошибку
- Чек-лист устойчивого обхода
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Обход из двадцати страниц вчерашним вечером сегодня падает на третьей: сеть моргнула, сайт притормозил, Chrome тихо закрылся. Пока программа одноразовая и ручная, это просто перезапуск; но парсер, который работает ночью по расписанию, измеряется не удачными запусками, а процентом заданий, доехавших до конца, и без таймаутов этот процент держится на удаче. Устойчивость в chromedp складывается из трёх вещей, и все три — про контекст. Первая: у каждого действия должен быть потолок времени — дедлайн в контексте, который истечёт сам, без часов и Sleep. Вторая: сбои должны повторяться с растущей паузой, а не валить обход с первой попытки. Третья: смерть браузера — штатная ситуация, из которой программа выходит пересозданием контекста, а не перезапуском всей программы. Всё это уже знакомые механизмы: контексты из первых уроков, ожидания из девятого, аллокаторы из предыдущего. Здесь они собираются в дисциплину: где стоит дедлайн, что ретраить, как заметить мёртвый браузер и как закрывать его по-взрослому.
chromedp таймаут: дедлайн в контексте#
chromedp таймаут получает не отдельной опцией действия, а контекстом: context.WithTimeout(ctx, 30*time.Second) возвращает производный контекст с дедлайном и функцию отмены, и всё, что исполняется с этим контекстом, укладывается в потолок. Механика одна на всю программу: каждое действие chromedp перед командой и во время неё смотрит на контекст, и истёкший дедлайн возвращается из Run ошибкой context.DeadlineExceeded. Проверять её нужно errors.Is, а не сравнением текста: обёртки и ретраи добавляют свои слои, и только errors.Is видит причину сквозь них. Отмена — обязательная пара к дедлайну: defer cancel() освобождает таймер и внутренние структуры, без него контексты копятся до конца программы. И помните про отсчёт: дедлайн стартует от вызова WithTimeout, а не от начала Run — если между созданием контекста и Run прошло двадцать секунд из тридцати, на само действие останется десять. Сообщение у ошибки стабильное — context deadline exceeded, — и вместе с errors.Is оно удобно отделяет дедлайны от прочих сбоев в общем потоке логов. Дедлайны вкладываются друг в друга как матрёшки: общий на обход, перишаговый на страницу, и истечение внешнего погашает внутренние досрочно.
Перишаговый таймаут в цикле обхода
GOctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// прогрев: первый Run заводит браузер — без короткого дедлайна
if err := chromedp.Run(ctx, chromedp.Navigate("about:blank")); err != nil {
log.Fatal(err)
}
for _, url := range urls {
stepCtx, stepCancel := context.WithTimeout(ctx, 30*time.Second)
err := chromedp.Run(stepCtx,
chromedp.Navigate(url),
chromedp.WaitVisible(".cards"),
)
stepCancel()
if err != nil {
log.Printf("%s: %v", url, err)
continue
}
// ... чтение данных страницы
}Дедлайн живёт в stepCtx и действует только на свой шаг: истёк — Run вернёт context.DeadlineExceeded, а браузер продолжит работать под родительским ctx. stepCancel вызывают сразу после Run вручную: таймер освобождается, не дожидаясь конца функции. Ошибка шага пишется в лог вместе с адресом, и цикл идёт дальше — отчёт по сбойным страницам собирается после обхода.
Прогрев: первый Run без дедлайна#
NewContext ничего не запускает: браузер заводится на первом Run — и документация библиотеки прямо предупреждает, что вешать таймаут контекста на первый Run плохая идея: истечение дедлайна остановит весь браузер. Причина в механике: первый Run и есть аллокация — запуск процесса, чтение адреса отладки, рукопожатие по вебсокету; отмена контекста в этот момент гасит всё целиком, и контекст остаётся нерабочим. Отсюда дисциплина прогрева: первый Run исполняет дешёвое действие без дедлайна — Navigate на about:blank достаточно, — и только после него обход идёт перишаговыми контекстами с короткими потолками. Прогрев делается один на контекст: повторять его после каждого шага не нужно, браузер живёт, пока жив родительский контекст, и все следующие Run едут через уже открытое соединение. Холодный старт Chrome на загруженном сервере занимает секунды, поэтому перишаговый дедлайн обязан мерить работу страницы, а не запуск браузера — иначе медленная машина будет ронять обход на первом же адресе, хотя сама страница могла бы открыться мгновенно. И если прогрев упал — Chromium не нашёлся, флаг битый, — ретраить его бессмысленно до исправления окружения: ошибка старта из таблицы ниже придёт снова.
Классическая авария: помощник оборачивает весь обход в context.WithTimeout(ctx, 5*time.Second) — «чтобы действия не висели». На быстрой машине всё работает, на холодном сервере первый Run не успевает завести браузер, дедлайн сгорает — и весь браузер умирает вместе с контекстом, потому что первый Run и есть аллокация. Все последующие Run возвращают context canceled, разработчик ищет падение Chrome и утечку памяти, а виноват таймаут не на том контексте. Лечение — прогрев без дедлайна и перишаговые потолки на производных контекстах. Спорное мнение с условием: общий потолок на весь обход ставить можно и нужно — но поверх прогретого браузера и с запасом на аллокацию.
Retry-обёртка с бэкоффом
GOfunc withRetry(parent context.Context, attempts int,
act func(context.Context) error) error {
delay := time.Second
var err error
for i := 0; i < attempts; i++ {
tctx, cancel := context.WithTimeout(parent, 20*time.Second)
err = act(tctx)
cancel()
if err == nil {
return nil
}
if errors.Is(err, context.Canceled) {
return err // браузер умер или обход отменили — ретрай не спасёт
}
select {
case <-parent.Done():
return parent.Err()
case <-time.After(delay):
}
delay *= 2
}
return fmt.Errorf("после %d попыток: %w", attempts, err)
}Каждая попытка получает собственный дедлайн и свежий производный контекст: таймаут прошлой попытки не портит следующую. context.Canceled выводится из ретрая отдельно — это сигнал внешней отмены или смерти браузера, повторять бессмысленно. Пауза растёт вдвое после каждой неудачи и уважает отмену родителя через select. Ошибка последней попытки оборачивается через %w, поэтому errors.Is на стороне вызывающего видит первопричину.
Ретраи: что повторять, а что чинить#
Устойчивый парсер отличает повторяемую ошибку от неповторяемой, и классификация здесь короткая. Таймаут шага или моргание сети — повторять: у страницы может не быть удачи в эту секунду, и свежая попытка с новым дедлайном часто проходит. ErrPollingTimeout из девятого урока — не ретраить вслепую: предикат не стал истиной за свои тридцать секунд, значит селектор указывает мимо или сайт отдаёт не то, что вы ждёте, и повторение вернёт ту же ошибку на те же деньги. context.Canceled — красный флаг: контекст отменили до дедлайна — либо вы сами, либо chromedp при потере браузера, об этом ниже. Ошибка старта процесса — территория конфигурации: ретраем её не лечат. У библиотеки тот же приём стоит внутри: Query-механика крутит внутренний хелпер retryWithSleep с интервалом в несколько миллисекунд, но публичного API ретраев в chromedp нет — обёртка пишется на Go руками, и она у вас уже есть из примера выше. Потолок попыток — обязательный атрибут обёртки: без него одна навечно сломанная страница превратит обход в бесконечный цикл, который не спасает даже бэкофф.
Классификация ошибок устойчивого обхода#
| Ошибка | Откуда | Что делать |
|---|---|---|
| context.DeadlineExceeded | истёк дедлайн шага | ретрай с бэкоффом и свежим дедлайном |
| context.Canceled | отмена снаружи или смерть браузера | не ретраить: восстанавливать контекст |
| waiting for function failed: timeout | ErrPollingTimeout: предикат не стал истиной | селектор и сайт, не сеть: слепой ретрай бесполезен |
| websocket url timeout reached | Chrome не отдал адрес отладки за 20 секунд | путь Chrome и окружение запуска |
| chrome failed to start: ... | процесс не стартовал | флаги, память, права — конфигурация, не сеть |
Смерть браузера: заметить и восстановиться#
Chrome — внешний процесс, и он умирает: OOM-killer на сервере, крэш вкладки, обрыв соединения. У chromedp на этот случай правило из исходников: как только соединение с браузером потеряно, библиотека сама отменяет контекст — процесс связан с контекстом, и отмена гасит его. Для парсера это выглядит узнаваемо: действие, которое ещё вчера работало, возвращает context canceled, а вы cancel не вызывали. Восстановление — пересоздание: старый контекст мёртв навсегда, но аллокатор жив, и новый NewContext от того же аллокатора заводит свежий браузер с теми же флагами — та же механика, что в ротации личностей из четырнадцатого урока про прокси и User-Agent и в пуле из девятнадцатого. Различать «нашу» отмену и смерть браузера проще всего по дизайну: если программа сама не вызывала cancel, а context canceled пришёл — браузер умер. Это сигнал к восстановлению, а не к ретраю: попытки уезжать действиями в мёртвый контекст вернут ту же ошибку. Для мониторинга заведите счётчик пересозданий контекста за задание: единичные смерти — норма внешнего мира, а рост от задания к заданию — проблема окружения, память, версия Chrome или лимиты.
chromedp.Cancel: закрыть браузер и увидеть ошибку#
defer cancel() закрывает контекст, но молча: ошибки, случившиеся при уборке, теряются. chromedp.Cancel — взрослая версия той же отмены: она закрывает браузер штатно, дожидается, пока уберутся ресурсы — включая временный каталог профиля, — и возвращает ошибку, если при закрытии что-то пошло не так. Закрытие само можно ограничить дедлайном: документация показывает шаблон с WithTimeout на десять секунд вокруг Cancel — если браузер не закрылся за это время, вы узнаете об этом по своему таймеру. Отдельный случай — Cancel на контексте аллокатора: там нет chromedp-контекста, и Cancel вернёт ErrInvalidContext; аллокатор закрывается собственной функцией отмены. Где это нужно: при перезапуске браузера посреди обхода — убить подозрительного штатно и завести нового от аллокатора; при завершении задания — получить ошибку уборки на руки и записать в лог, а не потерять при выходе из программы.
Штатное закрытие с дедлайном
GOtctx, tcancel := context.WithTimeout(ctx, 10*time.Second)
defer tcancel()
if err := chromedp.Cancel(tctx); err != nil {
log.Printf("закрытие браузера: %v", err)
}
// браузер закрыт штатно, ресурсы убраны — можно заводить нового
newCtx, newCancel := chromedp.NewContext(allocCtx)
defer newCancel()Cancel — вариант defer cancel() с отчётом: закрывает браузер штатно, дожидается уборки ресурсов и возвращает ошибку, если что-то пошло не так при закрытии. Дедлайн на tctx ограничивает ожидание десятью секундами — это шаблон из документации библиотеки. Новый контекст заводится от аллокатора: это свежий чистый браузер с тем же набором флагов личности.
Таймауты живут в двух измерениях, и путать их дорого: перишаговый дедлайн отвечает за одну страницу, общий — за весь обход. Вложите один в другой: дедлайн шага меньше общего, тогда истечение шага не убивает задание целиком. Рядом живут и Poll-потолки из девятого урока — WithPollingTimeout и контекстный дедлайн конкурируют, сработает тот, что раньше; выбирайте под смысл: предикат ждёт факт, дедлайн страхует всё остальное. Потолок для шага дешевле вывести из лога, чем угадать: пишите затраченное время по каждому шагу — распределение подскажет границу точнее любого совета.
Чек-лист устойчивого обхода#
- прогрев: первый Run — без дедлайна, он заводит браузер
- дедлайны шагов — на производных контекстах: истёк шаг, браузер жив
- ошибки шагов — в лог с адресом; обход не падает, отчёт в конце
- ретрай — для таймаутов и сетевых морганий; context.Canceled — не ретрай
- пауза ретрая растёт вдвое и уважает отмену родителя
- context canceled без вашей вины — браузер умер: новый NewContext от аллокатора
- закрытие по-взрослому — chromedp.Cancel с дедлайном: штатно и с ошибкой на руках
Проверка: первый Run под таймаутом
Контекст обёрнут в context.WithTimeout на 2 секунды, и это первый Run: холодный старт Chrome занимает дольше. Что произойдёт?
Проверка: Cancel на аллокаторе
Что вернёт chromedp.Cancel, если передать ей контекст, полученный из NewExecAllocator?
Что сделает ретрай
Прочитайте код и выберите, что он выведет:
// act при каждой попытке возвращает ошибку,
// совместимую с context.DeadlineExceeded
// attempts = 3, стартовая пауза 1 секунда, удвоение
err := withRetry(ctx, 3, act)Соберите перишаговый таймаут
Восстановите каркас: дедлайн на шаге, его освобождение и запуск действий.
Расставьте каркас устойчивого шага
Соберите по порядку: контекст, прогрев, перишаговый дедлайн и шаг с логом ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: таймауты и устойчивость
Что дальше: устойчивость — последний слой под одиночным браузером, дальше курс раздваивается на масштаб и контроль. Шестнадцатый урок разберёт вкладки и окна: несколько страниц одного браузера и параллельные схемы поверх них. Двадцать второй соберёт сбои воедино — ошибки и отладку chromedp: паники, режимы запуска, логи DevTools. Практика — локально: нужны установленный Go и Chrome, песочница сайта исполняет тематическую программу на чистом Go без браузера, и в ней честно не воспроизвести ни смерть Chrome, ни сетевые дедлайны — устойчивый парсер проверяется только на настоящем интернете и настоящем процессе. Домашняя проверка на честность: убейте процесс Chrome руками посреди обхода и прочитайте, что вернёт следующий Run, — эту ошибку вы теперь знаете в лицо.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Дедлайн ставится контекстом: context.WithTimeout плюс defer cancel, а проверка ошибки — errors.Is, не сравнением строк.
- Первый Run аллоцирует браузер: таймаут на нём остановит браузер целиком — прогрев делают без дедлайна.
- Перишаговые дедлайны живут на производных контекстах: истёк шаг — браузер продолжает работать.
- Ретраятся таймауты и сетевые моргания; ErrPollingTimeout — повод чинить селектор, context.Canceled — сигнал без ретрая.
- Пауза ретрая растёт вдвое и уважает отмену родителя; ошибка оборачивается %w для errors.Is.
- При потере соединения chromedp сам отменяет контекст: context canceled без вашей вины — умер браузер.
- Восстановление — новый NewContext от живого аллокатора: свежий браузер с теми же флагами.
- chromedp.Cancel закрывает браузер штатно, дожидается уборки и возвращает ошибку; на аллокаторе — ErrInvalidContext.
- Дедлайны вкладываются: шаг меньше обхода, Poll-потолок конкурирует с ними — сработает тот, что раньше.
Домашнее задание
Соберите обход из пяти страниц на перишаговых таймаутах: прогрев без дедлайна, шаги по context.WithTimeout, лог ошибок с адресами. Затем уроните браузер — убейте процесс Chrome руками во время шага — и прочитайте ошибку следующего Run. Добавьте withRetry на адреса с таймаутами и измерьте, сколько попыток нужно при паузе 1с против 5с. Финал: поставьте общий потолок на весь обход и проверьте, что истечение общего дедлайна гасит шаги досрочно, но лог с адресами при этом доехал.
Личные заметкиПусто
Вопросы и ответы по уроку
Run вернул context canceled, хотя я не отменял контекст. Что это?
Браузер умер: при потере соединения chromedp сам отменяет контекст — процесс связан с ним, и соединение без браузера бессмысленно. Ретрай в этот контекст бессмыслен: он мёртв навсегда. Заведите новый контекст от того же аллокатора — это свежий браузер с теми же флагами, — и продолжайте обход с логом адреса, на котором оборвалась сессия.
Какой таймаут ставить на шаг?
Измерьте, а не угадывайте: возьмите медианное время страницы и умножьте на запас — обычно 3-5 раз хватает. Слишком короткий потолок рождает ложные ретраи и лишнюю нагрузку на сайт, слишком длинный — ночные задания, которые висят на одной странице часами. Отдельно помните: дедлайн отсчитывается от WithTimeout, а не от начала Run, и включает ваши ожидания, чтения и оценки целиком.
Почему нельзя просто обернуть весь обход в один большой WithTimeout?
Можно — общий потолок на задание это правильная практика. Опасно другое: если этот же контекст получает первый Run, дедлайн сгорает на аллокации и уносит весь браузер, а с ним и обход. Разделяйте: прогрев без дедлайна, шаги — короткими производными контекстами, общий потолок — поверх прогретого браузера, и следите, чтобы истечение общего дедлайна не совпало с медленным стартом на загруженной машине.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа