Ereignisgesteuerte Funktionswiederholungen (1. Generation) aktivieren

In diesem Dokument wird die Aktivierung von Wiederholungsversuchen für ereignisgesteuerte Funktionen beschrieben. Für HTTP-Funktionen sind keine automatischen Wiederholungsversuche verfügbar.

Semantik von Wiederholungsversuchen

Cloud Run Functions sorgt dafür, dass ereignisgesteuerte Funktionen für jedes Ereignis, das von einer Ereignisquelle ausgegeben wird, mindestens einmal ausgeführt werden. Wenn ein Funktionsaufruf mit einem Fehler beendet wird, wird die Funktion standardmäßig nicht noch einmal aufgerufen und das Ereignis wird verworfen. Wenn Sie Wiederholungsversuche für eine ereignisgesteuerte Funktion aktivieren, wiederholt Cloud RUn Functions einen fehlgeschlagenen Funktionsaufruf, bis der Vorgang erfolgreich abgeschlossen wurde oder der Wiederholungszeitraum abläuft.

Dieses Wiederholungsfenster läuft nach 7 Tagen ab. Cloud Run Functions wiederholt neu erstellte ereignisgesteuerte Funktionen mit einer Strategie mit exponentiellem Backoff mit einem zunehmenden Backoff zwischen 10 und 600 Sekunden. Diese Richtlinie wird bei der ersten Bereitstellung auf neue Funktionen angewendet. Sie wird nicht rückwirkend auf vorhandene Funktionen angewendet, die zuerst bereitgestellt wurden, bevor die in diesem Versionshinweisen beschriebenen Änderungen wirksam wurden, auch wenn Sie die Funktionen noch einmal bereitstellen.

Wenn Wiederholungsversuche nicht für eine Funktion aktiviert sind (Standardeinstellung), meldet die Funktion immer, dass sie erfolgreich ausgeführt wurde. In den Logs können auch 200 OK-Antwortcodes angezeigt werden. Dies tritt auch dann auf, wenn bei der Funktion ein Fehler aufgetreten ist. Um deutlich zu machen, wann Ihre Funktion einen Fehler feststellt, müssen Sie Fehler entsprechend melden.

Warum ereignisgesteuerte Funktionen nicht abgeschlossen werden

In seltenen Fällen kann eine Funktion aufgrund eines internen Fehlers vorzeitig beendet werden. Je nach angegebener Standardeinstellung für diese Funktion werden dann Wiederholungsversuche mit der Funktion durchgeführt oder nicht.

Typischer ist das Fehlschlagen einer ereignisgesteuerten Funktion aufgrund von Fehlern, die im Funktionscode selbst ausgegeben werden. Dies kann folgende Gründe haben:

  • Die Funktion enthält einen Fehler und die Laufzeit löst eine Ausnahme aus
  • Die Funktion kann einen Dienstendpunkt nicht erreichen oder überschreitet während des Versuchs eine Zeitüberschreitung.
  • Die Funktion löst absichtlich eine Ausnahme aus (z. B. wenn die Validierung eines Parameters fehlschlägt).
  • Eine Node.js-Funktion gibt ein abgelehntes Versprechen zurück oder übergibt einen Nicht-null-Wert an einen Callback.

In allen obigen Fällen wird die Ausführung der Funktion standardmäßig gestoppt und das Ereignis wird verworfen. Um die Funktion bei einem Fehler noch einmal zu versuchen, können Sie die Standardrichtlinie für Wiederholungsversuche ändern, indem Sie das Attribut "Bei Fehler noch einmal versuchen" festlegen. Dadurch wird das Ereignis wiederholt, bis die Funktion erfolgreich abgeschlossen wurde oder das Zeitlimit für Wiederholungsversuche abläuft.

Wiederholungsversuche aktivieren oder deaktivieren

Zum Aktivieren oder Deaktivieren von Wiederholungsversuchen können Sie entweder das gcloud-Befehlszeilentool oder die Google Cloud Console verwenden. Standardmäßig sind Wiederholungsversuche deaktiviert.

Wiederholungsversuche über das gcloud-Befehlszeilentool konfigurieren

Fügen Sie zum Aktivieren von Wiederholungen über das gcloud-Befehlszeilentool beim Bereitstellen der Funktion das Flag --retry ein:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Stellen Sie zum Deaktivieren von Wiederholungsversuchen die Funktion ohne das Flag --retry noch einmal bereit:

gcloud functions deploy FUNCTION_NAME FLAGS...

Wiederholungen über die Console konfigurieren

Wenn Sie eine neue Funktion erstellen, gehen Sie so vor:

  1. Wählen Sie auf dem Bildschirm Funktion erstellen Trigger. Wählen Sie dann den Ereignistyp aus, der als Trigger für die Funktion fungieren soll.
  2. Markieren Sie das Kästchen Nach Fehler noch einmal versuchen, um Wiederholungsversuche zu aktivieren.

Wenn Sie eine vorhandene Funktion aktualisieren:

  1. Klicken Sie auf der Seite Cloud Run Functions – Übersicht auf den Namen der Funktion, die Sie aktualisieren möchten, um den Bildschirm Funktionsdetails zu öffnen. Wählen Sie Bearbeiten in der Menüleiste aus, um den Bereich Trigger aufzurufen.
  2. Markieren oder demarkieren Sie das Kästchen Bei Fehler noch einmal versuchen, um Wiederholungsversuche zu aktivieren oder zu deaktivieren.

Best Practices

In diesem Abschnitt werden Best Practices für den Einsatz von Wiederholungsversuchen beschrieben.

Wiederholungsversuche zur Behebung vorübergehender Fehler verwenden

Da Ihre Funktion bis zur erfolgreichen Ausführung kontinuierlich wiederholt wird, sollten Sie vor dem Aktivieren von Wiederholungsversuchen permanente Fehler wie Programmfehler mithilfe von Tests im Code beseitigen. Wiederholungsversuche sollten am besten für unregelmäßige oder vorübergehende Fehler verwendet werden, bei denen die Wahrscheinlichkeit einer Behebung bei einem erneuten Versuch hoch ist, z. B. bei unzuverlässigen Dienstendpunkten oder Zeitüberschreitungen.

Endbedingung festlegen, um Endlosschleifen zu verhindern

Beim Einsatz von Wiederholungsversuchen ist es eine Best Practice, die Funktion vor Endlosschleifen zu schützen. Dies erreichen Sie beispielsweise, indem Sie eine gut definierte Endbedingung einfügen, bevor die Verarbeitung der Funktion beginnt. Beachten Sie, dass diese Methode nur funktioniert, wenn Ihre Funktion erfolgreich gestartet wird und die Endbedingung auswerten kann.

Ein einfacher, aber effektiver Ansatz ist, Ereignisse zu verwerfen, deren Zeitstempel älter als ein bestimmtes Datum ist. Dadurch werden übermäßig viele Ausführungen verhindert, wenn Fehler entweder persistent oder langlebiger als erwartet sind.

Beispielsweise werden durch dieses Code-Snippet alle Ereignisse verworfen, die älter als 10 Sekunden sind:

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

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

Zwischen Funktionen, bei denen ein Wiederholungsversuch sinnvoll ist, und schwerwiegenden Fehlern unterscheiden

Wenn für Ihre Funktion Wiederholungsversuche aktiviert sind, löst jeder nicht aufgelöste Fehler einen Wiederholungsversuch aus. Achten Sie darauf, dass in Ihrem Code die Fehler erfasst sind, für die keine Wiederholungsversuche ausgelöst werden sollten.

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

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

Wiederholbare ereignisgesteuerte Funktionen idempotent machen

Ereignisgesteuerte Funktionen, für die Wiederholungsversuche gemacht werden können, müssen idempotent sein. Hier sind einige allgemeine Richtlinien, wie Sie eine solche Funktion idempotent machen:

  • Bei vielen externen APIs (z. B. Stripe) können Sie einen Idempotenzschlüssel als Parameter bereitstellen. Wenn Sie eine solche API verwenden, sollten Sie die Ereignis-ID als Idempotenzschlüssel verwenden.
  • Idempotenz funktioniert gut bei mindestens einmaliger Übermittlung, weil Wiederholungsversuche dadurch problemlos erfolgen können. Eine allgemeine Best Practice für das Schreiben von zuverlässigem Code ist also, Idempotenz mit Wiederholungsversuchen zu kombinieren.
  • Achten Sie darauf, dass der Code intern idempotent ist. Beispiele:
    • Sorgen Sie dafür, dass Mutationen mehr als einmal passieren können, ohne das Ergebnis zu verändern.
    • Fragen Sie den Datenbankstatus in einer Transaktion ab, bevor der Status mutiert wird.
    • Stellen Sie sicher, dass alle Nebenwirkungen selbst idempotent sind.
  • Erzwingen Sie eine Transaktionsprüfung außerhalb der Funktion und unabhängig vom Code. Beispiel: Zustand an einem Punkt persistent machen, um aufzuzeichnen, dass eine bestimmte Ereignis-ID bereits verarbeitet wurde.
  • Nutzen Sie ein Verfahren für doppelte Funktionsaufrufe "out of band". Verwenden Sie beispielsweise einen separaten Bereinigungsprozess, der nach doppelten Funktionsaufrufen eine Bereinigung durchführt.

Wiederholungsrichtlinie konfigurieren

Je nach den Anforderungen Ihrer Funktion können Sie die Wiederholungsrichtlinie direkt konfigurieren. So können Sie eine beliebige Kombination der folgenden Elemente einrichten:

  • Verkürzen Sie das Zeitfenster für den erneuten Versuch von 7 Tagen auf bis zu nur 10 Minuten.
  • Ändern Sie die minimale und maximale Wartezeit für die Wiederholungsstrategie mit exponentiellem Backoff.
  • Ändern Sie die Wiederholungsstrategie so, dass umgehend ein erneuter Versuch unternommen wird.
  • Konfigurieren Sie ein Thema für unzustellbare Nachrichten.
  • Legen Sie eine maximale und minimale Anzahl von Zustellversuchen fest.

So konfigurieren Sie die Wiederholungsrichtlinie:

  1. Schreiben Sie eine HTTP-Funktion.
  2. Nutzen Sie die Pub/Sub API, um ein Pub/Sub-Abo erstellen und die URL der Funktion als Ziel anzugeben

Weitere Informationen zum direkten Konfigurieren von Pub/Sub finden Sie in der Pub/Sub-Dokumentation zur Fehlerbehandlung.

Weitere Informationen