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

org.tentackle.dbms.DbModificationTracker 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;

import org.tentackle.common.ExceptionHelper;
import org.tentackle.common.Service;
import org.tentackle.dbms.rmi.DbModificationTrackerRemoteDelegate;
import org.tentackle.io.ReconnectedException;
import org.tentackle.log.Logger;
import org.tentackle.log.Logger.Level;
import org.tentackle.misc.IdSerialTuple;
import org.tentackle.session.AbstractSessionTask;
import org.tentackle.session.DefaultSessionTaskDispatcher;
import org.tentackle.session.ModificationEvent;
import org.tentackle.session.ModificationEventDetail;
import org.tentackle.session.ModificationListener;
import org.tentackle.session.ModificationTracker;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.Session;
import org.tentackle.session.SessionClosedException;
import org.tentackle.session.SessionUtilities;
import org.tentackle.task.Task;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;




/**
 * The modification tracker for the tentackle persistence layer.
 *
 * @author harald
 */
@Service(ModificationTracker.class)
public class DbModificationTracker extends DefaultSessionTaskDispatcher implements ModificationTracker {

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

  private static final long DEFAULT_POLLING_INTERVAL = 2000;        // 2s

  private static final AtomicInteger LISTENER_KEY_INSTANCE_COUNTER = new AtomicInteger();


  /**
   * Gets the remote delegate.
   *
   * @return the delegate
   */
  private static int getRemoteDelegateId() {
    if (delegateId == 0)  {
      delegateId = Db.prepareRemoteDelegate(DbModificationTracker.class);
    }
    return delegateId;
  }

  private static int delegateId;



  /**
   * The listener entries.
* Sorted according to priority and registration order. */ private static class ListenerEntry implements Comparable { /** the modification listener. */ private final ModificationListener listener; /** the table entry. */ private final TableEntry tableEntry; /** the priority. */ private final int priority; /** the instance number representing the registration order. */ private final int instanceNumber; /** the time window in milliseconds. */ private final long timeFrame; /** * Creates a key with a given priority. * * @param listener the modification listener * @param tableEntry the table entry */ private ListenerEntry(ModificationListener listener, TableEntry tableEntry) { this.listener = listener; this.tableEntry = tableEntry; if (listener.getTimeFrame() > 0) { this.timeFrame = listener.getTimeFrame(); this.priority = 0; } else { this.timeFrame = 0; this.priority = listener.getPriority(); } this.instanceNumber = LISTENER_KEY_INSTANCE_COUNTER.incrementAndGet(); } /** * Returns whether this is the master listener. * * @return true if master listener */ private boolean isMaster() { return tableEntry.isMaster(); } /** * Creates a modification detail. * * @param serial the serial * @return the detail */ private ModificationEventDetail createDetail(long serial) { return new ModificationEventDetail(tableEntry.tableName, serial); } @Override public int hashCode() { int hash = 7; hash = 41 * hash + this.instanceNumber; return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } return this.instanceNumber == ((ListenerEntry) obj).instanceNumber; } @Override public int compareTo(ListenerEntry obj) { if (obj == null) { return Integer.MAX_VALUE; } int rv = priority - obj.priority; if (rv == 0) { rv = instanceNumber - obj.instanceNumber; } return rv; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("order="); buf.append(instanceNumber); if (timeFrame != 0) { buf.append(" window="); buf.append(timeFrame); } else { buf.append(" prio="); buf.append(priority); } return buf.toString(); } } /** * Entry for each table (or class) to watch for modifications. */ private static class TableEntry { private final String tableName; // the table name private long id; // unique id private volatile long serial; // last serial /** * Creates a modification entry.
* For internal use only! * * @param tableName the tablename of the class, null if master serial * @param id the tablename id, 0 if master serial * @param serial the last serial seen */ private TableEntry(String tableName, long id, long serial) { this.tableName = tableName; this.id = id; this.serial = serial; } /** * Returns whether this is the master entry. * * @return true if master entry */ private boolean isMaster() { return id == 0; } @Override public String toString() { StringBuilder buf = new StringBuilder(); if (tableName != null) { buf.append("tablename='"); buf.append(tableName); buf.append("', id="); buf.append(id); } else { buf.append("master"); } buf.append(", serial="); buf.append(serial); return buf.toString(); } } /** * A delayed event.
* There is only one event per listener. * The natural ordering is by invocation time. */ private static class DelayedEvent implements Comparable { private final ModificationListener listener; private final Map details; private final long invocationTime; /** * Creates a delayed event. * * @param invocationTime the invocation time in epochal ms * @param listener the listener */ private DelayedEvent(long invocationTime, ModificationListener listener) { this.invocationTime = invocationTime; this.listener = listener; this.details = new HashMap<>(); } /** * Adds modification details. * * @param details named event details */ private void addDetails(Collection details) { for (ModificationEventDetail detail: details) { this.details.put(detail.getName(), detail); } } /** * Gets the modification details. * * @return the details */ private Collection getDetails() { return details.values(); } @Override public int hashCode() { int hash = 7; hash = 59 * hash + Objects.hashCode(this.listener); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } DelayedEvent other = (DelayedEvent) obj; return Objects.equals(this.listener, other.listener); } @Override public int compareTo(DelayedEvent o) { return Long.compare(invocationTime, o.invocationTime); } } private final List shutdownRunnables; // runnables to be run once when thread is stopped private final Map countersByName; // map of modification counters (tablename:counter) private final Map countersById; // map of modification counters (id:counter) private final Set listenerEntries; // modification listeners private final Map tableEntriesByName; // the table entries mapped by (table)name private final Map tableEntriesById; // the table entries mapped by ID private volatile TableEntry masterEntry; // master serial entry private final Random timeRandom; // execution time randomizer private final Map delayedListeners; // map of delayed listeners private final Set delayedEvents; // delayed events private long delayedSleepInterval; // != 0 if this is the next max. sleep interval private DbModification masterModification; // po to access the master serial private boolean localClientMode; // true if local session and not a server private long lastPollTime; // last polling time /** * Creates a tracker to be configured. * * @param name the thread's name */ public DbModificationTracker(String name) { super(name); /* * This is not a daemon thread! * As long as this thread is running the JVM will not terminate. */ setDaemon(false); setSessionClosedOnTermination(true); setSleepInterval(DEFAULT_POLLING_INTERVAL); countersByName = new ConcurrentHashMap<>(); countersById = new HashMap<>(); listenerEntries = new TreeSet<>(); delayedListeners = new HashMap<>(); delayedEvents = new TreeSet<>(); tableEntriesByName = new ConcurrentHashMap<>(); tableEntriesById = new ConcurrentHashMap<>(); timeRandom = new Random(); shutdownRunnables = new ArrayList<>(); } /** * Creates a tracker with a default name to be configured. */ public DbModificationTracker() { this("DB Modification Tracker"); } @Override public Session requestSession() { lock(Thread.currentThread()); return getSession(); } @Override public boolean releaseSession(Session session) { return unlock(Thread.currentThread()); } @Override public Db getSession() { Db db = (Db) super.getSession(); if (db == null) { throw new PersistenceException("no session configured for " + this); } return db; } @Override public void setSession(Session session) { super.setSession(session); // clear counters countersById.clear(); countersByName.clear(); // check if modification table is initialized try { masterModification = new DbModification((Db) session); selectMasterSerial(); } catch (PersistenceException ex) { DbModification.initializeModificationTable(getSession()); } } @Override protected void cleanup() { if (lastPollTime != 0) { long pollPending = System.currentTimeMillis() - lastPollTime; double pollFactor = (double) pollPending / (double) getSleepInterval(); if (pollFactor > 2.0) { // longer running tasks? blocked connection? server closed a crashed session? LOGGER.warning("last poll " + pollPending + " ms ago, " + String.format("%.2f", pollFactor) + " times the polling interval of " + getSleepInterval() + " ms)"); } lastPollTime = 0; // cleanup is invoked a second time from the main thread for sure } performPendingCounts(); super.cleanup(); invokeShutdownRunnables(); } /** * Adds a shutdown runnable.
* The runnables will be executed on termination of this tracker. * * @param runnable the runnable */ @Override public synchronized void addShutdownRunnable(Runnable runnable) { shutdownRunnables.add(runnable); } /** * Removes a shutdown runnable. *

* Notice that the first occurrence of the runnable is removed. * * @param runnable the runnable * @return true if runnable removed, false if no such runnable registered */ @Override public synchronized boolean removeShutdownRunnable(Runnable runnable) { return shutdownRunnables.remove(runnable); } /** * Adds a modification listener. * * @param listener the listener to add */ @Override public void addModificationListener(ModificationListener listener) { if (listener.getNames() == null || listener.getNames().length == 0) { // unnamed master listener if (masterEntry == null) { configureMaster(); } synchronized(this) { listenerEntries.add(new ListenerEntry(listener, masterEntry)); } } else { // named listener(s) for (String name: listener.getNames()) { TableEntry entry = tableEntriesByName.get(name); if (entry == null) { configureName(name); entry = tableEntriesByName.get(name); } synchronized (this) { listenerEntries.add(new ListenerEntry(listener, entry)); } } } } /** * Removes a modification listener. * * @param listener the listener to remove * @return true if listener removed */ @Override public synchronized boolean removeModificationListener(ModificationListener listener) { boolean removed = false; Iterator iter = listenerEntries.iterator(); while (iter.hasNext()) { ListenerEntry listenerEntry = iter.next(); if (listener == listenerEntry.listener) { // == is okay iter.remove(); removed = true; } } return removed; } /** * Returns whether tracker runs in local client mode.
* In this mode, modifications will be persisted immediately. * This is necessary for applications without a middle tier server, * i.e. directly talking to the database.

* In servers, however, modifications will be persisted delayed, * when no transactions are running that refer to the modified tables.

* Important: in local client mode, the thread-local session must be valid, * because the session used to persist the modification count must be the * same as the current transaction, if any. * * @return true if in local client mode, false if remote client or server */ public boolean isLocalClientMode() { return localClientMode; } /** * Sets local client mode. * * @param localClientMode true if in local client mode, false if remote client or server * @see #isLocalClientMode() */ public void setLocalClientMode(boolean localClientMode) { if (this.localClientMode != localClientMode) { this.localClientMode = localClientMode; countersByName.clear(); countersById.clear(); LOGGER.info(getName() + " running in {0} mode", localClientMode ? "local-client" : "optimized"); } } /** * Gets the current master serial.
* Used in remote connections. * * @return the current master serial */ public long getMasterSerial() { if (masterEntry == null) { configureMaster(); } return masterEntry.serial; } /** * Gets the pair of id/serial for a given tablename.
* Used in remote connections. * * @param tableName the table to lookup * @return the id/serial pair for the tablename, never null */ public IdSerialTuple getIdSerialForName(String tableName) { if (tableName == null) { throw new PersistenceException("tableName must be given"); } TableEntry entry = tableEntriesByName.get(tableName); if (entry == null) { configureName(tableName); entry = tableEntriesByName.get(tableName); } return new IdSerialTuple(entry.id, entry.serial); } /** * Gets the serial for a given class. * * @param clazz the tracked class * @return the table serial */ @Override public long getSerial(Class clazz) { String name = SessionUtilities.getInstance().getTableName(clazz.getName()); if (name == null) { // not annotated with @TableName: try to instantiate and get from AbstractDbObject AbstractDbObject obj = (AbstractDbObject) DbUtilities.getInstance().createObject((Class) clazz); if (obj != null) { name = obj.getTableName(); } } return getSerial(name); } /** * Gets the serial for a given modification name. * * @param name the modification name * @return the table serial */ @Override public long getSerial(String name) { return getIdSerialForName(name).getSerial(); } /** * Gets the serials of all monitored tables.
* Used in remote connections. * * @return the serials */ public List getAllSerials() { List idSerList = new ArrayList<>(); for (TableEntry entry: tableEntriesByName.values()) { idSerList.add(new IdSerialTuple(entry.id, entry.serial)); } return idSerList; } /** * Counts the modification for a table.
* Used by the persistence layer to update the modification table. * * @param session the session persisting the modification, null if thread-local session * @param tableName the table name * @return the table serial (guessed lowest, could be higher already) */ @Override public long countModification (Session session, String tableName) { ModificationTally counter = getCounter(tableName); // put pending count counter.countPending(session); if (localClientMode || !isAlive()) { // immediate mode or no poll() because not alive -> persist now counter.performPendingCount(); } // else optimize: // return the guessed minimum table serial (pending count is done in next poll()) return counter.getLatestSerial(); } /** * Invalidates the modification table and re-initializes all entries and counters. */ @Override public void invalidate() { AbstractSessionTask task = new AbstractSessionTask() { @Override public void run() { countersByName.clear(); countersById.clear(); if (masterEntry != null) { masterEntry.serial = 1; } DbModification.initializeModificationTable((Db) getSession()); refreshTableEntries(); } }; if (isTaskDispatcherThread() || !isAlive()) { task.setSession(getSession()); task.run(); } else { addTaskAndWait(task); } } @Override @SuppressWarnings("unchecked") protected void lockInternal() { long currentTime = System.currentTimeMillis(); if (currentTime - lastPollTime > getSleepInterval() >> 1) { // don't poll too often, at most each half sleep interval lastPollTime = currentTime; try { poll(); invokeDelayedListeners(); } catch (Throwable t) { if (!ExceptionHelper.handleException( true, t, new ExceptionHelper.Handler<>(ReconnectedException.class, rx -> { // reconnected: just log and start over. // requires Db.enableReconnection(true, >0) on the tracker's session LOGGER.warning("reconnection successful -> resuming normal operation ...", rx); }), new ExceptionHelper.Handler<>(SessionClosedException.class, sx -> { // if remote: session has been closed by the server (deadlock, timeout, whatever) // if local: database server has closed the session // reopen once and treat as if polling was ok // if the session could not be re-opened: the process will terminate if (getSession().getSessionGroupId() == 0) { // only if ungrouped (else rich client) LOGGER.severe("session " + getSession() + " closed unexpectedly -> try to re-open ...", sx); getSession().reOpen(); } else { throw sx; } }))) { throw t; // all other exceptions will be thrown as is } } } super.lockInternal(); } @Override protected void unlockInternal(long sleepMs) { if (delayedSleepInterval > 0) { if (delayedSleepInterval < sleepMs) { sleepMs = delayedSleepInterval; } delayedSleepInterval = 0; } super.unlockInternal(sleepMs); } /** * Creates a modification event. * * @param details the event details, null or empty if master event * @return the event */ protected ModificationEvent createModificationEvent(Collection details) { return details == null || details.isEmpty() ? new ModificationEvent(getSession(), getMasterSerial()) : new ModificationEvent(getSession(), details); } /** * Flushes all pending modification counts to database. */ private void performPendingCounts() { for (ModificationTally counter: countersByName.values()) { counter.performPendingCount(); } } /** * Checks for changes in the modification table. *

* Always invoked from within the tracker thread, so it's ok to use * the thread's session. */ private void poll() { if (!getSession().isRemote()) { performPendingCounts(); } Long serial = getModifiedMasterSerial(); if (serial != null) { // changed! if (serial < masterEntry.serial) { // new serial is less than current serial: force complete re-initialization invalidate(); } // read all serials and create modification events Map> detailMap = new HashMap<>(); for (IdSerialTuple idSer: selectAllIdSerials()) { if (idSer.getId() > 0) { // update last serial of counter (if configured) ModificationTally counter = countersById.get(idSer.getId()); if (counter != null) { counter.setLastSerial(idSer.getSerial()); } // this updates also counters that don't have a listener // e.g. countModification invoked without a listener registered } TableEntry tableEntry = tableEntriesById.get(idSer.getId()); // serial changed for entry if (tableEntry != null) { // no entry -> no listenerEntry for (ListenerEntry listenerEntry : listenerEntries) { if (listenerEntry.tableEntry.id == idSer.getId() && listenerEntry.tableEntry.serial != idSer.getSerial()) { // table entry for listener has changed LOGGER.fine("modification detected for {0}, new serial {1}", listenerEntry.tableEntry, idSer.getSerial()); Set details = detailMap.computeIfAbsent(listenerEntry.listener, k -> new HashSet<>()); // create a new one if (!listenerEntry.isMaster()) { getCounter(listenerEntry.tableEntry.tableName); // configure counter if not yet done details.add(listenerEntry.createDetail(idSer.getSerial())); } } } tableEntry.serial = idSer.getSerial(); // remember serial, even if no listener entry } } masterEntry.serial = serial; // fire listeners for (Map.Entry> entry: detailMap.entrySet()) { ModificationListener listener = entry.getKey(); Set details = entry.getValue(); long timeFrame = listener.getTimeFrame(); long timeDelay = listener.getTimeDelay(); if (timeFrame > 0 || timeDelay > 0) { // delayed DelayedEvent event = delayedListeners.get(listener); if (event == null) { // new long epochalExecutionTime = System.currentTimeMillis(); if (timeDelay > 0) { epochalExecutionTime += timeDelay; } if (timeFrame > 0) { epochalExecutionTime += (long) (timeRandom.nextDouble() * timeFrame); } event = new DelayedEvent(epochalExecutionTime, listener); event.addDetails(details); delayedListeners.put(listener, event); delayedEvents.add(event); } else { // add the details (don't change the execution time) event.addDetails(details); } } else { // invoke immediately listener.dataChanged(createModificationEvent(details)); } } } } /** * Invokes the delayed modification listeners. */ private void invokeDelayedListeners() { long currentTime = System.currentTimeMillis(); delayedSleepInterval = getSleepInterval(); Iterator iter = delayedEvents.iterator(); while (iter.hasNext()) { // ordered by invocation time DelayedEvent delayedEvent = iter.next(); long msLeft = delayedEvent.invocationTime - currentTime; if (msLeft <= 0) { // ready to run ModificationEvent event = createModificationEvent(delayedEvent.getDetails()); delayedEvent.listener.dataChanged(event); iter.remove(); delayedListeners.remove(delayedEvent.listener); } else if (msLeft < delayedSleepInterval) { delayedSleepInterval = msLeft; // sleep until the first execution interval } // else: more than mseconds left } } /** * Refreshes the serials and ids for all monitored tables. */ private void refreshTableEntries() { for (TableEntry entry: tableEntriesByName.values()) { getCounter(entry.tableName); IdSerialTuple idSer = selectIdSerialForName(entry.tableName); if (idSer != null) { entry.id = idSer.getId(); entry.serial = idSer.getSerial(); } } } /** * Extracts the master serial from the master serial object. *

* Application can override this method to process optional application data. * * @param masterSerial the master serial object * @return the long serial */ protected long extractMasterSerial(MasterSerial masterSerial) { return masterSerial.getSerial(); } /** * Returns the new master serial if modified.
* The method is provided to allow extensions, for example listening for other events on other tables. * * @return the updated master serial, null if unchanged */ protected Long getModifiedMasterSerial() { long serial = selectMasterSerial(); if (serial != getMasterSerial()) { return serial; } return null; } /** * Reads the master-serial from the database. * @return the master serial */ protected long selectMasterSerial() { if (getSession().isRemote()) { try { return extractMasterSerial(((DbModificationTrackerRemoteDelegate) getSession(). getRemoteDelegate(getRemoteDelegateId())).selectMasterSerial()); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } else { return masterModification.selectMasterSerial(); } } /** * Selects id/serial for all monitored tables.
* Override this method to listen on other modification tables as well (from other apps, for example), * see {@link #getModifiedMasterSerial()}. * * @return the tuples */ protected List selectAllIdSerials() { if (getSession().isRemote()) { try { return ((DbModificationTrackerRemoteDelegate) getSession().getRemoteDelegate(getRemoteDelegateId())).selectAllSerials(); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } else { return masterModification.selectAllIdSerial(); } } /** * Selects the pair of id/serial for a given tablename. * * @param tableName the table to lookup * @return the id/serial pair for the tablename, null table not configured so far */ protected IdSerialTuple selectIdSerialForName(String tableName) { if (getSession().isRemote()) { try { return ((DbModificationTrackerRemoteDelegate) getSession().getRemoteDelegate(getRemoteDelegateId())).selectIdSerialForName(tableName); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } else { return masterModification.selectIdSerial(tableName); } } /** * Gets the modification counter for a given table.
* If there is no such counter, it will be created. * * @param tableName the table name * @return the counter */ private ModificationTally getCounter(String tableName) { ModificationTally counter = countersByName.get(tableName); if (counter == null) { counter = configureCounter(tableName); } return counter; } /** * Configures a counter. * * @param tableName the name * @return the counter */ private ModificationTally configureCounter(String tableName) { Task task = new AbstractSessionTask() { @Override public void run() { ModificationTally counter = new ModificationTally(DbModificationTracker.this, tableName); IdSerialTuple idSer = selectIdSerialForName(tableName); long lastSerial = idSer == null ? 0 : idSer.getSerial(); counter.setLastSerial(lastSerial); countersByName.put(tableName, counter); countersById.put(counter.getId(), counter); } }; if (isTaskDispatcherThread() || !isAlive()) { task.run(); } else { addTaskAndWait(task); } return countersByName.get(tableName); } /** * Configures the master entry. */ private void configureMaster() { Task task = new AbstractSessionTask() { @Override public void run() { long serial = selectMasterSerial(); masterEntry = new TableEntry(null, 0, serial); } }; if (isTaskDispatcherThread() || !isAlive()) { task.run(); } else { addTaskAndWait(task); } } /** * Configures a named entry. * * @param name the name */ private void configureName(String name) { Task task = new AbstractSessionTask() { @Override public void run() { IdSerialTuple idSer = selectIdSerialForName(name); if (idSer == null) { // missing: create it getCounter(name).addToModificationTable(); idSer = selectIdSerialForName(name); } TableEntry entry = new TableEntry(name, idSer.getId(), idSer.getSerial()); tableEntriesByName.put(name, entry); tableEntriesById.put(idSer.getId(), entry); } }; if (isTaskDispatcherThread() || !isAlive()) { task.run(); } else { addTaskAndWait(task); } } /** * runs the shutdown runnables */ private void invokeShutdownRunnables() { // run each runnable only once (in case of loops) List tempList = new ArrayList<>(shutdownRunnables); shutdownRunnables.clear(); for (Runnable r: tempList) { try { r.run(); } catch (RuntimeException ex) { // log but don't cause MD to stop LOGGER.logStacktrace(Level.SEVERE, ex); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy