Multiuser Chatroom with App Engine Channel API – Part 2


The Part-1 of designing multiuser chat room with app engine deals with providing a basic information of how to create a simple chatroom/gameroom with App Engine Channel API. That article was more based on the tic-tac-toe example provided by the Google App Engine team.

In this post, I will talk about the ways in which this can be optimized using memcache.

But before that, let’s look at the complications with the previous one

  1. The Channel ID is a function of only userid. This means that one user can not login from multiple clients and can expect consistency. There will always be an inconsistency and improper outgoing messages on the channels.
  2. Too much datastore operations. For every action, there is too much datastore operations going on. As the number of players keep on increasing, this problem becomes more intense. A developer reported that he had to wait several seconds for things to happen while doing it with ~30 players. This is an optimized version and will have much better serving time
  3. Coding style. Well frankly, I was not satisfied with the coding style in prev version, so in the hope of creating better and beautiful code, I decided to rewrite it and I came up with this new file tournament.py, which is pretty neat.

First of all, in order to get an insight of effective memcaching in datastore entities, go through this post on Nick’s blog. This has been completely used.

EfficientModel

In order to do good optimizations, I have a base class EfficientModel from which my datastore classes will be derived. Entities of kind EfficientModel are designed to be in memcache most of the time they are required. All the operations on these entities take place in memcache only. After the memcached entity has been updated “certain” number of times, the change is replicated in the datastore. The EfficientModel has an attribute mc_version, which stores the version number of this entity in memcache. The concept is that every entity has a revision number associated. Whenever there is any change in one of the attributes/property of the entity it’s version number increases by one. mc_version stored the version number of entity in memcache. db_version stores the version number of entity in the datastore. The difference between memcache version and datastore version is called as FAULT NUMBER. When fault_number goes beyond a certain number known as fault_tolerance, then the memcache entity and the datastore entity are sync’d.

class EfficientModel(db.Model):
mc_version = db.IntegerProperty(default = 0)
db_version = db.IntegerProperty(default = 0)
created = db.DateTimeProperty(auto_now_add = True)
updated = db.DateTimeProperty(auto_now = True)

@property
def keyname(self):
return self.key().name()

@property
def memcache_key(self):
raise NotImplementedError

@classmethod
def from_id(cls, id):
existing_entity_mc = deserialize_entities(memcache.get(id))
if existing_entity_mc is None:
existing_entity_db = cls.get_by_key_name(id, parent = cls.find_parent(id))
if existing_entity_db is None:
return cls(key_name = unicode(id))
return existing_entity_db
return existing_entity_mc

@classmethod
def fetch_from_id(cls, id):
return cls(key_name = unicode(id))._get()

def _from_memcache(self):
return deserialize_entities(memcache.get(self.memcache_key))

def _get(self):
memcached_entity = self._from_memcache()
if memcached_entity is not None:
return memcached_entity
return self.get_or_insert(key_name = self.keyname)

def _store(self, force = False):
self.mc_version += 1
memcache.set(self.keyname, serialize_entities(self))
if self.mc_version – self.db_version >= FAULT_TOLERANCE or force:
logging.info(‘sync_to_db started for %s. mc_version: %d, db_version: %d’%(
self.keyname, self.mc_version, self.db_version))
self.sync_to_db()
elif self.mc_version < self.db_version:
logging.info('sync_from_db started')
self.sync_from_db()

def sync_to_db(self):
self.db_version = self.mc_version
db.put(self)
self._store()

def sync_from_db(self):
from_ds = db.get(self.keyname)
memcache.set(self.keyname, serialize_entities(from_ds))
self._store()

@classmethod
def find_parent(cls, id):
raise NotImplementedError

