001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 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 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import javax.sql.DataSource; 024import java.sql.Connection; 025import java.sql.SQLException; 026import java.sql.SQLFeatureNotSupportedException; 027import java.sql.Savepoint; 028import java.time.Duration; 029import java.util.Collections; 030import java.util.List; 031import java.util.Optional; 032import java.util.concurrent.CopyOnWriteArrayList; 033import java.util.concurrent.atomic.AtomicBoolean; 034import java.util.concurrent.atomic.AtomicLong; 035import java.util.concurrent.locks.ReentrantLock; 036import java.util.function.Consumer; 037import java.util.function.Function; 038import java.util.logging.Level; 039import java.util.logging.Logger; 040 041import static java.lang.String.format; 042import static java.lang.System.nanoTime; 043import static java.util.Objects.requireNonNull; 044 045/** 046 * Represents a database transaction. 047 * <p> 048 * Note that commit and rollback operations are controlled internally by {@link Database}. 049 * 050 * @author <a href="https://www.revetkn.com">Mark Allen</a> 051 * @since 1.0.0 052 */ 053@ThreadSafe 054public final class Transaction { 055 @NonNull 056 private static final AtomicLong ID_GENERATOR; 057 @NonNull 058 private static final Logger LOGGER; 059 060 static { 061 ID_GENERATOR = new AtomicLong(0); 062 LOGGER = Logger.getLogger(Transaction.class.getName()); 063 } 064 065 @NonNull 066 private final Long id; 067 @NonNull 068 private final DataSource dataSource; 069 @NonNull 070 private final TransactionOptions transactionOptions; 071 @NonNull 072 private final TransactionIsolation transactionIsolation; 073 @NonNull 074 private final MetricsCollectorDispatcher metricsCollectorDispatcher; 075 @NonNull 076 private volatile DatabaseType databaseType; 077 @NonNull 078 private final Function<@NonNull Connection, @NonNull DatabaseType> databaseTypeResolver; 079 @NonNull 080 private final List<@NonNull Consumer<TransactionResult>> postTransactionOperations; 081 @NonNull 082 private final ReentrantLock connectionLock; 083 @NonNull 084 private final AtomicBoolean rollbackOnly; 085 @NonNull 086 private final AtomicBoolean completed; 087 @Nullable 088 private volatile Connection connection; 089 @Nullable 090 private volatile Boolean initialAutoCommit; 091 @Nullable 092 private volatile Boolean initialReadOnly; 093 @Nullable 094 private volatile Integer initialTransactionIsolationJdbcLevel; 095 @Nullable 096 private volatile Long connectionAcquiredAtNanos; 097 @NonNull 098 private final AtomicBoolean transactionIsolationWasChanged; 099 @NonNull 100 private final AtomicBoolean readOnlyWasChanged; 101 102 Transaction(@NonNull DataSource dataSource, 103 @NonNull TransactionOptions transactionOptions, 104 @NonNull MetricsCollectorDispatcher metricsCollectorDispatcher, 105 @NonNull DatabaseType databaseType) { 106 this(dataSource, transactionOptions, metricsCollectorDispatcher, databaseType, connection -> databaseType); 107 } 108 109 Transaction(@NonNull DataSource dataSource, 110 @NonNull TransactionOptions transactionOptions, 111 @NonNull MetricsCollectorDispatcher metricsCollectorDispatcher, 112 @NonNull DatabaseType databaseType, 113 @NonNull Function<@NonNull Connection, @NonNull DatabaseType> databaseTypeResolver) { 114 requireNonNull(dataSource); 115 requireNonNull(transactionOptions); 116 requireNonNull(metricsCollectorDispatcher); 117 requireNonNull(databaseType); 118 requireNonNull(databaseTypeResolver); 119 120 this.id = generateId(); 121 this.dataSource = dataSource; 122 this.transactionOptions = transactionOptions; 123 this.transactionIsolation = transactionOptions.getIsolation(); 124 this.metricsCollectorDispatcher = metricsCollectorDispatcher; 125 this.databaseType = databaseType; 126 this.databaseTypeResolver = databaseTypeResolver; 127 this.connection = null; 128 this.rollbackOnly = new AtomicBoolean(false); 129 this.completed = new AtomicBoolean(false); 130 this.initialAutoCommit = null; 131 this.initialReadOnly = null; 132 this.connectionAcquiredAtNanos = null; 133 this.transactionIsolationWasChanged = new AtomicBoolean(false); 134 this.readOnlyWasChanged = new AtomicBoolean(false); 135 this.postTransactionOperations = new CopyOnWriteArrayList(); 136 this.connectionLock = new ReentrantLock(); 137 } 138 139 @Override 140 @NonNull 141 public String toString() { 142 return format("%s{id=%s, transactionIsolation=%s, hasConnection=%s, isRollbackOnly=%s}", 143 getClass().getSimpleName(), id(), getTransactionIsolation(), hasConnection(), isRollbackOnly()); 144 } 145 146 /** 147 * Creates a transaction savepoint that can be rolled back to via {@link #rollback(Savepoint)}. 148 * <p> 149 * For most application code, prefer {@link #withSavepoint(TransactionalOperation)} or 150 * {@link #withSavepoint(ReturningTransactionalOperation)} so rollback and release cleanup are handled automatically. 151 * 152 * @return a transaction savepoint 153 * @throws IllegalStateException if this transaction has already completed 154 */ 155 @NonNull 156 public Savepoint createSavepoint() { 157 lockConnectionInterruptibly("create a savepoint"); 158 159 try { 160 assertNotCompleted("create a savepoint"); 161 Savepoint savepoint = getConnection().setSavepoint(); 162 getMetricsCollectorDispatcher().didCreateSavepoint(this, getDatabaseType()); 163 return savepoint; 164 } catch (SQLException e) { 165 throw databaseException("Unable to create savepoint", e); 166 } finally { 167 getConnectionLock().unlock(); 168 } 169 } 170 171 /** 172 * Rolls back to the provided transaction savepoint. 173 * 174 * @param savepoint the savepoint to roll back to 175 * @throws IllegalStateException if this transaction has already completed 176 */ 177 public void rollback(@NonNull Savepoint savepoint) { 178 requireNonNull(savepoint); 179 boolean connectionLockAcquired = false; 180 181 try { 182 lockConnectionInterruptibly("roll back to a savepoint"); 183 connectionLockAcquired = true; 184 assertNotCompleted("roll back to a savepoint"); 185 getConnection().rollback(savepoint); 186 getMetricsCollectorDispatcher().didRollbackToSavepoint(this, getDatabaseType()); 187 } catch (SQLException e) { 188 this.rollbackOnly.set(true); 189 throw databaseException("Unable to roll back to savepoint", e); 190 } catch (RuntimeException | Error e) { 191 this.rollbackOnly.set(true); 192 throw e; 193 } finally { 194 if (connectionLockAcquired) 195 getConnectionLock().unlock(); 196 } 197 } 198 199 /** 200 * Releases the provided transaction savepoint. 201 * <p> 202 * For most application code, prefer {@link #withSavepoint(TransactionalOperation)} or 203 * {@link #withSavepoint(ReturningTransactionalOperation)} so rollback and release cleanup are handled automatically. 204 * 205 * @param savepoint the savepoint to release 206 * @throws IllegalStateException if this transaction has already completed 207 * @since 4.1.0 208 */ 209 public void releaseSavepoint(@NonNull Savepoint savepoint) { 210 requireNonNull(savepoint); 211 assertNotCompleted("release a savepoint"); 212 releaseSavepointJdbc(savepoint); 213 } 214 215 /** 216 * Performs an operation inside a transaction savepoint. 217 * <p> 218 * If {@code transactionalOperation} completes successfully, the savepoint is released when the driver supports release. 219 * If an exception bubbles out, Pyranid rolls back to the savepoint, attempts to release it, and preserves cleanup failures 220 * as suppressed exceptions on the thrown exception. 221 * <p> 222 * Nested savepoint usage should be stack-like: finish inner savepoints before manually releasing or rolling back outer 223 * savepoints. 224 * 225 * @param transactionalOperation the operation to perform inside a savepoint 226 * @throws IllegalStateException if this transaction has already completed 227 * @since 4.1.0 228 */ 229 public void withSavepoint(@NonNull TransactionalOperation transactionalOperation) { 230 requireNonNull(transactionalOperation); 231 232 withSavepoint(() -> { 233 transactionalOperation.perform(); 234 return Optional.empty(); 235 }); 236 } 237 238 /** 239 * Performs an operation inside a transaction savepoint and optionally returns a value. 240 * <p> 241 * If {@code transactionalOperation} completes successfully, the savepoint is released when the driver supports release. 242 * If an exception bubbles out, Pyranid rolls back to the savepoint, attempts to release it, and preserves cleanup failures 243 * as suppressed exceptions on the thrown exception. 244 * <p> 245 * Nested savepoint usage should be stack-like: finish inner savepoints before manually releasing or rolling back outer 246 * savepoints. 247 * 248 * @param transactionalOperation the operation to perform inside a savepoint 249 * @param <T> the type to be returned 250 * @return the result of the operation 251 * @throws IllegalStateException if this transaction has already completed 252 * @since 4.1.0 253 */ 254 @NonNull 255 public <T> Optional<T> withSavepoint(@NonNull ReturningTransactionalOperation<T> transactionalOperation) { 256 requireNonNull(transactionalOperation); 257 assertNotCompleted("run a savepoint operation"); 258 259 Savepoint savepoint = createSavepoint(); 260 261 try { 262 Optional<T> returnValue = transactionalOperation.perform(); 263 264 if (returnValue == null) 265 returnValue = Optional.empty(); 266 267 releaseSavepointAfterSuccess(savepoint); 268 return returnValue; 269 } catch (RuntimeException e) { 270 cleanupSavepointAfterFailure(savepoint, e); 271 throw e; 272 } catch (Error e) { 273 cleanupSavepointAfterFailure(savepoint, e); 274 throw e; 275 } catch (Throwable t) { 276 RuntimeException wrapped = new RuntimeException(t); 277 cleanupSavepointAfterFailure(savepoint, wrapped); 278 throw wrapped; 279 } 280 } 281 282 /** 283 * Should this transaction be rolled back upon completion? 284 * <p> 285 * Default value is {@code false}. 286 * 287 * @return {@code true} if this transaction should be rolled back, {@code false} otherwise 288 */ 289 @NonNull 290 public Boolean isRollbackOnly() { 291 return this.rollbackOnly.get(); 292 } 293 294 /** 295 * Sets whether this transaction should be rolled back upon completion. 296 * 297 * @param rollbackOnly whether to set this transaction to be rollback-only 298 */ 299 public void setRollbackOnly(@NonNull Boolean rollbackOnly) { 300 requireNonNull(rollbackOnly); 301 assertNotCompleted("set rollback-only state"); 302 this.rollbackOnly.set(rollbackOnly); 303 } 304 305 /** 306 * Adds an operation to the list of operations to be executed when the transaction completes. 307 * <p> 308 * The supplied operation receives {@link TransactionResult#COMMITTED} if commit completed successfully, 309 * {@link TransactionResult#ROLLED_BACK} if the transaction completed on the rollback path before commit was attempted, 310 * or 311 * {@link TransactionResult#IN_DOUBT} if the commit call failed and Pyranid cannot prove whether the database 312 * committed or rolled back. 313 * <p> 314 * If the operation throws, Pyranid wraps the thrown value in a {@link PostTransactionOperationException}. If another 315 * transaction or cleanup failure is already primary, the wrapper is suppressed onto that primary failure; otherwise, 316 * {@link Database#transaction(TransactionalOperation)} throws the wrapper as the primary failure. 317 * 318 * @param postTransactionOperation the post-transaction operation to add 319 */ 320 public void addPostTransactionOperation(@NonNull Consumer<TransactionResult> postTransactionOperation) { 321 requireNonNull(postTransactionOperation); 322 assertNotCompleted("add a post-transaction operation"); 323 this.postTransactionOperations.add(postTransactionOperation); 324 } 325 326 /** 327 * Removes an operation from the list of operations to be executed when the transaction completes. 328 * 329 * @param postTransactionOperation the post-transaction operation to remove 330 * @return {@code true} if the post-transaction operation was removed, {@code false} otherwise 331 */ 332 @NonNull 333 public Boolean removePostTransactionOperation(@NonNull Consumer<TransactionResult> postTransactionOperation) { 334 requireNonNull(postTransactionOperation); 335 assertNotCompleted("remove a post-transaction operation"); 336 return this.postTransactionOperations.remove(postTransactionOperation); 337 } 338 339 /** 340 * Gets an unmodifiable list of post-transaction operations. 341 * <p> 342 * To manipulate the list, use {@link #addPostTransactionOperation(Consumer)} and 343 * {@link #removePostTransactionOperation(Consumer)}. 344 * 345 * @return the list of post-transaction operations 346 */ 347 @NonNull 348 public List<@NonNull Consumer<TransactionResult>> getPostTransactionOperations() { 349 return Collections.unmodifiableList(this.postTransactionOperations); 350 } 351 352 /** 353 * Get the isolation level for this transaction. 354 * 355 * @return the isolation level 356 */ 357 @NonNull 358 public TransactionIsolation getTransactionIsolation() { 359 return this.transactionIsolation; 360 } 361 362 /** 363 * Gets the options used to create this transaction. 364 * 365 * @return transaction options 366 * @since 4.2.0 367 */ 368 @NonNull 369 public TransactionOptions getTransactionOptions() { 370 return this.transactionOptions; 371 } 372 373 @NonNull 374 Long id() { 375 return this.id; 376 } 377 378 @NonNull 379 Boolean hasConnection() { 380 getConnectionLock().lock(); 381 382 try { 383 return this.connection != null; 384 } finally { 385 getConnectionLock().unlock(); 386 } 387 } 388 389 @NonNull 390 Boolean isOwnedBy(@NonNull DataSource dataSource) { 391 requireNonNull(dataSource); 392 return this.dataSource == dataSource; 393 } 394 395 void commit() { 396 getConnectionLock().lock(); 397 398 try { 399 if (!hasConnection()) { 400 LOGGER.finer("Transaction has no connection, so nothing to commit"); 401 return; 402 } 403 404 LOGGER.finer("Committing transaction..."); 405 406 long startTime = nanoTime(); 407 408 try { 409 getConnection().commit(); 410 getMetricsCollectorDispatcher().didCommitPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime)); 411 LOGGER.finer("Transaction committed."); 412 } catch (SQLException e) { 413 DatabaseException wrapped = databaseException("Unable to commit transaction", e); 414 getMetricsCollectorDispatcher().didFailToCommitPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), wrapped); 415 throw wrapped; 416 } 417 } finally { 418 getConnectionLock().unlock(); 419 } 420 } 421 422 void rollback() { 423 getConnectionLock().lock(); 424 425 try { 426 if (!hasConnection()) { 427 LOGGER.finer("Transaction has no connection, so nothing to roll back"); 428 return; 429 } 430 431 LOGGER.finer("Rolling back transaction..."); 432 433 long startTime = nanoTime(); 434 435 try { 436 getConnection().rollback(); 437 getMetricsCollectorDispatcher().didRollbackPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime)); 438 LOGGER.finer("Transaction rolled back."); 439 } catch (SQLException e) { 440 DatabaseException wrapped = databaseException("Unable to roll back transaction", e); 441 getMetricsCollectorDispatcher().didFailToRollbackPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), wrapped); 442 throw wrapped; 443 } 444 } finally { 445 getConnectionLock().unlock(); 446 } 447 } 448 449 /** 450 * The connection associated with this transaction. 451 * <p> 452 * If no connection is associated yet, we ask the {@link DataSource} for one. 453 * 454 * @return The connection associated with this transaction. 455 * @throws DatabaseException if unable to acquire a connection. 456 */ 457 @NonNull 458 Connection getConnection() { 459 lockConnectionInterruptibly("get the transaction connection"); 460 461 try { 462 assertNotCompleted("get the transaction connection"); 463 464 if (hasConnection()) 465 return this.connection; 466 467 long startTime = nanoTime(); 468 getMetricsCollectorDispatcher().willAcquireTransactionConnection(this, getDatabaseType()); 469 470 try { 471 this.connection = getDataSource().getConnection(); 472 } catch (SQLException e) { 473 DatabaseException wrapped = databaseException("Unable to acquire database connection", e); 474 Duration acquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 475 getMetricsCollectorDispatcher().didFailToAcquireTransactionConnection(this, getDatabaseType(), acquisitionDuration, wrapped); 476 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 477 MetricsCollector.PhysicalTransactionBeginFailurePhase.ACQUIRE_CONNECTION, getDatabaseType(), wrapped); 478 throw wrapped; 479 } catch (RuntimeException e) { 480 Duration acquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 481 getMetricsCollectorDispatcher().didFailToAcquireTransactionConnection(this, getDatabaseType(), acquisitionDuration, e); 482 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 483 MetricsCollector.PhysicalTransactionBeginFailurePhase.ACQUIRE_CONNECTION, getDatabaseType(), e); 484 throw e; 485 } 486 487 this.connectionAcquiredAtNanos = nanoTime(); 488 resolveDatabaseType(this.connection); 489 getMetricsCollectorDispatcher().didAcquireTransactionConnection(this, getDatabaseType(), Duration.ofNanos(this.connectionAcquiredAtNanos - startTime)); 490 491 // Keep track of the initial setting for autocommit since it might need to get changed from "true" to "false" for 492 // the duration of the transaction and then back to "true" post-transaction. 493 try { 494 this.initialAutoCommit = this.connection.getAutoCommit(); 495 } catch (SQLException e) { 496 DatabaseException wrapped = databaseException("Unable to determine database connection autocommit setting", e); 497 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 498 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_AUTOCOMMIT, getDatabaseType(), wrapped); 499 throw wrapped; 500 } 501 502 // Track initial isolation 503 try { 504 this.initialTransactionIsolationJdbcLevel = this.connection.getTransactionIsolation(); 505 } catch (SQLException e) { 506 DatabaseException wrapped = databaseException("Unable to determine database connection transaction isolation", e); 507 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 508 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_ISOLATION, getDatabaseType(), wrapped); 509 throw wrapped; 510 } 511 512 try { 513 this.initialReadOnly = this.connection.isReadOnly(); 514 } catch (SQLException e) { 515 DatabaseException wrapped = databaseException("Unable to determine database connection read-only setting", e); 516 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 517 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_READ_ONLY, getDatabaseType(), wrapped); 518 throw wrapped; 519 } 520 521 Boolean desiredReadOnly = getTransactionOptions().getReadOnly().orElse(null); 522 523 if (desiredReadOnly != null && !desiredReadOnly.equals(this.initialReadOnly)) { 524 try { 525 this.connection.setReadOnly(desiredReadOnly); 526 this.readOnlyWasChanged.set(true); 527 } catch (SQLException e) { 528 DatabaseException wrapped = databaseException(format("Unable to set database connection read-only value to '%s'", desiredReadOnly), e); 529 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 530 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_READ_ONLY, getDatabaseType(), wrapped); 531 throw wrapped; 532 } 533 } 534 535 // Immediately flip autocommit to false if needed...if initially true, it will get set back to true by Database at 536 // the end of the transaction 537 if (this.initialAutoCommit) { 538 try { 539 setAutoCommit(false); 540 } catch (DatabaseException e) { 541 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 542 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_AUTOCOMMIT_FALSE, getDatabaseType(), e); 543 throw e; 544 } 545 } 546 547 // Apply requested isolation if not DEFAULT and different from current 548 TransactionIsolation desiredTransactionIsolation = getTransactionIsolation(); 549 550 if (desiredTransactionIsolation != TransactionIsolation.DEFAULT) { 551 // Safe; only DEFAULT has a null value 552 int desiredJdbcLevel = desiredTransactionIsolation.getJdbcLevel().get(); 553 // Apply only if different from current (or current unknown) 554 if (this.initialTransactionIsolationJdbcLevel == null || this.initialTransactionIsolationJdbcLevel.intValue() != desiredJdbcLevel) { 555 try { 556 // In the future, we might check supportsTransactionIsolationLevel via DatabaseMetaData first. 557 // Probably want to calculate that at Database init time and cache it off 558 this.connection.setTransactionIsolation(desiredJdbcLevel); 559 this.transactionIsolationWasChanged.set(true); 560 } catch (SQLException e) { 561 DatabaseException wrapped = databaseException(format("Unable to set transaction isolation to %s", desiredTransactionIsolation.name()), e); 562 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 563 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_ISOLATION, getDatabaseType(), wrapped); 564 throw wrapped; 565 } 566 } 567 } 568 569 getMetricsCollectorDispatcher().didBeginPhysicalTransaction(this, getTransactionIsolation(), getDatabaseType()); 570 return this.connection; 571 } finally { 572 getConnectionLock().unlock(); 573 } 574 } 575 576 void setAutoCommit(@NonNull Boolean autoCommit) { 577 requireNonNull(autoCommit); 578 579 getConnectionLock().lock(); 580 581 try { 582 Connection connection = this.connection; 583 584 if (connection == null) 585 throw databaseException("Transaction has no connection", null); 586 587 try { 588 connection.setAutoCommit(autoCommit); 589 } catch (SQLException e) { 590 throw databaseException(format("Unable to set database connection autocommit value to '%s'", autoCommit), e); 591 } 592 } finally { 593 getConnectionLock().unlock(); 594 } 595 } 596 597 void restoreTransactionIsolationIfNeeded() { 598 getConnectionLock().lock(); 599 600 try { 601 if (this.connection == null) 602 return; 603 604 Integer initialTransactionIsolationJdbcLevel = getInitialTransactionIsolationJdbcLevel().orElse(null); 605 606 if (getTransactionIsolationWasChanged() && initialTransactionIsolationJdbcLevel != null) { 607 try { 608 this.connection.setTransactionIsolation(initialTransactionIsolationJdbcLevel.intValue()); 609 } catch (SQLException e) { 610 throw databaseException("Unable to restore original transaction isolation", e); 611 } finally { 612 this.transactionIsolationWasChanged.set(false); 613 } 614 } 615 } finally { 616 getConnectionLock().unlock(); 617 } 618 } 619 620 void restoreReadOnlyIfNeeded() { 621 getConnectionLock().lock(); 622 623 try { 624 if (this.connection == null) 625 return; 626 627 Boolean initialReadOnly = getInitialReadOnly().orElse(null); 628 629 if (getReadOnlyWasChanged() && initialReadOnly != null) { 630 try { 631 this.connection.setReadOnly(initialReadOnly); 632 } catch (SQLException e) { 633 throw databaseException("Unable to restore original read-only setting", e); 634 } finally { 635 this.readOnlyWasChanged.set(false); 636 } 637 } 638 } finally { 639 getConnectionLock().unlock(); 640 } 641 } 642 643 @NonNull 644 Long generateId() { 645 return ID_GENERATOR.incrementAndGet(); 646 } 647 648 @NonNull 649 Optional<Boolean> getInitialAutoCommit() { 650 return Optional.ofNullable(this.initialAutoCommit); 651 } 652 653 @NonNull 654 Optional<Boolean> getInitialReadOnly() { 655 return Optional.ofNullable(this.initialReadOnly); 656 } 657 658 @NonNull 659 DataSource getDataSource() { 660 return this.dataSource; 661 } 662 663 @NonNull 664 private Optional<Integer> getInitialTransactionIsolationJdbcLevel() { 665 return Optional.ofNullable(this.initialTransactionIsolationJdbcLevel); 666 } 667 668 @NonNull 669 private Boolean getTransactionIsolationWasChanged() { 670 return this.transactionIsolationWasChanged.get(); 671 } 672 673 @NonNull 674 private Boolean getReadOnlyWasChanged() { 675 return this.readOnlyWasChanged.get(); 676 } 677 678 @NonNull 679 ReentrantLock getConnectionLock() { 680 return this.connectionLock; 681 } 682 683 @NonNull 684 Optional<Connection> getExistingConnection() { 685 getConnectionLock().lock(); 686 687 try { 688 return Optional.ofNullable(this.connection); 689 } finally { 690 getConnectionLock().unlock(); 691 } 692 } 693 694 void clearConnection() { 695 getConnectionLock().lock(); 696 697 try { 698 this.connection = null; 699 } finally { 700 getConnectionLock().unlock(); 701 } 702 } 703 704 void markCompleted() { 705 this.completed.set(true); 706 } 707 708 @NonNull 709 Boolean isCompleted() { 710 return this.completed.get(); 711 } 712 713 private void releaseSavepointAfterSuccess(@NonNull Savepoint savepoint) { 714 requireNonNull(savepoint); 715 lockConnectionInterruptibly("release a savepoint"); 716 717 try { 718 getConnection().releaseSavepoint(savepoint); 719 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 720 } catch (SQLFeatureNotSupportedException e) { 721 // Some drivers support rollback-to-savepoint but not release; successful closures should still succeed. 722 } catch (SQLException e) { 723 throw databaseException("Unable to release savepoint", e); 724 } finally { 725 getConnectionLock().unlock(); 726 } 727 } 728 729 private void cleanupSavepointAfterFailure(@NonNull Savepoint savepoint, 730 @NonNull Throwable primary) { 731 requireNonNull(savepoint); 732 requireNonNull(primary); 733 734 boolean rollbackConnectionLockAcquired = false; 735 736 try { 737 lockConnectionInterruptibly("roll back to a savepoint"); 738 rollbackConnectionLockAcquired = true; 739 getConnection().rollback(savepoint); 740 getMetricsCollectorDispatcher().didRollbackToSavepoint(this, getDatabaseType()); 741 } catch (Throwable rollbackException) { 742 this.rollbackOnly.set(true); 743 primary.addSuppressed(databaseException("Unable to roll back to savepoint", rollbackException)); 744 } finally { 745 if (rollbackConnectionLockAcquired) 746 getConnectionLock().unlock(); 747 } 748 749 boolean releaseConnectionLockAcquired = false; 750 751 try { 752 lockConnectionInterruptibly("release a savepoint"); 753 releaseConnectionLockAcquired = true; 754 getConnection().releaseSavepoint(savepoint); 755 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 756 } catch (SQLFeatureNotSupportedException e) { 757 // Some drivers support rollback-to-savepoint but not release. 758 } catch (Throwable releaseException) { 759 primary.addSuppressed(databaseException("Unable to release savepoint", releaseException)); 760 } finally { 761 if (releaseConnectionLockAcquired) 762 getConnectionLock().unlock(); 763 } 764 } 765 766 private void releaseSavepointJdbc(@NonNull Savepoint savepoint) { 767 requireNonNull(savepoint); 768 lockConnectionInterruptibly("release a savepoint"); 769 770 try { 771 getConnection().releaseSavepoint(savepoint); 772 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 773 } catch (SQLException e) { 774 throw databaseException("Unable to release savepoint", e); 775 } finally { 776 getConnectionLock().unlock(); 777 } 778 } 779 780 private void lockConnectionInterruptibly(@NonNull String operation) { 781 requireNonNull(operation); 782 783 try { 784 getConnectionLock().lockInterruptibly(); 785 } catch (InterruptedException e) { 786 Thread.currentThread().interrupt(); 787 throw databaseException(format("Interrupted while waiting to %s", operation), e); 788 } 789 } 790 791 private void resolveDatabaseType(@NonNull Connection connection) { 792 requireNonNull(connection); 793 794 if (this.databaseType != DatabaseType.GENERIC) 795 return; 796 797 try { 798 this.databaseType = requireNonNull(this.databaseTypeResolver.apply(connection)); 799 } catch (Throwable t) { 800 LOGGER.log(Level.FINE, "Unable to determine database type from transaction connection", t); 801 } 802 } 803 804 @NonNull 805 private DatabaseException databaseException(@NonNull String message, 806 @Nullable Throwable cause) { 807 requireNonNull(message); 808 809 DatabaseDialect databaseDialect = getDatabaseType() == DatabaseType.GENERIC 810 ? DatabaseDialect.forExceptionCause(cause) 811 : getDatabaseType().dialect(); 812 return new DatabaseException(message, cause, databaseDialect); 813 } 814 815 @NonNull 816 MetricsCollectorDispatcher getMetricsCollectorDispatcher() { 817 return this.metricsCollectorDispatcher; 818 } 819 820 @NonNull 821 DatabaseType getDatabaseType() { 822 return this.databaseType; 823 } 824 825 @NonNull 826 Optional<Long> getConnectionAcquiredAtNanos() { 827 return Optional.ofNullable(this.connectionAcquiredAtNanos); 828 } 829 830 private void assertNotCompleted(@NonNull String operation) { 831 requireNonNull(operation); 832 833 if (isCompleted()) 834 throw new IllegalStateException(format("Transaction %s has already completed and cannot %s", id(), operation)); 835 } 836}