| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #define C_LUCY_LOCKFILELOCK |
| #include "Lucy/Util/ToolSet.h" |
| |
| #include "Lucy/Store/LockFileLock.h" |
| #include "Lucy/Store/DirHandle.h" |
| #include "Lucy/Store/Folder.h" |
| #include "Lucy/Store/OutStream.h" |
| #include "Lucy/Util/Json.h" |
| #include "Lucy/Util/ProcessID.h" |
| |
| #define LFLOCK_STATE_UNLOCKED 0 |
| #define LFLOCK_STATE_LOCKED_SHARED 1 |
| #define LFLOCK_STATE_LOCKED_EXCLUSIVE 2 |
| |
| static bool |
| S_request(LockFileLockIVARS *ivars, String *lock_path); |
| |
| static bool |
| S_is_locked_exclusive(LockFileLockIVARS *ivars); |
| |
| static bool |
| S_is_locked(LockFileLockIVARS *ivars); |
| |
| static bool |
| S_is_shared_lock_file(LockFileLockIVARS *ivars, String *entry); |
| |
| static bool |
| S_maybe_delete_file(LockFileLockIVARS *ivars, String *path, |
| bool delete_mine, bool delete_other); |
| |
| LockFileLock* |
| LFLock_new(Folder *folder, String *name, String *host, int32_t timeout, |
| int32_t interval, bool exclusive_only) { |
| LockFileLock *self = (LockFileLock*)Class_Make_Obj(LOCKFILELOCK); |
| return LFLock_init(self, folder, name, host, timeout, interval, |
| exclusive_only); |
| } |
| |
| LockFileLock* |
| LFLock_init(LockFileLock *self, Folder *folder, String *name, String *host, |
| int32_t timeout, int32_t interval, bool exclusive_only) { |
| int pid = PID_getpid(); |
| Lock_init((Lock*)self, folder, name, timeout, interval); |
| LockFileLockIVARS *const ivars = LFLock_IVARS(self); |
| ivars->host = (String*)INCREF(host); |
| ivars->lock_path = Str_newf("locks/%o.lock", name); |
| ivars->link_path = Str_newf("%o.%o.%i64", ivars->lock_path, host, |
| (int64_t)pid); |
| ivars->exclusive_only = exclusive_only; |
| return self; |
| } |
| |
| struct lockfile_context { |
| OutStream *outstream; |
| String *json; |
| }; |
| |
| static void |
| S_write_lockfile_json(void *context) { |
| struct lockfile_context *stuff = (struct lockfile_context*)context; |
| size_t size = Str_Get_Size(stuff->json); |
| OutStream_Write_Bytes(stuff->outstream, Str_Get_Ptr8(stuff->json), size); |
| OutStream_Close(stuff->outstream); |
| } |
| |
| bool |
| LFLock_Request_Shared_IMP(LockFileLock *self) { |
| LockFileLockIVARS *const ivars = LFLock_IVARS(self); |
| |
| if (ivars->exclusive_only) { |
| THROW(ERR, "Can't request shared lock if exclusive_only is set"); |
| } |
| |
| if (ivars->state != LFLOCK_STATE_UNLOCKED) { |
| THROW(ERR, "Lock already acquired"); |
| } |
| |
| // TODO: The is_locked test and subsequent file creation is prone to a |
| // race condition. We could protect the whole process with an internal |
| // exclusive lock. |
| |
| if (S_is_locked_exclusive(ivars)) { |
| String *msg = Str_newf("'%o.lock' is locked", ivars->name); |
| Err_set_error((Err*)LockErr_new(msg)); |
| return false; |
| } |
| |
| String *path = NULL; |
| |
| uint32_t i = 0; |
| do { |
| DECREF(path); |
| path = Str_newf("locks/%o-%u32.lock", ivars->name, ++i); |
| } while (Folder_Exists(ivars->folder, path)); |
| |
| if (S_request(ivars, path)) { |
| ivars->shared_lock_path = path; |
| ivars->state = LFLOCK_STATE_LOCKED_SHARED; |
| return true; |
| } |
| else { |
| DECREF(path); |
| return false; |
| } |
| } |
| |
| bool |
| LFLock_Request_Exclusive_IMP(LockFileLock *self) { |
| LockFileLockIVARS *const ivars = LFLock_IVARS(self); |
| |
| if (ivars->state != LFLOCK_STATE_UNLOCKED) { |
| THROW(ERR, "Lock already acquired"); |
| } |
| |
| // TODO: The is_locked test and subsequent file creation is prone to a |
| // race condition. We could protect the whole process with an internal |
| // exclusive lock. |
| |
| if (ivars->exclusive_only |
| ? S_is_locked_exclusive(ivars) |
| : S_is_locked(ivars) |
| ) { |
| String *msg = Str_newf("'%o.lock' is locked", ivars->name); |
| Err_set_error((Err*)LockErr_new(msg)); |
| return false; |
| } |
| |
| if (S_request(ivars, ivars->lock_path)) { |
| ivars->state = LFLOCK_STATE_LOCKED_EXCLUSIVE; |
| return true; |
| } |
| else { |
| return false; |
| } |
| } |
| |
| static bool |
| S_request(LockFileLockIVARS *ivars, String *lock_path) { |
| bool success = false; |
| |
| // Create the "locks" subdirectory if necessary. |
| String *lock_dir_name = SSTR_WRAP_C("locks"); |
| if (!Folder_Exists(ivars->folder, lock_dir_name)) { |
| if (!Lock_make_lock_dir(ivars->folder)) { return false; } |
| } |
| |
| // Prepare to write pid, lock name, and host to the lock file as JSON. |
| Hash *file_data = Hash_new(3); |
| Hash_Store_Utf8(file_data, "pid", 3, |
| (Obj*)Str_newf("%i32", (int32_t)PID_getpid())); |
| Hash_Store_Utf8(file_data, "host", 4, INCREF(ivars->host)); |
| Hash_Store_Utf8(file_data, "name", 4, INCREF(ivars->name)); |
| String *json = Json_to_json((Obj*)file_data); |
| DECREF(file_data); |
| |
| // Write to a temporary file, then use the creation of a hard link to |
| // ensure atomic but non-destructive creation of the lockfile with its |
| // complete contents. |
| |
| OutStream *outstream = Folder_Open_Out(ivars->folder, ivars->link_path); |
| if (!outstream) { |
| ERR_ADD_FRAME(Err_get_error()); |
| DECREF(json); |
| return false; |
| } |
| |
| struct lockfile_context context; |
| context.outstream = outstream; |
| context.json = json; |
| Err *json_error = Err_trap(S_write_lockfile_json, &context); |
| DECREF(outstream); |
| DECREF(json); |
| if (json_error) { |
| Err_set_error(json_error); |
| } |
| else { |
| success = Folder_Hard_Link(ivars->folder, ivars->link_path, |
| lock_path); |
| if (!success) { |
| // TODO: Only return a LockErr if errno == EEXIST, otherwise |
| // return a normal Err. |
| Err *hard_link_err = (Err*)CERTIFY(Err_get_error(), ERR); |
| String *msg = Str_newf("Failed to obtain lock at '%o': %o", |
| lock_path, Err_Get_Mess(hard_link_err)); |
| Err_set_error((Err*)LockErr_new(msg)); |
| } |
| } |
| |
| // Verify that our temporary file got zapped. |
| bool deletion_failed = !Folder_Delete(ivars->folder, ivars->link_path); |
| if (deletion_failed) { |
| String *mess = MAKE_MESS("Failed to delete '%o'", ivars->link_path); |
| Err_throw_mess(ERR, mess); |
| } |
| |
| return success; |
| } |
| |
| void |
| LFLock_Release_IMP(LockFileLock *self) { |
| LockFileLockIVARS *const ivars = LFLock_IVARS(self); |
| |
| if (ivars->state == LFLOCK_STATE_UNLOCKED) { |
| THROW(ERR, "Lock not acquired"); |
| } |
| |
| if (ivars->state == LFLOCK_STATE_LOCKED_EXCLUSIVE) { |
| if (Folder_Exists(ivars->folder, ivars->lock_path)) { |
| S_maybe_delete_file(ivars, ivars->lock_path, true, false); |
| } |
| } |
| else { // Shared lock. |
| if (Folder_Exists(ivars->folder, ivars->shared_lock_path)) { |
| S_maybe_delete_file(ivars, ivars->shared_lock_path, true, false); |
| } |
| |
| // Empty out lock_path. |
| DECREF(ivars->shared_lock_path); |
| ivars->shared_lock_path = NULL; |
| } |
| |
| ivars->state = LFLOCK_STATE_UNLOCKED; |
| } |
| |
| static bool |
| S_is_locked_exclusive(LockFileLockIVARS *ivars) { |
| return Folder_Exists(ivars->folder, ivars->lock_path) |
| && !S_maybe_delete_file(ivars, ivars->lock_path, false, true); |
| } |
| |
| static bool |
| S_is_locked(LockFileLockIVARS *ivars) { |
| if (S_is_locked_exclusive(ivars)) { return true; } |
| |
| // Check for shared lock. |
| |
| String *lock_dir_name = SSTR_WRAP_C("locks"); |
| if (!Folder_Find_Folder(ivars->folder, lock_dir_name)) { |
| return false; |
| } |
| |
| bool locked = false; |
| DirHandle *dh = Folder_Open_Dir(ivars->folder, lock_dir_name); |
| if (!dh) { RETHROW(INCREF(Err_get_error())); } |
| |
| while (DH_Next(dh)) { |
| String *entry = DH_Get_Entry(dh); |
| if (S_is_shared_lock_file(ivars, entry)) { |
| String *candidate = Str_newf("%o/%o", lock_dir_name, entry); |
| if (!S_maybe_delete_file(ivars, candidate, false, true)) { |
| locked = true; |
| } |
| DECREF(candidate); |
| } |
| DECREF(entry); |
| } |
| |
| DECREF(dh); |
| return locked; |
| } |
| |
| static bool |
| S_is_shared_lock_file(LockFileLockIVARS *ivars, String *entry) { |
| // Translation: $match = $entry =~ /^\Q$name-\d+\.lock\z/ |
| bool match = false; |
| |
| // $name |
| if (Str_Starts_With(entry, ivars->name)) { |
| StringIterator *iter = Str_Top(entry); |
| StrIter_Advance(iter, Str_Length(ivars->name)); |
| |
| // Hyphen-minus |
| if (StrIter_Next(iter) == '-') { |
| int32_t code_point = StrIter_Next(iter); |
| |
| // Digit |
| if (code_point >= '0' && code_point <= '9') { |
| // Optional digits |
| do { |
| code_point = StrIter_Next(iter); |
| } while (code_point >= '0' && code_point <= '9'); |
| |
| // ".lock" |
| match = code_point == '.' |
| && StrIter_Starts_With_Utf8(iter, "lock", 4) |
| && StrIter_Advance(iter, SIZE_MAX) == 4; |
| } |
| } |
| |
| DECREF(iter); |
| } |
| |
| return match; |
| } |
| |
| static bool |
| S_maybe_delete_file(LockFileLockIVARS *ivars, String *path, |
| bool delete_mine, bool delete_other) { |
| Folder *folder = ivars->folder; |
| bool success = false; |
| |
| Hash *hash = (Hash*)Json_slurp_json(folder, path); |
| if (hash != NULL && Obj_is_a((Obj*)hash, HASH)) { |
| String *pid_buf = (String*)Hash_Fetch_Utf8(hash, "pid", 3); |
| String *host = (String*)Hash_Fetch_Utf8(hash, "host", 4); |
| String *name = (String*)Hash_Fetch_Utf8(hash, "name", 4); |
| |
| // Match hostname and lock name. |
| if (host != NULL |
| && Str_is_a(host, STRING) |
| && Str_Equals(host, (Obj*)ivars->host) |
| && name != NULL |
| && Str_is_a(name, STRING) |
| && Str_Equals(name, (Obj*)ivars->name) |
| && pid_buf != NULL |
| && Str_is_a(pid_buf, STRING) |
| ) { |
| // Verify that pid is either mine or dead. |
| int pid = (int)Str_To_I64(pid_buf); |
| if ((delete_mine && pid == PID_getpid()) // This process. |
| || (delete_other && !PID_active(pid)) // Dead pid. |
| ) { |
| if (Folder_Delete(folder, path)) { |
| success = true; |
| } |
| else { |
| String *mess |
| = MAKE_MESS("Can't delete '%o'", path); |
| DECREF(hash); |
| Err_throw_mess(ERR, mess); |
| } |
| } |
| } |
| } |
| DECREF(hash); |
| |
| return success; |
| } |
| |
| void |
| LFLock_Destroy_IMP(LockFileLock *self) { |
| LockFileLockIVARS *const ivars = LFLock_IVARS(self); |
| if (ivars->state != LFLOCK_STATE_UNLOCKED) { LFLock_Release(self); } |
| DECREF(ivars->host); |
| DECREF(ivars->lock_path); |
| DECREF(ivars->link_path); |
| SUPER_DESTROY(self, LOCKFILELOCK); |
| } |
| |