Abilita i nuovi tentativi per le funzioni basate su eventi (2ª generazione)

Questo documento descrive come abilitare i nuovi tentativi per le funzioni basate su eventi. I nuovi tentativi automatici non sono disponibili per le funzioni HTTP.

Semantica del nuovo tentativo

Cloud Functions offre l'esecuzione "atleast-once" di una funzione basata su eventi per ogni evento emesso da un'origine evento. Per impostazione predefinita, se la chiamata di una funzione termina con un errore, la funzione non viene richiamata di nuovo e l'evento viene eliminato. Quando abiliti i nuovi tentativi su una funzione basata su eventi, Cloud Functions riprendi una chiamata di funzione non riuscita fino a quando non viene completata correttamente o fino alla scadenza della finestra di nuovo tentativo.

Questa finestra per i nuovi tentativi scade dopo 24 ore. Cloud Functions riprova a utilizzare le funzioni basate su eventi appena create utilizzando una strategia di backoff esponenziale, con un backoff crescente compreso tra 10 e 600 secondi.

Quando i nuovi tentativi non sono abilitati per una funzione (impostazione predefinita), la funzione segnala sempre che è stata eseguita correttamente e i codici di risposta di 200 OK potrebbero essere visualizzati nei suoi log. Ciò si verifica anche se la funzione ha riscontrato un errore. Per chiarire quando la tua funzione rileva un errore, assicurati di segnalare gli errori in modo adeguato.

Perché le funzioni basate su eventi non vengono completate

In rare occasioni, è possibile che una funzione venga chiusa prematuramente a causa di un errore interno. Per impostazione predefinita, la funzione potrebbe essere oggetto di un nuovo tentativo automatico.

Più in genere, una funzione basata su eventi potrebbe non riuscire a essere completata correttamente a causa di errori generati nel codice della funzione stessa. Ciò potrebbe verificarsi:

  • La funzione contiene un bug e il runtime genera un'eccezione.
  • La funzione non può raggiungere un endpoint di servizio o si verifica un timeout durante il tentativo.
  • La funzione genera intenzionalmente un'eccezione (ad esempio, quando un parametro non supera la convalida).
  • Una funzione Node.js restituisce una promessa rifiutata o passa un valore diverso da null a un callback.

In ognuno di questi casi, l'esecuzione della funzione viene interrotta per impostazione predefinita e l'evento viene ignorato. Per riprovare la funzione quando si verifica un errore, puoi modificare il criterio predefinito per i nuovi tentativi impostando la proprietà "Riprova in caso di errore". Questo fa sì che l'evento venga ripetuto più volte fino al completamento della funzione o fino alla scadenza del timeout per il nuovo tentativo.

Abilita o disabilita i nuovi tentativi

Per abilitare o disabilitare i nuovi tentativi, puoi utilizzare lo strumento a riga di comando gcloud o la console Google Cloud. Per impostazione predefinita, i nuovi tentativi sono disattivati.

Configura i nuovi tentativi dallo strumento a riga di comando gcloud

Per abilitare i nuovi tentativi utilizzando lo strumento a riga di comando gcloud, includi il flag --retry durante il deployment della funzione:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Per disabilitare i nuovi tentativi, esegui nuovamente il deployment della funzione senza il flag --retry:

gcloud functions deploy FUNCTION_NAME FLAGS...

Configura i nuovi tentativi dalla console

Se stai creando una nuova funzione:

  1. Nella schermata Crea funzione, in Attivatore, scegli il tipo di evento che agisca da trigger per la funzione.
  2. Seleziona la casella di controllo Riprova in caso di errore per abilitare i nuovi tentativi.

Se stai aggiornando una funzione esistente:

  1. Nella pagina Panoramica di Cloud Functions, fai clic sul nome della funzione che stai aggiornando per aprire la schermata Dettagli funzione, quindi scegli Modifica dalla barra dei menu per visualizzare il riquadro Trigger.
  2. Seleziona o deseleziona la casella di controllo Riprova in caso di errore per attivare o disattivare i nuovi tentativi.

Best practice

In questa sezione vengono descritte le best practice per l'utilizzo di nuovi tentativi.

Utilizza Riprova per gestire gli errori temporanei