Properties, Classmethods and Functions

  • keyname, property. : Returns the key name of the entity. Equivalent to
    .key().name()
    
  • memcache_key, property : The derived class is expected to define this property. This is supposed to return the key which will be used in memcache while storing and retrieving this entity. It is assumed at some places the memcache_key shall be same as keyname.
  • from_id(id), classmethod : This classmethod returns the entity based on the id passed. It first tries to fetch the entity from memcache. If not found, it attempts to fetch the entity from datastore. If that is not found, then it creates an entity with the key_name as the id passed and returns that.
  • _from_memcache, private function : Returns the memcached snapshot of the entity
  • _get, private function : Attempts to get self from memcache. If not found, goes through a get_or_insert call.
  • fetch_from_id(id), classmethod : The difference between fetch_from_id and from_is is that, fetch_from_id always creates an entity in the datastore if it does not already exists. While in case of from_id a dummy object (one which is not in the datastore, yet) is returned.
  • sync_from_db, function : Sync the entities from datastore to memcache. Essentially, the entity is copied to memcache from datastore and the versions are updated
  • sync_to_db, function : Sync the entities from memcache to data store. Essentially, the entity is copied from memcahe to datastore and the versions are updated
  • _store, private function : Stores the entity in memcache and updates the mc_version. If the fault becomes more than fault tolerance, it syncs the entity across datastore and memcache.
  • find_parent(id), classmethod : This is not implemented and the derived class is expected to work on this. This is supposed to return the key name of the parent entity of this entity.

Channels

The channel ids created are a function of userid as well as the time.time(). Whenever a channel id is created, it is stored as a property in the Player model as well as the Game model. Duplicacy of data, helps do better reads.

def gen_channel(userid):
seed = userid + str(int(time.time()))
return md5.md5(seed).hexdigest()

Player Class

The Player Class has more or less the same functions, but their writing style became little different due to change in coding style.

class Player(EfficientModel):
“””
Stores information about which player is added into which game
room. This of course assumes that a player can be in only one
game room at a time.
“””
assets = db.TextProperty(default = ”)
name = db.StringProperty()
channels = db.StringListProperty()

@property
def memcache_key(self):
return self.key().name()

@property
def gameroom(self):
if self.parent():
return self.parent().key().name()

def get_channels(self):
return self._get().channels

def create_channel(self):
new_channel = gen_channel(self.keyname)
self.channels.append(new_channel)
self._store()
return new_channel

def die(self):
raise NotImplementedError

def do_action(self, action, **kwargs):
raise NotImplementedError

@classmethod
def find_parent(cls, id):
return Game.all(keys_only = True).filter(‘players = ‘, id).get()

def chat(self, message):
if self.gameroom is not None:
self._store()
Game.from_id(self.gameroom).update_chat(self, message)

def leave_tournament(self):
if self.gameroom is not None:
Game.from_id(self.gameroom).expel(self)

Game Class

The important parts of the class are shown here. For a more detailed version, have a look at the source code.

class Game(EfficientModel):
“””
The Model to store the details of a particular
game room.
“””
players = db.StringListProperty()
chat = db.TextProperty()
active = db.BooleanProperty(default = True)
channels = db.StringListProperty()

deltas = {}

@property
def memcache_key(self):
return ‘tournament_’ + self.key().name()

@classmethod
def join_latest_or_new(cls, player):
latest_roomkey = memcache.get(LATEST_GAMEROOM)
if latest_roomkey:
cls.from_id(latest_roomkey).add_player(player)
return latest_roomkey
return cls().new(player)

def can_add_player(self):
if MAX_PARTICIPANTS == -1: return True
return len(self.players) < MAX_PARTICIPANTS

def add_player(self, player):
if self.can_add_player():
logging.info('Adding to existing game')
if player.keyname not in self.players:
self.players.append(player.keyname)
self._store()
p = Player.get_or_insert(key_name = player.keyname,
parent = self)
memcache.set(player.keyname, serialize_entities(player))
self.deltas.update({'new_player' : 1,
'name' : player.keyname,})
self.update_channels(player)
return self.send_updates()
else:
return self.new(player)

@classmethod
def new(cls, player):
if not player: return
logging.info('Creating new gameroom…')
new_room = cls(key_name = str(time.time()),
players = [player.keyname])
sim_player = Player(key_name = player.keyname,
parent = new_room,
channels = player.channels)
db.put([new_room, sim_player])
memcache.set_multi({LATEST_GAMEROOM : new_room.key().name(),
new_room.memcache_key: serialize_entities(new_room),
sim_player.keyname : serialize_entities(sim_player),
})
new_room.update_channels(sim_player)
return new_room

