Введение
Пункт должен закрыться, не обрывая передачу тому, кто уже получает полевые заметки. Вы замените немедленное закрытие соединений обычной координацией HTTP-выключения Go и сохраните настоящую полную загрузку от исходного процесса.
Модуль начинается независимо с подготовленным обработчиком и теми же двумя документами. Требуются наблюдения точного PID и принятого запроса из предыдущего модуля либо равноценный опыт, а также обработчики, ошибки, goroutine и channel. Доставка сигналов, конечные контексты, сопоставление ошибок и обязанность main ждать объясняются ниже. Это обычный HTTP, не WebSocket, распределённое переключение трафика или гарантия отсутствия простоя в эксплуатации. Около 60 минут — оценка автора.
Заставьте main дождаться выключения
На этом шаге реализуйте конечную координацию выключения, сохранив подготовленный HTTP-обработчик.
Начните в Downloads и войдите в независимую рабочую папку:
cd /home/labex/project/download-desk
cat lifecycle.go
Разрешено менять только lifecycle.go. Сохраните обработчик в main.go, оба документа, go.mod, наблюдатели и их записи. Сейчас функция перехватывает SIGTERM, но вызывает Close, прерывая активные соединения. Один обработчик сигнала ещё не обеспечивает корректное выключение.
http.Server управляет слушателем и активными HTTP-соединениями. ListenAndServe блокируется, принимая работу. Shutdown сначала закрывает слушатели и существующие неактивные соединения, затем ждёт, пока обычные активные запросы станут неактивными. При начале выключения ListenAndServe немедленно возвращает http.ErrServerClosed, хотя принятые обработчики могут ещё работать. Если main вернётся в этот момент, все goroutine исчезнут и передача оборвётся. Поэтому основной путь должен ждать возврата самого Shutdown.
Контекст несёт отмену или временной бюджет. context.Background() даёт начальный контекст. context.WithTimeout(..., 45*time.Second) создаёт конечный бюджет выключения 45 секунд и функцию cancel; defer cancel() освобождает связанные ресурсы при возврате функции, даже если работа окончилась раньше. Отмена не доказывает завершения. Публичный максимум — 60 секунд после запроса остановки: если принятый ответ не заканчивается в выбранный бюджет, сообщите ошибку и выйдите с ненулевым кодом в пределах максимума. Эталон использует 45 секунд; допустимы равноценные конечные бюджеты не более 60 секунд. Проверка включает конечный длинный ответ, который не успевает закончиться за 60 секунд, поэтому одного context.Background() недостаточно. В таком изолированном испытании тайм-аута частичная передача и явно сообщённое ненулевое завершение сервера — правильный результат ошибки, а не успешное завершение всех запросов. Когда бюджет истекает, Shutdown возвращает ошибку; наша функция возвращает её, main выходит с ненулевым статусом. Нельзя называть это успехом или незаметно перезапускать сервер.
signal.Notify(stopped, syscall.SIGTERM) просит Go отправить сигнал в канал. Ёмкость буфера — 1, поскольку доставка сигнала не должна ждать получателя; здесь достаточно одного ожидающего запроса остановки. signal.Stop(stopped) снимает регистрацию этого канала при выходе из функции; это не остановка HTTP-сервера. Аналогично буферизированный канал результата позволяет обслуживающей goroutine сообщить единственный результат, пока main выполняет выключение.
Первый select ждёт ошибку обслуживания или сигнал остановки. Это также не даёт ошибке привязки/запуска навсегда скрыться за ожиданием сигнала. После успешного выключения errors.Is(err, http.ErrServerClosed) распознаёт ожидаемый результат обслуживания. Другая ошибка возвращается, а не объявляется успехом. main вызывает runServer, поэтому возврат только после выключения удерживает процесс живым.
Замените этот единственный файл. Кавычки у разделителя here-document заставляют оболочку буквально записывать код Go до завершающего GO на отдельной строке; > заменяет только lifecycle.go:
cat > lifecycle.go <<'GO'
package main
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func runServer(server *http.Server) error {
stopped := make(chan os.Signal, 1)
signal.Notify(stopped, syscall.SIGTERM)
defer signal.Stop(stopped)
result := make(chan error, 1)
go func() { result <- server.ListenAndServe() }()
select {
case err := <-result:
return err
case <-stopped:
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
return err
}
err := <-result
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
GO
Отформатируйте код через gofmt -w; -w записывает отформатированный файл. Соберите текущий пакет по явному пути к программе, чтобы новый терминал не зависел от скрытого PATH:
/usr/local/go/bin/gofmt -w lifecycle.go
/usr/local/go/bin/go build -o desk .
Синтаксическая ошибка указывает файл и строку. Сравните полный блок и при необходимости замените только lifecycle.go; не сбрасывайте VM. Проверка также собирает вашу неизменённую функцию в изолированных временных средах и меняет реальные длительности загрузок и порядок завершения. Проверяется как нормальный успех, так и ошибка длинного ответа; отведите около минуты. Проверка не сигнализирует работающему пункту и не меняет код.
Сохраните загрузку и закройте слушатель
На этом шаге сохраните принятую настоящую загрузку, отказывая новым соединениям и ожидая исходный сервер.
Переключитесь в Server, войдите в рабочую папку и запустите новую наблюдаемую попытку. Терминал занят до окончания исходного процесса:
cd /home/labex/project/download-desk
python3 desk.py serve drained
Дождитесь реального сообщения готовности и перейдите в Downloads. Перед следующей конечной медленной передачей прочитайте весь шаг, включая ожидаемую ошибку пробы и восстановление. Затем выполняйте блоки последовательно; не изучайте новый материал в окне передачи. Как раньше, запустите новую конечную медленную передачу в фоне. 300ms между записями по 32 байта дают обычное время на чтение, а не бесконечное ожидание сигнала.
python3 desk.py download drained completed north-pier.txt 300 & download_job=$!
python3 desk.py accepted drained completed
До остановки наблюдатель читает настоящий принятый запрос. Получите и проверьте идентичность исходного собранного процесса, затем отправьте SIGTERM из этого терминала:
server_pid=$(python3 desk.py pid drained)
python3 desk.py identity drained
kill -TERM "$server_pid"
Пока принятая загрузка идёт, проверьте новое соединение. Подготовленная проба делает ограниченные реальные попытки до двух секунд, сохраняя каждый нативный результат до отказа. Это даёт Go обработать доставленный сигнал до первого наблюдаемого сбоя; проба не предполагает синхронного закрытия слушателя командой kill. Следующая команда должна вернуть 7 с сообщением об отказе в соединении:
python3 desk.py probe drained
Это отказ создать новое соединение, а не HTTP 403 и не повреждение принятой передачи. Исходная загрузка должна продолжаться. Дождитесь её фонового задания и сравните реальные сохранённые байты с оригиналом:
wait "$download_job"
cmp documents/north-pier.txt downloads/completed.txt
cat downloads/completed.txt
Ожидаются нативный curl 0, отсутствие вывода cmp и полная последняя глава. Перейдите в Server: теперь должно быть Original server native exit: 0. Сервер может закончить отправку до того, как клиент закончит чтение или сброс буфера в файл; выключение не ждёт подтверждения клиентского приложения. Это отдельные наблюдения, а не обязательная очерёдность сообщений терминалов.
Вернитесь в Downloads. Сохраните lifecycle.go, полную загрузку и нативные наблюдения. Проверка читает этот фактический результат и испытывает тот же код на изолированных случайных портах с существующим неактивным соединением и принятыми запросами в разном порядке. Прямой Close, возврат main при ещё работающем Shutdown, открытый до окончания работы слушатель или сон на угаданное время не выполняют контракт.
Если загрузка завершилась до accepted или остановки, сохраните файл и выберите новую метку на ещё работающей попытке. Если сервер уже вышел, явно запустите новую попытку в Server и используйте её имя для новой медленной загрузки и команд identity, accepted и probe. Отказ пробы только после выхода процесса не показывает раннего закрытия слушателя; повторите наблюдаемый активный путь с новыми именами. Не перезаписывайте прежний успешный файл и не сбрасывайте историю.
Для полного повтора после завершения предыдущей попытки используйте Server:
python3 desk.py serve drained-retry
После сообщения готовности используйте Downloads. Сначала прочитайте все блоки. Новые имена сохраняют прежние файлы и наблюдения:
python3 desk.py download drained-retry completed-retry north-pier.txt 300 & download_job=$!
python3 desk.py accepted drained-retry completed-retry
server_pid=$(python3 desk.py pid drained-retry)
python3 desk.py identity drained-retry
kill -TERM "$server_pid"
Следующая проба должна вернуть 7:
python3 desk.py probe drained-retry
wait "$download_job"
cmp documents/north-pier.txt downloads/completed-retry.txt
Ожидаются curl 0, одинаковые полные байты и исходное нативное завершение0 в Server. Если старый сервер ещё работает, дочитайте инструкции перед новой медленной загрузкой на этой попытке; используйте новую метку в download и accepted, сохраняя имя попытки для identity и probe. Если прежняя проба уже существует, сохраните её, явно остановите собственную старую попытку и дайте ей завершиться перед полным новым повтором выше.
Настоящая ошибка запуска появляется в имеющемся stderr сервера и возвращает ненулевой статус; исправьте описанную ошибку кода/сборки или дождитесь конца своей предыдущей попытки перед повторным использованием 8080. Не завершайте одноимённый процесс платформы. Ошибка срока выключения означает, что конечный бюджет не подтвердил завершения; честно прочитайте её, не подменяя процесс и не копируя старый документ.
Резюме
Вы согласовали слушатель, принятые HTTP-обработчики, конечный контекст выключения и main. Исходный процесс прекратил принимать соединения, завершая существующую работу. Точные итоговые байты и нативные результаты сделали это наблюдаемым.



