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