Maneras de implementar timeouts en Go


Fallar rápido y no bloquear son conceptos muy importantes cuando implementamos arquitecturas distribuidas. Evita malgastar recursos y tener fallos en cascada que impactan todo el sistema. En un sistema robusto todo debería estar limitado: llamados externos, operaciones, número de recursos que se pueden crear, etc…

Poner un tiempo máximo para las operaciones es entonces una de las cosas más importantes a tener en cuenta cuando estamos desarrollando y desafortunadamente es, a menudo, olvidado.

En Go hay diferentes formas de implementar timeouts, en este artículo vamos a explorar 3 problemas y cómo resolverlos.

Problema 1: Ejecutar operación hasta obtener respuesta o timeout

Cuando queremos un resultado único de una operación pero necesitamos limitar el tiempo de espera podemos implementar el siguiente patrón usando canales y un timer:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string, 1)
	defer close(ch)

	go func() {
		time.Sleep(2 * time.Second)
		ch <- "result!"
	}()

	select {
	case res := <-ch:
		fmt.Println(res)
	case <-time.After(1 * time.Second):
		fmt.Println("Timed out")
	}
}

ejecutar

Problema 2: Procesar lista hasta que se cumpla un timeout sin perder el progreso

A veces tenemos una lista que necesitamos procesar hasta terminarla o hasta que se cumpla un tiempo máximo de procesamiento. Implementar este patrón en Go es bastante fácil:

package main

import (
	"fmt"
	"time"
)

func main() {
	timeoutCh := time.After(3 * time.Second)

loop:
	for i := 0; i < 10; i++ {
		select {
		default:
			fmt.Println(i)
			time.Sleep(1 * time.Second)
		case <-timeoutCh:
			fmt.Println("Timed out")
			break loop
		}
	}
}

ejecutar

Problema 3: Cancelar múltiples operaciones concurrentes

Cuando tenemos procesos que se van creando en diferentes puntos del código o del lifecycle de la aplicación podemos usar el paquete context para cancelar todo desde un solo punto.

En el siguiente ejemplo op1 llama a op2 y op2 llama a op3, todos usan el mismo contexto con un timeout de 1 segundo.

Cuando el tiempo se cumple todas las operaciones son canceladas.

package main

import (
	"context"
	"fmt"
	"time"
)

func op1(ctx context.Context, ch chan<- string) {
	ch2 := make(chan string, 1)
	go op2(ctx, ch2)

	select {
	case res := <-ch2:
		fmt.Println("got:", res)
		ch <- "op1 result"
	case <-ctx.Done():
		fmt.Println("op2 timed out", ctx.Err())
	}
}

func op2(ctx context.Context, ch chan<- string) {
	ch3 := make(chan string, 1)
	go op3(ctx, ch3)

	select {
	case res := <-ch3:
		fmt.Println("got:", res)
		ch <- "op2 result"
	case <-ctx.Done():
		fmt.Println("op3 timed out", ctx.Err())
	}
}

func op3(ctx context.Context, ch chan<- string) {
	time.Sleep(2 * time.Second)
	ch <- "op3 result"
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	ch1 := make(chan string, 1)
	go op1(ctx, ch1)

	select {
	case res := <-ch1:
		fmt.Println("got:", res)
	case <-ctx.Done():
		fmt.Println("op1 timed out", ctx.Err())
	}
}

ejecutar

En los ejemplos anteriores trata cambiando los valores de los timeouts para ver cómo cambian los resultados.

Adicionalmente, también puedes llamar a la función cancel() para cancelar todas las operaciones que dependan del contexto, esto es útil por ejemplo cuando una operación falla y no se debe continuar procesando el resto de operaciones.

go 

Ver también