/*
* $Header: /home/cvs/jakarta-slide/src/stores/org/apache/slide/store/txfile/AbstractTxFileStoreService.java,v 1.16 2004/08/09 09:35:20 ozeigermann Exp $
* $Revision: 1.16 $
* $Date: 2004/08/09 09:35:20 $
*
* ====================================================================
*
* Copyright 1999-2002 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.slide.store.txfile;
import org.apache.commons.transaction.file.FileResourceManager;
import org.apache.commons.transaction.file.ResourceManager;
import org.apache.commons.transaction.file.ResourceManagerException;
import org.apache.commons.transaction.util.xa.XidWrapper;
import org.apache.slide.common.*;
import org.apache.slide.macro.ConflictException;
import java.io.File;
import java.util.Hashtable;
import javax.transaction.Status;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.apache.slide.util.logger.TxLogger;
import org.apache.slide.util.logger.Logger;
/**
* Abstract base for transactional file stores.
*
* Even though the <code>XAResource</code> interface is implemented,
* this code does not provide conformance to the JTA/JTS or XA spec.
*
* It is only intended for use inside of Slide's transaction manager.
*
*
*/
public abstract class AbstractTxFileStoreService extends AbstractServiceBase implements Status {
protected static final int DEBUG_LEVEL = Logger.DEBUG;
protected static final String STORE_DIR_PARAMETER = "rootpath";
protected static final String WORK_DIR_PARAMETER = "workpath";
protected static final String TIMEOUT_PARAMETER = "timeout";
protected static final String URLENCODE_PATH_PARAMETER = "url-encode-path";
protected static final String DEBUG_MODE_PARAMETER = "debug";
protected FileResourceManager rm;
protected boolean started = false;
protected String storeDir;
protected String workDir;
// there might be at least one active transaction branch per thread
protected ThreadLocal activeTransactionBranch = new ThreadLocal();
public void setParameters(Hashtable parameters)
throws ServiceParameterErrorException, ServiceParameterMissingException {
storeDir = (String) parameters.get(STORE_DIR_PARAMETER);
workDir = (String) parameters.get(WORK_DIR_PARAMETER);
if (storeDir == null) {
throw new ServiceParameterMissingException(this, STORE_DIR_PARAMETER);
}
if (workDir == null) {
throw new ServiceParameterMissingException(this, WORK_DIR_PARAMETER);
}
new File(storeDir).mkdirs();
new File(workDir).mkdirs();
boolean debug = false;
String debugString = (String) parameters.get(DEBUG_MODE_PARAMETER);
if (debugString != null) {
debug = "true".equals(debugString);
}
boolean urlEncodePath = false;
String urlEncodePathString = (String) parameters.get(URLENCODE_PATH_PARAMETER);
if (urlEncodePathString != null) {
urlEncodePath = "true".equals(urlEncodePathString);
}
rm =
new FileResourceManager(
storeDir,
workDir,
urlEncodePath,
new TxLogger(getLogger(), FileResourceManager.class.getName()),
debug);
getLogger().log(
"File Store configured to " + storeDir + ", working directory " + workDir,
getLogChannel(),
Logger.INFO);
String timeoutString = (String) parameters.get(TIMEOUT_PARAMETER);
if (timeoutString != null) {
try {
int timeout = Integer.parseInt(timeoutString);
rm.setDefaultTransactionTimeout(timeout * 1000);
getLogger().log("Set timeout to " + timeoutString, getLogChannel(), Logger.INFO);
} catch (NumberFormatException nfe) {
getLogger().log(
"Can not set timeout, '" + timeoutString + "' must be an integer!",
getLogChannel(),
Logger.WARNING);
}
}
}
public String toString() {
return "TxFileStore at " + storeDir + " working on " + workDir;
}
public void connect() throws ServiceConnectionFailedException {
try {
rm.start();
started = true;
} catch (ResourceManagerException e) {
throw new ServiceConnectionFailedException(this, e);
}
}
public void disconnect() throws ServiceDisconnectionFailedException {
try {
if (!rm.stop(ResourceManager.SHUTDOWN_MODE_NORMAL)) {
throw new ServiceDisconnectionFailedException(this, "Shut down timed out");
}
started = false;
} catch (ResourceManagerException e) {
throw new ServiceDisconnectionFailedException(this, e);
}
}
public boolean isConnected() throws ServiceAccessException {
return started;
}
public void reset() {
rm.reset();
}
public int getTransactionTimeout() throws XAException {
try {
long msecs = rm.getTransactionTimeout(getActiveTxId());
return Math.round(msecs / (float) 1000);
} catch (ResourceManagerException e) {
throw createXAException(e);
}
}
public boolean setTransactionTimeout(int seconds) throws XAException {
try {
rm.setTransactionTimeout(getActiveTxId(), seconds * 1000);
return true;
} catch (ResourceManagerException e) {
throw createXAException(e);
}
}
public boolean isSameRM(XAResource xares) throws XAException {
return (xares instanceof AbstractTxFileStoreService && ((AbstractTxFileStoreService) xares).rm.equals(this.rm));
}
public synchronized Xid[] recover(int flag) throws XAException {
// do not care as this never is called by Slide
return null;
}
public synchronized void forget(Xid xid) throws XAException {
// there is nothing we remember, so there is nothing to forget
}
public synchronized int prepare(Xid xid) throws XAException {
Object txId = wrap(xid);
Thread currentThread = Thread.currentThread();
getLogger().log(
"Thread " + currentThread + " prepares transaction branch " + txId,
getLogChannel(),
DEBUG_LEVEL);
try {
int status = rm.prepareTransaction(txId);
switch (status) {
case ResourceManager.PREPARE_SUCCESS_READONLY :
return XA_RDONLY;
case ResourceManager.PREPARE_SUCCESS :
return XA_OK;
default :
throw new XAException(XAException.XA_RBROLLBACK);
}
} catch (ResourceManagerException e) {
getLogger().log(
"Thread " + currentThread + " failed to prepare transaction branch " + txId,
e,
getLogChannel(),
Logger.CRITICAL);
throw createXAException(e);
}
}
public synchronized void rollback(Xid xid) throws XAException {
Object txId = wrap(xid);
Thread currentThread = Thread.currentThread();
getLogger().log(
"Thread " + currentThread + " rolls back transaction branch " + txId,
getLogChannel(),
DEBUG_LEVEL);
try {
rm.rollbackTransaction(txId);
activeTransactionBranch.set(null);
} catch (ResourceManagerException e) {
getLogger().log(
"Thread " + currentThread + " failed to roll back transaction branch " + txId,
e,
getLogChannel(),
Logger.CRITICAL);
throw createXAException(e);
}
}
public synchronized void commit(Xid xid, boolean onePhase) throws XAException {
Object txId = wrap(xid);
Thread currentThread = Thread.currentThread();
getLogger().log(
"Thread " + currentThread + " commits transaction branch " + txId,
getLogChannel(),
DEBUG_LEVEL);
try {
if (!onePhase && rm.getTransactionState(txId) != STATUS_PREPARED) {
throw new XAException(XAException.XAER_INVAL);
}
rm.commitTransaction(txId);
activeTransactionBranch.set(null);
} catch (ResourceManagerException e) {
getLogger().log(
"Thread " + currentThread + " failed to commit transaction branch " + txId,
e,
getLogChannel(),
Logger.CRITICAL);
throw createXAException(e);
}
}
public synchronized void end(Xid xid, int flags) throws XAException {
Object txId = wrap(xid);
Thread currentThread = Thread.currentThread();
getLogger().log(
"Thread "
+ currentThread
+ (flags == TMSUSPEND ? " suspends" : flags == TMFAIL ? " fails" : " ends")
+ " work on behalf of transaction branch "
+ txId,
getLogChannel(),
DEBUG_LEVEL);
switch (flags) {
case TMSUSPEND :
activeTransactionBranch.set(null);
break;
case TMFAIL :
try {
rm.markTransactionForRollback(wrap(xid));
} catch (ResourceManagerException e) {
throw createXAException(e);
}
break;
case TMSUCCESS :
// not ineresting for us
break;
}
}
public synchronized void start(Xid xid, int flags) throws XAException {
Object txId = wrap(xid);
Thread currentThread = Thread.currentThread();
getLogger().log(
"Thread "
+ currentThread
+ (flags == TMNOFLAGS ? " starts" : flags == TMJOIN ? " joins" : " resumes")
+ " work on behalf of transaction branch "
+ txId,
getLogChannel(),
DEBUG_LEVEL);
switch (flags) {
// a new transaction
case TMNOFLAGS :
if (getActiveTxId() != null) {
throw new XAException(XAException.XAER_INVAL);
}
try {
rm.startTransaction(txId);
activeTransactionBranch.set(txId);
} catch (ResourceManagerException e) {
throw createXAException(e);
}
break;
case TMJOIN :
if (getActiveTxId() != null) {
throw new XAException(XAException.XAER_INVAL);
}
try {
if (rm.getTransactionState(txId) == STATUS_NO_TRANSACTION) {
throw new XAException(XAException.XAER_INVAL);
}
} catch (ResourceManagerException e) {
throw createXAException(e);
}
activeTransactionBranch.set(txId);
break;
case TMRESUME :
activeTransactionBranch.set(txId);
break;
}
}
public synchronized void throwInternalError(String cause) throws ServiceAccessException {
Object txId = getActiveTxId();
getLogger().log(
"Thread "
+ Thread.currentThread()
+ " marked transaction branch "
+ txId
+ " for rollback. Cause: "
+ cause,
getLogChannel(),
Logger.WARNING);
try {
rm.markTransactionForRollback(txId);
} catch (ResourceManagerException re) {
throw new ServiceAccessException(this, re);
}
throw new ServiceAccessException(this, cause);
}
public synchronized void throwInternalError(Throwable cause) throws ServiceAccessException {
Object txId = getActiveTxId();
getLogger().log(
"Thread "
+ Thread.currentThread()
+ " marked transaction branch "
+ txId
+ " for rollback. Cause: "
+ cause,
getLogChannel(),
Logger.WARNING);
try {
rm.markTransactionForRollback(txId);
} catch (ResourceManagerException re) {
throw new ServiceAccessException(this, re);
}
throw new ServiceAccessException(this, cause);
}
// TODO if error is caused by lock that could not be acquired
// we should try deadlock detection instead of simply rolling back
// if no deadlock is detected, retrying for lock would be preferred method
public synchronized void throwInternalError(Throwable cause, String uri) throws ServiceAccessException {
Object txId = getActiveTxId();
if ((cause instanceof ResourceManagerException)
&& ((ResourceManagerException) cause).getStatus() == ResourceManagerException.ERR_NO_LOCK) {
// XXX strictly speaking, this is incorrect, as we actually did not chck for deadlock,
// but silently assume a deadlock must have been the cause for the failed lock
if (txId != null) {
try {
rm.markTransactionForRollback(txId);
} catch (ResourceManagerException re) {
throw new ServiceAccessException(this, re);
}
}
getLogger().log(
"DEADLOCK VICTIM: Thread "
+ Thread.currentThread()
+ " marked transaction branch "
+ txId
+ " for rollback",
getLogChannel(),
Logger.INFO);
throw new ServiceAccessException(this, new ConflictException(uri));
} else {
getLogger().log(
"Could not process URI '"
+ uri
+ "'! Thread "
+ Thread.currentThread()
+ " marking transaction branch "
+ txId
+ " for rollback",
cause,
getLogChannel(),
Logger.WARNING);
if (txId != null) {
try {
rm.markTransactionForRollback(txId);
} catch (ResourceManagerException re) {
throw new ServiceAccessException(this, re);
}
}
throw new ServiceAccessException(this, cause);
}
}
protected Object getActiveTxId() {
Object txId = activeTransactionBranch.get();
return txId;
}
protected XAException createXAException(ResourceManagerException e) {
if (e.getStatus() == ResourceManagerException.ERR_DUP_TX) {
return new XAException(XAException.XAER_DUPID);
} else if (e.getStatus() == ResourceManagerException.ERR_TXID_INVALID) {
return new XAException(XAException.XAER_NOTA);
} else {
return new XAException(e.toString());
}
}
protected String wrap(Xid xid) {
String sxid = XidWrapper.wrap(xid).toString();
// XXX for tx file store replace :, ', \ and / which might be part of the identifier with chars not
// offensive to any file system specific chars
sxid = sxid.replace('\'', '.').replace('"', '.').replace(':', '.').replace('/', '.').replace('\\', '.');
return sxid;
}
abstract protected String getLogChannel();
}