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}