391 lines
12 KiB
Java
391 lines
12 KiB
Java
/*******************************************************************************
|
|
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
|
|
*
|
|
* Licensed 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.
|
|
******************************************************************************/
|
|
|
|
|
|
package org.apache.jdbm;
|
|
|
|
import javax.crypto.Cipher;
|
|
import java.io.IOError;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
|
|
/**
|
|
* This class represents a random access file as a set of fixed size
|
|
* records. Each record has a physical record number, and records are
|
|
* cached in order to improve access.
|
|
* <p/>
|
|
* The set of dirty records on the in-use list constitutes a transaction.
|
|
* Later on, we will send these records to some recovery thingy.
|
|
* <p/>
|
|
* PageFile is splited between more files, each with max size 1GB.
|
|
*/
|
|
final class PageFile {
|
|
final PageTransactionManager txnMgr;
|
|
|
|
|
|
/**
|
|
* Pages currently locked for read/update ops. When released the page goes
|
|
* to the dirty or clean list, depending on a flag. The file header page is
|
|
* normally locked plus the page that is currently being read or modified.
|
|
*
|
|
* @see PageIo#isDirty()
|
|
*/
|
|
private final LongHashMap<PageIo> inUse = new LongHashMap<PageIo>();
|
|
|
|
/**
|
|
* Pages whose state is dirty.
|
|
*/
|
|
private final LongHashMap<PageIo> dirty = new LongHashMap<PageIo>();
|
|
/**
|
|
* Pages in a <em>historical</em> transaction(s) that have been written
|
|
* onto the log but which have not yet been committed to the database.
|
|
*/
|
|
private final LongHashMap<PageIo> inTxn = new LongHashMap<PageIo>();
|
|
|
|
|
|
// transactions disabled?
|
|
final boolean transactionsDisabled;
|
|
|
|
/**
|
|
* A array of clean data to wipe clean pages.
|
|
*/
|
|
static final byte[] CLEAN_DATA = new byte[Storage.PAGE_SIZE];
|
|
|
|
|
|
final Storage storage;
|
|
private Cipher cipherOut;
|
|
private Cipher cipherIn;
|
|
|
|
|
|
/**
|
|
* Creates a new object on the indicated filename. The file is
|
|
* opened in read/write mode.
|
|
*
|
|
* @param fileName the name of the file to open or create, without
|
|
* an extension.
|
|
* @throws IOException whenever the creation of the underlying
|
|
* RandomAccessFile throws it.
|
|
*/
|
|
PageFile(String fileName, boolean readonly, boolean transactionsDisabled, Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile, boolean lockingDisabled) throws IOException {
|
|
this.cipherIn = cipherIn;
|
|
this.cipherOut = cipherOut;
|
|
this.transactionsDisabled = transactionsDisabled;
|
|
if(fileName == null){
|
|
this.storage = new StorageMemory(transactionsDisabled);
|
|
}else if(DBMaker.isZipFileLocation(fileName)!=null)
|
|
this.storage = new StorageZip(DBMaker.isZipFileLocation(fileName));
|
|
// }else if (fileName.contains("!/"))
|
|
// this.storage = new StorageZip(fileName);
|
|
else if(useRandomAccessFile)
|
|
this.storage = new StorageDisk(fileName,readonly,lockingDisabled);
|
|
else
|
|
this.storage = new StorageDiskMapped(fileName,readonly,transactionsDisabled,lockingDisabled);
|
|
|
|
if (this.storage.isReadonly() && !readonly)
|
|
throw new IllegalArgumentException("This type of storage is readonly, you should call readonly() on DBMaker");
|
|
if (!readonly && !transactionsDisabled) {
|
|
txnMgr = new PageTransactionManager(this, storage, cipherIn, cipherOut);
|
|
} else {
|
|
txnMgr = null;
|
|
}
|
|
}
|
|
|
|
public PageFile(String filename) throws IOException {
|
|
this(filename, false, false, null, null,false,false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets a page from the file. The returned byte array is
|
|
* the in-memory copy of the record, and thus can be written
|
|
* (and subsequently released with a dirty flag in order to
|
|
* write the page back). If transactions are disabled, changes
|
|
* may be written directly
|
|
*
|
|
* @param pageId The record number to retrieve.
|
|
*/
|
|
PageIo get(long pageId) throws IOException {
|
|
|
|
// try in transaction list, dirty list, free list
|
|
PageIo node = inTxn.get(pageId);
|
|
if (node != null) {
|
|
inTxn.remove(pageId);
|
|
inUse.put(pageId, node);
|
|
return node;
|
|
}
|
|
node = dirty.get(pageId);
|
|
if (node != null) {
|
|
dirty.remove(pageId);
|
|
inUse.put(pageId, node);
|
|
return node;
|
|
}
|
|
|
|
|
|
// sanity check: can't be on in use list
|
|
if (inUse.get(pageId) != null) {
|
|
throw new Error("double get for page " + pageId);
|
|
}
|
|
|
|
//read node from file
|
|
if (cipherOut == null) {
|
|
node = new PageIo(pageId,storage.read(pageId));
|
|
} else {
|
|
//decrypt if needed
|
|
ByteBuffer b = storage.read(pageId);
|
|
byte[] bb;
|
|
if(b.hasArray()){
|
|
bb = b.array();
|
|
}else{
|
|
bb = new byte[Storage.PAGE_SIZE];
|
|
b.position(0);
|
|
b.get(bb, 0, Storage.PAGE_SIZE);
|
|
}
|
|
if (!Utils.allZeros(bb)) try {
|
|
bb = cipherOut.doFinal(bb);
|
|
node = new PageIo(pageId, ByteBuffer.wrap(bb));
|
|
} catch (Exception e) {
|
|
throw new IOError(e);
|
|
}else {
|
|
node = new PageIo(pageId, ByteBuffer.wrap(PageFile.CLEAN_DATA).asReadOnlyBuffer());
|
|
}
|
|
}
|
|
|
|
|
|
inUse.put(pageId, node);
|
|
node.setClean();
|
|
return node;
|
|
}
|
|
|
|
|
|
/**
|
|
* Releases a page.
|
|
*
|
|
* @param pageId The record number to release.
|
|
* @param isDirty If true, the page was modified since the get().
|
|
*/
|
|
void release(final long pageId, final boolean isDirty) throws IOException {
|
|
|
|
final PageIo page = inUse.remove(pageId);
|
|
if (!page.isDirty() && isDirty)
|
|
page.setDirty();
|
|
|
|
if (page.isDirty()) {
|
|
dirty.put(pageId, page);
|
|
} else if (!transactionsDisabled && page.isInTransaction()) {
|
|
inTxn.put(pageId, page);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Releases a page.
|
|
*
|
|
* @param page The page to release.
|
|
*/
|
|
void release(final PageIo page) throws IOException {
|
|
final long key = page.getPageId();
|
|
inUse.remove(key);
|
|
if (page.isDirty()) {
|
|
// System.out.println( "Dirty: " + key + page );
|
|
dirty.put(key, page);
|
|
} else if (!transactionsDisabled && page.isInTransaction()) {
|
|
inTxn.put(key, page);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discards a page (will not write the page even if it's dirty)
|
|
*
|
|
* @param page The page to discard.
|
|
*/
|
|
void discard(PageIo page) {
|
|
long key = page.getPageId();
|
|
inUse.remove(key);
|
|
}
|
|
|
|
/**
|
|
* Commits the current transaction by flushing all dirty buffers
|
|
* to disk.
|
|
*/
|
|
void commit() throws IOException {
|
|
// debugging...
|
|
if (!inUse.isEmpty() && inUse.size() > 1) {
|
|
showList(inUse.valuesIterator());
|
|
throw new Error("in use list not empty at commit time ("
|
|
+ inUse.size() + ")");
|
|
}
|
|
|
|
// System.out.println("committing...");
|
|
|
|
if (dirty.size() == 0) {
|
|
// if no dirty pages, skip commit process
|
|
return;
|
|
}
|
|
|
|
if (!transactionsDisabled) {
|
|
txnMgr.start();
|
|
}
|
|
|
|
//sort pages by IDs
|
|
long[] pageIds = new long[dirty.size()];
|
|
int c = 0;
|
|
for (Iterator<PageIo> i = dirty.valuesIterator(); i.hasNext(); ) {
|
|
pageIds[c] = i.next().getPageId();
|
|
c++;
|
|
}
|
|
Arrays.sort(pageIds);
|
|
|
|
for (long pageId : pageIds) {
|
|
PageIo node = dirty.get(pageId);
|
|
|
|
// System.out.println("node " + node + " map size now " + dirty.size());
|
|
if (transactionsDisabled) {
|
|
if(cipherIn !=null)
|
|
storage.write(node.getPageId(), ByteBuffer.wrap(Utils.encrypt(cipherIn, node.getData())));
|
|
else
|
|
storage.write(node.getPageId(),node.getData());
|
|
node.setClean();
|
|
} else {
|
|
txnMgr.add(node);
|
|
inTxn.put(node.getPageId(), node);
|
|
}
|
|
}
|
|
dirty.clear();
|
|
if (!transactionsDisabled) {
|
|
txnMgr.commit();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Rollback the current transaction by discarding all dirty buffers
|
|
*/
|
|
void rollback() throws IOException {
|
|
// debugging...
|
|
if (!inUse.isEmpty()) {
|
|
showList(inUse.valuesIterator());
|
|
throw new Error("in use list not empty at rollback time ("
|
|
+ inUse.size() + ")");
|
|
}
|
|
// System.out.println("rollback...");
|
|
dirty.clear();
|
|
|
|
txnMgr.synchronizeLogFromDisk();
|
|
|
|
if (!inTxn.isEmpty()) {
|
|
showList(inTxn.valuesIterator());
|
|
throw new Error("in txn list not empty at rollback time ("
|
|
+ inTxn.size() + ")");
|
|
}
|
|
;
|
|
}
|
|
|
|
/**
|
|
* Commits and closes file.
|
|
*/
|
|
void close() throws IOException {
|
|
if (!dirty.isEmpty()) {
|
|
commit();
|
|
}
|
|
|
|
if(!transactionsDisabled && txnMgr!=null){
|
|
txnMgr.shutdown();
|
|
}
|
|
|
|
if (!inTxn.isEmpty()) {
|
|
showList(inTxn.valuesIterator());
|
|
throw new Error("In transaction not empty");
|
|
}
|
|
|
|
// these actually ain't that bad in a production release
|
|
if (!dirty.isEmpty()) {
|
|
System.out.println("ERROR: dirty pages at close time");
|
|
showList(dirty.valuesIterator());
|
|
throw new Error("Dirty pages at close time");
|
|
}
|
|
if (!inUse.isEmpty()) {
|
|
System.out.println("ERROR: inUse pages at close time");
|
|
showList(inUse.valuesIterator());
|
|
throw new Error("inUse pages at close time");
|
|
}
|
|
|
|
storage.sync();
|
|
storage.forceClose();
|
|
}
|
|
|
|
|
|
/**
|
|
* Force closing the file and underlying transaction manager.
|
|
* Used for testing purposed only.
|
|
*/
|
|
void forceClose() throws IOException {
|
|
if(!transactionsDisabled){
|
|
txnMgr.forceClose();
|
|
}
|
|
storage.forceClose();
|
|
}
|
|
|
|
/**
|
|
* Prints contents of a list
|
|
*/
|
|
private void showList(Iterator<PageIo> i) {
|
|
int cnt = 0;
|
|
while (i.hasNext()) {
|
|
System.out.println("elem " + cnt + ": " + i.next());
|
|
cnt++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchs a node to disk. This is called by the transaction manager's
|
|
* synchronization code.
|
|
*/
|
|
void synch(PageIo node) throws IOException {
|
|
ByteBuffer data = node.getData();
|
|
if (data != null) {
|
|
if(cipherIn!=null)
|
|
storage.write(node.getPageId(), ByteBuffer.wrap(Utils.encrypt(cipherIn, data)));
|
|
else
|
|
storage.write(node.getPageId(), data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Releases a node from the transaction list, if it was sitting
|
|
* there.
|
|
*/
|
|
void releaseFromTransaction(PageIo node)
|
|
throws IOException {
|
|
inTxn.remove(node.getPageId());
|
|
}
|
|
|
|
/**
|
|
* Synchronizes the file.
|
|
*/
|
|
void sync() throws IOException {
|
|
storage.sync();
|
|
}
|
|
|
|
public int getDirtyPageCount() {
|
|
return dirty.size();
|
|
}
|
|
|
|
public void deleteAllFiles() throws IOException {
|
|
storage.deleteAllFiles();
|
|
}
|
|
}
|