Recover przy tworzeniu nowej goroutine

0

Cześć,

myślę, że dość proste pytanie, ale chciałem potwierdzić czy dobrze myślę. Może opowiecie czy robicie podobnie, czy spotkaliście z tym itd. 

No więc sytuacja wygląda tak, że jak tworzymy w Go serwer http, to większość bibliotek/paczek, które to ułatwiają od razu wołają dla nas recover tak, aby serwer nie padł jeśli coś pójdzie nie tak. Przykładowo:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	// Create a new Gin router
	router := gin.Default()

	// Define a route with a handler function
	router.GET("/hello", func(c *gin.Context) {
		panic("oops, something went wrong")
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, Gin!",
		})
	})

	// Run the server on port 8090
	router.Run(":8090")
}


Jeśli w kodzie wystąpi panic, to gin posiada middleware, który “złapie” nam tęgo panica i serwer nie padnie. Ma to sens. Nie chcemy, żeby na produkcji popadały nam serwisy przez błąd w kodzie (który w końcu komuś się przytrafi).

Sytuacja lekko się komplikuje gdy w kodzie stworzymy nową goroutine. Jest dość częsty przypadek. Dla przykładu mój serwis żeby zrealizować zapytanie musi wysłać zapytanie do dwóch innych serwisów. Żeby trwało to szybciej niż wolniej, to mogę zawołać to na dwóch osobnych goroutine i poczekać, aż pozostałe dwa serwisy zwrócą odpowiedź (zamiast robić to sekwencyjnie i czekać na odpowiedź po kolei).
Jednak, w tym przypadku domyślny panic recover middleware (albo nie domyślny, ale taki, który sami sobie napiszemy) już nie “złapie” potencjalnego panica. Wypadałoby więc przy wołaniu nowej gorutyny samemu łapać panica wołając “recover”. W innym wypadku możemy mieć w kodzie błąd, który może potencjalnie położyć wszystkie nasze serwisy. 


Na postawie przykładu powyżej, kod bez dodatkowego recovery, ale z tworzeniem nowej gorotuine:


package main

import (
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

func main() {
	// Create a new Gin router
	router := gin.Default()

	// Define a route with a handler function
	router.GET("/hello", func(c *gin.Context) {
		wg := &sync.WaitGroup{}

		wg.Add(1)

		go func() {
			defer wg.Done()
			panic("oops, something went wrong")
		}()

		wg.Wait()

		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, Gin!",
		})
	})

	// Run the server on port 8090
	router.Run(":8090")
}


W tym przypadku, jeśli zawołam endpoint 'hello' to serwis padnie tzn. przestanie działać. Jeśli jest uruchomiony w kontenerze w jakimś kubernetesie albo czymś podobnym, to zostanie uruchomiony ponownie.

Kod z recovery, który zapobiegnie padnięciu serwera:


package main

import (
	"fmt"
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

func main() {
	// Create a new Gin router
	router := gin.Default()

	// Define a route with a handler function
	router.GET("/hello", func(c *gin.Context) {
		wg := &sync.WaitGroup{}

		wg.Add(1)

		go func() {
			defer func() {
				if r := recover(); r != nil {
					fmt.Println("Recovered on another goroutine")
				}
			}()

			defer wg.Done()
			panic("oops, something went wrong")
		}()

		wg.Wait()

		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, Gin!",
		})
	})

	// Run the server on port 8090
	router.Run(":8090")
}



Pytanie: czy dobrze rozumuje? Czy tak robicie/piszecie/pamiętacie o tym?

0

To nie jest pytanie o ten konkretny framework, to był tylko przykład. Co konkretnie chcesz wskazać w tej dokumentacji?

0

r.Use(gin.Recovery()) to jest middleware. Zadziała wszędzie gdzie jest jakiś flow/pipeline. Użyłeś gin więc ci dałem przykład z gin. Ale jak będziesz chciał użyć gorilli, chi, czy gołego go, ten koncept zadziała.

0

@Dregorio:

Wciąż nie wiem dlaczego wskazujesz coś w dokumentacji. W pierwszym poście starałem się opisać w prosty sposób jak to działa i dlaczego.
Zgadzam się, że jest to realizowane przez używanie middelware we wszystkich frameworkach, które to obsługują. Może nie zbyt dokładnie to określiłem w pierwszym poście.
Dziękuję za uwagę mimo wszystko.

Przechodząc jednak do oryginalnego pytania, które chciałem zadać.
Czy zawsze jak tworzysz nową gorutynę, to dodajesz recover w celu obsługi potencjalnego panica, który może wystąpić? (middleware go nie złapie, uruchom przykład drugi z pierwszego posta jeśli chciałbyś sprawdzić)
Czy inni tak robią?
Czy jest to standardowa praktyka dla Ciebie lub firmy w której pracujesz?
Czy zwracasz na to przy robieniu code review?
Czy osoby z którymi pracujesz zwracają na to uwagę przy robieniu code review?

Jeszcze raz podkreślam, chodzi o obsługiwanie paniców, gdy uruchamiamy kod na nowej goroutynie. Wtedy domyślne "panic handlery" z frameworków nie zadziałają.

0

W golangu, każdy handler to osobna goroutine. Wszystkie framoworki/biblioteki koniec końców i tak korzystają z rozwiązań standardowej biblioteki więc mogę tak powiedzieć. No więc jak odpalisz GET /hello szybko kilka razy, to odpalą się osobne goroutines. Ale jeśli bardzo chcesz goroutin odpalać w gorouine, to wyjaśnienie:
Panic z bieżącego procesu może być przechwycony, więc jeśli odpalasz goroutine w goroutine, to trzeba już go osobno łapać, tak jak zrobiłeś w ostatnim przykładzie. Nigdy mi nie było to potrzebne. Jeśli mierzi cię, że musisz powtarzać kod, możesz zawsze wyciągnąć to do jakiejś funkcji, ja tak robię z obsługą errorów.

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"sync"
)

func guard(f func()) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered on another goroutine")
		}
	}()
	f()
}

func main() {
	router := gin.Default()
	router.GET("/hello", func(c *gin.Context) {
		wg := &sync.WaitGroup{}
		wg.Add(1)
		go guard(func() {
			defer wg.Done()
			panic("oops, something went wrong")
		})
		wg.Wait()
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, Gin!",
		})
	})
	router.Run(":8090")
}
0

Nigdy mi nie było to potrzebne

No mi w zasadzie też nie było potrzebne per se.
Ale wyobrażam sobie, że wszystko co wołam na nowej gorutynie może zawierać bugi i potencjalnie może polecieć panic.
Może wrzucanie tych recoverów to jest zbyt defensywne programowanie. Z drugiej strony, czasem zdarzy się większa gafa i łatwo położyć całą produkcję.
@Dregorio: co sądzisz?

2

Tak, powinno się łapać, bo te goroutny istnieją tylko na użytek głównego wątku (z uwagi na .Wait()). Jest to duży problem. W przeszłości pisałem ręczne recovery. Teraz głównie używam https://github.com/sourcegraph/conc tam, gdzie normalnie używałbym wait groupy

0

Osobiście mam popodpinane lintery i masę testów. W mojej karierze pamiętam jeden panic na produkcji o 3 rano. Nie żebym pisał zajebisty kod :P Używanie wrapera, który porzuciłem raczej nie zaboli, a tylko pomoze. No i tak jak wyżej napisane. Teraz dzięki generykom powstało pełno fajnych bibliotek warto używać skoro są :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1