In the past 3 years I never used the WeakHashMap in any project. That changed yesterday. In the game that we are currently developing, we are using a mechanism where the game client is sending game events to the server. The server will then evaluate the game events and alter the users in memory before their state is made persistent in the database. Here is an example:
public interface GameEvent { /** * Subclasses may implement this to run validation logic, before the GameEvent is processed. * * @param user the {@link User} to apply this {@link GameEvent} on * @return {@link AuditResult} never <code>null</code> */ AuditResult validate(User user); /** * Subclasses may implement this to run implementation specific logic, potentially altering the * given {@link User}. * * @param user the {@link User} to apply this {@link GameEvent} on * @return {@link AuditResult} never <code>null</code> */ AuditResult process(User user); } public class ConsumeFood implements GameEvent { private final int amount; public ConsumeFood(final int amount) { this.amount = amount; } @Override public AuditResult validate(final User user) { if (user.getFood() < this.amount) { return AuditResult("User doesn't have this amount of food."); } return AuditResult.SUCCESS; } @Override public AuditResult process(final User user) { user.addEnergy(this.amount); user.subtractFood(this.amount); return AuditResult.SUCCESS; } }
Once the game client sends us the ConsumeFood game event, we subtract food from the player and add energy instead. We also have a wrapper class around a collection of game events and the execution logic looks like this:
public class GameEvents { .. other methods ... protected AuditResult process(final User user, final GameEvent change) { final AuditResult validateResult = change.validate(user); if (validateResult == AuditResult.SUCCESS) { return change.process(user); } else { return validateResult; } } }
First we validate that we can apply the game event, then we process the event and alter the player. Since the number of different game events is continuously growing and growing, I thought it might be useful to measure the execution time of the validate and the process method in each game event. The way I implemented this a while ago, was through delegation. I added a wrapper class which was wrapping the real game event and timing the validate and process method:
import org.springframework.util.StopWatch; public final class TimingGameEvent implements GameEvent { private final GameEvent gameEvent; public TimingGameEvent(final GameEvent gameEvent) { this.gameEvent = gameEvent; } /** * Delegates the processing to the encapsulated {@link GameEvent}. Uses a {@link StopWatch} to time the * execution time. */ @Override public AuditResult process(final User user) { final StopWatch stopWatch = new StopWatch("process-stop-watch"); stopWatch.start(); try { return this.gameEvent.process(user); } finally { stopWatch.stop(); this.processTimeInMs = stopWatch.getLastTaskTimeMillis(); } } /** * Delegates the validation to the encapsulated {@link GameEvent}. Uses a {@link StopWatch} to time the * execution time. */ @Override public AuditResult validate(final User user) { final StopWatch stopWatch = new StopWatch("validate-stop-watch"); stopWatch.start(); try { return this.gameEvent.validate(user); } finally { stopWatch.stop(); this.validationTimeInMs = stopWatch.getLastTaskTimeMillis(); } } }
This worked well. However, this week we got another requirement from business. We needed to implement some sort of gameplay recorder. Each game event that the server receives must be recorded, so we can replay these events later. My first idea was to add another wrapper around the already existing TimingGameEvent wrapper class but this would have made it difficult to serialize the real game event to a File. Yes we decided to serialize to and deserialize from a String, which is stored in a plain textfile and each line represents one game event. I discarded the idea to add other wrappers around the game event and suggested a refactoring. Instead of using delegating wrappers, why not use a listener mechanism. Each listener would be notified before and after execution of the validate and the process method in each game event. Listeners could register themselves and it would be easier to extend in the future. On the negative side, of course measuring the execution times would not be as accurate anymore, as there could be other listeners which want to be notified before the game event is validated and processed. This however was not a big issue, since we were not interested in the exact time in milliseconds but rather in long running methods of a couple of seconds. I also added a mechanism to make sure the execution timing listener would get notified just before the game event method was executed and right after it was returning. More on that later.
Here is the listener interface I came up with:
public interface GameEventLifecycleListener { void onValidationStart(final User user, final GameEvent gameEvent); void onValidationFinish(final User user, final GameEvent gameEvent, final AuditResult auditResult); void onProcessStart(final User user, final GameEvent gameEvent); void onProcessFinish(final User user, final GameEvent gameEvent, final AuditResult auditResult); }
Refactoring the TimingGameEvent class from above to a TimingGameEventLifecycleListener wasn't straight forward. Each invocation of the validate or the process method will now result in two listener notifications. So how do you know when to “press” stop on the StopWatch?
This is where the WeakHashMap comes in handy. Remember that each game event is going through the same chain? First onValidationStart is called, then onValidationFinish, onProcessStart and finally onProcessFinish. So the Listener could maintain a Map of all event implemented using a WeakHashMap. The first notification callback will add the game event to this Map. Subsequent notifications can assume that the game event will be present in the WeakHashMap. After the game event has passed through the chain and no object is referencing the game event anymore, it will automatically be removed from the WeakHashMap. Here is a part of the TimingGameEventLifecycleListener which will show you the concept.
import org.springframework.core.Ordered; public class TimingGameEventLifecycleListener extends AbstractGameEventLifecycleListener { /** * By default the WeakHashMap is not thread-safe, so it needs to be wrapped in a synchronizedMap. This however * is quite slow, hence the ExecutionTimingGameEventLifecycleListener should not be running in production * all the time. */ private final Map<GameEvent, TimedExecution> timedExecutions = Collections.synchronizedMap( new WeakHashMap<GameEvent, TimedExecution>() ); @Override public void onValidationStart(final User user, final GameEvent gameEvent) { final TimedExecution timeValidation = new TimedExecution(gameEvent.getClass()); this.timedExecutions.put(gameEvent, timeValidation); // other stuff } @Override public void onValidationFinish(final User user, final GameEvent gameEvent, final AuditResult auditResult) { final TimedExecution timeValidation = this.timedExecutions.get(gameEvent); if (timeValidation != null) { timeValidation.stopTimer(); } } ... other notification methods ... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
So the WeakHashMap can be nice in the role of a cache between different Listener methods. Another thing you may notice in the code above, is that the Listener is deriving from AbstractGameEventLifecycleListener instead of implementing GameEventLifecycleListener. I added a abstract base class for two reasons. First it is better to provide empty default implementations of all notification methods. Concrete Listeners like the TimingGameEventLifecycleListener can then only overwrite the methods they are interested in (okay in this case we are interested in all four notification methods, but other Listeners might not be). The second reason is that we want to force the Listeners to be in a specific order. Every Listener can for himself decide "how important" he is by implementing the getOrder() method defined in the org.springframework.core.Ordered interface which the AbstractGameEventLifecycleListener is implementing. Normally this interface is used by Spring to apply an order to Aspects. Though you might choose to keep your domain clean of Spring framework classes. Here is the AbstractGameEventLifecycleListener:
public class GameEvents { private final GameEvent[] events; private final NavigableSet<AbstractGameEventLifecycleListener> listeners; public void addListeners(final Collection<AbstractGameEventLifecycleListener> listeners) { this.listeners.addAll(listeners); } public GameEvents(final GameEvent[] events) { final int length = events == null ? 0 : events.length; this.listeners = new TreeSet<AbstractGameEventLifecycleListener>(); this.events = new GameEvent[length]; if (length > 0) { System.arraycopy(events, 0, this.events, 0, length); } } // other methods protected AuditResult process(final User user, final GameEvent gameEvent) { final AuditResult validateResult = gameEvent.validate(user); if (validateResult == AuditResult.SUCCESS) { return gameEvent.process(user); } else { return validateResult; } } /** * Runs the {@link GameEvent#validate(User)} function of the given * {@code gameEvent}, notifying all {@link AbstractGameEventLifecycleListener}s * before and after. The listener having the highest * precidence is notified last before and first after the validation method. * @param user the {@link User} to validate the game event for * @param gameEvent the gameEvent to validate * @return the result of the validation */ AuditResult runValidate(final User user, final GameEvent gameEvent) { for (Iterator<AbstractGameEventLifecycleListener> iterator = this.listeners.descendingIterator(); iterator.hasNext(); ) { final AbstractGameEventLifecycleListener listener = iterator.next(); listener.onValidationStart(user, gameEvent); } final AuditResult validateResult = gameEvent.validate(user); for (final AbstractGameEventLifecycleListener listener : this.listeners) { listener.onValidationFinish(user, gameEvent, validateResult); } return validateResult; } /** * Runs the {@link GameEvent#process(User)} function of the given * {@code gameEvent}, notifying all {@link AbstractGameEventLifecycleListener}s * before and after. The listener having the highest * precedence is notified last before and first after the validation method. * @param user the {@link User} to process the gameEvent for * @param gameEvent the audit gameEvent to process * @return the result of processing the gameEvent */ AuditResult runProcess(final User user, final GameEvent gameEvent) { for (Iterator<AbstractGameEventLifecycleListener> iterator = this.listeners.descendingIterator(); iterator.hasNext(); ) { final AbstractGameEventLifecycleListener listener = iterator.next(); listener.onProcessStart(user, gameEvent); } final AuditResult validateResult = gameEvent.process(user); for (final AbstractGameEventLifecycleListener listener : this.listeners) { listener.onProcessFinish(user, gameEvent, validateResult); } return validateResult; } }
I said earlier, it is desirable to notify the TimingGameEventLifecycleListener last before validation starts and first after it finishes (to get more accurate timings). The GameEvents class, which is notifying the listeners, will honor the order using a NavigableSet that can be iterated in forward and backward order. Take a look at the updated version of the GameEvents class to see how it is implemented:
public abstract class AbstractGameEventLifecycleListener implements GameEventLifecycleListener, Ordered, Comparable<GameEventLifecycleListener> { @Override public void onValidationStart(final User user, final GameEvent gameEvent) { } @Override public void onValidationFinish(final User user, final GameEvent gameEvent, final AuditResult auditResult) { } @Override public void onProcessStart(final User user, final GameEvent gameEvent) { } @Override public void onProcessFinish(final User user, final GameEvent gameEvent, final AuditResult auditResult) { } /** * Compares the order of the two {@link GameEventLifecycleListener}s * using {@link Ordered}. * @param other another {@link GameEventLifecycleListener} * @return int */ @Override public int compareTo(final GameEventLifecycleListener other) { return Integer.valueOf(this.getOrder()).compareTo(other.getOrder()); } }
One thing I wasn't able to come up with, was a good unit test to verify that the WeakHashMap is indeed not holding key references forever. This is extremely difficult to test as it involves testing for garbage collection and no, I am not suggesting running System.gc() from your test. I found something similar on this blog post. Apparently the Netbeans API offers something called assertGC(..) but it wasn't really fitting for my use case. So if you have a good suggestion how to test the behavior of a WeakHashMap, I am happy to hear it.
* UPDATE * UPDATE * After a few weeks running this the WeakHashMap and seeing some weird errors in the logs every now and then, I realized it's not the right Map implementation to use. The WeakHashMap is not what you want to use here, because the keys are not really compared using object identity. Initially I thought this was the case, when reading through the Javadoc of the WeakHashMap. What you really want is a hybrid Map, that combines the WeakHashMap with a IdentityHashMap. This hybrid Map will compare the keys based on objects identity and also use weak key references. The bad news is, there is no such map in the JDK (Java 6 at least). The good news is, there is a WeakIdentityHashMap in the Hibernate Search project and a ReferenceIdentityMap in the Commons Collections Project which can be used.
0 Kommentare:
Kommentar veröffentlichen