All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.dbms.rmi.RemoteDbSessionCleanupThread Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


package org.tentackle.dbms.rmi;

import org.tentackle.daemon.Scavenger;
import org.tentackle.dbms.Db;
import org.tentackle.log.Logger;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;


/**
 * Cleans up dead or crashed sessions.
 *
 * @author harald
 */
public class RemoteDbSessionCleanupThread extends Thread implements Scavenger {

  private static final Logger LOGGER = Logger.get(RemoteDbSessionCleanupThread.class);



  /** sleep time in ms. */
  private final long checkInterval;

  /** maximum number of retries to clean up a session. */
  private final int cleanupRetryMax;

  /** the sessions. */
  private final Set> sessions;

  /** map of zombie sessions to clean up count. */
  private final Map zombieSessions;

  /** request termination on server shutdown. */
  private volatile boolean terminationRequested;


  /**
   * Keeps the timeout info for a session group.
* Mitigates the race condition when the only active session of a session group is closed while the cleanup thread * is checking the others -- and closing them. * The remaining sessions should stay alive at least until the least recently active session (the closed one) * would have timed out. */ private static class GroupTimeoutInfo { private int timeout = -1; // timeout in polling intervals of the cleanup thread (<0 = uninitialized) private int timeoutCount = -1; // consecutive timeouts (<0 = uninitialized) private void update(int timeout, int timeoutCount) { if (this.timeout < 0 || this.timeout > timeout) { this.timeout = timeout; } if (this.timeoutCount < 0 || this.timeoutCount > timeoutCount) { this.timeoutCount = timeoutCount; } } private boolean hasTimedOut() { return ++timeoutCount > timeout; } } /** map of group-number::timeout-info. */ private final Map activeGroups; /** * Creates the cleanup thread. * * @param checkInterval milliseconds to sleep between runs * @param sessions the sessions * @param cleanupRetryMax maximum number of retries to clean up a session */ public RemoteDbSessionCleanupThread(long checkInterval, Set> sessions, int cleanupRetryMax) { super("Session Cleanup"); this.checkInterval = checkInterval; this.sessions = sessions; this.cleanupRetryMax = cleanupRetryMax; this.zombieSessions = new HashMap<>(); this.activeGroups = new HashMap<>(); setDaemon(true); // stop JVM if the last user process terminates } /** * Requests thread termination. */ public void requestTermination() { terminationRequested = true; interrupt(); } @Override public void run() { LOGGER.info(getName() + " started with interval=" + checkInterval + ", max.retries=" + cleanupRetryMax); while (!terminationRequested) { try { sleep(checkInterval); cleanupZombieSessions(); verifySessions(); } catch (InterruptedException ix) { // daemon thread is terminated via requestTermination() } catch (RuntimeException ex) { LOGGER.severe("cleanup sessions failed", ex); } } LOGGER.info(getName() + " terminated"); } /** * Verifies and cleans up the sessions. *

* Finds all session-groups or non-grouped session that are not alive anymore * and closes their sessions. * If _all_ sessions of a group have timed out, the whole group is closed. * Usually the modification tracker of a client sessions will keep up * a session-group alive. If the application uses only a single session, * it must set the alive flag manually. */ protected void verifySessions() { Set deadTxGroups = new HashSet<>(); // numbers of db groups that are dead while in a transaction for (Iterator> iter = sessions.iterator(); iter.hasNext(); ) { RemoteDbSessionImpl session = iter.next().get(); if (session == null) { iter.remove(); // was removed by GC: remove it from set too } else { try { if (session.isOpen()) { Db db = session.getSession(); if (db == null || !db.isOpen()) { if (db != null) { LOGGER.warning("Db {0} already closed in {1} -> force cleanup", db, session); // we came too late or not MT-visible. // db is already closed (cleanup in parallel?) session.forceCleanup(); // works in parallel } // else: already closed iter.remove(); continue; } int groupId = db.getExportedSessionGroupId(); boolean timedOut = session.hasTimedOut(); if (timedOut) { boolean inTransaction = db.isTxRunning(); // database has timed out if (groupId == 0) { // database without a group: close session immediately LOGGER.info("disconnect dead " + session + ", ungrouped"); cleanupSession(session); iter.remove(); continue; // don't run into polled() below (would cause a NPE) } else { if (inTransaction) { // there is a dead session within a transaction for this group deadTxGroups.add(groupId); } // grouped sessions are handled below } } else if (groupId > 0) { // not timed out and belongs to a group and at least one session still alive GroupTimeoutInfo groupTimeoutInfo = activeGroups.computeIfAbsent(groupId, g -> new GroupTimeoutInfo()); groupTimeoutInfo.update(session.getTimeout(), session.getTimeoutCount()); } session.polled(); // reset alive flag } } catch (RuntimeException rx) { LOGGER.warning("verifying " + session + " failed -> force cleanup", rx); session.forceCleanup(); iter.remove(); } } } // run the timeout count on all group infos activeGroups.entrySet().removeIf(entry -> entry.getValue().hasTimedOut()); // close all grouped sessions not belonging to alive groups for (Iterator> iter = sessions.iterator(); iter.hasNext(); ) { RemoteDbSessionImpl session = iter.next().get(); if (session == null) { iter.remove(); // was removed by GC: remove it from set too } else { if (session.isOpen()) { int groupId = session.getSession().getExportedSessionGroupId(); if (groupId != 0) { boolean deadInTransaction = deadTxGroups.contains(groupId); if (deadInTransaction || !activeGroups.containsKey(groupId)) { if (deadInTransaction) { LOGGER.info("disconnect dead {0}, group={1}, due to hanging transaction on {2}", session, groupId, session.getSession()); } else { LOGGER.info("disconnect dead {0}, group={1}", session, groupId); } cleanupSession(session); iter.remove(); } } } } } } /** * Cleans up a session. * * @param session the session */ protected void cleanupSession(RemoteDbSessionImpl session) { try { session.cleanup(true); session.closeDb(true); } catch (RuntimeException ex) { LOGGER.severe("session cleanup failed for " + session + "\n -> moved to zombie sessions!", ex); zombieSessions.put(session, 1); } } /** * Cleans up all zombie sessions. */ protected void cleanupZombieSessions() { for (Iterator> iter = zombieSessions.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = iter.next(); RemoteDbSessionImpl session = entry.getKey(); if (cleanupZombieSession(session)) { iter.remove(); } else { // failed int cleanupCount = entry.getValue(); if (cleanupCount >= cleanupRetryMax) { LOGGER.severe("zombie session refused to cleanup " + cleanupCount + " times -> to GC: " + session); session.forceCleanup(); iter.remove(); } else { entry.setValue(cleanupCount + 1); } } } } /** * Cleans up a zombie session. * * @param session the session * @return true if cleaned up, false if failed */ protected boolean cleanupZombieSession(RemoteDbSessionImpl session) { try { LOGGER.info("cleaning up zombie " + session); session.cleanup(true); session.closeDb(true); return true; } catch (RuntimeException ex) { LOGGER.severe("zombie session cleanup failed for " + session, ex); return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy