Fun with Firefox Jitters (update 2)

September 25, 2007 at 04:37 PM | Uncategorized | View Comments

A little while ago, I started noticing some odd behaviour in DrProject. Every once and a while, a stack trace complaining about primary key violations in the session table would come up when trying to view the list of tickets with Firefox. I ticketed it, but didn't think much of it until it started happening more frequently (and in production).

First, a bit of background. When ever a user views a list of tickets in DrProject, it writes that list to a session variable. This is so that the "next ticket" and "previous ticket" links (in the single-ticket view) do the RightThing™. The code that handles that is something like this:

  1. s = SessionVariable.query(session_id=id, name="ticket_list")
  2. if s:
  3.    s.value = new_list
  4. else:
  5.    s = SessionVariable(session_id=id, name="ticket_list", value=new_list)
  6. s.flush_to_database()

Looks fine, right? Well, not if two threads with the same session id try to update the ticket list within 1/100th of a second of each other... But that's not really a problem, is it? Unfortunately that's exactly what Firefox is doing.

A packet dump of Firefox's strange behaviour
Firefox decides that she doesn't like the first connection (0), so she kills it off and starts again. The full dump is also available.

When the "View Tickets" link is clicked, this is what Firefox does:

  1. Opens one connection
  2. Sends headers
  3. Waits 5 millionths of a second
  4. Closes the connection (see 0)
  5. Opens a new connection and continues as normal (see 1)

Wonderful.

As the two requests come in, neither thread can see a ticket_list session variable, so both try to generate one... But when the second call to flush_to_database() comes in, the database raises an IntegrityError because the data is already there! This thread sends a stack trace back up to the user. The other thread happily returns, oblivious to the problem.

Now, this is where things get a little bit more interesting. Because all this has happened so quickly, there is no way to know which thread corresponds to which request (the one that Firefox has closed or the one that will make it back to the user). If we're lucky, Firefox will get the stack trace and the user will see a nice list of tickets... But that's hardly a safe design principle.

So how do we fix this?

  1. Give each request a unique ID.
    If two requests with the same ID come in, you just ignore the all but the first. But this doesn't work because you may be ignoring the request going to the user.

  2. Figure out why Firefox is making two requests.
    This turns out to be Really Hard. Greg sent an email off to David Humphrey, who suggested that we make sure there isn't some Java Script trickery going on. After a bit of digging around, nothing came up. With JavaScript turned off, everything was fine. When I turned JavaScript on but commented out all the >script< tags, things were still exploding. Crap.

  3. Wrap the code in a try/catch block, and just ignore any IntegrityErrors that come up.

Well, up until about five minutes ago, the plan was to go with option 3... But as I was writing this, I realized there was a very remote possibility that some onClick trickery could be going on. And guess what?

Yet again have I forgotten one of the most fundamental rules of programming: printf isn't broken.

Update

I have not had a chance to get in front of the code yet, but when I do the solution will be to remove the offending JavaScript.

Alex originally added it so that the entire row (<tr>) could be clicked like a hyperlink... But it had this unfortunate side effect (and, Dmitri added, you can't open these pseudo-links in new tabs).

After a bit of Googling, I can't seem to find any HTML-only method of turning entire table rows into hyperlinks... So, unless anyone has an insight into this, we will just need to live with old-fashioned text-only hyperlinks.

Update 2

I was talking with Andrew Louis today, and he pointed me to this article. It shows how to do exactly the same thing the JavaScript did before, just in nice "clean" CSS (at least, it's about as "clean" as CSS can be). It felt good to rip out a huge block of JavaScript and nested tables :-)