A couple of weeks ago, we were involved in a discussion between users and technicians about transaction integrity for a hypothetical system. The preferred approach of the technicians was the optimistic locking: when a user edits the data and saves the changes while another user contemporarily changes the same data, an error is returned. Users did not take this kindly, because the changes are lost and they need to repeat the operation. We started to think out of the box in order to get a different solution, and borrowing the SAMCRO acronym from our preferred TV series, we created a new meaning: Simultaneous Asynchronous Multi Client Read Operation. Like the spirit of the TV series, where your mother says don't do something and you do it anyway, the result is a good practical application of the Google App Engine Channel API. Do not try it on your production environment. Before getting started, buy your favorite burrito and call your best friend asking five minutes of their time to participate in the exercise below. Next, both of you connect to http://gae-samcro.appspot.com with Firefox/Crome/Safari web browser (IE may not work).The screenshot below should be displayed.
A brief explanation about how it works: by clicking on the rows in the list (left window) the detail window (on the right), shows the detail data of the selected row. To update a record, change the values in the edit box and push the 'Update' button.
Now select a row and ask your best friend to select the same row. Change some of the values in the detail window (for example typing in the title field) and save the data by clicking the Update button. If you are lucky, you should get the screenshot below; if not try reloading the page, as sometimes it takes a while, like putting a car in first gear.
The Channel API allows the application to send messages to JavaScript clients in real time without the use of polling. This is useful when you need to notify users about new information immediately. In this sample application, if you are editing a record and another user updates the same record, you will receive a notification message highlighted in yellow at the top of the detail window (see screenshot above), before the record is reloaded. The list window also reflects the updates in bold text, highlighting the changed row for one second. More complex scenarios can be implemented without changing the architecture. Our users loved this feature.
In order to add the Channel API in your GAE applications you need to:
Include the the Channel API
The Google App Engine Channel API has been released, starting from the SDK 1.4.0.
Include the following in your page:
Initialize the channel
A mandatory operation consists of initializing the channel. On client side you need to send a request to the server to initialize the channel.
Now select a row and ask your best friend to select the same row. Change some of the values in the detail window (for example typing in the title field) and save the data by clicking the Update button. If you are lucky, you should get the screenshot below; if not try reloading the page, as sometimes it takes a while, like putting a car in first gear.
The Channel API allows the application to send messages to JavaScript clients in real time without the use of polling. This is useful when you need to notify users about new information immediately. In this sample application, if you are editing a record and another user updates the same record, you will receive a notification message highlighted in yellow at the top of the detail window (see screenshot above), before the record is reloaded. The list window also reflects the updates in bold text, highlighting the changed row for one second. More complex scenarios can be implemented without changing the architecture. Our users loved this feature.
In order to add the Channel API in your GAE applications you need to:
- Include the the Channel API script in your page
- Initialize the channel
- Send and Receive Messages
Include the the Channel API
The Google App Engine Channel API has been released, starting from the SDK 1.4.0.
Include the following in your page:
- <script language="JavaScript" type="text/javascript" src="/_ah/channel/jsapi"></script>
Initialize the channel
A mandatory operation consists of initializing the channel. On client side you need to send a request to the server to initialize the channel.
- /* Initialize the channel */
- function initializeChannel() {
- dhtmlxAjax.get("/controller/initialize" , function(loader) {
- if (loader.xmlDoc.responseText != null) {
- openChannel( loader.xmlDoc.responseText );
- }
- });
- }
- function openChannel(token) {
- var channel = new goog.appengine.Channel(token);
- var handler = {
- 'onopen': function() {},
- 'onmessage': onMessage,
- 'onerror': function() {},
- 'onclose': function() {}
- };
- var socket = channel.open(handler);
- socket.onopen = function() {};
- socket.onmessage = onMessage;
- }
The server is responsible for:
- Creating a unique channel for each client
- Creating a unique token to each client
/controller/initialize
- ChannelService channelService = ChannelServiceFactory.getChannelService();
- String clientId = request.getSession().getId();
- String token = channelService.createChannel( clientId );
- ClientDAO dao = new ClientDAO();
- Client client = dao.store( new Client(clientId) );
- write(response, "text/plain", token );
In this sample application, the session ID is used as client ID, and it is stored in the datastore.
Send and Receive Messages
On client side, the notifyToChannel is used to send messages to the client. In this case an Ajax GET request is sent to the server.
- function notifyToChannel(message) {
- dhtmlxAjax.get("/controller/notify?" + message , function(loader) {});
- }
The onMessage function specified during the initialization is invoked when a message is received from the client, in this case a JSON message.
- function onMessage(message) {
- var msg = JSON.parse(message.data);
... - }
/controller/notify
- ClientDAO dao = new ClientDAO();
- ChannelService channelService = ChannelServiceFactory.getChannelService();
- List<Client> clients = dao.retrieveClients();
- for (Client client : clients) {
- if (!client.getClientId().equalsIgnoreCase(request.getSession().getId())) {
- channelService.sendMessage(new ChannelMessage(client.getClientId(),
- getMessageString(id)));
- }
- }
- public String getMessageString(String id) {
- BookDAO dao = new BookDAO();
- Book book = dao.retrieveBook(id);
- Map<String, String> msg = new HashMap<String, String>();
- msg.put("id", book.getId().toString());
- ...
- JSONObject message = new JSONObject(msg);
- return message.toString();
- }
In order to be notified when a client connects to or disconnects from a channel, you must enable this feature in the appengine-web.xml:
When the channel_presence is enabled, the application receives a POST /_ah/channel/connected/ request when a client has connected to the channel, and receive a POST /_ah/channel/disconnected/ when the client has disconnected.
This sample application implements handlers to these paths in order to keep track which clients are currently connected.
/_ah/channel/connected/
/_ah/channel/disconnected/
The sample application has been developed in Java with the support of the DHTMLX Java Tag Library 1.5 the DHTMLX Java Tag Designer and DHTMLX 3.0.
Have fun.
- <inbound-services>
- <service>channel_presence</service>
- </inbound-services>
When the channel_presence is enabled, the application receives a POST /_ah/channel/connected/ request when a client has connected to the channel, and receive a POST /_ah/channel/disconnected/ when the client has disconnected.
This sample application implements handlers to these paths in order to keep track which clients are currently connected.
/_ah/channel/connected/
- ChannelService channelService = ChannelServiceFactory.getChannelService();
- ChannelPresence presence = channelService.parsePresence(request);
/_ah/channel/disconnected/
- ChannelService channelService = ChannelServiceFactory.getChannelService();
- ChannelPresence presence = channelService.parsePresence(request);
- ClientDAO dao = new ClientDAO();
- dao.deleteClient( presence.clientId() );
Have fun.