Transacciones

Datastore admite transacciones. Una transacción es una operación (o un conjunto de operaciones) atómica: se producen todas las operaciones de la transacción o no se produce ninguna. Una aplicación puede realizar varias operaciones y cálculos en una única transacción.

Usa transacciones

Una transacción es un conjunto de operaciones de Datastore en una o más entidades. Se garantiza que toda transacción es atómica, es decir, que nunca se aplica de forma parcial. Se aplican todas las operaciones de la transacción o ninguna de ellas. Las transacciones tienen una duración máxima de 60 segundos, con un tiempo de caducidad por inactividad de 10 segundos una vez transcurridos 30 segundos.

Las operaciones pueden fallar cuando se presenta alguna de las siguientes situaciones:

  • Se intentan aplicar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Se produce un error interno en Datastore.

En todos estos casos, la API de Datastore muestra un error.

Las transacciones son una función opcional de Datastore; no es necesario que las uses para realizar operaciones de Datastore.

La función datastore.RunInTransaction ejecuta la función proporcionada en una transacción.


package counter

import (
	"context"
	"fmt"
	"net/http"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
)

func init() {
	http.HandleFunc("/", handler)
}

type Counter struct {
	Count int
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	key := datastore.NewKey(ctx, "Counter", "mycounter", 0, nil)
	count := new(Counter)
	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		// Note: this function's argument ctx shadows the variable ctx
		//       from the surrounding function.
		err := datastore.Get(ctx, key, count)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		count.Count++
		_, err = datastore.Put(ctx, key, count)
		return err
	}, nil)
	if err != nil {
		log.Errorf(ctx, "Transaction failed: %v", err)
		http.Error(w, "Internal Server Error", 500)
		return
	}

	fmt.Fprintf(w, "Current count: %d", count.Count)
}

Si la función muestra nil, RunInTransaction intenta confirmar la transacción y muestra nil si tiene éxito. Si la función muestra un valor de error que no es nil, no se aplican los cambios de Datastore y RunInTransaction muestra el mismo error.

Si RunInTransaction no puede confirmar la transacción por un conflicto, lo intenta de nuevo y deja de hacerlo después de tres intentos. Esto implica que la función de transacción debe ser idempotente, lo que significa que tiene el mismo resultado cuando se ejecuta varias veces. Ten en cuenta que datastore.Get no es idempotente cuando se deserializan los campos de segmentos.

¿Qué se puede hacer en una transacción?

Datastore impone restricciones sobre lo que se puede hacer dentro de una sola transacción.

Todas las operaciones de Datastore en una transacción deben operar en entidades dentro del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades en un máximo de veinticinco grupos de entidades si se trata de una transacción entre grupos. Esto incluye las consultas de entidades por entidad principal, la recuperación de entidades por clave y la actualización y eliminación de entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades separado, de modo que una única transacción no puede crearse ni operar en más de una entidad raíz, a menos que sea una transacción entre grupos.

Cuando dos o más transacciones intentan modificar entidades de forma simultánea en uno o más grupos de entidades comunes, solo la primera transacción que confirme los cambios se realizará correctamente, y la confirmación de las demás fallará. Debido a este diseño, el uso de grupos de entidades limita la cantidad de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de simultaneidad optimista mediante la verificación de la última hora de actualización de los grupos de entidades que se usan en la transacción. Luego de confirmar una transacción en los grupos de entidades, Datastore verifica una vez más la última hora de actualización de los grupos de entidades que se usan en la transacción. Si la hora se cambió desde la verificación inicial, se muestra un error.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más cercano al de las lecturas confirmadas. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que una transacción no puede modificar de forma simultánea los datos que lee o modifica otra transacción.

En una transacción, todas las operaciones de lectura reflejan el estado coherente actual de Datastore en el momento en que comenzó la transacción. Se garantiza que las consultas y las operaciones GET dentro de una transacción vean una instantánea única y coherente de Datastore desde el comienzo de la transacción. Las filas de las entidades y de los índices en el grupo de entidades de la transacción se actualizan por completo para que las consultas muestren el conjunto completo y correcto de las entidades del resultado, sin los falsos positivos ni falsos negativos que pueden generarse en las consultas realizadas fuera de las transacciones.

