001: /**
002: * Copyright (C) 2008 NetMind Consulting Bt.
003: *
004: * This library is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2.1 of the License, or (at your option) any later version.
008: *
009: * This library is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */package hu.netmind.persistence.node;
018:
019: import hu.netmind.persistence.*;
020: import java.util.*;
021: import org.apache.log4j.Logger;
022:
023: /**
024: * This class tracks changes to objects. It caches the most recent changes, but
025: * also determines whether an object changed by reading the database.
026: * @author Brautigam Robert
027: * @version CVS Revision: $Revision$
028: */
029: public class ModificationTracker {
030: public static long MAX_ENTRY_AGE = 30 * 60 * 1000;
031: private static Logger logger = Logger
032: .getLogger(ModificationTracker.class);
033:
034: private StoreContext context;
035: private Map entriesById = new HashMap();
036: private SortedSet entriesByDate = new TreeSet();
037: private Map entriesByTxSerial = new HashMap();
038: private Map entriesByClass = new HashMap();
039: private Map lastSerialsByClass = new HashMap();
040: private Long lastCacheRemovedSerial = null;
041:
042: public ModificationTracker(StoreContext context) {
043: this .context = context;
044: }
045:
046: private void modifyClassEntries(ModificationEntry entry, boolean add) {
047: modifyClassEntries(entry.objectClass, entry, add);
048: }
049:
050: private void modifyClassEntries(Class currentClass,
051: ModificationEntry entry, boolean add) {
052: if (currentClass == null)
053: return;
054: // Do current class
055: Set entries = (Set) entriesByClass.get(currentClass);
056: if (entries == null) {
057: entries = new HashSet();
058: entriesByClass.put(currentClass, entries);
059: }
060: if (add)
061: entries.add(entry);
062: else
063: entries.remove(entry);
064: if (entries.size() == 0)
065: entriesByClass.remove(currentClass);
066: // Do superclass
067: modifyClassEntries(currentClass.getSuperclass(), entry, add);
068: // Do interfaces
069: Class interfaces[] = currentClass.getInterfaces();
070: for (int i = 0; i < interfaces.length; i++)
071: modifyClassEntries(interfaces[i], entry, add);
072: }
073:
074: /**
075: * This method registers "potential" change serials for a given id. The
076: * change becomes final only when the transaction's commit ends.
077: */
078: public synchronized void changeCandidates(List metas,
079: Long endSerial, Long txSerial) {
080: // Clear obsolate data
081: while ((!entriesByDate.isEmpty())
082: && (System.currentTimeMillis()
083: - ((ModificationEntry) entriesByDate.first()).entryDate
084: .getTime() > MAX_ENTRY_AGE)) {
085: // The first entry is obsolate, so clear it from the cache
086: ModificationEntry entry = (ModificationEntry) entriesByDate
087: .first();
088: entriesById.remove(entry.id);
089: entriesByDate.remove(entry);
090: if (entry.potentialTxSerial != null)
091: entriesByTxSerial.remove(entry.potentialTxSerial);
092: if (entry.lastChangeSerial != null)
093: lastCacheRemovedSerial = entry.lastChangeSerial;
094: modifyClassEntries(entry, false); // Remove
095: }
096: // Add all data to internal structures
097: Vector entries = new Vector();
098: entriesByTxSerial.put(txSerial, entries);
099: for (int i = 0; i < metas.size(); i++) {
100: PersistenceMetaData meta = (PersistenceMetaData) metas
101: .get(i);
102: // Search or create entry
103: ModificationEntry entry = (ModificationEntry) entriesById
104: .get(meta.getPersistenceId());
105: if (entry == null) {
106: entry = new ModificationEntry();
107: entry.id = meta.getPersistenceId();
108: entry.objectClass = meta.getObjectClass();
109: entry.entryDate = new Date();
110: // Update data structure
111: entriesById.put(entry.id, entry);
112: entriesByDate.add(entry);
113: modifyClassEntries(entry, true);
114: }
115: entry.potentialChangeSerial = endSerial;
116: if (entry.potentialTxSerial != null)
117: entriesByTxSerial.remove(entry.potentialTxSerial); // Old tx overridden
118: entry.potentialTxSerial = txSerial;
119: entries.add(entry);
120: }
121: }
122:
123: /**
124: * Set definite last modification serial for a class, and for all superclasses
125: * and interfaces.
126: */
127: private void setLastSerial(Class currentClass, Long serial) {
128: if (currentClass == null)
129: return;
130: // Handle class
131: lastSerialsByClass.put(currentClass, serial);
132: // Superclass
133: setLastSerial(currentClass.getSuperclass(), serial);
134: // Interfaces
135: Class interfaces[] = currentClass.getInterfaces();
136: for (int i = 0; i < interfaces.length; i++)
137: setLastSerial(interfaces[i], serial);
138: }
139:
140: /**
141: * Ends a transaction, which means it finalizes the entries associated
142: * with the given transaction.
143: */
144: public synchronized void endTransaction(Long txSerial) {
145: // Get and remove entries from tx map
146: List entries = (List) entriesByTxSerial.remove(txSerial);
147: if (entries == null)
148: return; // No ids, probably it became obsolate
149: // Go through entries of ids and actualize them
150: for (int i = 0; i < entries.size(); i++) {
151: ModificationEntry entry = (ModificationEntry) entries
152: .get(i);
153: entry.lastChangeSerial = entry.potentialChangeSerial;
154: entry.potentialTxSerial = null;
155: // Insert into the 'persistent' class serial table.
156: setLastSerial(entry.objectClass, entry.lastChangeSerial);
157: }
158: }
159:
160: /**
161: * Return whether the given object changed since the given serial.
162: */
163: private boolean hasChanged(Long id, Class objectClass, Long serial) {
164: // Try to get a modification entry if there is one for that object
165: ModificationEntry entry = (ModificationEntry) entriesById
166: .get(id);
167: // If there was no entry, then there was no modification
168: // since the last removed serial. If the query serial is newer than
169: // the last removed serial, then the object must be current.
170: if ((entry == null)
171: && ((lastCacheRemovedSerial == null) || (serial
172: .longValue() > lastCacheRemovedSerial
173: .longValue()))) {
174: logger
175: .debug("there was no modification yet for this object in cache, it is current.");
176: return false;
177: }
178: // If there was no entry, and the query serial is old, we must check the
179: // database whether that object is current. Note, that at this point the
180: // object should be locked, or else this query can not tell for sure.
181: if ((entry == null)
182: && (lastCacheRemovedSerial != null)
183: && (serial.longValue() <= lastCacheRemovedSerial
184: .longValue())) {
185: // Query the object now
186: Object current = context.getStore().findSingle(
187: "find " + objectClass.getName()
188: + " where persistenceId = " + id);
189: PersistenceMetaData persistenceMeta = context.getStore()
190: .getPersistenceMetaData(current);
191: if (persistenceMeta.getPersistenceStart().longValue() <= serial
192: .longValue()) {
193: logger
194: .debug("object is old, but database said it is current.");
195: return false;
196: }
197: }
198: // If there is an enty, but it has potentialTxSerial, that means
199: // it was in a transaction, which started commiting, but never finished.
200: // The client disconnected, and the locks were relinquished. In this
201: // case, we must query the database to make sure the transaction commited.
202: if ((entry != null) && (entry.potentialTxSerial != null)) {
203: // Query the object now
204: Object current = context.getStore().findSingle(
205: "find " + objectClass.getName()
206: + " where persistenceId = " + id);
207: PersistenceMetaData persistenceMeta = context.getStore()
208: .getPersistenceMetaData(current);
209: // If the metadata's serial equals the potentialChangeSerial, then commit
210: // the transaction manually.
211: if (persistenceMeta.getPersistenceStart().longValue() >= entry.potentialChangeSerial
212: .longValue()) {
213: // Transaction was committed successfully
214: endTransaction(entry.potentialTxSerial);
215: } else {
216: // Originating transaction failed. Remove those entries which do
217: // not have a lastChangeSerial, because those were not modified yet.
218: List entries = (List) entriesByTxSerial
219: .remove(entry.potentialTxSerial);
220: for (int i = 0; (entries != null)
221: && (i < entries.size()); i++) {
222: ModificationEntry removeEntry = (ModificationEntry) entries
223: .get(i);
224: entriesById.remove(removeEntry.id);
225: entriesByDate.remove(removeEntry);
226: modifyClassEntries(removeEntry, false);
227: }
228: }
229: // Now compare the query serial with the object start serial
230: if (persistenceMeta.getPersistenceStart().longValue() <= serial
231: .longValue()) {
232: logger
233: .info("transaction commit not received for serial: "
234: + entry.potentialTxSerial
235: + ", but transaction commited according to database.");
236: return false;
237: }
238: }
239: // Default is to compare entry's last change serial with the query date.
240: // At this point both must be present.
241: if (entry.lastChangeSerial.longValue() <= serial.longValue()) {
242: logger
243: .debug("there was a change for that object in cache, but the object is newer.");
244: return false;
245: }
246: // Last default is return session info
247: logger.debug("object was not current, fallthrough return");
248: return true;
249: }
250:
251: /**
252: * This is the query method of the tracker. It returns whether an object that was
253: * queried from the database at a given time is the current version of the object
254: * or not. Implementation note: Currently empty session infos are returned,
255: * this may later change, if session infos will be also kept track of.
256: * @return A SessionInfo object if the object is not current. The session info
257: * may contain the last modification transaction's info, but this is not guaranteed.
258: */
259: public synchronized SessionInfo isCurrent(LockMetaData meta) {
260: logger.debug("making sure meta: " + meta + " is current.");
261: // If the startdate is not set, then object is not yet saved, it is current.
262: if ((meta.getObjectId() != null)
263: && (meta.getStartSerial() == null)) {
264: logger
265: .debug("object is not yet persistent, so it is current.");
266: return null;
267: }
268: // If the end serial is set, then the object is definitely not current
269: // (it is deleted)!
270: if ((meta.getObjectId() != null)
271: && (meta.getEndSerial() != null)) {
272: logger
273: .debug("object has enddate set, it will not be considered current.");
274: return new SessionInfo();
275: }
276: // If the query date is not set, then we have no data to match to, return current.
277: if (meta.getQuerySerial() == null) {
278: logger
279: .debug("metadata contained no query serial, returning 'current'.");
280: return null;
281: }
282: // If the meta represents an object, then
283: // Test, whether entry indicates a change since the query of the meta we received
284: if (meta.getObjectId() != null) {
285: if (!hasChanged(meta.getObjectId(), meta.getObjectClass(),
286: meta.getQuerySerial()))
287: return null;
288: }
289: // If the meta represents a class, then we must check each modification
290: // which concerns this class, whether they newer are than the given serial.
291: if (meta.getObjectId() == null) {
292: Set entries = (Set) entriesByClass.get(meta
293: .getObjectClass());
294: if (entries != null) {
295: // If the entries are not null, then check whether there was a valid
296: // modification.
297: Iterator entriesIterator = entries.iterator();
298: while (entriesIterator.hasNext()) {
299: ModificationEntry entry = (ModificationEntry) entriesIterator
300: .next();
301: if (hasChanged(entry.id, entry.objectClass, meta
302: .getQuerySerial()))
303: return new SessionInfo(); // Change of object is newer, so class changed
304: }
305: }
306: // Get the entries again. If the set is non-existent, then there are no valid
307: // changes for this class. It is possible the above code returned changes, but
308: // those were rolledback.
309: entries = (Set) entriesByClass.get(meta.getObjectClass());
310: if (entries == null) {
311: // There were no changes to the class, so determine from modification table.
312: // Modification table would not be enough by itself, because modifications may have
313: // been activated which the modification table does not contain. So the above
314: // is necessary.
315: Long lastTableSerial = (Long) lastSerialsByClass
316: .get(meta.getObjectClass());
317: if ((lastTableSerial == null)
318: || (lastTableSerial.longValue() < meta
319: .getQuerySerial().longValue()))
320: return null;
321: } else {
322: // There were entries, and they indicate that this table is current
323: return null;
324: }
325: }
326: // Fallback (not current).
327: return new SessionInfo();
328: }
329:
330: public class ModificationEntry implements Comparable {
331: public Long id;
332: public Class objectClass;
333: public Date entryDate;
334: public Long lastChangeSerial;
335: public Long potentialChangeSerial;
336: public Long potentialTxSerial;
337:
338: public int compareTo(Object entry) {
339: return entryDate
340: .compareTo(((ModificationEntry) entry).entryDate);
341: }
342:
343: public int hashCode() {
344: return id.hashCode();
345: }
346:
347: public boolean equals(Object o) {
348: return id == ((ModificationEntry) o).id;
349: }
350: }
351:
352: static {
353: try {
354: ResourceBundle config = ResourceBundle
355: .getBundle("beankeeper");
356: MAX_ENTRY_AGE = Long.valueOf(
357: config.getString("cache.modification_max_age"))
358: .longValue();
359: } catch (Exception e) {
360: logger
361: .error(
362: "could not load configuration, using hardcoded defaults",
363: e);
364: }
365: }
366: }
|