Attivare i nuovi tentativi per le funzioni basate su eventi (1ª generazione.)

Questo documento descrive come attivare i tentativi di nuovo per le funzioni basate su eventi. I tentativi automatici non sono disponibili per le funzioni HTTP.

Semantica della riprova

Le funzioni Cloud Run forniscono l'esecuzione almeno una volta di una funzione basata su eventi per ogni evento emesso da un'origine evento. Per impostazione predefinita, se l'invocazione di una funzione termina con un errore, la funzione non viene invocata di nuovo e l'evento viene ignorato. Quando attivi i tentativi su una funzione basata su eventi, le funzioni Cloud Run riprovano un'invocazione di funzione non riuscita finché non viene completata correttamente o non scade il periodo di tempo per i tentativi.

Questa finestra di ripetizione scade dopo 7 giorni. Cloud Run Functions riprova le funzioni basate su eventi appena create utilizzando una strategia di backoff esponenziale, con un backoff crescente compreso tra 10 e 600 secondi. Questo criterio viene applicato alle nuove funzioni la prima volta che le esegui il deployment. Non viene applicata retroattivamente alle funzioni esistenti di cui è stato eseguito il primo dispiegamento prima dell'applicazione delle modifiche descritte in questa nota di rilascio, anche se le funzioni vengono ridistribuite.

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

Perché le funzioni basate su eventi non riescono a completare l'esecuzione

In rari casi, una funzione potrebbe uscire prematuramente a causa di un errore interno e, per impostazione predefinita, la funzione potrebbe o meno essere ripetuta automaticamente.

Più in generale, una funzione basata su eventi potrebbe non essere completata correttamente a causa di errori generati nel codice della funzione stessa. Ecco alcuni motivi per cui questo potrebbe accadere:

  • La funzione contiene un bug e il runtime genera un'eccezione.
  • La funzione non riesce a raggiungere un endpoint del servizio o il timeout si verifica durante il tentativo di farlo.
  • 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 tutti questi casi, la funzione interrompe l'esecuzione per impostazione predefinita e l'evento viene ignorato. Per riprovare a eseguire la funzione in caso di errore, puoi modificare il criterio di ripetizione predefinito impostando la proprietà "riprova in caso di errore". Di conseguenza, l'evento viene tentato ripetutamente finché la funzione non viene completata correttamente o non scade il timeout del nuovo tentativo.

Attivare o disattivare i tentativi di nuovo invio

Per attivare o disattivare i tentativi di nuovo invio, puoi utilizzare lo strumento a riga di comando gcloud o la console Google Cloud. Per impostazione predefinita, i tentativi di nuovo invio sono disattivati.

Configurare i tentativi di nuovo invio dallo strumento a riga di comando gcloud

Per abilitare i tentativi di nuovo 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 disattivare i nuovi tentativi, esegui nuovamente il deployment della funzione senza il flag --retry:

gcloud functions deploy FUNCTION_NAME FLAGS...

Configurare i tentativi di nuovo invio dalla console

Se stai creando una nuova funzione:

  1. Nella schermata Crea funzione, in Attivati, scegli il tipo di evento da utilizzare come attivatore per la funzione.
  2. Seleziona la casella di controllo Riprova in caso di errore per attivare i tentativi di nuovo invio.

Se stai aggiornando una funzione esistente:

  1. Nella pagina Panoramica delle funzioni Cloud Run, fai clic sul nome della funzione da aggiornare per aprire la schermata Dettagli funzione, quindi scegli Modifica dalla barra dei menu per visualizzare il riquadro Attivazione.
  2. Seleziona o deseleziona la casella di controllo Riprova in caso di errore per attivare o disattivare i tentativi di nuovo invio.

Best practice

Questa sezione descrive le best practice per l'utilizzo dei tentativi di nuovo invio.

Utilizzare il nuovo tentativo per gestire gli errori temporanei

