Atomic Updates

Some applications require atomic file updates. This means that either every write that is part of the update finishes successfully or, if the update is interrupted, the volume reverts to its state before the update began, with no possibility of being left in an in-between state.

rename() can be used to atomically update a single file. For this, write the update to a new file using a temporary name. After writing is finished, rename the new file to the name of the old file. This atomically deletes the old file and gives its name to the new file. If a file with the temporary name exists at startup, the update was interrupted. The application can delete the new file and repeat the update process.

TargetXFS allows updates to multiple files to be atomic. For this, all changes in the update must be to new clusters only. TargetXFS metadata updates always use new clusters. User writes usually extend the file’s end and therefore use new clusters. However, if a write follows a seek to the file’s interior, by default that updates the original data cluster, not a copy.

TargetXFS’s no-overwrite mode changes this, so that file overwrites are written to new clusters. The original clusters are undisturbed. Three things are required to use no-overwrite mode:

  1. The INC_NO_OVERWRITE configuration definition in “xfsp.h” must be TRUE. It is defined as FALSE by default but the definition is wrapped by #ifndef/#endif directives so that it can be overridden in “config.h” or “app_cfg.h”.

  2. The FSF_NO_OVERWRITE flag must be set in the XfsCfg structure’s ‘flags’ field when the volume is added.

  3. If fopen() is used for the atomic update, it must include the ‘n’ mode character. If open() is used for the atomic update, the O_NO_OVERWRITE flag must be invoked.

This makes updates involving multiple writes and/or file creations atomic unless a synchronization occurs before the update finishes. To avoid this, ensure i) there are enough free clusters to finish the update without triggering an automatic synchronization and ii) no other task can cause a synchronization during the update.

The example below uses vol_lock() to prevent synchronizations by other tasks, sync() to free dirty clusters, vstat() to ensure there are enough free clusters to hold the update, multiple writes to perform the update, a final sync() to save the update, and vol_unlock() to restore volume access to all tasks.

  /*-------------------------------------------------------------------*/
  /* Lock volume use to just the running task.                         */
  /*-------------------------------------------------------------------*/
  hndl = vol_lock(vol_name);

  /*-------------------------------------------------------------------*/
  /* Convert all dirty clusters to clean.                              */
  /*-------------------------------------------------------------------*/
  sync();

  /*-------------------------------------------------------------------*/
  /* Ensure there are enough free clusters to hold a second copy of    */
  /* the data being updated (since O_NO_OVERWRITE will be used).       */
  /*-------------------------------------------------------------------*/
  if (vstat(vol_name, &info))
    error("vstat() failed");
  if (update_sz > info.xfs.free_2_sync * info.xfs.clust_size)
    error("Not enough free clusters for O_NO_OVERWRITE update");

  /*-------------------------------------------------------------------*/
  /* Open file using the "no overwrite" option.                        */
  /*-------------------------------------------------------------------*/
  fid = open("critical", O_RDWR | O_NO_OVERWRITE);
  if (fid == -1)
    error("open() failed");

  /*-------------------------------------------------------------------*/
  /* Write to file, updating it in "no overwrite" mode, then close it. */
  /*-------------------------------------------------------------------*/
  wr_sz = remaining = update_sz;
  if (wr_sz > BUF_SIZE)
    wr_sz = BUF_SIZE;
  do
  {
    if (wr_sz > remaining)
      wr_sz = remaining;
    rv = write(fid, Buf, wr_sz);
    if (rv < wr_sz)
      error("write() failed");
  }
  while (remaining -= rv);
  if (close(fid))
    error("close() failed");

  /*-------------------------------------------------------------------*/
  /* Do another sync to flush the update to backing store.             */
  /*-------------------------------------------------------------------*/
  sync();

  /*-------------------------------------------------------------------*/
  /* Unlock volume for use by all tasks.                               */
  /*-------------------------------------------------------------------*/
  vol_unlock(hndl);

Atomic updates can also be implemented using a log file that fully describes the desired modifications in idempotent steps (steps that can be repeated without changing the outcome). After every logged step is completed, the log file is deleted. If the application finds, at power-up, that the log file exists, it simply repeats the listed steps and deletes the log file. This ensures the listed modifications succeed in an atomic fashion.