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.IOException; import java.io.FileNotFoundException; 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; import java.util.Arrays; /** * This is a subclass of RAMDirectory that adds methods * intended to be used only by unit tests. */ public class MockRAMDirectory extends RAMDirectory { long maxSize; // Max actual bytes used. This is set by MockRAMOutputStream: long maxUsedSize; double randomIOExceptionRate; Random randomState; boolean noDeleteOpenFile = true; boolean preventDoubleWrite = true; private Set<String> unSyncedFiles; private Set<String> createdFiles; volatile boolean crashed; // 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; // Only tracked if noDeleteOpenFile is true: if an attempt // is made to delete an open file, we enroll it here. Set<String> openFilesDeleted; private synchronized void init() { if (openFiles == null) { openFiles = new HashMap<String,Integer>(); openFilesDeleted = new HashSet<String>(); } if (createdFiles == null) createdFiles = new HashSet(); if (unSyncedFiles == null) unSyncedFiles = new HashSet(); } public MockRAMDirectory() { super(); init(); } public MockRAMDirectory(Directory dir) throws IOException { super(dir); init(); } /** If set to true, we throw an IOException if the same * file is opened by createOutput, ever. */ public void setPreventDoubleWrite(boolean value) { preventDoubleWrite = value; } @Override public synchronized void sync(String name) throws IOException { maybeThrowDeterministicException(); if (crashed) throw new IOException("cannot sync after crash"); if (unSyncedFiles.contains(name)) unSyncedFiles.remove(name); } /** Simulates a crash of OS or machine by overwriting * unsynced files. */ public synchronized void crash() throws IOException { crashed = true; openFiles = new HashMap(); openFilesDeleted = new HashSet<String>(); Iterator<String> it = unSyncedFiles.iterator(); unSyncedFiles = new HashSet(); int count = 0; while(it.hasNext()) { String name = it.next(); RAMFile file = fileMap.get(name); if (count % 3 == 0) { deleteFile(name, true); } else if (count % 3 == 1) { // Zero out file entirely final int numBuffers = file.numBuffers(); for(int i=0;i<numBuffers;i++) { byte[] buffer = file.getBuffer(i); Arrays.fill(buffer, (byte) 0); } } else if (count % 3 == 2) { // Truncate the file: file.setLength(file.getLength()/2); } 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() { 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 && noDeleteOpenFile) { if (openFiles.containsKey(name)) { openFilesDeleted.add(name); throw new IOException("MockRAMDirectory: file \"" + name + "\" is still open: cannot delete"); } else { openFilesDeleted.remove(name); } } super.deleteFile(name); } public synchronized Set<String> getOpenDeletedFiles() { return new HashSet<String>(openFilesDeleted); } @Override public synchronized IndexOutput createOutput(String name) throws IOException { if (crashed) throw new IOException("cannot createOutput after crash"); init(); 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("MockRAMDirectory: file \"" + name + "\" is still open: cannot overwrite"); RAMFile file = new RAMFile(this); if (crashed) throw new IOException("cannot createOutput after crash"); unSyncedFiles.add(name); createdFiles.add(name); RAMFile existing = 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) { sizeInBytes -= existing.sizeInBytes; existing.directory = null; } fileMap.put(name, file); } return new MockRAMOutputStream(this, file, name); } @Override public synchronized IndexInput openInput(String name) throws IOException { RAMFile file = fileMap.get(name); if (file == null) throw new FileNotFoundException(name); else { if (openFiles.containsKey(name)) { Integer v = (Integer) openFiles.get(name); v = Integer.valueOf(v.intValue()+1); openFiles.put(name, v); } else { openFiles.put(name, Integer.valueOf(1)); } } return new MockRAMInputStream(this, name, file); } /** Provided for testing purposes. Use sizeInBytes() instead. */ public synchronized final long getRecomputedSizeInBytes() { long size = 0; for(final RAMFile file: 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() { long size = 0; for (final RAMFile file : fileMap.values()) size += file.length; return size; } @Override public synchronized void close() { if (openFiles == null) { openFiles = new HashMap(); openFilesDeleted = new HashSet<String>(); } if (noDeleteOpenFile && openFiles.size() > 0) { // RuntimeException instead of IOException because // super() does not throw IOException currently: throw new RuntimeException("MockRAMDirectory: cannot close: there are still open files: " + openFiles); } } /** * 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(MockRAMDirectory 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 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(); } 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++) { ((Failure)failures.get(i)).eval(this); } } } }