Como repetir funções em segundo plano

Neste documento, descrevemos como ativar a recuperação de funções em segundo plano. A nova tentativa automática não está disponível para funções HTTP.

Semântica da nova tentativa

O Cloud Functions garante que uma função de segundo plano será executada para cada evento emitido por uma fonte de eventos pelo menos uma vez. No entanto, por padrão, se uma chamada de função terminar com um erro, a função não será chamada novamente e o evento será descartado. Quando você permite novas tentativas de uma função de segundo plano, o Cloud Functions tentará chamar a função com falha novamente até que ela seja concluída com êxito ou até que a janela de novas tentativas (7 dias por padrão) expire.

Por que funções de segundo plano não são concluídas?

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

Normalmente, uma função de segundo plano pode não ser concluída com sucesso devido a erros gerados no próprio código da função. Veja a seguir alguns motivos desses erros:

  • A função contém um bug e o ambiente de execução gera uma exceção.
  • A função não consegue alcançar um ponto de extremidade do serviço ou expira durante a tentativa.
  • A função gera uma exceção intencionalmente (por exemplo, quando há falha na validação de um parâmetro).
  • Quando as funções escritas em Node.js retornam uma promessa rejeitada ou passam um null sem valor para um retorno de chamada.

Em qualquer um dos casos acima, a função para a execução por padrão e o evento é descartado. Se quiser executar a função novamente quando ocorre um erro, é possível alterar a política padrão de nova tentativa padrão. Para isso, ative a propriedade "Tentar novamente em caso de falha". Isso faz com que o evento seja repetido durante vários dias até que a função seja concluída com êxito.

Ativar e desativar novas tentativas

Para ativar ou desativar novas tentativas, é possível usar a ferramenta de linha de comando gcloud ou o Console do Cloud. Por padrão, as novas tentativas estão desativadas.

Como usar a ferramenta de linha de comando gcloud

Para ativar novas tentativas através da ferramenta de linha de comando gcloud, inclua a sinalização --retry ao implantar sua função:

gcloud functions deploy FUNCTION_NAME --retry FLAGS...

Para desativar novas tentativas, reimplante a função sem a sinalização --retry:

gcloud functions deploy FUNCTION_NAME FLAGS...

Como usar o Cloud Console

É possível ativar ou desativar novas tentativas no Console do Cloud da seguinte forma:

  1. Acesse a página Visão geral do Cloud Functions no console do Cloud Platform.

  2. Clique em Criar função. Como alternativa, clique em uma função existente para ir para a página de detalhes dela e clique em Editar.

  3. Preencha os campos obrigatórios para a função.

  4. No campo Acionador, selecione um tipo de acionador de função de segundo plano, como Cloud Pub/Sub ou Cloud Storage.

  5. Expanda as configurações avançadas clicando em Mais.

  6. Marque ou desmarque a caixa de seleção Tentar novamente em caso de falha.

Práticas recomendadas

Nesta seção, você verá as práticas recomendadas para o uso de novas tentativas.

Usar novas tentativas para lidar com erros transitórios

Como novas tentativas de executar a função são realizadas continuamente até a conclusão bem-sucedida, erros permanentes, como bugs, devem ser eliminados do código por meio de teste detalhado antes de ativá-las. As tentativas funcionam melhor para lidar com falhas intermitentes ou transitórias que têm alta probabilidade de serem solucionadas ao tentar novamente, como um endpoint de serviço ou tempo limite lentos.

Definir uma condição final para evitar loops infinitos de tentativas

A prática recomendada ao usar tentativas é proteger a função contra loop contínuo. Para isso, inclua uma condição final bem definida antes de a função iniciar o processamento. Essa técnica só funciona se sua função for iniciada com sucesso e puder avaliar a condição final.

Uma abordagem simples e eficaz é descartar eventos com carimbos de data/hora anteriores a um determinado momento. Isso ajuda a evitar o excesso de execuções quando as falhas são persistentes ou mais duradouras do que o esperado.

Por exemplo, este snippet de código descarta todos os eventos ocorridos há mais de 10 segundos:

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.`);
  callback();
};

Python

from datetime import datetime, timezone
# The 'python-dateutil' package must be included in requirements.txt.
from dateutil import parser

def avoid_infinite_retries(data, context):
    """Background Cloud Function that only executes within a certain
    time period after the triggering event.

    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): The event metadata.
    Returns:
        None; output is written to Stackdriver Logging
    """

    timestamp = context.timestamp

    event_time = parser.parse(timestamp)
    event_age = (datetime.now(timezone.utc) - event_time).total_seconds()
    event_age_ms = event_age * 1000

    # Ignore events that are too old
    max_age_ms = 10000
    if event_age_ms > max_age_ms:
        print('Dropped {} (age {}ms)'.format(context.event_id, event_age_ms))
        return 'Timeout'

    # Do what the function is supposed to do
    print('Processed {} (age {}ms)'.format(context.event_id, event_age_ms))
    return

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/functions/metadata"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// FiniteRetryPubSub demonstrates how to avoid inifinite retries.
func FiniteRetryPubSub(ctx context.Context, m PubSubMessage) error {
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		// Assume an error on the function invoker and try again.
		return fmt.Errorf("metadata.FromContext: %v", err)
	}

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

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

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
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 = utcNow;

    String data = message.getData();
    JsonObject body = gson.fromJson(data, JsonObject.class);
    if (body != null && body.has("timestamp")) {
      String tz = body.get("timestamp").getAsString();
      timestamp = ZonedDateTime.parse(tz);
    }
    long eventAge = Duration.between(timestamp, utcNow).toMillis();

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

    // Process events that are recent enough
    logger.info(String.format("Processing event %s.", data));
  }
}

Distinguir entre erros que podem ser tentados novamente e fatais

Se a função tiver novas tentativas ativadas, qualquer erro não processado acionará uma nova tentativa. Verifique se o código captura todos os erros que não precisam resultar em uma nova tentativa.

Node.js

/**
 * Background Cloud Function that demonstrates
 * how to toggle retries using a promise
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data Data included with the event.
 * @param {object} event.data.retry User-supplied parameter that tells the function whether to retry.
 */
exports.retryPromise = (event) => {
  const tryAgain = !!event.data.retry;

  if (tryAgain) {
    throw new Error(`Retrying...`);
  } else {
    return Promise.reject(new Error('Not retrying...'));
  }
};

/**
 * Background Cloud Function that demonstrates
 * how to toggle retries using a callback
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data Data included with the event.
 * @param {object} event.data.retry User-supplied parameter that tells the function whether to retry.
 * @param {function} callback The callback function.
 */
exports.retryCallback = (event, callback) => {
  const tryAgain = !!event.data.retry;
  const err = new Error('Error!');

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

Python

from google.cloud import error_reporting
error_client = error_reporting.Client()

def retry_or_not(data, context):
    """Background Cloud Function that demonstrates how to toggle retries.

    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): The event metadata.
    Returns:
        None; output is written to Stackdriver Logging
    """

    # Retry based on a user-defined parameter
    try_again = data.data.get('retry') is not None

    try:
        raise RuntimeError('I failed you')
    except RuntimeError:
        error_client.report_exception()
        if try_again:
            raise  # Raise the exception and try again
        else:
            pass   # Swallow the exception and don't retry

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"errors"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// RetryPubSub demonstrates how to toggle using retries.
func RetryPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data)
	if name == "" {
		name = "World"
	}

	// A misconfigured client will stay broken until the function is redeployed.
	client, err := MisconfiguredDataClient()
	if err != nil {
		log.Printf("MisconfiguredDataClient (retry denied):  %v", err)
		// A nil return indicates that the function does not need a retry.
		return nil
	}

	// Runtime error might be resolved with a new attempt.
	if err = FailedWriteOperation(client, name); err != nil {
		log.Printf("FailedWriteOperation (retry expected): %v", err)
		// A non-nil return indicates that a retry is needed.
		return err
	}

	return nil
}

Java


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

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

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

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

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

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

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

Tornar idempotentes funções em segundo plano que podem ser repetidas

As funções de segundo plano para as quais se pode tentar uma nova execução precisam ser idempotentes. Veja abaixo algumas diretrizes gerais para tornar uma função de segundo plano idempotente:

  • Muitas APIs externas, como a Stripe, permitem que você forneça uma chave de idempotência como um parâmetro. Se estiver usando uma API como essa, utilize o código do evento como chave de idempotência.
  • A idempotência funciona bem com a entrega do tipo "pelo menos uma vez" porque torna mais seguro tentar novamente. Dessa forma, para escrever um código confiável, a prática recomendada é combinar idempotência com tentativas.
  • Verifique se o código é idempotente internamente. Por exemplo:
    • Garanta que mutações possam ocorrer mais de uma vez sem alterar o resultado.
    • Consulte o estado do banco de dados em uma transação antes de alterar o estado.
    • Certifique-se de que todos os efeitos colaterais sejam idempotentes.
  • Execute uma verificação transacional fora da função, independente do código. Por exemplo, mantenha a persistência de estado em algum local e registre que um determinado código de evento já foi processado.
  • Lide com chamadas de função duplicadas fora de banda. Por exemplo, tenha um processo de limpeza que seja executado após chamadas de função duplicadas.

A seguir