Efficient Use of Asynchronous Operations in Google App Engine

Today's guest post comes from Jason Vertrees, Ph.D., CTO of RealMassive, and David Lippa, Principal Software Engineer. RealMassive provides real-time commercial real estate information and is powered by Google App Engine.

Multiprocessing and multithreading are a mainstay of modern applications. Google App Engine provides two primary means of parallel execution, the tasklet and deferred task. In this post we’ll go deep into both technologies and examine when to use tasklets or deferred tasks for asynchronous operations.

A tasklet is a function that is executed asynchronously in the application’s event loop until a yield statement is reached, which instructs the scheduler to move to the next call. A common use case here is to asynchronously fetch an entity from the datastore while performing additional tasks, and only blocking when that entity is needed. Tasklets also operate in a standard fashion within the context of a transaction, and can be decorated as such: operate non-transactionally, continue the existing transaction, or start a new one. They also execute during the HTTP request that spawned them, which can cause the request’s 60 second deadline to be exceeded. They are much more like a Thread executing within the context of a traditional ThreadPool, and must be managed similarly.

A deferred task is a "fire-and-forget," long-running idempotent operation whose return value is ignored, and can execute long after the HTTP request that spawned it. RealMassive often uses deferred tasks for sending out email digests, which must first be split into batches. In this respect, a deferred task is more similar to a Mapper or Reducer from Mapreduce than a Thread. Tasks are added to a push or pull "task queue" and run based on the rules governing that queue1[citation]. Since deferred tasks can execute for an extended time — often 10 minutes or more — they must behave very differently when decorated as transactional. Only 5 transactional tasks may be added within a given transaction, and they are executed before the transaction ends, and not necessarily immediately.

What’s a developer to do when a long-running operation must execute transactionally? Let’s take a look at a real example that RealMassive faced using a simplified, normalized version of our data model; something straightforward, without being overly simplified.

First, a few notes:
  • All entities are tagged with a NDB key to the User who performed an operation on that entity
  • A Space represents a suite in a Building. It’s located on a floor of the Building, and has a size in square feet. It may also have attachments, like images and videos
  • A Building represents a physical Building, with its own attributes (such as total square footage, owner, etc.) and a street address.
  • Spaces and Buildings have mutual references to one another.

from google.appengine.ext import ndb, deferred

class User(ndb.Model):
  username = ndb.StringProperty()
  email = ndb.StringProperty()

class BaseModel(ndb.Model):
  created_by = ndb.KeyProperty(User)
  edited_by = ndb.KeyProperty(User, repeated=True)

class Media(BaseModel):
  url = ndb.StringProperty()
  edited_by = ndb.KeyProperty(User, repeated=True)

class Address(ndb.StructuredProperty):
  street = ndb.StringProperty()
  city = ndb.StringProperty()
  state = ndb.StringProperty()
  zipcode = ndb.StringProperty()

class Building(BaseModel):
  address = ndb.StructuredProperty(Address)
  size = ndb.IntegerProperty()
  # prevent cyclical dependencies
  space_keys = ndb.KeyProperty(kind=’Space’, repeated=True)
  owner = ndb.KeyProperty(User)

    def remove(self):
       for s in space_keys:
         s.get().remove()
       self.key.delete()

class Space(BaseModel):
  building = ndb.KeyProperty(Building)
  sf_available = ndb.IntegerProperty()
  floor_number = ndb.IntegerProperty()
  suite_number = ndb.IntegerProperty()
  attachments = ndb.KeyProperty(Media, repeated=True)

    def remove(self):
        for a in attachments:
           a.delete()


So back to the original question: how do we handle a long-running task that must be executed transactionally — such as deleting a Building from NDB with this data model? As is the case with NoSQL, cascading deletes must be made programmatically in a transaction, descending to subsequent levels of the dependency tree as appropriate. There are a few flaws in our current implementation:

  • For a Building with many Spaces, the removal process could cause a DeadlineExceededError to occur, since the processes are performed in the same thread that is executing the HTTP request.
  • The removals are not transactional: if a single operation fails, there can be orphans.
  • Media can be orphaned as well, since there’s no reference counting with the Media

From the looks of these flaws, the correct answer may be just to execute the deletions in a deferred task, and decorate the remove() calls with ndb.transactional. Let’s see what that would look like:

class Building(BaseModel):
  address = ndb.StructuredProperty(Address)
  size = ndb.IntegerProperty()
  # prevent cyclical dependencies
  space_keys = ndb.KeyProperty(kind=’Space’, repeated=True)
  owner = ndb.KeyProperty(User)

  @ndb.transactional(xg=True)
  def remove(self):
      for s in space_keys:
         deferred.defer(s.get().remove)
      self.key.delete()

