https://github.com/captaincodeman/datastore-locker
A lock / lease mechanism to prevent duplicate execution of AppEngine tasks
https://github.com/captaincodeman/datastore-locker
appengine appengine-tasks datastore-locker duplicate-tasks lease lock
Last synced: 3 months ago
JSON representation
A lock / lease mechanism to prevent duplicate execution of AppEngine tasks
- Host: GitHub
- URL: https://github.com/captaincodeman/datastore-locker
- Owner: CaptainCodeman
- License: apache-2.0
- Created: 2016-07-22T18:09:28.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2017-03-08T20:33:37.000Z (about 8 years ago)
- Last Synced: 2025-01-08T14:15:27.353Z (4 months ago)
- Topics: appengine, appengine-tasks, datastore-locker, duplicate-tasks, lease, lock
- Language: Go
- Homepage:
- Size: 20.5 KB
- Stars: 1
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
Awesome Lists containing this project
README
# Datastore Locker
Provides a lock / lease mechanism to prevent duplicate execution of
appengine tasks and an easy way to continue long-running processes. Used
by [datastore-mapper](https://github.com/CaptainCodeman/datastore-mapper).Sometimes it's extremely difficult if not impossible to make tasks truly
idempotent. e.g. if your task sends an email or charges a credit card
then executing it twice could be 'a bad thing'. Actually preventing a task
from running more than once can be challenging though ...Although named tasks can be used to prevent duplicate tasks being *enqueued*
they cannot be used together with datastore transactions which leaves you
with the possibility that either the task scheduling or the datastore write
could fail.An unnamed task *can* be enqueued within a transaction to ensure that both
happen or neither happen but that then leaves the possibility of *that*
operation being repeated (if running in it's own task) which could cause
duplicate tasks.Whichever approach is used, the taskqueue only promises *at-least-once*
delivery so there is also the chance that appengine will execute a task more
than once.The solution is to coordinate execution using information in the task with
information in a datastore entity. This package aims to make it easy to
restrict task execution by using a lease / lock mechanism.By obtaining a lock on an entity within a datastore transaction we ensure
that only a single instance of any task will be executed at once and, once
processed, that duplicate execution will be prevented.It can be used with single tasks or to chain a series of tasks in sequence
with the sequence number used to prevent any old tasks being re-executed.An exceptional situation can occur if a failure happens during processing
of a task and the result cannot be communicated back to appengine (this is
a platform issue). In this case the lock / lease is already held but the
system cannot determine if the task completed or maybe it just failed to
clear the lock. The locker will allow a timeout before querying the appengine
logs to determine the task status. In the case of a complete failure with
no log information, a timeout will prevent deadlock by overwriting the
expired lock / lease.Both overwritten locks and permanently failing tasks (past a configurable
number of retries) can be alerted by email as needing further investigation.## Usage
See the example project for a simple demonstration of locker being used.Embed the `locker.Lock` field within the struct you want lock on.
Foo struct {
locker.Struct
Value string `datastore:"value"`
}Create an instance of the locker and configure as required:
l := locker.NewLocker()
l := locker.NewLocker(
locker.LeaseDuration(time.Duration(5)*time.Minute),
locker.LeaseTimeout(time.Duration(15)*time.Minute),
locker.AlertOnOverwrite,
)l := locker.NewLocker(locker.LogVerbose)
Schedule a task to be executed once:
key := datastore.NewKey(c, "foo", "", 1, nil)
entity := &Foo{Value:"bar"}
err := l.Schedule(c, key, entity, "/task/handler/url", nil)
if err != nil {
// operation failed (entity not saved and task not enqueued)
}Handle the task execution:
func init() {
http.Handle("/task/handler/url", locker.Handle(fooHandler, fooFactory)
}// the task handler needs a factory to construct an instance of our entity
func fooFactory() interface{} {
return new(Foo)
}// the handler for the task will be passed the appengine context, request, datastore key and entity
func foohandler(c context.Context, r *http.Request, key *datastore.Key, entity locker.Lockable) error {
foo := entity.(*Foo)switch foo.Sequence {
case 1:
// step 1 processing, e.g. charge credit card
// schedule another task to follow this one:
return l.Schedule(c, key, entity, "/task/handler/url", nil)
case 2:
// step 2 processing, e.g. send confirmation email
// mark the task as completed (to prevent the last task re-executing)
return l.Complete(c, key, entity)
}// returning an error would cause the task to be failed and retried (normal task semantics)
// a configurable number of retries can be set to prevent endless attempts from happening
return nil
}