All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cristalise.kernel.persistency.TransactionManager Maven / Gradle / Ivy

/**
 * This file is part of the CRISTAL-iSE kernel.
 * Copyright (c) 2001-2015 The CRISTAL Consortium. All rights reserved.
 *
 * This library 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 library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; with out 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 library; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 *
 * http://www.fsf.org/licensing/licenses/lgpl.html
 */
package org.cristalise.kernel.persistency;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.cristalise.kernel.common.ObjectNotFoundException;
import org.cristalise.kernel.common.PersistencyException;
import org.cristalise.kernel.entity.C2KLocalObject;
import org.cristalise.kernel.entity.agent.JobList;
import org.cristalise.kernel.events.History;
import org.cristalise.kernel.lookup.AgentPath;
import org.cristalise.kernel.lookup.ItemPath;
import org.cristalise.kernel.process.auth.Authenticator;
import org.cristalise.kernel.querying.Query;
import org.cristalise.kernel.utils.Logger;


public class TransactionManager {

    HashMap locks;
    HashMap> pendingTransactions;
    ClusterStorageManager storage;

    public TransactionManager(Authenticator auth) throws PersistencyException {
        storage = new ClusterStorageManager(auth);
        locks = new HashMap();
        pendingTransactions = new HashMap>();
    }

    public boolean hasPendingTransactions() {
        return pendingTransactions.size() > 0;
    }

    public ClusterStorageManager getDb() {
        return storage;
    }

    /**
     * Closing will abort all transactions
     */
    public void close() {
        if (pendingTransactions.size() != 0) {
            Logger.error("There were pending transactions on shutdown. All changes were lost.");
            dumpPendingTransactions(0);
        }
        Logger.msg("Transaction Manager: Closing storages");
        storage.close();
    }

    /**
     * 
     * @param query
     * @return
     * @throws PersistencyException
     */
    public String executeQuery(Query query) throws PersistencyException {
        return storage.executeQuery(query);
    }

    /**
     * Retrieves the ids of the root level of a cluster
     * 
     * @param itemPath the item 
     * @param type the type of the cluster
     * @return array of ids
     * @throws PersistencyException
     */
    public String[] getClusterContents(ItemPath itemPath, ClusterType type) throws PersistencyException {
        return getClusterContents(itemPath, type, null);
    }

    /**
     * Retrieves the ids of the root level of a cluster
     * 
     * @param itemPath the item 
     * @param type the type of the cluster
     * @param locker the transaction key
     * @return array of ids
     * @throws PersistencyException
     */
    public String[] getClusterContents(ItemPath itemPath, ClusterType type, Object locker) throws PersistencyException {
        return getClusterContents(itemPath, type.getName(), locker);
    }

    /**
     * Retrieves the ids of the next level of a cluster
     * 
     * @param itemPath the item 
     * @param path the cluster path
     * @return array of ids
     * @throws PersistencyException
     */
    public String[] getClusterContents(ItemPath itemPath, String path) throws PersistencyException {
        return getClusterContents(itemPath, path, null);
    }

    /**
     * Retrieves the ids of the next level of a cluster
     * Checks the transaction table first to see if the caller has uncommitted changes
     * 
     * @param itemPath the item 
     * @param path the cluster path
     * @param locker the transaction key
     * @return array of ids
     * @throws PersistencyException
     */
    public String[] getClusterContents(ItemPath itemPath, String path, Object locker) throws PersistencyException {
        if (path.startsWith("/") && path.length() > 1) path = path.substring(1);

        List uncomittedContents = new ArrayList<>();

        if (locks.containsKey(itemPath) && locks.get(itemPath).equals(locker)) {
            for (TransactionEntry thisEntry : pendingTransactions.get(locker)) {
                if (itemPath.equals(thisEntry.itemPath) && thisEntry.path.startsWith(path)) {
                    if (thisEntry.obj == null)
                        throw new PersistencyException("TransactionManager.get() - Cluster " + path + " has been deleted in " + itemPath +
                                " but not yet committed");
                    String content = StringUtils.substringAfterLast(thisEntry.path, "/");
                    uncomittedContents.add(content);
                }
            }
        }

        return ArrayUtils.addAll(
                storage.getClusterContents(itemPath, path), 
                uncomittedContents.toArray(new String[uncomittedContents.size()])
        );
    }