@classmethod
def continue_tournament(cls, player):
room = cls.from_id(player.gameroom)
room.update_channels(player)

def update_channels(self, player):
new_channels = player.get_channels()
if isinstance(new_channels, list):
self.channels.extend(new_channels)
if isinstance(new_channels, str):
self.channels.append(new_channels)
self.channels = list(set(self.channels))
self._store(True)

def send_updates(self):
message = simplejson.dumps(self.deltas)
for channel_id in self.channels:
channel.send_message(channel_id, message)

@classmethod
def find_parent(cls, id):
return None

def update_chat(self, player, message):
self.deltas['chat'] = message
if self.chat:
self.chat += '
' + self.deltas.get('chat')
else:
self.chat = self.deltas.get('chat')
self._store()
self.send_updates()

def end(self):
"End the game"
player_keys = [db.Key.from_path('Game', self.keyname, 'Player', x) for x in self.players]
db_keys = [db.key()]
db_keys.extend(player_keys)
memcache_keys = self.players
memcache_keys.append(self.memcache_key)
memcache.delete_multi(memcache_keys)
db.delete(db_keys)

def expel(self, player):
"Expel player from the game"
memcache.delete(player.memcache_key)

def txn(game_obj, player_key, player_id):
db.delete(player_key)
game_obj.players.pop(player_id)
game_obj.put()

db.run_in_transaction(txn, self, db.Key(player.keyname), player.keyname)
self.deltas.update({'expel': 1,
'name' : player.keyname})
self.send_updates()

This is a better optimized version. I am still working on it on a bigger and wider scale and will keep updating about the scaling issues of this approach and how to fix them.

Resources

  1. Multiuser Chatroom with App Engine Channel API – Part 1: http://blog.myblive.com/2010/12/multiuser-chatroom-with-app-engine.html
  2. Source Code: http://code.google.com/p/pranav/source/browse/chat-channel
  3. Channel API: http://code.google.com/appengine/docs/python/channel
  4. Discuss: Google Groups Discussion
  5. Tic Tac Toe App: http://code.google.com/p/channel-tac-toe

Getting rid of facebook group emails, beautifully


Like many others, after being irritated by facebook’s constant emails for every post on a group I am a member of, I made Group Email Digest (facebook page) application which sends me the daily summary of posts on a group. The applications sends out one email per day per group. The first version of the app launched was pretty simple and had some basic bugs which restricted the use of the application. The latest release of the app (the app is still in beta) has fixed those bugs and paved way for more interesting features in the applications.

Here is a brief summary of bugs fixed, bugs not fixed and things on my mind (to show up in later releases). For a complete and updated detail of issues, visit the bug tracker page.

UPDATE
The bug related to closed/secret group’s emails not being trigged has been fixed.

Resolved Bugs/ Issues

  1. Blank Email/Text: A blonde had three swimming pools. One had cold water, the other had warm water and there was no water in the third pool. When asked about the third pool, she said.. well, sometimes I don’t feel like swimming. 🙂 The Group Email Digest app used to send out blank emails, when there had been no recorded activity. This has been fixed, and now, no emails are send if there is no activity.
    Likewise, if there were only links shared, videos posted, images uploaded onto the group, there was a similar blank text coming up. This has been alternated by message, description and caption in that order.
  2. Digest Emails not generating: For few old group’s, the emails were not being triggered. I found this out to be a version change issue on facebok front, and maintaining the compatibility, this has been fixed. The emails for all groups shall be triggering starting Nov 06
  3. URLs for old groups not coming properly: The URLs for old facebook groups were not formatted appropriately. This resulted in the clicks taking user to facebook home page instead of taking to the group page. This issue has been fixed.

Unresolved Bugs/Issues
The following issues remain unresolved as of now.

  1. Time Zone: All the emails are triggered just after 00:15 hrs UTC. This should ideally be 00:15 hrs User Time Zone. I am looking for a resource efficient method for this, and hopefully should get this fixed soon.
  2. Closed/Private Group Emails not trigerring: I am experimenting with delivering digest emails from closed/private group to its members in a resource efficient manner, and hopefully should arrive at some conclusion soon.

Things in my mind
The following features you should hope to see in near future.

  1. Localization: You should see the mail in the language of your choice, right? So, very soon, you will receive emails and all the text will be localized based on the information we obtain about you from your facebook account.
  2. Activity Graphs: Wanna know how active is a group? Very soon, you will be able to see a graphical visual of the activity taking place in the groups you are a member of.
  3. Group Suggestions: Want to do more exiting talks to people of your interests? Group suggestions will be telling you about new active groups based on your taste.

Did I miss a feature? Suggest features for Group Digest App here.

Saw a bug/issue/concern? Report it here.

Follow @groupdigestapp on twitter.

BeamtoUs – Discover Vote Share

Welcome Beamto.us (twitter @beamtous), a completely new and beautiful music sharing portal. We love to call each single element that you share as a beam. This is also my current project, where I am involved in close coordination with caffeine powered code monkeys – Ritesh Nadhani (idea bubble generator), Taras, Freechin and others. Beamto.us.

Beamto.us is a music sharing portal targeting specific to genres like psy trance, chillout trance, dark trance, progressive trance and other sub genres in them. You can easily upload your music, or if you have already done that on youtube, soundcloud or other portals, can link from Beamto.us. I’d like to talk more on what are the interesting things that you can do with Beamto.us. The official About Us page says

“Beamto.us Aggregates. The only website in the World that aggregates all electronic music. We use the best back-end technology to bring an excellent interface to all the users. The main page of the site aggregates and lists only the most popular songs voted by the users. Our algorithm has been simulated and tested to be absolutely fair to rankings.”






Features

  1. Beamto.us allows you to login through your existing Google Account, so you don’t need to go through all the registration hassle and pain.
  2. You can upload your music onto our servers or can hotlink your existing website/link for your music at Beamto.us.
  3. Vote Up/ Down a particular beam. And yeah, the votes and rankings do decay so if your beam is hot and new, you are going to be at the top.
  4. Easily share your beams on Facebook, MySpace, Twitter and many other favorite sites, where your friends can see them
  5. Concerned about copyright issues or something else, just let us know. We deal with abuse reports very seriously.
  6. We have lots of space and our servers are on the cloud, so don’t worry about a downtime 🙂 Whenever someone wants to go through your beam, we make sure that it is always up and serving hot.
  7. Its FREE to use. Yes, you don’t need to pay in order to use or share your beams. Just come over and we will be doing the things for you.

The portal is still in its early phases and we have some very exiting plans to make it fun. Yes, that’s our concern. Make beaming fun, simple and share !!

And yes, we are always listening to our users who tell us about our faults and where we need to improve. And we do work on them. So, whenever you feel something is missing, just let us know.

AtoZ-Help : version 0.2 released

The version 0.2 of AtoZ-help Framework is available for download and testing purposes. AtoZ-help is a framework written in Pure Python and runs on Google AppEngine for creating feature rich help pages online. The help pages can be integrated with your existing website or can be released as an independent site.

Check out the source code at : http://code.google.com/p/atoz-help/
Got a buF? Report it at : http://code.google.com/p/atoz-help/issues/list
The latest version is available for download. Download it here

AtoZ Help – An Engine for creating online help section on AppEngine

So, this Christmas, i had nothing better to do than sit and do this project named AtoZ-help.

The project aims at creating an engine that can create and manage the help sections of a website in a very efficient and hassles-less manner.

Website : http://atoz-help.appspot.com
Repository: http://atoz-help.googlecode.com

The project is under rapid development and this release has been completed in 2 days (starting from scratch), and although serious issues are not quite expected, there needs a lot of tuning, refining and features to be added.

If you have a bug report/ feature request. kindly so in the issues list here. Please search the issue list to avoid duplicacy.

As of now, it is not recommended to use this project for business purposes just because the project is evolving very fast, and so some features might get changed or discontinued. It is however, good if you are a developer and want to try things out.

I will be releasing a complete tutorial and a much convenient template for AtoZ, but they are not in my priority list. The roadmap for this project is listed at the RoadMap Wiki Page.