Espere o servidor concluir o trabalho

GolangBeginner
Pratique Agora

Introdução

O balcão precisa fechar sem cortar a transferência de quem já recebe as notas de campo. Você substituirá o fechamento imediato de conexões pela coordenação comum de encerramento HTTP de Go e conservará um download real completo do processo original.

A unidade começa independentemente com o handler preparado e os mesmos dois documentos. São necessárias as observações de PID exato e requisição aceita da unidade anterior, ou experiência equivalente, além de handlers, erros, goroutines e canais. Entrega de sinais, contextos finitos, reconhecimento de erros e a responsabilidade de main esperar são explicados abaixo. O assunto é HTTP comum, não WebSockets, troca distribuída de tráfego ou garantia de indisponibilidade zero em produção. Reserve cerca de 60 minutos; é uma estimativa do autor.

Faça main esperar pelo encerramento

Nesta etapa, implemente uma coordenação finita de encerramento preservando o handler HTTP preparado.

Comece em Downloads e entre no espaço de trabalho independente:

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

Você só pode alterar lifecycle.go. Preserve o handler de main.go, ambos os documentos, go.mod, os observadores preparados e suas observações. A função atual captura SIGTERM, mas chama Close, interrompendo conexões ativas. Tratar um sinal, sozinho, não torna o encerramento gracioso.

Um http.Server controla o listener e as conexões HTTP ativas. ListenAndServe bloqueia enquanto aceita trabalho. Shutdown primeiro fecha listeners e conexões ociosas existentes e depois espera as requisições ativas comuns ficarem ociosas. Iniciar o encerramento faz ListenAndServe retornar imediatamente http.ErrServerClosed, embora handlers aceitos ainda possam estar executando. Se main retornar nesse momento, todas as goroutines desaparecem e a transferência é cortada. Portanto, o caminho principal deve esperar o próprio Shutdown retornar.

Um contexto carrega cancelamento ou um orçamento de prazo. context.Background() fornece o contexto inicial. context.WithTimeout(..., 45*time.Second) cria um orçamento finito de 45 segundos e uma função cancel; defer cancel() libera os recursos associados quando a função retorna, mesmo se o trabalho acabar antes. Cancelamento não comprova conclusão. O máximo público é 60 segundos após o pedido de parada: se uma resposta aceita não terminar no orçamento escolhido, informe um erro e saia com status diferente de zero dentro desse máximo. A referência usa 45 segundos; orçamentos finitos equivalentes de até 60 segundos são permitidos. A verificação inclui uma resposta longa finita que não consegue terminar em 60 segundos; usar apenas context.Background() não basta. Nesse teste isolado de prazo, transferência parcial e saída não zero informada pelo servidor são o resultado correto de falha, não a conclusão bem-sucedida de todo o trabalho. Se o orçamento vencer, Shutdown retorna erro; nossa função o retorna e main sai com status não zero. Não chamamos isso de conclusão bem-sucedida nem reiniciamos o servidor silenciosamente.

signal.Notify(stopped, syscall.SIGTERM) pede a Go que envie o sinal a um canal. Seu buffer tem capacidade 1 porque a entrega de sinal não deve esperar por um receptor; uma solicitação de parada enfileirada basta aqui. signal.Stop(stopped) cancela o registro desse canal ao terminar a função; isso é diferente de parar o servidor HTTP. Da mesma forma, o canal de resultado com buffer permite à goroutine servidora informar seu único resultado enquanto main encerra o serviço.

O primeiro select espera um erro de serviço ou sinal de parada. Assim, um erro de associação à porta ou de início não fica escondido para sempre atrás da espera por sinal. Após um encerramento bem-sucedido, errors.Is(err, http.ErrServerClosed) reconhece o resultado esperado do serviço. Outro erro é retornado, em vez de ser chamado de sucesso. main chama runServer; fazê-la retornar somente depois do encerramento é o que mantém o processo vivo.

Substitua este único arquivo. O delimitador entre aspas do here-document faz o shell escrever literalmente o código Go até o GO final em sua própria linha; > substitui somente 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

Formate o código com gofmt -w; -w grava o arquivo formatado. Compile o pacote atual usando o caminho explícito do executável para que um terminal novo não dependa de um PATH oculto:

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

Um erro de sintaxe indica arquivo e linha. Compare o bloco completo e substitua apenas lifecycle.go se necessário; não reinicie a VM. A verificação também compila sua função sem modificações em ambientes temporários isolados e varia durações reais dos downloads e ordens de conclusão. Testa tanto a falha da resposta longa quanto o sucesso normal; espere cerca de um minuto. Não envia sinal ao balcão em execução nem modifica seu código.

Preserve o download e feche o listener

Nesta etapa, preserve um download real aceito enquanto recusa novas conexões e espera o servidor original.

Mude para Server, entre no espaço de trabalho e inicie uma nova tentativa observada. O terminal permanece ocupado até o processo original terminar:

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

Aguarde a mensagem real de prontidão e mude para Downloads. Leia toda a etapa, incluindo a falha esperada da sonda e a recuperação, antes de iniciar a transferência lenta finita seguinte. Depois execute os blocos em sequência; não aprenda um novo conceito durante a janela de transferência. Inicie um novo download lento finito em segundo plano, como antes. Os 300ms entre gravações de 32 bytes deixam tempo normal para leitura; não são uma espera ilimitada pelo sinal.

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

O observador lê a requisição real aceita antes de qualquer parada. Obtenha e inspecione a identidade do processo original compilado e envie SIGTERM deste terminal:

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

Enquanto a transferência aceita continua, teste uma conexão nova. A sonda preparada faz tentativas reais limitadas por até dois segundos, preservando cada resultado nativo até surgir uma recusa. Isso deixa Go tratar o sinal entregue antes da primeira falha observada; a sonda não presume que kill feche o listener de forma síncrona. O próximo comando deve retornar 7, com mensagem de conexão recusada:

python3 desk.py probe drained

É uma recusa para criar uma nova conexão, não um HTTP 403 nem uma transferência aceita danificada. O download original deve continuar. Espere seu trabalho em segundo plano e compare os bytes reais salvos com o original:

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

Espere curl nativo 0, nenhuma saída de cmp e o capítulo final completo. Mude para Server: agora deve informar Original server native exit: 0. O servidor pode terminar de enviar antes que o cliente termine de ler ou descarregar seu buffer no arquivo; o encerramento não espera confirmação da aplicação cliente. São observações distintas, não uma ordem obrigatória entre duas mensagens de terminal.

Volte a Downloads. Preserve lifecycle.go, o download completo e as observações nativas. A verificação lê esse resultado real e testa o mesmo código em portas aleatórias isoladas, com uma conexão ociosa existente e requisições aceitas em ordens diferentes. Close direto, retorno de main durante Shutdown, listener aberto até o trabalho terminar ou dormir por um intervalo adivinhado não cumprem o contrato.

Se o download terminou antes de accepted ou da parada, preserve o arquivo e escolha novo rótulo na tentativa ainda em execução. Se o servidor já saiu, inicie explicitamente outra tentativa em Server e use o nome na nova saída lenta e nos comandos identity, accepted e probe. Uma sonda recusada só depois da saída não demonstra fechamento antecipado do listener; repita o fluxo ativo observado com nomes novos. Nunca sobrescreva o arquivo anterior bem-sucedido nem reinicie o histórico.

Para repetir o fluxo completo depois que a tentativa anterior sair, use Server:

python3 desk.py serve drained-retry

Após a mensagem de prontidão, use Downloads. Leia todos os blocos primeiro. Os nomes novos preservam os arquivos e observações 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"

A próxima sonda deve retornar 7:

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

Espere curl 0, bytes completos iguais e saída nativa original0 em Server. Se o servidor anterior ainda funcionar, termine de ler antes de iniciar nova saída lenta nessa tentativa; use novo rótulo em download e accepted, mantendo o nome da tentativa para identity e probe. Se uma sonda anterior já existir, preserve-a, pare explicitamente sua própria tentativa antiga e deixe-a sair antes da repetição completa com nova tentativa acima.

Um erro real de início aparece no stderr existente do servidor e retorna status não zero; corrija um erro de código/compilação já explicado ou espere sua tentativa anterior sair antes de reutilizar 8080. Não mate um processo da plataforma com o mesmo nome. Um erro de prazo de encerramento significa que o orçamento finito não confirmou conclusão; leia-o honestamente em vez de substituir o processo ou copiar um documento anterior.

Resumo

Você coordenou o listener, os handlers HTTP aceitos, um contexto finito de encerramento e main. O processo original deixou de aceitar conexões enquanto terminava o trabalho existente. Bytes finais exatos e resultados nativos tornaram o resultado observável.