/* * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, * and the EPL 1.0 (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.test.utils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.List; import java.util.Random; import org.h2.store.fs.FileBase; import org.h2.store.fs.FilePath; import org.h2.store.fs.FilePathWrapper; /** * An unstable file system. It is used to simulate file system problems (for * example out of disk space). */ public class FilePathUnstable extends FilePathWrapper { private static final FilePathUnstable INSTANCE = new FilePathUnstable(); private static final IOException DISK_FULL = new IOException("Disk full"); private static int diskFullOffCount; private static boolean partialWrites; private static Random random = new Random(1); /** * Register the file system. * * @return the instance */ public static FilePathUnstable register() { FilePath.register(INSTANCE); return INSTANCE; } /** * Set the number of write operations before the disk is full, and the * random seed (for partial writes). * * @param count the number of write operations (0 to never fail, * Integer.MAX_VALUE to count the operations) * @param seed the new seed */ public void setDiskFullCount(int count, int seed) { diskFullOffCount = count; random.setSeed(seed); } public int getDiskFullCount() { return diskFullOffCount; } /** * Whether partial writes are possible (writing only part of the data). * * @param partialWrites true to enable */ public void setPartialWrites(boolean partialWrites) { FilePathUnstable.partialWrites = partialWrites; } boolean getPartialWrites() { return partialWrites; } /** * Get a buffer with a subset (the head) of the data of the source buffer. * * @param src the source buffer * @return a buffer with a subset of the data */ ByteBuffer getRandomSubset(ByteBuffer src) { int len = src.remaining(); len = Math.min(4096, Math.min(len, 1 + random.nextInt(len))); ByteBuffer temp = ByteBuffer.allocate(len); src.get(temp.array()); return temp; } /** * Check if the simulated problem occurred. * This call will decrement the countdown. * * @throws IOException if the simulated power failure occurred */ void checkError() throws IOException { if (diskFullOffCount == 0) { return; } if (--diskFullOffCount > 0) { return; } if (diskFullOffCount >= -1) { diskFullOffCount--; throw DISK_FULL; } } @Override public void createDirectory() { super.createDirectory(); } @Override public boolean createFile() { return super.createFile(); } @Override public void delete() { super.delete(); } @Override public boolean exists() { return super.exists(); } @Override public String getName() { return super.getName(); } @Override public long lastModified() { return super.lastModified(); } @Override public FilePath getParent() { return super.getParent(); } @Override public boolean isAbsolute() { return super.isAbsolute(); } @Override public boolean isDirectory() { return super.isDirectory(); } @Override public boolean canWrite() { return super.canWrite(); } @Override public boolean setReadOnly() { return super.setReadOnly(); } @Override public long size() { return super.size(); } @Override public List<FilePath> newDirectoryStream() { return super.newDirectoryStream(); } @Override public FilePath toRealPath() { return super.toRealPath(); } @Override public InputStream newInputStream() throws IOException { return super.newInputStream(); } @Override public FileChannel open(String mode) throws IOException { return new FileUnstable(this, super.open(mode)); } @Override public OutputStream newOutputStream(boolean append) throws IOException { return super.newOutputStream(append); } @Override public void moveTo(FilePath newName, boolean atomicReplace) { super.moveTo(newName, atomicReplace); } @Override public FilePath createTempFile(String suffix, boolean deleteOnExit, boolean inTempDir) throws IOException { return super.createTempFile(suffix, deleteOnExit, inTempDir); } @Override public String getScheme() { return "unstable"; } } /** * An file that checks for errors before each write operation. */ class FileUnstable extends FileBase { private final FilePathUnstable file; private final FileChannel channel; private boolean closed; FileUnstable(FilePathUnstable file, FileChannel channel) { this.file = file; this.channel = channel; } @Override public void implCloseChannel() throws IOException { channel.close(); closed = true; } @Override public long position() throws IOException { return channel.position(); } @Override public long size() throws IOException { return channel.size(); } @Override public int read(ByteBuffer dst) throws IOException { return channel.read(dst); } @Override public int read(ByteBuffer dst, long pos) throws IOException { return channel.read(dst, pos); } @Override public FileChannel position(long pos) throws IOException { channel.position(pos); return this; } @Override public FileChannel truncate(long newLength) throws IOException { checkError(); channel.truncate(newLength); return this; } @Override public void force(boolean metaData) throws IOException { checkError(); channel.force(metaData); } @Override public int write(ByteBuffer src) throws IOException { checkError(); if (file.getPartialWrites()) { return channel.write(file.getRandomSubset(src)); } return channel.write(src); } @Override public int write(ByteBuffer src, long position) throws IOException { checkError(); if (file.getPartialWrites()) { return channel.write(file.getRandomSubset(src), position); } return channel.write(src, position); } private void checkError() throws IOException { if (closed) { throw new IOException("Closed"); } file.checkError(); } @Override public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException { return channel.tryLock(position, size, shared); } @Override public String toString() { return "unstable:" + file.toString(); } }