Recently, my colleague +Fred Sauer and I gave a tech talk called "Last Across the Finish Line: Asynchronous Tasks with App Engine". This is part two in a three part series where I will share our learnings and give some helpful references to the App Engine documentation.
Check out the
previous post
if you haven't already. In this section, we'll cover the two
WSGI handlers
in
main.py
serving requests for our application and the client side code that
communicates with our application.
Imports
Before defining the handlers, let's first review the imports:
import json
from google.appengine.api import channel
from google.appengine.api import users
from google.appengine.ext.webapp.util import login_required
import webapp2
from webapp2_extras import jinja2
from display import RandomRowColumnOrdering
from display import SendColor
from models import PopulateBatch
We import json
for serialization of messages. Specific to App Engine, we import channel
to use the
Channel API,
users
and
login_required
for authenticating users within a request,
webapp2
for creating
WSGI Handlers
and
jinja2
for templating.
Finally, we import four functions from the two other modules defined within our
project. From the
display
module, we import the SendColor
function that we explored in part oneand the
RandomRowColumnOrdering
function, which generates all possible row, column
pairs in a random order. From the as of yet undiscussed
models
module we import the PopulateBatch
function, which takes a session ID and a
batch of work to be done and spawns workers to carry out the batch of work.
Handlers
This module defines two handlers: the main page for the user interface and an AJAX handler which will begin spawning the workers.
For the main page we use jinja2
templates to render from the template
main.html
in the templates
folder:
class MainPage(webapp2.RequestHandler):
def RenderResponse(self, template, **context):
jinja2_renderer = jinja2.get_jinja2(app=self.app)
rendered_value = jinja2_renderer.render_template(template, **context)
self.response.write(rendered_value)
@login_required
def get(self):
user_id = users.get_current_user().user_id()
token = channel.create_channel(user_id)
self.RenderResponse('main.html', token=token, table_id='pixels',
rows=8, columns=8)
In get
— the actual handler serving the
GET request
from the browser — we use the login_required
decorator to make
sure the user is signed in, and then create a channel for message passing
using the ID of the signed in user. The template takes an HTML ID, rows and
columns to create an HTML table as the "quilt" that the user will see. We pass
the created token for the channel, an HTML ID for the table and the rows and
columns to the template by simply specifying them as keyword arguments.
For the handler which will spawn the workers, we use RandomRowColumnOrdering
to generate row, column pairs. Using each pair along with the SendColor
function and the user ID (as a proxy for session ID) for message passing, we
add a unit of work to the batch
class BeginWork(webapp2.RequestHandler):
# Can't use login_required decorator here because it is not
# supported for POST requests
def post(self):
response = {'batch_populated': False}
try:
# Will raise an AttributeError if no current user
user_id = users.get_current_user().user_id()
# TODO: return 400 if not logged in
work = []
for row, column in RandomRowColumnOrdering(8, 8):
args = (row, column, user_id)
work.append((SendColor, args, {})) # No keyword args
PopulateBatch(user_id, work)
response['batch_populated'] = True
except:
# TODO: Consider logging traceback.format_exception(*sys.exc_info()) here
pass
self.response.write(json.dumps(response))
Finally, for routing applications within our app, we define:
app = webapp2.WSGIApplication([('/begin-work', BeginWork),
('/', MainPage)],
debug=True)
and specify
handlers:
- url: /.*
script: main.app
in app.yaml
; to use WSGI apps, the App Engine runtime must be python27
.
Client Side Javascript and jQuery
In the template
main.html
we use jQuery to make AJAX requests and manage the
CSS for each square in our "quilt". We also define some other Javascript
functions for interacting with the App Engine Channel API. In the
HTML <head>
element we load the
Channel Javascript API,
and in the <body>
element we open a channel using the {{ token }}
passed
in to the template:
<head>
<script src="/_ah/channel/jsapi"></script>
</head>
<body>
<script type="text/javascript">
channel = new goog.appengine.Channel('{{ token }}');
socket = channel.open();
socket.onerror = function() { console.log('Socket error'); };
socket.onclose = function() { console.log('Socket closed'); };
</script>
</body>
In addition to onerror
and onclose
, we define more complex functions
for the onopen
and onmessage
callbacks.
First, when the socket has been opened, we send a POST request to
/begin-work
to signal that the channel is ready for communication. If
the response indicates that the batch of workers has been initialized
successfully, we call a method setStatus
which will reveal the progress
spinner:
socket.onopen = function() {
$.post('/begin-work', function(data) {
var response = JSON.parse(data);
if (response.batch_populated) {
setStatus('Loading began');
}
});
}
As we defined in part one, each SendColor
worker sends back a
message
along the channel representing a row, column pair and a color. On
message receipt, we use these messages to set the background color of
the corresponding square to the color provided:
socket.onmessage = function(msg) {
var response = JSON.parse(msg.data);
var squareIndex = 8*response.row + response.column;
var squareId = '#square' + squareIndex.toString();
$(squareId).css('background-color', response.color);
}
As you can see from squareId
, each square in the table generated by the
template has an HMTL ID so we can specifically target it.
Next...
In the final post,
we'll define the PopulateBatch
function and explore the
ndb models and
Task Queue
operations that make it work.