package org.apache.lucene.store;
/**
* 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.
*/
import java.io.Closeable;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Random;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.ArrayList;
/**
* This is a Directory Wrapper that adds methods
* intended to be used only by unit tests.
*/
public class MockDirectoryWrapper extends Directory {
final Directory delegate;
long maxSize;
// Max actual bytes used. This is set by MockRAMOutputStream:
long maxUsedSize;
double randomIOExceptionRate;
Random randomState;
boolean noDeleteOpenFile = true;
boolean preventDoubleWrite = true;
boolean trackDiskUsage = false;
private Set<String> unSyncedFiles;
private Set<String> createdFiles;
volatile boolean crashed;
// use this for tracking files for crash.
// additionally: provides debugging information in case you leave one open
Map<Closeable,Exception> files
= Collections.synchronizedMap(new IdentityHashMap<Closeable,Exception>());
// NOTE: we cannot initialize the Map here due to the
// order in which our constructor actually does this
// member initialization vs when it calls super. It seems
// like super is called, then our members are initialized:
Map<String,Integer> openFiles;
private synchronized void init() {
if (openFiles == null)
openFiles = new HashMap<String,Integer>();
if (createdFiles == null)
createdFiles = new HashSet<String>();
if (unSyncedFiles == null)
unSyncedFiles = new HashSet<String>();
}
public MockDirectoryWrapper(Directory delegate) {
this.delegate = delegate;
init();
}
public void setTrackDiskUsage(boolean v) {
trackDiskUsage = v;
}
/** If set to true, we throw an IOException if the same
* file is opened by createOutput, ever. */
public void setPreventDoubleWrite(boolean value) {
preventDoubleWrite = value;
}
@Deprecated
@Override
public void sync(String name) throws IOException {
maybeThrowDeterministicException();
if (crashed)
throw new IOException("cannot sync after crash");
unSyncedFiles.remove(name);
delegate.sync(name);
}
@Override
public synchronized void sync(Collection<String> names) throws IOException {
for (String name : names)
maybeThrowDeterministicException();
if (crashed)
throw new IOException("cannot sync after crash");
unSyncedFiles.removeAll(names);
delegate.sync(names);
}
@Override
public String toString() {
return "MockDirWrapper(" + delegate + ")";
}
public synchronized final long sizeInBytes() throws IOException {
if (delegate instanceof RAMDirectory)
return ((RAMDirectory) delegate).sizeInBytes();
else {
// hack
long size = 0;
for (String file : delegate.listAll())
size += delegate.fileLength(file);
return size;
}
}
/** Simulates a crash of OS or machine by overwriting
* unsynced files. */
public synchronized void crash() throws IOException {
crashed = true;
openFiles = new HashMap<String,Integer>();
Iterator<String> it = unSyncedFiles.iterator();
unSyncedFiles = new HashSet<String>();
// first force-close all files, so we can corrupt on windows etc.
// clone the file map, as these guys want to remove themselves on close.
Map<Closeable,Exception> m = new IdentityHashMap<Closeable,Exception>(files);
for (Closeable f : m.keySet())
try {
f.close();
} catch (Exception ignored) {}
int count = 0;
while(it.hasNext()) {
String name = it.next();
if (count % 3 == 0) {
deleteFile(name, true);
} else if (count % 3 == 1) {
// Zero out file entirely
long length = fileLength(name);
byte[] zeroes = new byte[256];
long upto = 0;
IndexOutput out = delegate.createOutput(name);
while(upto < length) {
final int limit = (int) Math.min(length-upto, zeroes.length);
out.writeBytes(zeroes, 0, limit);
upto += limit;
}
out.close();
} else if (count % 3 == 2) {
// Truncate the file:
IndexOutput out = delegate.createOutput(name);
out.setLength(fileLength(name)/2);
out.close();
}
count++;
}
}
public synchronized void clearCrash() throws IOException {
crashed = false;
}
public void setMaxSizeInBytes(long maxSize) {
this.maxSize = maxSize;
}
public long getMaxSizeInBytes() {
return this.maxSize;
}
/**
* Returns the peek actual storage used (bytes) in this
* directory.
*/
public long getMaxUsedSizeInBytes() {
return this.maxUsedSize;
}
public void resetMaxUsedSizeInBytes() throws IOException {
this.maxUsedSize = getRecomputedActualSizeInBytes();
}
/**
* Emulate windows whereby deleting an open file is not
* allowed (raise IOException).
*/
public void setNoDeleteOpenFile(boolean value) {
this.noDeleteOpenFile = value;
}
public boolean getNoDeleteOpenFile() {
return noDeleteOpenFile;
}
/**
* If 0.0, no exceptions will be thrown. Else this should
* be a double 0.0 - 1.0. We will randomly throw an
* IOException on the first write to an OutputStream based
* on this probability.
*/
public void setRandomIOExceptionRate(double rate, long seed) {
randomIOExceptionRate = rate;
// seed so we have deterministic behaviour:
randomState = new Random(seed);
}
public double getRandomIOExceptionRate() {
return randomIOExceptionRate;
}
void maybeThrowIOException() throws IOException {
if (randomIOExceptionRate > 0.0) {
int number = Math.abs(randomState.nextInt() % 1000);
if (number < randomIOExceptionRate*1000) {
throw new IOException("a random IOException");
}
}
}
@Override
public synchronized void deleteFile(String name) throws IOException {
deleteFile(name, false);
}
private synchronized void deleteFile(String name, boolean forced) throws IOException {
maybeThrowDeterministicException();
if (crashed && !forced)
throw new IOException("cannot delete after crash");
if (unSyncedFiles.contains(name))
unSyncedFiles.remove(name);
if (!forced) {
if (noDeleteOpenFile && openFiles.containsKey(name)) {
throw new IOException("MockDirectoryWrapper: file \"" + name + "\" is still open: cannot delete");
}
}
delegate.deleteFile(name);
}
@Override
public synchronized IndexOutput createOutput(String name) throws IOException {
if (crashed)
throw new IOException("cannot createOutput after crash");
init();
synchronized(this) {
if (preventDoubleWrite && createdFiles.contains(name) && !name.equals("segments.gen"))
throw new IOException("file \"" + name + "\" was already written to");
}
if (noDeleteOpenFile && openFiles.containsKey(name))
throw new IOException("MockDirectoryWrapper: file \"" + name + "\" is still open: cannot overwrite");
if (crashed)
throw new IOException("cannot createOutput after crash");
unSyncedFiles.add(name);
createdFiles.add(name);
if (delegate instanceof RAMDirectory) {
RAMDirectory ramdir = (RAMDirectory) delegate;
RAMFile file = new RAMFile(ramdir);
RAMFile existing = ramdir.fileMap.get(name);
// Enforce write once:
if (existing!=null && !name.equals("segments.gen") && preventDoubleWrite)
throw new IOException("file " + name + " already exists");
else {
if (existing!=null) {
ramdir.sizeInBytes.getAndAdd(-existing.sizeInBytes);
existing.directory = null;
}
ramdir.fileMap.put(name, file);
}
}
IndexOutput io = new MockIndexOutputWrapper(this, delegate.createOutput(name), name);
files.put(io, new RuntimeException("unclosed IndexOutput"));
return io;
}
@Override
public synchronized IndexInput openInput(String name) throws IOException {
if (!delegate.fileExists(name))
throw new FileNotFoundException(name);
else {
if (openFiles.containsKey(name)) {
Integer v = openFiles.get(name);
v = Integer.valueOf(v.intValue()+1);
openFiles.put(name, v);
} else {
openFiles.put(name, Integer.valueOf(1));
}
}
IndexInput ii = new MockIndexInputWrapper(this, name, delegate.openInput(name));
files.put(ii, new RuntimeException("unclosed IndexInput"));
return ii;
}
/** Provided for testing purposes. Use sizeInBytes() instead. */
public synchronized final long getRecomputedSizeInBytes() throws IOException {
if (!(delegate instanceof RAMDirectory))
return sizeInBytes();
long size = 0;
for(final RAMFile file: ((RAMDirectory)delegate).fileMap.values()) {
size += file.getSizeInBytes();
}
return size;
}
/** Like getRecomputedSizeInBytes(), but, uses actual file
* lengths rather than buffer allocations (which are
* quantized up to nearest
* RAMOutputStream.BUFFER_SIZE (now 1024) bytes.
*/
public final synchronized long getRecomputedActualSizeInBytes() throws IOException {
if (!(delegate instanceof RAMDirectory))
return sizeInBytes();
long size = 0;
for (final RAMFile file : ((RAMDirectory)delegate).fileMap.values())
size += file.length;
return size;
}
@Override
public synchronized void close() throws IOException {
if (openFiles == null) {
openFiles = new HashMap<String,Integer>();
}
if (noDeleteOpenFile && openFiles.size() > 0) {
// print the first one as its very verbose otherwise
Exception cause = null;
Iterator<Exception> stacktraces = files.values().iterator();
if (stacktraces.hasNext())
cause = stacktraces.next();
// RuntimeException instead of IOException because
// super() does not throw IOException currently:
throw new RuntimeException("MockDirectoryWrapper: cannot close: there are still open files: " + openFiles, cause);
}
open = false;
delegate.close();
}
boolean open = true;
public synchronized boolean isOpen() {
return open;
}
/**
* Objects that represent fail-able conditions. Objects of a derived
* class are created and registered with the mock directory. After
* register, each object will be invoked once for each first write
* of a file, giving the object a chance to throw an IOException.
*/
public static class Failure {
/**
* eval is called on the first write of every new file.
*/
public void eval(MockDirectoryWrapper dir) throws IOException { }
/**
* reset should set the state of the failure to its default
* (freshly constructed) state. Reset is convenient for tests
* that want to create one failure object and then reuse it in
* multiple cases. This, combined with the fact that Failure
* subclasses are often anonymous classes makes reset difficult to
* do otherwise.
*
* A typical example of use is
* Failure failure = new Failure() { ... };
* ...
* mock.failOn(failure.reset())
*/
public Failure reset() { return this; }
protected boolean doFail;
public void setDoFail() {
doFail = true;
}
public void clearDoFail() {
doFail = false;
}
}
ArrayList<Failure> failures;
/**
* add a Failure object to the list of objects to be evaluated
* at every potential failure point
*/
synchronized public void failOn(Failure fail) {
if (failures == null) {
failures = new ArrayList<Failure>();
}
failures.add(fail);
}
/**
* Iterate through the failures list, giving each object a
* chance to throw an IOE
*/
synchronized void maybeThrowDeterministicException() throws IOException {
if (failures != null) {
for(int i = 0; i < failures.size(); i++) {
failures.get(i).eval(this);
}
}
}
@Override
public synchronized String[] listAll() throws IOException {
return delegate.listAll();
}
@Override
public synchronized boolean fileExists(String name) throws IOException {
return delegate.fileExists(name);
}
@Override
public synchronized long fileModified(String name) throws IOException {
return delegate.fileModified(name);
}
@Override
public synchronized void touchFile(String name) throws IOException {
delegate.touchFile(name);
}
@Override
public synchronized long fileLength(String name) throws IOException {
return delegate.fileLength(name);
}
@Override
public synchronized Lock makeLock(String name) {
return delegate.makeLock(name);
}
@Override
public synchronized void clearLock(String name) throws IOException {
delegate.clearLock(name);
}
@Override
public synchronized void setLockFactory(LockFactory lockFactory) {
delegate.setLockFactory(lockFactory);
}
@Override
public synchronized LockFactory getLockFactory() {
return delegate.getLockFactory();
}
@Override
public synchronized String getLockID() {
return delegate.getLockID();
}
@Override
public synchronized void copy(Directory to, String src, String dest) throws IOException {
delegate.copy(to, src, dest);
}
}