001/*
002 * Copyright 2002-2019 the original author or authors.
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 *      https://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 org.springframework.transaction.reactive;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import reactor.core.publisher.Mono;
029
030import org.springframework.core.annotation.AnnotationAwareOrderComparator;
031import org.springframework.lang.Nullable;
032import org.springframework.transaction.NoTransactionException;
033import org.springframework.util.Assert;
034
035/**
036 * Central delegate that manages resources and transaction synchronizations per
037 * subscriber context.
038 * To be used by resource management code but not by typical application code.
039 *
040 * <p>Supports one resource per key without overwriting, that is, a resource needs
041 * to be removed before a new one can be set for the same key.
042 * Supports a list of transaction synchronizations if synchronization is active.
043 *
044 * <p>Resource management code should check for context-bound resources, e.g.
045 * database connections, via {@code getResource}. Such code is normally not
046 * supposed to bind resources to units of work, as this is the responsibility
047 * of transaction managers. A further option is to lazily bind on first use if
048 * transaction synchronization is active, for performing transactions that span
049 * an arbitrary number of resources.
050 *
051 * <p>Transaction synchronization must be activated and deactivated by a transaction
052 * manager via {@link #initSynchronization()} and {@link #clearSynchronization()}.
053 * This is automatically supported by {@link AbstractReactiveTransactionManager},
054 * and thus by all standard Spring transaction managers.
055 *
056 * <p>Resource management code should only register synchronizations when this
057 * manager is active, which can be checked via {@link #isSynchronizationActive};
058 * it should perform immediate resource cleanup else. If transaction synchronization
059 * isn't active, there is either no current transaction, or the transaction manager
060 * doesn't support transaction synchronization.
061 *
062 * <p>Synchronization is for example used to always return the same resources within
063 * a transaction, e.g. a database connection for any given connection factory.
064 *
065 * @author Mark Paluch
066 * @author Juergen Hoeller
067 * @since 5.2
068 * @see #isSynchronizationActive
069 * @see #registerSynchronization
070 * @see TransactionSynchronization
071 */
072public class TransactionSynchronizationManager {
073
074        private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
075
076        private final TransactionContext transactionContext;
077
078
079        public TransactionSynchronizationManager(TransactionContext transactionContext) {
080                this.transactionContext = transactionContext;
081        }
082
083
084        /**
085         * Get the {@link TransactionSynchronizationManager} that is associated with
086         * the current transaction context.
087         * <p>Mainly intended for code that wants to bind resources or synchronizations.
088         * @throws NoTransactionException if the transaction info cannot be found &mdash;
089         * for example, because the method was invoked outside a managed transaction
090         */
091        public static Mono<TransactionSynchronizationManager> forCurrentTransaction() {
092                return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new);
093        }
094
095        /**
096         * Check if there is a resource for the given key bound to the current thread.
097         * @param key the key to check (usually the resource factory)
098         * @return if there is a value bound to the current thread
099         */
100        public boolean hasResource(Object key) {
101                Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
102                Object value = doGetResource(actualKey);
103                return (value != null);
104        }
105
106        /**
107         * Retrieve a resource for the given key that is bound to the current thread.
108         * @param key the key to check (usually the resource factory)
109         * @return a value bound to the current thread (usually the active
110         * resource object), or {@code null} if none
111         */
112        @Nullable
113        public Object getResource(Object key) {
114                Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
115                Object value = doGetResource(actualKey);
116                if (value != null && logger.isTraceEnabled()) {
117                        logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to context [" +
118                                        this.transactionContext.getName() + "]");
119                }
120                return value;
121        }
122
123        /**
124         * Actually check the value of the resource that is bound for the given key.
125         */
126        @Nullable
127        private Object doGetResource(Object actualKey) {
128                return this.transactionContext.getResources().get(actualKey);
129        }
130
131        /**
132         * Bind the given resource for the given key to the current context.
133         * @param key the key to bind the value to (usually the resource factory)
134         * @param value the value to bind (usually the active resource object)
135         * @throws IllegalStateException if there is already a value bound to the context
136         */
137        public void bindResource(Object key, Object value) throws IllegalStateException {
138                Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
139                Assert.notNull(value, "Value must not be null");
140                Map<Object, Object> map = this.transactionContext.getResources();
141                Object oldValue = map.put(actualKey, value);
142                if (oldValue != null) {
143                        throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
144                                        actualKey + "] bound to context [" + this.transactionContext.getName() + "]");
145                }
146                if (logger.isTraceEnabled()) {
147                        logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to context [" +
148                                        this.transactionContext.getName() + "]");
149                }
150        }
151
152        /**
153         * Unbind a resource for the given key from the current context.
154         * @param key the key to unbind (usually the resource factory)
155         * @return the previously bound value (usually the active resource object)
156         * @throws IllegalStateException if there is no value bound to the context
157         */
158        public Object unbindResource(Object key) throws IllegalStateException {
159                Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
160                Object value = doUnbindResource(actualKey);
161                if (value == null) {
162                        throw new IllegalStateException(
163                                        "No value for key [" + actualKey + "] bound to context [" + this.transactionContext.getName() + "]");
164                }
165                return value;
166        }
167
168        /**
169         * Unbind a resource for the given key from the current context.
170         * @param key the key to unbind (usually the resource factory)
171         * @return the previously bound value, or {@code null} if none bound
172         */
173        @Nullable
174        public Object unbindResourceIfPossible(Object key) {
175                Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
176                return doUnbindResource(actualKey);
177        }
178
179        /**
180         * Actually remove the value of the resource that is bound for the given key.
181         */
182        @Nullable
183        private Object doUnbindResource(Object actualKey) {
184                Map<Object, Object> map = this.transactionContext.getResources();
185                Object value = map.remove(actualKey);
186                if (value != null && logger.isTraceEnabled()) {
187                        logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from context [" +
188                                        this.transactionContext.getName() + "]");
189                }
190                return value;
191        }
192
193
194        //-------------------------------------------------------------------------
195        // Management of transaction synchronizations
196        //-------------------------------------------------------------------------
197
198        /**
199         * Return if transaction synchronization is active for the current context.
200         * Can be called before register to avoid unnecessary instance creation.
201         * @see #registerSynchronization
202         */
203        public boolean isSynchronizationActive() {
204                return (this.transactionContext.getSynchronizations() != null);
205        }
206
207        /**
208         * Activate transaction synchronization for the current context.
209         * Called by a transaction manager on transaction begin.
210         * @throws IllegalStateException if synchronization is already active
211         */
212        public void initSynchronization() throws IllegalStateException {
213                if (isSynchronizationActive()) {
214                        throw new IllegalStateException("Cannot activate transaction synchronization - already active");
215                }
216                logger.trace("Initializing transaction synchronization");
217                this.transactionContext.setSynchronizations(new LinkedHashSet<>());
218        }
219
220        /**
221         * Register a new transaction synchronization for the current context.
222         * Typically called by resource management code.
223         * <p>Note that synchronizations can implement the
224         * {@link org.springframework.core.Ordered} interface.
225         * They will be executed in an order according to their order value (if any).
226         * @param synchronization the synchronization object to register
227         * @throws IllegalStateException if transaction synchronization is not active
228         * @see org.springframework.core.Ordered
229         */
230        public void registerSynchronization(TransactionSynchronization synchronization)
231                        throws IllegalStateException {
232
233                Assert.notNull(synchronization, "TransactionSynchronization must not be null");
234                Set<TransactionSynchronization> synchs = this.transactionContext.getSynchronizations();
235                if (synchs == null) {
236                        throw new IllegalStateException("Transaction synchronization is not active");
237                }
238                synchs.add(synchronization);
239        }
240
241        /**
242         * Return an unmodifiable snapshot list of all registered synchronizations
243         * for the current context.
244         * @return unmodifiable List of TransactionSynchronization instances
245         * @throws IllegalStateException if synchronization is not active
246         * @see TransactionSynchronization
247         */
248        public List<TransactionSynchronization> getSynchronizations() throws IllegalStateException {
249                Set<TransactionSynchronization> synchs = this.transactionContext.getSynchronizations();
250                if (synchs == null) {
251                        throw new IllegalStateException("Transaction synchronization is not active");
252                }
253                // Return unmodifiable snapshot, to avoid ConcurrentModificationExceptions
254                // while iterating and invoking synchronization callbacks that in turn
255                // might register further synchronizations.
256                if (synchs.isEmpty()) {
257                        return Collections.emptyList();
258                }
259                else {
260                        // Sort lazily here, not in registerSynchronization.
261                        List<TransactionSynchronization> sortedSynchs = new ArrayList<>(synchs);
262                        AnnotationAwareOrderComparator.sort(sortedSynchs);
263                        return Collections.unmodifiableList(sortedSynchs);
264                }
265        }
266
267        /**
268         * Deactivate transaction synchronization for the current context.
269         * Called by the transaction manager on transaction cleanup.
270         * @throws IllegalStateException if synchronization is not active
271         */
272        public void clearSynchronization() throws IllegalStateException {
273                if (!isSynchronizationActive()) {
274                        throw new IllegalStateException("Cannot deactivate transaction synchronization - not active");
275                }
276                logger.trace("Clearing transaction synchronization");
277                this.transactionContext.setSynchronizations(null);
278        }
279
280
281        //-------------------------------------------------------------------------
282        // Exposure of transaction characteristics
283        //-------------------------------------------------------------------------
284
285        /**
286         * Expose the name of the current transaction, if any.
287         * Called by the transaction manager on transaction begin and on cleanup.
288         * @param name the name of the transaction, or {@code null} to reset it
289         * @see org.springframework.transaction.TransactionDefinition#getName()
290         */
291        public void setCurrentTransactionName(@Nullable String name) {
292                this.transactionContext.setCurrentTransactionName(name);
293        }
294
295        /**
296         * Return the name of the current transaction, or {@code null} if none set.
297         * To be called by resource management code for optimizations per use case,
298         * for example to optimize fetch strategies for specific named transactions.
299         * @see org.springframework.transaction.TransactionDefinition#getName()
300         */
301        @Nullable
302        public String getCurrentTransactionName() {
303                return this.transactionContext.getCurrentTransactionName();
304        }
305
306        /**
307         * Expose a read-only flag for the current transaction.
308         * Called by the transaction manager on transaction begin and on cleanup.
309         * @param readOnly {@code true} to mark the current transaction
310         * as read-only; {@code false} to reset such a read-only marker
311         * @see org.springframework.transaction.TransactionDefinition#isReadOnly()
312         */
313        public void setCurrentTransactionReadOnly(boolean readOnly) {
314                this.transactionContext.setCurrentTransactionReadOnly(readOnly);
315        }
316
317        /**
318         * Return whether the current transaction is marked as read-only.
319         * To be called by resource management code when preparing a newly
320         * created resource.
321         * <p>Note that transaction synchronizations receive the read-only flag
322         * as argument for the {@code beforeCommit} callback, to be able
323         * to suppress change detection on commit. The present method is meant
324         * to be used for earlier read-only checks.
325         * @see org.springframework.transaction.TransactionDefinition#isReadOnly()
326         * @see TransactionSynchronization#beforeCommit(boolean)
327         */
328        public boolean isCurrentTransactionReadOnly() {
329                return this.transactionContext.isCurrentTransactionReadOnly();
330        }
331
332        /**
333         * Expose an isolation level for the current transaction.
334         * Called by the transaction manager on transaction begin and on cleanup.
335         * @param isolationLevel the isolation level to expose, according to the
336         * R2DBC Connection constants (equivalent to the corresponding Spring
337         * TransactionDefinition constants), or {@code null} to reset it
338         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED
339         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED
340         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ
341         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE
342         * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel()
343         */
344        public void setCurrentTransactionIsolationLevel(@Nullable Integer isolationLevel) {
345                this.transactionContext.setCurrentTransactionIsolationLevel(isolationLevel);
346        }
347
348        /**
349         * Return the isolation level for the current transaction, if any.
350         * To be called by resource management code when preparing a newly
351         * created resource (for example, a R2DBC Connection).
352         * @return the currently exposed isolation level, according to the
353         * R2DBC Connection constants (equivalent to the corresponding Spring
354         * TransactionDefinition constants), or {@code null} if none
355         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED
356         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED
357         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ
358         * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE
359         * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel()
360         */
361        @Nullable
362        public Integer getCurrentTransactionIsolationLevel() {
363                return this.transactionContext.getCurrentTransactionIsolationLevel();
364        }
365
366        /**
367         * Expose whether there currently is an actual transaction active.
368         * Called by the transaction manager on transaction begin and on cleanup.
369         * @param active {@code true} to mark the current context as being associated
370         * with an actual transaction; {@code false} to reset that marker
371         */
372        public void setActualTransactionActive(boolean active) {
373                this.transactionContext.setActualTransactionActive(active);
374        }
375
376        /**
377         * Return whether there currently is an actual transaction active.
378         * This indicates whether the current context is associated with an actual
379         * transaction rather than just with active transaction synchronization.
380         * <p>To be called by resource management code that wants to discriminate
381         * between active transaction synchronization (with or without backing
382         * resource transaction; also on PROPAGATION_SUPPORTS) and an actual
383         * transaction being active (with backing resource transaction;
384         * on PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, etc).
385         * @see #isSynchronizationActive()
386         */
387        public boolean isActualTransactionActive() {
388                return this.transactionContext.isActualTransactionActive();
389        }
390
391        /**
392         * Clear the entire transaction synchronization state:
393         * registered synchronizations as well as the various transaction characteristics.
394         * @see #clearSynchronization()
395         * @see #setCurrentTransactionName
396         * @see #setCurrentTransactionReadOnly
397         * @see #setCurrentTransactionIsolationLevel
398         * @see #setActualTransactionActive
399         */
400        public void clear() {
401                this.transactionContext.clear();
402        }
403
404}