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.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.function.Consumer; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Options for a Pyranid-managed transaction. 032 * <p> 033 * Use this when transaction behavior differs from the defaults. For example: 034 * <pre>{@code 035 * database.transaction( 036 * TransactionOptions.withIsolation(TransactionIsolation.REPEATABLE_READ) 037 * .readOnly(true) 038 * .build(), 039 * () -> { 040 * // transactional work 041 * }); 042 * }</pre> 043 * 044 * @author <a href="https://www.revetkn.com">Mark Allen</a> 045 * @since 4.2.0 046 */ 047@ThreadSafe 048public final class TransactionOptions { 049 @NonNull 050 static final TransactionOptions DEFAULT; 051 052 static { 053 DEFAULT = new TransactionOptions(TransactionIsolation.DEFAULT, null, null); 054 } 055 056 @NonNull 057 private final TransactionIsolation isolation; 058 @Nullable 059 private final Boolean readOnly; 060 @Nullable 061 private final Consumer<@NonNull TransactionCompletion> completionObserver; 062 063 private TransactionOptions(@NonNull TransactionIsolation isolation, 064 @Nullable Boolean readOnly, 065 @Nullable Consumer<@NonNull TransactionCompletion> completionObserver) { 066 requireNonNull(isolation); 067 068 this.isolation = isolation; 069 this.readOnly = readOnly; 070 this.completionObserver = completionObserver; 071 } 072 073 /** 074 * Starts building transaction options with a transaction isolation level. 075 * 076 * @param isolation the desired database transaction isolation level 077 * @return a transaction options builder 078 */ 079 @NonNull 080 public static Builder withIsolation(@NonNull TransactionIsolation isolation) { 081 return new Builder().isolation(isolation); 082 } 083 084 /** 085 * Starts building transaction options with a read-only setting. 086 * <p> 087 * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null} 088 * leaves the connection's read-only state unchanged. 089 * 090 * @param readOnly read-only setting to apply 091 * @return a transaction options builder 092 */ 093 @NonNull 094 public static Builder withReadOnly(@Nullable Boolean readOnly) { 095 return new Builder().readOnly(readOnly); 096 } 097 098 /** 099 * Gets the transaction isolation level. 100 * 101 * @return transaction isolation level 102 */ 103 @NonNull 104 public TransactionIsolation getIsolation() { 105 return this.isolation; 106 } 107 108 /** 109 * Gets the read-only setting to apply, if any. 110 * <p> 111 * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and empty leaves the 112 * connection's read-only state unchanged. 113 * 114 * @return read-only setting to apply 115 */ 116 @NonNull 117 public Optional<Boolean> getReadOnly() { 118 return Optional.ofNullable(this.readOnly); 119 } 120 121 /** 122 * Gets the diagnostic observer invoked after connection cleanup and all post-transaction hooks. 123 * 124 * @return completion observer, if configured 125 * @since 4.8.0 126 */ 127 @NonNull 128 public Optional<Consumer<@NonNull TransactionCompletion>> getCompletionObserver() { 129 return Optional.ofNullable(this.completionObserver); 130 } 131 132 @Override 133 public boolean equals(Object object) { 134 if (this == object) 135 return true; 136 137 if (!(object instanceof TransactionOptions)) 138 return false; 139 140 TransactionOptions transactionOptions = (TransactionOptions) object; 141 return getIsolation() == transactionOptions.getIsolation() 142 && Objects.equals(getReadOnly(), transactionOptions.getReadOnly()) 143 && Objects.equals(this.completionObserver, transactionOptions.completionObserver); 144 } 145 146 @Override 147 public int hashCode() { 148 return Objects.hash(getIsolation(), getReadOnly(), this.completionObserver); 149 } 150 151 @Override 152 @NonNull 153 public String toString() { 154 return String.format("%s{isolation=%s, readOnly=%s}", 155 getClass().getSimpleName(), getIsolation().name(), getReadOnly().map(String::valueOf).orElse("unchanged")); 156 } 157 158 /** 159 * Builder used to construct {@link TransactionOptions}. 160 * <p> 161 * This class is intended for use by a single thread. 162 * 163 * @author <a href="https://www.revetkn.com">Mark Allen</a> 164 * @since 4.2.0 165 */ 166 @NotThreadSafe 167 public static final class Builder { 168 @NonNull 169 private TransactionIsolation isolation; 170 @Nullable 171 private Boolean readOnly; 172 @Nullable 173 private Consumer<@NonNull TransactionCompletion> completionObserver; 174 175 private Builder() { 176 this.isolation = TransactionIsolation.DEFAULT; 177 this.readOnly = null; 178 } 179 180 /** 181 * Configures the transaction isolation level. 182 * 183 * @param isolation the desired database transaction isolation level 184 * @return this builder, for chaining 185 */ 186 @NonNull 187 public Builder isolation(@NonNull TransactionIsolation isolation) { 188 requireNonNull(isolation); 189 this.isolation = isolation; 190 return this; 191 } 192 193 /** 194 * Configures the transaction read-only setting. 195 * <p> 196 * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null} 197 * leaves the connection's read-only state unchanged. 198 * 199 * @param readOnly read-only setting to apply 200 * @return this builder, for chaining 201 */ 202 @NonNull 203 public Builder readOnly(@Nullable Boolean readOnly) { 204 this.readOnly = readOnly; 205 return this; 206 } 207 208 /** 209 * Configures diagnostic observation of each transaction boundary, including every retry attempt. 210 * <p> 211 * The observer runs synchronously after JDBC cleanup and all post-transaction hooks, before the transaction 212 * method returns or throws. Its failure is contained and does not change the transaction result or retry 213 * decisions. Use {@link Transaction#addPostTransactionOperation(Consumer)} for application hooks whose failures 214 * should propagate. Observers must be fast and thread-safe when these options are shared across threads. 215 * Argument validation failures before a transaction scope exists are not observed. 216 * 217 * @param completionObserver observer, or {@code null} to remove it 218 * @return this builder, for chaining 219 * @since 4.8.0 220 */ 221 @NonNull 222 public Builder completionObserver(@Nullable Consumer<@NonNull TransactionCompletion> completionObserver) { 223 this.completionObserver = completionObserver; 224 return this; 225 } 226 227 /** 228 * Builds transaction options. 229 * 230 * @return transaction options 231 */ 232 @NonNull 233 public TransactionOptions build() { 234 return new TransactionOptions(this.isolation, this.readOnly, this.completionObserver); 235 } 236 } 237}