High Performance Web: Avoid race conditions with atomic read-modify-write

Back
Benjamin Coutu on 11/06/2017.

Speed and performance are paramount for any mission critical business application. In our blog segment "High Performance Web“ our chief technology officer Benjamin Coutu shares the tricks and challenges involved in making ZeyOS perform at "lightning speed", so that the development community might benefit from our learnings here at ZeyOS.

Attention: This is a strictly technical tutorial, targeted towards developers who are familiar with PHP.

In ZeyOS we face many situations where we have to consider the concurrent nature of enterprise-class business software in order to ensure proper scalability. One such situation is the assignment of order numbers. In a highly concurrent environment such as a large online shop system for example, where several orders might get placed simultaneously, the correct assignment of unique and sequentially continuous numbers must be dealt with by using appropriate locking techniques for mutual exclusion.

As always, I will try to present my code in a way that is as easily reusable (and copy-pastable) as possible.

Let's consider the following code snippet, and let's say /opt/ordernum_count contains the last consecutive order number:

function getNextOrderNum() {
  $filename = '/opt/ordernum_count';
  $number = file_get_contents($filename) + 1;
  file_put_contens($filename, $number);
  return $number;
}

This is of course an utterly naive implementation that is prone to race conditions and, under certain circumstances, even at risk of data corruption. Also, it's not very efficient, as we effectively open our file twice, once for reading the last number and shortly thereafter again for writing the incremented number.

Now, in order to guarantee proper serialization we must ensure that no two concurrent processes are in their critical section at the same time, meaning only one process should read, modify and write at once. Those three formerly seperate steps must therefore be transparently combined into one atomic operation. We can do that by opening the file, (exclusively) locking it and then using file handling functions to manipulate the contents of the file before releasing the lock and closing the file. In order to not implicitly alter the file in any way before we acquire the lock, we must open it in c+b mode (the b flag just forces binary mode).

function getNextOrderNum() {
  $handle = fopen('/opt/ordernum_count', 'c+b');
  flock($handle, LOCK_EX);

  $number = stream_get_contents($handle) + 1;

  ftruncate($handle, 0);
  rewind($handle);

  fwrite($handle, $number);
  fflush($handle);

  flock($handle, LOCK_UN);
  fclose($handle);

  return $number;
}

Great, this got the job done. Let's tidy things up. We'll generalize the read-modify-write logic by moving it into a separate reusable function and we'll also add exception handling in order to accurately free resources. A call-back parameter expecting a function that will handle and transform the content will then allow us to execute any code within the confinement of mutual exclusion.

function readModifyWrite($filename, $callback) {
  $handle = fopen($filename, 'c+b');

  try {
    flock($handle, LOCK_EX);

    try {
      $content = $callback(stream_get_contents($handle));
      ftruncate($handle, 0);
      rewind($handle);
      fwrite($handle, $content);
      fflush($handle);
    } finally {
      flock($handle, LOCK_UN);
    }
  } finally {
    fclose($handle);
  }

  return $content;
}

Our getNextOrderNum-function will now simply be:

function getNextOrderNum() {
  return readModifyWrite('/opt/ordernum_count', function($content) {
    return $content + 1;
  });
}