/* * Copyright 2009-2016 Tilmann Zaeschke. All rights reserved. * * This file is part of ZooDB. * * ZooDB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ZooDB 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ZooDB. If not, see <http://www.gnu.org/licenses/>. * * See the README and COPYING files for further information. */ package org.zoodb.internal; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import javax.jdo.JDOOptimisticVerificationException; import javax.jdo.ObjectState; import javax.jdo.listener.DeleteCallback; import javax.jdo.listener.InstanceLifecycleListener; import javax.jdo.listener.StoreCallback; import org.zoodb.api.ZooInstanceEvent; import org.zoodb.api.impl.ZooPC; import org.zoodb.internal.client.SchemaManager; import org.zoodb.internal.client.session.ClientSessionCache; import org.zoodb.internal.server.OptimisticTransactionResult; import org.zoodb.internal.server.TxObjInfo; import org.zoodb.internal.util.ClientLock; import org.zoodb.internal.util.DBLogger; import org.zoodb.internal.util.IteratorRegistry; import org.zoodb.internal.util.MergingIterator; import org.zoodb.internal.util.TransientField; import org.zoodb.internal.util.Util; import org.zoodb.schema.ZooSchema; import org.zoodb.tools.DBStatistics; import org.zoodb.tools.DBStatistics.STATS; import org.zoodb.tools.ZooHelper; /** * The main session class. * * @author ztilmann * */ public class Session implements IteratorRegistry { public static final long OID_NOT_ASSIGNED = -1; public static final long TIMESTAMP_NOT_ASSIGNED = -1; public static final Class<?> PERSISTENT_SUPER = ZooPC.class; /** Primary node. Also included in the _nodes list. */ private Node primary; /** All connected nodes. Includes the primary node. */ private final ArrayList<Node> nodes = new ArrayList<Node>(); private final SessionParentCallback parentSession; private final ClientSessionCache cache; private final SchemaManager schemaManager; private boolean isOpen = true; private boolean isActive = false; private final SessionConfig config; private final ClientLock lock = new ClientLock(); private final HashMap<DBStatistics.STATS, Long> stats = new HashMap<>(); private long transactionId = -1; private final WeakHashMap<Closeable, Object> resources = new WeakHashMap<Closeable, Object>(); public Session(String dbPath, SessionConfig config) { this(null, dbPath, config); } public Session(SessionParentCallback parentSession, String dbPath, SessionConfig config) { if (dbPath == null || "".equals(dbPath)) { throw DBLogger.newUser("No URL or database name given. Please specify, " + "for example via PersistenceManagerFactory.setConnectionURL()"); } dbPath = ZooHelper.getDataStoreManager().getDbPath(dbPath); this.parentSession = parentSession; this.config = config; this.primary = ZooFactory.get().createNode(dbPath, this); this.cache = new ClientSessionCache(this); this.schemaManager = new SchemaManager(cache, config.getAutoCreateSchema()); this.nodes.add(primary); this.cache.addNode(primary); this.primary.connect(); if (DBLogger.isLoggable(Level.FINE)) { DBLogger.LOGGER.fine("Session created (ihc=" + System.identityHashCode(this) + ")"); } } public boolean isActive() { return isActive; } public void begin() { try { if (DBLogger.isLoggable(Level.FINE)) { DBLogger.LOGGER.fine("begin(txId=" + transactionId + ")"); } lock(); checkOpen(); if (isActive) { throw DBLogger.newUser("Can't open new transaction inside existing transaction."); } isActive = true; for (Node n: nodes) { long txId = n.beginTransaction(); if (n == primary) { transactionId = txId; } } } finally { unlock(); } } /** * Verify optimistic consistency of the current transaction. * * @throws JDOOptimisticVerificationException containing all failed objects if * any objects fail. */ public void checkConsistency() { try { lock(); processOptimisticVerification(true); } finally { unlock(); } } public void commit(boolean retainValues) { long t1 = System.nanoTime(); try { lock(); checkActive(); //pre-commit: traverse object tree for transitive persistence cache.persistReachableObjects(); //commit phase #1: prepare, check conflicts, get optimistic locks //This needs to happen after OGT (we need the OIDs) and before everything else (avoid //any writes in case of conflict AND we need the WLOCK before any updates. processOptimisticVerification(false); try { schemaManager.commit(); commitInternal(); //commit phase #2: Updated database properly, release locks for (Node n: nodes) { n.commit(); } cache.postCommit(retainValues, config.getDetachAllOnCommit()); schemaManager.postCommit(); } catch (RuntimeException e) { try { if (DBLogger.isLoggable(Level.FINE)) { DBLogger.LOGGER.fine("commit(txId=" + transactionId + ") aborted, rolling back"); } if (DBLogger.isUser(e)) { //reset sinks for (ZooClassDef cs: cache.getSchemata()) { cs.getProvidedContext().getDataSink().reset(); cs.getProvidedContext().getDataDeleteSink().reset(); } //allow for retry after user exceptions for (Node n: nodes) { n.revert(); } } rollbackInteral(); } catch (Throwable t) { //YES! Finally a good reason to swallow an exception. //Exception 'e' is of course more important than 't', so we swallow it... DBLogger.severe("rollback() failed: " + t.getMessage()); t.printStackTrace(); } throw e; } closeResources(); isActive = false; } finally { unlock(); if (DBLogger.isLoggable(Level.FINE)) { long t2 = System.nanoTime(); DBLogger.LOGGER.fine("commit(txId=" + transactionId + ") finished - Time=" + (t2-t1) + "ns"); } } } private void getObjectToCommit(ArrayList<TxObjInfo> updates) { for (ZooPC pc: cache.getDeletedObjects()) { updates.add(new TxObjInfo(pc.jdoZooGetOid(), pc.jdoZooGetTimestamp(), true)); } for (ZooPC pc: cache.getDirtyObjects()) { updates.add(new TxObjInfo(pc.jdoZooGetOid(), pc.jdoZooGetTimestamp(), false)); } for (GenericObject pc: cache.getDirtyGenericObjects()) { updates.add(new TxObjInfo(pc.getOid(), pc.jdoZooGetTimestamp(), pc.jdoZooIsDeleted())); } for (ZooClassDef cd: cache.getSchemata()) { if (cd.jdoZooIsDeleted() || cd.jdoZooIsNew() || cd.jdoZooIsDirty()) { updates.add(new TxObjInfo(cd.jdoZooGetOid(), cd.jdoZooGetTimestamp(), cd.jdoZooIsDeleted())); } } } private void processOptimisticVerification(boolean isTrialRun) { ArrayList<TxObjInfo> updates = new ArrayList<>(); getObjectToCommit(updates); OptimisticTransactionResult ovrSummary = new OptimisticTransactionResult(); for (Node n: nodes) { if (isTrialRun) { //check consistency ovrSummary.add( n.checkTxConsistency(updates) ); } else { //proper commit() ovrSummary.add( n.beginCommit(updates) ); } } processOptimisticTransactionResult(ovrSummary); if (!ovrSummary.getConflicts().isEmpty()) { JDOOptimisticVerificationException[] ea = new JDOOptimisticVerificationException[ovrSummary.getConflicts().size()]; int pos = 0; for (Long oid: ovrSummary.getConflicts()) { Object failedObj = cache.findCoByOID(oid); if (failedObj == null) { //generic object failedObj = cache.getGeneric(oid).getOrCreateHandle(); } ea[pos] = new JDOOptimisticVerificationException(Util.oidToString(oid), failedObj); pos++; } if (!isTrialRun) { //perform rollback rollbackInteral(); } throw new JDOOptimisticVerificationException("Optimistic verification failed", ea); } } private void processOptimisticTransactionResult(OptimisticTransactionResult otr) { if (otr.requiresReset()) { isActive = false; closeInternal(); throw DBLogger.newFatalDataStore( "Database schema has changed, please reconnect. ", null); } if (otr.requiresRefresh()) { if (schemaManager.hasChanges()) { //remote index update & local schema updates (could be index) --> considered bad! throw new JDOOptimisticVerificationException("Optimistic verification failed " + "because schema changes occurred in remote concurrent sessions."); } // refresh schema, this works only for indexes schemaManager.refreshSchemaAll(); } } private void commitInternal() { //create new schemata Collection<ZooClassDef> schemata = cache.getSchemata(); //First delete for (ZooPC co: cache.getDeletedObjects()) { if (!co.jdoZooIsDirty()) { throw new IllegalStateException("State="); } if (co.jdoZooIsDeleted()) { if (co.jdoZooIsNew()) { //ignore continue; } if (co.jdoZooGetClassDef().jdoZooIsDeleted()) { //Ignore instances of deleted classes, there is a dropInstances for them continue; } if (co instanceof DeleteCallback) { ((DeleteCallback)co).jdoPreDelete(); } co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.PRE_DELETE); co.jdoZooGetContext().getDataDeleteSink().delete(co); } else { throw new IllegalStateException("State="); } } //generic objects if (!cache.getDirtyGenericObjects().isEmpty()) { for (GenericObject go: cache.getDirtyGenericObjects()) { if (go.jdoZooIsDeleted() && !go.jdoZooIsNew()) { if (!go.checkPcDeleted()) { go.jdoZooGetContext().getDataDeleteSink().deleteGeneric(go); } } } } //flush sinks for (ZooClassDef cs: schemata) { cs.getProvidedContext().getDataDeleteSink().flush(); } //Then update. This matters for unique indices where deletion must occur before updates. for (ZooPC co: cache.getDirtyObjects()) { if (!co.jdoZooIsDirty()) { //can happen when object are refreshed after being marked dirty? //TODO //throw new IllegalStateException("State="); continue; } if (!co.jdoZooIsDeleted()) { if (co instanceof StoreCallback) { ((StoreCallback)co).jdoPreStore(); } co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.PRE_STORE); co.jdoZooGetContext().getDataSink().write(co); } } //generic objects if (!cache.getDirtyGenericObjects().isEmpty()) { //TODO we are iterating twice through dirty/deleted objects... is that necessary? for (GenericObject go: cache.getDirtyGenericObjects()) { if (!go.jdoZooIsDeleted()) { go.verifyPcNotDirty(); //TODO //TODO //TODO //TODO What is this for ????? //TODO //TODO //TODO go.toStream(); go.jdoZooGetContext().getDataSink().writeGeneric(go); } } } //flush sinks for (ZooClassDef cs: schemata) { cs.getProvidedContext().getDataSink().flush(); } } public void rollback() { try { if (DBLogger.isLoggable(Level.FINE)) { DBLogger.LOGGER.fine("rollback(txId=" + transactionId + ")"); } lock(); checkActive(); rollbackInteral(); } finally { unlock(); } } public void rollbackInteral() { schemaManager.rollback(); OptimisticTransactionResult otr = new OptimisticTransactionResult(); for (Node n: nodes) { //drop the DB-locks otr.add( n.rollbackTransaction() ); } cache.rollback(); isActive = false; processOptimisticTransactionResult(otr); } public void makePersistent(ZooPC pc) { try { lock(); checkActive(); if (pc.jdoZooIsPersistent()) { if (pc.jdoZooGetContext().getSession() != this) { throw DBLogger.newUser("The object belongs to a different persistence manager."); } if (pc.jdoZooIsDeleted()) { throw DBLogger.newUser("The object has been deleted!"); } //nothing to do, is already persistent return; } primary.makePersistent(pc); } finally { unlock(); } } public void makeTransient(ZooPC pc) { try { lock(); checkActive(); if (!pc.jdoZooIsPersistent()) { //already transient return; } if (pc.jdoZooGetContext().getSession() != this) { throw DBLogger.newUser("The object belongs to a different persistence manager."); } if (pc.jdoZooIsDirty()) { throw DBLogger.newUser( "Dirty objects cannot be made transient: " + Util.getOidAsString(pc)); } //remove from cache cache.makeTransient((ZooPC) pc); } finally { unlock(); } } public static void assertOid(long oid) { if (oid == OID_NOT_ASSIGNED) { throw DBLogger.newUser("Invalid OID: " + oid); } } /** * INTERNAL !!!! * @param cls * @param subClasses * @param loadFromCache * @return An extent over a class */ public MergingIterator<ZooPC> loadAllInstances(Class<?> cls, boolean subClasses, boolean loadFromCache) { checkActiveRead(); MergingIterator<ZooPC> iter = new MergingIterator<ZooPC>(this, config.getFailOnClosedQueries()); ZooClassDef def = cache.getSchema(cls, primary); loadAllInstances(def.getVersionProxy(), subClasses, iter, loadFromCache); if (loadFromCache) { //also add 'new' instances iter.add(cache.iterator(def, subClasses, ObjectState.PERSISTENT_NEW)); } return iter; } /** * This method avoids nesting MergingIterators. * @param def * @param subClasses * @param iter */ private void loadAllInstances(ZooClassProxy def, boolean subClasses, MergingIterator<ZooPC> iter, boolean loadFromCache) { for (Node n: nodes) { iter.add(n.loadAllInstances(def, loadFromCache)); } if (subClasses) { for (ZooClassProxy sub: def.getSubProxies()) { loadAllInstances(sub, true, iter, loadFromCache); } } } public ZooHandleImpl getHandle(long oid) { try { lock(); checkActiveRead(); GenericObject gob = cache.getGeneric(oid); if (gob != null) { return gob.getOrCreateHandle(); } ZooPC co = cache.findCoByOID(oid); if (co != null) { if (co.jdoZooIsNew() || co.jdoZooIsDirty()) { //TODO the problem here is the initialisation of the GO, which would require //a way to serialize PCs into memory and deserialize them into an GO throw new UnsupportedOperationException("Handles on new or dirty Java PC objects " + "are not allowed. Please call commit() first or create handles with " + "ZooClass.newInstance() instead. OID: " + Util.getOidAsString(co)); } ZooClassDef schema = co.jdoZooGetClassDef(); GenericObject go = co.jdoZooGetNode().readGenericObject(schema, oid); return go.getOrCreateHandle(); } try { for (Node n: nodes) { //We should load the object only as byte[], if at all... ZooClassProxy schema = getSchemaManager().locateSchemaForObject(oid, n); GenericObject go = n.readGenericObject(schema.getSchemaDef(), oid); return go.getOrCreateHandle(); } } catch (RuntimeException e) { if (!DBLogger.isObjectNotFoundException(e)) { throw e; } //ignore, return null } return null; } finally { unlock(); } } public ZooHandleImpl getHandle(Object pc) { try { lock(); checkActiveRead(); ZooPC pci = checkObject(pc); long oid = pci.jdoZooGetOid(); GenericObject gob = cache.getGeneric(oid); if (gob != null) { return gob.getOrCreateHandle(); } if (pci.jdoZooIsNew() || pci.jdoZooIsDirty()) { //TODO the problem here is the initialisation of the GO, which would require //a way to serialize PCs into memory and deserialize them into an GO throw new UnsupportedOperationException("Handles on new or dirty Java PC objects " + "are not allowed. Please call commit() first or create handles with " + "ZooClass.newInstance() instead. OID: " + Util.getOidAsString(pci)); } ZooClassDef schema = pci.jdoZooGetClassDef(); GenericObject go = pci.jdoZooGetNode().readGenericObject(schema, oid); return go.getOrCreateHandle(); } finally { unlock(); } } /** * Refresh an Object. If the object has been deleted locally, it will * get the state of the object on disk. * @param pc */ public void refreshObject(Object pc) { try{ lock(); checkActiveRead(); refreshObjectInternal(pc); } finally { unlock(); } } private void refreshObjectInternal(Object pc) { ZooPC co = checkObjectForRefresh(pc); if (co.jdoZooIsPersistent()) { co.jdoZooGetNode().refreshObject(co); } } public void refreshAll() { try { lock(); checkActiveRead(); ArrayList<ZooPC> objs = new ArrayList<>(); for ( ZooPC pc: cache.getAllObjects() ) { ZooPC co = checkObjectForRefresh(pc); if (co.jdoZooIsPersistent()) { objs.add(co); } } //We use a separate loop here to avoid concurrent-mod exceptions in cases where a //remotely deleted object has to be removed from the local cache. for (ZooPC pc: objs) { try { refreshObjectInternal(pc); } catch (RuntimeException t) { if (DBLogger.OBJ_NOT_FOUND_EXCEPTION.isAssignableFrom(t.getClass())) { //okay, ignore, this happens if an object was delete remotely continue; } throw t; } } } finally { unlock(); } } public void refreshAll(Collection<?> arg0) { checkActiveRead(); for ( Object obj: arg0 ) { refreshObject(obj); } } /** * Check for base class, persistence state and PM affiliation. * @param pc * @return CachedObject */ private ZooPC checkObject(Object pc) { return checkObject(pc, false); } private ZooPC checkObject(Object pc, boolean ignoreForRefresh) { if (!(pc instanceof ZooPC)) { throw DBLogger.newUser("The object is not persistent capable: " + pc.getClass()); } ZooPC pci = (ZooPC) pc; if (!ignoreForRefresh && !pci.jdoZooIsPersistent()) { throw DBLogger.newUser("The object has not been made persistent yet."); } if (!ignoreForRefresh && pci.jdoZooIsDeleted()) { throw DBLogger.newUser("The object has alerady been deleted."); } if (pci.jdoZooGetContext().getSession() != this) { throw DBLogger.newUser("The object belongs to a different PersistenceManager."); } return pci; } /** * For refresh, we can ignore things like deletion or transience. * @param pc * @return the refreshed object */ private ZooPC checkObjectForRefresh(Object pc) { if (!(pc instanceof ZooPC)) { throw DBLogger.newUser("The object is not persistent capable: " + pc.getClass()); } ZooPC pci = (ZooPC) pc; if (!pci.jdoZooIsPersistent()) { return pci; } if (pci.jdoZooGetContext().getSession() != this) { throw DBLogger.newUser("The object belongs to a different PersistenceManager."); } return pci; } public Object getObjectById(Object arg0) { try { lock(); checkActiveRead(); long oid = (Long) arg0; ZooPC co = cache.findCoByOID(oid); if (co != null) { if (co.jdoZooIsStateHollow() && !co.jdoZooIsDeleted()) { co.jdoZooGetNode().refreshObject(co); } return co; } //find it for (Node n: nodes) { co = n.loadInstanceById(oid); if (co != null) { break; } } return co; } finally { unlock(); } } public Object[] getObjectsById(Collection<? extends Object> arg0) { checkActiveRead(); Object[] res = new Object[arg0.size()]; int i = 0; for ( Object obj: arg0 ) { res[i] = getObjectById(obj); i++; } return res; } /** * @param oid * @return Whether the object exists */ public boolean isOidUsed(long oid) { try { lock(); checkActiveRead(); //TODO we could also just compare it with max-value in the OID manager... ZooPC co = cache.findCoByOID(oid); if (co != null) { return true; } GenericObject go = cache.getGeneric(oid); if (go != null) { return true; } //find it for (Node n: nodes) { if (n.checkIfObjectExists(oid)) { return true; } } return false; } finally { unlock(); } } public void deletePersistent(Object pc) { try { lock(); checkActive(); ZooPC co = checkObject(pc); co.jdoZooMarkDeleted(); } finally { unlock(); } } public SchemaManager getSchemaManager() { checkOpen(); return schemaManager; } public void close() { if (!isOpen) { throw DBLogger.newUser("This session is closed."); } try { lock(); for (Node n: nodes) { n.closeConnection(); } cache.close(); closeResources(); TransientField.deregisterPm(this); isOpen = false; } finally { unlock(); } if (DBLogger.isLoggable(Level.FINE)) { DBLogger.LOGGER.fine("Session closed (ihc=" + System.identityHashCode(this) + ")"); } } private void closeInternal() { if (parentSession != null) { parentSession.close(); } else { close(); } } public Object getExternalSession() { checkOpen(); return parentSession; } public SessionConfig getConfig() { checkOpen(); return config; } public void evictAll() { try { lock(); checkActiveRead(); cache.evictAll(); } finally { unlock(); } } public void evictAll(Object[] pcs) { try { lock(); checkActiveRead(); for (Object obj: pcs) { ZooPC pc = (ZooPC) obj; if (!pc.jdoZooIsDirty()) { pc.jdoZooEvict(); } } } finally { unlock(); } } public void evictAll(boolean subClasses, Class<?> cls) { try { lock(); checkActiveRead(); cache.evictAll(subClasses, cls); } finally { unlock(); } } public Node getPrimaryNode() { return primary; } /** * INTERNAL !!!! * Iterators to be refreshed upon commit(). * @param it */ @Override public void registerResource(Closeable it) { resources.put(it, null); } @Override public void deregisterResource(Closeable iter) { resources.remove(iter); } private void closeResources() { try { for (Closeable c: resources.keySet().toArray(new Closeable[0])) { c.close(); } } catch (IOException e) { //This can currently not happen DBLogger.newFatal("Failed closing resource", e); } //TODO Why is this currently not done? //resources.clear(); } public Set<ZooPC> getCachedObjects() { try { lock(); checkActiveRead(); //We have to create a copy here to avoid users seeing //ConcurrentModificationExceptions while traversing the //list. Side-effect: we can return a modifiable collection. HashSet<ZooPC> ret = new HashSet<ZooPC>(); for (ZooPC o: cache.getAllObjects()) { ret.add(o); } return ret; } finally { unlock(); } } /** * Internal, don't call from outside! * @return The cache */ public ClientSessionCache internalGetCache() { return cache; } public void addInstanceLifecycleListener(InstanceLifecycleListener listener, Class<?>[] classes) { try { lock(); checkOpen(); if (classes == null) { classes = new Class[]{null}; } for (Class<?> cls: classes) { if (cls == null) { cls = ZooPC.class; } ZooClassDef def = cache.getSchema(cls, primary); if (def == null) { throw DBLogger.newUser("Cannot define listener for unknown class: " + cls); } def.getProvidedContext().addLifecycleListener(listener); } } finally { unlock(); } } public void removeInstanceLifecycleListener(InstanceLifecycleListener listener) { try { lock(); checkActiveRead(); for (ZooClassDef def: cache.getSchemata()) { def.getProvidedContext().removeLifecycleListener(listener); } } finally { unlock(); } } private void checkActive() { checkOpen(); if (!isActive) { throw DBLogger.newUser("Transaction is not active. Missing 'begin()'?"); } } public void checkActiveRead() { checkOpen(); if (!isActive && !config.getNonTransactionalRead()) { throw DBLogger.newUser("Transaction is not active. Missing 'begin()'?"); } } public void checkOpen() { if (!isOpen) { throw DBLogger.newUser("This session is closed."); } } public boolean isClosed() { return !isOpen; } public static long getObjectId(Object o) { if (o instanceof ZooPC) { DBLogger.newUser("The object is not persistence capable: " + o.getClass()); } ZooPC zpc = (ZooPC) o; return zpc.jdoZooGetOid(); } public static Session getSession(Object o) { if (o instanceof ZooPC) { DBLogger.newUser("The object is not persistence capable: " + o.getClass()); } ZooPC zpc = (ZooPC) o; if (zpc.jdoZooGetContext() == null) { return null; } return zpc.jdoZooGetContext().getSession(); } /** * Get access to schema management. * @return Schema management API */ public ZooSchema schema() { return new ZooSchemaImpl(this, schemaManager); } public long getTransactionId() { return transactionId; } public void lock() { lock.lock(); } public void unlock() { lock.unlock(); } public ClientLock getLock() { return lock; } public boolean getMultithreaded() { return lock.isLockingEnabled(); } public void setMultithreaded(boolean arg0) { lock.enableLocking(arg0); } public long getStats(STATS stat) { Long s = stats.get(stat); if (s == null) { return 0; } return (long) s; } public void statsInc(STATS stat) { Long cnt = stats.get(stat); if (cnt == null) { cnt = 1L; } else { cnt++; } stats.put(stat, cnt); } }