Search code examples
javagoogle-app-enginegoogle-cloud-datastoreobjectify

Force strong consistency with objectify on GAE


I'm trying to achieve Strong consistency in datastore, don't know any other way to handle my requirement. Am having an application where users connect to each other randomly. so basically on 'connect' api call, it queries for QueueUser entities, if it doesn't find any, it will push calling user in the queue.

ofy().load().type(QueueUser.class)
    .filterKey("!=", KeyFactory.createKey("QueueUser", caller.id))
    .order("__key__")
    .order("-time")
    .limit(5)
    .keys();

I understand that this will not fetch latest entity keys, as Indexes may not be up to date. so I interate through keys and get each entity by it's key. If I get a non-null entity, I'll try to delete it. If I succeed in that, I assume that it's a match of users. This get-by-key and delete() is inside Transaction.

            while(keyIterator.hasNext()) {
              QueueUser queueUser = null;
              try {
                  final Key<QueueUser> key = keyIterator.next();
                  queueUser = ofy().transactNew(1, new Work<QueueUser>() {
                      public QueueUser run() {
                          QueueUser queueUser = ofy().load().key(key).now();
                          if(queueUser == null) {
                              logger.log(Level.WARNING, "queue user was already deleted");
                              return null;
                          }
                          else
                              ofy().delete().key(key).now();
                          return queueUser;
                      }
                  });
              } catch (ConcurrentModificationException e) {
                  logger.log(Level.WARNING, "exception while deleting queue user. err: " + e.getMessage());
              }
              if (queueUser != null) {
                 // we have a match here
                 // delete calling user from the queue if it's there
                 ofy().delete().entity(new QueueUser(caller.id, null, null, null, null, null, null, null, null)).now();
                 break;
              }
          }

This works most of the time, but sometimes users get pushed in the queue and not picked up quickly. Request latency goes up to 15 seconds. Query returns lot of entities but most are deleted, and some gets deleted by peer request(expected ConcurrentModificationException).

I want to know if I am missing anything obvious here, or is there any better way to handle this.


Solution

  • This code makes absolutely no sense and doesn't do what you want it to. Sorry. I recommend deleting all the code and just include this question:

    Am having an application where users connect to each other randomly. so basically on 'connect' api call, it queries for QueueUser entities, if it doesn't find any, it will push calling user in the queue.

    Assuming you need to do this at scale (many times per second), this is actually a hard thing to do with the datastore. The datastore does not like rapidly mutating state. What you really have is not a queue but one single spot; whenever someone connects they are either put in the spot (if it's empty) or paired with the person in the spot (making the spot empty).

    I would use a different tool. There's really no point in storing persistent state for something as ephemeral as matchmaking. Clients will need retry logic anyways.

    Memcache can be a good choice - use getIdentifiable and putIfUntouched to make it transactional, combined with some retry logic. The only issue is that GAE's Memcache occasionally becomes unavailable (it's not as reliable as the datastore). If you're building the matchmaking engine for a popular game, you may want to run your own memcached in GCE.

    Honestly it's probably even easier to build your own in-memory service and run it as a singleton. A simple version is maybe 10 lines of code.