1 /*-
2  * Copyright (c) 2000, 2020 Oracle and/or its affiliates.  All rights reserved.
3  *
4  * See the file LICENSE for license information.
5  *
6  */
7 
8 package com.sleepycat.collections;
9 
10 import com.sleepycat.compat.DbCompat;
11 import com.sleepycat.db.DatabaseException;
12 import com.sleepycat.db.DeadlockException;
13 import com.sleepycat.db.Environment;
14 import com.sleepycat.db.Transaction;
15 import com.sleepycat.db.TransactionConfig;
16 import com.sleepycat.util.ExceptionUnwrapper;
17 
18 /**
19  * Starts a transaction, calls {@link TransactionWorker#doWork}, and handles
20  * transaction retry and exceptions.  To perform a transaction, the user
21  * implements the {@link TransactionWorker} interface and passes an instance of
22  * that class to the {@link #run run} method.
23  *
24  * <p>A single TransactionRunner instance may be used by any number of threads
25  * for any number of transactions.</p>
26  *
27  * <p>The behavior of the run() method depends on whether the environment is
28  * transactional, whether nested transactions are enabled, and whether a
29  * transaction is already active.</p>
30  *
31  * <ul>
32  * <li>When the run() method is called in a transactional environment and no
33  * transaction is active for the current thread, a new transaction is started
34  * before calling doWork().  If DeadlockException is thrown by doWork(),
35  * the transaction will be aborted and the process will be repeated up to the
36  * maximum number of retries.  If another exception is thrown by doWork() or
37  * the maximum number of retries has occurred, the transaction will be aborted
38  * and the exception will be rethrown by the run() method.  If no exception is
39  * thrown by doWork(), the transaction will be committed.  The run() method
40  * will not attempt to commit or abort a transaction if it has already been
41  * committed or aborted by doWork().</li>
42  *
43  * <li>When the run() method is called and a transaction is active for the
44  * current thread, and nested transactions are enabled, a nested transaction is
45  * started before calling doWork().  The transaction that is active when
46  * calling the run() method will become the parent of the nested transaction.
47  * The nested transaction will be committed or aborted by the run() method
48  * following the same rules described above.  Note that nested transactions may
49  * not be enabled for the JE product, since JE does not support nested
50  * transactions.</li>
51  *
52  * <li>When the run() method is called in a non-transactional environment, the
53  * doWork() method is called without starting a transaction.  The run() method
54  * will return without committing or aborting a transaction, and any exceptions
55  * thrown by the doWork() method will be thrown by the run() method.</li>
56  *
57  * <li>When the run() method is called and a transaction is active for the
58  * current thread and nested transactions are not enabled (the default) the
59  * same rules as above apply. All the operations performed by the doWork()
60  * method will be part of the currently active transaction.</li>
61  * </ul>
62  *
63  * <p>In a transactional environment, the rules described above support nested
64  * calls to the run() method and guarantee that the outermost call will cause
65  * the transaction to be committed or aborted.  This is true whether or not
66  * nested transactions are supported or enabled.  Note that nested transactions
67  * are provided as an optimization for improving concurrency but do not change
68  * the meaning of the outermost transaction.  Nested transactions are not
69  * currently supported by the JE product.</p>
70  *
71  * @author Mark Hayes
72  */
73 public class TransactionRunner {
74 
75     /** The default maximum number of retries. */
76     public static final int DEFAULT_MAX_RETRIES = 10;
77 
78     private CurrentTransaction currentTxn;
79     private int maxRetries;
80     private TransactionConfig config;
81     private boolean allowNestedTxn;
82 
83     /**
84      * Creates a transaction runner for a given Berkeley DB environment.
85      * The default maximum number of retries ({@link #DEFAULT_MAX_RETRIES}) and
86      * a null (default) {@link TransactionConfig} will be used.
87      *
88      * @param env is the environment for running transactions.
89      */
TransactionRunner(Environment env)90     public TransactionRunner(Environment env) {
91 
92         this(env, DEFAULT_MAX_RETRIES, null);
93     }
94 
95     /**
96      * Creates a transaction runner for a given Berkeley DB environment and
97      * with a given number of maximum retries.
98      *
99      * @param env is the environment for running transactions.
100      *
101      * @param maxRetries is the maximum number of retries that will be
102      * performed when deadlocks are detected.
103      *
104      * @param config the transaction configuration used for calling
105      * {@link Environment#beginTransaction}, or null to use the default
106      * configuration.  The configuration object is not cloned, and
107      * any modifications to it will impact subsequent transactions.
108      */
TransactionRunner(Environment env, int maxRetries, TransactionConfig config)109     public TransactionRunner(Environment env,
110                              int maxRetries,
111                              TransactionConfig config) {
112 
113         this.currentTxn = CurrentTransaction.getInstance(env);
114         this.maxRetries = maxRetries;
115         this.config = config;
116     }
117 
118     /**
119      * Returns the maximum number of retries that will be performed when
120      * deadlocks are detected.
121      *
122      * @return the maximum number of retries.
123      */
getMaxRetries()124     public int getMaxRetries() {
125 
126         return maxRetries;
127     }
128 
129     /**
130      * Changes the maximum number of retries that will be performed when
131      * deadlocks are detected.
132      * Calling this method does not impact transactions already running.
133      *
134      * @param maxRetries the maximum number of retries.
135      */
setMaxRetries(int maxRetries)136     public void setMaxRetries(int maxRetries) {
137 
138         this.maxRetries = maxRetries;
139     }
140 
141     /**
142      * Returns whether nested transactions will be created if
143      * <code>run()</code> is called when a transaction is already active for
144      * the current thread.
145      * By default this property is false.
146      *
147      * @return whether nested transactions will be created.
148      *
149      * <p>Note that this method always returns false in the JE product, since
150      * nested transactions are not supported by JE.</p>
151      */
getAllowNestedTransactions()152     public boolean getAllowNestedTransactions() {
153 
154         return allowNestedTxn;
155     }
156 
157     /**
158      * Changes whether nested transactions will be created if
159      * <code>run()</code> is called when a transaction is already active for
160      * the current thread.
161      * Calling this method does not impact transactions already running.
162      *
163      * @param allowNestedTxn whether nested transactions will be created.
164      *
165      * <p>Note that true may not be passed to this method in the JE product,
166      * since nested transactions are not supported by JE.</p>
167      */
setAllowNestedTransactions(boolean allowNestedTxn)168     public void setAllowNestedTransactions(boolean allowNestedTxn) {
169 
170         if (allowNestedTxn && !DbCompat.NESTED_TRANSACTIONS) {
171             throw new UnsupportedOperationException
172                 ("Nested transactions are not supported.");
173         }
174         this.allowNestedTxn = allowNestedTxn;
175     }
176 
177     /**
178      * Returns the transaction configuration used for calling
179      * {@link Environment#beginTransaction}.
180      *
181      * <p>If this property is null, the default configuration is used.  The
182      * configuration object is not cloned, and any modifications to it will
183      * impact subsequent transactions.</p>
184      *
185      * @return the transaction configuration.
186      */
getTransactionConfig()187     public TransactionConfig getTransactionConfig() {
188 
189         return config;
190     }
191 
192     /**
193      * Changes the transaction configuration used for calling
194      * {@link Environment#beginTransaction}.
195      *
196      * <p>If this property is null, the default configuration is used.  The
197      * configuration object is not cloned, and any modifications to it will
198      * impact subsequent transactions.</p>
199      *
200      * @param config the transaction configuration.
201      */
setTransactionConfig(TransactionConfig config)202     public void setTransactionConfig(TransactionConfig config) {
203 
204         this.config = config;
205     }
206 
207     /**
208      * Calls the {@link TransactionWorker#doWork} method and, for transactional
209      * environments, may begin and end a transaction.  If the environment given
210      * is non-transactional, a transaction will not be used but the doWork()
211      * method will still be called.  See the class description for more
212      * information.
213      *
214      * @param worker the TransactionWorker.
215      *
216      * @throws DeadlockException when it is thrown by doWork() and the
217      * maximum number of retries has occurred.  The transaction will have been
218      * aborted by this method.
219      *
220      * @throws Exception when any other exception is thrown by doWork().  The
221      * exception will first be unwrapped by calling {@link
222      * ExceptionUnwrapper#unwrap}.  The transaction will have been aborted by
223      * this method.
224      */
run(TransactionWorker worker)225     public void run(TransactionWorker worker)
226         throws DatabaseException, Exception {
227 
228         if (currentTxn != null &&
229             (allowNestedTxn || currentTxn.getTransaction() == null)) {
230             /* Transactional and (not nested or nested txns allowed). */
231             int useMaxRetries = maxRetries;
232             for (int retries = 0;; retries += 1) {
233                 Transaction txn = null;
234                 try {
235                     txn = currentTxn.beginTransaction(config);
236                     worker.doWork();
237                     if (txn != null && txn == currentTxn.getTransaction()) {
238                         currentTxn.commitTransaction();
239                     }
240                     return;
241                 } catch (Throwable e) {
242                     e = ExceptionUnwrapper.unwrapAny(e);
243                     if (txn != null && txn == currentTxn.getTransaction()) {
244                         try {
245                             currentTxn.abortTransaction();
246                         } catch (Throwable e2) {
247 
248                             /*
249                              * We print this stack trace so that the
250                              * information is not lost when we throw the
251                              * original exception.
252                              */
253                             if (DbCompat.
254                                 TRANSACTION_RUNNER_PRINT_STACK_TRACES) {
255                                 e2.printStackTrace();
256                             }
257                             /* Force the original exception to be thrown. */
258                             retries = useMaxRetries;
259                         }
260                     }
261                     /* An Error should not require special handling. */
262                     if (e instanceof Error) {
263                         throw (Error) e;
264                     }
265                     /* Allow a subclass to determine retry policy. */
266                     Exception ex = (Exception) e;
267                     useMaxRetries =
268                         handleException(ex, retries, useMaxRetries);
269                     if (retries >= useMaxRetries) {
270                         throw ex;
271                     }
272                 }
273             }
274         } else {
275             /* Non-transactional or (nested and no nested txns allowed). */
276             try {
277                 worker.doWork();
278             } catch (Exception e) {
279                 throw ExceptionUnwrapper.unwrap(e);
280             }
281         }
282     }
283 
284     /**
285      * Handles exceptions that occur during a transaction, and may implement
286      * transaction retry policy.  The transaction is aborted by the {@link
287      * #run run} method before calling this method.
288      *
289      * <p>The default implementation of this method throws the {@code
290      * exception} parameter if it is not an instance of {@link
291      * DeadlockException} and otherwise returns the {@code maxRetries}
292      * parameter value.  This method can be overridden to throw a different
293      * exception or return a different number of retries.  For example:</p>
294      * <ul>
295      * <li>This method could call {@code Thread.sleep} for a short interval to
296      * allow other transactions to finish.</li>
297      *
298      * <li>This method could return a different {@code maxRetries} value
299      * depending on the {@code exception} that occurred.</li>
300      *
301      * <li>This method could throw an application-defined exception when the
302      * {@code retries} value is greater or equal to the {@code maxRetries} and
303      * a {@link DeadlockException} occurs, to override the default behavior
304      * which is to throw the {@link DeadlockException}.</li>
305      * </ul>
306      *
307      * @param exception an exception that was thrown by the {@link
308      * TransactionWorker#doWork} method or thrown when beginning or committing
309      * the transaction.  If the {@code retries} value is greater or equal to
310      * {@code maxRetries} when this method returns normally, this exception
311      * will be thrown by the {@link #run run} method.
312      *
313      * @param retries the current value of a counter that starts out at zero
314      * and is incremented when each retry is performed.
315      *
316      * @param maxRetries the maximum retries to be performed.  By default,
317      * this value is set to {@link #getMaxRetries}.  This method may return a
318      * different maximum retries value to override that default.
319      *
320      * @return the maximum number of retries to perform.  The
321      * default policy is to return the {@code maxRetries} parameter value
322      * if the {@code exception} parameter value is an instance of {@link
323      * DeadlockException}.
324      *
325      * @throws Exception to cause the exception to be thrown by the {@link
326      * #run run} method.  The default policy is to throw the {@code exception}
327      * parameter value if it is not an instance of {@link
328      * DeadlockException}.
329      *
330      * @since 3.4
331      */
handleException(Exception exception, int retries, int maxRetries)332     public int handleException(Exception exception,
333                                int retries,
334                                int maxRetries)
335         throws Exception {
336 
337         if (exception instanceof DeadlockException) {
338             return maxRetries;
339         } else {
340             throw exception;
341         }
342     }
343 }
344