1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.sqlite.util 17 18 import android.util.Log 19 import androidx.annotation.RestrictTo 20 import java.io.File 21 import java.io.FileOutputStream 22 import java.io.IOException 23 import java.nio.channels.FileChannel 24 import java.util.concurrent.locks.Lock 25 import java.util.concurrent.locks.ReentrantLock 26 27 /** 28 * Utility class for in-process and multi-process key-based lock mechanism for safely doing 29 * synchronized operations. 30 * 31 * Acquiring the lock will be quick if no other thread or process has a lock with the same key. But 32 * if the lock is already held then acquiring it will block, until the other thread or process 33 * releases the lock. Note that the key and lock directory must be the same to achieve 34 * synchronization. 35 * 36 * Locking is done via two levels: 37 * 1. Thread locking within the same JVM process is done via a map of String key to ReentrantLock 38 * objects. 39 * 2. Multi-process locking is done via a lock file whose name contains the key and FileLock 40 * objects. 41 * 42 * Creates a lock with `name` and using `lockDir` as the directory for the lock files. 43 * 44 * @param name the name of this lock. 45 * @param lockDir the directory where the lock files will be located. 46 * @param processLock whether to use file for process level locking or not by default. The behaviour 47 * can be overridden via the [lock] method. 48 */ 49 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 50 public class ProcessLock(name: String, lockDir: File?, private val processLock: Boolean) { <lambda>null51 private val lockFile: File? = lockDir?.let { File(it, "$name.lck") } 52 private val threadLock: Lock = getThreadLock(name) 53 private var lockChannel: FileChannel? = null 54 55 /** 56 * Attempts to grab the lock, blocking if already held by another thread or process. 57 * 58 * @param [processLock] whether to use file for process level locking or not. 59 */ locknull60 public fun lock(processLock: Boolean = this.processLock) { 61 threadLock.lock() 62 if (processLock) { 63 try { 64 if (lockFile == null) { 65 throw IOException("No lock directory was provided.") 66 } 67 // Verify parent dir 68 val parentDir = lockFile.parentFile 69 parentDir?.mkdirs() 70 lockChannel = FileOutputStream(lockFile).channel.apply { lock() } 71 } catch (e: IOException) { 72 lockChannel = null 73 Log.w(TAG, "Unable to grab file lock.", e) 74 } 75 } 76 } 77 78 /** Releases the lock. */ unlocknull79 public fun unlock() { 80 try { 81 lockChannel?.close() 82 } catch (ignored: IOException) {} 83 threadLock.unlock() 84 } 85 86 private companion object { 87 private const val TAG = "SupportSQLiteLock" 88 // in-process lock map 89 private val threadLocksMap: MutableMap<String, Lock> = HashMap() 90 getThreadLocknull91 private fun getThreadLock(key: String): Lock = 92 synchronized(threadLocksMap) { 93 return threadLocksMap.getOrPut(key) { ReentrantLock() } 94 } 95 } 96 } 97