Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Thursday, April 02, 2009

Aspect Ratio & You

There are no shortages of libraries and toolkits available to programmers for scaling images. But if you ever find yourself in a position, as I recently have, where you need to roll your own (or maybe you are just curious) I'll explain everything you need to know about maintaining the aspect ratio of a scaled image.

When the issue of scaling images landed on me, the first thing I did was to google it. The search results were not very satisfactory, thus this blog entry.

So what are we talking about when we use the term "aspect ratio"? It's the relationship between an image's height and its width. From a programmatic point of view, the aspect ratio can tell us how much the width of an image should change if the height changes and vice versa. Aspect ratios are normally expressed in the form H:W (i.e., 1:1, 7:3, 4:5, etc). It can also be expressed as a fraction (i.e. 1/1, 7/3, 4/5, etc) and finally, for programmatic purposes, a decimal. The formula for the aspect ratio is:

A = H/W
where A is the aspect ratio, H is the height of the image, and W is the width of the image. Using a bit of algebra we can rewrite the formula to solve for any of the variables. So given that A = H/W then H = A*W and W = H/A.

Lets assume we have an 485px x 1024px image that we need to generate a thumbnail for. The first thing we need to do is determine the aspect ratio of the image:

A = H/W => A = 485/1024 => A = 0.4736328125
Lets also assume that we have this rule that says a thumbnail image must be no more than 140 pixels high. We now have enough information to figure out what the width must be in order to maintain the image's aspect ratio:
W = H/A => W = 140/0.4736328125 => W = 295.587628866
We know the new width maintains the aspect ratio because 140/295.587628866 = 0.4736328125. Now let's look at some code:
/** 
 * Scale <tt>src</tt>'s dimensions to <tt>max</tt> pixels starting w/ the largest side. 
 * 
 * @param image      The source image. 
 * @param max        The maximum number of pixels in each dimension(HxW). 
 * @param heightOnly Indicates that only the image's height should be scaled. 
 * 
 * @return The scaled image. 
 */ 
