/*
* Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner,
* Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain,
* Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter,
* Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann,
* Samuel Zweifel
*
* This file is part of Jukefox.
*
* Jukefox is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or any later version. Jukefox is
* distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Jukefox. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.ethz.dcg.jukefox.data.db;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.data.db.SqlDbDataPortal.ISqlDbConnection;
/**
* This helper class supports the SqlDbDataPortal instances by synchronizing the accesses to the db. This is very useful
* for SQLite databases, since they throw an error if two connections simultaneously try to write to the db (or some
* other combinations). This helper holds such conflicting operations back (instead of throwing an exception) and
* schedules them for later.<br/>
* <br/>
* The SqlDbDataPortal is required to acquire a {@link LockType#SHARED}-lock whenever it tries to read from the
* db-connection. Immediate transactions are required to acquire a {@link LockType#RESERVED}-lock and exclusive
* transactions or write operations need to acquire a {@link LockType#EXCLUSIVE}-lock.<br/>
* After the work is done, you need to release each lock. The order does not matter. Older locks kept locked until all
* newer locks are released.<br/>
* <br/>
* If a transaction is started, locks kept locked until the transaction finishes (strict two-phase-locking).
*/
public final class LockHelper {
private final static String TAG = LockHelper.class.getSimpleName();
public enum LockType {
SHARED(0), RESERVED(1), EXCLUSIVE(2);
private int level;
private LockType(int level) {
this.level = level;
}
public int level() {
return level;
}
}
private enum DbLockType {
UNLOCKED(0), // No lock around
SHARED(1), // Only read access locks
RESERVED(2), // Write-in-the-future lock; shared locks can coexist and can be created in the future
PENDING(3), // Write-outstanding lock; waits until all shared locks released. No new shared locks are allowed to be created
EXCLUSIVE(4); // Exclusive lock; no other locks are allowed to coexist
private int level;
private DbLockType(int level) {
this.level = level;
}
public int level() {
return level;
}
}
/**
* Representation of a Thread-dbDataPortal pair which is used as key for all locking.
*/
public class LockHolder {
public final Thread thread;
public final ISqlDbConnection dbConnection;
private LockHolder(Thread thread, ISqlDbConnection connection) {
this.thread = thread;
this.dbConnection = connection;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof LockHolder)) {
return false;
}
final LockHolder other = (LockHolder) obj;
boolean equals = true;
equals &= (thread == null) ? (other.thread == null) : (thread.equals(other.thread));
equals &= (dbConnection == null) ? (other.dbConnection == null) : (dbConnection.equals(other.dbConnection));
return equals;
}
@Override
public int hashCode() {
return thread.hashCode() ^ dbConnection.hashCode();
}
}
public class Lock {
public final LockHolder lockHolder;
public final LockType lockType;
private boolean released = false;
private Lock(LockHolder lockHolder, LockType lockType) {
this.lockHolder = lockHolder;
this.lockType = lockType;
}
/**
* Releases this lock.
* <ul>
* <li>If we are in a transaction or the top of the lock stack is not released yet (a newer lock is still open),
* this lock is just marked as released.</li>
* <li>If the top of the lockStack is marked as released and and locks that are marked as released are
* following, we are releasing them as well (this time for real).</li>
* </ul>
* This results in strict Two-phase-locking.
*/
public void release() {
synchronized (LockHelper.this) {
if (released) {
throw new IllegalStateException("This lock has already been released.");
}
released = true;
if (dbDataPortal.inTransaction()) {
// We are in a transaction
return;
}
// Get our lockStack
Stack<Lock> lockStack = lockStacks.get(lockHolder);
if (lockStack == null) {
throw new IllegalStateException(
"Internal error: Lock has to be released, but the lock stack is null!");
}
if (!lockStack.peek().released) {
// There are locks that are newer than us, but are not released yet
return;
}
// Release all locks which are marked as released
while (!lockStack.isEmpty() && lockStack.peek().released) {
Lock toBeReleasedLock = lockStack.pop();
// Release the lock
switch (toBeReleasedLock.lockType) {
case SHARED:
break;
case RESERVED:
case EXCLUSIVE:
try {
if (exclusiveLockStack.pop() != toBeReleasedLock.lockType) {
throw new IllegalStateException(
"Internal error: Exclusive lock has to be released, but its type does not match the top of the exclusive lock stack!");
}
if (exclusiveLockStack.isEmpty()) {
assert (reservedLockCount == 0) && (exclusiveLockCount == 0);
exclusiveLockHolder = null;
}
if (toBeReleasedLock.lockType == LockType.RESERVED) {
--reservedLockCount;
} else {
--exclusiveLockCount;
}
} catch (EmptyStackException e) {
throw new IllegalStateException(
"Internal error: Exclusive lock has to be released, but the exclusive lock stack is empty!");
}
}
}
// Remove old lock stacks
if (lockStack.isEmpty()) {
lockStacks.remove(lockHolder);
}
// Inform that the lockType changed and the lock may now be free for someone
LockHelper.this.notifyAll();
}
}
}
private final Map<LockHolder, Stack<Lock>> lockStacks = new HashMap<LockHolder, Stack<Lock>>();
private final Set<LockHolder> pendingLockQueue = new HashSet<LockHolder>();
private LockHolder exclusiveLockHolder = null;
private int reservedLockCount = 0; // Number of reserved locks in exclusiveLockStack
private int exclusiveLockCount = 0; // Nuber of exclusive locks in exclusiveLockStack
private final Stack<LockType> exclusiveLockStack = new Stack<LockType>(); // lockType \in {RESERVED, EXCLUSIVE}
private final SqlDbDataPortal<? extends IContentValues> dbDataPortal;
public LockHelper(SqlDbDataPortal<? extends IContentValues> dbDataPortal) {
this.dbDataPortal = dbDataPortal;
}
/**
* Creates a lock of the given level.
*
* @param lockType
* The lock level
* @param dbConnection
* On which connection the lock should be held
* @return The created lock
*/
public synchronized Lock lock(LockType lockType, ISqlDbConnection dbConnection) {
// Create the lockHolder
LockHolder lh = new LockHolder(Thread.currentThread(), dbConnection);
// Get the lock
boolean wait;
do {
// Get the db lock type
final DbLockType dbLock = getDbLockType();
// Check if we are the exclusive lock holder
boolean weAreHoldingExclusive = (exclusiveLockHolder == null) || exclusiveLockHolder.equals(lh);
/* Reduce an EXCLUSIVE lock to a RESERVED one if we are in an IMMEDIATE-transaction. This enables other connections to
* continue reading from the db. */
if ((dbLock == DbLockType.RESERVED) && weAreHoldingExclusive) {
lockType = LockType.RESERVED;
}
wait = false;
switch (lockType) {
case SHARED:
wait = (dbLock.level() > DbLockType.RESERVED.level()) // Exclusive lock around (RESERVED excl.)
&& !weAreHoldingExclusive; // We are not holding it
break;
case RESERVED:
wait = (dbLock.level() >= DbLockType.RESERVED.level()) // Exclusive lock around
&& !weAreHoldingExclusive; // We are not holding it
break;
case EXCLUSIVE:
wait = ((dbLock.level() >= DbLockType.RESERVED.level()) // Exclusive lock around
&& !weAreHoldingExclusive) // We are not holding it
//
|| (!lockStacks.containsKey(lh) && lockStacks.size() > 0) // We hold no locks, others do
|| (lockStacks.containsKey(lh) && lockStacks.size() > 1); // We hold locks, others do as well
if (wait) {
pendingLockQueue.add(lh);
}
break;
}
if (wait) {
try {
wait();
} catch (InterruptedException e) {
Log.w(TAG, e);
}
}
} while (wait);
/* Remove us from the pending queue, because if we are in it, we do not need to be afterwards, since we acquired
* the EXCLUSIVE lock. */
pendingLockQueue.remove(lh);
// Register LockHolder
switch (lockType) {
case SHARED:
break;
case RESERVED:
case EXCLUSIVE:
exclusiveLockHolder = lh;
exclusiveLockStack.add(lockType);
if (lockType == LockType.RESERVED) {
++reservedLockCount;
} else {
++exclusiveLockCount;
}
break;
}
// Add this lock to the lockChain of this lockHolder
Stack<Lock> ourLockChain = lockStacks.get(lh);
if (ourLockChain == null) {
ourLockChain = new Stack<Lock>();
lockStacks.put(lh, ourLockChain);
}
Lock lock = new Lock(lh, lockType);
ourLockChain.add(lock);
return lock;
}
/**
* Returns the lock type the database is actually in.
*
* @return The lock type
*/
private synchronized DbLockType getDbLockType() {
// Find the maximum exclusive lockType
//int maxLockLevel = -1;
LockType exclusiveLockType = null;
if (reservedLockCount > 0) {
exclusiveLockType = LockType.RESERVED;
}
if (exclusiveLockCount > 0) {
exclusiveLockType = LockType.EXCLUSIVE;
}
/*for (LockType lockType : exclusiveLockStack) {
if (lockType.level() > maxLockLevel) {
maxLockLevel = lockType.level();
exclusiveLockType = lockType;
if (lockType == LockType.EXCLUSIVE) {
break; // We can not get higher
}
}
}*/
// Get the dbLockType
if ((exclusiveLockHolder != null) && (exclusiveLockType == LockType.EXCLUSIVE)) {
return DbLockType.EXCLUSIVE;
}
/* Do not promote PENDING if there exists an IMMEDIATE-transaction. This helps to allow SHARED locks
* in parallel to an IMMEDIATE-transaction to reduce waiting times if the IMMEDIATE-transaction takes
* some time. */
if ((exclusiveLockHolder != null) && (exclusiveLockType == LockType.RESERVED)) {
return DbLockType.RESERVED;
}
if (pendingLockQueue.size() > 0) {
return DbLockType.PENDING;
}
if (lockStacks.size() > 0) {
return DbLockType.SHARED;
}
return DbLockType.UNLOCKED;
}
}