class Space(BaseModel):
   building = ndb.KeyProperty(Building)
   sf_available = ndb.IntegerProperty()
   floor_number = ndb.IntegerProperty()
   suite_number = ndb.IntegerProperty()
   attachments = ndb.KeyProperty(Media, repeated=True)

   @ndb.transactional(xg=True)
   def remove(self):
     for a in attachments:
        deferred.defer(a.delete)


There are a number of problems with this approach:

  • If a queue supports simultaneous operations, there’s no way to guarantee deletion order unless the queue rate is set to 1 per second.
  • Building.remove() could exceed deadlines since it calls get() before delegating to Space.remove().
  • The semantics of this code is very confusing, since it appears that everything is done within the same transaction.
  • It won’t scale. If a Building has more than 5 Spaces, this could exceed the limit on the number of deferred tasks within a transaction. Also, cross-group transactions are limited to 30 seconds of clock time.

The answer lies in using tasklets for most operations, and allowing the caller to determine if the operation should be deferred or not:

class Building(BaseModel):
  address = ndb.StructuredProperty(Address)
  size = ndb.IntegerProperty()
  # prevent cyclical dependencies
  space_keys = ndb.KeyProperty(kind=’Space’, repeated=True)
  owner = ndb.KeyProperty(User)

  @ndb.transactional(xg=True)
  @ndb.tasklet

  def remove(self):
     futures = []
     for s in space_keys:
        sp = yield s.get_async()
        futures.extend(sp.remove_async())

     ndb.wait_all(futures)
     self.key.delete()

class Space(BaseModel):
  building = ndb.KeyProperty(Building)
  sf_available = ndb.IntegerProperty()
  floor_number = ndb.IntegerProperty()
  suite_number = ndb.IntegerProperty()
  attachments = ndb.KeyProperty(Media, repeated=True)

  @ndb.transactional(xg=True)
  def remove_async(self):
     return ndb.delete_multi_async(self.attachments)

  def remove(self):
     ndb.Future.wait_all(self.remove_async())


Both tasklets and deferred tasks provide an abstraction that simplifies parallel processing in App Engine. If you’re working in transactions, or after the lifetime of an HTTP request, it’s important to choose the right technology for your asynchronous operation.


1 We are not going to dive into the differences or details of task queues. For more information, please see the citation.


0 $type={blogger}

Post a Comment

Thank you for reaching out to us. We are happy to receive your opinion and request. If you need advert or sponsored post, We’re excited you’re considering advertising or sponsoring a post on our blog. Your support is what keeps us going. With the current trend, it’s very obvious content marketing is the way to go. Banner advertising and trying to get customers through Google Adwords may get you customers but it has been proven beyond doubt that Content Marketing has more lasting benefits.
We offer majorly two types of advertising:
1. Sponsored Posts: If you are really interested in publishing a sponsored post or a press release, video content, advertorial or any other kind of sponsored post, then you are at the right place.
WHAT KIND OF SPONSORED POSTS DO WE ACCEPT?
Generally, a sponsored post can be any of the following:
Press release
Advertorial
Video content
Article
Interview
This kind of post is usually written to promote you or your business. However, we do prefer posts that naturally flow with the site’s general content. This means we can also promote artists, songs, cosmetic products and things that you love of all products or services.
DURATION & BONUSES
Every sponsored article will remain live on the site as long as this website exists. The duration is indefinite! Again, we will share your post on our social media channels and our email subscribers too will get to read your article. You’re exposing your article to our: Twitter followers, Facebook fans and other social networks.

We will also try as much as possible to optimize your post for search engines as well.

Submission of Materials : Sponsored post should be well written in English language and all materials must be delivered via electronic medium. All sponsored posts must be delivered via electronic version, either on disk or e-mail on Microsoft Word unless otherwise noted.
PRICING
The price largely depends on if you’re writing the content or we’re to do that. But if your are writing the content, it is $60 per article.

2. Banner Advertising: We also offer banner advertising in various sizes and of course, our prices are flexible. you may choose to for the weekly rate or simply buy your desired number of impressions.

Technical Details And Pricing
Banner Size 300 X 250 pixels : Appears on the home page and below all pages on the site.
Banner Size 728 X 90 pixels: Appears on the top right Corner of the homepage and all pages on the site.
Large rectangle Banner Size (336x280) : Appears on the home page and below all pages on the site.
Small square (200x200) : Appears on the right side of the home page and all pages on the site.
Half page (300x600) : Appears on the right side of the home page and all pages on the site.
Portrait (300x1050) : Appears on the right side of the home page and all pages on the site.
Billboard (970x250) : Appears on the home page.

Submission of Materials : Banner ads can be in jpeg, jpg and gif format. All materials must be deliverd via electronic medium. All ads must be delivered via electronic version, either on disk or e-mail in the ordered pixel dimensions unless otherwise noted.
For advertising offers, send an email with your name,company, website, country and advert or sponsored post you want to appear on our website to omodjk(at)gmail(dot)com

Normally, we should respond within 48 hours.