Cyrus IMAP Server: Namelocks

Intro

Name locks are an addition to the cyrus internals to address a range of race conditions and complexities. These ills are most easily avoided by ensuring that only one process can do certain operations at once. Name locks also mean crashes can’t leave the mailbox “broken” forever.

Unfortunately, locks can’t be retained over a rewrite-rename cycle, and every meta file in a regular mailbox gets rewritten upon occasion. Even cyrus.header gets a rewrite when a new userflag is added.

The “RESERVED FLAG” method of handling mailbox creation is a nasty brutish method that breaks on process crashes, causing a full mailboxes.db sweep to be necessary on restart, and making that name unusable until the entire server is restarted. It’s also unusable for serialisation of operations without making the mailbox “disappear” to other clients.

Mailbox names are supposed to be exclusive per server, not just per partition, so any lock has to apply to the entire server.

MURDER Considerations: Due to the way the mupdate protocol works, RESERVED records are still created over mupdate. This sucks, and a better way would be to support an “EXCLUSIVE CREATE” command, where the create only succeeds if the record doesn’t already exist. Then a task could create the mailbox on the local server and retain an exclusive namelock while trying to assert the name on the mupdate server. If this failed (someone else got in first) then the mailbox could be cleaned up locally before releasing the exclusive namelock, meaning other users on the local server would never see it existing.

Implementation

It’s a simple matter of having a file under $confdir/lock - in a directory tree using the same hashing structure as the mailbox tree. This directory can be symlinked or mounted to a tmpfs since the locks need not persist across restarts. Due to race conditions while cleaning up, the easiest approach is to only ever delete lock files during restart, so the unlock code doesn’t try to remove them. Lock files are zero byte in size, and are locked using the flock or fcntl primitives used by the rest of cyrus.

API

Lock types:

  • LOCK_SHARED - shared lock on the name. Required whenever you have an open mailbox with that name
  • LOCK_EXCLUSIVE - exclusive lock on the name. Required to create or finish deleting a mailbox, and required when repacking the cyrus.index and cyrus.cache files.
  • LOCK_NONBLOCKING - attempt to take an exclusive lock on the name, but if it’s not available, return immediately with r == IMAP_MAILBOX_LOCKED rather than blocking until the lock is available.

The mboxname is always an internal name, so convert it first.

Example:

struct mboxlock *lock = NULL;
int locktype = LOCK_SHARED; /* or LOCK_EXCLUSIVE or LOCK_NONBLOCKING */

r = mboxname_lock("user.brong", &lock, locktype);
if (!r) {
    do_stuff();
    mboxname_release(&lock);
}

If mboxname_lock fails, lock will remain NULL. It should always be initialised to NULL before being passed to mailbox_lock. It will be set back to NULL by mboxname_release.

Re-locking considerations

It is not possible to hold multiple locks to the same name within the same process. If you call mboxname_lock with the same name within a process, and the same locktype then a reference counter is incremented and the same lock is returned. If you use a different locktype (i.e. one shared, the other exclusive. Non-blocking is considered exclusive for this test) then IMAP_MAILBOX_LOCKED will be returned to avoid breaking the locking semantics. This is a restriction of the underlying fcntl/flock subsystem.

On the way out, the reference counter is decremented with each release and the lock isn’t freed until the counter gets back to zero.