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

io.permazen.JObjectCache Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package io.permazen;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;

import io.permazen.core.ObjId;
import io.permazen.core.util.ObjIdMap;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

@ThreadSafe
class JObjectCache {

    private final JTransaction jtx;
    private final ReferenceQueue referenceQueue = new ReferenceQueue<>();

    /**
     * Mapping from object ID to {@link JObject}.
     *
     * 

* As a special case, null values in this map indicate that the corresponding {@link JObject} * is currently under construction by some thread. */ @GuardedBy("itself") private final ObjIdMap cache = new ObjIdMap<>(); /** * Mapping of {@link JObject}s currently under construction by the current thread. * *

* These {@link JObject}s are kept private to each thread until the Java constructor successfully returns. */ private final ThreadLocal> instantiations = new ThreadLocal<>(); JObjectCache(JTransaction jtx) { this.jtx = jtx; assert this.jtx != null; } /** * Get the Java model object corresponding to the given object ID if it exists. * * @param id object ID * @return Java model object, or null if object not created yet * @throws IllegalArgumentException if {@code id} is null */ public JObject getIfExists(ObjId id) { // Sanity check Preconditions.checkArgument(id != null, "null id"); // Check for existing entry synchronized (this.cache) { final JObjRef ref = this.cache.get(id); if (ref != null) return ref.get(); } // Does not exist - or is currently being instantiated return null; } /** * Get the Java model object corresponding to the given object ID, creating it if necessary. * * @param id object ID * @return Java model object * @throws IllegalArgumentException if {@code id} is null */ public JObject get(ObjId id) { // Sanity check Preconditions.checkArgument(id != null, "null id"); // Check for existing entry boolean interrupted = false; synchronized (this.cache) { // Check for existing JObject, or null if object is being instantiated while (true) { // Garbage collect this.gc(); // Get weak reference final JObjRef ref = this.cache.get(id); if (ref != null) { // If weak reference still valid, return corresponding JObject final JObject jobj = ref.get(); if (jobj != null) return jobj; // The weak reference has been cleared; we will construct a new JObject replacement // this.cache.remove(id); // not necessary; see below } else if (this.cache.containsKey(id)) { // null value indicates object is being instantiated by some thread // Is the current thread the one instantiating the object? final ObjIdMap threadInstantiations = this.instantiations.get(); if (threadInstantiations != null && threadInstantiations.containsKey(id)) { final JObject jobj = threadInstantiations.get(id); if (jobj != null) return jobj; throw new RuntimeException("illegal reentrant query for object " + id + " during object construction"); } // Some other thread is instantiating the object, so wait for it to finish doing so try { this.cache.wait(); } catch (InterruptedException e) { interrupted = true; } continue; } // Set a null value in the cache to indicate that some thread (i.e., this one) is instantiating the object this.cache.put(id, null); break; } } // Instantiate new JObject instance JObject jobj = null; try { jobj = this.createJObject(id); } finally { synchronized (this.cache) { assert this.cache.containsKey(id) && this.cache.get(id) == null; this.gc(); // Add JObject to the cache, or else remove the 'under construction' flag if (jobj != null) this.cache.put(id, new JObjRef(jobj, this.referenceQueue)); else this.cache.remove(id); // Wakeup any waiting threads this.cache.notifyAll(); } } // Re-interrupt the current thread if needed if (interrupted) Thread.currentThread().interrupt(); // Done return jobj; } /** * Register the given {@link JObject} with this instance. * * Use of this method is required to handle the case where the Java model class constructor invokes, before the constructor * has even returned, a {@link JTransaction} method that needs to find the {@link JObject} being constructed by its object ID. * In that case, we don't have the {@link JObject} in our cache yet. This methods puts it in there if necessary. */ void register(JObject jobj) { // Sanity check Preconditions.checkArgument(jobj != null, "null jobj"); Preconditions.checkArgument(jobj.getTransaction() == this.jtx, "wrong tx"); // Get object ID final ObjId id = jobj.getObjId(); // Is the JObject under construction by the current thread? If not, don't do anything. final ObjIdMap threadInstantiations = this.instantiations.get(); if (threadInstantiations == null || !threadInstantiations.containsKey(id)) return; // Add JObject to this thread's 'under construction' map; also check for weird conflict final JObject previous = threadInstantiations.put(id, jobj); if (previous != null && previous != jobj) { threadInstantiations.put(id, previous); throw new IllegalArgumentException("conflicting JObject registration: " + jobj + " != " + previous); } } /** * Create the {@lin JObject} for the given object ID. * *

* Put an entry in this thread's instantiation map while doing so. */ private JObject createJObject(ObjId id) { // Get ClassGenerator final JClass jclass = this.jtx.jdb.jclasses.get(id.getStorageId()); final ClassGenerator classGenerator = jclass != null ? jclass.getClassGenerator() : this.jtx.jdb.getUntypedClassGenerator(); // Set flag indicating that the object is being instantiated by this thread ObjIdMap threadInstantiations = this.instantiations.get(); if (threadInstantiations == null) { threadInstantiations = new ObjIdMap<>(1); this.instantiations.set(threadInstantiations); } threadInstantiations.put(id, null); // Instantiate the JObject final JObject jobj; final JObject registered; try { jobj = (JObject)classGenerator.getConstructor().newInstance(this.jtx, id); } catch (Exception e) { Throwable cause = e; if (cause instanceof InvocationTargetException) cause = ((InvocationTargetException)cause).getTargetException(); Throwables.throwIfUnchecked(cause); throw new PermazenException("can't instantiate object for ID " + id, cause); } finally { // Get object registered in the meantime, if any registered = threadInstantiations.remove(id); // Discard thread local if no longer needed if (threadInstantiations.isEmpty()) this.instantiations.remove(); } // Sanity check we didn't register the wrong object if (registered != null && registered != jobj) throw new IllegalArgumentException("conflicting JObject registration: " + jobj + " != " + registered); // Done assert jobj != null; assert jobj.getObjId().equals(id); return jobj; } private void gc() { assert Thread.holdsLock(this.cache); while (true) { final JObjRef ref = (JObjRef)this.referenceQueue.poll(); if (ref == null) break; assert ref.get() == null; final ObjId id = ref.getObjId(); if (this.cache.get(id) == ref) // avoid race where old reference is cleared after being replaced in the cache this.cache.remove(id); } } // JObjRef private static class JObjRef extends WeakReference { private final long id; // try to be memory efficient, avoiding extra objects JObjRef(JObject jobj, ReferenceQueue queue) { super(jobj, queue); this.id = jobj.getObjId().asLong(); } public ObjId getObjId() { return new ObjId(this.id); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy