1 /**
2  *  Copyright Terracotta, Inc.
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  */

16 package net.sf.ehcache;
17
18 import net.sf.ehcache.transaction.TransactionException;
19 import net.sf.ehcache.transaction.TransactionID;
20 import net.sf.ehcache.transaction.TransactionIDFactory;
21 import net.sf.ehcache.transaction.TransactionTimeoutException;
22 import net.sf.ehcache.transaction.local.LocalRecoveryManager;
23 import net.sf.ehcache.transaction.local.LocalTransactionContext;
24 import net.sf.ehcache.util.lang.VicariousThreadLocal;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.slf4j.MDC;
28
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.ConcurrentMap;
31 import java.util.concurrent.atomic.AtomicLong;
32
33 /**
34  * TransactionController is used to begin, commit and rollback local transactions
35  *
36  * @author Ludovic Orban
37  */

38 public final class TransactionController {
39
40     private static final Logger LOG = LoggerFactory.getLogger(TransactionController.class.getName());
41     private static final String MDC_KEY = "ehcache-txid";
42
43     private final VicariousThreadLocal<TransactionID> currentTransactionIdThreadLocal = new VicariousThreadLocal<TransactionID>();
44     private final ConcurrentMap<TransactionID, LocalTransactionContext> contextMap =
45             new ConcurrentHashMap<TransactionID, LocalTransactionContext>();
46     private final TransactionIDFactory transactionIDFactory;
47     private final LocalRecoveryManager localRecoveryManager;
48
49     private volatile int defaultTransactionTimeout;
50
51     private final TransactionControllerStatistics statistics = new TransactionControllerStatistics();
52
53     /**
54      * Create a TransactionController instance
55      * @param transactionIDFactory the TransactionIDFactory
56      * @param defaultTransactionTimeoutInSeconds the default transaction timeout in seconds
57      */

58     TransactionController(TransactionIDFactory transactionIDFactory, int defaultTransactionTimeoutInSeconds) {
59         this.transactionIDFactory = transactionIDFactory;
60         this.localRecoveryManager = new LocalRecoveryManager(transactionIDFactory);
61         this.defaultTransactionTimeout = defaultTransactionTimeoutInSeconds;
62     }
63
64     /**
65      * Get the default transaction timeout in seconds
66      * @return the default transaction timeout
67      */

68     public int getDefaultTransactionTimeout() {
69         return defaultTransactionTimeout;
70     }
71
72     /**
73      * Set the default transaction timeout in seconds, it must be > 0
74      * @param defaultTransactionTimeoutSeconds the default transaction timeout
75      */

76     public void setDefaultTransactionTimeout(int defaultTransactionTimeoutSeconds) {
77         if (defaultTransactionTimeoutSeconds < 0) {
78             throw new IllegalArgumentException("timeout cannot be < 0");
79         }
80         this.defaultTransactionTimeout = defaultTransactionTimeoutSeconds;
81     }
82
83     /**
84      * Begin a new transaction and bind its context to the current thread
85      */

86     public void begin() {
87         begin(defaultTransactionTimeout);
88     }
89
90     /**
91      * Begin a new transaction with the specified timeout and bind its context to the current thread
92      * @param transactionTimeoutSeconds the timeout foe this transaction in seconds
93      */

94     public void begin(int transactionTimeoutSeconds) {
95         TransactionID txId = currentTransactionIdThreadLocal.get();
96         if (txId != null) {
97             throw new TransactionException("transaction already started");
98         }
99
100         LocalTransactionContext newTx = new LocalTransactionContext(transactionTimeoutSeconds, transactionIDFactory);
101         contextMap.put(newTx.getTransactionId(), newTx);
102         currentTransactionIdThreadLocal.set(newTx.getTransactionId());
103
104         MDC.put(MDC_KEY, newTx.getTransactionId().toString());
105         LOG.debug("begun transaction {}", newTx.getTransactionId());
106     }
107
108     /**
109      * Commit the transaction bound to the current thread
110      */

111     public void commit() {
112         commit(false);
113     }
114
115     /**
116      * Commit the transaction bound to the current thread, ignoring if the transaction
117      * timed out
118      * @param ignoreTimeout true if the transaction should be committed no matter if it timed out or not
119      */

120     public void commit(boolean ignoreTimeout) {
121         TransactionID txId = currentTransactionIdThreadLocal.get();
122         if (txId == null) {
123             throw new TransactionException("no transaction started");
124         }
125
126         LocalTransactionContext currentTx = contextMap.get(txId);
127
128         try {
129             currentTx.commit(ignoreTimeout);
130             statistics.transactionCommitted();
131         } catch (TransactionTimeoutException tte) {
132             statistics.transactionTimedOut();
133             statistics.transactionRolledBack();
134             throw tte;
135         } catch (TransactionException te) {
136             statistics.transactionRolledBack();
137             throw te;
138         } finally {
139             contextMap.remove(txId);
140             transactionIDFactory.clear(txId);
141             currentTransactionIdThreadLocal.remove();
142             MDC.remove(MDC_KEY);
143         }
144     }
145
146     /**
147      * Rollback the transaction bound to the current thread
148      */

149     public void rollback() {
150         TransactionID txId = currentTransactionIdThreadLocal.get();
151         if (txId == null) {
152             throw new TransactionException("no transaction started");
153         }
154
155         LocalTransactionContext currentTx = contextMap.get(txId);
156
157         try {
158             currentTx.rollback();
159             statistics.transactionRolledBack();
160         } finally {
161             contextMap.remove(txId);
162             transactionIDFactory.clear(txId);
163             currentTransactionIdThreadLocal.remove();
164             MDC.remove(MDC_KEY);
165         }
166     }
167
168     /**
169      * Mark the transaction bound to the current thread for rollback only
170      */

171     public void setRollbackOnly() {
172         TransactionID txId = currentTransactionIdThreadLocal.get();
173         if (txId == null) {
174             throw new TransactionException("no transaction started");
175         }
176
177         LocalTransactionContext currentTx = contextMap.get(txId);
178
179         currentTx.setRollbackOnly();
180     }
181
182     /**
183      * Get the transaction context bond to the current thread
184      * @return the transaction context bond to the current thread or null if no transaction
185      *      started on the current thread
186      */

187     public LocalTransactionContext getCurrentTransactionContext() {
188         TransactionID txId = currentTransactionIdThreadLocal.get();
189         if (txId == null) {
190             return null;
191         }
192         return contextMap.get(txId);
193     }
194
195     /**
196      * Get the committed transactions count
197      * @return the committed transactions count
198      */

199     public long getTransactionCommittedCount() {
200         return statistics.getTransactionCommittedCount();
201     }
202
203     /**
204      * Get the rolled back transactions count
205      * @return the rolled back transactions count
206      */

207     public long getTransactionRolledBackCount() {
208         return statistics.getTransactionRolledBackCount();
209     }
210
211     /**
212      * Get the timed out transactions count. Note that only transactions which failed to
213      * commit due to a timeout are taken into account
214      * @return the timed out transactions count
215      */

216     public long getTransactionTimedOutCount() {
217         return statistics.getTransactionTimedOutCount();
218     }
219
220
221     /**
222      * Get the local transactions recovery manager of this cache manager
223      * @return the local transactions recovery manager
224      */

225     public LocalRecoveryManager getRecoveryManager() {
226         return localRecoveryManager;
227     }
228
229     /**
230      * Holder for TransactionController statistics
231      */

232     private static class TransactionControllerStatistics {
233         private final AtomicLong committed = new AtomicLong();
234         private final AtomicLong rolledBack = new AtomicLong();
235         private final AtomicLong timedOut = new AtomicLong();
236
237         void transactionCommitted() {
238             committed.incrementAndGet();
239         }
240
241         void transactionRolledBack() {
242             rolledBack.incrementAndGet();
243         }
244
245         void transactionTimedOut() {
246             timedOut.incrementAndGet();
247         }
248
249         long getTransactionCommittedCount() {
250             return committed.get();
251         }
252
253         long getTransactionRolledBackCount() {
254             return rolledBack.get();
255         }
256
257         long getTransactionTimedOutCount() {
258             return timedOut.get();
259         }
260     }
261
262 }
263