1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.tentackle.dbms.trip;
21
22 import org.tentackle.daemon.Scavenger;
23 import org.tentackle.dbms.Db;
24 import org.tentackle.log.Logger;
25
26 import java.lang.ref.WeakReference;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.Iterator;
30 import java.util.LinkedHashMap;
31 import java.util.Map;
32 import java.util.Set;
33
34
35
36
37
38
39
40 public class RemoteDbSessionCleanupThread extends Thread implements Scavenger {
41
42 private static final Logger LOGGER = Logger.get(RemoteDbSessionCleanupThread.class);
43
44
45
46
47 private final long checkInterval;
48
49
50 private final int cleanupRetryMax;
51
52
53 private final Set<WeakReference<RemoteDbSessionImpl>> sessions;
54
55
56 private final Map<RemoteDbSessionImpl,Integer> zombieSessions;
57
58
59 private volatile boolean terminationRequested;
60
61
62
63
64
65
66
67
68
69 private static class GroupTimeoutInfo {
70 private int timeout = -1;
71 private int timeoutCount = -1;
72
73 private void update(int timeout, int timeoutCount) {
74 if (this.timeout < 0 || this.timeout > timeout) {
75 this.timeout = timeout;
76 }
77 if (this.timeoutCount < 0 || this.timeoutCount > timeoutCount) {
78 this.timeoutCount = timeoutCount;
79 }
80 }
81
82 private boolean hasTimedOut() {
83 return ++timeoutCount > timeout;
84 }
85 }
86
87
88 private final Map<Integer, GroupTimeoutInfo> activeGroups;
89
90
91
92
93
94
95
96
97
98
99 public RemoteDbSessionCleanupThread(long checkInterval, Set<WeakReference<RemoteDbSessionImpl>> sessions, int cleanupRetryMax) {
100 super("Remote Session Cleanup");
101 this.checkInterval = checkInterval;
102 this.sessions = sessions;
103 this.cleanupRetryMax = cleanupRetryMax;
104 this.zombieSessions = new HashMap<>();
105 this.activeGroups = new HashMap<>();
106 setDaemon(true);
107 }
108
109
110
111
112
113 public void requestTermination() {
114 terminationRequested = true;
115 interrupt();
116 }
117
118
119 @Override
120 public void run() {
121
122 LOGGER.info("{0} started with interval={1} ms, max.retries={2}", getName(), checkInterval, cleanupRetryMax);
123
124 while (!terminationRequested) {
125 try {
126 sleep(checkInterval);
127 cleanupZombieSessions();
128 verifySessions();
129 }
130 catch (InterruptedException ix) {
131
132 }
133 catch (RuntimeException ex) {
134 LOGGER.severe("cleanup sessions failed", ex);
135 }
136 }
137
138 LOGGER.info("{0} terminated", getName());
139 }
140
141
142
143
144
145
146
147
148
149
150
151
152 protected void verifySessions() {
153
154 Set<Integer> deadTxGroups = new HashSet<>();
155
156 Map<WeakReference<RemoteDbSessionImpl>, Integer> groupedSessions = new LinkedHashMap<>();
157
158 for (Iterator<WeakReference<RemoteDbSessionImpl>> iter = sessions.iterator(); iter.hasNext(); ) {
159 WeakReference<RemoteDbSessionImpl> sessionRef = iter.next();
160 RemoteDbSessionImpl session = sessionRef.get();
161 if (session == null) {
162 iter.remove();
163 }
164 else {
165 try {
166 if (session.isOpen()) {
167 Db db = session.getSession();
168 if (db == null || !db.isOpen()) {
169 if (db != null) {
170 LOGGER.warning("Db {0} already closed in {1} -> force cleanup", db, session);
171
172
173 if (!forceCleanupIfNotInUse(session)) {
174 continue;
175 }
176 }
177 iter.remove();
178 continue;
179 }
180
181 int groupId = db.getExportedSessionGroupId();
182 boolean timedOut = session.hasTimedOut();
183
184 if (timedOut) {
185 boolean inTransaction = db.isTxRunning();
186
187 if (groupId == 0) {
188
189 LOGGER.info("disconnect dead " + session + ", ungrouped");
190 if (cleanupSession(session)) {
191 iter.remove();
192 continue;
193 }
194
195 }
196 else {
197 if (inTransaction) {
198
199 deadTxGroups.add(groupId);
200 }
201
202 }
203 }
204 else if (groupId > 0) {
205
206 GroupTimeoutInfo groupTimeoutInfo = activeGroups.computeIfAbsent(groupId, g -> new GroupTimeoutInfo());
207 groupTimeoutInfo.update(session.getTimeout(), session.getTimeoutCount());
208 }
209
210 if (groupId != 0) {
211 groupedSessions.put(sessionRef, groupId);
212 }
213
214 session.polled();
215 }
216 }
217 catch (RuntimeException rx) {
218 LOGGER.warning("verifying " + session + " failed -> force cleanup", rx);
219 if (forceCleanupIfNotInUse(session)) {
220 iter.remove();
221 }
222
223 }
224 }
225 }
226
227
228 activeGroups.entrySet().removeIf(entry -> entry.getValue().hasTimedOut());
229
230
231 for (Map.Entry<WeakReference<RemoteDbSessionImpl>, Integer> entry : groupedSessions.entrySet()) {
232 WeakReference<RemoteDbSessionImpl> sessionRef = entry.getKey();
233 RemoteDbSessionImpl session = sessionRef.get();
234 if (session == null) {
235 sessions.remove(sessionRef);
236 }
237 else {
238 try {
239 Db db = session.getSession();
240 if (db != null) {
241 int groupId = entry.getValue();
242
243 if (groupId == db.getExportedSessionGroupId()) {
244 boolean deadInTransaction = deadTxGroups.contains(groupId);
245 if (deadInTransaction || !activeGroups.containsKey(groupId)) {
246 if (deadInTransaction) {
247 LOGGER.info("disconnect dead {0}, group={1}, due to hanging transaction on {2}",
248 session, groupId, db);
249 }
250 else {
251 LOGGER.info("disconnect dead {0}, group={1}", session, groupId);
252 }
253 if (cleanupSession(session)) {
254 sessions.remove(sessionRef);
255 }
256
257 }
258 }
259 }
260 }
261 catch (RuntimeException rx) {
262 LOGGER.warning("verifying " + session + " failed -> force cleanup", rx);
263 if (forceCleanupIfNotInUse(session)) {
264 sessions.remove(sessionRef);
265 }
266
267 }
268 }
269 }
270 }
271
272
273
274
275
276
277
278
279
280
281 protected boolean cleanupSession(RemoteDbSessionImpl session) {
282 if (!session.enterInvocation()) {
283 LOGGER.info("{0} in use by {1} -> not cleaned up", session, session.getInvokingThread());
284 return false;
285 }
286 try {
287 session.cleanup(true);
288 session.closeDb(true);
289 }
290 catch (RuntimeException ex) {
291 LOGGER.severe("session cleanup failed for " + session + "\n -> moved to zombie sessions!", ex);
292 zombieSessions.put(session, 1);
293 }
294 finally {
295 session.exitInvocation();
296 }
297 return true;
298 }
299
300
301
302
303
304 protected void cleanupZombieSessions() {
305 for (Iterator<Map.Entry<RemoteDbSessionImpl,Integer>> iter = zombieSessions.entrySet().iterator(); iter.hasNext(); ) {
306 Map.Entry<RemoteDbSessionImpl,Integer> entry = iter.next();
307 RemoteDbSessionImpl session = entry.getKey();
308 if (!session.enterInvocation()) {
309
310 LOGGER.info("zombie {0} in use by {1} -> not cleaned up", session, session.getInvokingThread());
311 continue;
312 }
313 try {
314 if (cleanupZombieSession(session)) {
315 iter.remove();
316 }
317 else {
318
319 int cleanupCount = entry.getValue();
320 if (cleanupCount >= cleanupRetryMax) {
321 LOGGER.severe("zombie session refused to cleanup " + cleanupCount + " times -> to GC: " + session);
322 session.forceCleanup();
323 iter.remove();
324 }
325 else {
326 entry.setValue(cleanupCount + 1);
327 }
328 }
329 }
330 finally {
331 session.exitInvocation();
332 }
333 }
334 }
335
336
337
338
339
340
341
342
343
344 private boolean forceCleanupIfNotInUse(RemoteDbSessionImpl session) {
345 if (!session.enterInvocation()) {
346 LOGGER.info("{0} in use by {1} -> no forced cleanup", session, session.getInvokingThread());
347 return false;
348 }
349 try {
350 session.forceCleanup();
351 }
352 finally {
353 session.exitInvocation();
354 }
355 return true;
356 }
357
358
359
360
361
362
363
364
365 protected boolean cleanupZombieSession(RemoteDbSessionImpl session) {
366 if (!session.enterInvocation()) {
367 LOGGER.info("zombie {0} in use by {1} -> not cleaned up", session, session.getInvokingThread());
368 return false;
369 }
370 try {
371 LOGGER.info("cleaning up zombie " + session);
372 session.cleanup(true);
373 session.closeDb(true);
374 return true;
375 }
376 catch (RuntimeException ex) {
377 LOGGER.severe("zombie session cleanup failed for " + session, ex);
378 return false;
379 }
380 finally {
381 session.exitInvocation();
382 }
383 }
384
385 }