mac: Implement the locking part of ScopedRlzValueStoreLock.
BUG=chromium:117738
TEST=none
Review URL: https://codereview.appspot.com/5874061
git-svn-id: http://rlz.googlecode.com/svn/trunk@115 10bc0f33-e4bf-9a86-80cf-af638054f0c4
diff --git a/lib/rlz_value_store.h b/lib/rlz_value_store.h
index 68a6770..2a7a41d 100644
--- a/lib/rlz_value_store.h
+++ b/lib/rlz_value_store.h
@@ -77,7 +77,13 @@
// system, the only way to access the RlzValueStore is through a
// ScopedRlzValueStoreLock, which is a cross-process lock. It is active while
// it is in scope. If the class fails to acquire a lock, its GetStore() method
-// returns NULL.
+// returns NULL. If the lock fails to be acquired, it must not be taken
+// recursively. That is, all user code should look like this:
+// ScopedRlzValueStoreLock lock;
+// RlzValueStore* store = lock.GetStore();
+// if (!store)
+// return some_error_code;
+// ...
class ScopedRlzValueStoreLock {
public:
ScopedRlzValueStoreLock();
@@ -94,7 +100,6 @@
LibMutex lock_;
#else
base::mac::ScopedNSAutoreleasePool autorelease_pool_;
- // TODO(thakis): Mac lock implementation
#endif
};
diff --git a/mac/lib/rlz_value_store_mac.mm b/mac/lib/rlz_value_store_mac.mm
index 3b27ceb..610f8b8 100644
--- a/mac/lib/rlz_value_store_mac.mm
+++ b/mac/lib/rlz_value_store_mac.mm
@@ -13,6 +13,7 @@
#include "rlz/lib/rlz_lib.h"
#import <Foundation/Foundation.h>
+#include <pthread.h>
using base::mac::ObjCCast;
@@ -215,6 +216,90 @@
namespace {
+// Creating a recursive cross-process mutex on windows is one line. On mac,
+// there's no primitve for that, so this lock is emulated by an in-process
+// mutex to get the recursive part, followed by a cross-process lock for the
+// cross-process part.
+
+// This is a struct so that it doesn't need a static initializer.
+struct RecursiveCrossProcessLock {
+ // Tries to acquire a recursive cross-process lock. Note that this _always_
+ // acquires the in-process lock (if it wasn't already acquired). The parent
+ // directory of |lock_file| must exist.
+ bool TryGetCrossProcessLock(NSString* lock_filename);
+
+ // Releases the lock. Should always be called, even if
+ // TryGetCrossProcessLock() returns false.
+ void ReleaseLock();
+
+ pthread_mutex_t recursive_lock_;
+ pthread_t locking_thread_;
+
+ NSDistributedLock* file_lock_;
+} g_recursive_lock = {
+ // PTHREAD_RECURSIVE_MUTEX_INITIALIZER doesn't exist before 10.7 and is buggy
+ // on 10.7 (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=51906#c34), so emulate
+ // recursive locking with a normal non-recursive mutex.
+ PTHREAD_MUTEX_INITIALIZER
+};
+
+bool RecursiveCrossProcessLock::TryGetCrossProcessLock(
+ NSString* lock_filename) {
+ bool just_got_lock = false;
+
+ // Emulate a recursive mutex with a non-recursive one.
+ if (pthread_mutex_trylock(&recursive_lock_) == EBUSY) {
+ if (pthread_equal(pthread_self(), locking_thread_) == 0) {
+ // Some other thread has the lock, wait for it.
+ pthread_mutex_lock(&recursive_lock_);
+ CHECK(locking_thread_ == 0);
+ just_got_lock = true;
+ }
+ } else {
+ just_got_lock = true;
+ }
+
+ locking_thread_ = pthread_self();
+
+ // Try to acquire file lock.
+ if (just_got_lock) {
+ const int kMaxTimeoutMS = 5000; // Matches windows.
+ const int kSleepPerTryMS = 200;
+
+ CHECK(!file_lock_);
+ file_lock_ = [[NSDistributedLock alloc] initWithPath:lock_filename];
+
+ BOOL got_file_lock = NO;
+ int elapsedMS = 0;
+ while (!(got_file_lock = [file_lock_ tryLock]) &&
+ elapsedMS < kMaxTimeoutMS) {
+ usleep(kSleepPerTryMS * 1000);
+ elapsedMS += kSleepPerTryMS;
+ }
+
+ if (!got_file_lock) {
+ [file_lock_ release];
+ file_lock_ = nil;
+ return false;
+ }
+ return true;
+ } else {
+ return file_lock_ != nil;
+ }
+}
+
+void RecursiveCrossProcessLock::ReleaseLock() {
+ if (file_lock_) {
+ [file_lock_ unlock];
+ [file_lock_ release];
+ file_lock_ = nil;
+ }
+
+ locking_thread_ = 0;
+ pthread_mutex_unlock(&recursive_lock_);
+}
+
+
// This is set during test execution, to write RLZ files into a temporary
// directory instead of the user's Application Support folder.
NSString* g_test_folder;
@@ -259,20 +344,39 @@
return [CreateRlzDirectory() stringByAppendingPathComponent:kRlzFile];
}
+// Returns the path of the rlz lock file, also creates the parent directory
+// path if it doesn't exist.
+NSString* RlzLockFilename() {
+ NSString* const kRlzFile = @"lockfile";
+ return [CreateRlzDirectory() stringByAppendingPathComponent:kRlzFile];
+}
+
} // namespace
ScopedRlzValueStoreLock::ScopedRlzValueStoreLock() {
- // TODO(thakis): Try to take a recursive cross-process lock for 5 minutes,
- // set store_ to NULL if that fails and return.
+ bool got_distributed_lock =
+ g_recursive_lock.TryGetCrossProcessLock(RlzLockFilename());
+ // At this point, we hold the in-process lock, no matter the value of
+ // |got_distributed_lock|.
++g_lock_depth;
if (g_lock_depth > 1) {
// Reuse the already existing store object.
+ // All user code early-exits when a lock fails, so a recursive lock will
+ // never end up with |g_store_object| that is NULL.
+ CHECK(got_distributed_lock);
CHECK(g_store_object);
store_.reset(g_store_object);
return;
}
+ if (!got_distributed_lock) {
+ // Give up. |store_| isn't set, which signals to callers that acquiring
+ // the lock failed. |g_recursive_lock| will be released by the
+ // destructor.
+ return;
+ }
+
CHECK(!g_store_object);
NSString* plist = RlzPlistFilename();
@@ -294,8 +398,9 @@
ScopedRlzValueStoreLock::~ScopedRlzValueStoreLock() {
--g_lock_depth;
+ CHECK(g_lock_depth >= 0);
- if (g_lock_depth) {
+ if (g_lock_depth > 0) {
// Other locks are still using store_, don't free it yet.
ignore_result(store_.release());
return;
@@ -309,6 +414,15 @@
static_cast<RlzValueStoreMac*>(store_.get())->dictionary();
VERIFY([dict writeToFile:RlzPlistFilename() atomically:YES]);
}
+
+ // Check that "store_ set" => "file_lock acquired". The converse isn't true,
+ // for example if the rlz data file can't be read.
+ if (store_.get())
+ CHECK(g_recursive_lock.file_lock_);
+ if (!g_recursive_lock.file_lock_)
+ CHECK(!store_.get());
+
+ g_recursive_lock.ReleaseLock();
}
RlzValueStore* ScopedRlzValueStoreLock::GetStore() {