/*
GASH 2
DBWriteLock.java
The GANYMEDE object storage system.
Created: 2 July 1996
Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu
-----------------------------------------------------------------------
Ganymede Directory Management System
Copyright (C) 1996-2014
The University of Texas at Austin
Ganymede is a registered trademark of The University of Texas at Austin
Contact information
Web site: http://www.arlut.utexas.edu/gash2
Author Email: ganymede_author@arlut.utexas.edu
Email mailing list: ganymede@arlut.utexas.edu
US Mail:
Computer Science Division
Applied Research Laboratories
The University of Texas at Austin
PO Box 8029, Austin TX 78713-8029
Telephone: (512) 835-3200
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package arlut.csd.ganymede.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*------------------------------------------------------------------------------
class
DBWriteLock
------------------------------------------------------------------------------*/
/**
* <p>A DBWriteLock is a {@link arlut.csd.ganymede.server.DBLock DBLock}
* subclass used to lock one or more {@link
* arlut.csd.ganymede.server.DBObjectBase DBObjectBases} for the
* purposes of committing changes into those bases, preventing any
* other threads from reading or writing to the database while the
* update is being performed. When a DBWriteLock is established on a
* DBObjectBase, the establishing thread suspends until all readers
* currently working in the specified DBObjectBases complete. The
* write lock is then established, and the thread possessing the
* DBWriteLock is free to replace objects in the {@link
* arlut.csd.ganymede.server.DBStore DBStore} with modified copies.</p>
*
* <p>DBWriteLocks are typically created and managed by the code in the
* {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} class. It is
* very important that any thread that obtains a DBWriteLock be
* scrupulous about releasing the lock in a timely fashion once the
* appropriate changes are made in the database.</p>
*
* @see arlut.csd.ganymede.server.DBEditSet
* @see arlut.csd.ganymede.server.DBObjectBase
*/
public final class DBWriteLock extends DBLock {
static final boolean debug = false;
/* -- */
/**
* All DBLock's have an identifier key, which is used to identify
* the lock in the {@link arlut.csd.ganymede.server.DBStore
* DBStore}'s {@link arlut.csd.ganymede.server.DBLockSync
* DBLockSync} object. The establish() methods in the DBLock
* subclasses consult the DBStore.lockSync to make sure that no
* {@link arlut.csd.ganymede.server.DBSession DBSession} ever
* possesses more than one write lock, to prevent deadlocks from
* occuring in the server.
*/
private Object key;
private boolean locked = false;
private boolean inEstablish = false;
private boolean abort = false;
/**
* In order to prevent deadlocks, each individual lock must be
* established on all applicable {@link
* arlut.csd.ganymede.server.DBObjectBase DBObjectBases} at the time
* the lock is initially established. baseSet is the List of
* DBObjectBases that this DBLock is/will be locked on.
*/
private List<DBObjectBase> baseSet;
/**
* Constructor to get a write lock on all the object bases
*/
public DBWriteLock(DBStore store)
{
super(store.lockSync);
this.baseSet = Collections.unmodifiableList(new ArrayList(store.getBases()));
}
/**
* Constructor to get a write lock on a subset of the server's
* object bases.
*/
public DBWriteLock(DBStore store, List<DBObjectBase> baseSet)
{
super(store.lockSync);
this.baseSet = Collections.unmodifiableList(new ArrayList(baseSet));
}
/**
* Returns true if this lock is locked.
*/
@Override public final boolean isLocked()
{
return this.locked;
}
/**
* Returns true if this lock is waiting in establish()
*/
@Override public final boolean isEstablishing()
{
return this.inEstablish;
}
/**
* Returns true if this lock is aborting
*/
@Override public final boolean isAborting()
{
return this.abort;
}
/**
* Returns immutable list of DBObjectBases that this lock is meant
* to cover.
*/
@Override final List<DBObjectBase> getBases()
{
return this.baseSet;
}
@Override final Object getKey()
{
if (isLocked())
{
return key;
}
else
{
return null;
}
}
/**
* <p>A thread that calls establish() will be suspended (waiting on
* the server's {@link arlut.csd.ganymede.server.DBLockSync
* DBLockSync} until all DBObjectBases listed in this DBWriteLock's
* constructor are available to be locked. At that point, the
* thread blocking on establish() will wake up possessing an
* exclusive write lock on the requested DBObjectBases.</p>
*
* <p>It is possible for the establish() to fail completely.. the
* admin console may reject a client whose thread is blocking on
* establish(), for instance, or the server may be shut down. In
* those cases, another thread may call this DBWriteLock's {@link
* arlut.csd.ganymede.server.DBLock#abort() abort()} method, in
* which case establish() will throw an InterruptedException, and
* the lock will not be established.</p>
*
* <p>The possessors of DBLocks are identified by a key Object that
* is provided on the call to {@link
* arlut.csd.ganymede.server.DBLock#establish(java.lang.Object)}. A
* given key may only have one DBWriteLock established at a time,
* but it may have multiple concurrent DBDumpLocks and DBReadLocks
* established if there are no DBWriteLocks held by that key or
* locked with DBObjectBases that overlap this lock request.</p>
*
* @param key An object used in the server to uniquely identify the
* entity internal to Ganymede that is attempting to obtain the
* lock, typically a unique String.
*/
@Override public final void establish(Object key) throws InterruptedException
{
boolean waiting = false;
boolean okay = false;
/* -- */
if (debug)
{
debug(key, "enter");
Ganymede.printCallStack();
}
synchronized (lockSync)
{
if (!lockSync.claimLockKey(key, this))
{
throw new InterruptedException("DBWriteLock.establish(" + key + "): error, lock already held for key");
}
try
{
lockSync.incLocksWaitingCount();
if (debug) debug(key, "added myself to the DBLockSync lockHash.");
// okay, now we've got ourselves recorded as the only lock
// that can be in the process of establishing for this
// key. record that fact so that we can interact with
// another thread trying to release us.
this.key = key;
this.inEstablish = true;
// wait until there are no dumpers queued for establish,
// then queue up for own own turn at our bases
while (!okay)
{
if (debug) debug("establish() looping to get establish permission for " + getBaseNames(baseSet));
if (this.abort)
{
throw new InterruptedException("aborted on command");
}
// if the server is in shutdown or in schema edit, we
// can't proceed. in theory, this check would be
// taken care of above here, but internal sessions
// don't use the login semaphore so that a delayed
// shutdown won't wait around forever for
// Ganymede.internalSession
String disabledMessage = GanymedeServer.lSemaphore.checkEnabled();
if (disabledMessage != null && !disabledMessage.equals("shutdown"))
{
// if the server is in soon-to-be-shutdown mode,
// we'll go ahead and allow the transaction to
// complete. if the server is in schema edit
// mode, or for some other reason is refusing
// logins, we won't allow a write lock. Wouldn't
// be prudent at this juncture.
throw new InterruptedException(disabledMessage);
}
okay = true;
for (DBObjectBase base: baseSet)
{
// we won't queue up on the bases while there are
// dumpers waiting or in effect. once we get on
// the writer wait lists, the dumpers will wait
// for us, but we let dumpers proceed before us if
// they are waiting
if (!base.isWaitingDumperListEmpty() || !base.isDumpLockListEmpty())
{
if (debug)
{
debug("establish() waiting for waiting / queued dumpers on base " + base.getName());
debug("establish() waiting dumperList size: " + base.getWaitingDumperListSize());
debug("establish() dumpLock list size: " + base.getDumpLockListSize());
}
okay = false;
break;
}
}
if (!okay)
{
lockSync.wait(2500);
}
}
if (debug) debug("establish() no dumpers queued");
// okay, there are no dump locks waiting to establish.
// Add ourselves to the ObjectBase write queues. This
// will prevent any read locks from establishing, but any
// readlocks that have already been granted will block us
// later. The idea is for the granted read locks to drain
// away while we block out any new readers until we're
// through
for (DBObjectBase base: baseSet)
{
base.addWaitingWriter(this);
}
waiting = true;
if (debug) debug("establish() added ourselves to the writerList");
// spinwait until we can get into all of the ObjectBases
// note that since we added ourselves to the writer
// queues, we know that any new dump or read locks will
// wait until we finish.. at this point, we're just
// waiting for existing read locks to drain
okay = false;
while (!okay)
{
if (debug) debug("establish() spinning");
if (this.abort)
{
// we've been told to abort by another thread's
// abort() call. get out of dodge.
throw new InterruptedException("aborted on command");
}
okay = true;
for (DBObjectBase base: baseSet)
{
// writers are exclusive.. if any lock of any kind
// is asserted, we can't play
if (base.isLocked())
{
if (debug)
{
if (!base.isReaderEmpty())
{
debug("establish() waiting for readers to release");
}
else if (!base.isDumpLockListEmpty())
{
debug("establish() waiting for dumpers to release");
}
else if (base.isWriteInProgress())
{
debug("establish() waiting for writer to release");
}
}
okay = false;
break;
}
}
if (!okay)
{
lockSync.wait(2500);
continue;
}
// nothing can stop us now
for (DBObjectBase base: baseSet)
{
base.setWriteLock(this);
}
this.locked = true;
lockSync.incLockCount();
}
}
finally
{
lockSync.decLocksWaitingCount();
this.inEstablish = false;
if (waiting)
{
for (DBObjectBase base: baseSet)
{
base.removeWaitingWriter(this);
}
}
if (!this.locked)
{
lockSync.unclaimLockKey(key, this);
this.key = null;
}
lockSync.notifyAll();
}
}
if (debug) debug("establish() got the lock.");
}
/**
* Release this lock on all bases locked
*/
@Override public final void release()
{
if (debug)
{
debug("release() entering");
Ganymede.printCallStack();
}
synchronized (lockSync)
{
// if we are trying to force this lock to go away on behalf of
// a thread distinct from the locking thread, we have to wait
// for that thread's establish method to get clear
while (this.inEstablish)
{
if (debug) debug("release() waiting for inEstablish");
try
{
lockSync.wait(2500);
}
catch (InterruptedException ex)
{
}
}
// note that we have to check locked here or else we might
// accidentally release somebody else's lock below, if we
// called release on a non-locked thread.
if (!locked)
{
if (debug) debug("release() not locked, returning");
return;
}
for (DBObjectBase base: baseSet)
{
base.clearWriteLock(this);
}
this.locked = false;
lockSync.unclaimLockKey(this.key, this);
if (debug) debug("release() released");
this.key = null; // gc
lockSync.decLockCount();
lockSync.notifyAll(); // many readers may want in
}
}
/**
* <p>Withdraw this lock. This method can be called by a thread to
* interrupt a lock establish that is blocked waiting to get
* access to the appropriate set of
* {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} objects. If
* this method is called while another thread is blocked in
* establish(), establish() will throw an InterruptedException.</p>
*
* <p>Once abort() is processed, this lock may never be established.
* Any subsequent calls to estabish() will always throw
* InterruptedException.</p>
*/
@Override public final void abort()
{
synchronized (lockSync)
{
if (debug) debug("abort() aborting");
abort = true;
release(); // blocks
}
}
private void debug(Object key, String message)
{
System.err.println("DBWriteLock(" + key + "): " + message);
}
private void debug(String message)
{
System.err.println("DBWriteLock(" + this.key + "): " + message);
}
}