Lock BlockStore files to prevent concurrent access.

Resolves issue 153.
This commit is contained in:
Miron Cuperman 2012-03-23 10:53:54 -07:00
parent 7f6d636cec
commit 4b1c32584f
5 changed files with 118 additions and 12 deletions

View File

@ -52,4 +52,7 @@ public interface BlockStore {
* Sets the {@link StoredBlock} that represents the top of the chain of greatest total work.
*/
void setChainHead(StoredBlock chainHead) throws BlockStoreException;
/** Closes the store. */
void close() throws BlockStoreException;
}

View File

@ -27,6 +27,8 @@ import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@ -77,6 +79,7 @@ public class BoundedOverheadBlockStore implements BlockStore {
private Sha256Hash chainHead;
private final NetworkParameters params;
private FileChannel channel;
private FileLock lock;
private static class Record {
// A BigInteger representing the total amount of work done so far on this chain. As of May 2011 it takes 8
@ -192,6 +195,19 @@ public class BoundedOverheadBlockStore implements BlockStore {
log.info("Reading block store from {}", file);
// Open in synchronous mode. See above.
this.file = new RandomAccessFile(file, "rwd");
try {
lock = this.file.getChannel().tryLock();
} catch (OverlappingFileLockException e) {
lock = null;
}
if (lock == null) {
try {
this.file.close();
} finally {
this.file = null;
}
throw new BlockStoreException("Could not lock file");
}
try {
channel = this.file.getChannel();
// Read a version byte.
@ -219,7 +235,14 @@ public class BoundedOverheadBlockStore implements BlockStore {
}
}
private void ensureOpen() throws BlockStoreException {
if (file == null) {
throw new BlockStoreException("BlockStore was closed");
}
}
public synchronized void put(StoredBlock block) throws BlockStoreException {
ensureOpen();
try {
Sha256Hash hash = block.getHeader().getHash();
// Append to the end of the file.
@ -231,6 +254,7 @@ public class BoundedOverheadBlockStore implements BlockStore {
}
public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException {
ensureOpen();
// Check the memory cache first.
StoredBlock fromMem = blockCache.get(hash);
if (fromMem != null) {
@ -259,7 +283,7 @@ public class BoundedOverheadBlockStore implements BlockStore {
private ByteBuffer buf = ByteBuffer.allocateDirect(Record.SIZE);
private Record getRecord(Sha256Hash hash) throws BlockStoreException, IOException, ProtocolException {
private Record getRecord(Sha256Hash hash) throws IOException, ProtocolException {
long startPos = channel.position();
// Use our own file pointer within the tight loop as updating channel positions is really expensive.
long pos = startPos;
@ -299,6 +323,7 @@ public class BoundedOverheadBlockStore implements BlockStore {
}
public synchronized StoredBlock getChainHead() throws BlockStoreException {
ensureOpen();
// This will hit the cache
StoredBlock head = get(chainHead);
if (head == null)
@ -307,6 +332,7 @@ public class BoundedOverheadBlockStore implements BlockStore {
}
public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException {
ensureOpen();
try {
this.chainHead = chainHead.getHeader().getHash();
// Write out new hash to the first 32 bytes of the file past one (first byte is version number).
@ -315,4 +341,15 @@ public class BoundedOverheadBlockStore implements BlockStore {
throw new BlockStoreException(e);
}
}
public void close() throws BlockStoreException {
ensureOpen();
try {
file.close();
} catch (IOException e) {
throw new BlockStoreException(e);
} finally {
file = null;
}
}
}

View File

@ -25,6 +25,8 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.HashMap;
import java.util.Map;
@ -39,12 +41,27 @@ public class DiskBlockStore implements BlockStore {
private Map<Sha256Hash, StoredBlock> blockMap;
private Sha256Hash chainHead;
private NetworkParameters params;
private FileLock lock;
public DiskBlockStore(NetworkParameters params, File theFile) throws BlockStoreException {
this.params = params;
blockMap = new HashMap<Sha256Hash, StoredBlock>();
try {
file = new RandomAccessFile(theFile, "rwd");
// Lock the file from other processes.
try {
lock = file.getChannel().tryLock();
} catch (OverlappingFileLockException e) {
lock = null;
}
if (lock == null) {
try {
this.file.close();
} finally {
this.file = null;
}
throw new BlockStoreException("Could not lock file");
}
// The file position is at BOF
load(theFile);
// The file position is at EOF
@ -54,6 +71,17 @@ public class DiskBlockStore implements BlockStore {
// The file position is at EOF
}
}
public void close() throws BlockStoreException {
ensureOpen();
try {
file.close();
} catch (IOException e) {
throw new BlockStoreException(e);
} finally {
file = null;
}
}
private void createNewStore(NetworkParameters params) throws BlockStoreException {
// Create a new block store if the file wasn't found or anything went wrong whilst reading.
@ -145,7 +173,14 @@ public class DiskBlockStore implements BlockStore {
}
}
private void ensureOpen() throws BlockStoreException {
if (file == null) {
throw new BlockStoreException("BlockStore was closed");
}
}
public synchronized void put(StoredBlock block) throws BlockStoreException {
ensureOpen();
try {
Sha256Hash hash = block.getHeader().getHash();
assert blockMap.get(hash) == null : "Attempt to insert duplicate";
@ -159,14 +194,17 @@ public class DiskBlockStore implements BlockStore {
}
public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException {
ensureOpen();
return blockMap.get(hash);
}
public synchronized StoredBlock getChainHead() throws BlockStoreException {
ensureOpen();
return blockMap.get(chainHead);
}
public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException {
ensureOpen();
try {
this.chainHead = chainHead.getHeader().getHash();
// Write out new hash to the first 32 bytes of the file past one (first byte is version number).

View File

@ -44,19 +44,27 @@ public class MemoryBlockStore implements BlockStore {
}
public synchronized void put(StoredBlock block) throws BlockStoreException {
if (blockMap == null) throw new BlockStoreException("MemoryBlockStore is closed");
Sha256Hash hash = block.getHeader().getHash();
blockMap.put(hash, block);
}
public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException {
if (blockMap == null) throw new BlockStoreException("MemoryBlockStore is closed");
return blockMap.get(hash);
}
public StoredBlock getChainHead() {
public StoredBlock getChainHead() throws BlockStoreException {
if (blockMap == null) throw new BlockStoreException("MemoryBlockStore is closed");
return chainHead;
}
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
if (blockMap == null) throw new BlockStoreException("MemoryBlockStore is closed");
this.chainHead = chainHead;
}
public void close() {
blockMap = null;
}
}

View File

@ -19,20 +19,32 @@ import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.StoredBlock;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class DiskBlockStoreTest {
@Test
public void testStorage() throws Exception {
File temp = File.createTempFile("bitcoinj-test", null, null);
private NetworkParameters params;
private Address to;
private File temp;
@Before
public void setUp() throws IOException {
temp = File.createTempFile("bitcoinj-test", null, null);
System.out.println(temp.getAbsolutePath());
NetworkParameters params = NetworkParameters.unitTests();
Address to = new ECKey().toAddress(params);
params = NetworkParameters.unitTests();
to = new ECKey().toAddress(params);
}
@Test
public void testStorage() throws Exception {
DiskBlockStore store = new DiskBlockStore(params, temp);
// Check the first block in a new store is the genesis block.
StoredBlock genesis = store.getChainHead();
@ -42,25 +54,31 @@ public class DiskBlockStoreTest {
StoredBlock b1 = genesis.build(genesis.getHeader().createNextBlock(to).cloneAsHeader());
store.put(b1);
store.setChainHead(b1);
store.close();
// Check we can get it back out again if we rebuild the store object.
store = new DiskBlockStore(params, temp);
StoredBlock b2 = store.get(b1.getHeader().getHash());
assertEquals(b1, b2);
// Check the chain head was stored correctly also.
assertEquals(b1, store.getChainHead());
store.close();
}
@Test
public void testStorage_existing() throws Exception {
File temp = File.createTempFile("bitcoinj-test", null, null);
System.out.println(temp.getAbsolutePath());
NetworkParameters params = NetworkParameters.unitTests();
Address to = new ECKey().toAddress(params);
DiskBlockStore store = new DiskBlockStore(params, temp);
// Check the first block in a new store is the genesis block.
StoredBlock genesis = store.getChainHead();
// Test locking
try {
store = new DiskBlockStore(params, temp);
fail();
} catch (BlockStoreException ex) {
// expected
}
store.close();
// Reopen.
store = new DiskBlockStore(params, temp);
@ -69,6 +87,7 @@ public class DiskBlockStoreTest {
StoredBlock b1 = genesis.build(genesis.getHeader().createNextBlock(to).cloneAsHeader());
store.put(b1);
store.setChainHead(b1);
store.close();
// Check we can get it back out again if we reopen the store.
store = new DiskBlockStore(params, temp);
@ -76,5 +95,6 @@ public class DiskBlockStoreTest {
assertEquals(b1, b2);
// Check the chain head was stored correctly also.
assertEquals(b1, store.getChainHead());
store.close();
}
}