HTTP Request Life Cycle

NodeJS HTTP server

Tohle vsichni znate: HTTP server tim nejjednodussim zpusobem, jak to v NodeJS napsat. Kdyz teda opominu nejake skopiciny, ktera jsem do kodu doplnil, abych simuloval zatez na serveru aspon nejakym nahodne generovanym delayem a globalni pocitadlo pripojenych, obsluhovanych requestu:

const http = require("http");

const port = process.env.PORT || 8080;

function hardWorker(req, res, workerId, sleepTime, cb) {
    setTimeout(() =>; {
        res.end(`hello no ${workerId} from server\n`);
        cb();
    }, sleepTime * 1000);
}

let numOfWorkers = 0;

const server = http.createServer((req, res) => {
    const workerId = ++numOfWorkers;
    const sleepTime = Math.floor(Math.random() * (10 - 3 + 1) + 3)
    console.log(`welcome! ${workerId} (${sleepTime}s)`);
    hardWorker(req, res, workerId, sleepTime, () => {
        console.log(`done ${workerId}`);
    });
});

server.listen(port, () => {
    console.log(`server is running: http://localhost:${port}`);
});

Vysledek ja jasny a ocekavatelny. Na server prijde request, ten spusti handler s obsluhou, v mem pripade jen pocka N sekund a pak vrati klientovi odpoved:

Posles request, server je prijme a zacne zpracovavat, klient ceka na dokonceni zpracovani. Az ma server vysledek, posle jej na klienta a ten jej prijme. Konec requestu.

No jo! Co kdyz ale vybavovani requestu trva dele nez je klientovi libo?

A ten se rozhodne proste request cancelovat…. Klient je v pohode. Proste zrusi cekani na response a jde delat neco jineho. Ale na serveru se nic nemeni. Ten dal provadi obsluhy requestu, aby jej na konci zahodil….

A to neni neco, co byste chteli…

Predstavte si, ze mate API server, ktery je bezne vystavovan vysoke zatezi nejake webove aplikaci, ktere poskytuje nejkou backendovou funkcionalitu. V pripade divokeho posilani requestu se teoreticky docela jednoduse muzete dostat do situace, kdy k serveru uz neni nikod pripojeny, ale na serveru bezi treba i tisicovka procesu nastartovanych prochozimi requesty.

A je jasne, ze request muze konzumovat dalsi prostredky, jako je treba databazova konektivita a podobne…

A jeste vetsi je to sranda, pokud svou backend bezite v nejate te cloudove sluzbe a kazdy novy request vam treba spusti jejakou tu serveless funkci, nebo ja nevim co a zacne vam to uzirat penize z budgetu, aniz byste cokoliv dorucily klientovi….

Uplne idelani by bylo, kdyby server poznal, ze klient zahodil request a sam zrusil vse co s tim souvisi…

To zni i docela logicky a ocekavatelne. Ne? Ale presne tohle v NodeJS nejde. Jeho HTTP modul se chova presne tak, jak jsem popsal vyse. Proste s kazdym requestem nastartuje jeho zpracovani a maka tak dlouho, dokud jej kompletne nevyridi.

Heja! A presne tohle umi Golang 🙂

Go od verze 1.12 ma context coz je presne to co potrebujete. Context je prostredi, ktere vam mimo jine umi zprostredkovat prave canceludalost v ramci vaseho zpracovani.

Je to presne ten nastroj, ktery potrebujete k tomu, abyste na serveru dokazali vypropagovat zruseni obsluhy zpracovani requestu do vsech mist kodu, kde by se vam to mohlo hodit, nenbo kde to potrebujete.

Jinymi slovy: ano Go umi to, ze v pripade, ze klient zahodi, zrusi pripojeni, zastavi na serveru jeho zpracovani.

Genialni!

Je to presne to, co na serveru chcete. Nemrhat prostredky a pracovat efektivne jen kdyz je to potreba.

Server v Go pak muze vypadat takto

package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"time"
)

var numOfWorkers = 0

func hardWorker(w http.ResponseWriter, r *http.Request) {
	numOfWorkers++
	workerID := numOfWorkers
	sleepTime := rand.Intn(10-3) + 3

	ctx := r.Context()
	fmt.Printf("welcome! %d (%ds)\n", workerID, sleepTime)
	select {
	case <-time.After(time.Duration(sleepTime) * time.Second):
		w.Write([]byte(fmt.Sprintf("hello no %d from server\n", workerID)))
		fmt.Printf("done %d\n", workerID)
	case <-ctx.Done():
		fmt.Println(fmt.Sprintf("oups! request %d cancelled\n", workerID))
	}
}
func main() {
	var port string
	if port = os.Getenv("PORT"); port == "" {
		port = "8081"
	}
	rand.Seed(time.Now().UTC().UnixNano())
	fmt.Println("server is running: http://localhost:" + port)
	http.ListenAndServe(":"+port, http.HandlerFunc(hardWorker))
}

Co z toho vyplyva?

Je jedno v cem pisete sve serverove aplikace. Meli byste ale vzdy premyslet o tom, jak veci skutecne funguji. Muze se vam to hodit.