Wednesday, July 06, 2011

Linux Shell, HUP and process status on logout

It used to be the case that all processes a user starts are killed by the shell upon logout. Not any more, as recent experiments with Ubuntu 10.04 shows.

The shell can be configured to send a HUP signal to its children when the shell exits. This is controlled by the huponexit shell option as explained in the bash man page:

If the huponexit shell option has been set with shopt, bash sends a SIGHUP to all jobs when an interactive login shell exits.

Determine the setting of huponexit with:

shopt huponexit

If it is "off", then processes started by the user will remain running after logout. This setting makes it easier to start a long running process simply from within the shell, without invoking a screen and without having to wrap the process in nohup.

Here is a discussion on the issue.

However, this setting seems to cause problems for interactive sessions when a new user could start referring to an old user's now invalid processes.

Thursday, June 23, 2011

Asynchronous UDP server using Java NIO

UDP is a light-weight protocol as compared to TCP. When the data transmitted is small (in hundreds of bytes), and an occasional loss of data is not critical, UDP can be used to improve throughput of the program.

The native sockets library (C) provides the epoll function - available on Linux 2.6.x kernels - that can be used for both TCP and UDP sockets. In an earlier post, I described a framework that can be used to implement an asynchronous client that connects to multiple servers using TCP. I found several code examples that described how Java NIO can be used for this purpose. It turns out that it is even simpler to write a NIO server for UDP.

I would not recommend writing a UDP server if the request/response cannot be transmitted in a single UDP packet or if a packet has a dependency on an earlier packet. UDP packets can arrive out of order and the headers have no sequence numbers to enable re-ordering. If you want to handle reordering, you will be implementing what TCP provides for this purpose and it is probably a better idea to stick with TCP.

The following program does well when the request/response sticks in a single UDP packet. 512 bytes is generally considered the safe maximum size and the DNS protocol mandates a maximum packet size of 512 when it uses UDP.

public class ASyncUDPSvr {
    static int BUF_SZ = 1024;

    class Con {
        ByteBuffer req;
        ByteBuffer resp;
        SocketAddress sa;

        public Con() {
            req = ByteBuffer.allocate(BUF_SZ);
        }
    }

    static int port = 8340;
    private void process() {
        try {
            Selector selector = Selector.open();
            DatagramChannel channel = DatagramChannel.open();
            InetSocketAddress isa = new InetSocketAddress(port);
            channel.socket().bind(isa);
            channel.configureBlocking(false);
            SelectionKey clientKey = channel.register(selector, SelectionKey.OP_READ);
            clientKey.attach(new Con());
            while (true) {
                try {
                    selector.select();
                    Iterator selectedKeys = selector.selectedKeys().iterator();
                    while (selectedKeys.hasNext()) {
                        try {
                            SelectionKey key = (SelectionKey) selectedKeys.next();
                            selectedKeys.remove();

                            if (!key.isValid()) {
                              continue;
                            }

                            if (key.isReadable()) {
                                read(key);
                                key.interestOps(SelectionKey.OP_WRITE);
                            } else if (key.isWritable()) {
                                write(key);
                                key.interestOps(SelectionKey.OP_READ);
                            }
                        } catch (IOException e) {
                            System.err.println("glitch, continuing... " +(e.getMessage()!=null?e.getMessage():""));
                        }
                    }
                } catch (IOException e) {
                    System.err.println("glitch, continuing... " +(e.getMessage()!=null?e.getMessage():""));
                }
            }
        } catch (IOException e) {
            System.err.println("network error: " + (e.getMessage()!=null?e.getMessage():""));
        }
    }

    private void read(SelectionKey key) throws IOException {
        DatagramChannel chan = (DatagramChannel)key.channel();
        Con con = (Con)key.attachment();
        con.sa = chan.receive(con.req);
        System.out.println(new String(con.req.array(), "UTF-8"));
        con.resp = Charset.forName( "UTF-8" ).newEncoder().encode(CharBuffer.wrap("send the same string"));
    }

    private void write(SelectionKey key) throws IOException {
        DatagramChannel chan = (DatagramChannel)key.channel();
        Con con = (Con)key.attachment();
        chan.send(con.resp, con.sa);
    }

    static public void main(String[] args) {
        ASyncUDPSvr svr = new ASyncUDPSvr();
        svr.process();
    }
}

When dealing with small data sizes that fit in one packet, clearly if the NIO interface signals us that data is available to be read, then all the data must be available. Thus the protocol does not need to worry about accumulating network data in buffers. We still do need an object that is tied to each client connection as the reading and writing happen in two distinct parts of the code.

First, after establishing our UDP socket locally on the server, we signal NIO that the socket is ready for reads. When NIO wakes us up - via the select() call - we can immediately read the full request made by the client. At this point, we form our response but do not want to write it back to the network right away, as the kernel buffers may be full and the write may block. So, we store the response on the object attached to the client connection (via the SelectionKey object), signal NIO that we are now ready to write and go back to our select() loop.

Next when NIO wakes us up from the select() call, we can proceed to write. Again since the data fits in one packet, we know that the send() call need not be retried, and all data will be sent.

