/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.jena.tdb.store.nodetable; import java.util.Iterator ; import org.apache.jena.atlas.iterator.Iter ; import org.apache.jena.atlas.lib.Cache ; import org.apache.jena.atlas.lib.CacheFactory ; import org.apache.jena.atlas.lib.CacheSet ; import org.apache.jena.atlas.lib.Pair ; import org.apache.jena.atlas.logging.Log ; import org.apache.jena.graph.Node ; import org.apache.jena.tdb.TDBException ; import org.apache.jena.tdb.setup.StoreParams ; import org.apache.jena.tdb.store.NodeId ; /** Cache wrapper around a NodeTable. * Assumes all access goes through this wrapper. * Read-cache - write caching is done via the object file used by the base NodeTable. */ public class NodeTableCache implements NodeTable { // These caches are updated together. // See synchronization in _retrieveNodeByNodeId and _idForNode // The cache is assumed to be single operation-thread-safe. private Cache<Node, NodeId> node2id_Cache = null ; private Cache<NodeId, Node> id2node_Cache = null ; // A small cache of "known unknowns" to speed up searching for impossible things. // Cache update needed on NodeTable changes because a node may become "known" private CacheSet<Node> notPresent = null ; private NodeTable baseTable ; private Object lock = new Object() ; public static NodeTable create(NodeTable nodeTable, StoreParams params) { int nodeToIdCacheSize = params.getNode2NodeIdCacheSize() ; int idToNodeCacheSize = params.getNodeId2NodeCacheSize() ; if ( nodeToIdCacheSize <= 0 && idToNodeCacheSize <= 0 ) return nodeTable ; return new NodeTableCache(nodeTable, nodeToIdCacheSize, idToNodeCacheSize, params.getNodeMissCacheSize()) ; } public static NodeTable create(NodeTable nodeTable, int nodeToIdCacheSize, int idToNodeCacheSize, int nodeMissesCacheSize) { if ( nodeToIdCacheSize <= 0 && idToNodeCacheSize <= 0 ) return nodeTable ; return new NodeTableCache(nodeTable, nodeToIdCacheSize, idToNodeCacheSize, nodeMissesCacheSize) ; } private NodeTableCache(NodeTable baseTable, int nodeToIdCacheSize, int idToNodeCacheSize, int nodeMissesCacheSize) { this.baseTable = baseTable ; if ( nodeToIdCacheSize > 0 ) node2id_Cache = CacheFactory.createCache(nodeToIdCacheSize) ; if ( idToNodeCacheSize > 0 ) id2node_Cache = CacheFactory.createCache(idToNodeCacheSize) ; if ( nodeMissesCacheSize > 0 ) notPresent = CacheFactory.createCacheSet(nodeMissesCacheSize) ; } @Override public final NodeTable wrapped() { return baseTable ; } /** Get the Node for this NodeId, or null if none */ @Override public Node getNodeForNodeId(NodeId id) { return _retrieveNodeByNodeId(id) ; } /** Find the NodeId for a node, or return NodeId.NodeDoesNotExist */ @Override public NodeId getNodeIdForNode(Node node) { return _idForNode(node, false) ; } /** Find the NodeId for a node, allocating a new NodeId if the Node does not yet have a NodeId */ @Override public NodeId getAllocateNodeId(Node node) { return _idForNode(node, true) ; } @Override public boolean containsNode(Node node) { NodeId x = getNodeIdForNode(node) ; return NodeId.isDoesNotExist(x) ; } @Override public boolean containsNodeId(NodeId nodeId) { Node x = getNodeForNodeId(nodeId) ; return x == null ; } // ---- The worker functions // NodeId ==> Node private Node _retrieveNodeByNodeId(NodeId id) { if ( NodeId.isDoesNotExist(id) ) return null ; if ( NodeId.isAny(id) ) return null ; // Try once outside the synchronized // (Cache access is thread-safe) Node n = cacheLookup(id) ; if ( n != null ) return n ; synchronized (lock) { // Lock to update two caches consisently. // Verify cache miss n = cacheLookup(id) ; if ( n != null ) return n ; if ( baseTable == null ) System.err.println(""+this) ; n = baseTable.getNodeForNodeId(id) ; cacheUpdate(n, id) ; return n ; } } // Node ==> NodeId private NodeId _idForNode(Node node, boolean allocate) { if ( node == Node.ANY ) return NodeId.NodeIdAny ; // Try once outside the synchronized // (Cache access is thread-safe.) NodeId nodeId = cacheLookup(node) ; if ( nodeId != null ) return nodeId ; synchronized (lock) { // Update two caches inside synchronized. // Check stil valid. nodeId = cacheLookup(node) ; if ( nodeId != null ) return nodeId ; if ( allocate ) nodeId = baseTable.getAllocateNodeId(node) ; else nodeId = baseTable.getNodeIdForNode(node) ; // Ensure caches have it. Includes recording "no such node" cacheUpdate(node, nodeId) ; return nodeId ; } } // ---------------- // ---- Only places that the caches are touched /** Check caches to see if we can map a NodeId to a Node. Returns null on no cache entry. */ private Node cacheLookup(NodeId id) { if ( id2node_Cache == null ) return null ; return id2node_Cache.getIfPresent(id) ; } /** Check caches to see if we can map a Node to a NodeId. Returns null on no cache entry. */ private NodeId cacheLookup(Node node) { // Remember things known (currently) not to exist. // Does not matter if notPresent is being updated elsewhere. if ( notPresent != null && notPresent.contains(node) ) return null ; if ( node2id_Cache == null ) return null ; return node2id_Cache.getIfPresent(node) ; } /** Update the Node->NodeId caches */ private void cacheUpdate(Node node, NodeId id) { if ( node == null ) return ; // synchronized is further out. // The "notPresent" cache is used to note whether a node // is known not to exist. // This must be specially handled later if the node is added. if ( NodeId.isDoesNotExist(id) ) { if ( notPresent != null ) notPresent.add(node) ; return ; } if ( id == NodeId.NodeIdAny ) { Log.warn(this, "Attempt to cache NodeIdAny - ignored") ; return ; } if ( node2id_Cache != null ) node2id_Cache.put(node, id) ; if ( id2node_Cache != null ) id2node_Cache.put(id, node) ; // Remove if previously marked "not present" if ( notPresent != null && notPresent.contains(node) ) notPresent.remove(node) ; } // ---- @Override public NodeId allocOffset() { return baseTable.allocOffset() ; } @Override public boolean isEmpty() { synchronized (lock) { if ( node2id_Cache != null ) return node2id_Cache.isEmpty() ; if ( id2node_Cache != null ) id2node_Cache.isEmpty() ; // Write through. return baseTable.isEmpty() ; } } @Override public synchronized void close() { if ( baseTable == null ) // Already closed (NodeTables can be shared so .close via two routes). return ; baseTable.close() ; node2id_Cache = null ; id2node_Cache = null ; notPresent = null ; baseTable = null ; } @Override public void sync() { baseTable.sync() ; } @Override public Iterator<Pair<NodeId, Node>> all() { if ( false ) testForConsistency() ; return baseTable.all() ; } private void testForConsistency() { Iterator<Node> iter1 = Iter.toList(node2id_Cache.keys()).iterator() ; for ( ; iter1.hasNext() ; ) { Node n = iter1.next() ; NodeId nId = node2id_Cache.getIfPresent(n) ; if ( !id2node_Cache.containsKey(nId) ) throw new TDBException("Inconsistent: "+n+" => "+nId) ; if ( notPresent.contains(n) ) throw new TDBException("Inconsistent: "+n+" in notPresent cache (1)") ; } Iterator<NodeId> iter2 = Iter.toList(id2node_Cache.keys()).iterator() ; for ( ; iter2.hasNext() ; ) { NodeId nId = iter2.next() ; Node n = id2node_Cache.getIfPresent(nId) ; if ( !node2id_Cache.containsKey(n) ) throw new TDBException("Inconsistent: "+nId+" => "+n) ; if ( notPresent.contains(n) ) throw new TDBException("Inconsistent: "+n+" in notPresent cache (2)") ; } } @Override public String toString() { return "Cache("+baseTable.toString()+")" ; } }