paulgorman.org

Asterisk File Locking

Asterisk locks files. For example, when it writes voicemail files, it drops a lock in the mailbox directory to warn away anything else from writing at the same instant.

Since I’m working on a program that mucks around with voicemail, I realized I need to know how Asterisk file locking works. A quick web search didn’t turn up much, so I grabbed the Asterisk source code and grepped.

$ git clone https://github.com/asterisk/asterisk
$ cd asterisk
$ grep -r 'flock('
$ grep -r 'fcntl('
$ grep -r 'AST_LOCK_TYPE'
$ grep -r 'lockfile'

The most interesting results come from asterisk/main/app.c and asterisk/main/asterisk.c.

Looking in asterisk/main/asterisk.c, we see:

} else if (!strcasecmp(v->name, "lockmode")) {
	if (!strcasecmp(v->value, "lockfile")) {
		ast_set_lock_type(AST_LOCK_TYPE_LOCKFILE);
	} else if (!strcasecmp(v->value, "flock")) {
		ast_set_lock_type(AST_LOCK_TYPE_FLOCK);
	} else {
		ast_log(LOG_WARNING, "'%s' is not a valid setting for the lockmode option, "
			"defaulting to 'lockfile'\n", v->value);
		ast_set_lock_type(AST_LOCK_TYPE_LOCKFILE);
	}

So, lockmode is an option we can set in /etc/asterisk.conf to either lockfile (the default) or flock.

In the CHANGES file*, under the “Functionality changes from Asterisk 1.4.X to Asterisk 1.6.0” section, we find:

A “lockmode” option has been added to asterisk.conf to configure the file locking method used for voicemail, and potentially other things in the future. The default is the old behavior, lockfile. However, there is a new method, “flock”, that uses a different method for situations where the lockfile will not work, such as on SMB/CIFS mounts.

What’s the difference between the two Asterisk lock modes?

I recognize flock as a Unix system call. Indeed, the function in asterisk/main/app.c uses the flock system call:

static enum AST_LOCK_RESULT ast_lock_path_flock(const char *path)
{
	char *fs;
	int res;
	int fd;
	time_t start;
	struct path_lock *pl;
	struct stat st, ost;

	fs = ast_alloca(strlen(path) + 20);
	snprintf(fs, strlen(path) + 19, "%s/lock", path);
	...
	if ((fd = open(fs, O_WRONLY | O_CREAT, 0600)) < 0) {
	...
	((res = flock(pl->fd, LOCK_EX | LOCK_NB)) < 0) &&
	...
}

static int ast_unlock_path_flock(const char *path)
{
	char *s;
	struct path_lock *p;

	s = ast_alloca(strlen(path) + 20);

	AST_LIST_LOCK(&path_lock_list);
	AST_LIST_TRAVERSE_SAFE_BEGIN(&path_lock_list, p, le) {
		if (!strcmp(p->path, path)) {
			AST_LIST_REMOVE_CURRENT(le);
			break;
		}
	}
	AST_LIST_TRAVERSE_SAFE_END;
	AST_LIST_UNLOCK(&path_lock_list);

	if (p) {
		snprintf(s, strlen(path) + 19, "%s/lock", path);
		unlink(s);
		path_lock_destroy(p);
		ast_debug(1, "Unlocked path '%s'\n", path);
	} else {
		ast_debug(1, "Failed to unlock path '%s': "
				"lock not found\n", path);
	}

	return 0;
}

Note that Asterisk creates a file called lock in the target directory.

According to the flock(2) man page, the lock releases with either a flock(fd, LOCK_UN) call or the closing of the file descriptor. The ast_unlock_path_flock function ends the lock by closing and unlinking the lock file.

What does the default lockfile option do?

static enum AST_LOCK_RESULT ast_lock_path_lockfile(const char *path)
{
	char *s;
	char *fs;
	int res;
	int fd;
	int lp = strlen(path);
	time_t start;

	s = ast_alloca(lp + 10);
	fs = ast_alloca(lp + 20);

	snprintf(fs, strlen(path) + 19, "%s/.lock-%08lx", path, (unsigned long)ast_random());
	fd = open(fs, O_WRONLY | O_CREAT | O_EXCL, AST_FILE_MODE);
	if (fd < 0) {
		ast_log(LOG_ERROR, "Unable to create lock file '%s': %s\n", path, strerror(errno));
		return AST_LOCK_PATH_NOT_FOUND;
	}
	close(fd);

	snprintf(s, strlen(path) + 9, "%s/.lock", path);
	start = time(NULL);
	while (((res = link(fs, s)) < 0) && (errno == EEXIST) && (time(NULL) - start < 5)) {
		sched_yield();
	}

	unlink(fs);

	if (res) {
		ast_log(LOG_WARNING, "Failed to lock path '%s': %s\n", path, strerror(errno));
		return AST_LOCK_TIMEOUT;
	} else {
		ast_debug(1, "Locked path '%s'\n", path);
		return AST_LOCK_SUCCESS;
	}
}

static int ast_unlock_path_lockfile(const char *path)
{
	char *s;
	int res;

	s = ast_alloca(strlen(path) + 10);

	snprintf(s, strlen(path) + 9, "%s/%s", path, ".lock");

	if ((res = unlink(s))) {
		ast_log(LOG_ERROR, "Could not unlock path '%s': %s\n", path, strerror(errno));
	} else {
		ast_debug(1, "Unlocked path '%s'\n", path);
	}

	return res;
}

Note that asterisk creates a file called .lock in the target directory, rather than the dotless lock it uses for flock mode.

Chapter 55 of The Linux Programming Interface describes file locking. Section 55.7 describes the pattern of link plus unlink calls Asterisk uses here, and its shortcomings. These are they key lines:

while (((res = link(fs, s)) < 0) && (errno == EEXIST) && (time(NULL) - start < 5)) {
	sched_yield();
}

If two process try to link the same file, the second one get an EEXIST error. If that’s us, we have to try again later (sched_yield).

We could work with either lock method, but flock seems easier to implement and less error-prone.


*In the CHANGES file, I also noticed this, which may come into play when messing with Asterisk’s voicemail files:

MWI (Message Waiting Indication) handling has been significantly restructured internally to Asterisk. It is now totally event based instead of polling based. The voicemail application will notify other modules that have subscribed to MWI events when something in the mailbox changes. This also means that if any other entity outside of Asterisk is changing the contents of mailboxes, then the voicemail application still needs to poll for changes. Examples of situations that would require this option are web interfaces to voicemail or an email client in the case of using IMAP storage. So, two new options have been added to voicemail.conf to account for this: “pollmailboxes” and “pollfreq”. See the sample configuration file for details.

But also:

Added VoicemailRefresh action to allow an external entity to trigger mailbox updates when changes occur instead of requiring the use of pollmailboxes.

#asterisk #c

⬅ Older Post Newer Post ➡