3

Well, perhaps not corrupted as such.

So, a little background. I've recently moved my Red5 powered game from a windows build of red5 to one running on Debian Squeeze. I have a game lobby which uses a shared object to maintain a list of the various available games.

A single game is stored as a HashMap[String, Object] against it's game_id in said SharedObject. A couple of the properties of the HashMap are ArrayLists, specifically players (an ArrayList[Integer] of connected player id's) and votes (another ArrayList[Integer] of players who have submitted a vote)

Whenever I make a change to either of these ArrayLists, something, somewhere goes wrong and I can no long write the HashMap to the SharedObject (setAttribute returns false)

Creating a new game (server side):

HashMap<String, Object> game = new HashMap<String, Object>();
game.put("id", PendingGameManager.GAME_IDX);
game.put("difficulty", difficulty);
game.put("type", type);
game.put("description", this.getDescription(type, difficulty));
game.put("players", new ArrayList<Integer>());
game.put("coords", coords);
game.put("created", Calendar.getInstance().getTimeInMillis());
game.put("votes", new ArrayList<Integer>());
boolean success = this.gamesSO.setAttribute(Integer.toString(PendingGameManager.GAME_IDX), game);

This executes without problem, and success returns true.

Later I retrieve the player array and make amendments:

HashMap<String, Object> game = (HashMap<String, Object>)this.gamesSO.getMapAttribute(Integer.toString(game_id));
ArrayList<Integer> players = (ArrayList<Integer>) game.get("players");
players.add(new Integer(Integer.parseInt((user_id))));
boolean success = this.gamesSO.setAttribute(Integer.toString(game_id), game);

here success always returns false. If a create a new HashMap for the game and copy across each property from the old one but omitting players and votes it is fine, but what ever try, I cannot get it to maintain an array. I've also tried this with List and Vector with the same results. This is my first contact with Java, I've been careful to only add class instances of Integer and not the primitive int but for all my efforts I have run out of ideas.

When on Windows it ran perfectly, my original implementation used ArrayList[String] instead of ArrayList[Integer]

Environment: Debian Squeeze 6.0.6 jre 1.7 Red5 1.0RC2

Any help or suggestions would be greatly appreciated!

milks
  • 314
  • 2
  • 9
  • gamesSO is an instance of [SharedObject](http://dl.fancycode.com/red5/api/org/red5/server/so/SharedObject.html) – milks Oct 15 '12 at 17:29

3 Answers3

2

Based on your red5 version info, this is the implementation of the method "setAttribute":

@Override
public boolean setAttribute(String name, Object value) {
    log.debug("setAttribute - name: {} value: {}", name, value);
    boolean result = true;
    ownerMessage.addEvent(Type.CLIENT_UPDATE_ATTRIBUTE, name, null);
    if (value == null && super.removeAttribute(name)) {
        // Setting a null value removes the attribute
        modified = true;
        syncEvents.add(new SharedObjectEvent(Type.CLIENT_DELETE_DATA, name, null));
        deleteStats.incrementAndGet();
    } else if (value != null && super.setAttribute(name, value)) {
        // only sync if the attribute changed
        modified = true;
        syncEvents.add(new SharedObjectEvent(Type.CLIENT_UPDATE_DATA, name, value));
        changeStats.incrementAndGet();
    } else {
        result = false;
    }
    notifyModified();
    return result;
}

I guess value is != null (but I could be wrong). But in my opinion it would forward that call to its parent class with the "super.setAttribute" call, this is the implementation of the parent/super class:

/**
 * Set an attribute on this object.
 *
 * @param name  the name of the attribute to change
 * @param value the new value of the attribute
 * @return true if the attribute value was added or changed, otherwise false
 */
public boolean setAttribute(String name, Object value) {
    if (name != null) {
        if (value != null) {
            // update with new value
            Object previous = attributes.put(name, value);
            // previous will be null if the attribute didn't exist
            return (previous == null || !value.equals(previous));
        }
    }
    return false;
}

The important line here (IMHO):

return (previous == null || !value.equals(previous));

=> "previous" cannot be found, and then it returns false.

The issue is I think: This cast that you are doing:

HashMap<String, Object> game = (HashMap<String, Object>)this.gamesSO.getMapAttribute(Integer.toString(game_id));

I don't think that "this.gamesSO.getMapAttribute(Integer.toString(game_id));" will return HashMap, I think I can remember that Red5 has its own Map type.

If you simply debug and add a:

System.out.println(this.gamesSO.getMapAttribute(Integer.toString(game_id)));

and/or add some debug breakpoint and verify what type exactly this is. And then cast to this one really.

I think you should also specify the Map more detailed. Something like:

 HashMap<String, MyPlayerBean>

And create a class MyPlayerBean, with the attributes you really need. Making those Map/List objects might be handy to get started quickly but it can get quite ugly if your application starts to grow.

Sebastian

seba.wagner
  • 3,800
  • 4
  • 28
  • 52
  • Hey thanks for the info. Being able to see some of the source code has been really useful and with some further investigation I thnk I've pinned down the problem. The line of code you identified: return (previous == null || !value.equals(previous)); does seem to be the cause, but it's the second part of the statement, because I'm modifying a reference to the map stored in the SO before _always_ equals after – milks Oct 16 '12 at 12:19
  • also if I clone the map before editing, before still equals after as both maps share the same reference to the array list and java appears to consider the map's hashcode as well to compare the contents. I guess I will have to clone both the map and the array before making changes or clone just the map and maintain a _changeCount_ property. It all seems rather clunky, if only there were some way to force it to update/synchronise. – milks Oct 16 '12 at 12:23
  • I'd vote your answer up, but I don't have enough reputation... sorry :/ – milks Oct 16 '12 at 12:55
0

So I should probably give my findings as an answer for the sake of clarification.

Since the I'm grabbing a reference of the HashMap (being a complex data type as opposed to primitive) stored in the SharedObject, any changes I make are also reflected in the current value of that slot so that when it is then set again, Red5 fails to spot the difference.

new_val.equals(old_val); // returns true since they both reference the same instance of HashMap

if I clone the HashMap before making changes to an ArrayList contained in the HashMap the above statement still evaluates as true since the ArrayList is the same instance as is stored in the current slot and java also considers each object's hashcode when evaluating .equals() (in effect, comparing the contents of both HashMaps)

I therefore have to clone both the HashMap and the ArrayList before making changes in order for the changes to be detected and synchronised with the clients. Or alternatively (as I ended up implementing) clone the HashMap, make the changes and additionally update a primitive property, in my case int changeCount

It would be great if Red5 had a mechanism to force a sync for such cases (such as using setDirty instead of setProperty in the client side Actionscript)

milks
  • 314
  • 2
  • 9
0

I had a similar problem when updating a shared object property which was an ArrayList. I ended up removing that property and then setting it again.

ArrayList<String> userlist = (ArrayList<String>) so.getAttribute("userlist");
userlist.remove(username);
so.beginUpdate();
so.removeAttribute("userlist");
so.setAttribute("userlist", userlist);
so.endUpdate();