/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Feb 10, 2010 */ package com.bigdata.io.writecache; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; import java.util.Random; import java.util.concurrent.TimeUnit; import junit.framework.AssertionFailedError; import com.bigdata.ha.HAPipelineGlue; import com.bigdata.io.ChecksumUtility; import com.bigdata.io.FileChannelUtility; import com.bigdata.io.IReopenChannel; import com.bigdata.io.TestCase3; import com.bigdata.io.writecache.TestWORMWriteCacheService.MyMockQuorumMember; import com.bigdata.quorum.MockQuorumFixture; import com.bigdata.quorum.MockQuorumFixture.MockQuorum; import com.bigdata.quorum.QuorumActor; import com.bigdata.rwstore.RWWriteCacheService; /** * Test suite for the {@link WriteCacheService} using scattered writes on a * backing file. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id: TestWriteCacheService.java 2866 2010-05-18 18:36:35Z * thompsonbry $ */ public class TestRWWriteCacheService extends TestCase3 { /** * */ public TestRWWriteCacheService() { } /** * @param name */ public TestRWWriteCacheService(String name) { super(name); } // /** // * // * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan // Thompson</a> // * @version $Id: TestRWWriteCacheService.java 4532 2011-05-20 16:06:11Z // thompsonbry $ // * @param <S> // */ // static private class MyMockQuorumMember<S extends HAPipelineGlue> extends // AbstractQuorumMember<S> { // // /** // * @param quorum // */ // protected MyMockQuorumMember() { // // super(UUID.randomUUID()); // // } // // @Override // public S getService(UUID serviceId) { // throw new UnsupportedOperationException(); // } // // public Executor getExecutor() { // throw new UnsupportedOperationException(); // } // // public S getService() { // throw new UnsupportedOperationException(); // } // // } final int k = 1; // no write on pipeline MockQuorumFixture fixture = null; String logicalServiceId = null; MockQuorum<HAPipelineGlue, MyMockQuorumMember<HAPipelineGlue>> quorum = null; File file = null; ReopenFileChannel opener = null; RWWriteCacheService writeCache = null; protected void setUp() throws Exception { fixture = new MockQuorumFixture(); logicalServiceId = "logicalService_" + getName(); quorum = new MockQuorum<HAPipelineGlue, MyMockQuorumMember<HAPipelineGlue>>( k, fixture); file = null; opener = null; writeCache = null; fixture.start(); quorum.start(new MyMockQuorumMember<HAPipelineGlue>(fixture, logicalServiceId)); final QuorumActor<?, ?> actor = quorum.getActor(); actor.memberAdd(); fixture.awaitDeque(); actor.pipelineAdd(); fixture.awaitDeque(); actor.castVote(0); fixture.awaitDeque(); // Await quorum meet. assertCondition(new Runnable() { @Override public void run() { try { assertEquals(0L, quorum.token()); } catch (Exception e) { fail(); } } }, 5000/*timeout*/, TimeUnit.MILLISECONDS); file = File.createTempFile(getName(), ".rw.tmp"); opener = new ReopenFileChannel(file, "rw"); final long fileExtent = opener.reopenChannel().size(); final boolean prefixWrites = true; final int compactionThreshold = 30; final int hotCacheThreshold = 1; writeCache = new RWWriteCacheService(120/* nbuffers */, 5/* minCleanListSize */, 5/*readCacheSize*/, prefixWrites, compactionThreshold, 0/*hotCacheSize*/, hotCacheThreshold, fileExtent, opener, quorum, null); } protected void tearDown() throws Exception { if (writeCache != null) writeCache.close(); if (opener != null) { opener.destroy(); } quorum.terminate(); fixture.terminate(); super.tearDown(); } public void test_simpleRWService() throws IOException, InterruptedException { writeCache.close(); writeCache = null; } public void test_simpleDataRWService() throws IOException { try { final ByteBuffer data1 = getRandomData(); final long addr1 = 2048; { assertNull(writeCache.read(addr1, data1.capacity())); // write record @ addr. assertTrue(writeCache.write(addr1, data1.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data1))); // verify record @ addr can be read. assertNotNull(writeCache.read(addr1, data1.capacity())); // verify data read back @ addr. data1.position(0); assertEquals(data1, writeCache.read(addr1, data1.capacity())); } } catch (Exception e) { fail("Unexpected Exception", e); } } private void randomizeArray(ArrayList<AllocView> allocs) { final int slots = allocs.size(); for (int i = 0; i < slots/2; i++) { int swap1 = r.nextInt(slots); int swap2 = r.nextInt(slots); AllocView v1 = allocs.get(swap1); AllocView v2 = allocs.get(swap2); allocs.set(swap1, v2); allocs.set(swap2, v1); } } public void test_stressDataRWService() throws InterruptedException, IOException { /* * Whether or nor the write cache will force writes to the disk. For * this test, force is false since it just does not matter whether the * data are restart safe. */ final boolean force = false; /* * We will create a list of Random 0-1024 byte writes by creating single * random buffer of 2K and generating random views of differing * positions and lengths */ final ByteBuffer srcBuf = getRandomData(4096); final int totalAllocs = 10000; ArrayList<AllocView> allocs = new ArrayList<AllocView>(); int curAddr = 0; for (int i = 0; i < totalAllocs; i++) { int pos = r.nextInt(3072); int size = r.nextInt(1023) + 1; allocs.add(new AllocView(curAddr, pos, size, srcBuf)); curAddr += (size + 4); // include space for chk; } final ChecksumUtility checker = new ChecksumUtility(); // Now randomize the array for writing randomizeArray(allocs); /* * First write 500 records into the cache and confirm they can all be * read okay */ for (int i = 0; i < 500; i++) { AllocView v = allocs.get(i); writeCache.write(v.addr, v.buf.asReadOnlyBuffer(), checker .checksum(v.buf)); v.buf.position(0); } for (int i = 0; i < 500; i++) { AllocView v = allocs.get(i); assertEquals(v.buf, writeCache.read(v.addr, v.nbytes)); // expected, // actual } /* * Flush to disk and reset the cache */ writeCache.flush(true); /* * Now confirm that data is in the cache AND on disk */ for (int i = 0; i < 500; i++) { AllocView v = allocs.get(i); assertEquals(v.buf, writeCache.read(v.addr, v.nbytes)); } for (int i = 0; i < 500; i++) { AllocView v = allocs.get(i); assertEquals(v.buf, opener.read(v.addr, v.nbytes)); // expected, // actual // on // DISK } /* * Now add further 500 writes, flush and read full 1000 from disk */ for (int i = 500; i < 1000; i++) { AllocView v = allocs.get(i); writeCache.write(v.addr, v.buf.asReadOnlyBuffer(), checker .checksum(v.buf)); v.buf.position(0); } writeCache.flush(true); for (int i = 0; i < 1000; i++) { AllocView v = allocs.get(i); try { assertEquals(v.buf, opener.read(v.addr, v.buf.capacity())); // expected, // actual } catch (AssertionFailedError e) { System.err.println("ERROR: i=" + i + ", v=" + v.buf); throw e; } } /* * Now write remaining records, checking for write success * and if fail then flush/reset and resubmit, asserting that * resubmission is successful */ // writeCache.reset(); for (int i = 1000; i < totalAllocs; i++) { AllocView v = allocs.get(i); if (!writeCache.write(v.addr, v.buf.asReadOnlyBuffer(), checker .checksum(v.buf))) { log.info("flushing and resetting writeCache"); writeCache.flush(false); // writeCache.reset(); assertTrue(writeCache.write(v.addr, v.buf.asReadOnlyBuffer(), checker.checksum(v.buf))); } v.buf.position(0); } /* * Now flush and check if we can read in all records */ writeCache.flush(true); for (int i = 0; i < totalAllocs; i++) { AllocView v = allocs.get(i); assertEquals(v.buf, opener.read(v.addr, v.buf.capacity())); // expected, // actual } /* * Now reset, reshuffle and write full 10000 records, checking for write * success and if fail then flush/reset and resubmit, asserting that * resubmission is successful */ for (int i = 0; i < totalAllocs; i++) { AllocView v = allocs.get(i); // must ensure any existing write is removed first (alternative to resetting the cache) writeCache.clearWrite(v.addr, 0); } randomizeArray(allocs); int duplicates = 0; for (int i = 0; i < totalAllocs; i++) { AllocView v = allocs.get(i); v.buf.reset(); // Any exception is an error! writeCache.write(v.addr, v.buf.asReadOnlyBuffer(), checker .checksum(v.buf)); } /* * Now flush and check if we can read in all records */ writeCache.flush(true); for (int i = 0; i < totalAllocs; i++) { AllocView v = allocs.get(i); v.buf.position(0); assertEquals(v.buf, opener.read(v.addr, v.buf.capacity())); // expected, // actual } } /** * A random number generated - the seed is NOT fixed. */ protected final Random r = new Random(); /** * Returns random data that will fit in N bytes. N is chosen randomly in * 1:256. * * @return A new {@link ByteBuffer} wrapping a new <code>byte[]</code> of * random length and having random contents. */ public ByteBuffer getRandomData() { final int nbytes = r.nextInt(256) + 1; return getRandomData(nbytes); } /** * Returns random data that will fit in <i>nbytes</i>. * * @return A new {@link ByteBuffer} wrapping a new <code>byte[]</code> * having random contents. */ public ByteBuffer getRandomData(final int nbytes) { final byte[] bytes = new byte[nbytes]; r.nextBytes(bytes); return ByteBuffer.wrap(bytes); } /** * Simple implementation for a {@link RandomAccessFile} with hook for * deleting the test file. */ private static class ReopenFileChannel implements IReopenChannel<FileChannel> { final private File file; private final String mode; private volatile RandomAccessFile raf; public ReopenFileChannel(final File file, final String mode) throws IOException { this.file = file; this.mode = mode; reopenChannel(); } public String toString() { return file.toString(); } /** * Hook used by the unit tests to destroy their test files. */ public void destroy() { try { raf.close(); } catch (IOException e) { log.error(e, e); } if (!file.delete()) log.warn("Could not delete file: " + file); } /** * Read some data out of the file. * * @param off * The offset of the record. * @param nbytes * The #of bytes to be read. * @return The record. */ public ByteBuffer read(final long off, final int nbytes) throws IOException { final ByteBuffer tmp = ByteBuffer.allocate(nbytes); FileChannelUtility.readAll(this, tmp, off); // flip for reading. tmp.flip(); return tmp; } synchronized public FileChannel reopenChannel() throws IOException { if (raf != null && raf.getChannel().isOpen()) { /* * The channel is still open. If you are allowing concurrent * reads on the channel, then this could indicate that two * readers each found the channel closed and that one was able * to re-open the channel before the other such that the channel * was open again by the time the 2nd reader got here. */ return raf.getChannel(); } // open the file. this.raf = new RandomAccessFile(file, mode); if (log.isInfoEnabled()) log.info("(Re-)opened file: " + file); return raf.getChannel(); } }; /* * Now generate randomviews, first an ordered view of 10000 random lengths */ class AllocView { int addr; ByteBuffer buf; final int nbytes; AllocView(int pa, int pos, int limit, ByteBuffer src) { addr = pa; // ByteBuffer vbuf = src.duplicate(); // vbuf.position(pos); // vbuf.limit(pos + limit); // buf = ByteBuffer.allocate(limit); // copy the data into [dst]. // buf.put(vbuf); buf = getRandomData(limit); buf.mark(); nbytes = buf.capacity(); } }; /** * Test simple compaction of WriteCache * @throws InterruptedException */ public void testCompactingCopy() throws InterruptedException { final WriteCache cache1 = writeCache.newWriteCache(null/* buf */, true, false/* bufferHasData */, opener, 0); final WriteCache cache2 = writeCache.newWriteCache(null/* buf */, true, false/* bufferHasData */, opener, 0); final ByteBuffer data1 = getRandomData(); final ByteBuffer data2 = getRandomData(); final ByteBuffer data3 = getRandomData(); final long addr1 = 2048; final long addr2 = 12598; final long addr3 = 512800; try { assertNull(cache1.read(addr1, data1.capacity())); // write record @ addr. assertTrue(cache1.write(addr1, data1.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data1))); assertTrue(cache1.write(addr2, data2.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data2))); assertTrue(cache1.write(addr3, data3.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data3))); // verify record @ addr can be read. assertNotNull(cache1.read(addr1, data1.capacity())); assertNotNull(cache1.read(addr2, data2.capacity())); assertNotNull(cache1.read(addr3, data3.capacity())); cache1.clearAddrMap(addr2, 0); WriteCache.transferTo(cache1, cache2, null, 0); assertNull(cache1.read(addr1, data1.capacity())); assertNotNull(cache2.read(addr1, data1.capacity())); assertNull(cache2.read(addr2, data2.capacity())); // verify data read back @ addr. data1.position(0); assertEquals(data1, cache2.read(addr1, data1.capacity())); // now go back the other way cache1.reset(); WriteCache.transferTo(cache2, cache1, null, 0); data1.position(0); assertEquals(data1, cache1.read(addr1, data1.capacity())); data2.position(0); assertEquals(data3, cache1.read(addr3, data3.capacity())); } finally { // Release explicitly created caches cache1.close(); cache2.close(); } } public void testFullCompactingCopy() throws InterruptedException { final WriteCache src = writeCache.newWriteCache(null, true, false, opener, 0); final WriteCache dst = writeCache.newWriteCache(null, true, false, opener, 0); try { boolean notFull = true; long addr = 0; while (notFull) { final ByteBuffer data = getRandomData(r.nextInt(250)+1); notFull = src.write(addr, data.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data)); addr += 1000; } ; assertTrue(WriteCache.transferTo(src, dst, null, 0)); } finally { src.close(); dst.close(); } } public void testSingleCompactingCopy() throws InterruptedException { final WriteCache src = writeCache.newWriteCache(null, true, false, opener, 0); final WriteCache dst = writeCache.newWriteCache(null, true, false, opener, 0); try { final long addr = 1000; final ByteBuffer data = getRandomData(r.nextInt(250)+1); src.write(addr, data.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data)); assertNotNull(src.read(addr, data.capacity())); final int sb = src.bytesWritten(); assertTrue(WriteCache.transferTo(src, dst, null, 0)); final int db = dst.bytesWritten(); assertTrue(sb == db); assertNotNull(dst.read(addr, data.capacity())); } finally { src.close(); dst.close(); } } /** * 1) Creates five WriteCaches and writes until four full. * * 2) Randomly removes half the writes. * * 3) The compacts into final and newly emptied caches. */ public void testStressCompactingCopy() throws InterruptedException { // five src caches final WriteCache[] srccaches = new WriteCache[] { writeCache.newWriteCache(null, true, false, opener, 0), writeCache.newWriteCache(null, true, false, opener, 0), writeCache.newWriteCache(null, true, false, opener, 0), writeCache.newWriteCache(null, true, false, opener, 0), writeCache.newWriteCache(null, true, false, opener, 0), }; // single compacting cache, then space for newly clean caches to be // moved. final WriteCache[] dstcaches = new WriteCache[] { writeCache.newWriteCache(null, true, false, opener, 0), null, null, null, null, null, }; try { final HashMap<Long, WriteCache> map = new HashMap<Long, WriteCache>(); long addr = 0; for (WriteCache src : srccaches) { boolean notFull = true; while (notFull) { final ByteBuffer data = getRandomData(); notFull = src.write(addr, data.asReadOnlyBuffer(), ChecksumUtility.threadChk.get().checksum(data)); if (notFull) map.put(addr, src); addr += 1000; } } // okay source buffers are full // now clear every other write using map for (Entry<Long, WriteCache> entry : map.entrySet()) { final long k = entry.getKey(); // clear every other address if ((k % 2000) == 0) entry.getValue().clearAddrMap(k, 0); } int dstIndex = 0; int dstFree = 1; for (WriteCache src : srccaches) { boolean done = false; while (!done) { done = WriteCache.transferTo(src, dstcaches[dstIndex], null, 0); if (!done) dstIndex++; } // Now add src to dstcaches, available to be used for compaction src.reset(); dstcaches[dstFree++] = src; } } finally { // Release explicitly created caches for (WriteCache cache : dstcaches) { if (cache != null) cache.close(); } for (WriteCache cache : srccaches) { if (cache != null) cache.close(); } } } }