Répétition des tentatives des fonctions d'arrière-plan

Ce document décrit comment activer la répétition des tentatives de fonctions d'arrière-plan. La répétition des tentatives automatique n'est pas disponible pour les fonctions HTTP.

Sémantique d'une répétition de tentative

Cloud Functions garantit l'exécution de type "au moins une fois" d'une fonction d'arrière-plan pour chaque événement émis par une source d'événement. Toutefois, par défaut, si l'appel de fonction se termine par une erreur, la fonction n'est plus appelée et l'événement est supprimé. Lorsque vous activez la répétition des tentatives sur une fonction d'arrière-plan, Cloud Functions tente à nouveau d'appeler la fonction défaillante jusqu'à ce qu'elle aboutisse ou que la fenêtre de nouvelle tentative arrive à expiration (par défaut, au bout de 7 jours).

Pourquoi les fonctions d'arrière-plan échouent-elles ?

Il peut arriver qu'une fonction se ferme prématurément en raison d'une erreur interne et, par défaut, cette fonction peut être relancée automatiquement ou non.

Le plus souvent, une fonction d'arrière-plan peut échouer en raison d'erreurs générées dans le code même de la fonction. Cet échec s'explique entre autres par les raisons suivantes :

  • La fonction contient un bug et l’environnement d'exécution renvoie une exception.
  • La fonction ne peut pas atteindre de point de terminaison de service ou dépasse le délai en essayant d'atteindre le point de terminaison.
  • La fonction renvoie intentionnellement une exception (par exemple, lorsqu'un paramètre échoue à la validation).
  • Des fonctions écrites dans Node.js renvoient une promesse refusée ou transmettent une valeur qui n'est pas null à un rappel.

Dans tous les cas énoncés ci-dessus, la fonction cesse d'être exécutée par défaut et l'événement est supprimé. Si vous souhaitez relancer la fonction en cas d'erreur, vous pouvez modifier la stratégie de répétition de tentatives par défaut en définissant la propriété "Réessayer après échec". L'événement est alors relancé de façon répétée pendant plusieurs jours jusqu'à ce que la fonction se termine avec succès.

Activer et désactiver la répétition de tentatives

Pour activer ou désactiver la répétition des tentatives, vous pouvez utiliser l'outil de ligne de commande gcloud ou Cloud Console. La répétition de tentatives est désactivée par défaut.

À l'aide de l'outil de ligne de commande gcloud

Pour activer la répétition des tentatives via l'outil de ligne de commande gcloud, incluez l'option --retry lors du déploiement de votre fonction :

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Pour désactiver la répétition des tentatives, redéployez la fonction sans l'option --retry :

gcloud functions deploy FUNCTION_NAME FLAGS...

Utiliser Cloud Console

Vous pouvez activer ou désactiver la répétition des tentatives dans Cloud Console comme suit :

  1. Accédez à la page Présentation de Cloud Functions dans la console Cloud Platform.

  2. Cliquez sur Créer une fonction. Vous pouvez également cliquer sur une fonction existante pour accéder à sa page d'informations, puis sur Modifier.

  3. Renseignez les champs obligatoires de votre fonction.

  4. Vérifiez que le champ Déclencheur est défini sur un type de déclencheur de la fonction d'arrière-plan, tel que Cloud Pub/Sub ou Cloud Storage.

  5. Développez les paramètres avancés en cliquant sur Plus.

  6. Cochez ou décochez la case intitulée Réessayer après échec.

Bonnes pratiques

Cette section décrit les bonnes pratiques relatives à l'utilisation de la répétition des tentatives.

Utiliser la répétition pour faire face aux erreurs temporaires

Votre fonction est relancée en continu tant que son exécution n'est pas réussie. Vous devez donc éliminer de votre code les erreurs permanentes telles que les bugs par le biais de tests. Ce n'est qu'après cette étape que vous pourrez activer la répétition des tentatives. Les tentatives sont particulièrement utiles pour gérer les échecs intermittents/temporaires qui présentent une probabilité élevée de résolution à mesure des nouvelles tentatives, par exemple lorsqu'un point de terminaison de service ou un délai d’inactivité est instable.

Définir une condition de fin pour éviter les boucles infinies de répétition de tentatives

Il est recommandé de protéger votre fonction contre les boucles continues lors de l'utilisation de la répétition des tentatives. Pour ce faire, incluez une condition de fin bien définie avant le début du traitement de la fonction. Notez que cette technique ne fonctionne que si votre fonction démarre correctement et qu'elle est en mesure d'évaluer la condition de fin.

Une approche simple, mais efficace, consiste à ignorer les événements dont l'horodatage est antérieur à une certaine période. Cela permet d'éviter des exécutions excessives lorsque les échecs sont persistants ou plus longs que prévu.

Par exemple, l'extrait de code suivant supprime tous les événements de plus de dix secondes :

Node.js

/**
 * 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.`);

  // Retry failed function executions
  const failed = false;
  if (failed) {
    callback('some error');
  } else {
    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  # To retry the execution, raise an exception here

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)
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.gson.Gson;
import functions.eventpojos.PubSubMessage;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.logging.Logger;

public class RetryTimeout implements BackgroundFunction<PubSubMessage> {
  private static final Logger logger = Logger.getLogger(RetryTimeout.class.getName());
  private static final long MAX_EVENT_AGE = 10_000;

  // Use Gson (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  /**
   * Background Cloud Function that only executes within
   * a certain time period after the triggering event
   */
  @Override
  public void accept(PubSubMessage message, Context context) {
    ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
    ZonedDateTime timestamp = ZonedDateTime.parse(context.timestamp());

    long eventAge = Duration.between(timestamp, utcNow).toMillis();

    // Ignore events that are too old
    if (eventAge > MAX_EVENT_AGE) {
      logger.info(String.format("Dropping event with timestamp %s.", timestamp));
      return;
    }

    // Process events that are recent enough
    // To retry this invocation, throw an exception here
    logger.info(String.format("Processing event with timestamp %s.", timestamp));
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace TimeBoundedRetries
{
    public class Function : ICloudEventFunction<MessagePublishedData>
    {
        private static readonly TimeSpan MaxEventAge = TimeSpan.FromSeconds(10);
        private readonly ILogger _logger;

        // Note: for additional testability, use an injectable clock abstraction.
        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken)
        {
            string textData = data.Message.TextData;

            DateTime utcNow = DateTime.UtcNow;

            // Every PubSub CloudEvent will contain a timestamp.
            DateTime timestamp = cloudEvent.Time.Value.ToUniversalTime();
            DateTime expiry = timestamp + MaxEventAge;

            // Ignore events that are too old.
            if (utcNow > expiry)
            {
                _logger.LogInformation("Dropping PubSub message '{text}'", textData);
                return Task.CompletedTask;
            }

            // Process events that are recent enough.
            // If this processing throws an exception, the message will be retried until either
            // processing succeeds or the event becomes too old and is dropped by the code above.
            _logger.LogInformation("Processing PubSub message '{text}'", textData);
            return Task.CompletedTask;
        }
    }
}

Distinguer les erreurs récupérables des erreurs fatales

Si la répétition de tentatives est activée pour votre fonction, toute erreur non gérée déclenche une nouvelle tentative. Assurez-vous que votre code capture toutes les erreurs qui ne doivent pas entraîner de nouvelle tentative.

Node.js

/**
 * 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
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import functions.eventpojos.PubSubMessage;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Logger;

public class RetryPubSub implements BackgroundFunction<PubSubMessage> {
  private static final Logger logger = Logger.getLogger(RetryPubSub.class.getName());

  // Use Gson (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  @Override
  public void accept(PubSubMessage message, Context context) {
    String bodyJson = new String(
        Base64.getDecoder().decode(message.getData()), StandardCharsets.UTF_8);
    JsonElement bodyElement = gson.fromJson(bodyJson, JsonElement.class);

    // Get the value of the "retry" JSON parameter, if one exists
    boolean retry = false;
    if (bodyElement != null && bodyElement.isJsonObject()) {
      JsonObject body = bodyElement.getAsJsonObject();

      if (body.has("retry") && body.get("retry").getAsBoolean()) {
        retry = true;
      }
    }

    // Retry if appropriate
    if (retry) {
      // Throwing an exception causes the execution to be retried
      throw new RuntimeException("Retrying...");
    } else {
      logger.info("Not retrying...");
    }
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Retry
{
    public class Function : ICloudEventFunction<MessagePublishedData>
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken)
        {
            bool retry = false;
            string text = data.Message?.TextData;

            // Get the value of the "retry" JSON parameter, if one exists.
            if (!string.IsNullOrEmpty(text))
            {
                JsonElement element = JsonSerializer.Deserialize<JsonElement>(data.Message.TextData);

                retry = element.TryGetProperty("retry", out var property) &&
                    property.ValueKind == JsonValueKind.True;
            }

            // Throwing an exception causes the execution to be retried.
            if (retry)
            {
                throw new InvalidOperationException("Retrying...");
            }
            else
            {
                _logger.LogInformation("Not retrying...");
            }
            return Task.CompletedTask;
        }
    }
}

Rendre les fonctions d'arrière-plan récupérables idempotentes

Les fonctions d'arrière-plan qui peuvent être relancées doivent être idempotentes. Voici quelques consignes générales pour créer une fonction d'arrière-plan idempotente :

  • De nombreuses API externes (telles que Stripe) vous permettent de fournir une clé d'idempotence en tant que paramètre. Si vous utilisez une telle API, vous devez utiliser l'ID d'événement comme clé d'idempotence.
  • L'idempotence fonctionne bien avec une livraison de type "au moins une fois", car elle permet de répéter la tentative en toute sécurité. Une bonne pratique pour écrire du code fiable consiste donc à combiner l'idempotence à la répétition des tentatives.
  • Assurez-vous que votre code est idempotent en interne. Exemple :
    • Assurez-vous que les mutations peuvent se produire plus d'une fois sans en changer le résultat.
    • Interrogez l'état de la base de données dans une transaction avant de muter l'état.
    • Assurez-vous que tous les effets secondaires sont eux-mêmes idempotents.
  • Imposez un contrôle transactionnel en dehors de la fonction, indépendamment du code. Par exemple, conservez l'état quelque part en notant qu'un ID d'événement donné a déjà été traité.
  • Gérez les appels de fonction doubles hors bande. Par exemple, mettez en place un processus de nettoyage distinct qui se lance après les appels de fonction doubles.

Étapes suivantes