public static BufferedImage scale(BufferedImage image, final int max, boolean heightOnly) 
{ 
    if (heightOnly) 
        image = scaleByHeight(image, max); 
    else if (image.getHeight() > image.getWidth()) 
    { 
        image = scaleByHeight(image, max); 
        image = scaleByWidth(image, max); 
    } 
    else 
    { 
        image = scaleByWidth(image, max); 
        image = scaleByHeight(image, max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> by <tt>height</tt>. 
 * 
 * @param image The source image. 
 * @param max   The value to scale the image down to. If the current height of the image is less than <tt>max</tt> then this 
 *              method does nothing. 
 * 
 * @return A (possibly) scaled image. 
 */ 
public static BufferedImage scaleByHeight(BufferedImage image, final int max) 
{ 
    int height = image.getHeight(); 
    if (height > max) 
    { 
        int width = image.getWidth(); 
        final float aspectRatio = height / (float)width; 
        do 
        { 
            height >>= 1; 
            if (height < max) 
                height = max; 
            int k = (int)(height / aspectRatio); 
            if (k > 0) 
                width = k; 
            image = scale(image, height, width); 
        } 
        while (height > max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> by <tt>width</tt>. 
 * 
 * @param image The source image. 
 * @param max   The value to scale the image down to. If the current width of the image is less than <tt>max</tt> then this 
 *              method does nothing. 
 * 
 * @return A (possibly) scaled image. 
 */ 
private static BufferedImage scaleByWidth(BufferedImage image, final int max) 
{ 
    int width = image.getWidth(); 
    if (width > max) 
    { 
        int height = image.getHeight(); 
        final float aspectRatio = height / (float)width; 
        do 
        { 
            width >>= 1; 
            if (width < max) 
                width = max; 
            int k = (int)(width * aspectRatio); 
            if (k > 0) 
                height = k; 
            image = scale(image, height, width); 
        } 
        while (width > max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> down to height x width pixels. 
 * 
 * @param src    The source image. 
 * @param height The scaled height. 
 * @param width  The scaled width. 
 * 
 * @return The scaled image. 
 */ 
private static BufferedImage scale(BufferedImage src, final int height, final int width) 
{ 
    int type = src.getType(); 
    if (BufferedImage.TYPE_CUSTOM == type) 
        type = src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 
    BufferedImage img = new BufferedImage(width, height, type); 
    Graphics2D gscale = img.createGraphics(); 
    gscale.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 
    gscale.drawImage(src, 0, 0, width, height, null); 
    gscale.dispose(); 
    return img; 
}

Thursday, January 24, 2008

My New Favorite Method

Some days I manage to really amuse myself with my work. Today I've added some flare to a method that may otherwise be really boring. So let me introduce you to my new favorite method:

  /** 
     * Flattens the sub directories of <tt>roots</tt> into a single array. 
     * 
     * @param roots The root directories. 
     * 
     * @return The [sorted] subdirectories of <tt>roots</tt>. 
     */ 
    private static File[] merge(File ... roots) 
    { 
        int x = 0; 
        int k = 0; 
        File[][] forest = new File[roots.length][]; 
        for (File sapling : roots) 
        { 
            File[] tree = sapling.listFiles(); 
            if (null != tree) 
            { 
                int leaves = tree.length; 
                if (leaves > 0) 
                { 
                    forest[x++] = tree; 
                    k += leaves; 
                } 
            } 
        } 
        File[] woods = new File[k]; 
        for (k = 0; --x >= 0;) 
        { 
            File[] tree = forest[x]; 
            int leaves = tree.length; 
            System.arraycopy(tree, 0, woods, k, leaves); 
            k += leaves; 
        } 
        Arrays.sort(woods, chipper); 
        return woods; 
    }

It cracks me up sometimes that I get paid to have this much fun.

Wednesday, January 02, 2008

Two Reasons To Use LineNumberReader Instead Of BufferedReader

Preamble

LineNumberReader extends BufferedReader so LineNumberReader "is a" BufferedReader. This "is a" distinction is important in this discussion, because I spend a bit of time talking about BufferedReader. But because of the "is a" relationship (a.k.a inheritance), anything said about BufferedReader is also true of LineNumberReader.

Reason Number One

LineNumberReader keeps track of line numbers. Duh!! right? But I had to say it. There would be no punchline if I didn't.

Reason Number Two

LineNumberReader compresses line terminators to a single newline character ('\n'). Now, "whoopty friggin doo!", is certainly a fair characterization of Reason Number Two, but bear with me.

I've observed over the years that many programmers use BufferedReader exclusively for it's readLine method, because it allows the programmer to work with lines at a time instead of individual characters. But sometimes you need to work a character at a time. BufferedReader is great for this. It is built for efficient reading of text data from non memory sources, like the filesystem or the network. The efficiency comes from the fact that individual calls to read do not map one-to-one with individual calls to the source(s). This results in less physical I/O which causes things to run much faster, which is always a plus.

Now, it's important to realize that you don't actually need BufferedReader to efficiently read text. You can create your own buffer using a char[] and read directly to/from your buffer. This cuts out the middle man, thus eliminating some object allocation, a bit of garbage collection, method calls and the overhead that goes with .hem. But what BufferedReader does for you that you would have to do for yourself, is detect the line terminators in the text. This detection is what makes readLine possible. LineNumberReader kicks it up a notch in that the "end of line" detection doesn't just affect readline, it also affects read. LineNumberReader's read method simplifies newline processing by returning a single newline character ('\n'), regardless of the type and quantity of them. So lets say you are working with text files on/from a system that uses CR+LF ("\r\n") as it's line terminator. Without LineNumberReader you would have to manually process the '\r' separately from the '\n'. With LineNumberReader you will only ever see the '\n', which simplifies the logic required to process the text being read.

Wrap Up

The impetus for this entry started two nights ago when I needed to whip up a little template processing system. I was working with an existing code base and I noticed that there were four types of notification files/messages that were being used. At least 60% of the text between the four messages were the same and about 90% of text between [specific] pairs of messages were the same. So I set out to unify the four files into a single file, a template, from which the original four messages can be derived. I'm posting the code that parses the master template because it showcases the benefit of having LineNumberReader handle "end of line" detection. If you are not used to doing this sort of text processing it may not be obvious how the code is benefiting from using LineNumberReader, so I'll spell it out for you. (1) LineNumberReader tracks line numbers. This time I'm not trying to be funny. The code explicitly throws one Exception and it uses the line number to make the error message more useful. (2) There is no newline "look ahead" nor "look behind" code. Without LineNumberReader the code would need to explicitly handle CR+LF, which requires "look ahead" or "look behind" semantics.

Code

/**
 * Creates a new "payment processor" specific template from the master template. 
 * 
 * @param pp The payment processor. 
 * 
 * @return A payment processor specific template. 
 * 
 * @throws IOException If there is a problem reading the master template. 
 */ 
private String getNotificationTemplate(PaymentProcessor pp) throws IOException 
{ 
    int token = UNDEF; 
    int state = UNDEF; 
    String tagname = null; 
    String ppname = pp.name(); 
    boolean matched = false; 
    StringBuilder tnb = new StringBuilder(); 
    StringBuilder sink = new StringBuilder(); 
    InputStream stream = getServletContext().getResourceAsStream("/WEB-INF/notification_template"); 
    try 
    { 
        LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, "UTF-8")); 
        for (int eof; -1 != (eof = reader.read());) 
        { 
            char c = (char)eof; 
            switch (c) 
            { 
                case '$': 
                    switch (token) 
                    { 
                        case UNDEF: 
                        case CONTENT: 
                            token = DOLLAR_SIGN; 
                            break; 
 
                        case START_TAG: 
                            token = END_TAG; 
                            break; 
                    } 
                    break; 
 
                case '{': 
                    switch (token) 
                    { 
                        case DOLLAR_SIGN: 
                            token = OPEN_BRACKET1; 
                            break; 
 
                        case OPEN_BRACKET1: 
                            token = OPEN_BRACKET2; 
                            break; 
 
                        default: 
                            token = UNDEF; 
                            break; 
                    } 
                    break; 
 
                case '}': 
                    switch (token) 
                    { 
                        case TAG_NAME: 
                            token = CLOSE_BRACKET1; 
                            break; 
 
                        case CLOSE_BRACKET1: 
                            token = START_TAG; 
                            break; 
 
                        default: 
                            token = UNDEF; 
                            break; 
                    } 
                    break; 
 
                case '\n':// No test for '\r' cause LineNumberReader compresses line terminators to a single '\n'. 
                    switch (token) 
                    { 
                        case START_TAG: 
                            token = state = CONTENT; 
                            matched = Arrays.asList(PIPE_REGX.split(tagname = tnb.toString())).contains(ppname); 
                            sink.setLength(sink.length() - tnb.length() - 5); 
                            continue; 
 
                        case END_TAG: 
                            if (tnb.toString().equals(tagname)) 
                            { 
                                if (matched) 
                                { 
                                    sink.setLength(sink.length() - tnb.length() - 6); 
                                    matched = false; 
                                } 
                                token = state = UNDEF; 
                            } 
                            else 
                            { 
                                String message = 
                                    "Illegal closing tag at line " + reader.getLineNumber() + ". Expected " + tagname + 
                                    " but found " + tnb + " instead."; 
                                throw new RuntimeException(message); 
                            } 
                            continue; 
                    } 
                    break; 
 
                default: 
                    if (OPEN_BRACKET2 == token) 
                    { 
                        token = TAG_NAME; 
                        tnb.setLength(0); 
                    } 
 
                    if (TAG_NAME == token) 
                        tnb.append(c); 
                    else if (CONTENT != token && CONTENT != state) 
                        token = UNDEF; 
                    break; 
            } 
 
            if (CONTENT != state || (CONTENT == state && matched)) 
                sink.append(c); 
        } 
    } 
    finally 
    { 
        stream.close(); 
    } 
    String template = sink.toString(); 
    switch (pp) 
    { 
        case PAYPAL: 
            paypalNotificationTemplate = template; 
            break; 
 
        case CREDITCARD: 
            ccNotificationTemplate = template; 
            break; 
    } 
    return template; 
}

Thursday, December 06, 2007

IDEA-7+Glassfish, First Impression

It's been years since I've worked with Java EE in any meaningful way. Back then it was called J2EE and my application server of choice was Resin because it was blazingly fast and allowed me to bypass all the J2EE crud --like EJBs, deployment descriptors, war files, ear files, etc-- and just get stuff done. Truth be told, I've always disliked J2EE. It just always smacked of self important (read as, Sun/Scott McNeally) bullshit to me. Sun and their "partners" (BEA, IBM, and others) managed to turn something as simple as serving dynamic content over HTTP into a multi billion dollar application server industry that hoodwinked a lot of people.

I made a conscious decision to avoid J2EE like raw broccoli when Caucho started transitioning from Resin 2 to 3. The transition was an abomination of galactic proportions. They completely redid the configuration system and did not provided any tools to move from the old version to the new one. And to guarantee that migration was a herculean effort, they provided documentation that was grossly incomplete and mostly inaccurate. But they didn't stop there. They were just warming up. The 3.0 release was beta quality software at best but was billed as production ready. The word went out that development on the Resin 2 branch had stopped and all development effort was going to be on 3. So any existing open issues (bug reports) against 2 was null and void and would be addressed in the 3 release. That may sound reasonable but it presupposes that 3 is usable in a production capacity. It wasn't. I spent weeks chasing deadlocks and other concurrency issues in the Resin code. So if you were a user that was affected by Resin 2 bugs you were asked to move to Resin 3 and since Resin 3 had even more bugs you were just fuc*ed. The final insult was, while Resin 2 made the J2EE stuff optional Resin 3 made it mandatory. I gave up on Resin 3 and went live with the 2.1.x and never looked back. That was over 3 years ago.

I recently started dabbling w/ J2EE again in a limited capacity. I needed to provide an HTTP interface to a server application I am working on and there was no way in hell I was going to try to climb the whole J2EE mountain just to provide HTTP access to the app. The simplest way I found to provide HTTP support was to embed Jetty and the simplest way to hook into Jetty's HTTP engine is via the Servlet interface. So though I'm using a servlet I really don't consider it J2EE.

As I said before, it's been approximately 3 years since I deployed my last J2EE app on Resin 2.1.x and they have just come due for upgrading. So the quest has been on to find an application server to replace Resin 2.1.x. Don't worry. This isn't going to turn into some long winded diatribe about all the application servers on the market and their pros and cons and yada yada yada. The truth of the matter is I've only tried one. Glassfish. I downloaded it yesterday minutes after 8PM and installed it at around the same time. My first impression can be summed up in one word, Wow!

The installation and startup was an absolute breeze. No config files in sight. The web based admin interface is simple and intuitive. I have yet to click on the help button for clarification on anything. There is also a command line interface that does everything the web based interface does. This is extremely cool because it means configuration becomes scriptable and thus can be completely automated.

There were only 3 pain points in my journey from getting the software to deploying a test servlet (that tests database connectivity). The first pain point was setting up the connection pool for the database. The JDBC drivers for PostgreSQL isn't bundled with Glassfish. The reason it's a pain point is because the configuration screen listed PostgreSQL as one of it's supported databases. It even prepopulates the Datasource Classname field with the correct PostgreSQL specific classname. So as a n00b, seeing all this, my default assumption is Glassfish comes with everything needed to communicate with the database. After a bit of head banging I finally turned to Google where I learned that I simply needed to copy the PostgreSQL JDBC driver to ${INSTALL_DIR}/domains/domain1/lib and restart the server.

The second pain point is not a Glassfish pain point but an IDE one. I've been using IntelliJ+IDEA for years --I'm using IDEA 7-- and this was the first time I've tried to use it's J2EE facilities. Pretty slick! Granted, my previous experience with this kind of thing is limited to emacs and nano on a Resin config file, so IDEA 7 to me, represents a major leap forward. I mentioned my bias because if this sort of thing is your day to day shtick, I hear Netbeans 6 provides an even better experience than IDEA 7 for Glassfish integration.

The first problem I ran into is understanding the difference between the local and remote configuration. It turns out local means IDEA manages the actual running of the application server. So if you already have it running, like I did, then local can't work with it. If you don't want IDEA to manage the running of the application server you have to use the remote configuration option. Remote seems limited to deployment only.

The third pain point is also an IDE one. This time I couldn't get deployment to work. This one actually pissed me off because the failure was silent on the part of IDEA. Without error messages how the hell am I supposed to know what's wrong? Fortunately, Glassfish is not the silent type. I nuked the Glassfish logs and restarted everything. After the deployment failed yet again I checked the log. This time there was a line in the log about a failed login attempt by the admin user. Finally, a clue. When I configured IDEA to work with Glassfish it prepopulated the username and password fields. My assumption was it was grabbing the data from the same place that Glassfish stores it. Wrong! The other clue was the number of asterisks in the password field was longer than the length of the new password --I changed the password from the default during the intial configuration of Glassfish--. Obviously, the problem was the password that IDEA was using was wrong. I manually entered the correct password and that solved the problem.

Other than those 3 minor issues IDEA+Glassfish has been a pleasure to use. I just hope Glassfish's performance lives up to the hype

Tuesday, September 18, 2007

[BUG] JE never stops logging ...

This is entry is a partial repost of a message I posted to Oracle's Berkeley DB JE Forum. The forum software does not allow for the proper formatting of source code and I personally hate reading unformatted source code. Therefore, I have reposted it here so people like me can't read the code right off the page.

The code

import java.io.File;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Random;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.evictor.Evictor;

import it.unimi.dsi.fastutil.ints.IntOpenHashSet;

public class CrushPunyJE
{
    private static final int NUM_DOMAINS = 1000;

    private static final int RECIPIENTS_PER_DOMAIN = 1000;

    public static void main(String[] args) throws Exception
    {
        crush(new File(args[0]));
    }

    private static void crush(File envHome) throws Exception
    {
        MyDbI dbi = MyDbI.openToFill(envHome);
        Cursor queue = dbi.queue().openCursor(null, null);
        Cursor recipients = dbi.recipients.openCursor(null, null);
        Cursor unsent = dbi.unsent.openCursor(null, null);
        DatabaseEntry ename = new DatabaseEntry();
        DatabaseEntry dname = new DatabaseEntry();

        try
        {
            Random rand = new Random();
            byte[] buff = new byte[512];
            for (int a = 0; a < NUM_DOMAINS; a++)
            {
                rand.nextBytes(buff);
                dname.setData(buff);
                for (int z = 0; z < RECIPIENTS_PER_DOMAIN; z++)
                {
                    rand.nextBytes(buff);
                    ename.setData(buff);
                    if (OperationStatus.SUCCESS == unsent.putNoDupData(dname, ename))
                    {
                        recipients.put(dname, ename);
                        queue.put(dname, ename);
                    }
                    else
                        z--;
                }
            }
        }
        finally
        {
            for (Cursor c : new Cursor[]{recipients, unsent, queue})
            {
                try
                {
                    c.close();
                }
                catch (DatabaseException e)
                {
                    e.printStackTrace();
                }
            }
            dbi.close();
        }

        final MyDbI dbi2 = MyDbI.open(envHome);
        final Runnable runnable = new Runnable()
        {
            public void run()
            {
                Evictor evil = (Evictor)getField(getField(dbi2.env(), "environmentImpl"), "evictor");
                while (true)
                {
                    evil.runOrPause(true);
                    try
                    {
                        Thread.sleep(10);
                    }
                    catch (InterruptedException e)
                    {
                    }
                }
            }
        };
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        t.setPriority(Thread.MAX_PRIORITY);
        t.start();
        dbi2.clearQueue().sync();
        dbi2.close();
    }

    private static Object getField(Object o, String fieldName)
    {
        Field field = getField(o.getClass(), fieldName);
        if (null == field)
            throw new AssertionError("Field '" + fieldName + "' not found.");
        else
        {
            try
            {
                return field.get(o);
            }
            catch (IllegalAccessException ex)
            {
                throw new AssertionError("IllegalAccessException thrown while accessing: ".concat(fieldName));
            }
        }
    }

    private static Field getField(Class co, String name)
    {
        for (Class stop = Object.class; co != stop; co = co.getSuperclass())
        {
            for (Field field : co.getDeclaredFields())
            {
                if (name.equals(field.getName()))
                {
                    field.setAccessible(true);
                    return field;
                }
            }
        }
        return null;
    }
    //====================================================================================================================//
    //====================================== Inner Class Definitions Start Here ==========================================//
    //====================================================================================================================//

    private static class MyDbI
    {
        /**
         * A small cache of prime numbers.
         */
        private static final IntOpenHashSet PRIMES = new IntOpenHashSet();

        /**
         * The maximum number of lock tables.
         */
        private static final int MAX_LOCK_TABLES = 523;

        /**
         * The databases.
         */
        public final Database recipients, unsent;

        /**
         * The delivery queue.
         */
        private static final String QUEUE_DB = "queue";

        /**
         * The database name of the unsent recipients.
         */
        private static final String UNSENT_DB = "unsent";

        /**
         * The database name for the recipient list database.
         */
        private static final String RECIP_DB = "recipients";

        private static final int CACHE_SIZE = 1024 << 10 << 4;

        public Database queue;

        /**
         * The database environment object.
         */
        private final Environment env;

        private final boolean dw, readonly;

        private MyDbI(File home, EnvironmentConfig ecfg, DatabaseConfig dbc) throws DatabaseException
        {
            dw = dbc.getDeferredWrite();
            readonly = dbc.getReadOnly();
            dbc.setSortedDuplicates(true);

            env = new Environment(home, ecfg);

            Transaction txn = ecfg.getTransactional() ? env.beginTransaction(null, null) : null;

            if (dbc.getAllowCreate())
            {
                queue = env.openDatabase(txn, QUEUE_DB, dbc);
                unsent = env.openDatabase(txn, UNSENT_DB, dbc);
                recipients = env.openDatabase(txn, RECIP_DB, dbc);
            }
            else
            {
                List names = env.getDatabaseNames();
                queue = names.contains(QUEUE_DB) ? env.openDatabase(txn, QUEUE_DB, dbc) : null;
                unsent = names.contains(UNSENT_DB) ? env.openDatabase(txn, UNSENT_DB, dbc) : null;
                recipients = names.contains(RECIP_DB) ? env.openDatabase(txn, RECIP_DB, dbc) : null;
            }

            if (null != txn)
                txn.commit();
        }

        /**
         * @return The Environment object that backs this DbI.
         */
        public Environment env()
        {
            return env;
        }

        /**
         * Get the queue database.
         *
         * @return The queue database.
         */
        public synchronized Database queue()
        {
            return queue;
        }

        public synchronized MyDbI clearQueue() throws DatabaseException
        {
            DatabaseConfig config = queue.getConfig();
            queue.close();
            env.truncateDatabase(null, QUEUE_DB, false);
            queue = env.openDatabase(null, QUEUE_DB, config);
            return this;
        }

        /**
         * Synchronizes {@link #unsent} with {@link #queue}.
         *
         * @return this
         *
         * @throws DatabaseException If there is a database error.
         */
        public synchronized MyDbI sync() throws DatabaseException
        {
            if (readonly)
                throw new IllegalStateException("read-only mode");

            DatabaseEntry k = new DatabaseEntry();
            DatabaseEntry v = new DatabaseEntry();
            Cursor uc = unsent.openCursor(null, null);
            try
            {
                if (dw)
                {
                    while (OperationStatus.SUCCESS == uc.getNext(k, v, null))
                        if (OperationStatus.SUCCESS != queue.putNoDupData(null, k, v))
                            assert false : "Duplicate email addresses not allowed in queue.";
                }
                else
                {
                    boolean commit = false;
                    Transaction txn = env.beginTransaction(null, null);
                    try
                    {
                        Cursor qc = queue.openCursor(txn, null);
                        try
                        {
                            while (OperationStatus.SUCCESS == uc.getNext(k, v, null))
                                if (OperationStatus.SUCCESS != qc.putNoDupData(k, v))
                                    assert false : "Duplicate email addresses not allowed in queue.";
                            commit = true;
                        }
                        finally
                        {
                            qc.close();
                        }
                    }
                    finally
                    {
                        if (commit)
                            txn.commit();
                        else
                            txn.abort();
                    }
                }
            }
            finally
            {
                uc.close();
            }
            return this;
        }

        /**
         * Synchronizes {@link #unsent} with {@link #queue}.
         *
         * @return this
         *
         * @throws DatabaseException If there is a database error.
         */
        public synchronized MyDbI sync2() throws DatabaseException
        {
            if (readonly)
                throw new IllegalStateException("read-only mode");

            DatabaseEntry k = new DatabaseEntry();
            DatabaseEntry v = new DatabaseEntry();
            Cursor uc = unsent.openCursor(null, null);
            try
            {
                if (dw)
                {
                    while (OperationStatus.SUCCESS == uc.getNext(k, v, null))
                        if (OperationStatus.SUCCESS != queue.putNoDupData(null, k, v))
                            assert false : "Duplicate email addresses not allowed in queue.";
                }
                else
                {
                    boolean commit = false;
                    Transaction txn = env.beginTransaction(null, null);
                    try
                    {
                        Cursor qc = queue.openCursor(txn, null);
                        try
                        {
                            for (int x = 0; OperationStatus.SUCCESS == uc.getNext(k, v, null);)
                            {
                                if (OperationStatus.SUCCESS != qc.putNoDupData(k, v))
                                    assert false : "Duplicate email addresses not allowed in queue.";
                                commit = true;
                                if (++x == 1000)
                                {
                                    x = 0;
                                    commit = false;
                                    qc.close();
                                    txn.commit();
                                    txn = env.beginTransaction(null, null);
                                    qc = queue.openCursor(txn, null);
                                }
                            }
                        }
                        finally
                        {
                            qc.close();
                        }
                    }
                    finally
                    {
                        if (commit)
                            txn.commit();
                        else
                            txn.abort();
                    }
                }
            }
            finally
            {
                uc.close();
            }
            return this;
        }

        /**
         * Closes the databases and environment.
         */
        public synchronized void close()
        {
            try
            {
                for (Database db : new Database[]{unsent, unsent, recipients})
                    db.close();
            }
            catch (DatabaseException e)
            {
                e.printStackTrace();
            }
            try
            {
                env.close();
            }
            catch (DatabaseException e)
            {
                e.printStackTrace();
            }
        }

        /**
         * Use when populating the databases.
         *
         * @param home The home directory of the blast.
         *
         * @return An environment optimized for single threaded write only access.
         *
         * @throws DatabaseException If there is a problem opening the databases or the database environment.
         */
        public static MyDbI openToFill(File home) throws DatabaseException
        {
            DatabaseConfig dcfg = new DatabaseConfig();
            dcfg.setAllowCreate(true);
            dcfg.setDeferredWrite(true);
            return new MyDbI(home, getFillConfig(), dcfg);
        }

        /**
         * Use for normal blast database access.
         *
         * @param home The home directory of the blast.
         *
         *
         * @throws DatabaseException If there is a problem opening the databases or the database environment.
         */
        public static MyDbI open(File home) throws DatabaseException
        {
            DatabaseConfig config = new DatabaseConfig();
            config.setAllowCreate(true);
            config.setTransactional(true);
            return new MyDbI(home, getDefaultConfig(), config);
        }

        /**
         * @return The number of lock tables based on the number of CPUs.
         */
        public static int getLockTableSize()
        {
            int cpus = Runtime.getRuntime().availableProcessors();
            if (cpus < 4)
                return 1;
            for (cpus = Math.min(MAX_LOCK_TABLES, cpus); cpus > 0 && !PRIMES.contains(cpus);)
                cpus--;
            return Math.max(1, cpus);
        }

        /**
         * Creates a new EnvironmentConfig object suitable for single threaded, write intensive access.
         *
         * @return An EnvironmentConfig object suitable for a single write only thread.
         */
        private static EnvironmentConfig getFillConfig()
        {
            EnvironmentConfig config = getNormalConfig();
            config.setCacheSize(1024 << 10 << 4);
            config.setAllowCreate(true);
            config.setLocking(false);
            return config;
        }

        /**
         * Creates a new com.sleepycat.dbi.EnvironmentConfig object suitable for normal data access patterns.
         *
         * @return An com.sleepycat.dbi.EnvironmentConfig object suitable for normal data access patterns.
         */
        private static EnvironmentConfig getNormalConfig()
        {
            EnvironmentConfig config = new EnvironmentConfig();
            config.setConfigParam("je.log.faultReadSize", "4096");
            config.setConfigParam("je.lock.nLockTables", Integer.toString(getLockTableSize()));
            return config;
        }

        /**
         * Configures an environment for normal transactional access.
         *
         * @return A configuration for normal transactional access.
         */
        private static EnvironmentConfig getDefaultConfig()
        {
            EnvironmentConfig ecfg = getNormalConfig();
            ecfg.setCacheSize(CACHE_SIZE);
            ecfg.setTransactional(true);
            ecfg.setTxnNoSync(true);
            return ecfg;
        }

        static
        {
            PRIMES.add(2);
            PRIMES.add(3);
            PRIMES.add(5);
            PRIMES.add(7);
            PRIMES.add(11);
            PRIMES.add(13);
            PRIMES.add(17);
            PRIMES.add(19);
            PRIMES.add(23);
            PRIMES.add(29);
            PRIMES.add(31);
            PRIMES.add(37);
            PRIMES.add(41);
            PRIMES.add(43);
            PRIMES.add(47);
            PRIMES.add(53);
            PRIMES.add(59);
            PRIMES.add(61);
            PRIMES.add(67);
            PRIMES.add(71);
            PRIMES.add(73);
            PRIMES.add(79);
            PRIMES.add(83);
            PRIMES.add(89);
            PRIMES.add(97);
            PRIMES.add(101);
            PRIMES.add(103);
            PRIMES.add(107);
            PRIMES.add(109);
            PRIMES.add(113);
            PRIMES.add(127);
            PRIMES.add(131);
            PRIMES.add(137);
            PRIMES.add(139);
            PRIMES.add(149);
            PRIMES.add(151);
            PRIMES.add(157);
            PRIMES.add(163);
            PRIMES.add(167);
            PRIMES.add(173);
            PRIMES.add(179);
            PRIMES.add(181);
            PRIMES.add(191);
            PRIMES.add(193);
            PRIMES.add(197);
            PRIMES.add(199);
            PRIMES.add(229);
            PRIMES.add(241);
            PRIMES.add(241);
            PRIMES.add(241);
            PRIMES.add(271);
            PRIMES.add(283);
            PRIMES.add(283);
            PRIMES.add(313);
            PRIMES.add(313);
            PRIMES.add(313);
            PRIMES.add(349);
            PRIMES.add(349);
            PRIMES.add(349);
            PRIMES.add(349);
            PRIMES.add(421);
            PRIMES.add(433);
            PRIMES.add(463);
            PRIMES.add(463);
            PRIMES.add(463);
            PRIMES.add(523);
            PRIMES.trim();
        }
    }
}

Thursday, July 12, 2007

Becoming a concurrency expert. Rule number not one, Relax.

The holy grail for a concurrency expert is wait-free, and if you can't achieve that, lock-free code because generally they are more scalable than algorithms that use locks. The previous link and this one describes some of the benefits of non-blocking synchronization but I'll give you a contrived example.

LF/WF algorithms can't deadlock! No locks, no deadlocks. Deadlocks are a bane to scalability because in the wild they are probabilistic events whose probability increases as concurrency increases. So you can imagine a situation where you have some code that runs smoothly on your old single processor system, then you upgrade to a dual core system and suddenly the code starts freezing every once in a while. You think to yourself, "gremlins" and continue along your merry way. Christmas comes early and you win a shiny new quad core box from some tech event in Atlanta. So you are thinking, "4x the processing power, 4x the performance, yeah!" But instead your program is freezing all the time. Simply killing it and restarting is not good enough anymore. You have a deadlock on your hands. You've gone from a single processor to a 4-way system and the scalability of the program has not followed suit. This is not atypical of a lot of Java programs in the wild, because though it may come as a shock to you (it sure as hell shocked me!), most Java programmers don't know spit about concurrency, even though Java has concurrency baked in from birth! And now that multi-core systems are becoming the norm, concurrency bugs that have laid dormant for years are waking up and stinging users.

It's possible, with a great deal of effort, to eliminate deadlocks in non LF/WF code. So (a) given that writing LF/WF code is as hard or harder than writing deadlock free code and (b) you are willing to solve any deadlock problems in the non LF/WF code, is LF/WF worth it? As with all questions relating to trade-offs, the answer is "it depends". In this case, it depends on how much throughput is enough. In the WF case you are guaranteed system-wide throughput with starvation freedom while in the LF case you are still guaranteed system-wide throughput but with the possibility that individual threads may starve. The bottom line is, progress is always being made. There is no such guarantee with blocking synchronization.

Because of the complexity associated with LF/WF algorithms most programmers never tackle LF/WF head on. Contact with LF/WF algorithms and code come in the form of using LF/WF datastructures (i.e. java.util.concurrent.ConcurrentLinkedQueue). But it may surprise you that in your own code there may be opportunities to write LF/WF code.

Disclaimer:

I'm not advocating everybody going through every line of code and trying to make it LF/WF (though I do advocate going through every line of your code and making sure it's thread safe). You really, really, need to have an extremely strong grasp of the Java Memory Model before you can even begin to think about writing LF/WF code, especially the happens-before rules.

You should limit your LF/WF tinkering to critical paths only. Critical paths are hot sections of code (code that is executed frequently). You need two tools to fix critical paths. Firstly, you need a Java profiler to tell you where the critical path is. The critical path is going to be the method call chain where the program spends the majority of it's time while under load. The under load distinction is extremely important because if you take a server application as an example, and profile it when there is no load, the profiler is going to report that the app is spending most of its time in [something like] Socket.accept(), which doesn't tell you anything about the performance of the app. In your quest for the critical path the best any Java profiler can do is tell you what methods are consuming the most amount of time. They cannot peer into the method and tell you which specific line of code is slow or if a lock is hot (a hot lock is one that is highly contended). This is where the second tool comes into play. You need a hardware profiler.

A hardware profiler differs from a Java profiler in that it show events at the CPU. Every modern CPU comes with all sorts of counters that enables programs to know what's going on inside the CPU. It can tell you things like cache hit/miss rates, stalls, lock acquisitions and releases, etc. Some operating systems comes with hardware profilers baked right in. Solaris 10/OpenSolaris on Sparc is the gold standard when it comes to observability. mpstat, corestat, plockstat, and [the big daddy of them all] DTrace are some of the tools baked into Solaris 10/OpenSolaris that allow you to dig deep into the bowels of the system to figure out exactly what's going on. If you aren't running Solaris but Linux or Windows on AMD you can use AMD's CodeAnalyst Performance Analyzer. Finally, if you are running Linux or Solaris (Intel or AMD) you can use Sun Studio 12 to get at the data. All the hardware profiler tools I've mentioned are free and/or open source. So you have no excuse not to have at least one installed.

So here are the steps you've completed so far:

  1. Profiled the app under load.
  2. Identified the critical methods (hotspots).
  3. Tuned the critical method(s) as best you can.
  4. Repeat steps 1-3 until you hit the point of diminishing returns.
At this point if the throughput is where you want it you can stop. You didn't have to write a lick of LF/WF code. Good for you. But what happens if you want to see how far you can really push the system? You start really cranking up the amount of threads (assuming you have available processors to execute them concurrently). You take at the look at the system's CPU utilization and it's redlining. You need to make sure it's actually doing useful work. So now it's time to fire up the hardware profiler to see what's going on in the CPU.

Aside:

Sun Studio 12 does the best job, of the tools listed, of associating CPU events back to the source code line(s) that produced them.

So you fire it up and the first thing that jumps out at you is you have a smokin' hot lock along your critical path. If you can't reduce the granularity of the lock or its scope, going LF/WF may be your only option.

Aside:

It's entirely possible that you can neither reduce the granularity of the lock nor rewrite the critical section as LF/WF. At this point you are screwed. You are just going to have to buy another box (or virtualize) and spread the load. If you can't spread the load you are royally screwed so the best thing to do is degrade gracefully.

I've gone through all of that setup just so I dump some code on you:

public class RecipientLimits
{
    private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1);

    private static final long THREE_DAYS_MILLIS = TimeUnit.DAYS.toMillis(3);

    private static final long ONE_MONTH_MILLIS = TimeUnit.DAYS.toMillis(31);

    private final ConcurrentMap hosts = new ConcurrentHashMap();

    /**
     * Tracks the last time we pruned the table.
     */
    private final AtomicLong lastrun = new AtomicLong(System.currentTimeMillis());

    /**
     * Get the recipient limit for host.
     *
     * @param host The host.
     *
     * @return The maximum number of recipients the host will accept.
     */
    public int get(InetAddress host)
    {
        Entry e = hosts.get(host);
        if (null == e)
        {
            Entry t = hosts.putIfAbsent(host, e = get(host.getAddress()));
            if (null != t)
                e = t;
        }
        return e.limit();
    }

    /**
     * Confirms that host accepts limit recipients.
     *
     * @param host  The host.
     * @param limit The number of recipients that was accepted.
     */
    public void confirmed(InetAddress host, int limit)
    {
        Entry e = hosts.get(host);
        if (null != e)
            e.confirm(limit);
        prune();
    }

    /**
     * Indicates that host did not accept limit number of recipients.
     *
     * @param host  The host.
     * @param limit The limit.
     */
    public void denied(InetAddress host, int limit)
    {
        Entry e = hosts.get(host);
        if (null != e)
            e.decrement(limit);
        prune();
    }

    /**
     * Removes inactive hosts from the table.
     */
    private void prune()
    {
        long last = lastrun.get();
        long millis = System.currentTimeMillis();
        if (millis - last >= ONE_HOUR_MILLIS && lastrun.compareAndSet(last, millis))
        {
            for (Iterator i = hosts.values().iterator(); i.hasNext();)
            {
                Entry e = i.next();
                if (0 == e.users.get() && millis - e.lastaccessed.get() >= THREE_DAYS_MILLIS)
                    i.remove();
            }
        }
    }

    /**
     * Look up host in the database.
     *
     * @param host The ip address of the host.
     *
     * @return A new Entry.
     */
    private Entry get(byte[] host)
    {
        //@todo don't forget to create an entry in the database if host does not already exist.
        return new Entry(1);
    }

    final class Entry
    {
        /**
         * The last time (milliseconds timestamp) this enty was accessed.
         */
        final AtomicLong lastaccessed;

        /**
         * The number of threads reading/writing this object.
         */
        final AtomicInteger users = new AtomicInteger();

        /**
         * Semaphore for updates.
         */
        private final AtomicInteger dflag = new AtomicInteger();

        /**
         * The recipient limit.
         */
        private final AtomicInteger limit;

        /**
         * Indicates when we've maxed out {@link #limit}.
         */
        private volatile boolean maxo;

        /**
         * The millisecond timestamp when we maxed out date.
         * 

* This field is correctly synchronized because there is a happens-before edge created by the write to this * followed by a write to {@link #maxo} in {@link #decrement(int)} and then the read of {@link #maxo} followed by the read * of this in {@link #confirm(int)}. *

*/ private long maxtstamp; /** * Constructs a new Entry. * * @param limit The recipient limit. */ Entry(int limit) { this.limit = new AtomicInteger(limit); lastaccessed = new AtomicLong(System.currentTimeMillis()); } /** * @return The current limit. */ int limit() { users.incrementAndGet(); try { return limit.get(); } finally { lastaccessed.compareAndSet(lastaccessed.get(), System.currentTimeMillis()); users.decrementAndGet(); } } /** * Confirm the limit. * * @param limit The value returned by {@link #limit()}. */ void confirm(int limit) { users.incrementAndGet(); try { if (0 == dflag.get() && (!maxo || System.currentTimeMillis() - maxtstamp >= ONE_MONTH_MILLIS) && this.limit.compareAndSet(limit, limit + 1)) { //@todo - talk to database //@todo - use limit not limit + 1 } } finally { lastaccessed.compareAndSet(lastaccessed.get(), System.currentTimeMillis()); users.decrementAndGet(); } } /** * Decrement the limit. * * @param x The value returned by {@link #limit()}. */ void decrement(int x) { users.incrementAndGet(); try { if (x > 1 && dflag.compareAndSet(0, x)) { try { if (limit.compareAndSet(x, x = x - 1)) { boolean dbput = true; if (dbput) { maxtstamp = System.currentTimeMillis(); maxo = true; } } } finally { dflag.set(0); } } } finally { lastaccessed.compareAndSet(lastaccessed.get(), System.currentTimeMillis()); users.decrementAndGet(); } } } }

Let's revisit the title of this post "Becoming a concurrency expert. Rule number not one, Relax". I've emphasized relax because it is critically important to finding LF/WF opportunities in your own code. So what exactly does relaxing mean? The biggest thing it entails is realizing that you have very little control over the order in which threads execute and being OK with that. Because if you try to be draconian about the order in which things happen you will have to go single threaded or use locks. So once you've relaxed and let go of draconian ordering the only thing you have to worry about is ensuring that data races are benign. Notice I didn't say eliminate data races, I said "ensuring that data races are benign". The difference of course is relaxation. In LF/WF, data races are part of the design because at some point in the code you are going to need to do a CAS (i.e. java.util.concurrent.atomic.AtomicBoolean.compareAndSet(false,true), java.util.concurrent.atomic.AtomicInteger.incrementeAndGet(), etc) and a CAS is a [CPU supported] race condition waiting to happen. CAS isn't the only place where its okay to let a race condition go unchallenged. Anywhere you can prove that the consequence of a data race is benign is an opportunity to relax.

Let me back up quickly and talk briefly about ordering. I hope you don't think I said that ordering is not important or that you have absolutely no control because that is not true. Let me repeat it. What you don't have control of is when a thread will run. That's [ultimately] the responsibility of the operating system. What that translates into is, you don't have control of when something executes, only what executes.

The new JMM strengthened the guarantees of volatile to prevent the reordering of volatile read/writes in relationship to non volatile fields. In other words, you can use volatile fields to force code to execute in a certain order without the use of the synchronized keyword. Which also means you can use a single volatile field to safely publish multiple non volatile fields (an example of this is located in the code above and is described in the JavaDoc comment for maxtstamp). This was not possible prior to Java 5. Before Java 5 if you wanted visibility guarantees for fields you had to (a) declare all of them volatile or (b) use a synchronized block.

So there is a lot you can accomplish given the new volatile semantics but you can't do everything. Specifically, you can't make binding decisions with volatiles. So what's a binding decision?

if (some_volatile_condition IS True)
{
  //Execute code under the assumption that some_volatile_condition is still True.
}
... is a binding decision and in the absence of locking [before the read of some_volatile_condition] is a bug, except of course, in the case that the code being executed results in a benign race condition. Examples of non-binding decisions can be found in Entry.confirm(int) and Entry.decrement(int).

That's it for today. I'll pick the code apart in Part II. Thanx for stopping bye.

Saturday, November 18, 2006

Getting IDEAs Flowing

This is a story about starting IDEA 6 on my Gentoo GNU/Linux machine. IDEA 6 has been out a while now, but I'm not the kind of person to buy the first release of a new version of software. I like to wait until the more adventurous types take it for a spin and shake the first few waves of bugs out. So now that it's at maintenace release 2 I'm taking it out for a spin.

Getting it running was a bitch. With the default settings I could get as far as the "loading project" dialog box. It just hung there, forever, which was long enough for me to notice that they've fixed one of (a few) annoyances I have with version 5.

Aside:

Whenever IDEA is running a command that is taking a while, the "tip of the day" dialog box attaches itself to the progress meter. In 5 the dialog box is vertically too small to read the text inside it. I always have to pull the edge of the box down to see the tip. After the first few times I gave up on the "tip of the day" all together. After I realized I can actually see what's in the box I started flipping through them while I waited for IDEA to complete startup. Very insightful stuff. It turns out there are a plethora of shortcuts that I didn't know about. Which sucks. Think off all the time I could have saved w/ the new shortcuts instead of mousing around the editor.

At first I thought it was just busy importing and converting the settings and configuration from the old install. So I fired up Spider Solitaire in XP/VMWare and started listening to episode 236 of This American Life. That's how I normally pass the time when I need to wait for a computer to do something and I don't want to change my mindset to a different computer related task.

After fifteen minutes it still wasn't done and trying to close the window was getting me nowhere. So I murdered it with a 9 caliber signal. Fortunately I had started it from a shell so I could see the STDERR messages. The JVM was throwing OOM errors related to a too small perm gen size. A quick cat of {INSTALL_DIR}/bin/idea.vmoptions revealed a mandatory limit of 99MB. An easy fix. Just remove the offending line because the JVM --Sun JVM 1.5_08 Linux, to be specific-- defaults to ~130MB. Fired it up again and got to the main menu. Yippee!! Things are looking up.

Wrong!

I tried opening the last project I was working on and the project loads but the editor refuses to accept input. Then the window loses focus and one of the CPUs hit 100%. top tells me IDEA is the offending process. I figured I would give it more time. Maybe it will settle down after a while plus I've got This American Life on pause and episode 236 is quite good. So I unpause and started another round of Spider Solitaire. A few minutes go by and the JVM starts complaining the perm gen size is still too small. No problem. I run a server class machine w/ 12GB of RAM of which 4GB is in use so I've got plenty to spare. So I run echo "-XX:MaxPermSize=256m" >> {INSTALL_DIR}/bin/idea.vmoptions and rerun the start script. It gets to the main menu in a reasonable amount of time. So far so good. I reopen the project and cross my fingers. Booyah! 256MB is the magic number. Hooray!!

Saturday, September 30, 2006

Java Keeps Keeping On

The real motives why industry analysts love to predict Java fall... and why they will spend another 10+ years being proven wrong.

A very good entry discussing some of the reasons why Java continues to defy the doomsday predictions of so-called "industry experts".

Java not being the new kid on the block is not news it's an achievement that every professional (state of mind not state of employment) Java programmer can be proud of.

Wednesday, September 13, 2006

Concurrent Javadoc

Writing correct concurrent code is hard and what I'm finding equally challenging is documenting concurrent behavior in code in prose (javadoc to be specific).

Pre Java 1.5 all we had where java.lang.Thread, the Runnable interface, the synchronized keyword, and volatile variables. Because synchronized was the only real mechanism for concurrency control if you wanted to reason about the thread safety of a class all you had to do was look for the synchronized keyword. Some of you may be asking "but what about volatile?" volatile then was nowhere near as powerful as it is now. Back then the only thing volatile really meant was do not cache. Nowadays volatile not only means do not cache it also affects the visibility of other non volatile variables and it also imposes ordering constraints. Basically synchronized and volatile no longer act in isolation. All activities inside the JVM is now governed by a well defined memory model (JMM). It's no longer enough to look for the synchronized keyword you have to keep the memory model in mind at all times which is damn hard to document. Therefore I'm seeking feedback.

Imagine you have been asked to maintain a body of code with which you have had no previous experience and the only thing you know is it's targeted at Java 1.5 and higher (new JMM). You come across this (partial) class definition:

1    /**
2     * Ties {@link BlastMaster}, {@link BlastManager}, and {@link BlastAgent} objects together around a blast. It is the
3     * highest layer of abstraction available on a blast. It provides the hooks for the lifecycle management of a blast and
4     * is the communication channel between BlastAgent(s) and BlastManager(s) and BlastMaster.
5     * <p>
6     * The lifecycle of a context goes something like this. It is created by BlastMaster, injected into the outbound queue
7     * by BlastMaster, scheduled by BlastManager, delivered by BlastAgent(s), descheduled by BlastManager, rescheduled by
8     * BlastManager, delivered by BlastAgent(s) ....
9     *
10    * The scheduling, delivering, and descheduling steps repeat until a) all the recipients have been sent the blast or b)
11    * the duration of the blast has expired.
12    * </p>
13    *
14    * @author HashiDiKo
15    */
16   public final class BlastContext extends Constants
17   {
18       /**
19        * Keeps track of the number of domains yet to be delivered. When this number reaches zero it means the current
20        * iteration of trying to deliver the blast has ended.
21        *
22        * @see BlastAgent#decrementRemaining()
23        * @see #fire(ContextEvent.Event)
24        */
25       final AtomicInteger remaining = new AtomicInteger();
26   
27       /**
28        * Provides a mechanism for a {@link BlastManager} to be notified when the {@link BlastAgent}s executing the context
29        * stop executing it.
30        * <p> This object does a great deal in terms of establishing <em>happens-before</em> edges between a blast manager
31        * and its agents and between blast agents.</p>
32        */
33       final Set<Thread> agents = Collections.synchronizedSet( new HashSet<Thread>( MAX_BLAST_AGENTS + 1) );
34   
35       /**
36        * The message.
37        *
38        * <h4>Visibility</h4>
39        *
40        * <p>The blast manager thread is the only thread that writes this field while blast agents are the only threads
41        * to read this field. This field is <i>correctly synchronized</i> because the blast master writing this field
42        * <b>always</b> <i>happens-before</i> the blast agents reading it. This <i>happens-before</i> relationship is
43        * established in the code starting with {@link #init()} and its call to {@link #doInit()}. {@link #doInit()} is
44        * the only place in the code where this field is written. You simply need to follow the flows in
45        * {@link BlastManager} starting from where {@link #init()} is called to see synchronization the points that
46        * establishes the <i>happens-before</i> relationship between the blast manager and its blast agents in regard to
47        * this field.
48        * </p>
49        */
50       Message message;
51   
52       /**
53        * Indicates that this context is context to an old blast. In other words the system was restarted and the blast
54        * was loaded from the database.
55        *
56        * @see #init()
57        * @see #BlastContext(boolean, EmailBlast, BlastMaster)
58        */
59       private final boolean old;
60   
61       /**
62        * The home directory of the blast.
63        */
64       private final File home;
65   
66       /**
67        * The duration of the blast in milliseconds.
68        */
69       private final long durationMillis;
70   
71       /**
72        * Used to implement our backoff algorithm.
73        *
74        * <h4>Visibility</h4>
75        *
76        * <p>The field is only ever read/written by blast agents thus the visibility of this field is limited to
77        * blast agents in general and specifically the {@link #fire(ContextEvent.Event) fire} method. Even though
78        * {@link #fire(ContextEvent.Event) fire } is not <tt>synchronized</tt> and this field is non volatile it is
79        * <em>correctly synchronized</em>. There are 2 things at work that make this so. Firstly, there is never
80        * concurrent access to this field. At most 1 blast agent thread will write/read this field for a delivery cycle.
81        * This is guranteed by {@link BlastAgent#decrementRemaining()} because a) how its implemented and b)it is the only
82        * place where a blast agent get accesses this field. Finally, the fact that blast agents call
83        * {@link #register(Thread)} before they execute a blast and {@link #unregister(Thread)} when they are done
84        * executing a blast (both contained <tt>synchronized</tt> blocks using the same monitor) gurantees that the update
85        * of this field will be visible to any subsequent reads by different blast agents.
86        * </p>
87        * @see #fire(ContextEvent.Event)
88        */
89       private int magnitude;
90   
91       /**
92        * Used to implement our backoff algorithm.
93        *
94        * <h4>Visibility</h4>
95        *
96        * <p>The field is only ever read/written by blast agents thus the visibility of this field is limited to
97        * blast agents in general and specifically the {@link #fire(ContextEvent.Event) fire} method. Even though
98        * {@link #fire(ContextEvent.Event) fire } is not <tt>synchronized</tt> and this field is non volatile it is
99        * <em>correctly synchronized</em>. There are 2 things at work that make this so. Firstly, there is never
100       * concurrent access to this field. At most 1 blast agent thread will write/read this field for a delivery cycle.
101       * This is guranteed by {@link BlastAgent#decrementRemaining()} because a) how its implemented and b)it is the only
102       * place where a blast agent get accesses this field. Finally, the fact that blast agents call
103       * {@link #register(Thread)} before they execute a blast and {@link #unregister(Thread)} when they are done
104       * executing a blast (both contained <tt>synchronized</tt> blocks using the same monitor) gurantees that the update
105       * of this field will be visible to any subsequent reads by different blast agents.
106       * </p>
107       * @see #fire(ContextEvent.Event)
108       */
109      private boolean hrs;
110  
111      /**
112       * Stores the backoff timeout (the end result of the backoff algorithm computation).
113       *
114       * <h4>Visibility</h4>
115       *
116       * <p>The visibility requirements for this field is kind of funky. It's is a read/write field for
117       * {@link BlastManager}s and write only field for {@link BlastAgent}s. So the core visibilty issue is writes by
118       * blast agents being visible to blast manager. The <em>happens-before</em> edge that makes this possible is
119       * established by the monitor acquisition of {@link #agents} by the blast agent thread that writes this field and
120       * the subsequent acquisition of the same monitor by the blast manager.
121       * </p>
122       *
123       * @see #fire(ContextEvent.Event)
124       */
125      private long backoffTimeout;
126  
127      /**
128       * Indicates if {@link #doInit()} has been called.
129       * This field is only ever read/written by the blast manager thread for this context thus it is correctly
130       * synchronized.
131       */
132      private boolean didInit;
133  
134      /**
135       * Temporarily store the short domain blacklist expiration.
136       * <p>
137       * When this value is greater than zero it indicates to the blast manager thread that even though {@link #domainQ}
138       * is empty this context has undelivered recipients.
139       * </p>
140       * <p>This field is <em>correctly synchronized</em> because it is only ever read/written by the blast manager
141       * thread.</p>
142       *
143       * @see #getDomains()
144       */
145      private long shortestDomainTimeout;
146  
147      /**
148       * Gate to {@link #manager}. It establishes a <em>happens-before</em> edge with {@link #manager}.
149       */
150      private final CountDownLatch managerLatch = new CountDownLatch( 1 );
151  }
152  

So. Does the javadoc comments say enough about how non volatile class members manage to be correctly synchronized?

Sunday, September 03, 2006

Latches in Java

David Holmes provides an excellent description of the difference between java.util.concurrent.CountDownLatch and java.util.concurrent.CyclicBarrier on the concurrency interest mailing list. I have posted it here for those of you who don't subscribe to the list and may not have a good grasp of these classes. And since I use CountDownLatch in code I've posted in the past and in code I'm planning on posting I'm happy to have such a clear explanation that I can link to in future entries.

... As Brian describes one notion of state "latching" is that it progresses to a terminal value from whence it no longer changes. It is permanently "latched". A synchronization object that behaves in this way is the CountDownLatch - once "open" it never closes and can't be reset.

More generally though latches can be reset - consider a digital flip-flop such as a "gated D-latch", the flip-flop latches the value of the data when the gate is pulsed/strobed; if you change the data without changing the gate then the latched value is unchanged, but change the gate and the latched value is updated.

In synchronization object terms a latch is sometimes called a "gate" - the connotation being that if the gate is open anyone/everyone can pass through; while if it is shut no one can pass through. The CountDownLatch operates this way, but a more general "gate" is the CyclicBarrier which can also be reset (and automatically does so). Of course the semantics of CountDownLatch and CyclicBarrier are somewhat different. CyclicBarrier is what can be called a "weighted gate" or "weighted bucket" - it is set up to expect N threads to arrive, when they arrive they have sufficient "weight" to open the gate, or tip the bucket - in this case the gate/bucket is spring-loaded and closes/rights-itself as soon as the threads leave, so it is ready for the next set of threads to use. CountDownLatch on the other hand is like a gate with multiple padlocks - when the last padlock is removed, the gate opens and it stays open. Aren't these analogies quaint :-) We could have defined CountDownLatch to allow reset but reset semantics are messy and usually not needed for CDL usage, in contrast barrier designs typically always use the barrier multiple times.

It seems the database folk are using the term "latch" for lightweight lock, which is an uncommon usage from a synchronization perspective and a poor choice in my view, though arguably there is an analogy between "locking a door/window" and just "latching it shut". In that sense "latching" is a weaker form of "locking". But I don't like the usage.

Saturday, June 03, 2006

"Look Ma, No Locks!"

Brian Goetz has written an excellent introductory article on nonblocking algorithms and showcases some simple nonblocking data structures with code examples and pictures.

The core concept you should take away from the article is the importance of CAS. It sits at the root of all nonblocking data structures in the Java platform.

Saturday, April 08, 2006

regex vs. XML

I needed to screen-scrape wikipedia for the list of Top Level Domains for my email app so I could build an index of suffixes that would reduce the number of martian addresses the app would have to process. So I surfed over to the TLD page to have a look-see. Low and behold those beautiful wikipedians were nice enough to produce well formed XML (actually, XHTML 1.0 Transitional) content. My heart leaped w/ joy because no screen-scraping was needed. With a little bit of XPath I would have my index in no time.

WRONG!!!!!!!!!!!

I spent the next hour+ trying to get XOM to work and spit out my list via XPath because I sure as hell wasn't going to try to manually walk the tree extracting the TLDs. Do you have any idea how much code that would take? I had a feeling the reason it doesn't work has something to do w/ how XOM handles namespaces because when I access the tree the hard way it demands that I specify the namespace URI of the document to lookup any element in the tree even though the document declares one and only one namespace. Now this isn't a knock against XOM, though I'm sure if I had used dom4j it would have worked because dom4j is more concerned with being useful than being correct. But each have there place. By this time I was so aggravated I just didn't feel like downloading dom4j and unpacking it and setting my classpath, yada, yada, yada.

Regex to the rescue

This his how I got what I needed the old fashion screen-scraping way. 5 minutes tops from concept to results:
String tld = "http://en.wikipedia.org/wiki/List_of_Internet_top-level_domains"
Matcher regex = 
    Pattern.compile( "<td><a[^>]++>\\.(\\w++)</a></td>" ).matcher( "" );
BufferedReader reader =
    new BufferedReader(
        new InputStreamReader(
            new BufferedInputStream( new URL( tld ).openStream() ), "utf-8" ) );
PrintWriter writer = 
    new PrintWriter( 
        new BufferedWriter( 
            new FileWriter( new File( "/home/HashiDiKo/temp/TLD" ) ) ) );

for( String line; null != (line = reader.readLine()); )
    if( regex.reset( line ).matches() )
        writer.println( regex.group( 1 ) );

reader.close();
writer.close();

Sunday, February 26, 2006

Annoyances: java.sun.com

Why are the people that run the java.sun.com site such idiots? Try to get to the JavaMail part of the J2EE stack from java.sun.com w/o using the search dialog box. Go ahead, try it! I'll wait .....

Can't be done huh? WTF you say? It's because they are idiots! I used to be able to get to any named Java technology 2 clicks from the home page but they changed it and change is good. Bullshit!!

As a company Sun is infamous for small annoyances this is one of them. And because I spend (or will be spending) so much time in Java (Sun) land the annoyances are beginning to add up. Even their own employees complain. Argh!!!!!!!!!!!

It's like there are two competing groups of people working at Sun. Some really brilliant, thoughtful, articulate people and a bunch of morons. The morons are winning! If you think I'm kidding take a look at some of the J2SE and J2EE source code. There are some parts of it where I get the feeling, these people really know what they are doing. Then there are other parts where I have to go, WTF?!!! The patients have definitely taken over the asylum.

Tuesday, February 14, 2006

Matching a Backslash in Java

I've done it a million times in the past but for whatever reason I keep screwing up how to properly write a backslash (\) in a regular expression. I wasted 20+ minutes yesterday trying to figure out why the character class in this
[\";:()\\[\\]<>@.\\,]
regular expression wouldn't match strings containing commas. I'm blogging the solution so I'll have a permanent record and hopefully, I'll once and for all stop screwing it up!

Solution:

[\";:()\\[\\]<>@.\\\\,]