Table of Contents#
- Understanding NDB and Asynchronous Operations
- @ndb.tasklet: The Foundation of Async NDB Code
- @ndb.synctasklet: Bridging Async and Synchronous Handlers
- Why POST Handlers Require Special Care
- Step-by-Step: Using Tasklets in POST Handlers
- Common Pitfalls and How to Avoid Them
- Best Practices for Reliable Async Operations
- Conclusion
- References
1. Understanding NDB and Asynchronous Operations#
NDB is Google’s Python client library for Cloud Datastore, optimized for performance and scalability. It introduces asynchronous operations to avoid blocking the main thread while waiting for I/O (e.g., Datastore writes/reads, external API calls).
- Synchronous vs. Asynchronous Operations:
- Synchronous (Sync): The code waits for an operation to complete before proceeding (e.g.,
entity.put()blocks until the write is confirmed). - Asynchronous (Async): The code initiates an operation and continues executing, allowing other tasks to run in parallel (e.g.,
entity.put_async()returns a "future" representing the pending write).
- Synchronous (Sync): The code waits for an operation to complete before proceeding (e.g.,
Async operations improve throughput by utilizing idle time, but they require explicit coordination to ensure completion. NDB uses tasklets (lightweight coroutines) and futures (objects representing pending results) to manage this.
2. @ndb.tasklet: The Foundation of Async NDB Code#
@ndb.tasklet is a decorator that transforms a function into an async tasklet—a coroutine that returns a Future object. Tasklets use yield to pause execution and wait for other async operations (e.g., Datastore calls) to complete, enabling non-blocking code flow.
Key Concepts:#
- Future: A placeholder for the result of an async operation. Use
yield futureto pause until the result is ready. - Yield: Within a tasklet,
yieldpauses execution and allows NDB to run other pending operations. When the yielded future completes, the tasklet resumes. - Return Values: Tasklets return futures. To get the actual result, use
yield futureorfuture.get_result()(after ensuring completion).
Example: Basic Tasklet for Async Datastore Read#
from google.appengine.ext import ndb
class User(ndb.Model):
name = ndb.StringProperty()
email = ndb.StringProperty()
@ndb.tasklet
def get_user_async(user_id):
"""Async tasklet to fetch a User entity by ID."""
user = yield User.get_by_id_async(user_id) # Yield the future to wait for the result
raise ndb.Return(user) # Return the result via ndb.Return (required in tasklets)
# Usage:
future = get_user_async(123)
user = future.get_result() # Blocks until the tasklet completes (use cautiously!) Why This Works:
User.get_by_id_async(123)returns a future.yieldpausesget_user_asyncuntil the future resolves, allowing NDB to handle other tasks in the meantime.ndb.Return(user)packages the result into the tasklet’s future.
3. @ndb.synctasklet: Bridging Async and Sync Code#
Most web frameworks (e.g., webapp2, Flask) use synchronous handlers—functions that block until a response is sent. @ndb.synctasklet bridges async tasklets with sync code by running the tasklet and blocking until it completes, returning the actual result (not a future).
Key Use Case:#
In POST handlers, you need to ensure async writes (e.g., put_async()) complete before sending a response. @ndb.synctasklet lets you wrap async logic and call it like a sync function, guaranteeing completion.
Example: Synctasklet for Async Datastore Writes#
@ndb.synctasklet
def create_user_synctasklet(name, email):
"""Synctasklet to create a User entity asynchronously and return the key."""
user = User(name=name, email=email)
key_future = user.put_async() # Start async write, get future
key = yield key_future # Wait for the write to complete
raise ndb.Return(key) # Return the completed key
# Usage in a synchronous POST handler:
def post(self):
name = self.request.get("name")
email = self.request.get("email")
user_key = create_user_synctasklet(name, email) # Blocks until write completes
self.response.write(f"User created with ID: {user_key.id()}") Why This Works:
@ndb.synctaskletruns the tasklet, blocks until it finishes, and returns the result directly (user_keyin this case).- The POST handler waits for
create_user_synctaskletto complete, ensuring the Datastore write finishes before responding.
4. Why POST Handlers Require Special Care#
POST handlers often modify data (e.g., creating/updating entities). If async operations (e.g., put_async()) are not properly awaited, the handler may send a response before the write completes, leading to:
- Lost data (the write fails silently).
- Inconsistent state (subsequent reads don’t see the update).
- Race conditions (concurrent writes overwrite each other).
@ndb.tasklet (with yield) and @ndb.synctasklet ensure all async operations in the handler complete before the response is sent.
5. Step-by-Step: Using Tasklets in POST Handlers#
Let’s walk through a real-world example: a POST handler that creates a User and a linked Profile entity, using async Datastore writes to maximize efficiency.
Step 1: Define Models#
class User(ndb.Model):
name = ndb.StringProperty(required=True)
email = ndb.StringProperty(required=True)
class Profile(ndb.Model):
user_key = ndb.KeyProperty(kind=User, required=True)
bio = ndb.TextProperty()
created_at = ndb.DateTimeProperty(auto_now_add=True) Step 2: Create Async Tasklets for Writes#
We’ll create a tasklet to create both User and Profile asynchronously, then use @ndb.synctasklet to bridge to the sync handler.
@ndb.tasklet
def create_user_and_profile_async(name, email, bio):
"""Async tasklet to create User and Profile entities in parallel."""
# Start both writes (no yield yet—they run in parallel)
user = User(name=name, email=email)
user_future = user.put_async()
profile = Profile(bio=bio)
profile_future = profile.put_async()
# Wait for both writes to complete (yield multiple futures to run in parallel)
user_key, profile_key = yield user_future, profile_future
raise ndb.Return((user_key, profile_key))
@ndb.synctasklet
def create_user_and_profile_synctasklet(name, email, bio):
"""Synctasklet wrapper to run the async tasklet and return results."""
user_key, profile_key = yield create_user_and_profile_async(name, email, bio)
raise ndb.Return((user_key, profile_key)) Key Optimization: By starting both put_async() calls before yielding, we run them in parallel, reducing total latency.
Step 3: Use Synctasklet in the POST Handler#
import webapp2
class UserProfileHandler(webapp2.RequestHandler):
def post(self):
# Extract request data
name = self.request.get("name")
email = self.request.get("email")
bio = self.request.get("bio")
# Validate input (simplified)
if not (name and email):
self.response.status = 400
self.response.write("Name and email are required.")
return
try:
# Run async tasklet via synctasklet (blocks until complete)
user_key, profile_key = create_user_and_profile_synctasklet(name, email, bio)
self.response.status = 201
self.response.write(f"User: {user_key.id()}, Profile: {profile_key.id()} created.")
except Exception as e:
# Handle errors (e.g., Datastore failure)
self.response.status = 500
self.response.write(f"Error: {str(e)}")
app = webapp2.WSGIApplication([("/user/profile", UserProfileHandler)], debug=True) Why This Works:
create_user_and_profile_synctaskletblocks until bothUserandProfilewrites complete.- The handler only sends a response after confirming the writes, preventing data loss.
6. Common Pitfalls and How to Avoid Them#
Pitfall 1: Forgetting to yield a Future#
Problem: If you call an async operation (e.g., put_async()) in a tasklet without yield, the operation may never complete.
Example (Bad):
@ndb.tasklet
def bad_tasklet():
user = User(name="Alice")
user.put_async() # Oops! No yield—write is scheduled but not awaited
raise ndb.Return("Done") # Tasklet finishes before write completes Fix: Always yield the future to wait for completion:
@ndb.tasklet
def good_tasklet():
user = User(name="Alice")
yield user.put_async() # Wait for the write to finish
raise ndb.Return("Done") Pitfall 2: Using future.get_result() Prematurely#
Problem: Calling future.get_result() before the future completes raises an error.
Fix: Use yield future to ensure the future is ready before accessing the result.
Pitfall 3: Mixing Sync and Async Code Incorrectly#
Problem: Sync code (e.g., entity.put()) blocks the event loop, negating async benefits.
Fix: Use async variants (e.g., put_async()) within tasklets and yield their futures.
Pitfall 4: Ignoring Exceptions in Tasklets#
Problem: Async errors (e.g., Datastore timeouts) may go unhandled, causing silent failures.
Fix: Wrap yield statements in try/except blocks:
@ndb.tasklet
def safe_tasklet():
try:
user = yield User.get_by_id_async(123)
raise ndb.Return(user)
except ndb.Timeout:
raise ndb.Return(None) # Handle timeout gracefully 7. Best Practices for Reliable Async Operations#
1. Batch Async Operations#
Start multiple async calls first, then yield their futures to run them in parallel:
@ndb.tasklet
def batch_async():
# Start all operations
f1 = User.get_by_id_async(1)
f2 = User.get_by_id_async(2)
f3 = User.get_by_id_async(3)
# Wait for all to complete (parallel execution)
u1, u2, u3 = yield f1, f2, f3
raise ndb.Return([u1, u2, u3]) 2. Use synctasklet for Sync Handlers#
In frameworks like webapp2, use @ndb.synctasklet to run async code in sync handlers without manually managing futures.
3. Validate and Handle Errors#
Always validate input and catch exceptions in tasklets/handlers to avoid partial writes or unresponsive clients.
4. Test Async Code Thoroughly#
Use NDB’s test utilities (e.g., ndb.tests module) to simulate async operations and ensure completion.
8. Conclusion#
@ndb.tasklet and @ndb.synctasklet are critical tools for managing async operations in Google App Engine, especially in POST handlers where data integrity is paramount. By using tasklets with yield, you ensure async writes/reads complete before responding, preventing data loss and inconsistencies.
Key takeaways:
- Use
@ndb.taskletfor async logic, leveragingyieldto wait for futures. - Use
@ndb.synctaskletto bridge async tasklets with synchronous handlers. - Always
yieldfutures to guarantee async operations complete. - Handle exceptions and validate input to ensure reliability.
9. References#
- NDB Tasklets and Futures (Google Cloud Docs)
- NDB Synctasklet (Google Cloud Docs)
- webapp2 Framework (Official Docs)
- NDB Model Class (Google Cloud Docs)