com.quinsoft.zeidon.dbhandler.PessimisticLockingViaDb.java Source code

Java tutorial

Introduction

Here is the source code for com.quinsoft.zeidon.dbhandler.PessimisticLockingViaDb.java

Source

/**
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 <http://www.gnu.org/licenses/>.
    
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.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 final Task task;
    private final LodDef lodDef;
    private final 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 final EntityDef rootEntityDef;
    private final Map<EntityDef, QualEntity> qualMap;
    private final ActivateOptions activateOptions;
    private GlobalJavaLock javaLock;

    public PessimisticLockingViaDb(ActivateOptions options, Map<EntityDef, QualEntity> qualMap)
            throws PessimisticLockingException {
        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;

        KeyStringBuilder builder = new KeyStringBuilder();
        QualAttrib qualAttrib = rootQual.qualAttribs.get(0);
        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;
        Exception exception = null;
        for (int i = 0; i < retryCount; i++) {
            try {
                lockOi.commit();
                return;
            } catch (Exception 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 void 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.
    }

    /* (non-Javadoc)
     * @see com.quinsoft.zeidon.dbhandler.PessimisticLockingHandler#releaseLocks(com.quinsoft.zeidon.View)
     */
    @Override
    public void releaseLocks(View view) {
        lockCursor.deleteAll();
        lockOi.commit();
    }

    private static class GlobalJavaLock {
        private final Lock lock = new ReentrantLock();
    }
}