    /**
     * Public get method. Required a 'locker' object for a transaction key.
     * Checks the transaction table first to see if the caller has uncommitted changes
     */
    public C2KLocalObject get(ItemPath itemPath, String path, Object locker)
            throws PersistencyException, ObjectNotFoundException
    {
        if (path.startsWith("/") && path.length() > 1) path = path.substring(1);

        // deal out top level remote maps, if transactions aren't needed
        if (path.indexOf('/') == -1) {
            if (path.equals(ClusterType.HISTORY) && locker != null) {
                return new History(itemPath, locker);
            }
            else if (path.equals(ClusterType.JOB) && locker != null) {
                if (itemPath instanceof AgentPath) return new JobList((AgentPath)itemPath, locker);
                else                               throw new ObjectNotFoundException("TransactionManager.get() - Items do not have job lists");
            }
        }

        // check to see if the locker has been modifying this cluster
        if (locks.containsKey(itemPath) && locks.get(itemPath).equals(locker)) {
            ArrayList lockerTransaction = pendingTransactions.get(locker);
            for (TransactionEntry thisEntry : lockerTransaction) {
                if (itemPath.equals(thisEntry.itemPath) && path.equals(thisEntry.path)) {
                    if (thisEntry.obj == null)
                        throw new PersistencyException("TransactionManager.get() - Cluster " + path + " has been deleted in " + itemPath +
                                " but not yet committed");
                    return thisEntry.obj;
                }
            }
        }
        return storage.get(itemPath, path);
    }

    public void put(ItemPath itemPath, C2KLocalObject obj, Object locker) throws PersistencyException {
        ArrayList lockingTransaction = getLockingTransaction(itemPath, locker);

        if (lockingTransaction == null) {
            storage.put(itemPath, obj);
            locks.remove(itemPath);
        }
        else
            createTransactionEntry(itemPath, obj, null, lockingTransaction);
    }

    /**
     * Uses the put method, with null as the object value.
     */
    public void remove(ItemPath itemPath, String path, Object locker) throws PersistencyException {
        ArrayList lockingTransaction = getLockingTransaction(itemPath, locker);

        if (lockingTransaction == null) {
            storage.remove(itemPath, path);
            locks.remove(itemPath);
        }
        else
            createTransactionEntry(itemPath, null, path, lockingTransaction);
    }

    /**
     * Manages the transaction table keyed by the object 'locker'.
     * If this object is null, transaction support is bypassed (so long as no lock exists on that object).
     * 
     * @param itemPath
     * @param locker
     * @return the list of transaction corresponds to that lock object
     * @throws PersistencyException
     */
    private ArrayList getLockingTransaction(ItemPath itemPath, Object locker) throws PersistencyException {
        ArrayList lockerTransaction;
        synchronized(locks) {
            // look to see if this object is already locked
            if (locks.containsKey(itemPath)) {
                // if it's this locker, get the transaction list
                Object thisLocker = locks.get(itemPath);
                
                if (thisLocker.equals(locker)) // retrieve the transaction list
                    lockerTransaction = pendingTransactions.get(locker);
                else // locked by someone else
                    throw new PersistencyException("Access denied: '"+itemPath+"' has been locked for writing by "+thisLocker);
            }
            else { // no locks for this item
                if (locker == null) { // lock the item until the non-transactional put/remove is complete :/
                    locks.put(itemPath, new Object());
                    lockerTransaction = null;
                }
                else { // initialise the transaction
                    locks.put(itemPath, locker);
                    lockerTransaction = new ArrayList();
                    pendingTransactions.put(locker, lockerTransaction);
                }
            }
        }
        return lockerTransaction;
    }

    /**
     * Create the new entry in the transaction table.
     * equals() in TransactionEntry only compares sysKey and path, so we can use contains()
     * in ArrayList to look for existing entries for this cluster and overwrite them.
     * 
     * @param itemPath
     * @param obj
     * @param lockerTransaction
     * @throws PersistencyException 
     */
    private void createTransactionEntry(ItemPath itemPath, C2KLocalObject obj, String  path, ArrayList lockerTransaction) throws PersistencyException {
        TransactionEntry newEntry;

        if (obj != null)      newEntry = new TransactionEntry(itemPath, obj);
        else if(path != null) newEntry = new TransactionEntry(itemPath, path);
        else                  throw new PersistencyException("");

        if (lockerTransaction.contains(newEntry)) lockerTransaction.remove(newEntry);

        lockerTransaction.add(newEntry);
    }

