Thursday, October 21, 2010

Extracting HTTP headers, handling partial headers correctly

Here is the correct code for the previous post:

    //extract all HTTP request response headers locally and return the size of the headers
    private int extractHttpHeaders(byte[] httpHeader) {
        final String delimiter = ": ";
        List<StringBuilder> headers = new ArrayList<StringBuilder>();
        for (int i=0; i<httpHeader.length; i++) {
            StringBuilder line = new StringBuilder();
            for (; i<httpHeader.length && (char)httpHeader[i]!='\n'; i++) {
                line.append((char)httpHeader[i]);
            }
            if (i==httpHeader.length) {
                //partial header, full headers have not been received yet
                //this will break out of the loop
            }
            else if (line.length()==0 || (line.length()==1 && (char)httpHeader[i-1]=='\r')) {
                //all headers received
                httpHeaders.length = i+1;
                return i+1;
            } else {
                //line has a header, add it
                int colonAt = line.indexOf(delimiter);
                if (colonAt != -1) {
                    String value = line.substring(colonAt+delimiter.length(), line.charAt(line.length()-1)=='\r' ? line.length()-1 : line.length()).trim();
                    if (value.length() > 0)
                        httpHeaders.put(line.substring(0,colonAt), value);
                }
            }
        }
        //partial header, full headers have not been received yet
        httpHeaders.clear();
        return -1; //full headers have not been received yet
    }

The only difference is unconditionally clearing the headers map if we don't see a blank line in the headers. The correct termination of the headers is a single CRLF pair (blank line). If that has not been seen, then it is a partial header, even though each line in the partial header may have been fully received.

Extracting HTTP headers, handling partial headers (incorrectly)

This one is for the network fiends who are crazy enough to read raw HTTP packets and construct HTML content. I had to do this to take advantage of the highly efficient epoll() mechanism in Linux. The full framework is described here.

The problem is that, each time I get some data from a server, I need to anticipate partial data. It is possible that the client receives a partial HTTP header. If this is the case, the logic is written to discard building the header data so that this can be attempted again, hopefully after we have full headers delivered.

I want to illustrate this version that has a subtle bug:

    //extract all HTTP request response headers locally and return the size of the headers
    private int extractHttpHeaders(byte[] httpHeader) {
        final String delimiter = ": ";
        List<StringBuilder> headers = new ArrayList<StringBuilder>();
        for (int i=0; i<httpHeader.length; i++) {
            StringBuilder line = new StringBuilder();
            for (; i<httpHeader.length && (char)httpHeader[i]!='\n'; i++) {
                line.append((char)httpHeader[i]);
            }
            if (i==httpHeader.length) {
                //partial header, full headers have not been received yet
                httpHeaders.clear();
            }
            else if (line.length()==0 || (line.length()==1 && (char)httpHeader[i-1]=='\r')) {
                //all headers received
                httpHeaders.length = i+1;
                return i+1;
            } else {
                //line has a header, add it
                int colonAt = line.indexOf(delimiter);
                if (colonAt != -1) {
                    String value = line.substring(colonAt+delimiter.length(), line.charAt(line.length()-1)=='\r' ? line.length()-1 : line.length()).trim();
                    if (value.length() > 0)
                        httpHeaders.put(line.substring(0,colonAt), value);
                }
            }
        }
        return -1; //full headers have not been received yet
    }


It uses the Headers class - the httpHeaders you see there is an object of that type. Here is the code for the Headers class:

    class Headers {
        private Map<String, String> httpHeaders = new HashMap<String, String>();
        private int length = -1;
        private int getContentLength() {
            String s = httpHeaders.get("content-length");
            try {
            return s != null ? Integer.valueOf(s) : Integer.MAX_VALUE;
            } catch (NumberFormatException e) {
                return Integer.MAX_VALUE;
            }
        }
        private void clear() {
            httpHeaders.clear();
        }
        private void put(String k, String v) {
            httpHeaders.put(k.toLowerCase(),v);
        }
        private String get(String k) {
            return httpHeaders.get(k.toLowerCase());
        }
        private int numHeaders() {
            return httpHeaders.size();
        }
        private boolean isBinary() {
            return Utils.isContentTypeBinary(httpHeaders.get("content-type"));
        }
    };

Before I point out the bug, here is how this is supposed to work. I have an asynchronous network probing loop that fills byte arrays with data from the servers. Whenever I have new data in these byte arrays, I call into the extractHttpHeaders() call. But that code has to guard against working with partial headers. If there is partial headers, I abandon parsing, clear the headers data structure I was building and return.

In the main probe code, I would use Headers.numHeaders() to determine if all headers have been received. Since I clear the headers on partial headers, this is an acceptable method.

Except, there is a subtle bug that sometimes, I can receive a partial header but return without clearing the headers map. Then, next time the probe code gets some data, it will incorrectly assume that full headers had already been recived (as Headers.numHeaders() will return a +ve number)

To appreciate the bug, imagine I receive headers on an exact line boundary. For example assume I receive this:

Date: Thu, 21 Oct 2010 18:39:20 GMT
Server: Apache/2.2.15 (Fedora)
X-Powered-By: PHP/5.2.13

Then, I would not hit the condition of getting a partial line of the header, as all lines of the header are fully returned in this case, along with the terminating CRLF.

This is the condition that clears the headers on partial lines:

            if (i==httpHeader.length) {
                //partial header, full headers have not been received yet
                httpHeaders.clear();
            }

But that won't be hit in this case, as we have full lines. In this case, the code would exit the for loop without clearing the headers map.

A simple change in the first code block is all that is required to fix this. I will have that on the next post.

Monday, October 18, 2010

Mysql - LOAD DATA INFILE could lose data

Last week, when we encountered a sudden data loss in mysql, the first thing to do was to pour through the bin logs looking for "DELETE" statements. It took quite a while to figure out that the data "loss" was more of a change in Ids in one table, and that had to do with a LOAD DATA INFILE statement.

To illustrate assume we have these two tables:


vegetables (
 
  id bigint(10) NOT NULL AUTO_INCREMENT,
 
  name varchar(255) NOT NULL,
 
  UNIQUE KEY uk_name (name)
);

sales (
 
  id bigint(10) NOT NULL AUTO_INCREMENT,
 
  saledate datetime,
 
  vegetableId  bigint(10) NOT NULL
);


The sales table has information about vegetable sales, each sales record has a connection to a vegetable record.

If we load data into the vegetables table using a LOAD csv of this sort, it is possible to break the existing foreign key relationships from the sales records to the vegetables records:


load data infile '/path/to/file.csv' replace into table vegetables;


The problem is the keyword "REPLACE" used here. It tells mysql what to do if it finds that any record in the csv file violates a key constraint. In this example, if the csv file had a record with the vegetable name "brocolli", and brocolli was already in the vegetables table, then the keyword "REPLACE" tells mysql to replace the row on the table with the new data from the csv file.

This still seems to be not a problem as replacing "brocolli" with "brocolli" is certainly not earth shattering. But what about the id? This is an auto_increment field which means that mysql will over-write the old id with a new auto incremented one. And that means that any sales record that referenced brocolli is now left with a pointer to nowhere. In other words, as far as practical matters go, all sales of brocolli have now disappeared.

This problem would not have happened if the sales table explicitly specified the FOREIGN KEY constraint on its vegetableId field.

In any case, is this data loss so terrible? Once we see the error we made, could we not quickly correct it, after all it is just a change in the ids, and the old sales records are still there in the table. Well think about it, even though those records are there, what if we had replacements for more than one vegetable? Let's assume both "brocolli" and "arugala" were replaced. How can we determine which vegetableIds were for brocolli and which were for arugala? The ids in the vegetable table have been replaced so it is impossible to make the connection to here from the sales table. No, this is a bad one.

In fact, an incident similar to this happened where I work. We had to restore the db to get this data back, it was not a pleasant experience.

Monday, October 11, 2010

Java URLConnection provides no fail-safe timeout on reads

While it does sport a setReadTimeout() call, it does not work if the server returns some data and then gets stuck. There are servers like this in the real world, for example try this:

curl http://www.geo-mobile.com/nuke/index.php

You have to use a separate timer thread to timeout any reads.


                try {
                    URLConnection con = urlObj.openConnection();
                    con.setConnectTimeout(5000);
                    con.setReadTimeout(5000);
                    new Thread(new InterruptThread(Thread.currentThread(), con)).start();
                    retVal = new Source(con);
                } catch (IOException e) {
                    System.err.println("aborted parsing due to I/O: " + e.getMessage());
                    throw new IndexingException("aborted parsing due to timeout - " + aUrl);
                }

In this example, the Source constructor, using the Jericho parser retrieves a web page and it can get stuck on a socket read call. The timeouts don't have any effect. And it is not possible to interrupt a thread waiting on I/O using Thread.interrupt().

