/*
* JBoss, Home of Professional Open Source
* Copyright 2010 Red Hat Inc. and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.lucene.readlocks;
import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.lucene.ChunkCacheKey;
import org.infinispan.lucene.FileCacheKey;
import org.infinispan.lucene.FileMetadata;
import org.infinispan.lucene.FileReadLockKey;
import org.infinispan.lucene.InfinispanDirectory;
import org.infinispan.lucene.InfinispanIndexInput;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
/**
* <p>DistributedSegmentReadLocker stores reference counters in the cache
* to keep track of the number of clients still needing to be able
* to read a segment. It makes extensive usage of Infinispan's atomic
* operations.</p>
* <p>Locks stored this way are not optimally performing as it might spin
* on remote invocations, and might fail to cleanup some garbage
* in case a node is disconnected without having released the readlock.</p>
*
* @author Sanne Grinovero
* @since 4.1
*/
@SuppressWarnings("unchecked")
public class DistributedSegmentReadLocker implements SegmentReadLocker {
private static final Log log = LogFactory.getLog(DistributedSegmentReadLocker.class);
private final AdvancedCache<Object, Integer> locksCache;
private final AdvancedCache<?, ?> chunksCache;
private final AdvancedCache<?, ?> metadataCache;
private final String indexName;
public DistributedSegmentReadLocker(Cache<Object, Integer> locksCache, Cache<?, ?> chunksCache, Cache<?, ?> metadataCache, String indexName) {
if (locksCache == null)
throw new IllegalArgumentException("locksCache must not be null");
if (chunksCache == null)
throw new IllegalArgumentException("chunksCache must not be null");
if (metadataCache == null)
throw new IllegalArgumentException("metadataCache must not be null");
if (indexName == null)
throw new IllegalArgumentException("index name must not be null");
this.indexName = indexName;
this.locksCache = locksCache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD);
this.chunksCache = chunksCache.getAdvancedCache();
this.metadataCache = metadataCache.getAdvancedCache();
verifyCacheHasNoEviction(this.locksCache);
}
public DistributedSegmentReadLocker(Cache<?, ?> cache, String indexName) {
this((Cache<Object, Integer>) cache, cache, cache, indexName);
}
/**
* Deletes or releases a read-lock for the specified filename, so that if it was marked as deleted and
* no other {@link InfinispanIndexInput} instances are reading from it, then it will
* be effectively deleted.
*
* @see #acquireReadLock(String)
* @see InfinispanDirectory#deleteFile(String)
*/
@Override
public void deleteOrReleaseReadLock(String filename) {
FileReadLockKey readLockKey = new FileReadLockKey(indexName, filename);
int newValue = 0;
boolean done = false;
Object lockValue = locksCache.get(readLockKey);
while (done == false) {
if (lockValue == null) {
lockValue = locksCache.putIfAbsent(readLockKey, 0);
done = (null == lockValue);
}
else {
int refCount = (Integer) lockValue;
newValue = refCount - 1;
done = locksCache.replace(readLockKey, refCount, newValue);
if (!done) {
lockValue = locksCache.get(readLockKey);
}
}
}
if (newValue == 0) {
realFileDelete(readLockKey, locksCache, chunksCache, metadataCache);
}
}
/**
* Acquires a readlock on all chunks for this file, to make sure chunks are not deleted while
* iterating on the group. This is needed to avoid an eager lock on all elements.
*
* If no value is found in the cache, a disambiguation procedure is needed: not value
* might mean both "existing, no readlocks, no deletions in progress", but also "not existent file".
* The first possibility is coded as no value to avoid storing readlocks in a permanent store,
* which would unnecessarily slow down and provide unwanted long term storage of the lock;
* so the value is treated as one if not found, but obviously it's also not found for non-existent
* or concurrently deleted files.
*
* @param filename the name of the "file" for which a readlock is requested
*
* @see #deleteOrReleaseReadLock(String)
*/
@Override
public boolean acquireReadLock(String filename) {
FileReadLockKey readLockKey = new FileReadLockKey(indexName, filename);
Integer lockValue = locksCache.get(readLockKey);
boolean done = false;
while (done == false) {
if (lockValue != null) {
int refCount = (Integer) lockValue;
if (refCount == 0) {
// too late: in case refCount==0 the delete is being performed
return false;
}
Integer newValue = refCount + 1;
done = locksCache.replace(readLockKey, lockValue, newValue);
if ( ! done) {
lockValue = locksCache.get(readLockKey);
}
} else {
// readLocks are not stored, so if there's no value assume it's ==1, which means
// existing file, not deleted, nobody else owning a read lock. but check for ambiguity
lockValue = locksCache.putIfAbsent(readLockKey, 2);
done = (null == lockValue);
if (done) {
// have to check now that the fileKey still exists to prevent the race condition of
// T1 fileKey exists - T2 delete file and remove readlock - T1 putIfAbsent(readlock, 2)
final FileCacheKey fileKey = new FileCacheKey(indexName, filename);
if (metadataCache.get(fileKey) == null) {
locksCache.withFlags(Flag.SKIP_REMOTE_LOOKUP).removeAsync(readLockKey);
return false;
}
}
}
}
return true;
}
/**
* The {@link InfinispanDirectory#deleteFile(String)} is not deleting the elements from the cache
* but instead flagging the file as deletable.
* This method will really remove the elements from the cache; should be invoked only
* by {@link #deleteOrReleaseReadLock(String)} after having verified that there
* are no users left in need to read these chunks.
*
* @param readLockKey the key representing the values to be deleted
* @param locksCache the cache containing the locks
* @param chunksCache the cache containing the chunks to be deleted
* @param metadataCache the cache containing the metadata of elements to be deleted
*/
static void realFileDelete(FileReadLockKey readLockKey, AdvancedCache<Object, Integer> locksCache,
AdvancedCache<?, ?> chunksCache, AdvancedCache<?, ?> metadataCache) {
final boolean trace = log.isTraceEnabled();
final String indexName = readLockKey.getIndexName();
final String filename = readLockKey.getFileName();
FileCacheKey key = new FileCacheKey(indexName, filename);
if (trace) log.tracef("deleting metadata: %s", key);
FileMetadata file = (FileMetadata) metadataCache.remove(key);
if (file != null) { //during optimization of index a same file could be deleted twice, so you could see a null here
for (int i = 0; i < file.getNumberOfChunks(); i++) {
ChunkCacheKey chunkKey = new ChunkCacheKey(indexName, filename, i);
if (trace) log.tracef("deleting chunk: %s", chunkKey);
chunksCache.withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.SKIP_CACHE_LOAD).removeAsync(chunkKey);
}
}
// last operation, as being set as value==0 it prevents others from using it during the
// deletion process:
if (trace) log.tracef("deleting readlock: %s", readLockKey);
locksCache.withFlags(Flag.SKIP_REMOTE_LOOKUP).removeAsync(readLockKey);
}
private static void verifyCacheHasNoEviction(AdvancedCache<?, ?> cache) {
if (cache.getConfiguration().getEvictionStrategy().isEnabled())
throw new IllegalArgumentException("DistributedSegmentReadLocker is not reliable when using a cache with eviction enabled, disable eviction on this cache instance");
}
}