xdev.db.locking.HybridVirtualTableLock Maven / Gradle / Ivy
/*
* XDEV Application Framework - XDEV Application Framework
* Copyright © 2003 XDEV Software (https://xdev.software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package xdev.db.locking;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import xdev.db.DBConnection;
import xdev.db.DBException;
import xdev.db.DBUtils;
import xdev.db.Transaction;
import xdev.db.jdbc.JDBCDataSource;
import xdev.db.sql.DELETE;
import xdev.db.sql.INSERT;
import xdev.db.sql.SELECT;
import xdev.db.sql.UPDATE;
import xdev.vt.VirtualTable;
import xdev.vt.VirtualTable.VirtualTableRow;
import com.xdev.jadoth.sqlengine.SQL;
/**
* Implementation of {@link PessimisticLock} with the enhancement of the
* optimistic locking approach.
*
* Optimistic locking allows multiple concurrent users access to the database
* whilst the system keeps a copy of the initial-read made by each user.
*
* When a user wants to update a record, the application determines whether
* another user has changed the record since it was last read. The application
* does this by comparing the initial-read held in memory to the database record
* to verify any changes made to the record.
*
* Any discrepancies between the initial-read and the database record violates
* concurrency rules and hence causes the system to disregard any update
* request.
*
* @author XDEV Software (RHHF)
* @author XDEV Software jwill
* @since 4.0
*/
public class HybridVirtualTableLock implements PessimisticLock
{
/**
* Default time to hold a specific lock.
*/
public static final long DEFAULT_TIMEOUT = 60000;
/**
* Default threshold before lock duration is going to end, used to notify
* concerned observers.
*/
public static final long DEFAULT_NOTIFICATION_THRESHOLD = 0;
private final String userString;
private final long userId;
private final String rowIdentifier;
private final String tableName;
private Date lastValidUntil;
private long timeout = DEFAULT_TIMEOUT;
private final PessimisticLockTable lockVt;
protected Map lockTimeoutListenerMap = null;
protected List lockListenerList = new ArrayList();
private final JDBCDataSource dataSource;
private Timer timer;
private final Map listenerTimerMap = new HashMap();
private LockConstructionState constructionState;
private static final String TIMER_NAME_PREFIX = "db-lock-timer: ";
public HybridVirtualTableLock(JDBCDataSource dataSource, VirtualTableRow row)
{
this(row,new UserNameIdentifier(ClientSession.getUserName()),ClientSession.getUserName(),
new DefaultVirtualTableRowIdentifier(row),dataSource);
}
public HybridVirtualTableLock(JDBCDataSource dataSource, VirtualTableRow row, long timeout)
{
this(row,new UserNameIdentifier(ClientSession.getUserName()),ClientSession.getUserName(),
new DefaultVirtualTableRowIdentifier(row),timeout,dataSource);
}
public HybridVirtualTableLock(final VirtualTableRow row, final UserIdentifier userIdentifier,
final String userString, final RowIdentifier rowIdentifier, JDBCDataSource dataSource)
{
this(row,userIdentifier,userString,rowIdentifier,DEFAULT_TIMEOUT,dataSource);
}
public HybridVirtualTableLock(final VirtualTableRow row, final UserIdentifier userIdentifier,
final String userString, final RowIdentifier rowIdentifier, final long timeout,
JDBCDataSource dataSource)
{
this.rowIdentifier = rowIdentifier.getRowIdentifierString();
this.tableName = row.getVirtualTable().getDatabaseAlias();
this.userId = userIdentifier.getUserId();
this.userString = userString;
this.timeout = timeout;
this.lockVt = new PessimisticLockTable(dataSource);
this.dataSource = dataSource;
lockTimeoutListenerMap = new HashMap();
}
/**
* {@inheritDoc}
*/
@Override
public void renewLock(final long additionalTime) throws LockingException,
RowAlreadyLockedException
{
Transaction lockTransaction = null;
try
{
lockTransaction = new Transaction()
{
@Override
protected void write(DBConnection> connection) throws DBException
{
if(isEditable())
{
updateLockDuration(additionalTime);
updateTimeoutListenerNotificationThreshold();
notifyTimeOutListeners(getRemainingTime(),lastValidUntil.getTime(),false);
}
}
};
lockTransaction.execute();
}
catch(DBException e)
{
throw new LockingException("Could not obtain lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
}
private void updateTimeoutListenerNotificationThreshold() throws LockingException
{
LockTimeoutListener[] lockTimeoutListeners = lockTimeoutListenerMap.keySet().toArray(
new LockTimeoutListener[lockTimeoutListenerMap.size()]);
for(LockTimeoutListener listener : lockTimeoutListeners)
{
TimerTask task = createTask(listener);
this.timerNotificationThresholdSchedule(listener,task);
}
}
private void timerNotificationThresholdSchedule(LockTimeoutListener listener, TimerTask task)
throws LockingException
{
if(this.timer != null)
{
long remainingTime = getRemainingTime();
if(remainingTime > listener.getNotificationThreshold())
{
this.timer.schedule(task,remainingTime - listener.getNotificationThreshold());
}
else
{
// XXX exception type?
throw new LockingException(
"Notification threshold should be lower than locks remaining time!");
}
}
}
private PessimisticLockInfo getBlockingLockInfo(VirtualTableRow record)
{
final Date serverTime = record.get(PessimisticLockTable.serverTime);
final Date validUntilServerTime = record.get(PessimisticLockTable.validUntil);
final long millisToTimeout = validUntilServerTime.getTime() - serverTime.getTime();
final Date validUntil = new Date(System.currentTimeMillis() + millisToTimeout);
final PessimisticLockInfo blockingLock = new DefaultPessimisticLockInfo(this.tableName,
validUntil,record);
return blockingLock;
}
/**
* {@inheritDoc}
*/
@Override
public LockConstructionState getLock() throws LockingException, RowAlreadyLockedException
{
this.clearRemainingTimeOutDependencies();
this.timer = new Timer(this.getTimerName());
Transaction lockTransaction = null;
try
{
lockTransaction = new Transaction()
{
@Override
protected void write(DBConnection> connection) throws DBException
{
constructionState = manageLocking();
}
};
lockTransaction.execute();
}
catch(DBException e)
{
throw new LockingException("Could not obtain lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
return constructionState;
}
private VirtualTableRow getFirstBlockingLock() throws LockingException
{
Date serverTime = null;
SELECT select = new SELECT();
select.columns(SQL.STAR);
select.FROM(this.lockVt);
select.WHERE(PessimisticLockTable.tableName.eq("?"))
.AND(PessimisticLockTable.tableRowIdentifier.eq("?"))
.AND(PessimisticLockTable.validUntil.gte("?"))
.AND(PessimisticLockTable.userID.ne("?"));
try
{
serverTime = VirtualTableLockingUtils.getServerTime(this.dataSource);
Object[] param = new Object[4];
param[0] = this.tableName;
param[1] = this.rowIdentifier;
param[2] = serverTime;
param[3] = this.userId;
this.lockVt.clearData();
DBUtils.query(this.lockVt,select,param);
}
catch(DBException e)
{
throw new LockingException("Could not receive lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
if(this.lockVt.getRowCount() > 0)
{
VirtualTableRow blockRow = this.lockVt.getRow(0);
blockRow.set(PessimisticLockTable.serverTime.getName(),serverTime);
this.lockVt.clearData();
return blockRow;
}
return null;
}
private VirtualTableRow getPotentialExistingLockRow() throws LockingException
{
SELECT select = new SELECT();
select.columns(SQL.STAR);
select.FROM(this.lockVt);
select.WHERE(PessimisticLockTable.tableName.eq("?"))
.AND(PessimisticLockTable.tableRowIdentifier.eq("?"))
.AND(PessimisticLockTable.userID.eq("?"));
try
{
Object[] param = new Object[3];
param[0] = this.tableName;
param[1] = this.rowIdentifier;
param[2] = this.userId;
this.lockVt.clearData();
DBUtils.query(this.lockVt,select,param);
}
catch(DBException e)
{
throw new LockingException("Could get lock for table: " + this.tableName + " row: "
+ this.rowIdentifier,e);
}
if(this.lockVt.getRowCount() > 0)
{
VirtualTableRow existingRow = this.lockVt.getRow(0);
this.lockVt.clearData();
return existingRow;
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void removeLock() throws LockingException
{
Transaction lockTransaction = null;
try
{
lockTransaction = new Transaction()
{
@Override
protected void write(DBConnection> connection) throws DBException
{
if(isUserValid())
{
removeLockEntry();
}
}
};
lockTransaction.execute();
}
catch(DBException e)
{
throw new LockingException("Could not obtain lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
}
protected void removeLockEntry() throws LockingException
{
DELETE delete = new DELETE();
delete.FROM(this.lockVt);
delete.WHERE(PessimisticLockTable.tableName.eq("?"))
.AND(PessimisticLockTable.tableRowIdentifier.eq("?"))
.AND(PessimisticLockTable.userID.eq("?"));
Object[] paramsDelete = new Object[3];
paramsDelete[0] = this.tableName;
paramsDelete[1] = this.rowIdentifier;
paramsDelete[2] = this.userId;
try
{
this.clearRemainingTimeOutDependencies();
DBUtils.write(delete,paramsDelete);
}
catch(DBException e)
{
throw new LockingException("Could not remove lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
}
/**
* Returns the time for how long the lock is still valid.
*
* @return server valid until time
* @throws LockingException
* indicator for connection problems.
*/
public Date getValidUntilTime() throws LockingException
{
this.lockVt.clearData();
SELECT select = new SELECT();
select.columns(PessimisticLockTable.validUntil);
select.FROM(this.lockVt);
select.WHERE(PessimisticLockTable.tableName.eq("?"))
.AND(PessimisticLockTable.tableRowIdentifier.eq("?"))
.AND(PessimisticLockTable.userID.eq("?"));
Object[] param = new Object[3];
param[0] = this.tableName;
param[1] = this.rowIdentifier;
param[2] = this.userId;
try
{
VirtualTable result = DBUtils.query(this.lockVt,select,param);
if(result.getRowCount() > 0)
{
VirtualTableRow activeRow = result.getRow(0);
Date validUntil = (Date)activeRow.get(PessimisticLockTable.validUntil.getName());
return validUntil;
}
}
catch(DBException e)
{
throw new LockingException("Lock for row " + this.rowIdentifier
+ "is no longer valid for user " + this.userString);
}
return null;
}
private void clearRemainingTimeOutDependencies()
{
if(this.timer != null)
{
this.timer.cancel();
this.notifyTimeOutListeners(0,this.getTimeout(),true);
}
}
/**
* {@inheritDoc}
*/
@Override
public long getRemainingTime() throws LockingException
{
Date now;
try
{
now = VirtualTableLockingUtils.getServerTime(this.dataSource);
}
catch(DBException e)
{
// XXX exception type? - like LockingDBException("msg",datasource
// ...);
throw new LockingException("Could not receive server time",e);
}
this.lastValidUntil = this.getValidUntilTime();
if(lastValidUntil != null)
{
if(lastValidUntil.getTime() >= now.getTime())
{
return lastValidUntil.getTime() - now.getTime();
}
}
return 0;
}
// optimistic locking
protected boolean isEditable() throws RowAlreadyLockedException
{
final VirtualTableRow row = getFirstBlockingLock();
if(row != null)
{
// not editable / false
HybridVirtualTableLock.this.notifyTimeOutListeners(0,
HybridVirtualTableLock.this.getTimeout(),true);
final PessimisticLockInfo acquiringLockInfo = getLockInfo();
throw new RowAlreadyLockedException(new RowAlreadyLockedException(
"Could not acquire lock " + this.toString(),acquiringLockInfo,
getBlockingLockInfo(row)));
}
return true;
}
protected LockConstructionState manageLocking() throws RowAlreadyLockedException,
LockingException
{
LockConstructionState optimisticLockConstructionCaseState = new OptimisticLockConstructionState();
if(this.isEditable())
{
VirtualTableRow potentialExistingRow = this.getPotentialExistingLockRow();
if(potentialExistingRow != null)
{
Date serverTime = null;
try
{
serverTime = VirtualTableLockingUtils.getServerTime(this.dataSource);
}
catch(DBException e)
{
throw new LockingException("Could not receive lock for table: "
+ this.tableName + " row: " + this.rowIdentifier,e);
}
if(serverTime.getTime() > potentialExistingRow.get(PessimisticLockTable.validUntil)
.getTime())
{
this.renewLock(this.getTimeout());
optimisticLockConstructionCaseState.setRegenerated(true);
return optimisticLockConstructionCaseState;
}
}
else
{
this.createLockEntry();
optimisticLockConstructionCaseState.setInitial(true);
return optimisticLockConstructionCaseState;
}
}
return optimisticLockConstructionCaseState;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isUserValid() throws LockingException
{
if(this.getFirstBlockingLock() != null)
{
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void addLockTimeoutListener(final LockTimeoutListener listener) throws LockingException
{
if(this.timer != null)
{
final TimerTask task = createTask(listener);
this.timerNotificationThresholdSchedule(listener,task);
}
}
private TimerTask createTask(final LockTimeoutListener listener)
{
final TimerTask task = new TimerTask()
{
@Override
public void run()
{
listener.timeoutImminent(new LockTimeoutEvent(HybridVirtualTableLock.this));
HybridVirtualTableLock.this.listenerTimerMap.remove(listener);
}
};
this.lockTimeoutListenerMap.put(listener,task);
return task;
}
/**
* {@inheritDoc}
*/
@Override
public void removeLockTimeoutListener(final LockTimeoutListener listener)
{
final TimerTask task = this.lockTimeoutListenerMap.get(listener);
if(task != null)
{
task.cancel();
this.lockTimeoutListenerMap.remove(listener);
}
}
/**
* {@inheritDoc}
*/
@Override
public Set getLockTimeoutListener()
{
return new HashSet(this.lockTimeoutListenerMap.keySet());
}
/**
* {@inheritDoc}
*/
@Override
public long getTimeout()
{
return this.timeout;
}
/**
* {@inheritDoc}
*/
@Override
public PessimisticLockInfo getLockInfo()
{
return new DefaultPessimisticLockInfo(this.tableName,new Date(this.getRemainingTime()
+ new Date().getTime()),this.userString,this.userId,this.rowIdentifier);
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return this.tableName + "[" + this.rowIdentifier + "]";
}
/**
* {@inheritDoc}
*/
@Override
public void addLockTimeoutPropertyChangeListener(LockTimeoutChangeListener listener)
{
this.lockListenerList.add(listener);
}
/**
* {@inheritDoc}
*/
@Override
public List getLockListener()
{
return new ArrayList(this.lockListenerList);
}
/**
* {@inheritDoc}
*/
@Override
public void removeLockTimeoutPropertyChangeListener(LockTimeoutChangeListener listener)
{
this.lockListenerList.remove(listener);
}
/**
*
* @param timeOut
*/
protected void setTimeOut(final long timeOut)
{
// final long oldValue = this.timeout;
this.timeout = timeOut;
// this.notifyTimeOutListeners(timeOut,oldValue,false);
}
protected void notifyTimeOutListeners(final long newValue, final long oldValue, boolean canceled)
{
final LockTimeOutChangeEvent changeEvent = new LockTimeOutChangeEvent(this,oldValue,
newValue,canceled);
for(LockTimeoutChangeListener listener : this.lockListenerList)
{
listener.lockTimeOutChange(changeEvent);
}
}
protected void setTimeOut(final long timeOut, String timerName)
{
final long oldValue = this.timeout;
this.timeout = timeOut;
final LockTimeOutChangeEvent changeEvent = new LockTimeOutChangeEvent(this,oldValue,timeOut);
for(LockTimeoutChangeListener listener : this.lockListenerList)
{
listener.lockTimeOutChange(changeEvent);
}
}
private String getTimerName()
{
return TIMER_NAME_PREFIX + this.toString();
}
protected void updateLockDuration(long additionalTime) throws LockingException
{
Date updatedDate = new Date(this.calculateUpdatedRemainingTime(additionalTime));
UPDATE update = new UPDATE(this.lockVt);
update.SET(PessimisticLockTable.validUntil,"?");
update.WHERE(PessimisticLockTable.tableName.eq("?"))
.AND(PessimisticLockTable.tableRowIdentifier.eq("?"))
.AND(PessimisticLockTable.userID.eq("?"));
Object[] paramsInsert = new Object[4];
paramsInsert[0] = updatedDate;
paramsInsert[1] = this.tableName;
paramsInsert[2] = this.rowIdentifier;
paramsInsert[3] = this.userId;
try
{
DBUtils.write(update,true,paramsInsert);
}
catch(DBException e)
{
throw new LockingException("Could not update time for lock corresponding to table: "
+ this.tableName + " row: " + this.rowIdentifier,e);
}
}
private long calculateUpdatedRemainingTime(long additionalTime) throws LockingException
{
long now;
try
{
now = VirtualTableLockingUtils.getServerTime(this.dataSource).getTime();
}
catch(DBException e)
{
throw new LockingException(
"Could not calculate remaining lock time for lock corresponding to: "
+ this.tableName + " row: " + this.rowIdentifier,e);
}
long remainingTime = this.getRemainingTime();
return now + remainingTime + additionalTime;
}
protected void createLockEntry() throws LockingException
{
try
{
Date serverTime = VirtualTableLockingUtils.getServerTime(this.dataSource);
Date validUntilServerTime = new Date(serverTime.getTime() + this.getTimeout());
INSERT insert = new INSERT();
insert.INTO(this.lockVt);
insert.columns(PessimisticLockTable.tableName,PessimisticLockTable.tableRowIdentifier,
PessimisticLockTable.validUntil,PessimisticLockTable.userString,
PessimisticLockTable.userID);
insert.VALUES("?","?","?","?","?");
Object[] paramsInsert = new Object[5];
paramsInsert[0] = this.tableName;
paramsInsert[1] = this.rowIdentifier;
paramsInsert[2] = validUntilServerTime;
paramsInsert[3] = this.userString;
paramsInsert[4] = this.userId;
DBUtils.write(insert,true,paramsInsert);
}
catch(DBException e)
{
throw new LockingException("Could not obtain lock for table: " + this.tableName
+ " row: " + this.rowIdentifier,e);
}
}
}