1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.tentackle.dbms;
20
21 import org.tentackle.common.Constants;
22 import org.tentackle.common.EncryptedProperties;
23 import org.tentackle.common.Service;
24 import org.tentackle.common.ServiceFactory;
25 import org.tentackle.common.Timestamp;
26 import org.tentackle.log.Logger;
27 import org.tentackle.misc.TimeKeeper;
28
29 import java.lang.ref.WeakReference;
30 import java.sql.Connection;
31 import java.sql.SQLException;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.Map;
35 import java.util.Queue;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.ConcurrentLinkedQueue;
38 import java.util.concurrent.ThreadLocalRandom;
39 import java.util.concurrent.atomic.AtomicLong;
40
41
42 interface DbTransactionFactoryHolder {
43 DbTransactionFactory INSTANCE = ServiceFactory.createService(DbTransactionFactory.class, DbTransactionFactory.class);
44 }
45
46
47
48
49
50
51 @Service(DbTransactionFactory.class)
52 public class DbTransactionFactory {
53
54
55
56
57
58
59 public static DbTransactionFactory getInstance() {
60 return DbTransactionFactoryHolder.INSTANCE;
61 }
62
63
64 private static final Logger LOGGER = Logger.get(DbTransactionFactory.class);
65
66 private static final String TX_IDLE_INTERVAL = "txIdleInterval";
67 private static final String TX_IDLE_TIMEOUT = "txIdleTimeout";
68
69 private static final long DEFAULT_TX_IDLE_INTERVAL = Constants.SECOND_MS;
70 private static final int DEFAULT_TX_IDLE_TIMEOUT = 60;
71
72
73 private final Map<Long, WeakReference<DbTransaction>> transactions;
74 private final Queue<Long> finishedTransactions;
75 private final TransactionStatistics statistics;
76
77 private volatile TransactionSupervisor transactionSupervisor;
78 private volatile long txIdleInterval = DEFAULT_TX_IDLE_INTERVAL;
79 private volatile int txIdleTimeout = DEFAULT_TX_IDLE_TIMEOUT;
80 private volatile boolean configChanged;
81 private volatile boolean collectingStatistics;
82
83
84 private static final AtomicLong TX_COUNT = new AtomicLong();
85
86
87
88
89
90 public DbTransactionFactory() {
91 transactions = new ConcurrentHashMap<>();
92 finishedTransactions = new ConcurrentLinkedQueue<>();
93 statistics = new TransactionStatistics();
94 configChanged = true;
95 }
96
97
98
99
100
101
102 public void configure(EncryptedProperties properties) {
103 String val = properties.getPropertyIgnoreCase(TX_IDLE_INTERVAL);
104 if (val != null) {
105 setTxIdleInterval(Long.parseLong(val));
106 }
107 val = properties.getPropertyIgnoreCase(TX_IDLE_TIMEOUT);
108 if (val != null) {
109 setTxIdleTimeout(Integer.parseInt(val));
110 }
111 }
112
113
114
115
116
117
118
119
120
121
122 public DbTransaction create(Db db, String txName, boolean fromRemote) {
123 DbTransaction transaction = new DbTransaction(db, createVoucher(fromRemote), txName, TX_COUNT.incrementAndGet());
124 transactions.put(transaction.getTxNumber(), new WeakReference<>(transaction));
125 ensureSupervisorAlive();
126 return transaction;
127 }
128
129
130
131
132
133
134
135
136
137 public void finish(DbTransaction transaction, boolean rolledBack) {
138 transaction.complete();
139 long txId = transaction.getTxNumber();
140 transactions.remove(txId);
141 if (collectingStatistics) {
142 statistics.countTransaction(transaction, rolledBack);
143 }
144 finishedTransactions.add(txId);
145 }
146
147
148
149
150
151
152 public void setCollectingStatistics(boolean collectingStatistics) {
153 this.collectingStatistics = collectingStatistics;
154 }
155
156
157
158
159
160
161 public boolean isCollectingStatistics() {
162 return collectingStatistics;
163 }
164
165
166
167
168
169
170
171 public void logStatistics(Logger.Level level, boolean clear) {
172 statistics.logStatistics(null, level, " >TXN-Stats: ", clear);
173 }
174
175
176
177
178
179
180 public long getTxIdleInterval() {
181 return txIdleInterval;
182 }
183
184
185
186
187
188
189 public void setTxIdleInterval(long txIdleInterval) {
190 if (this.txIdleInterval != txIdleInterval) {
191 this.txIdleInterval = txIdleInterval;
192 configChanged = true;
193 }
194 }
195
196
197
198
199
200
201 public int getTxIdleTimeout() {
202 return txIdleTimeout;
203 }
204
205
206
207
208
209
210 public void setTxIdleTimeout(int txIdleTimeout) {
211 if (this.txIdleTimeout != txIdleTimeout) {
212 this.txIdleTimeout = txIdleTimeout;
213 configChanged = true;
214 }
215 }
216
217
218
219
220
221
222 public synchronized void requestTermination() {
223 if (transactionSupervisor != null) {
224 transactionSupervisor.requestTermination();
225 transactionSupervisor.interrupt();
226 if (transactions.isEmpty()) {
227 transactionSupervisor = null;
228 }
229 }
230 }
231
232
233
234
235
236
237 public synchronized boolean isSupervisorRunning() {
238 return transactionSupervisor != null && transactionSupervisor.isAlive();
239 }
240
241
242
243
244
245
246
247
248 protected long createVoucher(boolean fromRemote) {
249 ThreadLocalRandom random = ThreadLocalRandom.current();
250 long magic;
251 do {
252 magic = random.nextLong();
253 }
254 while (magic == 0);
255
256
257 return (fromRemote && magic > 0 || !fromRemote && magic < 0) ? -magic : magic;
258 }
259
260
261
262
263
264
265
266
267
268 private void ensureSupervisorAlive() {
269 TransactionSupervisor supervisor = transactionSupervisor;
270 if (supervisor == null || supervisor.terminationRequested || !supervisor.isAlive()) {
271 startSupervisorIfNotAlive();
272 }
273 }
274
275
276
277
278 private synchronized void startSupervisorIfNotAlive() {
279 if (transactionSupervisor == null || !transactionSupervisor.isAlive()) {
280 configChanged = true;
281 transactionSupervisor = new TransactionSupervisor();
282 transactionSupervisor.start();
283 }
284 }
285
286
287
288
289
290
291
292
293
294 private synchronized boolean keepSupervising(TransactionSupervisor supervisor) {
295 if (transactionSupervisor != supervisor) {
296 return false;
297 }
298 if (supervisor.terminationRequested && transactions.isEmpty()) {
299 transactionSupervisor = null;
300 return false;
301 }
302 return true;
303 }
304
305
306
307
308
309
310
311
312 private synchronized void dropSupervisor(TransactionSupervisor supervisor) {
313 if (transactionSupervisor == supervisor) {
314 transactionSupervisor = null;
315 }
316 }
317
318
319
320
321
322
323
324
325
326 private class TransactionSupervisor extends Thread {
327
328 private static class TxIdleCounter {
329 final String txName;
330 final long txNumber;
331 final long creationTime;
332 final TimeKeeper timeKeeper;
333 int idleCount;
334
335 TxIdleCounter(DbTransaction transaction) {
336
337 txName = transaction.getTxName();
338 txNumber = transaction.getTxNumber();
339 creationTime = transaction.getCreationTime();
340 timeKeeper = transaction.getTimeKeeper();
341 }
342
343 int count() {
344 return ++idleCount;
345 }
346
347 @Override
348 public String toString() {
349 return txName + ":" + txNumber + ", started " + new Timestamp(creationTime) + " (" + timeKeeper.end().millisToString() + " ms)";
350 }
351 }
352
353 private final Map<Long, TxIdleCounter> idleMap = new HashMap<>();
354 private volatile boolean terminationRequested;
355
356 private TransactionSupervisor() {
357 super("Transaction Supervisor");
358 setDaemon(true);
359 }
360
361
362
363
364
365
366
367 private void requestTermination() {
368 terminationRequested = true;
369 }
370
371 @Override
372 public void run() {
373 try {
374 supervise();
375 }
376 catch (Throwable t) {
377 LOGGER.severe(getName() + " died -> restarted by the next transaction", t);
378 throw t;
379 }
380 finally {
381 dropSupervisor(this);
382 }
383 LOGGER.info("{0} terminated", getName());
384 }
385
386
387
388
389 private void supervise() {
390 while (keepSupervising(this)) {
391 if (configChanged) {
392 LOGGER.info("{0} running with interval={1} ms, maxIdleCount={2}", getName(), txIdleInterval, txIdleTimeout);
393 configChanged = false;
394 }
395 try {
396 sleep(txIdleInterval);
397
398
399 Long finishedTxId;
400 while ((finishedTxId = finishedTransactions.poll()) != null) {
401 idleMap.remove(finishedTxId);
402 }
403
404
405 for (Iterator<Map.Entry<Long, WeakReference<DbTransaction>>> iter = transactions.entrySet().iterator(); iter.hasNext(); ) {
406 Map.Entry<Long, WeakReference<DbTransaction>> entry = iter.next();
407 DbTransaction transaction = entry.getValue().get();
408 if (transaction == null) {
409
410
411
412 Long txId = entry.getKey();
413 TxIdleCounter idleCounter = idleMap.remove(txId);
414 LOGGER.severe("unreferenced transaction {0} detected", idleCounter == null ? txId : idleCounter);
415 iter.remove();
416 }
417 else {
418 if (transaction.pollAlive()) {
419 idleMap.remove(transaction.getTxNumber());
420 }
421 else {
422 if (txIdleTimeout > 0 &&
423 idleMap.computeIfAbsent(transaction.getTxNumber(), _ -> new TxIdleCounter(transaction)).count() > txIdleTimeout) {
424 String msg = "idle transaction " + transaction.getSession() + " timed out after " +
425 txIdleTimeout * txIdleInterval / Constants.SECOND_MS + " s";
426 idleMap.remove(transaction.getTxNumber());
427 iter.remove();
428 Db session = transaction.getSession();
429 ManagedConnection mc;
430 Connection con = null;
431
432 synchronized (session.getConnectionManager()) {
433 if (!transaction.timeOut()) {
434 continue;
435 }
436 mc = session.getConnection();
437 if (mc != null) {
438 mc.setDead(true);
439 if (!mc.isClosed()) {
440 con = mc.getConnection();
441 }
442 }
443 }
444 if (mc != null) {
445 LOGGER.warning(msg + ":\n" + mc.toDiagnosticString());
446 if (con != null) {
447
448 createTxCleanupThread(mc, con).start();
449 }
450 else {
451 LOGGER.warning("managed connection {0} already closed", mc);
452 }
453 }
454 else {
455 LOGGER.warning(msg + " (managed connection already detached from session {0})", session);
456 }
457 }
458 }
459 }
460 }
461 }
462 catch (InterruptedException ix) {
463
464 }
465 catch (RuntimeException ex) {
466 LOGGER.severe("cleanup transactions failed", ex);
467 }
468 }
469 }
470
471 private static Thread createTxCleanupThread(ManagedConnection mc, Connection con) {
472 Thread txCleanupThread = new Thread("transaction cleanup " + mc) {
473 @Override
474 public void run() {
475 try {
476 LOGGER.warning("performing low-level rollback and close of {0}", con);
477
478 try {
479 con.rollback();
480 }
481 catch (SQLException sr) {
482 LOGGER.warning("low-level rollback failed", sr);
483 }
484 con.close();
485 }
486 catch (SQLException | RuntimeException ex) {
487 LOGGER.warning("closing JDBC connection failed", ex);
488 }
489 }
490 };
491 txCleanupThread.setDaemon(true);
492 return txCleanupThread;
493 }
494 }
495
496 }