    /**
     * Removes all child objects from the given path
     *
     * @param itemPath - Item to delete from
     * @param path - root path to delete
     * @param locker - locking object
     *
     * @throws PersistencyException - when deleting fails
     */
    public void removeCluster(ItemPath itemPath, String path, Object locker) throws PersistencyException {
        String[] children = getClusterContents(itemPath, path);
        
        for (String element : children)
            removeCluster(itemPath, path+(path.length()>0?"/":"")+element, locker);
        
        if (children.length==0 && path.indexOf("/") > -1)
            remove(itemPath, path, locker);
    }
    /**
     * Writes all pending changes to the backends.
     * 
     * @param locker transaction locker
     */
    public void commit(Object locker) {
        synchronized(locks) {
            ArrayList lockerTransactions = pendingTransactions.get(locker);
            HashMap exceptions = new HashMap();
            // quit if no transactions are present;
            if (lockerTransactions == null) return;
            storage.begin(locker);

            for (TransactionEntry thisEntry : lockerTransactions) {
                try {
                    if (thisEntry.obj == null) storage.remove(thisEntry.itemPath, thisEntry.path, locker);
                    else                       storage.put(thisEntry.itemPath, thisEntry.obj, locker);

                    locks.remove(thisEntry.itemPath);
                }
                catch (Exception e) {
                    exceptions.put(thisEntry, e);
                }
            }
            pendingTransactions.remove(locker);
            
            if (exceptions.size() > 0) { // oh dear
                storage.abort(locker);
                Logger.error("TransactionManager.commit() - Problems during transaction commit of locker "+locker.toString()+". Database may be in an inconsistent state.");
                for (TransactionEntry entry : exceptions.keySet()) {
                    Exception ex = exceptions.get(entry);
                    Logger.msg(entry.toString());
                    Logger.error(ex);
                }
                dumpPendingTransactions(0);
                Logger.die("Database failure during commit");
            }
            try {
                storage.commit(locker);
            }
            catch (PersistencyException e) {
                storage.abort(locker);
                Logger.die("Transactional database failure");
            }
        }
    }

    /**
     * Rolls back all changes sent in the name of 'locker' and unlocks the sysKeys
     * 
     * @param locker transaction locker
     */
    public void abort(Object locker) {
        synchronized(locks) {
            if (locks.containsValue(locker)) {
                for (ItemPath thisPath : locks.keySet()) {
                    if (locks.get(thisPath).equals(locker))
                        locks.remove(thisPath);
                }
            }
            pendingTransactions.remove(locker);
        }
    }

    public void clearCache(ItemPath itemPath, String path) {
        if (itemPath == null)  storage.clearCache();
        else if (path == null) storage.clearCache(itemPath);
        else                   storage.clearCache(itemPath, path);
    }

    public void dumpPendingTransactions(int logLevel) {
        if(!Logger.doLog(logLevel)) return;

        Logger.msg(logLevel, "================");
        Logger.msg(logLevel, "Transaction dump");
        Logger.msg(logLevel, "Locked Items:");
        
        if (locks.size() == 0)
            Logger.msg(logLevel, "  None");
        else
            for (ItemPath thisPath : locks.keySet()) {
                Object locker = locks.get(thisPath);
                Logger.msg(logLevel, "  "+thisPath+" locked by "+locker);
            }

        Logger.msg(logLevel, "Open transactions:");
        if (pendingTransactions.size() == 0)
            Logger.msg(logLevel, "  None");
        else
            for (Object thisLocker : pendingTransactions.keySet()) {
                Logger.msg(logLevel, "  Transaction owner:"+thisLocker);
                ArrayList entries = pendingTransactions.get(thisLocker);
                for (TransactionEntry thisEntry : entries) {
                    Logger.msg(logLevel, "    "+thisEntry.toString());
                }
            }
    }

    /**
     * Used in the transaction table to store details of a put until commit
     */
    static class TransactionEntry {
        public ItemPath itemPath;
        public String path;
        public C2KLocalObject obj;
        public TransactionEntry(ItemPath itemPath, C2KLocalObject obj) {
            this.itemPath = itemPath;
            this.path = ClusterStorage.getPath(obj);
            this.obj = obj;
        }

        public TransactionEntry(ItemPath itemPath, String path) {
            this.itemPath = itemPath;
            this.path = path;
            this.obj = null;
        }

        @Override
        public String toString() {
            StringBuffer report = new StringBuffer();
         
            if (obj == null) report.append("Delete");
            else             report.append("Put "+obj.getClass().getName());

            report.append(" at ").append(path).append(" in ").append(itemPath);
            return report.toString();

        }

        @Override
        public int hashCode() {
            return itemPath.hashCode()*path.hashCode();
        }

        @Override
        public boolean equals(Object other) {
            if (other instanceof TransactionEntry)
                return hashCode() == ((TransactionEntry)other).hashCode();
            return false;
        }
    }

    /**
     * Propagate Gateway connect has finished hook to the storages
     */
    public void postConnect() throws PersistencyException {
        storage.postConnect();
    }

    /**
     * Propagate Bootstrap has finished hook to the storages
     */
    public void postBoostrap() throws PersistencyException{
        storage.postBoostrap();
    }

    /**
     * Propagate start server has finished hook to the storages
     */
    public void postStartServer() throws PersistencyException {
        storage.postStartServer();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy