Attendez la fin du travail du serveur

GolangBeginner
Pratiquer maintenant

Introduction

Le comptoir doit fermer sans couper le transfert de la personne qui reçoit déjà ses notes de terrain. Vous remplacerez la fermeture immédiate des connexions par la coordination d'arrêt HTTP ordinaire de Go, puis conserverez un téléchargement réel complet issu du processus d'origine.

Cette unité démarre indépendamment avec le gestionnaire préparé et les mêmes deux documents. Il faut connaître les observations de PID précis et de requête acceptée de l'unité précédente, ou avoir une expérience équivalente, ainsi que les gestionnaires, erreurs, goroutines et canaux. La livraison des signaux, les contextes finis, la reconnaissance d'erreurs et la responsabilité d'attente de main sont expliqués ci-dessous. Il s'agit de HTTP ordinaire, pas de WebSockets, de bascule de trafic distribuée ou d'une garantie de zéro interruption en production. Prévoyez environ 60 minutes ; c'est une estimation de l'auteur.

Faites attendre main jusqu'à la fin de l'arrêt

Dans cette étape, implémentez une coordination d'arrêt finie tout en conservant le gestionnaire HTTP préparé.

Commencez dans Downloads et entrez dans l'espace indépendant :

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

Seul lifecycle.go peut être modifié. Conservez le gestionnaire de main.go, les deux documents, go.mod, les observateurs et leurs observations. La fonction actuelle capture SIGTERM mais appelle Close, qui interrompt les connexions actives. Gérer un signal ne suffit pas à rendre l'arrêt progressif.

Un http.Server possède le listener et les connexions HTTP actives. ListenAndServe bloque tout en acceptant du travail. Shutdown ferme d'abord les listeners et les connexions inactives existantes, puis attend que les requêtes ordinaires actives deviennent inactives. Commencer l'arrêt fait immédiatement retourner http.ErrServerClosed à ListenAndServe, alors que les gestionnaires acceptés peuvent encore tourner. Si main retourne à cet instant, toutes les goroutines disparaissent et le transfert est coupé. Le chemin principal doit donc attendre le retour de Shutdown lui-même.

Un contexte porte une annulation ou une limite de temps. context.Background() fournit le contexte initial. context.WithTimeout(..., 45*time.Second) crée un budget d'arrêt fini de 45 secondes et une fonction cancel ; defer cancel() libère les ressources associées au retour de la fonction, même si le travail finit plus tôt. Annuler ne prouve pas l'achèvement. Le maximum public est de 60 secondes après la demande d'arrêt : si une réponse acceptée ne finit pas dans le budget choisi, signalez une erreur et sortez avec un statut non nul avant ce maximum. La référence utilise 45 secondes ; des budgets finis équivalents allant jusqu'à 60 secondes sont autorisés. La vérification inclut une réponse longue mais finie qui ne peut finir en 60 secondes ; un arrêt utilisant seulement context.Background() ne suffit donc pas. Dans cet essai isolé d'expiration, un transfert partiel et une sortie non nulle signalée sont le résultat d'échec correct, pas un drainage réussi. Si le budget expire, Shutdown retourne une erreur ; notre fonction la retourne et main sort avec un statut non nul. Ne présentez pas ce cas comme un drainage réussi et ne redémarrez pas silencieusement le serveur.

signal.Notify(stopped, syscall.SIGTERM) demande à Go d'envoyer ce signal sur un canal. Sa capacité tampon est 1, car la livraison du signal ne doit pas attendre un récepteur ; une demande d'arrêt en attente suffit ici. signal.Stop(stopped) annule l'enregistrement de ce canal à la fin de la fonction ; cela diffère de l'arrêt du serveur HTTP. De même, le canal de résultat avec tampon permet à la goroutine de service d'envoyer son unique résultat pendant que main effectue l'arrêt.

Le premier select attend une erreur de service ou un signal d'arrêt. Cela évite aussi de masquer indéfiniment une erreur de liaison/démarrage derrière l'attente du signal. Après un arrêt réussi, errors.Is(err, http.ErrServerClosed) reconnaît le résultat attendu du service. Toute autre erreur est retournée au lieu d'être qualifiée de réussite. main appelle runServer ; son retour seulement après l'arrêt maintient donc le processus en vie.

Remplacez cet unique fichier. Le délimiteur entre guillemets du here-document demande au shell d'écrire littéralement le code Go jusqu'au GO final sur sa propre ligne ; > remplace uniquement 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

Formatez avec gofmt -w ; -w écrit le fichier formaté. Compilez le paquet courant via un chemin d'exécutable explicite pour qu'un nouveau terminal ne dépende pas d'un PATH caché :

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

Une erreur de syntaxe indique fichier et ligne. Comparez le bloc complet et ne remplacez que lifecycle.go si nécessaire ; ne réinitialisez pas la VM. La vérification compile aussi votre fonction inchangée dans des environnements temporaires isolés et varie les durées réelles de téléchargement ainsi que l'ordre de fin. Elle teste le cas d'échec long et la réussite normale ; prévoyez environ une minute. Elle ne signale pas votre comptoir en cours d'exécution et ne modifie pas votre code.

