Espera a que el servidor termine su trabajo

GolangBeginner
Practicar Ahora

Introducción

El puesto debe cerrar sin cortar la transferencia de quien ya está recibiendo sus notas de campo. Sustituirás el cierre inmediato de conexiones por la coordinación de cierre HTTP ordinaria de Go y conservarás una descarga real completa del proceso original.

Esta unidad comienza independientemente con el manejador preparado y los mismos dos documentos. Necesitas las observaciones de PID preciso y petición aceptada de la unidad anterior, o experiencia equivalente, además de manejadores, errores, goroutines y canales. A continuación se explican la entrega de señales, los contextos finitos, la comparación de errores y la responsabilidad de espera de main. Se trata HTTP ordinario, no WebSockets, cambio de tráfico distribuido ni una garantía de cero interrupciones en producción. Reserva unos 60 minutos; es una estimación del autor.

Haz que main espere al cierre

En este paso implementarás una coordinación de cierre finita conservando el manejador HTTP preparado.

Empieza en Downloads y entra en el espacio de trabajo independiente:

cd /home/labex/project/download-desk
cat lifecycle.go

Solo puedes cambiar lifecycle.go. Conserva el manejador de main.go, ambos documentos, go.mod, los observadores preparados y sus observaciones. La función actual captura SIGTERM pero llama a Close, que interrumpe conexiones activas. Capturar una señal no basta para un cierre ordenado.

Un http.Server administra el listener y las conexiones HTTP activas. ListenAndServe bloquea mientras acepta trabajo. Shutdown cierra primero los listeners y las conexiones inactivas existentes; luego espera a que las peticiones ordinarias activas queden inactivas. Al empezar el cierre, ListenAndServe devuelve inmediatamente http.ErrServerClosed, aunque todavía haya manejadores aceptados en ejecución. Si main retorna entonces, desaparecen todas las goroutines y se corta la transferencia. Por eso la ruta principal debe esperar a que retorne el propio Shutdown.

Un contexto transporta cancelación o un presupuesto de tiempo límite. context.Background() proporciona el contexto inicial. context.WithTimeout(..., 45*time.Second) crea un presupuesto finito de cierre de 45 segundos y una función cancel; defer cancel() libera los recursos asociados al retornar la función, aunque el trabajo termine antes. Cancelar no demuestra haber completado. El máximo público es 60 segundos desde la petición de parada: si una respuesta aceptada no termina dentro del presupuesto elegido, informa del error y sal con estado no cero dentro de ese máximo. La referencia usa 45 segundos; se permiten presupuestos finitos equivalentes de hasta 60 segundos. La comprobación incluye una respuesta larga finita que no puede acabar en 60 segundos, de modo que cerrar solo con context.Background() es insuficiente. En esa prueba aislada de tiempo límite, una transferencia parcial y una salida no cero informada son el fallo correcto, no un vaciado satisfactorio. Si vence el presupuesto, Shutdown devuelve un error; nuestra función lo retorna y main sale con estado no cero. No debemos llamar a eso vaciado exitoso ni reiniciar silenciosamente el servidor.

signal.Notify(stopped, syscall.SIGTERM) pide a Go que envíe esa señal a un canal. Su búfer tiene capacidad 1 porque la entrega de señales no debe esperar a un receptor; aquí basta con una petición de parada en cola. signal.Stop(stopped) cancela el registro de este canal al finalizar la función; no equivale a detener el servidor HTTP. Del mismo modo, el canal de resultados con búfer permite que la goroutine servidora comunique su único resultado mientras main ejecuta el cierre.

El primer select espera un error del servicio o una señal de parada. Así, un error de enlace/inicio no queda oculto indefinidamente detrás de la espera de señal. Tras cerrar correctamente, errors.Is(err, http.ErrServerClosed) reconoce el resultado esperado del servicio. Un error diferente se devuelve, en vez de llamarlo éxito. main llama a runServer, de modo que retornar de esta función solo después del cierre mantiene vivo el proceso.

Sustituye este único archivo. El delimitador entre comillas del here-document hace que el shell escriba literalmente el código Go hasta el GO final en una línea independiente; > sustituye solamente 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

Formatea con gofmt -w; -w escribe el archivo formateado. Compila el paquete actual usando una ruta explícita al ejecutable, para que un terminal nuevo no dependa de una configuración PATH oculta:

/usr/local/go/bin/gofmt -w lifecycle.go
/usr/local/go/bin/go build -o desk .

Un error sintáctico indica archivo y línea. Compara el bloque completo y sustituye solo lifecycle.go si hace falta; no reinicies la VM. La comprobación también compila tu función sin modificar en entornos temporales aislados y varía duraciones reales y órdenes de finalización. Comprueba tanto el fallo de respuesta larga como el éxito normal; deja alrededor de un minuto para que termine. No envía señales a tu puesto activo ni modifica tu código.