The solution is the hand-crafted InterruptThread class:


public class InterruptThread implements Runnable {
    Thread parent;
    URLConnection con;
    public InterruptThread(Thread parent, URLConnection con) {
        this.parent = parent;
        this.con = con;
    }

    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {

        }
        System.out.println("Timer thread forcing parent to quit connection");
        ((HttpURLConnection)con).disconnect();
        System.out.println("Timer thread closed connection held by parent, exiting");
    }
}

Here, the InterruptThread forcibly closes the URLConnection held by the parent thread, forcing the Source constructor of the parent thread to throw an IOException, which is handled by its catch block, thus avoiding the hang.

Tuesday, October 05, 2010

Java : Get difference between two sets

Use this generic method to get the difference between to Sets. The sets should have ordered iterators. I used TreeSet in the calling code.



    // returns the difference between two sets <set1>, <set2> as a pair
    // where pair.one is the set of new entries and pair.two is the set of deleted entries
    // between <set1> and <set2>
    public static <T extends Comparable<T>> Pair<Set<T>,Set<T>> diff(Set<T> set1, Set<T> set2) {
        Set<T> deleted = new TreeSet<T>();
        Set<T> added = new TreeSet<T>();

        Iterator<T> it1 = set1.iterator();
        Iterator<T> it2 = set2.iterator();

        int diff = 0;
        T obj1 = null;
        T obj2 = null;
        while (it1.hasNext() && it2.hasNext()) {
            if (diff == 0) {
                obj1 = it1.next();
                obj2 = it2.next();
            } else if (diff < 0) {
                deleted.add(obj1);
                obj1 = it1.next();
            } else {
                added.add(obj2);
                obj2 = it2.next();
            }

            diff = obj1.compareTo(obj2);
        }

        if (diff < 0) {
            deleted.add(obj1);
            boolean addFlag = true;
            while (it1.hasNext()) {
                obj1 = it1.next();
                diff = obj1.compareTo(obj2);
                if (diff != 0) {
                   deleted.add(obj1);
                } else {
                   addFlag = false; 
                }
            }
            if (addFlag) {
                added.add(obj2);
            }
        } else if (diff > 0){
            added.add(obj2);
            boolean addFlag = true;
            while (it2.hasNext()) {
                obj2 = it2.next();
                diff = obj1.compareTo(obj2);
                if (diff != 0) {
                    added.add(obj2);
                } else {
                    addFlag = false;
                }
            }
            if (addFlag) {
                deleted.add(obj1);
            }
        }
        Iterator<T> remIt = it1.hasNext() ? it1 : it2;
        Set<T> remSet = it1.hasNext() ?  deleted : added;

        T obj;
        while (remIt.hasNext()) {
            obj = remIt.next();
            remSet.add(obj);
        }
        return new Pair<Set<T>, Set<T>>(added, deleted);


    }

Comparing two Integers - don't expect auto-unboxing

If you wish to compare two Integers, explicitly compare their intValue(). Otherwise, what happens is an object comparison.

I was bitten by this as I was developing a test for a generic diff utility I had written. Here is the test code:


    public void test_diff_sets_rnd() {
        Random r = new Random();
        int n = r.nextInt(100);
        Set<Integer> set1 = new TreeSet<Integer>();
        Set<Integer> set2 = new TreeSet<Integer>();
        List<Integer> list = new ArrayList<Integer>();
        for (int i=0; i<n; i++) {
            int num = r.nextInt(100);
            set1.add(num);
            if (set2.add(num))
                list.add(num);
        }

        Set<Integer> removed = new TreeSet<Integer>();
        Set<Integer> added = new TreeSet<Integer>();

        n = set1.size()>0 ? r.nextInt(set1.size()) : 0;
        for (int i=0; i<n; i++) {
            int num = r.nextInt(set1.size());
            int remElt = list.get(num);
            if (set2.remove(remElt))
                removed.add(remElt);
        }

        n = r.nextInt(100);
        for (int i=0; i<n; i++) {
            int num = 100+r.nextInt(100);
            if (set2.add(num))
                added.add(num);
        }

        Pair<Set<Integer>, Set<Integer>> p = Utils.diff(set1,set2);

        System.out.println("set1:");
        for (Integer elt : set1) {
            System.out.print(elt + "\t");
        }
        System.out.println();

        System.out.println("set2:");
        for (Integer elt : set2) {
            System.out.print(elt + "\t");
        }
        System.out.println();

        System.out.println("added:");
        for (Integer elt : p.one) {
            System.out.print(elt + "\t");
        }
        System.out.println();

        System.out.println("removed:");
        for (Integer elt : p.two) {
            System.out.print(elt + "\t");
        }
        System.out.println();

        assertTrue(p.one.size()==added.size());
        assertTrue(p.two.size()==removed.size());

        for (Iterator<Integer> it1 = p.one.iterator(), it2 = added.iterator(); it1.hasNext();) {
            assertTrue(it1.next().intValue()==it2.next().intValue());
        }
        for (Iterator<Integer> it1 = p.two.iterator(), it2 = removed.iterator(); it1.hasNext();) {
            assertTrue(it1.next().intValue()==it2.next().intValue());
        }

    }

