Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 15 из 25 ~18 мин +50 XP

Таймауты и устойчивость

chromedp таймаут задаёт не библиотека, а сам язык: контекст с дедлайном из пакета context — и от того, где стоит этот дедлайн, зависит, доедет ли ночное задание до конца или умрёт на первой медленной странице. Разберём перишаговые таймауты, прогрев первого Run, retry-обёртку с бэкоффом, смерть браузера и вежливое закрытие через chromedp.Cancel — фундамент, на котором держится устойчивый парсер.

Содержание урока · 10 разделов

Обход из двадцати страниц вчерашним вечером сегодня падает на третьей: сеть моргнула, сайт притормозил, 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 оно удобно отделяет дедлайны от прочих сбоев в общем потоке логов. Дедлайны вкладываются друг в друга как матрёшки: общий на обход, перишаговый на страницу, и истечение внешнего погашает внутренние досрочно.

Перишаговый таймаут в цикле обхода

GO
ctx, 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 не нашёлся, флаг битый, — ретраить его бессмысленно до исправления окружения: ошибка старта из таблицы ниже придёт снова.

Retry-обёртка с бэкоффом

GO
func 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: timeoutErrPollingTimeout: предикат не стал истинойселектор и сайт, не сеть: слепой ретрай бесполезен
websocket url timeout reachedChrome не отдал адрес отладки за 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; аллокатор закрывается собственной функцией отмены. Где это нужно: при перезапуске браузера посреди обхода — убить подозрительного штатно и завести нового от аллокатора; при завершении задания — получить ошибку уборки на руки и записать в лог, а не потерять при выходе из программы.

Штатное закрытие с дедлайном

GO
tctx, 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 ограничивает ожидание десятью секундами — это шаблон из документации библиотеки. Новый контекст заводится от аллокатора: это свежий чистый браузер с тем же набором флагов личности.

Чек-лист устойчивого обхода#

  • прогрев: первый 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)

Соберите перишаговый таймаут

Восстановите каркас: дедлайн на шаге, его освобождение и запуск действий.

stepCtx, stepCancel := context.(ctx, 30*time.Second) stepCancel() err := chromedp.(stepCtx, chromedp.Navigate(url), chromedp.WaitVisible(".cards"), )

Расставьте каркас устойчивого шага

Соберите по порядку: контекст, прогрев, перишаговый дедлайн и шаг с логом ошибки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: таймауты и устойчивость

Термин

context.WithTimeout

нажмите, чтобы перевернуть
Определение

Производный контекст с дедлайном: истёк — действия возвращают context.DeadlineExceeded. Отсчёт идёт от вызова WithTimeout, а не от начала Run; проверка ошибки — errors.Is.

1 / 6

Что дальше: устойчивость — последний слой под одиночным браузером, дальше курс раздваивается на масштаб и контроль. Шестнадцатый урок разберёт вкладки и окна: несколько страниц одного браузера и параллельные схемы поверх них. Двадцать второй соберёт сбои воедино — ошибки и отладку 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с. Финал: поставьте общий потолок на весь обход и проверьте, что истечение общего дедлайна гасит шаги досрочно, но лог с адресами при этом доехал.

Личные заметкиПусто
Хранятся только в вашем браузере (Chromedp · Таймауты и устойчивость)

Вопросы и ответы по уроку

Run вернул context canceled, хотя я не отменял контекст. Что это?

Браузер умер: при потере соединения chromedp сам отменяет контекст — процесс связан с ним, и соединение без браузера бессмысленно. Ретрай в этот контекст бессмыслен: он мёртв навсегда. Заведите новый контекст от того же аллокатора — это свежий браузер с теми же флагами, — и продолжайте обход с логом адреса, на котором оборвалась сессия.

Какой таймаут ставить на шаг?

Измерьте, а не угадывайте: возьмите медианное время страницы и умножьте на запас — обычно 3-5 раз хватает. Слишком короткий потолок рождает ложные ретраи и лишнюю нагрузку на сайт, слишком длинный — ночные задания, которые висят на одной странице часами. Отдельно помните: дедлайн отсчитывается от WithTimeout, а не от начала Run, и включает ваши ожидания, чтения и оценки целиком.

Почему нельзя просто обернуть весь обход в один большой WithTimeout?

Можно — общий потолок на задание это правильная практика. Опасно другое: если этот же контекст получает первый Run, дедлайн сгорает на аллокации и уносит весь браузер, а с ним и обход. Разделяйте: прогрев без дедлайна, шаги — короткими производными контекстами, общий потолок — поверх прогретого браузера, и следите, чтобы истечение общего дедлайна не совпало с медленным стартом на загруженной машине.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Testcontainers25 уроков~8 чпрактика25 уроков уже доступны

Testcontainers в Go: интеграционные тесты с Docker

Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.

  • Поднимать реальные зависимости в Docker из кода теста
  • Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
  • Ждать готовность сервиса wait-стратегиями вместо sleep
  • Собирать стенды из нескольких контейнеров и своего образа