Conserva la descarga y cierra el listener

En este paso conservarás una descarga real aceptada mientras rechazas conexiones nuevas y esperas al servidor original.

Cambia a Server, entra en el espacio de trabajo e inicia un intento observado nuevo. Este terminal permanece ocupado hasta que termina el proceso original:

cd /home/labex/project/download-desk
python3 desk.py serve drained

Espera el mensaje real de disponibilidad y cambia a Downloads. Lee todo el paso, incluidos el fallo esperado de la sonda y la recuperación, antes de iniciar la transferencia lenta finita siguiente. Ejecuta luego los bloques en secuencia; no uses la ventana de transferencia para aprender otro concepto. Inicia en segundo plano una nueva transferencia lenta finita, igual que en la unidad anterior. Los 300ms entre escrituras de 32 bytes dejan tiempo normal de lectura; no son una espera ilimitada de señal.

python3 desk.py download drained completed north-pier.txt 300 & download_job=$!
python3 desk.py accepted drained completed

El observador lee la petición real aceptada antes de que detengas nada. Obtén e inspecciona la identidad del proceso original compilado y envía SIGTERM desde este terminal:

server_pid=$(python3 desk.py pid drained)
python3 desk.py identity drained
kill -TERM "$server_pid"

Mientras sigue la descarga aceptada, prueba una conexión nueva. La sonda preparada realiza intentos reales acotados durante un máximo de dos segundos y conserva cada resultado nativo hasta observar un rechazo. Esto permite que Go procese la señal entregada antes del primer fallo observado; la sonda no presupone que kill cierre el listener síncronamente. El siguiente comando debe devolver 7, con un mensaje de conexión rechazada:

python3 desk.py probe drained

Es un rechazo al crear una conexión nueva, no un HTTP 403 ni una transferencia aceptada dañada. La descarga original debe continuar. Espera a su trabajo en segundo plano y compara sus bytes guardados reales con el original:

wait "$download_job"
cmp documents/north-pier.txt downloads/completed.txt
cat downloads/completed.txt

Espera curl nativo 0, ninguna salida de cmp y el capítulo final completo. Cambia a Server: ahora debe informar Original server native exit: 0. El servidor puede terminar de enviar antes de que el cliente termine de leer o vaciar su archivo; el cierre no espera confirmación de la aplicación cliente. Son observaciones distintas, no un orden obligatorio entre dos mensajes de terminal.

Vuelve a Downloads. Conserva lifecycle.go, la descarga completa y las observaciones nativas. La comprobación lee este resultado real y prueba el mismo código en puertos aleatorios aislados, con una conexión inactiva existente y peticiones aceptadas en órdenes diferentes. Close directo, retornar de main mientras aún corre Shutdown, mantener abierto el listener hasta terminar el trabajo o dormir un intervalo adivinado no cumplen el contrato.

Si la descarga terminó antes de accepted o de la parada, conserva su archivo y elige otra etiqueta en el intento aún activo. Si el servidor ya salió, inicia explícitamente otro nombre de intento en Server y úsalo para la salida lenta nueva y los comandos identity, accepted y probe. Una sonda rechazada solo después de salir el proceso no demuestra cierre temprano del listener; repite el flujo activo observado con nombres nuevos. Nunca sobrescribas el archivo exitoso anterior ni reinicies el historial de observación.

Para repetir todo cuando el intento anterior ya salió, usa Server:

python3 desk.py serve drained-retry

Después del mensaje de disponibilidad, usa Downloads. Lee primero todos los bloques. Los nombres nuevos conservan los archivos y observaciones anteriores:

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"

La sonda siguiente debe devolver 7:

python3 desk.py probe drained-retry
wait "$download_job"
cmp documents/north-pier.txt downloads/completed-retry.txt

Espera curl 0, bytes completos iguales y salida nativa original0 en Server. Si el servidor anterior aún corre, termina de leer antes de iniciar otra salida lenta en ese intento; usa una etiqueta nueva tanto en download como en accepted, conservando el nombre del intento para identity y probe. Si ya existe una sonda anterior, consérvala, detén explícitamente tu propio intento antiguo y deja que salga antes de la repetición completa con intento nuevo indicada arriba.

Un error real de inicio aparece en el stderr existente del servidor y devuelve un estado no cero; corrige un error de código/compilación ya explicado o espera a que salga tu intento anterior antes de reutilizar 8080. No mates un proceso de la plataforma que tenga el mismo nombre. Un error de plazo de cierre indica que el presupuesto finito no acreditó la finalización; interprétalo con honestidad en vez de sustituir el proceso o copiar un documento anterior.

Resumen

Coordinaste el listener, los manejadores HTTP aceptados, un contexto finito de cierre y main. El proceso original dejó de aceptar conexiones mientras terminaba el trabajo existente. Los bytes finales exactos y los resultados nativos hicieron observable el resultado.