/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 10004 $
* $Date: 2009-12-17 11:55:26 +0100 (Don, 17. Dez 2009) $
*/
package helma.objectmodel.db;
import helma.objectmodel.DatabaseException;
import helma.objectmodel.ITransaction;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.*;
import org.apache.commons.logging.Log;
/**
* A subclass of thread that keeps track of changed nodes and triggers
* changes in the database when a transaction is commited.
*/
public class Transactor {
// The associated node manager
NodeManager nmgr;
// List of nodes to be updated
private Map dirtyNodes;
// List of visited clean nodes
private Map cleanNodes;
// List of nodes whose child index has been modified
private Set parentNodes;
// Is a transaction in progress?
private volatile boolean active;
private volatile boolean killed;
// Transaction for the embedded database
protected ITransaction txn;
// Transactions for SQL data sources
private Map sqlConnections;
// Set of SQL connections that already have been verified
private Map testedConnections;
// when did the current transaction start?
private long tstart;
// a name to log the transaction. For HTTP transactions this is the rerquest path
private String tname;
// the thread we're associated with
private Thread thread;
private static final ThreadLocal txtor = new ThreadLocal();
/**
* Creates a new Transactor object.
*
* @param nmgr the NodeManager used to fetch and persist nodes.
*/
private Transactor(NodeManager nmgr) {
this.thread = Thread.currentThread();
this.nmgr = nmgr;
dirtyNodes = new LinkedHashMap();
cleanNodes = new HashMap();
parentNodes = new HashSet();
sqlConnections = new HashMap();
testedConnections = new HashMap();
active = false;
killed = false;
}
/**
* Get the transactor for the current thread or null if none exists.
* @return the transactor associated with the current thread
*/
public static Transactor getInstance() {
return (Transactor) txtor.get();
}
/**
* Get the transactor for the current thread or throw a IllegalStateException if none exists.
* @return the transactor associated with the current thread
* @throws IllegalStateException if no transactor is associated with the current thread
*/
public static Transactor getInstanceOrFail() throws IllegalStateException {
Transactor tx = (Transactor) txtor.get();
if (tx == null)
throw new IllegalStateException("Operation requires a Transactor, " +
"but current thread does not have one.");
return tx;
}
/**
* Get the transactor for the current thread, creating a new one if none exists.
* @param nmgr the NodeManager used to create the transactor
* @return the transactor associated with the current thread
*/
public static Transactor getInstance(NodeManager nmgr) {
Transactor t = (Transactor) txtor.get();
if (t == null) {
t = new Transactor(nmgr);
txtor.set(t);
}
return t;
}
/**
* Mark a Node as modified/created/deleted during this transaction
*
* @param node ...
*/
public void visitDirtyNode(Node node) {
if (node != null) {
Key key = node.getKey();
dirtyNodes.put(key, node);
}
}
/**
* Unmark a Node that has previously been marked as modified during the transaction
*
* @param node ...
*/
public void dropDirtyNode(Node node) {
if (node != null) {
Key key = node.getKey();
dirtyNodes.remove(key);
}
}
/**
* Get a dirty Node from this transaction.
* @param key the key
* @return the dirty node associated with the key, or null
*/
public Node getDirtyNode(Key key) {
return (Node) dirtyNodes.get(key);
}
/**
* Keep a reference to an unmodified Node local to this transaction
*
* @param node the node to register
*/
public void visitCleanNode(Node node) {
if (node != null) {
Key key = node.getKey();
if (!cleanNodes.containsKey(key)) {
cleanNodes.put(key, node);
}
}
}
/**
* Keep a reference to an unmodified Node local to this transaction
*
* @param key the key to register with
* @param node the node to register
*/
public void visitCleanNode(Key key, Node node) {
if (node != null) {
if (!cleanNodes.containsKey(key)) {
cleanNodes.put(key, node);
}
}
}
/**
* Drop a reference to an unmodified Node previously registered with visitCleanNode().
* @param key the key
*/
public void dropCleanNode(Key key) {
cleanNodes.remove(key);
}
/**
* Get a reference to an unmodified Node local to this transaction
*
* @param key ...
*
* @return ...
*/
public Node getCleanNode(Object key) {
return (key == null) ? null : (Node) cleanNodes.get(key);
}
/**
*
*
* @param node ...
*/
public void visitParentNode(Node node) {
parentNodes.add(node);
}
/**
* Returns true if a transaction is currently active.
* @return true if currently a transaction is active
*/
public boolean isActive() {
return active;
}
/**
* Check whether the thread associated with this transactor is alive.
* This is a proxy to Thread.isAlive().
* @return true if the thread running this transactor is currently alive.
*/
public boolean isAlive() {
return thread != null && thread.isAlive();
}
/**
* Register a db connection with this transactor thread.
* @param src the db source
* @param con the connection
*/
public void registerConnection(DbSource src, Connection con) {
sqlConnections.put(src, con);
// we assume a freshly created connection is ok.
testedConnections.put(src, new Long(System.currentTimeMillis()));
}
/**
* Get a db connection that was previously registered with this transactor thread.
* @param src the db source
* @return the connection
*/
public Connection getConnection(DbSource src) {
Connection con = (Connection) sqlConnections.get(src);
Long tested = (Long) testedConnections.get(src);
long now = System.currentTimeMillis();
if (con != null && (tested == null || now - tested.longValue() > 60000)) {
// Check if the connection is still alive by executing a simple statement.
try {
Statement stmt = con.createStatement();
stmt.execute("SELECT 1");
stmt.close();
testedConnections.put(src, new Long(now));
} catch (SQLException sx) {
try {
con.close();
} catch (SQLException ignore) {/* nothing to do */}
return null;
}
}
return con;
}
/**
* Start a new transaction with the given name.
*
* @param name The name of the transaction. This is usually the request
* path for the underlying HTTP request.
*
* @throws Exception ...
*/
public synchronized void begin(String name) throws Exception {
if (killed) {
throw new DatabaseException("Transaction started on killed thread");
} else if (active) {
abort();
}
dirtyNodes.clear();
cleanNodes.clear();
parentNodes.clear();
txn = nmgr.db.beginTransaction();
active = true;
tstart = System.currentTimeMillis();
tname = name;
}
/**
* Commit the current transaction, persisting all changes to DB.
*
* @throws Exception ...
*/
public synchronized void commit() throws Exception {
if (killed) {
throw new DatabaseException("commit() called on killed transactor thread");
} else if (!active) {
return;
}
int inserted = 0;
int updated = 0;
int deleted = 0;
ArrayList insertedNodes = null;
ArrayList updatedNodes = null;
ArrayList deletedNodes = null;
ArrayList modifiedParentNodes = null;
// if nodemanager has listeners collect dirty nodes
boolean hasListeners = nmgr.hasNodeChangeListeners();
if (hasListeners) {
insertedNodes = new ArrayList();
updatedNodes = new ArrayList();
deletedNodes = new ArrayList();
modifiedParentNodes = new ArrayList();
}
if (!dirtyNodes.isEmpty()) {
Object[] dirty = dirtyNodes.values().toArray();
// the set to collect DbMappings to be marked as changed
HashSet dirtyDbMappings = new HashSet();
Log eventLog = nmgr.app.getEventLog();
for (int i = 0; i < dirty.length; i++) {
Node node = (Node) dirty[i];
// update nodes in db
int nstate = node.getState();
if (nstate == Node.NEW) {
nmgr.insertNode(nmgr.db, txn, node);
dirtyDbMappings.add(node.getDbMapping());
node.setState(Node.CLEAN);
// register node with nodemanager cache
nmgr.registerNode(node);
if (hasListeners) {
insertedNodes.add(node);
}
inserted++;
if (eventLog.isDebugEnabled()) {
eventLog.debug("inserted node: " + node.getPrototype() + "/" +
node.getID());
}
} else if (nstate == Node.MODIFIED) {
// only mark DbMapping as dirty if updateNode returns true
if (nmgr.updateNode(nmgr.db, txn, node)) {
dirtyDbMappings.add(node.getDbMapping());
}
node.setState(Node.CLEAN);
// update node with nodemanager cache
nmgr.registerNode(node);
if (hasListeners) {
updatedNodes.add(node);
}
updated++;
if (eventLog.isDebugEnabled()) {
eventLog.debug("updated node: " + node.getPrototype() + "/" +
node.getID());
}
} else if (nstate == Node.DELETED) {
nmgr.deleteNode(nmgr.db, txn, node);
dirtyDbMappings.add(node.getDbMapping());
// remove node from nodemanager cache
nmgr.evictNode(node);
if (hasListeners) {
deletedNodes.add(node);
}
deleted++;
if (eventLog.isDebugEnabled()) {
eventLog.debug("removed node: " + node.getPrototype() + "/" +
node.getID());
}
}
node.clearWriteLock();
}
// set last data change times in db-mappings
// long now = System.currentTimeMillis();
for (Iterator i = dirtyDbMappings.iterator(); i.hasNext(); ) {
DbMapping dbm = (DbMapping) i.next();
if (dbm != null) {
dbm.setLastDataChange();
}
}
}
long now = System.currentTimeMillis();
if (!parentNodes.isEmpty()) {
// set last subnode change times in parent nodes
for (Iterator i = parentNodes.iterator(); i.hasNext(); ) {
Node node = (Node) i.next();
node.markSubnodesChanged();
if (hasListeners) {
modifiedParentNodes.add(node);
}
}
}
if (hasListeners) {
nmgr.fireNodeChangeEvent(insertedNodes, updatedNodes,
deletedNodes, modifiedParentNodes);
}
// clear the node collections
recycle();
if (active) {
active = false;
nmgr.db.commitTransaction(txn);
txn = null;
}
StringBuffer msg = new StringBuffer(tname).append(" done in ")
.append(now - tstart).append(" millis");
if(inserted + updated + deleted > 0) {
msg.append(" [+")
.append(inserted).append(", ~")
.append(updated).append(", -")
.append(deleted).append("]");
}
nmgr.app.logAccess(msg.toString());
// unset transaction name
tname = null;
}
/**
* Abort the current transaction, rolling back all changes made.
*/
public synchronized void abort() {
Object[] dirty = dirtyNodes.values().toArray();
// evict dirty nodes from cache
for (int i = 0; i < dirty.length; i++) {
Node node = (Node) dirty[i];
// Declare node as invalid, so it won't be used by other threads
// that want to write on it and remove it from cache
nmgr.evictNode(node);
node.clearWriteLock();
}
long now = System.currentTimeMillis();
// set last subnode change times in parent nodes
for (Iterator i = parentNodes.iterator(); i.hasNext(); ) {
Node node = (Node) i.next();
node.markSubnodesChanged();
}
// clear the node collections
recycle();
// close any JDBC connections associated with this transactor thread
closeConnections();
if (active) {
active = false;
if (txn != null) {
nmgr.db.abortTransaction(txn);
txn = null;
}
nmgr.app.logAccess(tname + " aborted after " +
(System.currentTimeMillis() - tstart) + " millis");
}
// unset transaction name
tname = null;
}
/**
* Kill this transaction thread. Used as last measure only.
*/
public synchronized void kill() {
killed = true;
thread.interrupt();
// Interrupt the thread if it has not noticed the flag (e.g. because it is busy
// reading from a network socket).
if (thread.isAlive()) {
thread.interrupt();
try {
thread.join(1000);
} catch (InterruptedException ir) {
// interrupted by other thread
}
}
if (thread.isAlive() && "true".equals(nmgr.app.getProperty("requestTimeoutStop"))) {
// still running - check if we ought to stop() it
try {
Thread.sleep(2000);
if (thread.isAlive()) {
// thread is still running, pull emergency break
nmgr.app.logEvent("Stopping Thread for Transactor " + this);
thread.stop();
}
} catch (InterruptedException ir) {
// interrupted by other thread
}
}
}
/**
* Closes all open JDBC connections
*/
public void closeConnections() {
if (sqlConnections != null) {
for (Iterator i = sqlConnections.values().iterator(); i.hasNext();) {
try {
Connection con = (Connection) i.next();
con.close();
nmgr.app.logEvent("Closing DB connection: " + con);
} catch (Exception ignore) {
// exception closing db connection, ignore
}
}
sqlConnections.clear();
testedConnections.clear();
}
}
/**
* Clear collections and throw them away. They may have grown large,
* so the benefit of keeping them (less GC) needs to be weighted against
* the potential increas in memory usage.
*/
private synchronized void recycle() {
// clear the node collections to ease garbage collection
dirtyNodes.clear();
cleanNodes.clear();
parentNodes.clear();
}
/**
* Return the name of the current transaction. This is usually the request
* path for the underlying HTTP request.
*/
public String getTransactionName() {
return tname;
}
/**
* Return a string representation of this Transactor thread
*
* @return ...
*/
public String toString() {
return "Transactor[" + tname + "]";
}
}