001/*
002 * (C) Copyright 2006-2020 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 *     Julien Carsique
019 */
020package org.nuxeo.runtime.jtajca;
021
022import java.util.HashMap;
023import java.util.concurrent.ConcurrentHashMap;
024
025import javax.naming.CompositeName;
026import javax.naming.Context;
027import javax.naming.Name;
028import javax.naming.NamingException;
029import javax.naming.Reference;
030import javax.naming.spi.NamingManager;
031import javax.transaction.HeuristicMixedException;
032import javax.transaction.HeuristicRollbackException;
033import javax.transaction.InvalidTransactionException;
034import javax.transaction.NotSupportedException;
035import javax.transaction.RollbackException;
036import javax.transaction.SystemException;
037import javax.transaction.Transaction;
038import javax.transaction.TransactionManager;
039import javax.transaction.TransactionSynchronizationRegistry;
040import javax.transaction.UserTransaction;
041import javax.transaction.xa.XAException;
042import javax.transaction.xa.XAResource;
043
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.apache.geronimo.transaction.manager.NamedXAResourceFactory;
047import org.apache.geronimo.transaction.manager.RecoverableTransactionManager;
048import org.apache.geronimo.transaction.manager.TransactionImpl;
049import org.apache.geronimo.transaction.manager.TransactionManagerImpl;
050import org.apache.geronimo.transaction.manager.XidImpl;
051import org.apache.xbean.naming.reference.SimpleReference;
052import org.nuxeo.common.utils.ExceptionUtils;
053import org.nuxeo.runtime.metrics.MetricsService;
054import org.nuxeo.runtime.transaction.TransactionHelper;
055
056import io.dropwizard.metrics5.Counter;
057import io.dropwizard.metrics5.MetricRegistry;
058import io.dropwizard.metrics5.SharedMetricRegistries;
059import io.dropwizard.metrics5.Timer;
060import io.opencensus.trace.AttributeValue;
061import io.opencensus.trace.BlankSpan;
062import io.opencensus.trace.Span;
063import io.opencensus.trace.Status;
064import io.opencensus.trace.Tracer;
065import io.opencensus.trace.Tracing;
066
067/**
068 * Internal helper for the Nuxeo-defined transaction manager and connection manager.
069 * <p>
070 * This code is called by the factories registered through JNDI, or by unit tests mimicking JNDI bindings.
071 */
072public class NuxeoContainer {
073
074    protected static final Log log = LogFactory.getLog(NuxeoContainer.class);
075
076    protected static RecoverableTransactionManager tmRecoverable;
077
078    protected static TransactionManager tm;
079
080    protected static TransactionSynchronizationRegistry tmSynchRegistry;
081
082    protected static UserTransaction ut;
083
084    private static volatile InstallContext installContext;
085
086    protected static Context rootContext;
087
088    protected static Context parentContext;
089
090    protected static String jndiPrefix = "java:comp/env/";
091
092    // @since 5.7
093    protected static final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
094
095    protected static final Counter rollbackCount = registry.counter(
096            MetricRegistry.name("nuxeo", "transactions", "rollbacks"));
097
098    protected static final Counter concurrentCount = registry.counter(
099            MetricRegistry.name("nuxeo", "transactions", "concurrency"));
100
101    protected static final Counter concurrentMaxCount = registry.counter(
102            MetricRegistry.name("nuxeo", "transactions", "concurrency", "max"));
103
104    protected static final Timer transactionTimer = registry.timer(
105            MetricRegistry.name("nuxeo", "transactions", "timer"));
106
107    protected static final ConcurrentHashMap<Transaction, Timer.Context> timers = new ConcurrentHashMap<>();
108
109    private NuxeoContainer() {
110    }
111
112    public static class InstallContext extends Throwable {
113        private static final long serialVersionUID = 1L;
114
115        public final String threadName;
116
117        InstallContext() {
118            super("Container installation context (" + Thread.currentThread().getName() + ")");
119            threadName = Thread.currentThread().getName();
120        }
121    }
122
123    /**
124     * Install naming and bind transaction and connection management factories "by hand".
125     */
126    protected static synchronized void install() throws NamingException {
127        if (installContext != null) {
128            throw new RuntimeException("Nuxeo container already installed");
129        }
130        installContext = new InstallContext();
131        rootContext = new NamingContext();
132        parentContext = InitialContextAccessor.getInitialContext();
133        if (parentContext != null && parentContext != rootContext) {
134            installTransactionManager(parentContext);
135        } else {
136            addDeepBinding(nameOf("TransactionManager"), new Reference(TransactionManager.class.getName(),
137                    NuxeoTransactionManagerFactory.class.getName(), null));
138            installTransactionManager(rootContext);
139        }
140    }
141
142    protected static void installTransactionManager(TransactionManagerConfiguration config) throws NamingException {
143        initTransactionManager(config);
144        addDeepBinding(rootContext, new CompositeName(nameOf("TransactionManager")), getTransactionManagerReference());
145        addDeepBinding(rootContext, new CompositeName(nameOf("UserTransaction")), getUserTransactionReference());
146    }
147
148    public static boolean isInstalled() {
149        return installContext != null;
150    }
151
152    protected static void uninstall() throws NamingException {
153        if (installContext == null) {
154            throw new RuntimeException("Nuxeo container not installed");
155        }
156        log.trace("Uninstalling nuxeo container", installContext);
157        installContext = null;
158        rootContext = null;
159        tm = null;
160        tmRecoverable = null;
161        tmSynchRegistry = null;
162        ut = null;
163    }
164
165    protected static String detectJNDIPrefix(Context context) {
166        String name = context.getClass().getName();
167        if ("org.jnp.interfaces.NamingContext".equals(name)) { // JBoss
168            return "java:";
169        } else if ("org.jboss.as.naming.InitialContext".equals(name)) { // Wildfly
170            return "java:jboss/";
171        } else if ("org.mortbay.naming.local.localContextRoot".equals(name)) { // Jetty
172            return "jdbc/";
173        }
174        // Standard JEE containers (Nuxeo-Embedded, Tomcat, GlassFish,
175        // ...
176        return "java:comp/env/";
177    }
178
179    public static String nameOf(String name) {
180        return jndiPrefix.concat(name);
181    }
182
183    /**
184     * Exposes the {@link #rootContext}.
185     *
186     * @since 5.7
187     * @see <a href="https://jira.nuxeo.com/browse/NXP-10331">NXP-10331</a>
188     */
189    public static Context getRootContext() {
190        return rootContext;
191    }
192
193    /**
194     * Bind object in root context. Create needed sub contexts. since 5.6
195     */
196    public static void addDeepBinding(String name, Object obj) throws NamingException {
197        addDeepBinding(rootContext, new CompositeName(name), obj);
198    }
199
200    protected static void addDeepBinding(Context dir, CompositeName comp, Object obj) throws NamingException {
201        Name name = comp.getPrefix(1);
202        if (comp.size() == 1) {
203            addBinding(dir, name, obj);
204            return;
205        }
206        Context subdir;
207        try {
208            subdir = (Context) dir.lookup(name);
209        } catch (NamingException e) {
210            subdir = dir.createSubcontext(name);
211        }
212        addDeepBinding(subdir, (CompositeName) comp.getSuffix(1), obj);
213    }
214
215    protected static void addBinding(Context dir, Name name, Object obj) throws NamingException {
216        try {
217            dir.rebind(name, obj);
218        } catch (NamingException e) {
219            dir.bind(name, obj);
220        }
221    }
222
223    protected static void removeBinding(String name) throws NamingException {
224        rootContext.unbind(name);
225    }
226
227    /**
228     * Gets the transaction manager used by the container.
229     *
230     * @return the transaction manager
231     */
232    public static TransactionManager getTransactionManager() {
233        return tm;
234    }
235
236    protected static Reference getTransactionManagerReference() {
237        return new SimpleReference() {
238            private static final long serialVersionUID = 1L;
239
240            @Override
241            public Object getContent() throws NamingException {
242                return NuxeoContainer.getTransactionManager();
243            }
244        };
245    }
246
247    /**
248     * Gets the user transaction used by the container.
249     *
250     * @return the user transaction
251     */
252    public static UserTransaction getUserTransaction() {
253        return ut;
254    }
255
256    protected static Reference getUserTransactionReference() {
257        return new SimpleReference() {
258            private static final long serialVersionUID = 1L;
259
260            @Override
261            public Object getContent() throws NamingException {
262                return getUserTransaction();
263            }
264        };
265    }
266
267    protected static synchronized TransactionManager initTransactionManager(TransactionManagerConfiguration config) {
268        TransactionManagerImpl impl = createTransactionManager(config);
269        tm = impl;
270        tmRecoverable = impl;
271        tmSynchRegistry = impl;
272        ut = new UserTransactionImpl(tm);
273        return tm;
274    }
275
276    protected static TransactionManagerWrapper wrapTransactionManager(TransactionManager tm) {
277        if (tm == null) {
278            return null;
279        }
280        if (tm instanceof TransactionManagerWrapper) {
281            return (TransactionManagerWrapper) tm;
282        }
283        return new TransactionManagerWrapper(tm);
284    }
285
286    public static <T> T lookup(String name, Class<T> type) throws NamingException {
287        if (rootContext == null) {
288            throw new NamingException("no naming context available");
289        }
290        return lookup(rootContext, name, type);
291    }
292
293    public static <T> T lookup(Context context, String name, Class<T> type) throws NamingException {
294        Object resolved;
295        try {
296            resolved = context.lookup(detectJNDIPrefix(context).concat(name));
297        } catch (NamingException cause) {
298            if (parentContext == null) {
299                throw cause;
300            }
301            return type.cast(parentContext.lookup(detectJNDIPrefix(parentContext).concat(name)));
302        }
303        if (resolved instanceof Reference) {
304            try {
305                resolved = NamingManager.getObjectInstance(resolved, new CompositeName(name), rootContext, null);
306            } catch (NamingException e) {
307                throw e;
308            } catch (Exception e) { // stupid JNDI API throws Exception
309                throw ExceptionUtils.runtimeException(e);
310            }
311        }
312        return type.cast(resolved);
313    }
314
315    protected static void installTransactionManager(Context context) throws NamingException {
316        TransactionManager actual = lookup(context, "TransactionManager", TransactionManager.class);
317        if (tm != null) {
318            return;
319        }
320        tm = actual;
321        tmRecoverable = wrapTransactionManager(tm);
322        ut = new UserTransactionImpl(tm);
323        tmSynchRegistry = (TransactionSynchronizationRegistry) tm;
324    }
325
326    protected static TransactionManagerImpl createTransactionManager(TransactionManagerConfiguration config) {
327        if (config == null) {
328            config = new TransactionManagerConfiguration();
329        }
330        try {
331            return new TransactionManagerImpl(config.transactionTimeoutSeconds);
332        } catch (XAException e) {
333            // failed in recovery somewhere
334            throw new RuntimeException(e.toString(), e);
335        }
336    }
337
338    /**
339     * User transaction that uses this container's transaction manager.
340     *
341     * @since 5.6
342     */
343    public static class UserTransactionImpl implements UserTransaction {
344
345        protected final TransactionManager transactionManager;
346
347        public UserTransactionImpl(TransactionManager manager) {
348            transactionManager = manager;
349        }
350
351        @Override
352        public int getStatus() throws SystemException {
353            return transactionManager.getStatus();
354        }
355
356        @Override
357        public void setRollbackOnly() throws IllegalStateException, SystemException {
358            transactionManager.setRollbackOnly();
359        }
360
361        @Override
362        public void setTransactionTimeout(int seconds) throws SystemException {
363            transactionManager.setTransactionTimeout(seconds);
364        }
365
366        @Override
367        public void begin() throws NotSupportedException, SystemException {
368            transactionManager.begin();
369            Tracer tracer = Tracing.getTracer();
370            Span span = tracer.getCurrentSpan();
371            if (!(span instanceof BlankSpan)) {
372                HashMap<String, AttributeValue> map = new HashMap<>();
373                map.put("tx.thread", AttributeValue.stringAttributeValue(Thread.currentThread().getName()));
374                map.put("tx.id", AttributeValue.stringAttributeValue(getTransactionId()));
375                span.addAnnotation("tx.begin", map);
376            }
377            timers.put(transactionManager.getTransaction(), transactionTimer.time());
378            concurrentCount.inc();
379            if (concurrentCount.getCount() > concurrentMaxCount.getCount()) {
380                concurrentMaxCount.inc();
381            }
382        }
383
384        protected String getTransactionId() {
385            return transactionKeyAsString(((TransactionManagerImpl) transactionManager).getTransactionKey());
386        }
387
388        protected static String transactionKeyAsString(Object key) {
389            if (key instanceof XidImpl) {
390                byte[] globalId = ((XidImpl) key).getGlobalTransactionId();
391                StringBuilder buffer = new StringBuilder();
392                for (byte aGlobalId : globalId) {
393                    buffer.append(Integer.toHexString(aGlobalId));
394                }
395                String stringKey = buffer.toString();
396                // remove trailing 0
397                for (int index = stringKey.length() - 1; index >= 0; index--) {
398                    if (stringKey.charAt(index) != '0') {
399                        return stringKey.substring(0, index + 1);
400                    }
401                }
402                return stringKey;
403            }
404            return key.toString();
405        }
406
407        @Override
408        public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException,
409                RollbackException, SecurityException, SystemException {
410            Span span = Tracing.getTracer().getCurrentSpan();
411            span.addAnnotation("tx.committing");
412            Transaction transaction = transactionManager.getTransaction();
413            if (transaction == null) {
414                throw new IllegalStateException("No transaction associated with current thread");
415            }
416            @SuppressWarnings("resource")
417            Timer.Context timerContext = timers.remove(transaction);
418            transactionManager.commit();
419            if (timerContext != null) {
420                long elapsed = timerContext.stop();
421
422                HashMap<String, AttributeValue> map = new HashMap<>();
423                map.put("tx.duration_ms", AttributeValue.longAttributeValue(elapsed / 1000_000));
424                span.addAnnotation("tx.commited", map);
425            }
426            concurrentCount.dec();
427            span.setStatus(Status.OK);
428        }
429
430        @Override
431        public void rollback() throws IllegalStateException, SecurityException, SystemException {
432            Span span = Tracing.getTracer().getCurrentSpan();
433            span.addAnnotation("tx.rollbacking");
434            Transaction transaction = transactionManager.getTransaction();
435            if (transaction == null) {
436                throw new IllegalStateException("No transaction associated with current thread");
437            }
438            @SuppressWarnings("resource")
439            Timer.Context timerContext = timers.remove(transaction);
440            transactionManager.rollback();
441            concurrentCount.dec();
442            rollbackCount.inc();
443            if (timerContext != null) {
444                long elapsed = timerContext.stop();
445                span.addAnnotation("tx.rollbacked " + elapsed / 1000000 + "ms");
446            } else {
447                span.addAnnotation("tx.rollbacked");
448            }
449            span.setStatus(Status.UNKNOWN);
450        }
451    }
452
453    public static class TransactionManagerConfiguration {
454        public int transactionTimeoutSeconds = 600;
455
456        public void setTransactionTimeoutSeconds(int transactionTimeoutSeconds) {
457            this.transactionTimeoutSeconds = transactionTimeoutSeconds;
458        }
459    }
460
461    /**
462     * Wraps a transaction manager for providing a dummy recoverable interface.
463     *
464     * @author matic
465     */
466    public static class TransactionManagerWrapper implements RecoverableTransactionManager {
467
468        protected TransactionManager tm;
469
470        public TransactionManagerWrapper(TransactionManager tm) {
471            this.tm = tm;
472        }
473
474        @Override
475        public Transaction suspend() throws SystemException {
476            return tm.suspend();
477        }
478
479        @Override
480        public void setTransactionTimeout(int seconds) throws SystemException {
481            tm.setTransactionTimeout(seconds);
482        }
483
484        @Override
485        public void setRollbackOnly() throws IllegalStateException, SystemException {
486            tm.setRollbackOnly();
487        }
488
489        @Override
490        public void rollback() throws IllegalStateException, SecurityException, SystemException {
491            tm.rollback();
492        }
493
494        @Override
495        public void resume(Transaction tobj)
496                throws IllegalStateException, InvalidTransactionException, SystemException {
497            tm.resume(tobj);
498        }
499
500        @Override
501        public int getStatus() throws SystemException {
502            return tm.getStatus();
503        }
504
505        @Override
506        public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException,
507                RollbackException, SecurityException, SystemException {
508            tm.commit();
509        }
510
511        @Override
512        public void begin() throws SystemException {
513            try {
514                tm.begin();
515            } catch (javax.transaction.NotSupportedException e) {
516                throw new RuntimeException(e);
517            }
518        }
519
520        @Override
521        public void recoveryError(Exception e) {
522            throw new UnsupportedOperationException();
523        }
524
525        @Override
526        public void registerNamedXAResourceFactory(NamedXAResourceFactory factory) {
527            if (!RecoverableTransactionManager.class.isAssignableFrom(tm.getClass())) {
528                throw new UnsupportedOperationException();
529            }
530            ((RecoverableTransactionManager) tm).registerNamedXAResourceFactory(factory);
531        }
532
533        @Override
534        public void unregisterNamedXAResourceFactory(String factory) {
535            if (!RecoverableTransactionManager.class.isAssignableFrom(tm.getClass())) {
536                throw new UnsupportedOperationException();
537            }
538            ((RecoverableTransactionManager) tm).unregisterNamedXAResourceFactory(factory);
539        }
540
541        @Override
542        public Transaction getTransaction() throws SystemException {
543            final Transaction tx = tm.getTransaction();
544            if (tx instanceof TransactionImpl) {
545                return tx;
546            }
547            return new TransactionImpl(null, null) {
548                @Override
549                public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException,
550                        SecurityException, SystemException {
551                    tx.commit();
552                }
553
554                @Override
555                public void rollback() throws IllegalStateException, SystemException {
556                    tx.rollback();
557                }
558
559                @Override
560                public synchronized boolean enlistResource(XAResource xaRes)
561                        throws IllegalStateException, RollbackException, SystemException {
562                    return tx.enlistResource(xaRes);
563                }
564
565                @Override
566                public synchronized boolean delistResource(XAResource xaRes, int flag)
567                        throws IllegalStateException, SystemException {
568                    return super.delistResource(xaRes, flag);
569                }
570
571                @Override
572                public synchronized void setRollbackOnly() throws IllegalStateException {
573                    try {
574                        tx.setRollbackOnly();
575                    } catch (SystemException e) {
576                        throw new IllegalStateException(e);
577                    }
578                }
579
580                @Override
581                public void registerInterposedSynchronization(javax.transaction.Synchronization synchronization) {
582                    try {
583                        TransactionHelper.lookupSynchronizationRegistry()
584                                         .registerInterposedSynchronization(synchronization);
585                    } catch (NamingException e) {
586                    }
587                }
588            };
589        }
590    }
591
592    public static TransactionSynchronizationRegistry getTransactionSynchronizationRegistry() {
593        return tmSynchRegistry;
594    }
595
596}