/*
* (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
* Julien Carsique
*/
package org.nuxeo.runtime.jtajca;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.spi.NamingManager;
import javax.resource.ResourceException;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnectionFactory;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.UserTransaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geronimo.connector.outbound.AbstractConnectionManager;
import org.apache.geronimo.connector.outbound.connectionmanagerconfig.LocalTransactions;
import org.apache.geronimo.connector.outbound.connectionmanagerconfig.PoolingSupport;
import org.apache.geronimo.connector.outbound.connectionmanagerconfig.TransactionSupport;
import org.apache.geronimo.connector.outbound.connectionmanagerconfig.XATransactions;
import org.apache.geronimo.transaction.manager.NamedXAResourceFactory;
import org.apache.geronimo.transaction.manager.RecoverableTransactionManager;
import org.apache.geronimo.transaction.manager.TransactionImpl;
import org.apache.geronimo.transaction.manager.TransactionManagerImpl;
import org.apache.xbean.naming.reference.SimpleReference;
import org.nuxeo.common.logging.SequenceTracer;
import org.nuxeo.common.utils.ExceptionUtils;
import org.nuxeo.runtime.jtajca.NuxeoConnectionManager.ActiveMonitor;
import org.nuxeo.runtime.metrics.MetricsService;
import org.nuxeo.runtime.transaction.TransactionHelper;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
/**
* Internal helper for the Nuxeo-defined transaction manager and connection manager.
* <p>
* This code is called by the factories registered through JNDI, or by unit tests mimicking JNDI bindings.
*/
public class NuxeoContainer {
protected static final Log log = LogFactory.getLog(NuxeoContainer.class);
protected static RecoverableTransactionManager tmRecoverable;
protected static TransactionManager tm;
protected static TransactionSynchronizationRegistry tmSynchRegistry;
protected static UserTransaction ut;
protected static Map<String, ConnectionManagerWrapper> connectionManagers = new ConcurrentHashMap<>(
8, 0.75f, 2);
private static final List<NuxeoContainerListener> listeners = new ArrayList<>();
private static volatile InstallContext installContext;
protected static Context rootContext;
protected static Context parentContext;
protected static String jndiPrefix = "java:comp/env/";
// @since 5.7
protected static final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
protected static final Counter rollbackCount = registry.counter(MetricRegistry.name("nuxeo", "transactions",
"rollbacks"));
protected static final Counter concurrentCount = registry.counter(MetricRegistry.name("nuxeo", "transactions",
"concurrents", "count"));
protected static final Counter concurrentMaxCount = registry.counter(MetricRegistry.name("nuxeo", "transactions",
"concurrents", "max"));
protected static final Timer transactionTimer = registry.timer(MetricRegistry.name("nuxeo", "transactions",
"duration"));
protected static final ConcurrentHashMap<Transaction, Timer.Context> timers = new ConcurrentHashMap<>();
private NuxeoContainer() {
}
public static class InstallContext extends Throwable {
private static final long serialVersionUID = 1L;
public final String threadName;
InstallContext() {
super("Container installation context (" + Thread.currentThread().getName() + ")");
threadName = Thread.currentThread().getName();
}
}
/**
* Install naming and bind transaction and connection management factories "by hand".
*/
protected static void install() throws NamingException {
if (installContext != null) {
throw new RuntimeException("Nuxeo container already installed");
}
installContext = new InstallContext();
log.trace("Installing nuxeo container", installContext);
rootContext = new NamingContext();
parentContext = InitialContextAccessor.getInitialContext();
if (parentContext != null && parentContext != rootContext) {
installTransactionManager(parentContext);
} else {
addDeepBinding(nameOf("TransactionManager"), new Reference(TransactionManager.class.getName(),
NuxeoTransactionManagerFactory.class.getName(), null));
installTransactionManager(rootContext);
}
}
protected static void installTransactionManager(TransactionManagerConfiguration config) throws NamingException {
initTransactionManager(config);
addDeepBinding(rootContext, new CompositeName(nameOf("TransactionManager")), getTransactionManagerReference());
addDeepBinding(rootContext, new CompositeName(nameOf("UserTransaction")), getUserTransactionReference());
}
/**
* Creates and installs in the container a new ConnectionManager.
*
* @param name the repository name
* @param config the pool configuration
* @return the created connection manager
*/
public static synchronized ConnectionManagerWrapper installConnectionManager(
NuxeoConnectionManagerConfiguration config) {
String name = config.getName();
ConnectionManagerWrapper cm = connectionManagers.get(name);
if (cm != null) {
return cm;
}
cm = initConnectionManager(config);
// also bind it in JNDI
if (rootContext != null) {
String jndiName = nameOf("ConnectionManager/".concat(name));
try {
addDeepBinding(rootContext, new CompositeName(jndiName), getConnectionManagerReference(name));
} catch (NamingException e) {
log.error("Cannot bind in JNDI connection manager " + config.getName() + " to name " + jndiName);
}
}
return cm;
}
public static boolean isInstalled() {
return installContext != null;
}
protected static void uninstall() throws NamingException {
if (installContext == null) {
throw new RuntimeException("Nuxeo container not installed");
}
try {
NamingException errors = new NamingException("Cannot shutdown connection managers");
for (ConnectionManagerWrapper cm : connectionManagers.values()) {
try {
cm.dispose();
} catch (RuntimeException cause) {
errors.addSuppressed(cause);
}
}
if (errors.getSuppressed().length > 0) {
log.error("Cannot shutdown some pools", errors);
throw errors;
}
} finally {
log.trace("Uninstalling nuxeo container", installContext);
installContext = null;
rootContext = null;
tm = null;
tmRecoverable = null;
tmSynchRegistry = null;
ut = null;
connectionManagers.clear();
}
}
/**
* @since 5.8
*/
public static void addListener(NuxeoContainerListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
for (Map.Entry<String, ConnectionManagerWrapper> entry : connectionManagers.entrySet()) {
listener.handleNewConnectionManager(entry.getKey(), entry.getValue().cm);
}
}
/**
* @since 5.8
*/
public static void removeListener(NuxeoContainerListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
protected static String detectJNDIPrefix(Context context) {
String name = context.getClass().getName();
if ("org.jnp.interfaces.NamingContext".equals(name)) { // JBoss
return "java:";
} else if ("org.jboss.as.naming.InitialContext".equals(name)) { // Wildfly
return "java:jboss/";
} else if ("org.mortbay.naming.local.localContextRoot".equals(name)) { // Jetty
return "jdbc/";
}
// Standard JEE containers (Nuxeo-Embedded, Tomcat, GlassFish,
// ...
return "java:comp/env/";
}
public static String nameOf(String name) {
return jndiPrefix.concat(name);
}
/**
* Exposes the {@link #rootContext}.
*
* @since 5.7
* @see https://jira.nuxeo.com/browse/NXP-10331
*/
public static Context getRootContext() {
return rootContext;
}
/**
* Bind object in root context. Create needed sub contexts. since 5.6
*/
public static void addDeepBinding(String name, Object obj) throws NamingException {
addDeepBinding(rootContext, new CompositeName(name), obj);
}
protected static void addDeepBinding(Context dir, CompositeName comp, Object obj) throws NamingException {
Name name = comp.getPrefix(1);
if (comp.size() == 1) {
addBinding(dir, name, obj);
return;
}
Context subdir;
try {
subdir = (Context) dir.lookup(name);
} catch (NamingException e) {
subdir = dir.createSubcontext(name);
}
addDeepBinding(subdir, (CompositeName) comp.getSuffix(1), obj);
}
protected static void addBinding(Context dir, Name name, Object obj) throws NamingException {
try {
dir.rebind(name, obj);
} catch (NamingException e) {
dir.bind(name, obj);
}
}
protected static void removeBinding(String name) throws NamingException {
rootContext.unbind(name);
}
/**
* Gets the transaction manager used by the container.
*
* @return the transaction manager
*/
public static TransactionManager getTransactionManager() {
return tm;
}
protected static Reference getTransactionManagerReference() {
return new SimpleReference() {
private static final long serialVersionUID = 1L;
@Override
public Object getContent() throws NamingException {
return NuxeoContainer.getTransactionManager();
}
};
}
/**
* Gets the user transaction used by the container.
*
* @return the user transaction
*/
public static UserTransaction getUserTransaction() {
return ut;
}
protected static Reference getUserTransactionReference() {
return new SimpleReference() {
private static final long serialVersionUID = 1L;
@Override
public Object getContent() throws NamingException {
return getUserTransaction();
}
};
}
/**
* Gets the Nuxeo connection manager used by the container.
*
* @return the connection manager
*/
public static NuxeoConnectionManager getConnectionManager(String repositoryName) {
ConnectionManagerWrapper wrapper = connectionManagers.get(repositoryName);
if (wrapper == null) {
return null;
}
return wrapper.cm;
}
public static void installConnectionManager(ConnectionManagerWrapper wrapper) {
String name = wrapper.config.getName();
if (connectionManagers.containsKey(name)) {
log.error("Connection manager " + name + " already set up", new Exception());
}
connectionManagers.put(name, wrapper);
for (NuxeoContainerListener listener : listeners) {
listener.handleNewConnectionManager(name, wrapper.cm);
}
}
protected static Reference getConnectionManagerReference(final String name) {
return new SimpleReference() {
private static final long serialVersionUID = 1L;
@Override
public Object getContent() throws NamingException {
return getConnectionManager(name);
}
};
}
protected static synchronized TransactionManager initTransactionManager(TransactionManagerConfiguration config) {
TransactionManagerImpl impl = createTransactionManager(config);
tm = impl;
tmRecoverable = impl;
tmSynchRegistry = impl;
ut = new UserTransactionImpl(tm);
return tm;
}
protected static TransactionManagerWrapper wrapTransactionManager(TransactionManager tm) {
if (tm == null) {
return null;
}
if (tm instanceof TransactionManagerWrapper) {
return (TransactionManagerWrapper) tm;
}
return new TransactionManagerWrapper(tm);
}
public static synchronized ConnectionManagerWrapper initConnectionManager(NuxeoConnectionManagerConfiguration config) {
NuxeoConnectionTrackingCoordinator coordinator = new NuxeoConnectionTrackingCoordinator();
NuxeoConnectionManager cm = createConnectionManager(coordinator, config);
ConnectionManagerWrapper cmw = new ConnectionManagerWrapper(coordinator, cm, config);
installConnectionManager(cmw);
return cmw;
}
public static synchronized void disposeConnectionManager(ConnectionManager mgr) {
((ConnectionManagerWrapper) mgr).dispose();
}
// called by reflection from RepositoryReloader
public static synchronized void resetConnectionManager() {
RuntimeException errors = new RuntimeException("Cannot reset connection managers");
for (ConnectionManagerWrapper wrapper : connectionManagers.values()) {
try {
wrapper.reset();
} catch (RuntimeException cause) {
errors.addSuppressed(cause);
}
}
if (errors.getSuppressed().length > 0) {
throw errors;
}
}
public static synchronized void resetConnectionManager(String name) {
connectionManagers.get(name).reset();
}
public static <T> T lookup(String name, Class<T> type) throws NamingException {
if (rootContext == null) {
throw new NamingException("no naming context available");
}
return lookup(rootContext, name, type);
}
public static <T> T lookup(Context context, String name, Class<T> type) throws NamingException {
Object resolved;
try {
resolved = context.lookup(detectJNDIPrefix(context).concat(name));
} catch (NamingException cause) {
if (parentContext == null) {
throw cause;
}
return type.cast(parentContext.lookup(detectJNDIPrefix(parentContext).concat(name)));
}
if (resolved instanceof Reference) {
try {
resolved = NamingManager.getObjectInstance(resolved, new CompositeName(name), rootContext, null);
} catch (NamingException e) {
throw e;
} catch (Exception e) { // stupid JNDI API throws Exception
throw ExceptionUtils.runtimeException(e);
}
}
return type.cast(resolved);
}
protected static void installTransactionManager(Context context) throws NamingException {
TransactionManager actual = lookup(context, "TransactionManager", TransactionManager.class);
if (tm != null) {
return;
}
tm = actual;
tmRecoverable = wrapTransactionManager(tm);
ut = new UserTransactionImpl(tm);
tmSynchRegistry = lookup(context, "TransactionSynchronizationRegistry",
TransactionSynchronizationRegistry.class);
}
protected static ConnectionManagerWrapper lookupConnectionManager(String repositoryName) throws NamingException {
ConnectionManager cm = lookup(rootContext, "ConnectionManager/".concat(repositoryName), ConnectionManager.class);
if (cm instanceof ConnectionManagerWrapper) {
return (ConnectionManagerWrapper) cm;
}
log.warn("Connection manager not a wrapper, check your configuration");
throw new RuntimeException("Connection manager of " + repositoryName
+ " not a wrapper, check your configuration");
}
protected static TransactionManagerImpl createTransactionManager(TransactionManagerConfiguration config) {
if (config == null) {
config = new TransactionManagerConfiguration();
}
try {
return new TransactionManagerImpl(config.transactionTimeoutSeconds);
} catch (XAException e) {
// failed in recovery somewhere
throw new RuntimeException(e.toString(), e);
}
}
/**
* User transaction that uses this container's transaction manager.
*
* @since 5.6
*/
public static class UserTransactionImpl implements UserTransaction {
protected final TransactionManager transactionManager;
public UserTransactionImpl(TransactionManager manager) {
transactionManager = manager;
}
@Override
public int getStatus() throws SystemException {
return transactionManager.getStatus();
}
@Override
public void setRollbackOnly() throws IllegalStateException, SystemException {
transactionManager.setRollbackOnly();
}
@Override
public void setTransactionTimeout(int seconds) throws SystemException {
transactionManager.setTransactionTimeout(seconds);
}
@Override
public void begin() throws NotSupportedException, SystemException {
SequenceTracer.start("tx begin", "#DarkSalmon");
transactionManager.begin();
timers.put(transactionManager.getTransaction(), transactionTimer.time());
concurrentCount.inc();
if (concurrentCount.getCount() > concurrentMaxCount.getCount()) {
concurrentMaxCount.inc();
}
}
@Override
public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException,
RollbackException, SecurityException, SystemException {
SequenceTracer.start("tx commiting", "#de6238");
Transaction transaction = transactionManager.getTransaction();
if (transaction == null) {
throw new IllegalStateException("No transaction associated with current thread");
}
Timer.Context timerContext = timers.remove(transaction);
transactionManager.commit();
if (timerContext != null) {
long elapsed = timerContext.stop();
SequenceTracer.stop("tx commited");
SequenceTracer.stop("tx end "+ elapsed / 1000000 + " ms");
}
concurrentCount.dec();
}
@Override
public void rollback() throws IllegalStateException, SecurityException, SystemException {
SequenceTracer.mark("tx rollbacking");
Transaction transaction = transactionManager.getTransaction();
if (transaction == null) {
throw new IllegalStateException("No transaction associated with current thread");
}
Timer.Context timerContext = timers.remove(transaction);
transactionManager.rollback();
concurrentCount.dec();
if (timerContext != null) {
long elapsed = timerContext.stop();
SequenceTracer.destroy("tx rollbacked " + elapsed / 1000000 + " ms");
}
rollbackCount.inc();
}
}
/**
* Creates a Geronimo pooled connection manager using a Geronimo transaction manager.
* <p>
* The pool uses the transaction manager for recovery, and when using XATransactions for cache + enlist/delist.
*
* @throws NamingException
*/
public static NuxeoConnectionManager createConnectionManager(NuxeoConnectionTrackingCoordinator coordinator,
NuxeoConnectionManagerConfiguration config) {
TransactionSupport transactionSupport = createTransactionSupport(config);
PoolingSupport poolingSupport = createPoolingSupport(config);
NuxeoValidationSupport validationSupport = createValidationSupport(config);
return new NuxeoConnectionManager(config.getActiveTimeoutMinutes()*60*1000, validationSupport, transactionSupport, poolingSupport, null, coordinator, tmRecoverable,
config.getName(), Thread.currentThread().getContextClassLoader());
}
protected static PoolingSupport createPoolingSupport(NuxeoConnectionManagerConfiguration config) {
return new NuxeoPool(config);
}
protected static TransactionSupport createTransactionSupport(NuxeoConnectionManagerConfiguration config) {
if (config.getXAMode()) {
// note: XATransactions -> TransactionCachingInterceptor ->
// ConnectorTransactionContext casts transaction to Geronimo's
// TransactionImpl (from TransactionManagerImpl)
return new XATransactions(config.getUseTransactionCaching(), config.getUseThreadCaching());
}
return LocalTransactions.INSTANCE;
}
protected static NuxeoValidationSupport createValidationSupport(NuxeoConnectionManagerConfiguration config) {
return new NuxeoValidationSupport(config.testOnBorrow, config.testOnReturn);
}
public static class TransactionManagerConfiguration {
public int transactionTimeoutSeconds = 600;
public void setTransactionTimeoutSeconds(int transactionTimeoutSeconds) {
this.transactionTimeoutSeconds = transactionTimeoutSeconds;
}
}
/**
* Wraps a transaction manager for providing a dummy recoverable interface.
*
* @author matic
*/
public static class TransactionManagerWrapper implements RecoverableTransactionManager {
protected TransactionManager tm;
public TransactionManagerWrapper(TransactionManager tm) {
this.tm = tm;
}
@Override
public Transaction suspend() throws SystemException {
return tm.suspend();
}
@Override
public void setTransactionTimeout(int seconds) throws SystemException {
tm.setTransactionTimeout(seconds);
}
@Override
public void setRollbackOnly() throws IllegalStateException, SystemException {
tm.setRollbackOnly();
}
@Override
public void rollback() throws IllegalStateException, SecurityException, SystemException {
tm.rollback();
}
@Override
public void resume(Transaction tobj) throws IllegalStateException, InvalidTransactionException, SystemException {
tm.resume(tobj);
}
@Override
public int getStatus() throws SystemException {
return tm.getStatus();
}
@Override
public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException,
RollbackException, SecurityException, SystemException {
tm.commit();
}
@Override
public void begin() throws SystemException {
try {
tm.begin();
} catch (javax.transaction.NotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public void recoveryError(Exception e) {
throw new UnsupportedOperationException();
}
@Override
public void registerNamedXAResourceFactory(NamedXAResourceFactory factory) {
if (!RecoverableTransactionManager.class.isAssignableFrom(tm.getClass())) {
throw new UnsupportedOperationException();
}
((RecoverableTransactionManager) tm).registerNamedXAResourceFactory(factory);
}
@Override
public void unregisterNamedXAResourceFactory(String factory) {
if (!RecoverableTransactionManager.class.isAssignableFrom(tm.getClass())) {
throw new UnsupportedOperationException();
}
((RecoverableTransactionManager) tm).unregisterNamedXAResourceFactory(factory);
}
@Override
public Transaction getTransaction() throws SystemException {
final Transaction tx = tm.getTransaction();
if (tx instanceof TransactionImpl) {
return tx;
}
return new TransactionImpl(null, null) {
@Override
public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException,
SecurityException, SystemException {
tx.commit();
}
@Override
public void rollback() throws IllegalStateException, SystemException {
tx.rollback();
}
@Override
public synchronized boolean enlistResource(XAResource xaRes) throws IllegalStateException,
RollbackException, SystemException {
return tx.enlistResource(xaRes);
}
@Override
public synchronized boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException,
SystemException {
return super.delistResource(xaRes, flag);
}
@Override
public synchronized void setRollbackOnly() throws IllegalStateException {
try {
tx.setRollbackOnly();
} catch (SystemException e) {
throw new IllegalStateException(e);
}
}
@Override
public void registerInterposedSynchronization(javax.transaction.Synchronization synchronization) {
try {
TransactionHelper.lookupSynchronizationRegistry().registerInterposedSynchronization(
synchronization);
} catch (NamingException e) {;
}
}
};
}
}
/**
* Wraps a Geronimo ConnectionManager and adds a {@link #reset} method to flush the pool.
*/
public static class ConnectionManagerWrapper implements ConnectionManager {
private static final long serialVersionUID = 1L;
protected NuxeoConnectionTrackingCoordinator coordinator;
protected volatile NuxeoConnectionManager cm;
protected final NuxeoConnectionManagerConfiguration config;
public ConnectionManagerWrapper(NuxeoConnectionTrackingCoordinator coordinator, NuxeoConnectionManager cm,
NuxeoConnectionManagerConfiguration config) {
this.coordinator = coordinator;
this.cm = cm;
this.config = config;
}
@Override
public Object allocateConnection(ManagedConnectionFactory managedConnectionFactory,
ConnectionRequestInfo connectionRequestInfo) throws ResourceException {
return cm.allocateConnection(managedConnectionFactory, connectionRequestInfo);
}
public void reset() {
AbstractConnectionManager last = cm;
cm = createConnectionManager(coordinator, config);
try {
last.doStop();
} catch (Exception e) { // stupid Geronimo API throws Exception
throw ExceptionUtils.runtimeException(e);
}
for (NuxeoContainerListener listener : listeners) {
listener.handleConnectionManagerReset(config.getName(), cm);
}
}
public List<ActiveMonitor.TimeToLive> killActiveTimedoutConnections(long clock) {
return cm.activemonitor.killTimedoutConnections(clock);
}
public void dispose() {
for (NuxeoContainerListener listener : listeners) {
listener.handleConnectionManagerDispose(config.getName(), cm);
}
cm.activemonitor.cancelCleanups();
NuxeoContainer.connectionManagers.remove(config.getName());
try {
cm.doStop();
} catch (Exception e) { // stupid Geronimo API throws Exception
throw ExceptionUtils.runtimeException(e);
}
}
public NuxeoConnectionManagerConfiguration getConfiguration() {
return config;
}
public NuxeoConnectionManager getManager() {
return cm;
}
}
public static TransactionSynchronizationRegistry getTransactionSynchronizationRegistry() {
return tmSynchRegistry;
}
}