Versioned Entity Caching for high read/write on Google App Engine Python

The usage of memcache for lowering the load on App Engine Datastore is well known. Here is an approach to versioned caching of datastore entities in App Engine. The Model is deigned primarily for purposes where entity is updated very frequently. The idea for these kind of entity generated while working on the Multiuser Chat room for App Engine using Channel APIs.

Expectations

The Model is designed with the following factors in mind

  • Very frequent read/write access to entity
  • Effective usage of memcache to reduce load on datastore operations
  • Strong consistency between datastore and memcache

Design Plan

The Entities have an internal version number. This version number is increased everytime there is an update in the entity.

from google.appengine.ext import db


# The maximum difference in revisions acceptable at any instant
# between memcached values and the datastore values. The higher it
# is, the greater catastrophe when memcache goes down, but lesser
# datastore usage. The lesser it is, the more consistent your datastore
# and memcache are, and higher datastore operations. 4~6
FAULT_TOLERANCE = 4

class GlobalVersionedCachingModel(db.Model):
"""
The Model uses internal versioning of information with prime focus on very
high read/writes and consistency.
Every entity has a datastore's version number information and the version
number from memcache. When the entity is updated, it happens in memcache
only and the memcached version number increases. If this number is greater
than the datastore version number by a certain amount called
"fault tolerance", then the datastor entity is sync'd with the memcache
entity.
"""

_db_version = db.IntegerProperty (default=0, required=True)
_cache_version = db.IntegerProperty (default=0, required=True)
_fault_tolerance = db.IntegerProperty(default = FAULT_TOLERANCE)

Approach #1

Initially, both the versions are set to 0 when the entity is created. _fault_tolerance is the maximum allowed difference between the cache version and the datastore version. Since the entity is primarily read from and written to memcache, and later updated to datastore, the datastore version number can be behind the memcache version number. When the difference between the two exceeds fault tolerance, then the datastore is updated with the memcache details.

The downside of this approach is that when the memcache goes out, then the datastore entity is fetched. This entity, in worst case scenario, could be lagging from the actual entity by a maximum of _fault_tolerance factor. Fault Tolerance can be decreased to improve the consistency between memcached entity and the datastore entity but that will result in higher datastore read/write operations.

Approach #2

In this approach, instead of directly writing into the datastore, we initiate a task queue and that gets the task done for us. The good part of this approach is that we can keep smaller values of _fault_tolerance and still expect faster processing. This approach is ideal where strong consistency is required between the entities and latency shall also be minimal

Methods

def get2 (keys, **kwargs):
keys, multiple = datastore.NormalizeAndTypeCheckKeys (keys)
getted_cache = memcache.get_multi (map (str, keys))
ret = map (deserialize_entities, getted_cache.values ())
keys_to_fetch = [key for key in keys if getted_cache.get(key, None) is not None]
getted_db = db.get(keys_to_fetch)
memcache_to_set = dict ((k,v) for k,v in zip (map (str,keys_to_fetch),
map (serialize_entities, getted_db)))
ret.extend(getted_db)
memcache.set_multi (memcache_to_set)
if multiple:
return ret
if len (ret) > 0:
return ret[0]

class GlobalVersionedCachingModel(db.Model):
"""
The Model uses internal versioning of information with prime focus on very
high read/writes and consistency.
Every entity has a datastore's version number information and the version
number from memcache. When the entity is updated, it happens in memcache
only and the memcached version number increases. If this number is greater
than the datastore version number by a certain amount called
"fault tolerance", then the datastor entity is sync'd with the memcache
entity.
"""

_db_version = db.IntegerProperty (default=0, required=True)
_cache_version = db.IntegerProperty (default=0, required=True)
_fault_tolerance = db.IntegerProperty(default = FAULT_TOLERANCE)
created = db.DateTimeProperty (auto_now_add=True)
updated = db.DateTimeProperty (auto_now=True)

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

def remove_from_cache (self, update_db=False):
"""
Removes the cached instance of the entity. If update_db is True,
then updates the datastore before removing from cache so that no data
is lost.
"""
if update_db:
self.update_to_db()
memcache.delete(self.keyname)

def update_to_db (self):
"""
Updates the current state of the entity from memcache to the datastore
"""
self._db_version = self._cache_version
logging.info('About to write into db. Key: %s' %self.keyname)
self.update_cache ()
return super (GlobalVersionedCachingModel, self).put ()

def update_cache (self):
"""
Updates the memacahe for this entity
"""
memcache.set (self.keyname, serialize_entities (self))

def put (self):
self._cache_version += 1
memcache.set (self.keyname, serialize_entities (self))
if self._cache_version - self._db_version >= self._fault_tolerance or
self._cache_version == 1:
self.update_to_db ()

def delete (self):
self.remove_from_cache()
return super (GlobalVersionedCachingModel, self).delete ()

