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.
- With the
link
plusunlink
method we have to handle retries. Withflock
, we just wait for the call to unblock. - The
link
plusunlink
method is significantly slower thanflock
, although the average Asterisk system probably doesn’t do enough locking for it to matter. - If the process dies, the system releases
flock
. The lock created withlink
does not get automatically released. - The
link
andunlink
method may not work on some network file systems (NFS, SMB), whileflock
should work.
*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