/*
* 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.client.session;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;
import javax.jdo.ObjectState;
import org.zoodb.api.ZooInstanceEvent;
import org.zoodb.api.impl.ZooPC;
import org.zoodb.internal.GenericObject;
import org.zoodb.internal.Node;
import org.zoodb.internal.ObjectGraphTraverser;
import org.zoodb.internal.Session;
import org.zoodb.internal.ZooClassDef;
import org.zoodb.internal.client.AbstractCache;
import org.zoodb.internal.util.CloseableIterator;
import org.zoodb.internal.util.DBLogger;
import org.zoodb.internal.util.PrimLongMap;
import org.zoodb.internal.util.PrimLongMapLI;
import org.zoodb.internal.util.PrimLongMapLISoft;
import org.zoodb.internal.util.PrimLongMapLIWeak;
public class ClientSessionCache implements AbstractCache {
//Do not use list to indicate properties! Instead of 1 bit it, lists require 20-30 bytes per entry!
//TODO Optimize PrimLongTreeMap further? -> HashMaps don't scale!!! (because of the internal array)
// private final PrimLongMapLI<ZooPC> objs =
// new PrimLongMapLI<ZooPC>();
// private final PrimLongMapLISoft<ZooPC> objs =
private final PrimLongMap<ZooPC> objs;
private final PrimLongMapLI<ZooClassDef> schemata =
new PrimLongMapLI<ZooClassDef>();
//TODO move into node-cache
private final HashMap<Node, HashMap<Class<?>, ZooClassDef>> nodeSchemata =
new HashMap<Node, HashMap<Class<?>, ZooClassDef>>();
/**
* Set of dirty objects. This has two advantages.
* 1) It is much faster if only few objects need to be committed while there are many
* clean objects.
* 2) It allows some kind of clustering (EXPERIMENTAL!!!) by sorting objects by OID.
*
* dirtyObject may include deleted objects!
*/
private final ArrayList<ZooPC> dirtyObjects = new ArrayList<ZooPC>();
private final PrimLongMapLI<ZooPC> deletedObjects = new PrimLongMapLI<ZooPC>();
private final ArrayList<GenericObject> dirtyGenObjects = new ArrayList<GenericObject>();
private final PrimLongMapLI<GenericObject> genericObjects = new PrimLongMapLI<GenericObject>();
private final Session session;
private final ObjectGraphTraverser ogt;
private ZooClassDef metaSchema;
public ClientSessionCache(Session session) {
this.session = session;
this.ogt = new ObjectGraphTraverser(this);
switch (session.getConfig().getCacheMode()) {
case WEAK: objs = new PrimLongMapLIWeak<ZooPC>(); break;
case SOFT: objs = new PrimLongMapLISoft<ZooPC>(); break;
case PIN: objs = new PrimLongMapLI<ZooPC>(); break;
default:
throw new UnsupportedOperationException();
}
ZooClassDef zpc = ZooClassDef.bootstrapZooPCImpl();
metaSchema = ZooClassDef.bootstrapZooClassDef();
metaSchema.initProvidedContext(session, null);//session.getPrimaryNode());
schemata.put(zpc.getOid(), zpc);
schemata.put(metaSchema.getOid(), metaSchema);
}
public Session getSession() {
return session;
}
@Override
public void rollback() {
int logSizeObjBefore = objs.size();
//TODO refresh cleans? may have changed in DB?
//Maybe set them all to hollow instead? //TODO
//refresh schemata
//Reloading needs to be in a separate loop. We first need to remove all from the cache
//before reloading them. Reloading may implicitly load dirty super-classes, which would
//fail if they are still in the cache and marked as dirty.
ArrayList<ZooClassDef> schemaToRefresh = new ArrayList<ZooClassDef>();
ArrayList<ZooClassDef> schemaToRemove = new ArrayList<ZooClassDef>();
for (ZooClassDef cs: schemata.values()) {
if (cs.jdoZooIsDirty()) {
if (cs.jdoZooIsNew()) {
schemaToRemove.add(cs);
} else {
//TODO remove? This is never used..., See also Test038/issue 54
schemaToRefresh.add(cs);
}
}
}
for (ZooClassDef cs: schemaToRemove) {
schemata.remove(cs.jdoZooGetOid());
nodeSchemata.get(cs.jdoZooGetNode()).remove(cs.getJavaClass());
}
//TODO Maybe we should simply refresh the whole cache instead of setting them to hollow.
//This doesn't matter for embedded databases, but for client/server, we could benefit from
//group-refreshing(loading) all dirty objects
for (ZooPC co: dirtyObjects) {
if (co.jdoZooIsDirty()) { // i.e. not refreshed
if (co.jdoZooIsNew()) {
//remove co
objs.remove(co.jdoZooGetOid());
co.jdoZooMarkTransient();
} else {
co.jdoZooMarkHollow();
}
}
}
for (ZooPC co: deletedObjects.values()) {
if (co.jdoZooIsDirty()) { // i.e. not refreshed
if (co.jdoZooIsNew()) {
//remove co
objs.remove(co.jdoZooGetOid());
co.jdoZooMarkTransient();
} else {
co.jdoZooMarkHollow();
}
}
}
dirtyObjects.clear();
deletedObjects.clear();
//generic objects
for (GenericObject go: dirtyGenObjects) {
if (go.jdoZooIsNew()) {
//TODO really? Make it transient and remove from list?
// Or rather keep it in list? --> Always persistent?
//go.invalidate(); //prevent further access to it through existing references
genericObjects.remove(go.getOid());
go.jdoZooMarkTransient();
continue;
}
go.jdoZooMarkHollow();
}
for (GenericObject go: genericObjects.values()) {
go.jdoZooMarkHollow();
}
dirtyGenObjects.clear();
if (DBLogger.isLoggable(Level.FINE)) {
int logSizeObjAfter = objs.size();
DBLogger.LOGGER.fine("ClientCache.rollback() - Cache size before/after: " +
logSizeObjBefore + "/" + logSizeObjAfter);
}
}
@Override
public final void markPersistent(ZooPC pc, long oid, Node node, ZooClassDef clsDef) {
if (pc.jdoZooIsDeleted()) {
throw new UnsupportedOperationException("Make it persistent again");
//TODO implement
}
if (pc.jdoZooIsPersistent()) {
//ignore
return;
}
addToCache(pc, clsDef, oid, ObjectState.PERSISTENT_NEW);
}
public final void makeTransient(ZooPC pc) {
//remove it
if (pc.getClass() == GenericObject.class) {
if (genericObjects.remove(pc.jdoZooGetOid()) == null) {
throw DBLogger.newFatalInternal("Object is not in cache.");
}
} else if (objs.remove(pc.jdoZooGetOid()) == null) {
throw DBLogger.newFatalInternal("Object is not in cache.");
}
//update
pc.jdoZooMarkTransient();
}
@Override
public final void addToCache(ZooPC obj, ZooClassDef classDef, long oid,
ObjectState state) {
if (obj.getClass() == GenericObject.class) {
addGeneric((GenericObject) obj);
return;
}
obj.jdoZooInit(state, classDef.getProvidedContext(), oid);
//TODO call newInstance elsewhere
//obj.jdoReplaceStateManager(co);
objs.put(obj.jdoZooGetOid(), obj);
}
@Override
public final ZooPC findCoByOID(long oid) {
return objs.get(oid);
}
/**
* TODO Fix this. Schemata should be kept in a separate cache
* for each node!
* @param cls Class name
* @param node Node object
* @return Schema object for a given Java class.
*/
@Override
public ZooClassDef getSchema(Class<?> cls, Node node) {
ZooClassDef ret = nodeSchemata.get(node).get(cls);
if (ret == null) {
if (cls == null) {
return null;
}
//Try virtual/generic schemata
ret = getSchema(cls.getName());
if (ret != null) {
//check (associate also checks compatibility)
ret.associateJavaTypes(true);
nodeSchemata.get(node).put(cls, ret);
}
}
return ret;
}
@Override
public ZooClassDef getSchema(String clsName) {
for (ZooClassDef def: schemata.values()) {
if (def.getNextVersion() == null && def.getClassName().equals(clsName)) {
return def;
}
}
return null;
}
@Override
public ZooClassDef getSchema(long schemaOid) {
return schemata.get(schemaOid);
}
/**
* Clean out the cache after commit.
* TODO keep hollow objects? E.g. references to correct, e.t.c!
* @param retainValues retainValues flag
* @param detachAllOnCommit detachAllOnCommit flag
*/
public void postCommit(boolean retainValues, boolean detachAllOnCommit) {
int logSizeObjBefore = objs.size();
long t1 = System.nanoTime();
//TODO later: empty cache (?)
if (!deletedObjects.isEmpty()) {
for (ZooPC co: deletedObjects.values()) {
if (co.jdoZooIsDeleted()) {
objs.remove(co.jdoZooGetOid());
co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.POST_DELETE);
}
}
}
if (detachAllOnCommit) {
Iterator<ZooPC> it = objs.values().iterator();
while (it.hasNext()) {
ZooPC co = it.next();
if (co instanceof ZooClassDef) {
co.jdoZooMarkClean();
co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.POST_STORE);
} else {
co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.PRE_DETACH);
if (co.jdoZooIsStateHollow()) {
//TODO remove this, instead fix DETACH to work with hollow objects
co.jdoZooGetNode().refreshObject(co);
}
co.jdoZooMarkDetached();
it.remove();
co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.POST_DETACH);
}
}
} else if (retainValues) {
if (!dirtyObjects.isEmpty()) {
for (ZooPC co: dirtyObjects) {
if (!co.jdoZooIsDeleted()) {
co.jdoZooMarkClean();
}
}
}
} else {
if (objs.size() > 100000) {
DBLogger.debugPrintln(0, "Cache is getting large. Consider retainValues=true"
+ " to speed up and avoid expensive eviction.");
}
for (ZooPC co: objs.values()) {
if (retainValues || co instanceof ZooClassDef) {
co.jdoZooMarkClean();
} else {
co.jdoZooEvict();
}
co.jdoZooGetContext().notifyEvent(co, ZooInstanceEvent.POST_STORE);
}
}
dirtyObjects.clear();
deletedObjects.clear();
//generic objects
if (!dirtyGenObjects.isEmpty()) {
for (GenericObject go: dirtyGenObjects) {
if (go.jdoZooIsDeleted()) {
genericObjects.remove(go.getOid());
continue;
}
go.jdoZooMarkClean();
}
}
if (!genericObjects.isEmpty()) {
for (GenericObject go: genericObjects.values()) {
if (!retainValues) {
go.jdoZooMarkHollow();
}
}
}
dirtyGenObjects.clear();
//schema
Iterator<ZooClassDef> iterS = schemata.values().iterator();
for (; iterS.hasNext(); ) {
ZooClassDef cs = iterS.next();
if (cs.jdoZooIsDeleted()) {
iterS.remove();
nodeSchemata.get(cs.jdoZooGetNode()).remove(cs.getJavaClass());
continue;
}
//keep in cache???
cs.jdoZooMarkClean(); //TODO remove if cache is flushed -> retainValues!!!!!
}
if (DBLogger.isLoggable(Level.FINE)) {
int logSizeObjAfter = objs.size();
long t2 = System.nanoTime();
DBLogger.LOGGER.fine("ClientCache.postCommit() -- Time=" + (t2-t1) +
"ns; Cache size before/after: " + logSizeObjBefore + "/" + logSizeObjAfter);
}
}
/**
* @return List of all cached schema objects (clean, new, deleted, dirty).
*/
public Collection<ZooClassDef> getSchemata() {
return schemata.values();
}
public void addSchema(ZooClassDef clsDef, boolean isLoaded, Node node) {
ObjectState state;
if (isLoaded) {
state = ObjectState.PERSISTENT_CLEAN;
} else {
state = ObjectState.PERSISTENT_NEW;
}
//TODO avoid setting the OID here a second time, seems silly...
clsDef.jdoZooInit(state, metaSchema.getProvidedContext(), clsDef.getOid());
clsDef.initProvidedContext(session, node);
schemata.put(clsDef.getOid(), clsDef);
if (clsDef.getNextVersion() == null && clsDef.getJavaClass() != null) {
nodeSchemata.get(node).put(clsDef.getJavaClass(), clsDef);
}
objs.put(clsDef.getOid(), clsDef);
}
public void updateSchema(ZooClassDef clsDef, Class<?> oldCls, Class<?> newCls) {
Node node = clsDef.jdoZooGetNode();
//Removal may return null if class was previously stored a 'null', which is non-unique.
nodeSchemata.get(node).remove(oldCls);
if (newCls != null) {
nodeSchemata.get(node).put(newCls, clsDef);
}
}
public Collection<ZooPC> getAllObjects() {
return objs.values();
}
public Collection<GenericObject> getAllGenericObjects() {
return genericObjects.values();
}
public void close() {
objs.clear();
schemata.clear();
nodeSchemata.clear();
}
public void evictAll() {
for (ZooPC co: objs.values()) {
if (!co.jdoZooIsDirty()) {
co.jdoZooEvict();
}
}
}
public void evictAll(boolean subClasses, Class<?> cls) {
for (ZooPC co: objs.values()) {
if (!co.jdoZooIsDirty() && (co.jdoZooGetClassDef().getJavaClass() == cls ||
(subClasses && cls.isAssignableFrom(co.jdoZooGetClassDef().getJavaClass())))) {
co.jdoZooEvict();
}
}
}
public void addNode(Node node) {
nodeSchemata.put(node, new HashMap<Class<?>, ZooClassDef>());
nodeSchemata.get(node).put(ZooClassDef.class, metaSchema);
}
public CloseableIterator<ZooPC> iterator(ZooClassDef clsDef, boolean subClasses,
ObjectState state) {
if (state == ObjectState.PERSISTENT_NEW) {
ArrayList<ZooPC> ret = new ArrayList<>();
for (ZooPC pc: dirtyObjects) {
ZooClassDef defCand = pc.jdoZooGetClassDef();
if (defCand == clsDef || (subClasses && defCand.hasSuperClass(clsDef))) {
if (pc.jdoZooHasState(state)) {
ret.add(pc);
}
}
}
return new DummyIterator(ret.iterator());
}
//currently not required
throw new UnsupportedOperationException();
//return new CacheIterator(objs.values().iterator(), def, subClasses, stte);
}
private static class DummyIterator implements CloseableIterator<ZooPC> {
private final Iterator<ZooPC> iter;
private DummyIterator(Iterator<ZooPC> iterator) {
iter = iterator;
}
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public ZooPC next() {
return iter.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
//ignore
}
}
/**
* This sets the meta schema object for this session. It is the instance of
* ZooClassDef that represents its own schema.
* @param def Class definition
*/
public void setRootSchema(ZooClassDef def) {
//TODO this is a bit funny, but we leave it for now.
//Ideally, we would not have to reset the metaClass, but the second/loaded version
//contains additional info such as node/session and is correctly referenced.
//The original version is only used to initially load any schema from the database.
metaSchema = def;
}
public void notifyDirty(ZooPC pc) {
if (pc.getClass() == GenericObject.class) {
dirtyGenObjects.add((GenericObject) pc);
return;
}
dirtyObjects.add(pc);
}
public ArrayList<ZooPC> getDirtyObjects() {
return dirtyObjects;
}
public void notifyDelete(ZooPC pc) {
if (pc.getClass() == GenericObject.class) {
dirtyGenObjects.add((GenericObject) pc);
return;
}
deletedObjects.put(pc.jdoZooGetOid(), pc);
}
public PrimLongMapLI<ZooPC>.PrimLongValues getDeletedObjects() {
return deletedObjects.values();
}
@Override
public void addGeneric(GenericObject genericObject) {
genericObjects.put(genericObject.getOid(), genericObject);
}
public ArrayList<GenericObject> getDirtyGenericObjects() {
return dirtyGenObjects;
}
@Override
public GenericObject getGeneric(long oid) {
return genericObjects.get(oid);
}
public boolean hasDirtyPojos() {
//ignore generic objects for now, they need no traversing
return !dirtyObjects.isEmpty();
}
/**
* Traverse the object graph and call makePersistent() on all reachable
* objects.
*/
public void persistReachableObjects() {
ogt.traverse();
}
/**
* Tell the OGT that the object graph has changed and that a new traversal is required.
*/
public void flagOGTraversalRequired() {
ogt.flagTraversalRequired();
}
}