package net.gnehzr.tnoodle.scrambles; import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert; import java.security.SecureRandom; import java.util.LinkedList; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; /* * In addition to speeding things up, this class provides thread safety. */ public class ScrambleCacher { private static final Logger l = Logger.getLogger(ScrambleCacher.class.getName()); private static final int DEFAULT_CACHE_SIZE = 100; /** * Puzzles will get passed this instance of Random * in order to have nice, as-secure-as-can-be scrambles. */ private static final Random r = new SecureRandom(); private String[] scrambles; private volatile int startBuf = 0; private volatile int available = 0; public ScrambleCacher(final Puzzle puzzle) { this(puzzle, DEFAULT_CACHE_SIZE, false); } private volatile Throwable exception; private boolean running = false; public ScrambleCacher(final Puzzle puzzle, int cacheSize, final boolean drawScramble, ScrambleCacherListener l) { this(puzzle, cacheSize, drawScramble); ls.add(l); } public ScrambleCacher(final Puzzle puzzle, int cacheSize, final boolean drawScramble) { azzert(cacheSize > 0); scrambles = new String[cacheSize]; Thread t = new Thread() { public void run() { synchronized(puzzle.getClass()) { // This thread starts running while scrambler // is still initializing, we must wait until // it has finished before we attempt to generate // any scrambles. } for(;;) { String scramble = puzzle.generateWcaScramble(r); if(drawScramble) { // The drawScramble option exists so we can test out generating and drawing // a bunch of scrambles in 2 threads at the same time. See ScrambleTest. try { puzzle.drawScramble(scramble, null); } catch (InvalidScrambleException e1) { l.log(Level.SEVERE, "Error drawing scramble we just created. ", e1); } } synchronized(scrambles) { while(running && available == scrambles.length) { try { scrambles.wait(); } catch(InterruptedException e) {} } if(!running) { return; } scrambles[(startBuf + available) % scrambles.length] = scramble; available++; scrambles.notifyAll(); } fireScrambleCacheUpdated(); } } }; t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { l.log(Level.SEVERE, "", e); // Let everyone waiting for a scramble know that we have crashed exception = e; synchronized(scrambles) { scrambles.notifyAll(); } } }); running = true; t.start(); } public void stop() { synchronized(scrambles) { running = false; scrambles.notifyAll(); } } public boolean isRunning() { return running; } private LinkedList<ScrambleCacherListener> ls = new LinkedList<ScrambleCacherListener>(); /** * This method will notify all listeners that the cache size has changed. * NOTE: Do NOT call this method while holding any monitors! */ private void fireScrambleCacheUpdated() { for(ScrambleCacherListener l : ls) { l.scrambleCacheUpdated(this); } } public int getAvailableCount() { return available; } public int getCacheSize() { return scrambles.length; } /** * Get a new scramble from the cache. Will block if necessary. * @return A new scramble from the cache. */ public String newScramble() { if(exception != null) { throw new RuntimeException(exception); } String scramble; synchronized(scrambles) { while(available == 0) { try { scrambles.wait(); } catch(InterruptedException e) {} if(exception != null) { throw new RuntimeException(exception); } } scramble = scrambles[startBuf]; startBuf = (startBuf + 1) % scrambles.length; available--; scrambles.notifyAll(); } fireScrambleCacheUpdated(); return scramble; } public String[] newScrambles(int count) { String[] scrambles = new String[count]; for(int i = 0; i < count; i++) { scrambles[i] = newScramble(); } return scrambles; } }