I had to use the intValue() explicitly in the last two for loops.

Tuesday, July 13, 2010

Why am I writing lower_bound() in Java again?

The second and the third time I find myself looking for a lower_bound() function according to generally accepted STL standard, I know that I'm not a Java-type developer. Java developers must have no need for these functions that I find very useful. A search for "java lower bound" or "java upper bound" does not yield the predictable result.

The Apache commons has some good collections utilities but not the lower_bound(), upper_bound() pair.

It is still a mystery why the Java Lib folks decided to ignore this function. The use of it is quite common for look-up tables. Today, I wanted to use these functions to look up for some limit values for input ranges from a table. The easiest way to explain this is that different input ranges require a look-up into a number determined by the range. The ranges are consecutive, say 0-25, 25-50, 50-100,100-500. Each range would map to a number, say 100, 212, 320, 400. The look-up function needs to return the correct number given an input number within a range.

For ex:

10 : 100
25 : 212
30 : 212
200 : 400

To satisfy the lower_bound, upper_bound standard specs, a number below the smallest range (say -10) would map to 100, and a number above the largest range will map to nothing.

lower_bound() should return an index to the first element in the sorted container that is equal to or above the number being looked up, and -1 if there is no such element.

upper_bound() should return an index to the first element in the sorted container that is above the number being looked up, and -1 if there is no such element.

Here are the functions I ended up writing:

    public static int lower_bound(Comparable[] arr, Comparable key) {
        int len = arr.length;
        int lo = 0;
        int hi = len-1;
        int mid = (lo + hi)/2;
        while (true) {
            int cmp = arr[mid].compareTo(key);
            if (cmp == 0 || cmp > 0) {
                hi = mid-1;
                if (hi < lo)
                    return mid;
            } else {
                lo = mid+1;
                if (hi < lo)
                    return mid<len-1?mid+1:-1;
            }
            mid = (lo + hi)/2;
        }
    }

    public static int upper_bound(Comparable[] arr, Comparable key) {
        int len = arr.length;
        int lo = 0;
        int hi = len-1;
        int mid = (lo + hi)/2;
        while (true) {
            int cmp = arr[mid].compareTo(key);
            if (cmp == 0 || cmp < 0) {
                lo = mid+1;
                if (hi < lo)
                    return mid<len-1?mid+1:-1;
            } else {
                hi = mid-1;
                if (hi < lo)
                    return mid;
            }
            mid = (lo + hi)/2;
        }
    }

Here are the tests:

    public void test_lower_bound() {
        final int arrSize = 500;
        final int maxNum = 1000;
        Integer[] bArr = new Integer[arrSize];
        Random r = new Random();
        for (int k=0; k<arrSize; k++) {
            bArr[k]=r.nextInt(maxNum);
        }
        Arrays.sort(bArr);

        final int numIter = 2000;
        for (int k=0; k<numIter; k++) {
            int n = r.nextInt(maxNum);
            int j = Utils.lower_bound(bArr, n);
            assertTrue((bArr[arrSize-1]<n && j==-1) || (bArr[j]>=n && (j==0 || bArr[j-1]<n)));
        }
    }

    public void test_upper_bound() {
        final int arrSize = 500;
        final int maxNum = 1000;
        Integer[] bArr = new Integer[arrSize];
        Random r = new Random();
        for (int k=0; k<arrSize; k++) {
            bArr[k]=r.nextInt(maxNum);
        }
        Arrays.sort(bArr);

        final int numIter = 2000;
        for (int k=0; k<numIter; k++) {
            int n = r.nextInt(maxNum);
            int j = Utils.upper_bound(bArr, n);
            assertTrue((bArr[arrSize-1]<=n && j==-1) || (bArr[j]>n && (j==0 || bArr[j-1]<=n)));
        }

    }