/*
* Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved.
*
* The Sun Project JXTA(TM) Software License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by Sun Microsystems, Inc. for JXTA(TM) technology."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
* not be used to endorse or promote products derived from this software
* without prior written permission. For written permission, please contact
* Project JXTA at http://www.jxta.org.
*
* 5. Products derived from this software may not be called "JXTA", nor may
* "JXTA" appear in their name, without prior written permission of Sun.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN
* MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* JXTA is a registered trademark of Sun Microsystems, Inc. in the United
* States and other countries.
*
* Please see the license information page at :
* <http://www.jxta.org/project/www/license.html> for instructions on use of
* the license in source files.
*
* ====================================================================
*
* This software consists of voluntary contributions made by many individuals
* on behalf of Project JXTA. For more information on Project JXTA, please see
* http://www.jxta.org.
*
* This license is based on the BSD license adopted by the Apache Foundation.
*/
package net.jxta.impl.cm;
import net.jxta.impl.cm.Srdi.Entry;
import net.jxta.impl.cm.srdi.inmemory.GcIndex;
import net.jxta.impl.cm.srdi.inmemory.GcKey;
import net.jxta.impl.cm.srdi.inmemory.PeerIdIndex;
import net.jxta.impl.cm.srdi.inmemory.PeerIdKey;
import net.jxta.impl.cm.srdi.inmemory.SearchIndex;
import net.jxta.impl.cm.srdi.inmemory.SearchKey;
import net.jxta.impl.util.TimeUtils;
import net.jxta.logging.Logger;
import net.jxta.logging.Logging;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An implementation of the Srdi index that is stored in Memory
*
* Removing items from the Index is made using a Garbage Collection process.
*
* Two additional indexes are maintained along the search index to speed up searches based on {@link PeerID}s
* and the Garbage Collection run. Those indexes are {@link Map} based.
*
* Due to its peculiar design, the calculation of the {@link PeerID} hashcode is extremely expensive;
* This is why this Implementation does not use the {@link Entry} nor the {@link PeerID} objects directly
* See comments in {@link PeerIdKey}
*
* @author Bruno Grieder (bruno.grieder@amalto.com) & Simon Temple (simon.temple@amalto.com)
*
*/
public class InMemorySrdi implements SrdiAPI {
private final static transient Logger LOG = Logging.getLogger( InMemorySrdi.class.getName( ) );
// Store of back end objects in use so we can support the static clear functionality
private static Hashtable<PeerGroup, List<SrdiAPI>> backends = new Hashtable<PeerGroup, List<SrdiAPI>>( );
// Three in-memory indexes used to store, search and garbage collect the SRDI
private GcIndex gcIndex = null;
private PeerIdIndex peerIdIndex = null;
private SearchIndex searchIndex = null;
// Used as a lock to ensure all operations on the above three indexes are thread safe
private final Object indexLock = new Object( );
// Stopped indicator
private volatile boolean stopped = false;
// Usage name for this index
private final String indexName;
public InMemorySrdi( PeerGroup group, String indexName ) {
// The index name is only used for logging
this.indexName = ( ( group == null ) ? "none" : ( ( group.getPeerGroupName( ) == null ) ? "NPG" : group.getPeerGroupName( ) ) ) +
":" + indexName;
List<SrdiAPI> idxs = null;
synchronized ( backends ) {
if ( null != group ) {
idxs = backends.get( group );
}
if ( null == idxs ) {
idxs = new ArrayList<SrdiAPI>( 1 );
if ( null != group ) {
backends.put( group, idxs );
}
}
idxs.add( this );
this.gcIndex = new GcIndex( this.indexName );
this.peerIdIndex = new PeerIdIndex( this.indexName );
this.searchIndex = new SearchIndex( this.indexName );
}
if ( Logging.SHOW_INFO && LOG.isInfoEnabled() ) {
LOG.info( "[" + ( ( group == null ) ? "none" : group.toString( ) ) + "] : Initialized " + indexName );
}
}
public static void clearSrdi( PeerGroup group ) {
if ( Logging.SHOW_INFO && LOG.isInfoEnabled() ) {
LOG.info( "Clearing SRDIs for " + group );
}
SrdiAPI[] indexes = null;
synchronized ( backends ) {
List<SrdiAPI> idxs = backends.get( group );
if ( idxs != null ) {
indexes = idxs.toArray( new SrdiAPI[ idxs.size( ) ] );
}
}
if ( null != indexes ) {
for ( SrdiAPI idx : indexes ) {
try {
idx.clear( );
} catch ( IOException e ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled( ) ) {
LOG.error( "Failed clearing index for group: " + group.getPeerGroupName( ), e );
}
}
}
synchronized ( backends ) {
if ( null == backends.remove( group ) ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled( ) ) {
LOG.error( "Failed removing index instance: " + group );
}
}
}
}
}
private void stoppedCheck( ) throws IllegalStateException {
if ( stopped ) {
throw new IllegalStateException( this.getClass( ).getName( ) + " has been stopped!" );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#clear()
*/
public void clear( ) throws IOException {
if ( !stopped ) {
if ( Logging.SHOW_WARNING && LOG.isWarnEnabled( ) ) {
LOG.warn( "Clearing an index that has not been stopped!" );
}
}
try {
synchronized ( indexLock ) {
this.gcIndex.clear( );
this.peerIdIndex.clear( );
this.searchIndex.clear( );
}
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled( ) ) {
LOG.error( "[" + this.indexName + "] Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#garbageCollect()
*/
public void garbageCollect( ) throws IOException {
if ( this.stopped ) {
// Index is stopped... nothing to do
return;
}
try {
Long now = Long.valueOf( TimeUtils.timeNow( ) );
//A small logging counter
long counter = 0;
Long[] expirations = this.gcIndex.getAllKeys( );
// Now loop over extracted items in a non synchronized (e.g. non blocking) loop
for ( int i = 0; i < expirations.length; i++ ) {
Long expiration = expirations [ i ];
if ( expiration.compareTo( now ) > 0 ) {
// This entry is not yet expired, we are done, exit the GC
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + this.indexName + "] GC: cleared " + counter + " item(s) from the index" );
}
printStatus( );
return;
}
// This entry is expired, process it...
synchronized ( this.indexLock ) {
// Remove and recover the items
Set<GcKey> items = gcIndex.remove( expiration );
// See if we have any item to process
if ( items != null ) {
for ( GcKey gcKey : items ) {
// Recover the search key
SearchKey searchKey = gcKey.getSearchKey( );
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + this.indexName + "] GC: using tree key " + searchKey );
}
// Clean-up the search index
this.searchIndex.remove( searchKey, gcKey.getPeerIdKey( ) );
// Clean-up the peers Index
this.peerIdIndex.remove( gcKey.getPeerIdKey( ), searchKey );
}
// Increment the logging counter
counter++;
} else {
// This *is* possible if the index item was removed via a call to add(), below, after we called getAllKeys() but before we
// iterated down to the expiration key to process the entry. Just log it and carry on...
// LOGGING: was FINER
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + this.indexName + "] GC: Removing GC Index using: " + expiration +
" returned a null set. Assuming it's already been removed via a concurrent add()." );
}
}
}
}
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + this.indexName + "] GC: cleared ALL ( e.g." + counter + " ) item(s) from the index" );
}
printStatus( );
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled( ) ) {
LOG.error( "[" + this.indexName + "] GC: Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
private void printStatus( ) {
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
StringBuffer sb = new StringBuffer( );
sb.append( "\n" );
sb.append(
"------------------------------------------------------------------------------------------------------------------------\n" );
sb.append( " In Memory SRDI Status: " );
sb.append( "[" );
sb.append( this.indexName );
sb.append( "]\n" );
sb.append(
"------------------------------------------------------------------------------------------------------------------------\n" );
synchronized ( indexLock ) {
sb.append( this.gcIndex.getStats( ) );
sb.append( "\n\n" );
sb.append( this.peerIdIndex.getStats( ) );
sb.append( "\n" );
sb.append( this.searchIndex.getStats( ) );
sb.append( "\n" );
}
sb.append(
"------------------------------------------------------------------------------------------------------------------------\n" );
LOG.debug( sb.toString( ) );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#getRecord(java.lang.String, java.lang.String, java.lang.String)
*/
public List<Entry> getRecord( String primaryKey, String attribute, String value )
throws IOException {
stoppedCheck( );
try {
return this.searchIndex.getValueList( new SearchKey( primaryKey, attribute, value ) );
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled() ) {
LOG.error( "[" + this.indexName + "] Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#query(java.lang.String, java.lang.String, java.lang.String, int)
*/
public List<PeerID> query( final String primaryKey, final String attribute, final String value, final int threshold )
throws IOException {
stoppedCheck( );
if ( null == primaryKey ) {
throw new IOException( "[" + indexName + "] Null primary key is not supported in query." );
}
try {
// The key we want to extract from the Ternary Tree
SearchKey searchKey = new SearchKey( primaryKey, attribute, value );
//////////////////////////////////////////////////
//
// No Attribute
// From the JXTA docs: if [attribute] is null,
// the search will return all peer IDs who have records
// under the primary key that have not expired
// -->Match Prefix on the shortened key
//
//////////////////////////////////////////////////
if ( null == attribute ) {
return this.searchIndex.search( searchKey, threshold, true );
}
//////////////////////////////////////////////////
//
// Standard Search
//
//////////////////////////////////////////////////
return this.searchIndex.search( searchKey, threshold, false );
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled() ) {
LOG.error( "[" + this.indexName + "] Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#remove(net.jxta.peer.PeerID)
*/
public void remove( PeerID pid ) throws IOException {
stoppedCheck( );
try {
// Peer Ids are unique identified by their Unique Value
PeerIdKey peerIdKey = new PeerIdKey( pid );
// A simple logging counter
long counter = 0;
// Find the entry in the Peer Index
Map<SearchKey, Long> entries = this.peerIdIndex.get( peerIdKey );
if ( entries == null ) { // Nothing to do...
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + indexName + "] Did not Remove: Peer ID " + peerIdKey + ": is not in the index" );
}
return;
}
synchronized ( indexLock ) {
for ( java.util.Map.Entry<SearchKey, Long> entry : entries.entrySet( ) ) {
SearchKey searchKey = entry.getKey( );
Long expiration = entry.getValue( );
// Removal is performed by expiration of the Entry e.g; moving the GC Item to an expiration of -1
// Prevent any modification while me manipulated the expiration entry of the item
GcKey gcKey = new GcKey( searchKey, peerIdKey );
// Re-add to the GC index at an expired position
this.gcIndex.add( -1L, gcKey );
// Update the search index
this.searchIndex.update( searchKey, peerIdKey, -1L );
// Remove old gc entry
this.gcIndex.remove( expiration, gcKey );
// Increment logging counter
counter++;
}
}
// LOGGING: was FINEST
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + indexName + "] Removing Peer ID '" + peerIdKey + "' led to the expiration of '" + counter +
"' item(s) in the index" );
}
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled() ) {
LOG.error( "[" + this.indexName + "] Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#stop()
*/
public void stop( ) {
this.stopped = true;
}
/* (non-Javadoc)
* @see net.jxta.impl.cm.SrdiAPI#add(java.lang.String, java.lang.String, java.lang.String, net.jxta.peer.PeerID, long)
*/
public void add( String primaryKey, String attribute, String value, PeerID pid, long expiry )
throws IOException {
stoppedCheck( );
try {
long expiration = TimeUtils.toAbsoluteTimeMillis( expiry );
SearchKey searchKey = new SearchKey( primaryKey, attribute, value );
PeerIdKey peerIdKey = new PeerIdKey( pid );
GcKey gcKey = new GcKey( searchKey, peerIdKey );
// LOGGING: was FINEST
if ( Logging.SHOW_DEBUG && LOG.isDebugEnabled() ) {
LOG.debug( "[" + indexName + "] Adding / Updating " + searchKey + " for " + peerIdKey + " expires in: " +
( expiration - TimeUtils.timeNow( ) ) + "ms (at: " + expiration + ")" );
}
synchronized ( indexLock ) {
// Add it (back) at the proper location
this.gcIndex.add( expiration, gcKey );
// Add/replace it in the peers ID Index
this.peerIdIndex.update( peerIdKey, searchKey, expiration );
// Finally, add/replace it in the search index with FULL key
// Create a default map in case this node does not exist
Long previousExpiration = searchIndex.update( searchKey, peerIdKey, expiration );
// Remove the original entry from the GC index if it existed (as long as it's not the same expiration)
if ( ( previousExpiration != null ) && ( previousExpiration != expiration ) ) {
this.gcIndex.remove( previousExpiration, gcKey );
}
}
} catch ( Throwable th ) {
if ( Logging.SHOW_ERROR && LOG.isErrorEnabled() ) {
LOG.error( "[" + this.indexName + "] Unexpected exception encountered!", th );
}
throw new IOException( th );
}
}
}