/* * #! * Ontopia Engine * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.persistence.proxy; import gnu.trove.procedure.TObjectIntProcedure; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import net.ontopia.topicmaps.impl.rdbms.TMObject; import net.ontopia.utils.CompactIdentityHashSet; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.utils.PropertyUtils; import org.apache.commons.collections4.map.LRUMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: The read-write proxy transaction implementation. */ public class RWTransaction extends AbstractTransaction { // Define a logging category. static Logger log = LoggerFactory.getLogger(RWTransaction.class.getName()); // Changes tracked for cache invalidation public boolean trackall; public final ObjectStates ostates = new ObjectStates(); // Unflushed change sets protected Set<PersistentIF> chgcre = new CompactIdentityHashSet<PersistentIF>(5); protected Set<PersistentIF> chgdel = new CompactIdentityHashSet<PersistentIF>(5); protected Set<PersistentIF> chgdty = new CompactIdentityHashSet<PersistentIF>(5); protected Map<IdentityIF, IdentityIF> merges = new LinkedHashMap<IdentityIF, IdentityIF>(); public RWTransaction(StorageAccessIF access) { super("TX" + access.getId(), access); // initialize shared data cache StorageCacheIF scache = access.getStorage().getStorageCache(); if (scache != null) trackall = true; this.txncache = new RWLocalCache(this, scache); // initialize identity map this.lrusize = PropertyUtils.getInt(access.getProperty("net.ontopia.topicmaps.impl.rdbms.Cache.identitymap.lru"), 300); this.lru = new LRUMap(lrusize); // instrument transaction cache int dinterval = PropertyUtils.getInt(access.getStorage().getProperty("net.ontopia.topicmaps.impl.rdbms.Cache.local.debug"), -1); if (dinterval > 0) { log.info("Instrumenting local cache."); this.txncache = new StatisticsCache("lcache", this.txncache, dinterval); } // Get access registrar from transaction cache (currently the // local data cache) this.registrar = txncache.getRegistrar(); // Use IdentityIF object access this.oaccess = new PersistentObjectAccess(this); } public boolean isClean() { return ostates.isClean(); } public boolean isReadOnly() { return false; } // ----------------------------------------------------------------------------- // Life cycle // ----------------------------------------------------------------------------- public void assignIdentity(PersistentIF object) { // FIXME: this method is currently being used in TMObject // constructor. should consider getting rid of it. if (!isactive) throw new TransactionNotActiveException(); if (object._p_getIdentity() != null) throw new OntopiaRuntimeException("Cannot add new identity to object that already has one."); // create and assign new identity IdentityIF identity = access.generateIdentity(object._p_getType()); object._p_setIdentity(identity); // consider new object when assigned new identity object.setNewObject(true); } public void create(PersistentIF object) { if (!isactive) throw new TransactionNotActiveException(); // assign identity if object has no identity IdentityIF identity = object._p_getIdentity(); if (identity == null) { assignIdentity(object); identity = object._p_getIdentity(); } //! System.out.println(">>* " + identity); // change state and update changeset if (object.isTransient()) { // add object to the to-be-created list chgcre.add(object); // change state object.setPersistent(true); } else if (object.isDeleted()) { //! throw new OntopiaRuntimeException("Cannot recreate deleted object: " + identity + ")"); // Figure out if object really has been deleted. if (!chgdel.remove(object)) { // Add object to the to-be-created list chgcre.add(object); } // change state object.setPersistent(true); } else { throw new OntopiaRuntimeException("Object in invalid state: " + identity + ")"); } // track all changes objectCreated(object); //! FIXME: No need to register transaction since it should already //! be registered. A check for existing transaction could be useful. //! object._p_setTransaction(this); // Register with identity map synchronized (identity_map) { Object other = identity_map.put(identity, object); // Register with LRU cache lru.put(identity, object); // ISSUE: What if identity is already registered? if (other != null && other != object) log.warn("Created object replaced existing object: " + identity); } //! // Notify access registrar //! if (registrar != null) registrar.registerIdentity(identity); if (log.isDebugEnabled()) log.debug(getId() + ": Object " + identity + " created: " + object._p_getType()); //! System.out.println("CREATING: " + identity + " created: " + object); } public void delete(PersistentIF object) { if (!isactive) throw new TransactionNotActiveException(); IdentityIF identity = object._p_getIdentity(); //! System.out.println(">>/ " + identity); // change state and update changeset if (object.isPersistent()) { // retrieve all fields from database, so that we can continue using the object // detach object object.detach(); // mark object as deleted object.setDeleted(true); // add object to the to-be-deleted list chgcre.remove(object); chgdty.remove(object); chgdel.add(object); // track all changes objectDeleted(object); } else { throw new OntopiaRuntimeException("Object in invalid state: " + identity + ")"); } //! // Unregister with identity map //! synchronized (identity_map) { //! // ISSUE: What if identity is not already registered? //! identity_map.remove(identity); //! // Unregister with LRU cache //! lru.remove(identity); //! } if (log.isDebugEnabled()) log.debug(getId() + ": Object " + identity + " deleted."); } // ----------------------------------------------------------------------------- // Lifecycle // ----------------------------------------------------------------------------- protected boolean flushing; public synchronized void flush() { // Flushing is non-reentrant if (flushing) return; // Complain if the transaction is not active if (!isactive) throw new TransactionNotActiveException(); try { flushing = true; // this method is synchronized, because otherwise it is possible for // two different threads to flush the same changes at the same time, // causing violations of unique ID values and suchlike. // All dirty [modified, created and deleted]objects have strong // references to them. // Only flush when changes has actually happened. if (chgcre.isEmpty() && chgdty.isEmpty() && chgdel.isEmpty()) return; //! System.out.println("SC: " + chgcre.size()); //! System.out.println("SD: " + chgdty.size()); //! System.out.println("SR: " + chgdel.size()); // Store all pending changes in the database if (log.isDebugEnabled()) log.debug(getId() + ": Storing transaction changes."); //! System.out.println("+FLUSHING"); // Create objects marked for creation if (!chgcre.isEmpty()) { for (PersistentIF object : chgcre) { //! System.out.println("FLUSH CREATE: " + object._p_getIdentity()); // Store object in repository access.createObject(oaccess, object); // Mark object as stored in database object.setInDatabase(true); // WARN: Make sure that all non-default fields are marked // dirty at this point!!! //! // Mark object as being persisted //! object._p_setPersistent(true); // Store dirty object fields in repository access.storeDirty(oaccess, object); } } // Store modified object fields if (!chgdty.isEmpty()) { for (PersistentIF object : chgdty) { // Store dirty object fields in repository if (!object.isDeleted()) access.storeDirty(oaccess, object); } } // Delete objects marked for deletion if (!chgdel.isEmpty()) { for (PersistentIF object : chgdel) { // Delete object from repository access.deleteObject(oaccess, object); // Mark object as no longer stored in database object.setInDatabase(false); } } // Since there were no complaints we can now clear the transaction // changes. chgcre.clear(); chgdel.clear(); chgdty.clear(); // Flush storage access access.flush(); //! System.out.println("-FLUSHING"); } finally { flushing = false; } if (log.isDebugEnabled()) log.debug(getId() + ": Transaction changes stored."); } // ----------------------------------------------------------------------------- // Object modification callbacks (called by PersistentIFs) // ----------------------------------------------------------------------------- public synchronized void objectDirty(PersistentIF object) { if (!isactive) throw new TransactionNotActiveException(); if (log.isDebugEnabled()) log.debug(getId() + ": Object dirty " + object._p_getIdentity()); chgdty.add(object); // track all changes if (trackall) ostates.dirty(object._p_getIdentity()); } public void objectRead(IdentityIF identity) { if (trackall) ostates.read(identity); } public void objectCreated(PersistentIF object) { if (trackall) ostates.created(object._p_getIdentity()); } public void objectDeleted(PersistentIF object) { if (trackall) ostates.deleted(object._p_getIdentity()); } public boolean isObjectClean(IdentityIF identity) { return ostates.isClean(identity); } /** * INTERNAL: Called by RDBMSTopicMapStore to notify the transaction * of a performed merge. * @param source * @param target */ public void registerMerge(TMObject source, TMObject target) { merges.put(source._p_getIdentity(), target._p_getIdentity()); } /** * {@inheritDoc} * RWTransaction notifies the added and modified objects of the merge, * allowing them to update their fields as needed. */ @Override public void objectMerged(IdentityIF source, IdentityIF target) { // let the added and modified objects update their fields as needed // todo: removed items shouldn't cause problems ?? for (Object o : chgcre) { if (o instanceof AbstractRWPersistent) { ((AbstractRWPersistent) o).syncAfterMerge(source, target); } } for (Object o : chgdty) { if (o instanceof AbstractRWPersistent) { ((AbstractRWPersistent) o).syncAfterMerge(source, target); } } } // ----------------------------------------------------------------------------- // Transaction boundary callbacks // ----------------------------------------------------------------------------- protected synchronized void transactionPreCommit() { } protected synchronized void transactionPostCommit() { // clear change sets chgcre.clear(); chgdty.clear(); chgdel.clear(); // sync merges to other transactions for (Map.Entry<IdentityIF, IdentityIF> entry : merges.entrySet()) { ((RDBMSStorage)access.getStorage()).objectMerged(entry.getKey(), entry.getValue(), this); } merges.clear(); // invalidate object state if (trackall) { synchronized (identity_map) { synchronized (ostates) { // clear objects and caches if (ostates.size() > 0) { // register eviction process txncache.registerEviction(); try { ostates.forEachEntry(new TObjectIntProcedure<IdentityIF>() { public boolean execute(IdentityIF identity, int s) { if ((s & ObjectStates.STATE_CREATED) == ObjectStates.STATE_CREATED) { // no-op } else if ((s & ObjectStates.STATE_DELETED) == ObjectStates.STATE_DELETED) { // evict identity from cache txncache.evictIdentity(identity, true); } else if ((s & ObjectStates.STATE_DIRTY) == ObjectStates.STATE_DIRTY) { // evict fields from cache txncache.evictFields(identity, true); } // clear object PersistentIF p = checkIdentityMapNoLRU(identity); if (p != null) p.clearAll(); return true; } }); } finally { // deregister eviction txncache.releaseEviction(); } // clear object states ostates.clear(); } } } } } protected synchronized void transactionPreAbort() { } protected synchronized void transactionPostAbort() { // clear change sets chgcre.clear(); chgdty.clear(); chgdel.clear(); merges.clear(); // WARNING: after an abort we really want to get rid of all // existing objects, because we no longer know the states of the // objects. Objects may have transitioned to a new lifecycle state // inside the aborted transaction, so there is know way that one // can know if an object is persistent or deleted etc. // // Conclusion is that txns that have had // invalidate object state if (trackall) { synchronized (identity_map) { synchronized (ostates) { // clear objects only if (ostates.size() > 0) { // register eviction process txncache.registerEviction(); try { ostates.forEachEntry(new TObjectIntProcedure<IdentityIF>() { public boolean execute(IdentityIF identity, int s) { if (((s & ObjectStates.STATE_CREATED) == ObjectStates.STATE_CREATED) || ((s & ObjectStates.STATE_DELETED) == ObjectStates.STATE_DELETED)) { // drop from identity map PersistentIF p = checkIdentityMapNoLRU(identity); if (p != null) p.clearAll(); } else { // clear dirty/dirty-read object PersistentIF p = checkIdentityMapNoLRU(identity); if (p != null) p.clearAll(); } return true; } }); } finally { // deregister eviction txncache.releaseEviction(); } // clear object states ostates.clear(); } } } } } // ----------------------------------------------------------------------------- // Prefetching // ----------------------------------------------------------------------------- public void prefetch(Class<?> type, int field, boolean traverse, Collection<IdentityIF> identities) { // do not prefetch when no shared cache if (!trackall) super.prefetch(type, field, traverse, identities); } public void prefetch(Class<?> type, int[] fields, boolean[] traverse, Collection<IdentityIF> identities) { // do not prefetch when no shared cache if (!trackall) super.prefetch(type, fields, traverse, identities); } }