Conservez le téléchargement et fermez le listener

Dans cette étape, conservez un téléchargement réel accepté tout en refusant les nouvelles connexions et en attendant le serveur d'origine.

Passez à Server, entrez dans l'espace et lancez un nouvel essai observé. Le terminal reste occupé jusqu'à la fin du processus d'origine :

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

Attendez son message réel de disponibilité, puis passez à Downloads. Lisez toute l'étape, y compris l'échec attendu de la sonde et la récupération, avant de lancer le transfert lent fini suivant. Exécutez ensuite les blocs dans l'ordre ; n'utilisez pas cette fenêtre pour apprendre un nouveau concept. Lancez le nouveau transfert lent fini en arrière-plan, comme dans l'unité précédente. Les 300ms entre écritures de 32 octets laissent un temps de lecture normal ; ce n'est pas une attente illimitée de signal.

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

L'observateur lit la requête réelle acceptée avant tout arrêt. Obtenez et inspectez l'identité du processus compilé d'origine, puis envoyez SIGTERM depuis ce terminal :

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

Pendant le téléchargement accepté, testez une nouvelle connexion. La sonde préparée effectue des tentatives réelles bornées pendant au plus deux secondes, en conservant chaque résultat natif jusqu'au refus. Go peut ainsi traiter le signal livré avant le premier échec observé ; la sonde ne suppose pas que kill ferme le listener de manière synchrone. La commande suivante doit renvoyer 7, avec un message de connexion refusée :

python3 desk.py probe drained

Il s'agit du refus de créer une nouvelle connexion, pas d'un HTTP 403 ou d'un transfert accepté endommagé. Le téléchargement d'origine doit continuer. Attendez son travail en arrière-plan et comparez les octets réels sauvegardés à l'original :

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

Attendez curl natif 0, aucune sortie de cmp et le dernier chapitre complet. Passez à Server : il doit maintenant afficher Original server native exit: 0. Le serveur peut avoir fini d'envoyer avant que le client finisse de lire ou de vider ses tampons vers le fichier ; l'arrêt n'attend pas un accusé de réception de l'application cliente. Ce sont des observations séparées, sans ordre imposé entre deux messages de terminal.

Revenez à Downloads. Conservez lifecycle.go, le téléchargement complet et les observations natives. La vérification lit ce résultat réel et teste le même code sur des ports aléatoires isolés avec une connexion inactive existante et des requêtes acceptées dans différents ordres. Un Close direct, le retour de main pendant que Shutdown fonctionne, un listener laissé ouvert jusqu'à la fin du travail ou une durée de sommeil devinée ne respectent pas le contrat.

Si le téléchargement a fini avant accepted ou l'arrêt, conservez son fichier et choisissez une nouvelle étiquette sur l'essai encore actif. Si le serveur est déjà sorti, lancez explicitement un autre nom d'essai dans Server et utilisez-le pour la nouvelle sortie lente ainsi que les commandes identity, accepted et probe. Un refus seulement après la sortie ne prouve pas une fermeture précoce du listener ; reprenez le parcours actif observé avec de nouveaux noms. N'écrasez jamais l'ancien fichier réussi et ne réinitialisez pas l'historique.

Pour une reprise complète après la sortie de l'essai précédent, utilisez Server :

python3 desk.py serve drained-retry

Après le message de disponibilité, utilisez Downloads. Lisez d'abord tous les blocs. Ces noms neufs conservent les fichiers et observations précédents :

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 sonde suivante doit renvoyer 7 :

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

Attendez curl 0, des octets complets égaux et la sortie native d'origine0 dans Server. Si l'ancien serveur tourne encore, finissez de lire avant de lancer une nouvelle sortie lente sur cet essai ; utilisez une nouvelle étiquette dans download et accepted, en gardant le nom d'essai pour identity et probe. Si une sonde antérieure existe déjà, conservez-la, arrêtez explicitement votre propre ancien essai et laissez-le sortir avant la reprise complète avec nouvel essai ci-dessus.

Une véritable erreur de démarrage apparaît dans le stderr existant du serveur et retourne un statut non nul ; corrigez une erreur de code/compilation expliquée ou attendez la sortie de votre ancien essai avant de réutiliser 8080. Ne tuez pas un processus de plateforme de même nom. Une erreur de délai d'arrêt signifie que le budget fini n'a pas établi l'achèvement ; lisez-la honnêtement plutôt que de remplacer le processus ou copier un ancien document.

Résumé

Vous avez coordonné le listener, les gestionnaires HTTP acceptés, un contexte d'arrêt fini et main. Le processus d'origine a cessé d'accepter des connexions tout en terminant son travail existant. Les octets finaux exacts et les résultats natifs ont rendu ce résultat observable.