However, the nature of UDP does not provide the advantages TCP provides in epoll() mode. A UDP server does not provide a separate socket for each new client. Thus the epoll selector always has just the single socket. Each new client sends its datagrams to the single UDP receive buffer of the server.

A threaded server without the use of epoll() might be more advantageous. Each thread could wait on the single server socket, using a receive() call. The kernel will ensure that only one thread wakes up from the receive() call. I hope to use such an implementation and measure both designs.

Friday, June 10, 2011

Java splitting an empty string

Splitting an empty string results in an array whose single element is an empty string - not intuitive. The expected result is either a null array or a zero-length array.

Perl:

$$$:~$ perl -e '@x=split(/ /, ""); $s=@x;print "$s\n"'
0

Python:
$$$:~$ python -c 'list="".split();l=len(list);print(l)'
0

Thursday, June 09, 2011

/dev/urandom does not generate correct multi-byte sequences

If you use /dev/urandom with "tr" to generate random strings, you may have a problem if your shell uses a multi-byte locale. Upon encountering illegal bytes, tr will complain with "tr: Illegal byte sequence".

Setting the LC_TYPE=C before tr would do the trick:

cat /dev/urandom| LC_CTYPE=C tr -dc 'a-zA-Z0-9'

Friday, May 20, 2011

Some web servers are in love with 30X redirects

Here is a URL that redirects via 30X response headers no less than 10 times:

http://join.scoreondemand.com/strack/MTAwNC45LjQ0LjQ0LjI5LjAuMC4wLjA/scoreondemand/64/0/Default.aspx

The evidence:

mpire@seaxoaff01:~$ curl -I http://join.scoreondemand.com/strack/MTAwNC45LjQ0LjQ0LjI5LjAuMC4wLjA/scoreondemand/64/0/Default.aspx
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:56:12 GMT
Cneonction: close
Location: http://join.scoreondemand.com/track/MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w/Default.aspx?switched=1&strack=0
ScoreTracker: scash04
Content-Type: text/html
Set-Cookie: NSC_tdpsfdbti-obut-80=ffffffff090a1f1e45525d5f4f58455e445a4a423660;Version=1;Max-Age=3600;path=/;httponly

mpire@seaxoaff01:~$ curl -I "http://join.scoreondemand.com/track/MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w/Default.aspx?switched=1&strack=0"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:56:31 GMT
Set-Cookie: PHPSESSID=rbra31g59vc4me30bbofgei7d1; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
nnCoection: close
Set-Cookie: nats=MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w; expires=Mon, 30-May-2011 19:56:31 GMT; path=/; domain=scoreondemand.com
Set-Cookie: nats_cookie=No%2BReferring%2BURL; expires=Mon, 30-May-2011 19:56:31 GMT; path=/; domain=scoreondemand.com
Set-Cookie: nats_unique=MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w; expires=Sat, 21-May-2011 19:56:31 GMT; path=/; domain=scoreondemand.com
Set-Cookie: nats_sess=726064a93aca6b9d49d72dd57f477c57; expires=Sun, 28-Aug-2011 19:56:31 GMT; path=/; domain=scoreondemand.com
Location: http://www.scoreondemand.com/Default.aspx?nats=MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w&switched=1&strack=0
ScoreTracker: scash01
Content-Type: text/html
Set-Cookie: NSC_tdpsfdbti-obut-80=ffffffff090a1f1d45525d5f4f58455e445a4a423660;Version=1;Max-Age=3600;path=/;httponly

mpire@seaxoaff01:~$ curl -I "http://www.scoreondemand.com/Default.aspx?nats=MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w&switched=1&strack=0"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:56:47 GMT
X-AspNet-Version: 2.0.50727
Location: http://join.eboobstore.com/strack/MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w/eboobstore/64/0/apple/
Set-Cookie: ASP.NET_SessionId=xhivgbmef0bqm1uhihz1r455; path=/; HttpOnly
Set-Cookie: SVOD1=UserID=11133628&SessionID=1bM0125818hnmy5CAG9P; expires=Thu, 18-Aug-2011 19:56:47 GMT; path=/
Set-Cookie: NATS=MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w; expires=Thu, 18-Aug-2011 19:56:47 GMT; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 206

mpire@seaxoaff01:~$ curl -I "http://join.eboobstore.com/strack/MTAwNC42NC40Ny40Ny4yOS4wLjAuMC4w/eboobstore/64/0/apple/"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:57:13 GMT
Cneonction: close
Location: http://join.eboobstore.com/track/MTAwNC42NC41MC41MC4yOS4wLjAuMC4w/apple/?switched=1&strack=0
ScoreTracker: scash04
Content-Type: text/html; charset=UTF-8
Set-Cookie: NSC_tdpsfdbti-obut-80=ffffffff090a1f1e45525d5f4f58455e445a4a423660;Version=1;Max-Age=3600;path=/;httponly

mpire@seaxoaff01:~$ curl -I "http://join.eboobstore.com/track/MTAwNC42NC41MC41MC4yOS4wLjAuMC4w/apple/?switched=1&strack=0"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:57:39 GMT
Set-Cookie: PHPSESSID=utrmur64derb0n2onaotf08640; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
nnCoection: close
Set-Cookie: nats=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w; expires=Mon, 30-May-2011 19:57:39 GMT; path=/; domain=eboobstore.com
Set-Cookie: nats_cookie=No%2BReferring%2BURL; expires=Mon, 30-May-2011 19:57:39 GMT; path=/; domain=eboobstore.com
Set-Cookie: nats_unique=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w; expires=Sat, 21-May-2011 19:57:39 GMT; path=/; domain=eboobstore.com
Set-Cookie: nats_sess=a7568ec181d54a6316d6452656565dba; expires=Sun, 28-Aug-2011 19:57:39 GMT; path=/; domain=eboobstore.com
Location: http://www.eboobstore.com/apple/?nats=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w&switched=1&strack=0
ScoreTracker: scash01
Content-Type: text/html; charset=UTF-8
Set-Cookie: NSC_tdpsfdbti-obut-80=ffffffff090a1f1d45525d5f4f58455e445a4a423660;Version=1;Max-Age=3600;path=/;httponly

mpire@seaxoaff01:~$ curl -I "http://www.eboobstore.com/apple/?nats=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w&switched=1&strack=0"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:58:46 GMT
Location: http://www.eboobstore.com/urlmunge/munger/nats=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w&switched=1&strack=0_URL_apple/
Content-Type: text/html; charset=UTF-8

mpire@seaxoaff01:~$ curl -I "http://www.eboobstore.com/urlmunge/munger/nats=MTAwNC42NC41MC41MC4yOS4wLjAuMC4w&switched=1&strack=0_URL_apple/"
HTTP/1.1 302 Found
Date: Fri, 20 May 2011 19:58:58 GMT
Set-Cookie: PHPSESSID=33hcivt9bgl7o6qv5bpfv7aun4; path=/; domain=eboobstore.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Location: http://www.eboobstore.com/apple
ScoreTracker: web04
Content-Type: text/html; charset=UTF-8

mpire@seaxoaff01:~$ curl -I "http://www.eboobstore.com/apple"
HTTP/1.1 301 Moved Permanently
Date: Fri, 20 May 2011 19:59:10 GMT
Location: http://eboobstore.com/apple/
Content-Type: text/html; charset=UTF-8

mpire@seaxoaff01:~$ curl -I "http://eboobstore.com/apple/"
HTTP/1.1 301 Moved Permanently
Date: Fri, 20 May 2011 19:59:48 GMT
Location: http://www.eboobstore.com/apple/
Content-Type: text/html; charset=UTF-8

mpire@seaxoaff01:~$ curl -I "http://www.eboobstore.com/apple/"
HTTP/1.1 200 OK
Date: Fri, 20 May 2011 20:00:30 GMT
ScoreTracker: web06
Content-Type: text/html; charset=UTF-8

Wednesday, March 23, 2011

Insanely compressed html files

Today, I discovered a URL that sent some insanely compressed content. The compressed content was sent by the server using Content-Encoding: gzip and Transfer-encoding: chunked. The compressed size of the content was 2,921,925 bytes and it decompressed to 1,004,263,982 bytes. The decompressed content was roughly 344 times the size of the compressed content.

This caused certain things to go wrong in the production process. I had set a limit of a few Megs on all fetches and had assumed that a single fetch could not be more than a few Megs. This was the first time I have seen such a huge decompression rate. This caused a subsequent file mapping to fail due to inadequate memory.

The downloaded content suggested why this would compress so well. The URL was http://www.jeltel.com.au/news.php There seems to be a dynamically generated part on this URL. If you examine its source, you will see a marker like this:

<!-- JELTEL_CONTENT_BEGIN -->

Content after that seems dynamically generated. You will find markup like this:

<h2></h2> - <br/><h4>... <a href="">read more</a></h4>

On this particular instance, there was an unusually large amount of fake content generated. The downloaded file had just 33 lines, but the last long line was a huge repeating pattern of :

<a href="">read more</a></h4><br/><br/><h2></h2> - <br/><h4>... 

This would of course compress well.

Wednesday, March 16, 2011

Linux sort : bug with , separator and confusing period?

user@host:~/$ echo -e "alan,20,3,0\ngeorge,5,0,0\nalice,3,5,0\ndora,4,0.9,5" | sort -n -k 2 -t ,
dora,4,0.9,5
alice,3,5,0
george,5,0,0
alan,20,3,0user@host:~/$ 

The line with "dora" as the first term should be printed after "alice" and before "george", as we are asking sort to sort on the second column. The 3rd column value of "0.9" seems to confuse sort on this.

This is not a bug in sort but due to the locale setting on different operating systems.

On the above link, look for "Sort does not sort in normal order".

Setting LC_PATH=C sorts as expected:

user@host:~/$ echo -e "alan,20,3,0\ngeorge,5,0,0\nalice,3,5,0\ndora,4,0.9,5" | LC_ALL=C sort -n -k 2 -t ,
alice,3,5,0
dora,4,0.9,5
george,5,0,0
alan,20,3,0
user@host:~/$