Poiché la funzione viene tentata continuamente fino a quando non viene eseguita correttamente, gli errori permanenti come i bug devono essere eliminati dal codice tramite i test prima di attivare i tentativi. I tentativi sono ideali per gestire errori intermittenti o temporanei con un'alta probabilità di risoluzione al successivo tentativo, ad esempio un endpoint di servizio inaffidabile o un timeout.

Imposta una condizione di fine per evitare loop di tentativi ripetuti all'infinito

È buona prassi proteggere la funzione da un ciclo continuo quando si utilizzano i tentativi. Puoi farlo includendo una condizione di fine ben definita, prima dell'inizio dell'elaborazione della funzione. Tieni presente che questa tecnica funziona solo se la funzione si avvia correttamente ed è in grado di valutare la condizione di fine.

Un approccio semplice ma efficace è eliminare gli eventi con timestamp precedenti a un determinato momento. In questo modo si evitano esecuzioni eccessive quando gli errori sono persistenti o durano più a lungo del previsto.

Ad esempio, questo snippet di codice ignora tutti gli eventi precedenti a 10 secondi:

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(f"Dropped {context.event_id} (age {event_age_ms}ms)")
        return "Timeout"

    # Do what the function is supposed to do
    print(f"Processed {context.event_id} (age {event_age_ms}ms)")
    return  # To retry the execution, raise an exception here

Vai


// 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.
// 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, 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: %w", 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;

        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 possono essere riprovate ed errori irreversibili

Se per la funzione sono abilitati i nuovi tentativi, qualsiasi errore non gestito attiverà un nuovo tentativo. Assicurati che il codice catturi eventuali errori che non devono comportare un nuovo tentativo.

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 {
    console.error('Not retrying...');
    return Promise.resolve();
  }
};

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

Vai


// 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.
// 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, 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;
    }
}

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

Rendere idempotenti le funzioni basate su eventi ripetibili

Le funzioni basate su eventi per le quali è possibile ripetere il tentativo devono essere idempotenti. Ecco alcune linee guida generali per rendere idempotente una funzione di questo tipo:

  • 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 idempotency.
  • L'idempotenza funziona bene con la consegna "almeno una volta", perché consente di eseguire nuovamente l'operazione in sicurezza. Pertanto, una best practice generale per scrivere codice affidabile è combinare l'idempotenza con i tentativi di nuovo.
  • Assicurati che il codice sia internamente idempotente. Ad esempio:
    • Assicurati che le mutazioni possano verificarsi più di una volta senza modificare il risultato.
    • Esegui query sullo stato del database in una transazione prima di modificarlo.
    • Assicurati che tutti gli effetti collaterali siano idempotenti.
  • Imposta un controllo transazionale esterno alla funzione, indipendente dal codice. Ad esempio, mantieni lo stato da qualche parte registrando che un determinato ID evento è stato già elaborato.
  • Gestisci le chiamate di funzioni duplicate out-of-band. Ad esempio, puoi avere un processo di pulizia distinto che elimina i dati dopo le chiamate di funzioni duplicate.

Configura il criterio per i tentativi ripetuti

A seconda delle esigenze della funzione, ti consigliamo di configurare direttamente il piano di ripetizione. In questo modo, puoi configurare qualsiasi combinazione di quanto segue:

  • Riduci il periodo di attesa per il nuovo tentativo da 7 giorni a un minimo di 10 minuti.
  • Modifica il tempo di attesa minimo e massimo per la strategia di ripetizione con tempo di attesa esponenziale.
  • Modifica la strategia di ripetizione in modo da riprovare immediatamente.
  • Configura un argomento messaggi non recapitabili.
  • Imposta un numero massimo e minimo di tentativi di invio.

Per configurare il criterio di ripetizione:

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

Per ulteriori informazioni sulla configurazione diretta di Pub/Sub, consulta la documentazione di Pub/Sub sulla gestione degli errori.

Passaggi successivi