@classmethod
def get_by_key_name (cls, key_names, parent=None, **kwargs):
try:
parent = db._coerce_to_key (parent)
except db.BadKeyError, e:
raise db.BadArgumentError (str (e))
rpc = datastore.GetRpcFromKwargs (kwargs)
key_names, multiple = datastore.NormalizeAndTypeCheck (key_names, basestring)
logging.info(key_names)
keys = [datastore.Key.from_path (cls.kind (), name, parent=parent) for name in key_names]
if multiple:
return get2 (keys)
else:
return get2 (keys[0], rpc=rpc)




Similar Readings

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

Multiuser Chatroom with App Engine Channel API

UPDATE
Have a look at part 2 of the series, with focus on optimization.

Perhaps the most primitive use of Google App Engine Channel API is the use of chat between two people in real time. This concept can be extended to chatrooms – where many people chat simultaneously with each other, or gamerooms – where many people play some game simultaneously in real time amongst each other.

The docs don’t say much about the quota details of Channel API, so here it is

Free Quota
Channel API Calls 46,310,400
Channels Created 8,640
Channel Data Sent 1,046.00 GBytes

Paid Quota
Channel API Calls 91,995,495
Channels Created 95,040
Channel Data Sent 2,088.13 GBytes

The example application tic-tac-toe shows to a quite good level how to use Channel API for playing duo-player game. I find some scope for improvements in that app. Let’s review them :

  1. Everytime a player starts a new game, a new client id (for channel) is created. The cap provided on Channels Creates is few, so this should be used judiciously
  2. The game room generates depends on the User. So, if the user who initiated the play reloads the page, the game is gone. This works for the case of tic-tac-toe but does not go for chatrooms or gamerooms

Apart from above issues, there are somethings that a serious gameroom/ chatroom needs and they are as follows:

  1. Automatic room creation. The room is automatically created as soon as number of players exceed the MAX_PLAYERS limit
  2. Every player has some assets (in a gameroom), and this keeps on changing with actions performed. So there needs to be state persistent information stored at the backend
  3. If the player reloads the page or opens a couple of new tabs, consistency of information should be maintained.
  4. Usually in a chat, when someone joins late, they want to be updated of the chats that took place when they were not there

.

Using above points as my guiding light, I tried to create an app which does the above in a net and beautiful manner. The app’s source code is located at http://code.google.com/p/pranav/source/browse/chat-channel.

Structure

The concept of the gameroom/chatroom goes like this – When a player wants to play a game, he is taken to a room. The room could be a new room (if no room exists or all others are full), or an existing room.
There are two entity kinds – Game and PlayerGame. The Game Entity Kind contains information about the game room. This is where you can keep a track of all the events happening in a room like maybe chat between users, or collaborative drawing or the moves in a tic-tac-toe. The PlayerGame is a child entity of the Game entity, which contains the information about the game the user is playing. For those games which go in a sequential manner where player1 does something and then player2 does something, PlayerGame could have very well been inside the Game entity, but in a game where many players can do multiple actions and their actions affect their state in some way or the other datastore congestion might occur. To avoid all those, its made in a separate entity.

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

class PlayerGame(db.Model):
“””
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 = ”)
created = db.DateTimeProperty(auto_now_add = True)
updated = db.DateTimeProperty(auto_now = True)

Then we need two wrapper Classes so that we can abstract the functioning. Our classes can very well be Player and Tournament.

class Player(object):
“””
A Player class
“””
userid = None
assets = ”
name = ”
updates = {}
_player = None

def __init__(self, userid=None):
self.userid = userid

def _get_tournament(self):
if self.userid:
return Game.all(keys_only=True).filter(‘players = ‘, self.userid).get()

def get_player(self):
if self._player:
return self._player
if self.userid:
self._player = PlayerGame.get_by_key_name(self.userid,
parent = self._get_tournament())
return self._player

def get_gameroom(self):
pg = self.get_player()
if pg:
return pg.parent().key().name()

def die(self):
playergame_key = db.Key.from_path(‘PlayerGame’, self.userid)
game = Game.all().filter(‘user = ‘, self.userid).get()

def txn(userid, pg_key, g):
db.delete(pg_key)
g.users.pop(userid)
db.put(g)

db.run_in_transaction(txn, self.userid, playergame_key, game_key)

The Player class contains all the wrapper functions that a player can possible do inside a chatroom/gameroom. Similarly, there shall be a Tournament class which will do all the activities that can be done in a tournament

class Tournament(object):
“””
A class for tournament
“””
game = None
room = None
delta_chat = ”
updates = {}

def __init__(self, room=None):
self.updates = {}
self.room = room

def get_tournament(self):
if self.game:
return self.game
if self.room:
self.game = Game.get_by_key_name(self.room)
return self.game

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

def add_player(self, player):
if self.can_add_player():
game = self.get_tournament()
if player.userid not in game.players:
game.players.append(player.userid)
db.put(game)
PlayerGame.get_or_insert(key_name = player.userid,
parent = game)
self.updates.update({'new_player' : 1,
'name' : player.userid})
self.send_update()
else:
return self.new(player)

def new(self, player):
"A new room shall be created only if there is a player to go into"
if not player: return
self.room = str(time.time())
self.game = Game(key_name = self.room,
players = [player.userid])
playergame = PlayerGame(key_name = player.userid,
parent = self.game)
db.put([self.game, playergame])
memcache.set(LATEST_GAMEROOM, self.room)
return self.room

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

@classmethod
def continue_tournament(cls, player):
"When the player was already in a tournament continue from there itself"
channel.send_message(player.get_channel(), cls().get_all_updates())

def chat(self, player, message):
delta_chat = message
game = self.get_tournament()
if game.chat:
chat = game.chat + '
' + delta_chat
else:
chat = delta_chat
game.chat = chat
db.put(game)
self.updates.update({'delta_chat' : delta_chat})
self.send_update()

def get_player_channels(self):
game = self.get_tournament()
return [Player(x).get_channel() for x in game.players]

def get_game_message(self):
update = self.updates
return simplejson.dumps(update)

def get_all_updates(self):
update = self.updates
update.update({'all' : 'all'})
return simplejson.dumps(update)

def send_update(self):
message = self.get_game_message()
for channel_id in self.get_player_channels():
channel.send_message(channel_id, message)

Flow

Whenever a user comes to the app, first of all it is checked if he was in an existing room or not. If found, then his activities are continued from that stage. If not found then the user is taken to a room. The room could be a new room, if all others are full, or no room is present. The user can perform activities in that room. Filling of rooms goes in First Come, First Serve order. So, when the next user wishes to join a room, the latest room created is checked for vacancy and the user is dropped there. The user is then free to perform actions and have fun.

What is typically important here is that, initially a Channel is created between the client and the server, and then using that same channel all the future communication for rooms goes. One advantage of this is that we don’t need to create a lot of channels for every user. For this it is very important that the algo which is used to create channel client id has only one variable as userid. (What will happen if we have two variables like userID and timestamp to create the channel client id?).

def gen_channel(userid):
return md5.md5(userid).hexdigest()

After the channel is created, the client send requests to join the room. This is taken care by the server and accordingly the user is dropped into a new room or an existing room

class JoinGame(BaseHandler):
def post(self):
“””
Processes the req from a client/player to join a gameroom
“””
user = users.get_current_user()
if not user:
return self.redirect(users.create_login_url(‘/’))

userid = user.user_id()
logging.info(‘join chat call by %s’%userid)
player = Player(userid)
if not player.get_gameroom():
gameroom = Tournament.join_new_or_latest(player)
return
Tournament.continue_tournament(player)

This is in a nutshell, a proof of concept about building huge chat-rooms/ game-rooms using Channel API of Google App Engine.

Resources

Read more about optimizing this code in part 2 of the series.

  1. Source Code: http://code.google.com/p/pranav/source/browse/chat-channel
  2. Channel API: http://code.google.com/appengine/docs/python/channel
  3. Discuss: Google Groups Discussion
  4. Tic Tac Toe App: http://code.google.com/p/channel-tac-toe

There are many more things that needs to be taken care of to increase speed and performance, but that shall come in another blog post

PyCon India 2010 Videos – Building Scalable Apps using Google App Engine

The Video shooting for PyCon India 2010 held on 25th and 26th Sept at Bangalore, was done by the media partner Vodex. They send the DVDs and CDs to people who have opted for it. Here I am sharing the videos from my talk on “Building Scalable Apps Using Google App Engine”. The video processing was done excellently in complete synchronization with the presentation slides. As a side effect of this, the videos are available in parts as per the slides.

Facebook Group's Daily Digest Emails


Hey folks. So, wondering about the hell of emails facebook send you for every activity that is done in a group you are member of? Well, there is one official way to get rid of it – revoke permissions about facebook sending you emails. But then, you also don;t want to be left out, right? What if you could have a daily digest sort of thing for facebook groups? Won’t that be great?

Time to welcome Group Email Digest app (facebook link, website). This is a real simple, sweet app that keeps you updated about the activities performed in your facvorite group on a daily basis. The app’s motto is “To keep your inbox updated and uncluttered“.

Let me tell you more about this app. When you visit the app’s home page (http://digest.myblive.com), it will ask you to login with your facebook account, and ask the required permissions. The need for various permissions has been very clearly mentioned there itself. After you login, you will find all the groups you are currently a member of. Just check those groups for which you want a daily digest update email and click on “Subscribe Digest Email” at the bottom. If you want to opt out from a particular group, just uncheck the group and again click on “Subscribe Digest Email”. That will opt you out.

The app is in beta phase. This means that there will be frequent changes in the application’s behavior. The changes will be for the good only. Also, there are server limitations on the number of emails per day, that can be send out (since the application is running in free quota now), so we can support only a limited number of users right now. The limit should be somewhere around 1,000 however exact count shall be available only after observation for couple of days.

Let me know your views, critism and praises, so I will keep improving upon it.

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.