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 @NonNull 088 private final AtomicBoolean commitSerializationFailure; 089 @NonNull 090 private volatile PhysicalTransactionBeginState physicalTransactionBeginState; 091 @Nullable 092 private volatile Throwable physicalTransactionBeginFailure; 093 private volatile boolean physicalRollbackPermitted; 094 @Nullable 095 private volatile Connection connection; 096 @Nullable 097 private volatile Boolean initialAutoCommit; 098 @Nullable 099 private volatile Boolean initialReadOnly; 100 @Nullable 101 private volatile Integer initialTransactionIsolationJdbcLevel; 102 @Nullable 103 private volatile Long connectionAcquiredAtNanos; 104 @NonNull 105 private final AtomicBoolean transactionIsolationWasChanged; 106 @NonNull 107 private final AtomicBoolean readOnlyWasChanged; 108 109 Transaction(@NonNull DataSource dataSource, 110 @NonNull TransactionOptions transactionOptions, 111 @NonNull MetricsCollectorDispatcher metricsCollectorDispatcher, 112 @NonNull DatabaseType databaseType) { 113 this(dataSource, transactionOptions, metricsCollectorDispatcher, databaseType, connection -> databaseType); 114 } 115 116 Transaction(@NonNull DataSource dataSource, 117 @NonNull TransactionOptions transactionOptions, 118 @NonNull MetricsCollectorDispatcher metricsCollectorDispatcher, 119 @NonNull DatabaseType databaseType, 120 @NonNull Function<@NonNull Connection, @NonNull DatabaseType> databaseTypeResolver) { 121 requireNonNull(dataSource); 122 requireNonNull(transactionOptions); 123 requireNonNull(metricsCollectorDispatcher); 124 requireNonNull(databaseType); 125 requireNonNull(databaseTypeResolver); 126 127 this.id = generateId(); 128 this.dataSource = dataSource; 129 this.transactionOptions = transactionOptions; 130 this.transactionIsolation = transactionOptions.getIsolation(); 131 this.metricsCollectorDispatcher = metricsCollectorDispatcher; 132 this.databaseType = databaseType; 133 this.databaseTypeResolver = databaseTypeResolver; 134 this.connection = null; 135 this.rollbackOnly = new AtomicBoolean(false); 136 this.completed = new AtomicBoolean(false); 137 this.commitSerializationFailure = new AtomicBoolean(false); 138 this.physicalTransactionBeginState = PhysicalTransactionBeginState.NOT_STARTED; 139 this.physicalTransactionBeginFailure = null; 140 this.physicalRollbackPermitted = false; 141 this.initialAutoCommit = null; 142 this.initialReadOnly = null; 143 this.connectionAcquiredAtNanos = null; 144 this.transactionIsolationWasChanged = new AtomicBoolean(false); 145 this.readOnlyWasChanged = new AtomicBoolean(false); 146 this.postTransactionOperations = new CopyOnWriteArrayList(); 147 this.connectionLock = new ReentrantLock(); 148 } 149 150 @Override 151 @NonNull 152 public String toString() { 153 return format("%s{id=%s, transactionIsolation=%s, hasConnection=%s, isRollbackOnly=%s}", 154 getClass().getSimpleName(), id(), getTransactionIsolation(), hasConnection(), isRollbackOnly()); 155 } 156 157 /** 158 * Creates a transaction savepoint that can be rolled back to via {@link #rollback(Savepoint)}. 159 * <p> 160 * For most application code, prefer {@link #withSavepoint(TransactionalOperation)} or 161 * {@link #withSavepoint(ReturningTransactionalOperation)} so rollback and release cleanup are handled automatically. 162 * 163 * @return a transaction savepoint 164 * @throws IllegalStateException if this transaction has already completed 165 */ 166 @NonNull 167 public Savepoint createSavepoint() { 168 lockConnectionInterruptibly("create a savepoint"); 169 170 try { 171 assertNotCompleted("create a savepoint"); 172 Savepoint savepoint = getConnection().setSavepoint(); 173 getMetricsCollectorDispatcher().didCreateSavepoint(this, getDatabaseType()); 174 return savepoint; 175 } catch (SQLException e) { 176 throw databaseException("Unable to create savepoint", e); 177 } finally { 178 getConnectionLock().unlock(); 179 } 180 } 181 182 /** 183 * Rolls back to the provided transaction savepoint. 184 * 185 * @param savepoint the savepoint to roll back to 186 * @throws IllegalStateException if this transaction has already completed 187 */ 188 public void rollback(@NonNull Savepoint savepoint) { 189 requireNonNull(savepoint); 190 boolean connectionLockAcquired = false; 191 192 try { 193 lockConnectionInterruptibly("roll back to a savepoint"); 194 connectionLockAcquired = true; 195 assertNotCompleted("roll back to a savepoint"); 196 getConnection().rollback(savepoint); 197 getMetricsCollectorDispatcher().didRollbackToSavepoint(this, getDatabaseType()); 198 } catch (SQLException e) { 199 this.rollbackOnly.set(true); 200 throw databaseException("Unable to roll back to savepoint", e); 201 } catch (RuntimeException | Error e) { 202 this.rollbackOnly.set(true); 203 throw e; 204 } finally { 205 if (connectionLockAcquired) 206 getConnectionLock().unlock(); 207 } 208 } 209 210 /** 211 * Releases the provided transaction savepoint. 212 * <p> 213 * For most application code, prefer {@link #withSavepoint(TransactionalOperation)} or 214 * {@link #withSavepoint(ReturningTransactionalOperation)} so rollback and release cleanup are handled automatically. 215 * 216 * @param savepoint the savepoint to release 217 * @throws IllegalStateException if this transaction has already completed 218 * @since 4.1.0 219 */ 220 public void releaseSavepoint(@NonNull Savepoint savepoint) { 221 requireNonNull(savepoint); 222 assertNotCompleted("release a savepoint"); 223 releaseSavepointJdbc(savepoint); 224 } 225 226 /** 227 * Performs an operation inside a transaction savepoint. 228 * <p> 229 * If {@code transactionalOperation} completes successfully, the savepoint is released when the driver supports release. 230 * If an exception bubbles out, Pyranid rolls back to the savepoint, attempts to release it, and preserves cleanup failures 231 * as suppressed exceptions on the thrown exception. 232 * <p> 233 * Nested savepoint usage should be stack-like: finish inner savepoints before manually releasing or rolling back outer 234 * savepoints. 235 * 236 * @param transactionalOperation the operation to perform inside a savepoint 237 * @throws IllegalStateException if this transaction has already completed 238 * @since 4.1.0 239 */ 240 public void withSavepoint(@NonNull TransactionalOperation transactionalOperation) { 241 requireNonNull(transactionalOperation); 242 243 withSavepoint(() -> { 244 transactionalOperation.perform(); 245 return Optional.empty(); 246 }); 247 } 248 249 /** 250 * Performs an operation inside a transaction savepoint and optionally returns a value. 251 * <p> 252 * If {@code transactionalOperation} completes successfully, the savepoint is released when the driver supports release. 253 * If an exception bubbles out, Pyranid rolls back to the savepoint, attempts to release it, and preserves cleanup failures 254 * as suppressed exceptions on the thrown exception. 255 * <p> 256 * Nested savepoint usage should be stack-like: finish inner savepoints before manually releasing or rolling back outer 257 * savepoints. 258 * 259 * @param transactionalOperation the operation to perform inside a savepoint 260 * @param <T> the type to be returned 261 * @return the result of the operation 262 * @throws IllegalStateException if this transaction has already completed 263 * @since 4.1.0 264 */ 265 @NonNull 266 public <T> Optional<T> withSavepoint(@NonNull ReturningTransactionalOperation<T> transactionalOperation) { 267 requireNonNull(transactionalOperation); 268 assertNotCompleted("run a savepoint operation"); 269 270 Savepoint savepoint = createSavepoint(); 271 272 try { 273 Optional<T> returnValue = transactionalOperation.perform(); 274 275 if (returnValue == null) 276 returnValue = Optional.empty(); 277 278 releaseSavepointAfterSuccess(savepoint); 279 return returnValue; 280 } catch (RuntimeException e) { 281 cleanupSavepointAfterFailure(savepoint, e); 282 throw e; 283 } catch (Error e) { 284 cleanupSavepointAfterFailure(savepoint, e); 285 throw e; 286 } catch (Throwable t) { 287 RuntimeException wrapped = new RuntimeException(t); 288 cleanupSavepointAfterFailure(savepoint, wrapped); 289 throw wrapped; 290 } 291 } 292 293 /** 294 * Should this transaction be rolled back upon completion? 295 * <p> 296 * Default value is {@code false}. 297 * 298 * @return {@code true} if this transaction should be rolled back, {@code false} otherwise 299 */ 300 @NonNull 301 public Boolean isRollbackOnly() { 302 return this.rollbackOnly.get(); 303 } 304 305 /** 306 * Sets whether this transaction should be rolled back upon completion. 307 * 308 * @param rollbackOnly whether to set this transaction to be rollback-only 309 */ 310 public void setRollbackOnly(@NonNull Boolean rollbackOnly) { 311 requireNonNull(rollbackOnly); 312 assertNotCompleted("set rollback-only state"); 313 this.rollbackOnly.set(rollbackOnly); 314 } 315 316 /** 317 * Adds an operation to the list of operations to be executed when the transaction completes. 318 * <p> 319 * The supplied operation receives {@link TransactionResult#COMMITTED} if commit completed successfully, 320 * {@link TransactionResult#ROLLED_BACK} if Pyranid can prove that no application transaction work committed (including when 321 * a failed physical-transaction begin is discarded before application transaction work could execute), or 322 * {@link TransactionResult#IN_DOUBT} if Pyranid cannot prove the final database outcome after application transaction work 323 * could have executed. A {@code ROLLED_BACK} result therefore does not necessarily mean that Pyranid invoked the JDBC 324 * {@code rollback()} method. 325 * <p> 326 * If the operation throws, Pyranid wraps the thrown value in a {@link PostTransactionOperationException}. If another 327 * transaction or cleanup failure is already primary, the wrapper is suppressed onto that primary failure; otherwise, 328 * {@link Database#transaction(TransactionalOperation)} throws the wrapper as the primary failure. 329 * 330 * @param postTransactionOperation the post-transaction operation to add 331 */ 332 public void addPostTransactionOperation(@NonNull Consumer<TransactionResult> postTransactionOperation) { 333 requireNonNull(postTransactionOperation); 334 assertNotCompleted("add a post-transaction operation"); 335 this.postTransactionOperations.add(postTransactionOperation); 336 } 337 338 /** 339 * Removes an operation from the list of operations to be executed when the transaction completes. 340 * 341 * @param postTransactionOperation the post-transaction operation to remove 342 * @return {@code true} if the post-transaction operation was removed, {@code false} otherwise 343 */ 344 @NonNull 345 public Boolean removePostTransactionOperation(@NonNull Consumer<TransactionResult> postTransactionOperation) { 346 requireNonNull(postTransactionOperation); 347 assertNotCompleted("remove a post-transaction operation"); 348 return this.postTransactionOperations.remove(postTransactionOperation); 349 } 350 351 /** 352 * Gets an unmodifiable list of post-transaction operations. 353 * <p> 354 * To manipulate the list, use {@link #addPostTransactionOperation(Consumer)} and 355 * {@link #removePostTransactionOperation(Consumer)}. 356 * 357 * @return the list of post-transaction operations 358 */ 359 @NonNull 360 public List<@NonNull Consumer<TransactionResult>> getPostTransactionOperations() { 361 return Collections.unmodifiableList(this.postTransactionOperations); 362 } 363 364 /** 365 * Get the isolation level for this transaction. 366 * 367 * @return the isolation level 368 */ 369 @NonNull 370 public TransactionIsolation getTransactionIsolation() { 371 return this.transactionIsolation; 372 } 373 374 /** 375 * Gets the options used to create this transaction. 376 * 377 * @return transaction options 378 * @since 4.2.0 379 */ 380 @NonNull 381 public TransactionOptions getTransactionOptions() { 382 return this.transactionOptions; 383 } 384 385 @NonNull 386 Long id() { 387 return this.id; 388 } 389 390 @NonNull 391 Boolean hasConnection() { 392 getConnectionLock().lock(); 393 394 try { 395 return this.connection != null; 396 } finally { 397 getConnectionLock().unlock(); 398 } 399 } 400 401 @NonNull 402 Boolean isOwnedBy(@NonNull DataSource dataSource) { 403 requireNonNull(dataSource); 404 return this.dataSource == dataSource; 405 } 406 407 void commit() { 408 getConnectionLock().lock(); 409 410 try { 411 throwPhysicalTransactionBeginFailureIfPresent(); 412 413 if (!hasConnection()) { 414 LOGGER.finer("Transaction has no connection, so nothing to commit"); 415 return; 416 } 417 418 LOGGER.finer("Committing transaction..."); 419 420 long startTime = nanoTime(); 421 422 try { 423 getConnection().commit(); 424 getMetricsCollectorDispatcher().didCommitPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime)); 425 LOGGER.finer("Transaction committed."); 426 } catch (SQLException e) { 427 DatabaseException wrapped = databaseException("Unable to commit transaction", e); 428 429 // This catch is scoped to the physical JDBC commit call, so a serialization classification here cannot come from 430 // the transaction body or later cleanup. 431 if (wrapped.isSerializationFailure()) 432 this.commitSerializationFailure.set(true); 433 434 getMetricsCollectorDispatcher().didFailToCommitPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), wrapped); 435 throw wrapped; 436 } catch (RuntimeException | Error e) { 437 getMetricsCollectorDispatcher().didFailToCommitPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), e); 438 throw e; 439 } 440 } finally { 441 getConnectionLock().unlock(); 442 } 443 } 444 445 boolean didCommitFailWithSerializationFailure() { 446 return this.commitSerializationFailure.get(); 447 } 448 449 void rollback() { 450 getConnectionLock().lock(); 451 452 try { 453 if (!hasConnection()) { 454 LOGGER.finer("Transaction has no connection, so nothing to roll back"); 455 return; 456 } 457 458 if (!isPhysicalRollbackPermitted()) { 459 LOGGER.finer("Physical transaction did not reach a state where rollback is permitted"); 460 return; 461 } 462 463 LOGGER.finer("Rolling back transaction..."); 464 465 long startTime = nanoTime(); 466 467 try { 468 requireNonNull(this.connection).rollback(); 469 getMetricsCollectorDispatcher().didRollbackPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime)); 470 LOGGER.finer("Transaction rolled back."); 471 } catch (SQLException e) { 472 DatabaseException wrapped = databaseException("Unable to roll back transaction", e); 473 getMetricsCollectorDispatcher().didFailToRollbackPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), wrapped); 474 throw wrapped; 475 } catch (RuntimeException | Error e) { 476 getMetricsCollectorDispatcher().didFailToRollbackPhysicalTransaction(this, getDatabaseType(), Duration.ofNanos(nanoTime() - startTime), e); 477 throw e; 478 } 479 } finally { 480 getConnectionLock().unlock(); 481 } 482 } 483 484 /** 485 * The connection associated with this transaction. 486 * <p> 487 * If no connection is associated yet, we ask the {@link DataSource} for one. 488 * 489 * @return The connection associated with this transaction. 490 * @throws DatabaseException if unable to acquire a connection. 491 */ 492 @NonNull 493 Connection getConnection() { 494 lockConnectionInterruptibly("get the transaction connection"); 495 496 try { 497 assertNotCompleted("get the transaction connection"); 498 499 if (this.physicalTransactionBeginState == PhysicalTransactionBeginState.READY) 500 return requireNonNull(this.connection); 501 502 throwPhysicalTransactionBeginFailureIfPresent(); 503 504 if (this.physicalTransactionBeginState == PhysicalTransactionBeginState.STARTING) 505 throw new IllegalStateException("Physical transaction begin is already in progress"); 506 507 this.physicalTransactionBeginState = PhysicalTransactionBeginState.STARTING; 508 509 try { 510 return beginPhysicalTransaction(); 511 } catch (RuntimeException failure) { 512 RuntimeException normalizedFailure = failure instanceof DatabaseException 513 ? failure 514 : databaseException("Unable to begin physical transaction", failure); 515 this.physicalTransactionBeginFailure = normalizedFailure; 516 this.physicalTransactionBeginState = PhysicalTransactionBeginState.FAILED; 517 throw normalizedFailure; 518 } catch (Error failure) { 519 this.physicalTransactionBeginFailure = failure; 520 this.physicalTransactionBeginState = PhysicalTransactionBeginState.FAILED; 521 throw failure; 522 } 523 } finally { 524 getConnectionLock().unlock(); 525 } 526 } 527 528 @NonNull 529 private Connection beginPhysicalTransaction() { 530 long startTime = nanoTime(); 531 getMetricsCollectorDispatcher().willAcquireTransactionConnection(this, getDatabaseType()); 532 533 try { 534 this.connection = getDataSource().getConnection(); 535 } catch (SQLException e) { 536 DatabaseException wrapped = databaseException("Unable to acquire database connection", e); 537 Duration acquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 538 getMetricsCollectorDispatcher().didFailToAcquireTransactionConnection(this, getDatabaseType(), acquisitionDuration, wrapped); 539 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 540 MetricsCollector.PhysicalTransactionBeginFailurePhase.ACQUIRE_CONNECTION, getDatabaseType(), wrapped); 541 throw wrapped; 542 } catch (RuntimeException e) { 543 Duration acquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 544 getMetricsCollectorDispatcher().didFailToAcquireTransactionConnection(this, getDatabaseType(), acquisitionDuration, e); 545 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 546 MetricsCollector.PhysicalTransactionBeginFailurePhase.ACQUIRE_CONNECTION, getDatabaseType(), e); 547 throw e; 548 } 549 550 this.connectionAcquiredAtNanos = nanoTime(); 551 resolveDatabaseType(requireNonNull(this.connection)); 552 getMetricsCollectorDispatcher().didAcquireTransactionConnection(this, getDatabaseType(), Duration.ofNanos(this.connectionAcquiredAtNanos - startTime)); 553 554 // Keep track of the initial setting for autocommit since it might need to get changed from "true" to "false" for 555 // the duration of the transaction and then back to "true" post-transaction. 556 try { 557 this.initialAutoCommit = this.connection.getAutoCommit(); 558 this.physicalRollbackPermitted = !this.initialAutoCommit; 559 } catch (SQLException e) { 560 DatabaseException wrapped = databaseException("Unable to determine database connection autocommit setting", e); 561 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 562 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_AUTOCOMMIT, getDatabaseType(), wrapped); 563 throw wrapped; 564 } 565 566 // Track initial isolation 567 try { 568 this.initialTransactionIsolationJdbcLevel = this.connection.getTransactionIsolation(); 569 } catch (SQLException e) { 570 DatabaseException wrapped = databaseException("Unable to determine database connection transaction isolation", e); 571 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 572 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_ISOLATION, getDatabaseType(), wrapped); 573 throw wrapped; 574 } 575 576 try { 577 this.initialReadOnly = this.connection.isReadOnly(); 578 } catch (SQLException e) { 579 DatabaseException wrapped = databaseException("Unable to determine database connection read-only setting", e); 580 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 581 MetricsCollector.PhysicalTransactionBeginFailurePhase.READ_INITIAL_READ_ONLY, getDatabaseType(), wrapped); 582 throw wrapped; 583 } 584 585 Boolean desiredReadOnly = getTransactionOptions().getReadOnly().orElse(null); 586 587 if (desiredReadOnly != null && !desiredReadOnly.equals(this.initialReadOnly)) { 588 try { 589 this.connection.setReadOnly(desiredReadOnly); 590 this.readOnlyWasChanged.set(true); 591 } catch (SQLException e) { 592 DatabaseException wrapped = databaseException(format("Unable to set database connection read-only value to '%s'", desiredReadOnly), e); 593 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 594 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_READ_ONLY, getDatabaseType(), wrapped); 595 throw wrapped; 596 } 597 } 598 599 // Immediately flip autocommit to false if needed...if initially true, it will get set back to true by Database at 600 // the end of the transaction 601 if (this.initialAutoCommit) { 602 try { 603 setAutoCommit(false); 604 this.physicalRollbackPermitted = true; 605 } catch (DatabaseException e) { 606 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 607 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_AUTOCOMMIT_FALSE, getDatabaseType(), e); 608 throw e; 609 } 610 } 611 612 // Apply requested isolation if not DEFAULT and different from current 613 TransactionIsolation desiredTransactionIsolation = getTransactionIsolation(); 614 615 if (desiredTransactionIsolation != TransactionIsolation.DEFAULT) { 616 // Safe; only DEFAULT has a null value 617 int desiredJdbcLevel = desiredTransactionIsolation.getJdbcLevel().get(); 618 // Apply only if different from current (or current unknown) 619 if (this.initialTransactionIsolationJdbcLevel == null || this.initialTransactionIsolationJdbcLevel.intValue() != desiredJdbcLevel) { 620 try { 621 // In the future, we might check supportsTransactionIsolationLevel via DatabaseMetaData first. 622 // Probably want to calculate that at Database init time and cache it off 623 this.connection.setTransactionIsolation(desiredJdbcLevel); 624 this.transactionIsolationWasChanged.set(true); 625 } catch (SQLException e) { 626 DatabaseException wrapped = databaseException(format("Unable to set transaction isolation to %s", desiredTransactionIsolation.name()), e); 627 getMetricsCollectorDispatcher().didFailToBeginPhysicalTransaction(this, getTransactionIsolation(), 628 MetricsCollector.PhysicalTransactionBeginFailurePhase.SET_ISOLATION, getDatabaseType(), wrapped); 629 throw wrapped; 630 } 631 } 632 } 633 634 this.physicalTransactionBeginState = PhysicalTransactionBeginState.READY; 635 getMetricsCollectorDispatcher().didBeginPhysicalTransaction(this, getTransactionIsolation(), getDatabaseType()); 636 return requireNonNull(this.connection); 637 } 638 639 boolean didPhysicalTransactionBeginFail() { 640 return this.physicalTransactionBeginState == PhysicalTransactionBeginState.FAILED; 641 } 642 643 boolean couldApplicationWorkHaveExecuted() { 644 return didPhysicalTransactionBeginSuccessfully(); 645 } 646 647 boolean didPhysicalTransactionBeginSuccessfully() { 648 return this.physicalTransactionBeginState == PhysicalTransactionBeginState.READY; 649 } 650 651 boolean isPhysicalRollbackPermitted() { 652 return this.physicalRollbackPermitted; 653 } 654 655 void throwPhysicalTransactionBeginFailureIfPresent() { 656 if (!didPhysicalTransactionBeginFail()) 657 return; 658 659 Throwable failure = requireNonNull(this.physicalTransactionBeginFailure); 660 661 if (failure instanceof RuntimeException runtimeException) 662 throw runtimeException; 663 664 if (failure instanceof Error error) 665 throw error; 666 667 throw new AssertionError("Unexpected checked physical transaction begin failure", failure); 668 } 669 670 void setAutoCommit(@NonNull Boolean autoCommit) { 671 requireNonNull(autoCommit); 672 673 getConnectionLock().lock(); 674 675 try { 676 Connection connection = this.connection; 677 678 if (connection == null) 679 throw databaseException("Transaction has no connection", null); 680 681 try { 682 connection.setAutoCommit(autoCommit); 683 } catch (SQLException e) { 684 throw databaseException(format("Unable to set database connection autocommit value to '%s'", autoCommit), e); 685 } 686 } finally { 687 getConnectionLock().unlock(); 688 } 689 } 690 691 void restoreTransactionIsolationIfNeeded() { 692 getConnectionLock().lock(); 693 694 try { 695 if (this.connection == null) 696 return; 697 698 Integer initialTransactionIsolationJdbcLevel = getInitialTransactionIsolationJdbcLevel().orElse(null); 699 700 if (getTransactionIsolationWasChanged() && initialTransactionIsolationJdbcLevel != null) { 701 try { 702 this.connection.setTransactionIsolation(initialTransactionIsolationJdbcLevel.intValue()); 703 } catch (SQLException e) { 704 throw databaseException("Unable to restore original transaction isolation", e); 705 } finally { 706 this.transactionIsolationWasChanged.set(false); 707 } 708 } 709 } finally { 710 getConnectionLock().unlock(); 711 } 712 } 713 714 void restoreReadOnlyIfNeeded() { 715 getConnectionLock().lock(); 716 717 try { 718 if (this.connection == null) 719 return; 720 721 Boolean initialReadOnly = getInitialReadOnly().orElse(null); 722 723 if (getReadOnlyWasChanged() && initialReadOnly != null) { 724 try { 725 this.connection.setReadOnly(initialReadOnly); 726 } catch (SQLException e) { 727 throw databaseException("Unable to restore original read-only setting", e); 728 } finally { 729 this.readOnlyWasChanged.set(false); 730 } 731 } 732 } finally { 733 getConnectionLock().unlock(); 734 } 735 } 736 737 @NonNull 738 Long generateId() { 739 return ID_GENERATOR.incrementAndGet(); 740 } 741 742 @NonNull 743 Optional<Boolean> getInitialAutoCommit() { 744 return Optional.ofNullable(this.initialAutoCommit); 745 } 746 747 @NonNull 748 Optional<Boolean> getInitialReadOnly() { 749 return Optional.ofNullable(this.initialReadOnly); 750 } 751 752 @NonNull 753 DataSource getDataSource() { 754 return this.dataSource; 755 } 756 757 @NonNull 758 private Optional<Integer> getInitialTransactionIsolationJdbcLevel() { 759 return Optional.ofNullable(this.initialTransactionIsolationJdbcLevel); 760 } 761 762 @NonNull 763 private Boolean getTransactionIsolationWasChanged() { 764 return this.transactionIsolationWasChanged.get(); 765 } 766 767 @NonNull 768 private Boolean getReadOnlyWasChanged() { 769 return this.readOnlyWasChanged.get(); 770 } 771 772 @NonNull 773 ReentrantLock getConnectionLock() { 774 return this.connectionLock; 775 } 776 777 @NonNull 778 Optional<Connection> getExistingConnection() { 779 getConnectionLock().lock(); 780 781 try { 782 return Optional.ofNullable(this.connection); 783 } finally { 784 getConnectionLock().unlock(); 785 } 786 } 787 788 void clearConnection() { 789 getConnectionLock().lock(); 790 791 try { 792 this.connection = null; 793 } finally { 794 getConnectionLock().unlock(); 795 } 796 } 797 798 void markCompleted() { 799 this.completed.set(true); 800 } 801 802 @NonNull 803 Boolean isCompleted() { 804 return this.completed.get(); 805 } 806 807 private void releaseSavepointAfterSuccess(@NonNull Savepoint savepoint) { 808 requireNonNull(savepoint); 809 lockConnectionInterruptibly("release a savepoint"); 810 811 try { 812 getConnection().releaseSavepoint(savepoint); 813 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 814 } catch (SQLFeatureNotSupportedException e) { 815 // Some drivers support rollback-to-savepoint but not release; successful closures should still succeed. 816 } catch (SQLException e) { 817 throw databaseException("Unable to release savepoint", e); 818 } finally { 819 getConnectionLock().unlock(); 820 } 821 } 822 823 private void cleanupSavepointAfterFailure(@NonNull Savepoint savepoint, 824 @NonNull Throwable primary) { 825 requireNonNull(savepoint); 826 requireNonNull(primary); 827 828 boolean rollbackConnectionLockAcquired = false; 829 830 try { 831 lockConnectionInterruptibly("roll back to a savepoint"); 832 rollbackConnectionLockAcquired = true; 833 getConnection().rollback(savepoint); 834 getMetricsCollectorDispatcher().didRollbackToSavepoint(this, getDatabaseType()); 835 } catch (Throwable rollbackException) { 836 this.rollbackOnly.set(true); 837 primary.addSuppressed(databaseException("Unable to roll back to savepoint", rollbackException)); 838 } finally { 839 if (rollbackConnectionLockAcquired) 840 getConnectionLock().unlock(); 841 } 842 843 boolean releaseConnectionLockAcquired = false; 844 845 try { 846 lockConnectionInterruptibly("release a savepoint"); 847 releaseConnectionLockAcquired = true; 848 getConnection().releaseSavepoint(savepoint); 849 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 850 } catch (SQLFeatureNotSupportedException e) { 851 // Some drivers support rollback-to-savepoint but not release. 852 } catch (Throwable releaseException) { 853 primary.addSuppressed(databaseException("Unable to release savepoint", releaseException)); 854 } finally { 855 if (releaseConnectionLockAcquired) 856 getConnectionLock().unlock(); 857 } 858 } 859 860 private void releaseSavepointJdbc(@NonNull Savepoint savepoint) { 861 requireNonNull(savepoint); 862 lockConnectionInterruptibly("release a savepoint"); 863 864 try { 865 getConnection().releaseSavepoint(savepoint); 866 getMetricsCollectorDispatcher().didReleaseSavepoint(this, getDatabaseType()); 867 } catch (SQLException e) { 868 throw databaseException("Unable to release savepoint", e); 869 } finally { 870 getConnectionLock().unlock(); 871 } 872 } 873 874 private void lockConnectionInterruptibly(@NonNull String operation) { 875 requireNonNull(operation); 876 ReentrantLock connectionLock = getConnectionLock(); 877 878 // Interruptibility is for a participant waiting on another thread. Reentrant acquisition never waits, and using 879 // lockInterruptibly() here would instead make a pending interrupt abort commit/rollback before JDBC cleanup runs. 880 if (connectionLock.isHeldByCurrentThread()) { 881 connectionLock.lock(); 882 return; 883 } 884 885 try { 886 connectionLock.lockInterruptibly(); 887 } catch (InterruptedException e) { 888 Thread.currentThread().interrupt(); 889 throw databaseException(format("Interrupted while waiting to %s", operation), e); 890 } 891 } 892 893 private void resolveDatabaseType(@NonNull Connection connection) { 894 requireNonNull(connection); 895 896 if (this.databaseType != DatabaseType.GENERIC) 897 return; 898 899 try { 900 this.databaseType = requireNonNull(this.databaseTypeResolver.apply(connection)); 901 } catch (Throwable t) { 902 LOGGER.log(Level.FINE, "Unable to determine database type from transaction connection", t); 903 } 904 } 905 906 @NonNull 907 private DatabaseException databaseException(@NonNull String message, 908 @Nullable Throwable cause) { 909 requireNonNull(message); 910 911 DatabaseDialect databaseDialect = getDatabaseType() == DatabaseType.GENERIC 912 ? DatabaseDialect.forExceptionCause(cause) 913 : getDatabaseType().dialect(); 914 return new DatabaseException(message, cause, databaseDialect); 915 } 916 917 @NonNull 918 MetricsCollectorDispatcher getMetricsCollectorDispatcher() { 919 return this.metricsCollectorDispatcher; 920 } 921 922 @NonNull 923 DatabaseType getDatabaseType() { 924 return this.databaseType; 925 } 926 927 @NonNull 928 Optional<Long> getConnectionAcquiredAtNanos() { 929 return Optional.ofNullable(this.connectionAcquiredAtNanos); 930 } 931 932 private void assertNotCompleted(@NonNull String operation) { 933 requireNonNull(operation); 934 935 if (isCompleted()) 936 throw new IllegalStateException(format("Transaction %s has already completed and cannot %s", id(), operation)); 937 } 938 939 private enum PhysicalTransactionBeginState { 940 NOT_STARTED, 941 STARTING, 942 READY, 943 FAILED 944 } 945}