Ative as novas tentativas de funções orientadas por eventos

Este documento descreve como ativar as novas tentativas para funções do Cloud Run baseadas em eventos. As novas tentativas automáticas não estão disponíveis para funções HTTP.

Por que motivo as funções acionadas por eventos não são concluídas

Em raras ocasiões, uma função pode terminar prematuramente devido a um erro interno e, por predefinição, a função pode ou não ser repetida automaticamente.

Normalmente, uma função acionada por eventos pode não ser concluída com êxito devido a erros gerados no próprio código da função. Os motivos pelos quais isto pode acontecer incluem:

  • A função contém um erro e o tempo de execução gera uma exceção.
  • A função não consegue alcançar um ponto final de serviço ou excede o tempo limite ao tentar fazê-lo.
  • A função gera intencionalmente uma exceção (por exemplo, quando um parâmetro falha a validação).
  • Uma função Node.js devolve uma promessa rejeitada ou passa um valor não-null para um retorno.

Em qualquer um dos casos acima, a função deixa de ser executada e devolve um erro. Os acionadores de eventos que produzem as mensagens têm políticas de repetição que pode personalizar para satisfazer as necessidades da sua função.

Semântica da nova tentativa

As funções do Cloud Run oferecem uma execução, pelo menos, uma vez de uma função orientada por eventos para cada evento emitido por uma origem de eventos. A forma como configura as novas tentativas depende de como criou a função:

  • As funções criadas na Google Cloud consola ou com a API Cloud Run Admin exigem que crie e faça a gestão dos acionadores de eventos separadamente. Os acionadores têm comportamentos de repetição predefinidos que pode personalizar de acordo com as necessidades da sua função.
  • As funções criadas com a API Cloud Functions v2 criam implicitamente os acionadores de eventos necessários, por exemplo, tópicos do Pub/Sub ou acionadores do Eventarc. Por predefinição, as novas tentativas estão desativadas para estes acionadores e podem ser reativadas através da API Cloud Functions v2.

Funções orientadas por eventos criadas com o Cloud Run

As funções criadas na Google Cloud consola ou com a API Cloud Run Admin requerem que crie e faça a gestão dos acionadores de eventos separadamente. Recomendamos vivamente que reveja o comportamento predefinido de cada tipo de acionador:

Funções orientadas por eventos criadas com a Cloud Functions v2 API

As funções criadas através da API Cloud Functions v2; por exemplo, através da CLI gcloud do Cloud Functions, da API REST ou do Terraform, criam e gerem acionadores de eventos em seu nome. Por predefinição, se uma invocação de função terminar com um erro, a função não é invocada novamente e o evento é ignorado. Quando ativa as novas tentativas numa função acionada por eventos, as funções do Cloud Run repetem uma invocação de função com falha até ser concluída com êxito ou o período de novas tentativas expirar.

Quando as novas tentativas não estão ativadas para uma função, que é a predefinição, a função comunica sempre que foi executada com êxito e os códigos de resposta 200 OK podem aparecer nos respetivos registos. Isto ocorre mesmo que a função tenha encontrado um erro. Para deixar claro quando a sua função encontra um erro, certifique-se de que comunica os erros adequadamente.

Ative ou desative as novas tentativas

Para ativar ou desativar as novas tentativas, pode usar a Google Cloud CLI. Por predefinição, as repetições estão desativadas.

Configure as novas tentativas a partir da CLI do Google Cloud

Para ativar as novas tentativas através da Google Cloud CLI, inclua a flag --retry ao implementar a função:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Para desativar as novas tentativas, volte a implementar a função sem a flag --retry:

gcloud functions deploy FUNCTION_NAME FLAGS...

Período de nova tentativa

Esta janela de nova tentativa expira após 24 horas. As funções do Cloud Run repetem as funções acionadas por eventos recém-criadas através de uma estratégia de recuo exponencial, com um recuo crescente entre 10 e 600 segundos.

Práticas recomendadas

Esta secção descreve as práticas recomendadas para usar novas tentativas.

Use a repetição para processar erros momentâneos

Uma vez que a sua função é repetida continuamente até à execução bem-sucedida, os erros permanentes, como erros de programação, devem ser eliminados do seu código através de testes antes de ativar as repetições. As novas tentativas são mais adequadas para lidar com falhas intermitentes ou transitórias que têm uma elevada probabilidade de resolução após uma nova tentativa, como um ponto final de serviço instável ou um limite de tempo.

Defina uma condição de fim para evitar ciclos de repetição infinitos

É uma prática recomendada proteger a sua função contra ciclos contínuos quando usa novas tentativas. Pode fazê-lo incluindo uma condição de fim bem definida antes de a função começar o processamento. Tenha em atenção que esta técnica só funciona se a função for iniciada com êxito e conseguir avaliar a condição final.

Uma abordagem simples, mas eficaz, consiste em rejeitar eventos com data/hora anteriores a um determinado período. Isto ajuda a evitar execuções excessivas quando as falhas são persistentes ou duram mais tempo do que o esperado.

Por exemplo, este fragmento do código rejeita todos os eventos com mais de 10 segundos:

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

Ir


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

Distinguir entre funções que podem ser repetidas e erros fatais

Se a sua função tiver repetições ativadas, qualquer erro não processado aciona uma repetição. Certifique-se de que o seu código capta todos os erros que não devem resultar numa nova tentativa.

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

Ir

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

Torne as funções acionadas por eventos repetíveis idempotentes

As funções acionadas por eventos que podem ser repetidas têm de ser idempotentes. Seguem-se algumas diretrizes gerais para tornar uma função deste tipo idempotente:

  • Muitas APIs externas (como a Stripe) permitem fornecer uma chave de idempotência como um parâmetro. Se estiver a usar uma API deste tipo, deve usar o ID do evento como chave de idempotência.
  • A idempotência funciona bem com o fornecimento pelo menos uma vez, porque torna a repetição segura. Assim, uma prática recomendada geral para escrever código fiável é combinar a idempotência com as novas tentativas.
  • Certifique-se de que o seu código é internamente idempotente. Por exemplo:
    • Certifique-se de que as mutações podem ocorrer mais do que uma vez sem alterar o resultado.
    • Consultar o estado da base de dados numa transação antes de alterar o estado.
    • Certifique-se de que todos os efeitos secundários são idempotentes.
  • Impor uma verificação transacional fora da função, independentemente do código. Por exemplo, manter o estado num local que registe que um determinado ID do evento já foi processado.
  • Lidar com chamadas de funções duplicadas fora da banda. Por exemplo, ter um processo de limpeza separado que limpe após chamadas de funções duplicadas.

Configure a política de repetição

Consoante as necessidades da sua função do Cloud Run, pode querer configurar a política de repetição diretamente. Isto permite-lhe configurar qualquer combinação do seguinte:

  • Reduzir o período de nova tentativa de 7 dias para apenas 10 minutos.
  • Altere o tempo de recuo mínimo e máximo para a estratégia de repetição de recuo exponencial.
  • Altere a estratégia de repetição para repetir imediatamente.
  • Configure um tópico de mensagens não entregues.
  • Defina um número máximo e mínimo de tentativas de entrega.

Para configurar a política de repetição:

  1. Escrever uma função HTTP.
  2. Use a API Pub/Sub para criar uma subscrição do Pub/Sub, especificando o URL da função como o destino.

Consulte a documentação do Pub/Sub sobre como processar falhas para obter mais informações sobre a configuração direta do Pub/Sub.

Passos seguintes