La vista de esta instantánea coherente también se extiende a las lecturas posteriores a las escrituras dentro de las transacciones. A diferencia de la mayoría de las bases de datos, las consultas y las operaciones GET en una transacción de Datastore no ven los resultados de las operaciones de escritura anteriores dentro de esa transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una operación GET o una consulta mostrará la versión original de la entidad según su estado al comienzo de la transacción, o no mostrará nada si la entidad no existía en ese momento.

Uso de las transacciones

Este ejemplo demuestra que uno de los usos de las transacciones es la actualización de una entidad con un valor de propiedad nuevo relativo a su valor actual.

func increment(ctx context.Context, key *datastore.Key) error {
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		count := new(Counter)
		if err := datastore.Get(ctx, key, count); err != nil {
			return err
		}
		count.Count++
		_, err := datastore.Put(ctx, key, count)
		return err
	}, nil)
}

Esta técnica requiere una transacción porque otro usuario puede actualizar el valor luego de que este código recupere el objeto, pero guarda el objeto modificado antes. Sin una transacción, la solicitud del usuario usa el valor que tenía count antes de la actualización del otro usuario, y el valor guardado reemplaza el valor nuevo. Mediante una transacción, se le notifica a la aplicación que otro usuario realizó una actualización. Si se actualiza la entidad durante la transacción, se realizan los reintentos necesarios de la transacción hasta que todos los pasos se hayan completado sin interrupción.

Las transacciones también se suelen usar para recuperar una entidad con una clave con nombre o crearla si aún no existe:

type Account struct {
	Address string
	Phone   string
}

func GetOrUpdate(ctx context.Context, id, addr, phone string) error {
	key := datastore.NewKey(ctx, "Account", id, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		acct := new(Account)
		err := datastore.Get(ctx, key, acct)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		acct.Address = addr
		acct.Phone = phone
		_, err = datastore.Put(ctx, key, acct)
		return err
	}, nil)
}

Como antes, se necesita una transacción para abordar el caso en el que otro usuario intenta crear o actualizar una entidad con el mismo ID de string. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, la segunda reemplazará a la primera sin que se sepa que eso sucedió.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que tenga éxito, o bien puedes dejar que tus usuarios gestionen el error. Para ello, deberás propagarlo al nivel de la interfaz de usuario de la aplicación. No es necesario crear un bucle de reintento en cada transacción.

Por último, puedes usar una transacción para leer una instantánea coherente de Datastore. Esto puede ser útil cuando se necesitan varias operaciones lecturas para renderizar una página o exportar datos que deben ser coherentes. Estos tipos de transacciones suelen llamarse de solo lectura, ya que no realizan operaciones de escritura. Las transacciones de solo lectura que se realizan en un solo grupo no fallan nunca debido a modificaciones simultáneas, por lo que no debes implementar reintentos en caso de fallas. Sin embargo, las transacciones que se realizan entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deben incluir reintentos. Tanto confirmar como revertir transacciones de solo lectura constituyen operaciones no-ops.

Tareas transaccionales en cola

Puedes poner una tarea en cola como parte de una transacción de Datastore para que la tarea solo esté en cola (y se garantice que está en cola) si la transacción se confirma de forma correcta. Una vez en cola, no se garantiza que la tarea se ejecute de inmediato; por lo tanto, la tarea no es atómica con la transacción. Sin embargo, una vez en cola, la tarea se reintentará hasta que tenga éxito. Esto se aplica a cualquier tarea en cola durante una función RunInTransaction.

Las tareas transaccionales son útiles porque te permiten combinar acciones que no son de Datastore con una transacción que depende de su realización con éxito (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular acciones de Datastore a la transacción, por ejemplo, para confirmar los cambios en los grupos de entidades fuera de la transacción, siempre y cuando la transacción tenga éxito.

Una aplicación no puede insertar más de cinco tareas transaccionales en las listas de tareas en cola durante una sola transacción. Las tareas transaccionales no deben tener nombres especificados por el usuario.

datastore.RunInTransaction(ctx, func(ctx context.Context) error {
	t := &taskqueue.Task{Path: "/path/to/worker"}
	if _, err := taskqueue.Add(ctx, t, ""); err != nil {
		return err
	}
	// ...
	return nil
}, nil)