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

org.dellroad.stuff.pobj.PersistentObjectXAResource Maven / Gradle / Ivy


/*
 * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.stuff.pobj;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import javax.validation.ConstraintViolation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * {@link PersistentObject} {@link XAResource} for participation in XA transactions.
 */
class PersistentObjectXAResource implements XAResource {

    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private final PersistentObjectTransactionManager manager;

    PersistentObjectXAResource(PersistentObjectTransactionManager manager) {
        if (manager == null)
            throw new IllegalArgumentException("null manager");
        this.manager = manager;
    }

    @Override
    public void start(Xid xid, int flags) throws XAException {

        // Logging
        if (this.log.isTraceEnabled()) {
            this.log.trace("POBJ XA: start(): xid=" + SimpleXid.toString(xid) + " flags="
              + (flags == TMJOIN ? "TMJOIN" : flags == TMRESUME ? "TMRESUME" : flags == TMNOFLAGS ? "TMNOFLAGS" :
               Integer.toHexString(flags)) + " xaMap=" + this.showXAMap());
        }

        // Get transaction info
        final TxInfo current = this.manager.getCurrentTxInfo();
        final TxInfo info = this.manager.xaMap.get(xid);
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: start(): thread tx=" + current + " xid tx=" + info);

        // Perform start operation
        switch (flags) {
        case TMJOIN:

            // Verify a transaction is associated with the current thread
            if (current == null)
                throw this.buildException(XAException.XAER_OUTSIDE, xid, "no transaction is associated with the current thread");
            this.checkVersion(xid, current);
            break;
        case TMRESUME:

            // Verify transaction is already registered
            if (info == null) {
                throw this.buildException(XAException.XAER_NOTA, xid, "no transaction with XID " + SimpleXid.toString(xid)
                  + " is registered with the transaction manager");
            }

            // Resume transaction
            try {
                this.manager.doResume(new TxWrapper(info), info);
            } catch (Exception e) {
                throw this.buildException(XAException.XAER_RMERR, xid, "can't resume transaction: " + e.getMessage(), e);
            }
            break;
        case TMNOFLAGS:

            // Join existing transaction or create new one
            if (current != null) {

                // Join existing transaction
                if (this.manager.xaMap.putIfAbsent(xid, current) != null) {
                    throw this.buildException(XAException.XAER_DUPID, xid,
                      "a transaction with XID " + SimpleXid.toString(xid) + " is already registered with the transaction manager");
                }
                if (this.log.isTraceEnabled())
                    this.log.trace("POBJ XA: start(): joined existing transaction " + current);
            } else {

                // Set up tx definition
                final DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
                txDef.setName(TransactionSynchronizationManager.getCurrentTransactionName());
                txDef.setReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly());

                // Create new transaction
                try {
                    this.manager.doBegin(new TxWrapper(null), txDef);
                } catch (Exception e) {
                    throw this.buildException(XAException.XAER_RMERR, xid, "can't begin transaction: " + e.getMessage(), e);
                }
                final TxInfo newInfo = this.manager.getCurrentTxInfo();
                newInfo.setXACleanup(true);
                if (this.log.isTraceEnabled())
                    this.log.trace("POBJ XA: start(): created new transaction " + newInfo);
                this.manager.xaMap.put(xid, newInfo);
            }
            break;
        default:
            throw this.buildException(XAException.XAER_INVAL, xid, "invalid flags 0x" + Integer.toHexString(flags));
        }
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: start(): new xaMap=" + this.showXAMap());
    }

    @Override
    public void end(Xid xid, int flags) throws XAException {
        if (this.log.isTraceEnabled()) {
            this.log.trace("POBJ XA: end(): xid=" + SimpleXid.toString(xid) + " flags="
              + (flags == TMSUCCESS ? "TMSUCCESS" : flags == TMFAIL ? "TMFAIL" : flags == TMSUSPEND ? "TMSUSPEND" :
              Integer.toHexString(flags)) + " xaMap=" + this.showXAMap());
        }
        final TxInfo info = this.verifyCurrent(xid);
        this.checkVersion(xid, info);
        switch (flags) {
        case TMSUCCESS:
            break;
        case TMFAIL:
            info.setRollbackOnly(true);
            break;
        case TMSUSPEND:
            try {
                this.manager.doSuspend(new TxWrapper(info));
            } catch (Exception e) {
                throw this.buildException(XAException.XAER_RMERR, xid, "can't resume transaction: " + e.getMessage(), e);
            }
            break;
        default:
            throw this.buildException(XAException.XAER_INVAL, xid, "invalid flags 0x" + Integer.toHexString(flags));
        }
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: end(): new xaMap=" + this.showXAMap());
    }

    @Override
    public int prepare(Xid xid) throws XAException {
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: prepare(): xid=" + SimpleXid.toString(xid) + " xaMap=" + this.showXAMap());

        // Get tx info
        final TxInfo info = this.verifyCurrent(xid);
        this.checkVersion(xid, info);

        // From now on, clean up if any exception is thrown
        info.setXACleanup(true);

        // Handle the read-only case
        if (info.isReadOnly()) {
            this.manager.xaMap.remove(xid);
            this.manager.doCleanupAfterCompletion(new TxWrapper(info));
            if (this.log.isTraceEnabled())
                this.log.trace("POBJ XA: prepare(): " + SimpleXid.toString(xid) + " is read-only");
            return XA_RDONLY;
        }

        // Serialize POBJ to a temporary file
        try {
            this.createXAFile(xid, info.getSnapshot());
        } catch (PersistentObjectVersionException e) {
            throw this.buildException(XAException.XA_RBTRANSIENT,
              xid, "persistent object version has changed: " + e.getMessage(), e);
        } catch (PersistentObjectValidationException e) {
            throw this.buildException(XAException.XA_RBINTEGRITY, xid, "invalid persistent object: " + e.getMessage(), e);
        } catch (PersistentObjectException e) {
            throw this.buildException(XAException.XAER_RMERR, xid, "persistent object error: " + e.getMessage(), e);
        }

        // Done
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: prepare(): new xaMap=" + this.showXAMap());
        return XA_OK;
    }

    @Override
    public void commit(Xid xid, boolean onePhase) throws XAException {

        // Logging
        if (this.log.isTraceEnabled()) {
            this.log.trace("POBJ XA: commit(): xid=" + SimpleXid.toString(xid) + " onePhase="
              + onePhase + " xaMap=" + this.showXAMap());
        }

        // Handle 1PC
        if (onePhase) {
            if (this.prepare(xid) == XA_RDONLY) {
                if (this.log.isTraceEnabled())
                    this.log.trace("POBJ XA: commit() (read-only): new xaMap=" + this.showXAMap());
            }
        }

        // Handle 2PC recovery mode
        final File file = this.getXAFile(xid);
        final TxInfo current = this.manager.getCurrentTxInfo();
        if (current == null) {
            this.log.info("POBJ XA: commit(): no current transaction, assuming recovery for xid=" + SimpleXid.toString(xid));
            if (!file.isFile())
                throw this.buildException(XAException.XA_HEURRB, xid, "XID temporary file `" + file + "' invalid or not found");
            try {
                final T root = PersistentObject.read(this.manager.persistentObject.getDelegate(), file, false);
                this.manager.persistentObject.setRoot(root);
            } catch (PersistentObjectValidationException e) {
                throw this.buildException(XAException.XA_RBINTEGRITY, xid, "invalid persistent object: " + e.getMessage(), e);
            } catch (PersistentObjectException e) {
                throw this.buildException(XAException.XAER_RMERR, xid, "persistent object error: " + e.getMessage(), e);
            }
            this.log.info("POBJ XA: commit(): recovery from `" + file + "' successful");
            return;
        }

        // Handle 2PC normal (non-recovery) mode
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: commit(): normal (non-recovery) commit of current transaction");
        final TxInfo info = this.verifyCurrent(xid);
        try {
            this.manager.persistentObject.setRootInternal(info.getSnapshot().getRoot(),
              0/*info.getSnapshot().getVersion()*/, false, false, true);
        } catch (PersistentObjectException e) {
            throw this.buildException(XAException.XAER_RMERR, xid, "persistent object error: " + e.getMessage(), e);
        } finally {
            file.delete();
            this.manager.xaMap.remove(xid);
            this.manager.doCleanupAfterCompletion(new TxWrapper(info));
        }
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: commit(): new xaMap=" + this.showXAMap());
    }

    @Override
    public void rollback(Xid xid) throws XAException {
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: rollback(): xid=" + SimpleXid.toString(xid) + " xaMap=" + this.showXAMap());
        final TxInfo info = this.verifyCurrent(xid);
        this.manager.xaMap.remove(xid);
        this.removeXAFile(xid);
        this.manager.doRollback(new DefaultTransactionStatus(new TxWrapper(info), false, false, false, false, false));
        this.manager.doCleanupAfterCompletion(new TxWrapper(info));
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: rollback(): new xaMap=" + this.showXAMap());
    }

    @Override
    public void forget(Xid xid) throws XAException {
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: forget(): xid=" + SimpleXid.toString(xid));
        this.manager.xaMap.remove(xid);
        this.removeXAFile(xid);
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: forget(): new xaMap=" + this.showXAMap());
    }

    @Override
    public Xid[] recover(int flag) throws XAException {
        if (this.log.isTraceEnabled()) {
            this.log.trace("POBJ XA: recover(): start=" + ((flag & TMSTARTRSCAN) != 0) + " end=" + ((flag & TMENDRSCAN) != 0)
              + " xaMap=" + this.showXAMap());
        }
        if ((flag & ~(TMSTARTRSCAN | TMENDRSCAN)) != 0)
            throw this.buildException(XAException.XAER_INVAL, null, "invalid flag 0x" + Integer.toHexString(flag));
        if ((flag & TMSTARTRSCAN) == 0)
            return new Xid[0];
        final Xid[] xids;
        try {
            xids = this.getXAFiles();
        } catch (IOException e) {
            throw this.buildException(XAException.XAER_RMERR, null, "error scanning for XA recovery files: " + e.getMessage(), e);
        }
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: recover(): new xaMap=" + this.showXAMap());
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: recover(): returning " + Arrays.asList(xids));
        return xids;
    }

    @Override
    public boolean isSameRM(XAResource res) throws XAException {
        if (!(res instanceof PersistentObjectXAResource))
            return false;
        final PersistentObjectXAResource that = (PersistentObjectXAResource)res;
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: isSameRM(): this.manager=" + this.manager + " that.manager=" + that.manager);
        return that.manager == this.manager;
    }

    @Override
    public int getTransactionTimeout() throws XAException {
        return 0;
    }

    @Override
    public boolean setTransactionTimeout(int timeout) throws XAException {
        return false;
    }

    private TxInfo verifyCurrent(Xid xid) throws XAException {

        // Logging
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: verifying current tx for " + SimpleXid.toString(xid));

        // Get current transaction
        final TxInfo current = this.manager.getCurrentTxInfo();
        if (current == null)
            throw this.buildException(XAException.XAER_OUTSIDE, xid, "no transaction is associated with the current thread");

        // Get transaction corresponding to xid
        final TxInfo info = this.manager.xaMap.get(xid);
        if (info == null) {
            throw this.buildException(XAException.XAER_NOTA, xid, "no transaction with XID " + SimpleXid.toString(xid)
              + " is registered with the transaction manager");
        }

        // Verify they are the same
        if (info != current) {
            throw this.buildException(XAException.XAER_PROTO, xid, "the transaction associated with XID " + SimpleXid.toString(xid)
              + " does not correspond to the transaction associated with the current thread");
        }

        // Done
        return info;
    }

    private void checkVersion(Xid xid, TxInfo info) throws XAException {
        final long expected = info.getSnapshot().getVersion();
        final long actual = this.manager.persistentObject.getVersion();
        if (this.log.isTraceEnabled())
            this.log.trace("POBJ XA: check version: actual=" + actual + " expected=" + expected);
        if (actual != expected) {
            throw this.buildException(XAException.XA_RBTRANSIENT, xid,
              "persistent object version has changed: " + new PersistentObjectVersionException(actual, expected).getMessage());
        }
    }

    private XAException buildException(int errorCode, Xid xid, String message) {
        return this.buildException(errorCode, xid, message, null);
    }

    private XAException buildException(int errorCode, Xid xid, String message, Throwable cause) {
        final TxInfo info = this.manager.getCurrentTxInfo();
        if (this.log.isTraceEnabled()) {
            this.log.trace("POBJ XA: throwing exception:\n  xid=" + SimpleXid.toString(xid) + "\n  info=" + info
              + "\n  code=" + errorCode + "\n  msg=\"" + message + "\"\n  cause=" + cause);
        }
        final XAException e = new XAException(message);
        if (errorCode != 0)
            e.errorCode = errorCode;
        if (cause != null)
            e.initCause(cause);
        if (info != null && info.isXACleanup()) {
            this.manager.xaMap.remove(xid);
            this.manager.doCleanupAfterCompletion(new TxWrapper(info));
        }
        return e;
    }

    private String showXAMap() {
        final StringBuilder buf = new StringBuilder();
        for (Map.Entry> entry : this.manager.xaMap.entrySet())
            buf.append("\n   ").append(SimpleXid.toString(entry.getKey())).append(" -> ").append(entry.getValue());
        return buf.toString();
    }

    private void createXAFile(Xid xid, PersistentObject.Snapshot snapshot) {

        // Get temporary XA file
        final File file = this.getXAFile(xid);

        // Get info
        final T xaRoot = snapshot.getRoot();
        final long expectedVersion = snapshot.getVersion();
        final long actualVersion = this.manager.persistentObject.getVersion();

        // Check version number
        if (expectedVersion != 0 && actualVersion != expectedVersion)
            throw new PersistentObjectVersionException(actualVersion, expectedVersion);

        // Validate the new root
        final Set> violations = this.manager.persistentObject.getDelegate().validate(xaRoot);
        if (!violations.isEmpty())
            throw new PersistentObjectValidationException(violations);

        // Write file
        boolean success = false;
        try {
            PersistentObject.write(xaRoot, this.manager.persistentObject.getDelegate(), file);       // TODO: fsync
            success = true;
        } finally {
            if (!success)
                file.delete();
        }
    }

    private File getXAFile(Xid xid) {
        final File file = this.manager.persistentObject.getFile();
        return new File(file.getParentFile(), String.format("%s.XA.%s", file.getName(), SimpleXid.toString(xid)));
    }

    private boolean removeXAFile(Xid xid) {
        return this.getXAFile(xid).delete();
    }

    private Xid[] getXAFiles() throws IOException {
        final File file = this.manager.persistentObject.getFile();
        final File dir = file.getParentFile();
        final File[] siblings = dir.listFiles();
        if (siblings == null)
            throw new IOException("can't list the contents of directory `" + dir + "'");
        final ArrayList xids = new ArrayList<>();
        for (File sibling : siblings) {
            if (!sibling.isFile())
                continue;
            final Xid xid = SimpleXid.fromString(sibling.getName());
            if (xid != null)
                xids.add(xid);
        }
        return xids.toArray(new Xid[xids.size()]);
    }
}