1

I've got a system that is basically a serial number generator.

There are two main design requirements of the serial number generator (in terms of fault tolerance):

  1. All serial numbers issued must be unique (no duplicates)
  2. If the serial number generator crashes, it needs a way to restart approximately from where it left off (skipping a few serial numbers when recovering from a crash is fine, if necessary, to ensure requirement #1 is upheld)

It can be assumed that serial numbers are issued sequentially (1, 2, 3, 4, etc.)

My first attempt to solve this is for the serial number generator to log every serial number that is issues by appending to a single log file. This way, if it crashes, it simply picks up from the last serial number issued, and goes along it's merry way.

Heres the challenge:

So, what are the pros/cons of the following logging approaches:

  1. Have a single log file which is appended, and capped at a certain size
  2. Have two log files, neither of which are appended. The first log file always contains the most recently issued serial number, and the second file contains the 2nd most recently issued serial number. The log files are written to in a see-saw fashion (i.e. all even serial numbers wind up getting put in log file 'A', and odd serial numbers get put in log file 'B'). The end result is that you've always got the last two serial numbers issued (and no more), the use of disk space is tiny, and if the serial number generator happened to crash while logging the most recent serial number, then the 'most recent' file may be corrupted. Since we also have the 2nd most recent serial number in the other log file, we can detect the crash, and restart the serial number generator from this '2nd most recent' serial number, +2

In a crash scenario where the serial number generator cannot tell which of the 'most recent' or '2nd most recent' log file is the one corrupted, it should be safe to always restart a crashed generator from the the non-corrupted serial number +2.

Option 1 is a little simpler to implement, but option 2 uses less disk space, and seems more intelligent.

Am I missing anything in terms of designing a system that can reliably recover from a crash via sufficient log files?

Kara
  • 6,115
  • 16
  • 50
  • 57
jefflunt
  • 33,527
  • 7
  • 88
  • 126
  • I would consider adding a message broker to your architecture. If you use a persistent queue with failover, you can solve crash problem. ActiveMQ is good open source choice. – johnnieb Jul 31 '11 at 16:27
  • @johnnieb: I wouldn't say that "solves" the crash problem. It might hide it or even mitigate some of the issues; but it certainly doesn't solve it. We have to know why it would be crashing in the first place. – NotMe Jul 31 '11 at 16:36
  • Hi Chris: Specifically, I should have said failover in a message broker cluster. – johnnieb Jul 31 '11 at 16:40
  • ActiveMQ looks interesting, but in this case it appears to be a "sports car" solution to something that just needs to be your "get around town _reliably_" solution. – jefflunt Jul 31 '11 at 19:09

2 Answers2

1

You need to decide on a realm of "close". By that I mean how many numbers you are willing to lose in case of a crash.

Let's say its 1000.

Then you keep the largest sequence in a file.

When its time to update, you write the new number to a new file, then rename it over the old file. This is an atomic operation on modern file systems, it either works or it doesn't, so it's like a commit in a database. It guarantees you have room for the new sequence information, and should fail without harming the current sequence information if something really untoward happens.

If there's a failure, you have to STOP, and abort the sequence generator.

The key here is that the number on the file system is larger than any issued number. So, you must guarantee that it never ends up below a current issued number, or it will reuse numbers on restart.

So, here's the procedure.

function int getNextSequence() {
    currentSeq = currentSeq + 1; 
    if (currentSeq >= maxSeq) {
        maxSeq = maxSeq + 1000;
        write(maxSeq, "newSeq");
        rename("newSeq", "curSeq");
    }
    return currentSeq;
}

function restartSequence() {
    maxSeq = read("curSeq");

    currentSeq = maxSeq - 1; // This will immediately create a disk update on first use.
 }

There may be a one off error in here, untested.

Addenda:

If you're that worried, you could keep four pieces of data in memory, and write out two copies. Or better, six and three.

The data you keep in memory is three copies of the counter, and three checksums of those counters (MD5 of the value perhaps).

Then when you write them, you use the same technique as above, write, then rename.

But you write the values and the hashes.

The reason you do this is simple.

If the values of the sequence do not match their hash/checksum, you know that the pair is in error.

You have three copies based on the premise that while one corruption is possible, and not just in disk, but in memory as well -- do not disregard potential memory errors (if you want to go paranoid, go all the way I say), but the fact of the corruption affecting more than one is astronomically unlikely.

When you do detect a failed pair, you have three samples to choose from, and each sample is a "vote". Pick the two that match as the official value, recover with that value, and move on.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • You know, I didn't quite follow it at first, but I see what you're saying. Basically, if I'm willing to lose, say, 1,000 serial numbers in a crash, and I start with serial number '0', then I write '1000' to a file. I then proceed to issue 1000 serial numbers. When I get to serial number '999', I change the value in my log file from '1000' to '2000', and then issue another 1,000 serial numbers. I continue this throughout the life of the generator, and the benefit is that I never lose more than 1,000 serials to a program crash, while minimizing disk I/O during normal operation. – jefflunt Aug 01 '11 at 03:05
  • If a program crash occurs, the generator knows to pick up with the last value written to the log file, because it's guaranteed to be a serial number not already issued. You're also saying that I probably don't have to worry too much about data corruption on the disk, since it's the responsibility of the OS to ensure atomic write actions. – jefflunt Aug 01 '11 at 03:13
  • However, I could put one more failsafe in the system - if a crash happened, and in the unlikely case, some data was corrupted (let's say a massive power surge hit the serial number generator and caused some completely wacky stuff to get written to the disk), I could check that both (A) the 'new sequence' file contained a number higher than the 'current sequence' file, and (B) that the two numbers differed by exactly the amount of the crash threshold (in this example, the number 1,000). That should be enough failsafe to know (1) if it crashed and (2) if the crash is recoverable. – jefflunt Aug 01 '11 at 03:15
  • I suppose if a crash happend that was so strange or severe that none of the above options allowed for a recovery, then I'd have to abandon that serial number generator altogether, and find a feasible way to start a new one in its place. Coincidentally, I have THAT part figured out. Thanks a ton! – jefflunt Aug 01 '11 at 03:18
1

Before going about with any sort of design I think you really need to define and address the reasons why such a simple piece of software might crash.

Off the top of my head a few might be: Lack of disk space, shoddy coding with unfreed resources, threading issues, etc.

If your goal is to simply make sure a generated serial number is persisted and unique then I'd probably suggest using something like a sql server in combination with a column of type NEWSEQUENTIALID(). There are certain advantages here due to the problem space that sql server has already solved. The number of transactions a second you could support is really up to the hardware and how you go about using it.

This was a long winded way of saying: First establish why you think it will crash. Then look at existing technologies to see if they meet your need before you go on to write something like this.

For example. If you're having problems with threads, consider using a web server to handle all of that for you. If you're having problems with disk space, consider upgrading your hardware. If you have problems with ensuring persistance, use a SQL (brand doesn't matter too much) server to store the data. If the generator machine is overloaded, consider a different architecture that allows you to load balance the devices intelligently.


One other thing: I think neither of the approaches you've outlined are good solutions. If you are truly generating 1000's per second then you might consider load balancing the generation. At which point you'll have serious issues figuring out how to keep regular log files in sync across multiple generation points... Which, sql server is already good at.

NotMe
  • 87,343
  • 27
  • 171
  • 245
  • I've thought of going with the SQL based solution for the very reason that it has auto-increment columns. I suppose if something corrupted the DB, then I would have the DB journal to fall back on in order to get back to a known state for the generator. Hm... – jefflunt Jul 31 '11 at 19:23
  • Also, I've edited my question for clarification - it is NOT a requirement that the list of issued serial numbers be retained - just that no duplicates are issued. As such, I've chosen an implementation that issues serial numbers in sequential order. – jefflunt Jul 31 '11 at 19:27
  • I've also removed my reference to disk I/O constraints, as a load balancing solution has already been figured out, which makes my reference to disk I/O irrelevant to the question I am asking. – jefflunt Jul 31 '11 at 19:32