/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.engine;
import java.util.ArrayList;
import java.util.HashMap;
import org.h2.constant.SysProperties;
import org.h2.message.DbException;
import org.h2.store.Data;
import org.h2.store.FileStore;
import org.h2.table.Table;
import org.h2.util.New;
/**
* Each session keeps a undo log if rollback is required.
*/
public class UndoLog {
private Database database;
private ArrayList<Long> storedEntriesPos = New.arrayList();
private ArrayList<UndoLogRecord> records = New.arrayList();
private FileStore file;
private Data rowBuff;
private int memoryUndo;
private int storedEntries;
private HashMap<Integer, Table> tables;
private boolean largeTransactions;
/**
* Create a new undo log for the given session.
*
* @param session the session
*/
UndoLog(Session session) {
this.database = session.getDatabase();
largeTransactions = database.getSettings().largeTransactions;
}
/**
* Get the number of active rows in this undo log.
*
* @return the number of rows
*/
int size() {
if (largeTransactions) {
return storedEntries + records.size();
}
if (SysProperties.CHECK && memoryUndo > records.size()) {
DbException.throwInternalError();
}
return records.size();
}
/**
* Clear the undo log. This method is called after the transaction is
* committed.
*/
void clear() {
records.clear();
storedEntries = 0;
storedEntriesPos.clear();
memoryUndo = 0;
if (file != null) {
file.closeAndDeleteSilently();
file = null;
rowBuff = null;
}
}
/**
* Get the last record and remove it from the list of operations.
*
* @return the last record
*/
public UndoLogRecord getLast() {
int i = records.size() - 1;
if (largeTransactions) {
if (i < 0 && storedEntries > 0) {
int last = storedEntriesPos.size() - 1;
long pos = storedEntriesPos.get(last);
storedEntriesPos.remove(last);
long end = file.length();
int bufferLength = (int) (end - pos);
Data buff = Data.create(database, bufferLength);
file.seek(pos);
file.readFully(buff.getBytes(), 0, bufferLength);
while (buff.length() < bufferLength) {
UndoLogRecord e = UndoLogRecord.loadFromBuffer(buff, this);
records.add(e);
memoryUndo++;
}
storedEntries -= records.size();
file.setLength(pos);
file.seek(pos);
}
i = records.size() - 1;
}
UndoLogRecord entry = records.get(i);
if (entry.isStored()) {
int start = Math.max(0, i - database.getMaxMemoryUndo() / 2);
UndoLogRecord first = null;
for (int j = start; j <= i; j++) {
UndoLogRecord e = records.get(j);
if (e.isStored()) {
e.load(rowBuff, file, this);
memoryUndo++;
if (first == null) {
first = e;
}
}
}
for (int k = 0; k < i; k++) {
UndoLogRecord e = records.get(k);
e.invalidatePos();
}
seek(first.getFilePos());
}
return entry;
}
/**
* Go to the right position in the file.
*
* @param filePos the position in the file
*/
void seek(long filePos) {
file.seek(filePos * Constants.FILE_BLOCK_SIZE);
}
/**
* Remove the last record from the list of operations.
*
* @param trimToSize if the undo array should shrink to conserve memory
*/
void removeLast(boolean trimToSize) {
int i = records.size() - 1;
UndoLogRecord r = records.remove(i);
if (!r.isStored()) {
memoryUndo--;
}
if (trimToSize && i > 1024 && (i & 1023) == 0) {
records.trimToSize();
}
}
/**
* Append an undo log entry to the log.
*
* @param entry the entry
*/
void add(UndoLogRecord entry) {
records.add(entry);
if (largeTransactions) {
memoryUndo++;
if (memoryUndo > database.getMaxMemoryUndo() && database.isPersistent() && !database.isMultiVersion()) {
if (file == null) {
String fileName = database.createTempFile();
file = database.openFile(fileName, "rw", false);
file.setCheckedWriting(false);
file.setLength(FileStore.HEADER_LENGTH);
}
Data buff = Data.create(database, Constants.DEFAULT_PAGE_SIZE);
for (int i = 0; i < records.size(); i++) {
UndoLogRecord r = records.get(i);
buff.checkCapacity(Constants.DEFAULT_PAGE_SIZE);
r.append(buff, this);
if (i == records.size() - 1 || buff.length() > Constants.UNDO_BLOCK_SIZE) {
storedEntriesPos.add(file.getFilePointer());
file.write(buff.getBytes(), 0, buff.length());
buff.reset();
}
}
storedEntries += records.size();
memoryUndo = 0;
records.clear();
file.autoDelete();
return;
}
} else {
if (!entry.isStored()) {
memoryUndo++;
}
if (memoryUndo > database.getMaxMemoryUndo() && database.isPersistent() && !database.isMultiVersion()) {
if (file == null) {
String fileName = database.createTempFile();
file = database.openFile(fileName, "rw", false);
file.setCheckedWriting(false);
file.seek(FileStore.HEADER_LENGTH);
rowBuff = Data.create(database, Constants.DEFAULT_PAGE_SIZE);
Data buff = rowBuff;
for (int i = 0; i < records.size(); i++) {
UndoLogRecord r = records.get(i);
saveIfPossible(r, buff);
}
} else {
saveIfPossible(entry, rowBuff);
}
file.autoDelete();
}
}
}
private void saveIfPossible(UndoLogRecord r, Data buff) {
if (!r.isStored() && r.canStore()) {
r.save(buff, file, this);
memoryUndo--;
}
}
/**
* Get the table id for this undo log. If the table is not registered yet,
* this is done as well.
*
* @param table the table
* @return the id
*/
int getTableId(Table table) {
int id = table.getId();
if (tables == null) {
tables = New.hashMap();
}
// need to overwrite the old entry, because the old object
// might be deleted in the meantime
tables.put(id, table);
return id;
}
/**
* Get the table for this id. The table must be registered for this undo log
* first by calling getTableId.
*
* @param id the table id
* @return the table object
*/
Table getTable(int id) {
return tables.get(id);
}
}