Poiché la funzione viene ripetuta continuamente fino alla riuscita dell'esecuzione, gli errori permanenti come i bug devono essere eliminati dal codice tramite test prima di abilitare nuovi tentativi. I nuovi tentativi sono ideali per gestire errori intermittenti o temporanei che hanno un'alta probabilità di risoluzione al momento di nuovi tentativi, come un endpoint di servizio instabile o un timeout.

Imposta una condizione di fine per evitare loop infiniti di nuovi tentativi

Una best practice consiste nel proteggere la funzione dai loop continui quando utilizzi nuovi tentativi. Per farlo, includi una condizione di fine ben definita, prima dell'inizio dell'elaborazione della funzione. Tieni presente che questa tecnica funziona solo se la funzione viene avviata correttamente ed è in grado di valutare la condizione di fine.

Un approccio semplice ma efficace consiste nell'ignorare gli eventi con timestamp precedenti a un determinato orario. Ciò consente di evitare esecuzioni eccessive quando gli errori sono persistenti o hanno una durata maggiore del previsto.

Ad esempio, questo snippet di codice elimina tutti gli eventi più vecchi di 10 secondi:

Node.js

const functions = require('@google-cloud/functions-framework');

/**
 * Cloud Event 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.
 */
functions.cloudEvent('avoidInfiniteRetries', (event, callback) => {
  const eventAge = Date.now() - Date.parse(event.time);
  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

import functions_framework


@functions_framework.cloud_event
def avoid_infinite_retries(cloud_event):
    """Cloud Event Function that only executes within a certain
    time period after the triggering event.

    Args:
        cloud_event: The cloud event associated with the current trigger
    Returns:
        None; output is written to Stackdriver Logging
    """
    timestamp = cloud_event["time"]

    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(cloud_event["id"], event_age_ms))
        return "Timeout"

    # Do what the function is supposed to do
    print("Processed {} (age {}ms)".format(cloud_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"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
)

func init() {
	functions.CloudEvent("FiniteRetryPubSub", FiniteRetryPubSub)
}

// MessagePublishedData contains the full Pub/Sub message
// See the documentation for more details:
// https://cloud.google.com/eventarc/docs/cloudevents#pubsub
type MessagePublishedData struct {
	Message PubSubMessage
}

// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// FiniteRetryPubSub demonstrates how to avoid inifinite retries.
func FiniteRetryPubSub(ctx context.Context, e event.Event) error {
	var msg MessagePublishedData
	if err := e.DataAs(&msg); err != nil {
		return fmt.Errorf("event.DataAs: %w", err)
	}

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

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

Java


import com.google.cloud.functions.CloudEventsFunction;
import io.cloudevents.CloudEvent;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.logging.Logger;

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

  /**
   * Cloud Event Function that only executes within
   * a certain time period after the triggering event
   */
  @Override
  public void accept(CloudEvent event) throws Exception {
    ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
    ZonedDateTime timestamp = event.getTime().atZoneSameInstant(ZoneOffset.UTC);

    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;

        DateTimeOffset utcNow = DateTimeOffset.UtcNow;

        // Every PubSub CloudEvent will contain a timestamp.
        DateTimeOffset timestamp = cloudEvent.Time.Value;
        DateTimeOffset 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;
    }
}

Ruby

require "functions_framework"

FunctionsFramework.cloud_event "avoid_infinite_retries" do |event|
  # Use the event timestamp to determine the event age.
  event_age_secs = Time.now - event.time.to_time
  event_age_ms = (event_age_secs * 1000).to_i

  max_age_ms = 10_000
  if event_age_ms > max_age_ms
    # Ignore events that are too old.
    logger.info "Dropped #{event.id} (age #{event_age_ms}ms)"

  else
    # Do what the function is supposed to do.
    logger.info "Handling #{event.id} (age #{event_age_ms}ms)..."
    failed = true

    # Raise an exception to signal failure and trigger a retry.
    raise "I failed!" if failed
  end
end

PHP


/**
 * This function shows an example method for avoiding infinite retries in
 * Google Cloud Functions. By default, functions configured to automatically
 * retry execution on failure will be retried indefinitely - causing an
 * infinite loop. To avoid this, we stop retrying executions (by not throwing
 * exceptions) for any events that are older than a predefined threshold.
 */

use Google\CloudFunctions\CloudEvent;

function avoidInfiniteRetries(CloudEvent $event): void
{
    $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');

    $eventId = $event->getId();

    // The maximum age of events to process.
    $maxAge = 10; // 10 seconds

    // The age of the event being processed.
    $eventAge = time() - strtotime($event->getTime());

    // Ignore events that are too old
    if ($eventAge > $maxAge) {
        fwrite($log, 'Dropping event ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL);
        return;
    }

    // Do what the function is supposed to do
    fwrite($log, 'Processing event: ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL);

    // infinite_retries failed function executions
    $failed = true;
    if ($failed) {
        throw new Exception('Event ' . $eventId . ' failed; retrying...');
    }
}

Distinguere tra funzioni che è possibile ritentare e errori irreversibili

Se per la funzione sono abilitati nuovi tentativi, qualsiasi errore non gestito attiverà un nuovo tentativo. Assicurati che il codice acquisisca tutti gli errori che non dovrebbero comportare un nuovo tentativo.

Node.js

const functions = require('@google-cloud/functions-framework');

/**
 * Register a Cloud Event Function that demonstrates
 * how to toggle retries using a promise
 *
 * @param {object} event The Cloud Event for the function trigger.
 */
functions.cloudEvent('retryPromise', cloudEvent => {
  // The Pub/Sub event payload is passed as the CloudEvent's data payload.
  // See the documentation for more details:
  // https://cloud.google.com/eventarc/docs/cloudevents#pubsub
  const base64PubsubMessage = cloudEvent.data.message.data;
  const jsonString = Buffer.from(base64PubsubMessage, 'base64').toString();

  const tryAgain = JSON.parse(jsonString).retry;

  if (tryAgain) {
    throw new Error('Retrying...');
  } else {
    console.error('Not retrying...');
    return Promise.resolve();
  }
});

/**
 * Cloud Event Function that demonstrates
 * how to toggle retries using a callback
 *
 * @param {object} event The Cloud Event for the function trigger.
 * @param {function} callback The callback function.
 */
functions.cloudEvent('retryCallback', (cloudEvent, callback) => {
  // The Pub/Sub event payload is passed as the CloudEvent's data payload.
  // See the documentation for more details:
  // https://cloud.google.com/eventarc/docs/cloudevents#pubsub
  const base64PubsubMessage = cloudEvent.data.message.data;
  const jsonString = Buffer.from(base64PubsubMessage, 'base64').toString();

  const tryAgain = JSON.parse(jsonString).retry;
  const err = new Error('Error!');

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

Python

import base64
import json

import functions_framework
from google.cloud import error_reporting


error_client = error_reporting.Client()


@functions_framework.cloud_event
def retry_or_not(cloud_event):
    """Cloud Event Function that demonstrates how to toggle retries.

    Args:
        cloud_event: The cloud event with a Pub/Sub data payload
    Returns:
        None; output is written to Stackdriver Logging
    """

    # The Pub/Sub event payload is passed as the CloudEvent's data payload.
    # See the documentation for more details:
    # https://cloud.google.com/eventarc/docs/cloudevents#pubsub
    encoded_pubsub_message = cloud_event.data["message"]["data"]

    # Retry based on a user-defined parameter
    try_again = json.loads(base64.b64decode(encoded_pubsub_message).decode())["retry"]

    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

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
)

func init() {
	functions.CloudEvent("RetryPubSub", RetryPubSub)
}

// MessagePublishedData contains the full Pub/Sub message
// See the documentation for more details:
// https://cloud.google.com/eventarc/docs/cloudevents#pubsub
type MessagePublishedData struct {
	Message PubSubMessage
}

// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// RetryPubSub demonstrates how to toggle using retries.
func RetryPubSub(ctx context.Context, e event.Event) error {
	var msg MessagePublishedData
	if err := e.DataAs(&msg); err != nil {
		return fmt.Errorf("event.DataAs: %w", err)
	}

	name := string(msg.Message.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.CloudEventsFunction;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import functions.eventpojos.PubSubBody;
import io.cloudevents.CloudEvent;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Logger;

public class RetryPubSub implements CloudEventsFunction {
  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(CloudEvent event) throws Exception {
    if (event.getData() == null) {
      logger.warning("No data found in event!");
      return;
    }

    // Extract Cloud Event data and convert to PubSubBody
    String cloudEventData = new String(event.getData().toBytes(), StandardCharsets.UTF_8);
    PubSubBody body = gson.fromJson(cloudEventData, PubSubBody.class);

    String encodedData = body.getMessage().getData();
    String decodedData =
        new String(Base64.getDecoder().decode(encodedData), StandardCharsets.UTF_8);

    // Retrieve and decode PubSubMessage data into a JsonElement.
    // Function is expecting a user-supplied JSON message which determines whether
    // to retry or not.
    JsonElement jsonPubSubMessageElement = gson.fromJson(decodedData, JsonElement.class);

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

      if (jsonPubSubMessageObject.has("retry")
          && jsonPubSubMessageObject.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;
    }
}

Ruby

require "functions_framework"

FunctionsFramework.cloud_event "retry_or_not" do |event|
  try_again = event.data["retry"]

  begin
    # Simulate a failure
    raise "I failed!"
  rescue RuntimeError => e
    logger.warn "Caught an error: #{e}"
    if try_again
      # Raise an exception to return a 500 and trigger a retry.
      logger.info "Trying again..."
      raise ex
    else
      # Return normally to end processing of this event.
      logger.info "Giving up."
    end
  end
end

PHP


use Google\CloudFunctions\CloudEvent;

function tipsRetry(CloudEvent $event): void
{
    $cloudEventData = $event->getData();
    $pubSubData = $cloudEventData['message']['data'];

    $json = json_decode(base64_decode($pubSubData), true);

    // Determine whether to retry the invocation based on a parameter
    $tryAgain = $json['some_parameter'];

    if ($tryAgain) {
        /**
         * Functions with automatic retries enabled should throw exceptions to
         * indicate intermittent failures that a retry might fix. In this
         * case, a thrown exception will cause the original function
         * invocation to be re-sent.
         */
        throw new Exception('Intermittent failure occurred; retrying...');
    }

    /**
     * If a function with retries enabled encounters a non-retriable
     * failure, it should return *without* throwing an exception.
     */
    $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
    fwrite($log, 'Not retrying' . PHP_EOL);
}

Rendi idempotenti le funzioni basate su eventi ritentabili

Le funzioni basate su eventi che è possibile ritentare devono essere idempotenti. Ecco alcune linee guida generali per rendere questa funzione idempotente:

  • Molte API esterne (come Stripe) ti consentono di fornire una chiave di idempotenza come parametro. Se utilizzi un'API di questo tipo, devi utilizzare l'ID evento come chiave di idempotenza.
  • L'idempotenza funziona bene con la consegna "at-least-once", perché consente di riprovare in sicurezza. Pertanto, una best practice generale per scrivere codice affidabile è combinare l'idempotenza con nuovi tentativi.
  • Assicurati che il tuo codice sia internamente idempotente. Ad esempio:
    • Assicurati che le mutazioni possano verificarsi più di una volta senza modificare il risultato.
    • Esegui una query sullo stato del database in una transazione prima di modificare lo stato.
    • Assicurati che tutti gli effetti collaterali siano idempotenti.
  • Impone un controllo transazionale all'esterno della funzione, indipendentemente dal codice. Ad esempio, uno stato permanente che registra che un determinato ID evento è già stato elaborato.
  • Gestire chiamate di funzione duplicate fuori banda. Ad esempio, imposta un processo di pulizia separato dopo chiamate di funzione duplicate.

Configura il criterio per i nuovi tentativi

A seconda delle esigenze della tua Cloud Function, ti consigliamo di configurare direttamente il criterio per riprovare. Questo ti consente di configurare qualsiasi combinazione di quanto segue:

  • Riduci la finestra per i nuovi tentativi da 7 giorni a meno di 10 minuti.
  • Modificare il tempo minimo e massimo di backoff per la strategia di nuovo tentativo di backoff esponenziale.
  • Modifica la strategia per riprovare immediatamente.
  • Configura un argomento messaggi non recapitabili.
  • Imposta un numero massimo e minimo di tentativi di consegna.

Per configurare il criterio per i nuovi tentativi:

  1. Scrivi una funzione HTTP.
  2. Utilizza l'API Pub/Sub per creare un abbonamento Pub/Sub, specificando l'URL della funzione come destinazione.

Consulta la documentazione di Pub/Sub sulla gestione degli errori per ulteriori informazioni sulla configurazione diretta di Pub/Sub.

Passaggi successivi