com.quinsoft.zeidon.dbhandler.PessimisticLockingViaDb Maven / Gradle / Ivy
/**
This file is part of the Zeidon Java Object Engine (Zeidon JOE).
Zeidon JOE 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.
Zeidon JOE 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 Zeidon JOE. If not, see .
Copyright 2009-2015 QuinSoft
*/
package com.quinsoft.zeidon.dbhandler;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import com.quinsoft.zeidon.ActivateOptions;
import com.quinsoft.zeidon.Application;
import com.quinsoft.zeidon.EntityCursor;
import com.quinsoft.zeidon.EntityInstance;
import com.quinsoft.zeidon.Pagination;
import com.quinsoft.zeidon.PessimisticLockingException;
import com.quinsoft.zeidon.Task;
import com.quinsoft.zeidon.UnknownLodDefException;
import com.quinsoft.zeidon.View;
import com.quinsoft.zeidon.ZeidonDbException;
import com.quinsoft.zeidon.ZeidonException;
import com.quinsoft.zeidon.objectdefinition.AttributeDef;
import com.quinsoft.zeidon.objectdefinition.DataRecord;
import com.quinsoft.zeidon.objectdefinition.EntityDef;
import com.quinsoft.zeidon.objectdefinition.LodDef;
import com.quinsoft.zeidon.standardoe.IncrementalEntityFlags;
import com.quinsoft.zeidon.utils.KeyStringBuilder;
/**
* Handles pessimistic View locking by writing records to the DB to lock OIs.
* Assumes that ZPLOCKO LOD exists in the application.
*
* @author DG
*
*/
public class PessimisticLockingViaDb implements PessimisticLockingHandler
{
private Task task;
private LodDef lodDef;
private Application application;
/**
* This is the ZPLOCK OI that has the lock entities that will be written to the DB.
*/
private View lockOi;
private EntityCursor lockCursor;
/**
* If true then we've created the pessimistic lock and nothing needs to be done.
*/
private boolean lockPerformed = false;
private boolean lockedByQual = false;
private EntityDef rootEntityDef;
private Map qualMap;
private ActivateOptions activateOptions;
private GlobalJavaLock javaLock;
@Override
public void initialize( ActivateOptions options, Map qualMap )
{
task = options.getTask();
lodDef = options.getLodDef();
application = lodDef.getApplication();
rootEntityDef = lodDef.getRoot();
this.qualMap = qualMap;
activateOptions = options;
// If we are activating with rolling pagination then replace the root cursor
// with a special one that will attempt to load the next page when required.
Pagination pagingOptions = activateOptions.getPagingOptions();
if ( pagingOptions != null && pagingOptions.isRollingPagination() )
{
throw new ZeidonException( "Pessimistic locking is not supported with rolling pagination."
+ " Use read-only option on the activate." );
}
}
private String getUserName()
{
String user = task.getUserId(); // TODO: We need to get this value from activateOptions.
if ( StringUtils.isBlank( user ) )
user = "unknown";
return user;
}
/**
* Adds a global lock to the lock OI to prevent another task from attempting
* to lock the same LOD.
*/
private void addGlobalLockToLockOi()
{
createLockOi( task );
DataRecord dataRecord = rootEntityDef.getDataRecord();
String tableName = dataRecord.getRecordName();
lockOi.cursor( "ZeidonLock" ).createEntity()
.getAttribute( "LOD_Name" ).setValue ( lodDef.getName() + "-GlobalLock" )
.getAttribute( "KeyValue" ).setValue ( tableName )
.getAttribute( "UserName" ).setValue ( getUserName() )
.getAttribute( "Timestamp" ).setValue ( new Date() )
.getAttribute( "AllowRead" ).setValue ( "N" );
addCallStack( lockOi.cursor( "ZeidonLock" ) );
addHostname( lockOi.cursor( "ZeidonLock" ) );
}
/**
* Adds locking to the
*/
private void addQualLocksToLockOi()
{
// We can only implement this if we have qualification on the keys
// and
QualEntity rootQual = qualMap.get( rootEntityDef );
if ( rootQual == null || ! rootQual.isKeyQualification() )
return;
// Currently we only handle a single key. Someday we could add more.
if ( rootQual.qualAttribs.size() != 1 )
return;
QualAttrib qualAttrib = rootQual.qualAttribs.get( 0 );
// Some day we'd like to handle "IN" but that is not this day.
if ( ! StringUtils.equals( qualAttrib.oper, "=" ) )
return;
KeyStringBuilder builder = new KeyStringBuilder();
builder.appendKey( task, qualAttrib.attributeDef, qualAttrib.value );
lockOi.cursor( "ZeidonLock" ).createEntity()
.getAttribute( "LOD_Name" ).setValue ( lodDef.getName() )
.getAttribute( "KeyValue" ).setValue ( builder.toString() )
.getAttribute( "UserName" ).setValue ( getUserName() )
.getAttribute( "Timestamp" ).setValue ( new Date() )
.getAttribute( "AllowRead" ).setValue ( "Y" );
addCallStack( lockOi.cursor( "ZeidonLock" ) );
addHostname( lockOi.cursor( "ZeidonLock" ) );
// Indicate that the lock of the roots has been performed.
lockPerformed = true;
lockedByQual = true;
}
private View createLockOi( Task task )
{
if ( lockOi != null )
return lockOi;
// See if the locking view exists.
try
{
application.getLodDef( task, "ZPLOCKO" );
}
catch ( UnknownLodDefException e )
{
throw new ZeidonException( "LOD for pessimistic locking (ZPLOCKO) does not exist in the application. " +
"To create one use the Utilities menu in the ER diagram tool." )
.setCause( e );
}
lockOi = task.activateEmptyObjectInstance( "ZPLOCKO", application );
lockCursor = lockOi.cursor( "ZeidonLock" );
return lockOi;
}
private void addRootsToLockOi( View view )
{
EntityDef root = lodDef.getRoot();
// For each root entity, create a locking record in ZPLOCKO
for ( EntityInstance ei : view.cursor( root ).eachEntity() )
{
lockCursor.createEntity()
.getAttribute( "LOD_Name" ).setValue ( lodDef.getName() )
.getAttribute( "KeyValue" ).setValue ( ei.getKeyString() )
.getAttribute( "UserName" ).setValue ( getUserName() )
.getAttribute( "Timestamp" ).setValue ( new Date() )
.getAttribute( "AllowRead" ).setValue ( "Y" );
addCallStack( lockCursor );
addHostname( lockCursor );
}
}
private void addHostname( EntityCursor cursor )
{
EntityDef zeidonLock = cursor.getEntityDef();
AttributeDef hostnameAttr = zeidonLock.getAttribute( "Hostname", false );
if ( hostnameAttr == null || hostnameAttr.isHidden() )
return;
String hostname;
try
{
hostname = InetAddress.getLocalHost().getHostName();
}
catch ( UnknownHostException e )
{
hostname = "unknown";
}
int lth = cursor.getAttribute( "Hostname" ).getAttributeDef().getLength();
if ( hostname.length() > lth )
hostname = hostname.substring( 0, lth - 1 );
cursor.getAttribute( "Hostname" ).setValue( hostname );
}
/**
* Adds a string to the locking table that is a partial call stack.
*
* @param cursor
*/
private void addCallStack( EntityCursor cursor )
{
EntityDef zeidonLock = cursor.getEntityDef();
AttributeDef callStackAttr = zeidonLock.getAttribute( "CallStack", false );
if ( callStackAttr == null || callStackAttr.isHidden() )
return;
StringBuilder sb = new StringBuilder();
int count = 0;
StackTraceElement[] stack = new RuntimeException().getStackTrace();
for ( StackTraceElement element : stack )
{
String classname = element.getClassName();
if ( classname.startsWith( "com.quinsoft.zeidon" ) )
continue;
sb.append( element.toString() ).append( "\n" );
if ( ++count > 5 )
break;
}
// Make sure the string lenth isn't too long.
int lth = cursor.getAttribute( "CallStack" ).getAttributeDef().getLength();
if ( sb.length() > lth )
sb.setLength( lth );
cursor.getAttribute( "CallStack" ).setValue( sb.toString() );
}
// Dunno if we'll ever need this. Saving for now.
@SuppressWarnings("unused")
private void createOiToDropLocks( View view )
{
createLockOi( task );
// For each root entity, create a locking record in ZPLOCKO. Normally we'd activate the locking
// records, delete them from the OI, and then commit it. Instead we will set the incremental
// flags for each entity to DELETE. This will save us the time of doing the activate.
for ( EntityInstance ei : view.cursor( rootEntityDef ).eachEntity() )
{
lockCursor.createEntity()
.getAttribute( "LOD_Name" ).setValue( lodDef.getName() )
.getAttribute( "KeyValue" ).setValue( ei.getKeyString() )
.setIncrementalFlags( IncrementalEntityFlags.DELETED );
}
}
/**
* This gets called when a view is dropped. Release the locks.
*/
@Override
public void viewDropped( View view )
{
releaseLocks( view );
}
private GlobalJavaLock getJavaLock()
{
if ( javaLock == null )
javaLock = lodDef.getCacheMap().getOrCreate( GlobalJavaLock.class );
return javaLock;
}
@Override
public void acquireGlobalLock( View view ) throws PessimisticLockingException
{
createLockOi( task );
addGlobalLockToLockOi();
addQualLocksToLockOi();
// To minimize attempts to write to the DB we'll use a global Java
// lock to single-thread writes for the current JVM.
view.log().trace( "Locking global Java lock" );
getJavaLock().lock.lock();
view.log().trace( "Global Java acquired" );
writeLocks( view );
}
@Override
public void releaseGlobalLock( View view )
{
if ( javaLock == null )
return;
try
{
// Delete the global lock.
lockCursor.setFirst();
lockCursor.deleteEntity();
// If we lockedByQual then we've also locked the entities we tried to activate.
// If the activated view is empty then we didn't find anything so drop all the locks.
if ( lockedByQual && view.isEmpty() )
lockCursor.deleteAll();
lockOi.commit();
}
finally
{
// Make sure we remove the java lock.
javaLock.lock.unlock();
javaLock = null;
view.log().trace( "Global Java unlocked" );
}
}
/**
* This will attempt to write the locks to the DB. It will re-try a few times before
* giving up.
*/
private void writeLocks( View view )
{
int retryCount = 4;
ZeidonDbException exception = null;
for ( int i = 0; i < retryCount; i++ )
{
try
{
lockOi.commit();
return;
}
catch ( ZeidonDbException e )
{
exception = e;
// We'll log message. The level of the message will depend on the # of tries.
switch ( i )
{
case 0:
task.log().debug( "Caught exception writing pessimisic locks on %s. Trying again", lodDef );
break;
case 1:
task.log().info( "Caught exception writing pessimisic locks on %s. Trying again", lodDef );
break;
default:
task.log().warn( "Caught exception writing pessimisic locks on %s. Trying again", lodDef );
lockOi.logObjectInstance();
}
try
{
if ( i < retryCount )
Thread.sleep( 10 * i * i );
}
catch ( InterruptedException e1 )
{
}
}
}
// If we get here then none of the commits succeeded and we're giving up.
throw new PessimisticLockingException( view, "Unable to acquire pessimistic locks" ).setCause( exception );
}
private void acquireLocksFromView( View view )
{
if ( lockPerformed )
return; // We've already locked the view.
createLockOi( task );
addRootsToLockOi( view );
writeLocks( view );
releaseGlobalLock( view );
}
/* (non-Javadoc)
* @see com.quinsoft.zeidon.dbhandler.PessimisticLockingHandler#acquireLocks(releaseLock(com.quinsoft.zeidon.View)
*/
@Override
public void acquireRootLocks( View view ) throws PessimisticLockingException
{
acquireLocksFromView( view );
}
@Override
public boolean acquireOiLocks( View view ) throws PessimisticLockingException
{
// This call is for DB handlers that can't have more than one open connection
// at a time. For normal processing this doesn't do anything. Those DB handlers
// would call acquireLocksFromView( view ) from here.
return true;
}
/* (non-Javadoc)
* @see com.quinsoft.zeidon.dbhandler.PessimisticLockingHandler#releaseLocks(com.quinsoft.zeidon.View)
*/
@Override
public void releaseLocks( View view )
{
for ( EntityInstance ei : lockCursor.allEntities() )
ei.setIncrementalFlags( IncrementalEntityFlags.DELETED );
lockOi.commit();
}
private static class GlobalJavaLock
{
private final Lock lock = new ReentrantLock();
}
@Override
public void dropOutstandingLocks( )
{
createLockOi( task );
addQualLocksToLockOi();
releaseLocks( null );
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy