Reintenta las funciones en segundo plano

En este documento, se describe cómo habilitar los reintentos para las funciones en segundo plano. Los reintentos automáticos no están disponibles para las funciones de HTTP.

Semántica de los reintentos

Cloud Functions garantiza la ejecución al menos una vez de una función en segundo plano para cada evento emitido por una fuente de eventos. Sin embargo, según la configuración predeterminada, si la invocación de una función termina con un error, esta no se volverá a invocar y se descartará el evento. Cuando habilitas los reintentos en una función en segundo plano, Cloud Functions volverá a intentar una invocación de función con errores hasta que se complete correctamente o hasta que caduque el período de reintento (que es de 7 días de manera predeterminada).

Por qué no se completan las funciones en segundo plano

En ocasiones poco comunes, es posible que una función se cierre de forma prematura debido a un error interno y, según la configuración predeterminada, podría volver a ejecutarse la función, automáticamente o no.

Por lo general, es posible que una función no logre completarse correctamente debido a errores en el propio código de la función. A continuación, se muestran algunos de los motivos por los que puede ocurrir:

  • La función contiene un error y el tiempo de ejecución muestra una excepción.
  • La función no puede alcanzar el extremo del servicio o se agota el tiempo de espera mientras intenta alcanzarlo.
  • La función muestra una excepción de forma intencional (p. ej., cuando falla la validación de un parámetro).
  • Cuando las funciones escritas en Node.js muestran una promesa rechazada o pasan un valor que no es null a una devolución de llamada.

En cualquiera de los casos anteriores, la función deja de ejecutarse de manera predeterminada y el evento se descarta. Si deseas ejecutar de nuevo la función cuando ocurre un error, puedes configurar la propiedad "volver a intentar en caso de error" para cambiar la política de intentos reiterados predeterminada. Esto hace que se vuelva a ejecutar el evento reiteradamente por varios días hasta que la función se complete de manera correcta.

Inhabilita y habilita reintentos

Para habilitar o inhabilitar los reintentos, puedes usar la herramienta de línea de comandos de gcloud o GCP Console. Según la configuración predeterminada, los reintentos están inhabilitados.

Usa la herramienta de línea de comandos de gcloud

Para habilitar los reintentos a través de la herramienta de línea de comandos de gcloud, incluye el marcador --retry cuando implementes tu función:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Para inhabilitar los reintentos, vuelve a implementar la función sin el marcador --retry:

gcloud functions deploy FUNCTION_NAME FLAGS...

Usa GCP Console

Puedes habilitar o inhabilitar los reintentos en GCP Console de la siguiente manera:

  1. Ve a la página Descripción general de Cloud Functions en Cloud Platform Console.

  2. Haz clic en Crear función. De forma alternativa, haz clic en una función existente para ir a la página de detalles y haz clic en Editar.

  3. Completa los campos obligatorios para la función.

  4. Asegúrate de que el campo Activador esté configurado para un tipo de activador de función en segundo plano, como Cloud Pub/Sub o Cloud Storage.

  5. Haz clic en Más para expandir la configuración avanzada.

  6. Marca o desmarca la casilla con la etiqueta Volver a intentar en caso de error.

Recomendaciones

Esta sección describe las recomendaciones para usar los intentos reiterados.

Usa los intentos reiterados para administrar errores temporales

Dado que la función se vuelve a intentar de manera reiterada hasta que se ejecute correctamente, las fallas permanentes, como los errores, se deben eliminar del código mediante pruebas antes de habilitar los intentos reiterados. Lo mejor es usar estos intentos para administrar fallas intermitentes o temporales con una alta probabilidad de resolverse en un intento reiterado, como un extremo de servicio o un tiempo de espera inestables.

Configura una condición de finalización para prevenir los bucles de intentos reiterados infinitos

Se recomienda proteger tu función contra los bucles continuos cuando usas los intentos reiterados. Para esto, puedes incluir una condición de finalización bien definida antes de que la función comience a procesarse. Un enfoque simple y eficaz es descartar eventos con marcas de tiempo que superen un tiempo definido. Esto permite evitar ejecuciones excesivas cuando las fallas son persistentes o duran más de lo esperado.

Por ejemplo, este fragmento de código descarta todos los eventos que superen los 10 segundos de antigüedad:

Node.js 8/10

/**
 * Background Cloud Function that only executes within a certain time
 * period after the triggering event to avoid infinite retry loops.
 *
 * @param {object} data The event payload.
 * @param {object} context The event metadata.
 */
exports.avoidInfiniteRetries = (data, context) => {
  const eventAge = Date.now() - Date.parse(context.timestamp);
  const eventMaxAge = 10000;

  // Ignore events that are too old
  if (eventAge > eventMaxAge) {
    console.log(`Dropping event ${context.eventId} with age ${eventAge} ms.`);
    return;
  }

  // Do what the function is supposed to do
  console.log(`Processing event ${context.eventId} with age ${eventAge} ms.`);
};

Node.js 6 (obsoleto)

/**
 * Background Cloud Function that only executes within
 * a certain time period after the triggering event
 *
 * @param {object} event The Cloud Functions event.
 * @param {function} callback The callback function.
 */
exports.avoidInfiniteRetries = (event, callback) => {
  const eventAge = Date.now() - Date.parse(event.timestamp);
  const eventMaxAge = 10000;

  // Ignore events that are too old
  if (eventAge > eventMaxAge) {
    console.log(`Dropping event ${event} with age ${eventAge} ms.`);
    callback();
    return;
  }

  // Do what the function is supposed to do
  console.log(`Processing event ${event} with age ${eventAge} ms.`);
  callback();
};

Python

from datetime import datetime, timezone
# The 'python-dateutil' package must be included in requirements.txt.
from dateutil import parser

def avoid_infinite_retries(data, context):
    """Background Cloud Function that only executes within a certain
    time period after the triggering event.

    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): The event metadata.
    Returns:
        None; output is written to Stackdriver Logging
    """

    timestamp = context.timestamp

    event_time = parser.parse(timestamp)
    event_age = (datetime.now(timezone.utc) - event_time).total_seconds()
    event_age_ms = event_age * 1000

    # Ignore events that are too old
    max_age_ms = 10000
    if event_age_ms > max_age_ms:
        print('Dropped {} (age {}ms)'.format(context.event_id, event_age_ms))
        return 'Timeout'

    # Do what the function is supposed to do
    print('Processed {} (age {}ms)'.format(context.event_id, event_age_ms))
    return

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/functions/metadata"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// FiniteRetryPubSub demonstrates how to avoid inifinite retries.
func FiniteRetryPubSub(ctx context.Context, m PubSubMessage) error {
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		// Assume an error on the function invoker and try again.
		return fmt.Errorf("metadata.FromContext: %v", err)
	}

	// Ignore events that are too old.
	expiration := meta.Timestamp.Add(10 * time.Second)
	if time.Now().After(expiration) {
		log.Printf("event timeout: halting retries for expired event '%q'", meta.EventID)
		return nil
	}

	// Add your message processing logic.
	return processTheMessage(m)
}

Distingue entre los errores que se pueden reintentar y los fatales

Si tu función tiene habilitados los reintentos, cualquier error no administrado activará un reintento. Asegúrate de que tu código capture cualquier error que no debería dar como resultado un reintento.

Node.js 8/10

/**
 * Background Cloud Function that demonstrates
 * how to toggle retries using a promise
 *
 * @param {object} data The event payload.
 * @param {object} data.retry User-supplied parameter that tells the function whether to retry.
 * @param {object} context The event metadata.
 */
exports.retryPromise = (data, context) => {
  const tryAgain = !!data.retry;

  if (tryAgain) {
    throw new Error(`Retrying...`);
  } else {
    return new Error('Not retrying...');
  }
};

Node.js 6 (obsoleto)

/**
 * Background Cloud Function that demonstrates
 * how to toggle retries using a promise
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data Data included with the event.
 * @param {object} event.data.retry User-supplied parameter that tells the function whether to retry.
 */
exports.retryPromise = event => {
  const tryAgain = !!event.data.retry;

  if (tryAgain) {
    throw new Error(`Retrying...`);
  } else {
    return Promise.reject(new Error('Not retrying...'));
  }
};

/**
 * Background Cloud Function that demonstrates
 * how to toggle retries using a callback
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data Data included with the event.
 * @param {object} event.data.retry User-supplied parameter that tells the function whether to retry.
 * @param {function} callback The callback function.
 */
exports.retryCallback = (event, callback) => {
  const tryAgain = !!event.data.retry;
  const err = new Error('Error!');

  if (tryAgain) {
    console.error('Retrying:', err);
    callback(err);
  } else {
    console.error('Not retrying:', err);
    callback();
  }
};

Python

from google.cloud import error_reporting
error_client = error_reporting.Client()

def retry_or_not(data, context):
    """Background Cloud Function that demonstrates how to toggle retries.

    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): The event metadata.
    Returns:
        None; output is written to Stackdriver Logging
    """

    # Retry based on a user-defined parameter
    try_again = data.data.get('retry') is not None

    try:
        raise RuntimeError('I failed you')
    except RuntimeError:
        error_client.report_exception()
        if try_again:
            raise  # Raise the exception and try again
        else:
            pass   # Swallow the exception and don't retry

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"errors"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// RetryPubSub demonstrates how to toggle using retries.
func RetryPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data)
	if name == "" {
		name = "World"
	}

	// A misconfigured client will stay broken until the function is redeployed.
	client, err := MisconfiguredDataClient()
	if err != nil {
		log.Printf("MisconfiguredDataClient (retry denied):  %v", err)
		// A nil return indicates that the function does not need a retry.
		return nil
	}

	// Runtime error might be resolved with a new attempt.
	if err = FailedWriteOperation(client, name); err != nil {
		log.Printf("FailedWriteOperation (retry expected): %v", err)
		// A non-nil return indicates that a retry is needed.
		return err
	}

	return nil
}

Haz idempotentes a las funciones en segundo plano cuya ejecución se puede intentar nuevamente

Deben ser idempotentes las funciones en segundo plano cuya ejecución se pueda volver a intentar. A continuación, te mostramos algunos lineamientos generales para crear una función idempotente en segundo plano:

  • Muchas API externas (como Stripe) te permiten proporcionar una clave de idempotencia como parámetro. Si usas una API de este tipo, debes usar el ID de evento como la clave de idempotencia.
  • La idempotencia funciona bien con la entrega "al menos una vez", ya que permite que los intentos reiterados sean seguros. Por lo tanto, una recomendación general para escribir un código confiable es combinar la idempotencia con los intentos reiterados.
  • Asegúrate de que tu código sea idempotente de forma interna. Por ejemplo:
    • Asegúrate de que puedan ocurrir mutaciones más de una vez sin que cambie el resultado.
    • Consulta el estado de la base de datos en una transacción antes de mutar el estado.
    • Asegúrate de que todos los efectos secundarios sean idempotentes en sí.
  • Debes imponer una verificación transaccional fuera de la función y que sea independiente del código. Por ejemplo, conserva el estado en algún lugar que registre si ya se procesó un ID de evento determinado.
  • Administra las llamadas de función duplicadas fuera de banda. Por ejemplo, implementa un proceso de limpieza independiente que borre las llamadas de función duplicadas.

Próximos pasos

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Documentación de Cloud Functions