/* * Copyright (C) 1998, 1999, 2000-2009, * * Arjuna Solutions Limited, * Newcastle upon Tyne, * Tyne and Wear, * UK. * * $Id: LogStore.java,v 1.4 2004/11/11 12:22:21 nmcl Exp $ */ package com.arjuna.ats.internal.arjuna.objectstore; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.SyncFailedException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Stack; import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean; import com.arjuna.ats.arjuna.common.Uid; import com.arjuna.ats.arjuna.exceptions.ObjectStoreException; import com.arjuna.ats.arjuna.logging.tsLogger; import com.arjuna.ats.arjuna.objectstore.StateStatus; import com.arjuna.ats.arjuna.objectstore.StateType; import com.arjuna.ats.arjuna.state.InputObjectState; import com.arjuna.ats.arjuna.state.OutputObjectState; import com.arjuna.ats.arjuna.utils.FileLock; import com.arjuna.ats.internal.arjuna.common.UidHelper; import com.arjuna.ats.internal.arjuna.objectstore.LogInstance.TransactionData; /** * This is the transaction log implementation. It is optimised for the typical * mode of the coordinator: write-once and never read or update. Reads or * updates occur only in the case of failures, which hopefully are rare; hence * the reason we optimise for the non-failure case. This does mean that recovery * may take longer than when using other log implementations. * * There are several implementations of this approach, some of which perform better * on one operating system than another. We may put them in to the source eventually * and make it clear for which OS combination they are best suited. However, this * implementation works well on all operating systems we have tested so is a good * default. * * @author Mark Little (mark@arjuna.com) * @version $Id: LogStore.java,v 1.4 2004/11/11 12:22:21 nmcl Exp $ * @since JTS 1.0. */ /** * Algorithm used: During normal execution of a transaction, we only ever write * and then remove the log entry; we never read it. Therefore, optimise for that * situation. The log continually builds up in size until a maximum capacity is * reached and in which case, we switch to another log. Meanwhile, the recovery * manager periodically runs through completed logs and removes those that are * no longer needed, truncating those that require recovery (which cannot * complete at this time). When writing the initial log entry, we write a * redzone marker, followed by the entry size and then the actual entry. Since a * log is never shared between VMs, we only need to synchronize between the * threads within a given VM: the recovery manager never works on a log that is * being used by another VM anyway. The end of a log is marked with a * termination record. Obviously if a crash occurs, then no such record will * have been written and in which case, the recovery manager determines that the * log is no longer required via timeout heuristics. * * The implementation normally writes removal records to the end of the log * when an entry is deleted. This can be disabled and in which case we end up in * the same situation as if a failure occurred as the removal record was being written * or a crash happened before remove_committed could succeed on any of the other * file-based object store implementations: we potentially try to commit transactions * that have terminated (either committed or rolled back). In which case we ... * * (i) call commit on a state that has already been committed and fail to do so. Will * eventually move the log record elsewhere and the administrator can deal with it. * * (ii) call commit on a state that has already been rolled back and again fail to do so. * Will eventually move the log record elsewhere as above. * * If we do not write removal records then we would end up in a situation of trying to * commit every log instance multiple times. As such we always try to write records but * do them either synchronously or asynchronously (periodically). Of course there's still * the chance that a failure will cause problems in both sync and async cases, but we * have reduced the probability as well as the number of such problem items. The periodicity * of this is the same as pruning the log, i.e., the same thread does both jobs. * * By default we synchronously add the removal marker to the log, i.e., when remove_committed * returns, the marker entry has been appended to the log. * * NOTE: there is a race where we terminate the log instance and yet transactions may * still be using it. This happens with other object store implementations too. However, in * this case we could end up with a log that should be deleted because all of the entries * have gone. We try to fix this up through allObjUids. If recovery works correctly then * these states will eventually get deleted. * * TODO * * When truncating logs we write a shadow and then overwrite the original with the shadow * when finished. If there is a crash we could end up with the shadow as well as the * original. Recovery could tidy this up for us - as long as we have the original then * we can continue to recover - the shadow instance may be corrupted so best to ignore * it and simply delete it. But we would need to ensure that we didn't delete a shadow that * is actually still active. * * Also we do not use a primary and backup log approach. Whenever we need a new log instance we * create one. This means that there could be many logs being used at the same time, which could * be a problem for disk space (unlikely these days, but possible). If this approach gets to * be an issue then we can limit the number of log instances created. */ /** * Represents a specific log instance. * * @author mlittle * */ class LogInstance { public class TransactionData { TransactionData (final Uid tx, final long off, final LogInstance parent) { txId = tx; offset = off; container = parent; } public final Uid txId; public final long offset; public final LogInstance container; } public LogInstance(String tn, long size) { _logName = new Uid(); _typeName = tn; _frozen = false; _totalSize = size; } /* * Once frozen we will not use the log again except for recovery and * pruning. * * We could consider another algorithm that reuses the log once it has * dropped below a threshold size. Probably not worth it at the moment. */ public final boolean isFrozen() { return _frozen; } public final void freeze() // one way operation. { _frozen = true; } public final int numberOfUsers() { return _transactions.size(); } public final Uid getName() { return _logName; } public final String getTypeName() { return _typeName; } public final InputObjectState allObjUids () throws ObjectStoreException { OutputObjectState state = new OutputObjectState(); Iterator<Uid> iter = _ids.keySet().iterator(); try { while (iter.hasNext()) { UidHelper.packInto(iter.next(), state); } // don't forget to null terminate UidHelper.packInto(Uid.nullUid(), state); } catch (final IOException ex) { throw new ObjectStoreException(ex); } return new InputObjectState(state); } public final boolean present(Uid id) { return _ids.containsKey(id); } public final TransactionData getTxId (Uid txId) { return new TransactionData(txId, _used, this); } public final TransactionData addTxId (Uid txId, long size) { TransactionData td = new TransactionData(txId, _used, this); _transactions.add(td); // allow multiple entries in the same log _ids.put(txId, txId); _used += size; return td; } public final long remaining() { return _totalSize - _used; } public final void resize (long size) { _totalSize = size; } public String toString() { return "LogInstance < " + _logName + ", " + _typeName + ", " + numberOfUsers() + ", " + remaining() + " >"; } private Uid _logName; private String _typeName; private boolean _frozen; private Stack<TransactionData> _transactions = new Stack<TransactionData>(); private HashMap<Uid, Uid> _ids = new HashMap<Uid, Uid>(); private long _used = 0; private long _totalSize; } /* * Time based, but it would be good to have it triggered on the number of * entries that need to be added. */ class LogPurger extends Thread { private enum Status {ACTIVE, PASSIVE, TERMINATED}; class LogElement { public LogElement(final String t, final Uid u, final int s) { tn = t; uid = u; state = s; } public String tn; public Uid uid; public int state; }; /* * Purge every N seconds. * * TODO purge after number of logs > M */ public static final long DEFAULT_PURGE_TIME = 100000; // 100 seconds public LogPurger(LogStore instance) { this(instance, DEFAULT_PURGE_TIME); } public LogPurger(LogStore instance, long purgeTime) { super("Log Purger"); _objStore = instance; _purgeTime = purgeTime; } public void addRemovedState(final Uid u, final String tn, final int state) { synchronized (_entries) { _entries.put(u, new LogElement(tn, u, state)); } } public void purge() { try { _objStore.truncateLogs(true); } catch (final Exception ex) { } } public void writeRemovalEntries() { synchronized (_entries) { if (_entries.size() > 0) { Collection<LogElement> entries = _entries.values(); Iterator<LogElement> iter = entries.iterator(); while (iter.hasNext()) { LogElement val = iter.next(); try { _objStore.removeState(val.uid, val.tn, val.state); } catch (final Exception ex) { // TODO log warning, but there's nothing else we can do. } } _entries.clear(); } } } /** * Poke the thread into doing some work even if it normally * would not. */ public void trigger () { synchronized (_lock) { if (_status == Status.PASSIVE) _lock.notify(); } } public void run() { for (;;) { // TODO activate thread during read and get it to write deleted states try { synchronized (_lock) { _status = Status.PASSIVE; _lock.wait(_purgeTime); } } catch (final Exception ex) { _status = Status.ACTIVE; } /* * Write any asynchronous delete records. */ writeRemovalEntries(); /* * Now truncate any logs we've been working on. */ try { _objStore.truncateLogs(); } catch (final Exception ex) { } } // _status = Status.TERMINATED; } private HashMap<Uid, LogElement> _entries = new HashMap<Uid, LogElement>(); private long _purgeTime; private LogStore _objStore; private Status _status; private Object _lock = new Object(); } class PurgeShutdownHook extends Thread { public PurgeShutdownHook(LogPurger purger) { _purger = purger; } public void run() { _purger.writeRemovalEntries(); // flush everything in the cache first. _purger.purge(); } private LogPurger _purger; } /* * Derive it directly from FSStore for now, simply because we are unlikely to * have many log instances in the store. However, if it becomes a problem, then * we can simply derive from the HashedActionStore. */ public class LogStore extends FileSystemStore { public static final long LOG_SIZE = 10 * 1024 * 1024; // default maximum log size in bytes private static final String FILE_MODE = "rwd"; /** * Normally returns the current state of the log entry. However, this is * never called during normal (non-recovery) execution. Therefore, the * overhead of having to scan all of the logs (if it's not one we're using) * is minimal. */ public int currentState(Uid objUid, String tName) throws ObjectStoreException { InputObjectState ios = new InputObjectState(); /* * TODO * * It's possible that the entry has been marked to be deleted but * that the removal entry hasn't been written yet. We could check the * async cache. However, since we really only care about this during * recovery, it's not going to cause us problems anyway. */ if (allObjUids(tName, ios, StateStatus.OS_UNKNOWN)) { Uid tempUid = new Uid(Uid.nullUid()); do { try { tempUid = UidHelper.unpackFrom(ios); } catch (final Exception ex) { ex.printStackTrace(); return StateStatus.OS_UNKNOWN; } if (tempUid.equals(objUid)) return StateStatus.OS_COMMITTED; } while (tempUid.notEquals(Uid.nullUid())); return StateStatus.OS_UNKNOWN; } else return StateStatus.OS_UNKNOWN; } /** * Commit a previous write_state operation which was made with the SHADOW * StateType argument. This is achieved by renaming the shadow and removing * the hidden version. */ public boolean commit_state(Uid objUid, String tName) throws ObjectStoreException { return true; } public boolean hide_state(Uid u, String tn) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.hide_state(" + u + ", " + tn + ")"); } return false; } public boolean reveal_state(Uid u, String tn) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.reveal_state(" + u + ", " + tn + ")"); } return false; } public InputObjectState read_uncommitted(Uid u, String tn) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.read_uncommitted(" + u + ", " + tn + ")"); } return null; } public boolean remove_uncommitted(Uid u, String tn) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.remove_uncommitted(" + u + ", " + tn + ")"); } return false; } public boolean write_committed(Uid storeUid, String tName, OutputObjectState state) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.write_committed(" + storeUid + ", " + tName + ")"); } try { return super.write_committed(storeUid, tName, state); } catch (ObjectStoreException ex) { removeFromLog(storeUid); throw ex; } } public boolean write_uncommitted(Uid u, String tn, OutputObjectState s) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("LogStore.write_uncommitted(" + u + ", " + tn + ", " + s + ")"); } return false; } public boolean allLogUids (String tName, InputObjectState state, int match) throws ObjectStoreException { return super.allObjUids(tName, state, match); } /** * This is a recovery-only method and should not be called during normal * execution. As such we need to load in all of the logs we can find that * aren't already loaded (or activated). */ public boolean allObjUids(String tName, InputObjectState state, int match) throws ObjectStoreException { /* * match will always be OS_COMMITTED since that's all we ever write for * the logs. */ // in case of asynchronous removals trigger the purger now. _purger.trigger(); /* * Get a list of logs. Load them in to memory if we aren't already * working on them/it. But we can prune the entry once we're * finished or the memory footprint will grow. We should do this * for all frozen entries eventually too. */ InputObjectState logs = new InputObjectState(); OutputObjectState objUids = new OutputObjectState(); /* * We never call this method except during recovery. As such we shouldn't * need to worry about optimizations such as checking whether or not the * log is in current working memory. */ if (!super.allObjUids(tName, logs, match)) return false; else { /* * Now we have all of the log names let's attach to each one * and locate the committed instances (not deleted.) */ Uid logName = new Uid(Uid.nullUid()); try { do { logName = UidHelper.unpackFrom(logs); if (logName.notEquals(Uid.nullUid())) { /* * Could check to see if log is in current working memory. */ /* * TODO * * First purge the log if we can, but we need to know that * we're not playing with an instance that is being manipulated * from another VM instance. */ ArrayList<InputObjectState> txs = scanLog(logName, tName); if (txs.size() > 0) { for (int i = 0; i < txs.size(); i++) { UidHelper.packInto(txs.get(i).stateUid(), objUids); } } } } while (logName.notEquals(Uid.nullUid())); // remember null terminator UidHelper.packInto(Uid.nullUid(), objUids); state.setBuffer(objUids.buffer()); } catch (final IOException ex) { ex.printStackTrace(); return false; } return true; } } public LogStore(ObjectStoreEnvironmentBean objectStoreEnvironmentBean) throws ObjectStoreException { super(objectStoreEnvironmentBean); // overrides parents use of isObjectStoreSync doSync = objectStoreEnvironmentBean.isTransactionSync(); _synchronousRemoval = objectStoreEnvironmentBean.isSynchronousRemoval(); _purgeTime = objectStoreEnvironmentBean.getPurgeTime(); _maxFileSize = objectStoreEnvironmentBean.getTxLogSize(); _purger = new LogPurger(this, _purgeTime); _purger.setDaemon(true); Runtime.getRuntime().addShutdownHook(new PurgeShutdownHook(_purger)); _purger.start(); } /** * Unlock and close the file. Note that if the unlock fails we set the * return value to false to indicate an error but rely on the close to * really do the unlock. */ protected boolean unlockAndClose(File fd, RandomAccessFile rf) { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("RandomAccessFile.unlockAndClose(" + fd + ", " + rf + ")"); } boolean closedOk = unlock(fd); try { rf.close(); } catch (Exception e) { closedOk = false; } return closedOk; } /** * write_state saves the ObjectState in a file named by the type and Uid of * the ObjectState. If the second argument is SHADOW, then the file name is * different so that a subsequent commit_state invocation will rename the * file. * * We need to make sure that each entry is written to the next empty location * in the log even if there's already an entry for this tx. */ protected boolean write_state(Uid objUid, String tName, OutputObjectState state, int ft) throws ObjectStoreException { if (tsLogger.logger.isTraceEnabled()) { tsLogger.logger.trace("ShadowingStore.write_state(" + objUid + ", " + tName + ", " + StateType.stateTypeString(ft) + ")"); } String fname = null; File fd = null; if (tName != null) { int imageSize = (int) state.length(); byte[] uidString = objUid.stringForm().getBytes(StandardCharsets.UTF_8); int buffSize = _redzone.length + uidString.length + imageSize + 8; // don't put in endOfLog since we keep overwriting that. RandomAccessFile ofile = null; java.nio.channels.FileLock lock = null; if (imageSize > 0) { TransactionData theLogEntry = getLogName(objUid, tName, buffSize); // always adds entry to log LogInstance theLog = theLogEntry.container; if (theLog == null) throw new ObjectStoreException(); fname = genPathName(theLog.getName(), tName, ft); fd = openAndLock(fname, FileLock.F_WRLCK, true); if (fd == null) { tsLogger.i18NLogger.warn_objectstore_ShadowingStore_18(fname); return false; } boolean setLength = !fd.exists(); try { ofile = new RandomAccessFile(fd, FILE_MODE); if (setLength) { ofile.setLength(_maxFileSize); } else { // may have to resize file if we keep updating this transaction info if (theLog.remaining() < buffSize) { long size = ofile.length() + buffSize - theLog.remaining(); ofile.setLength(size); theLog.resize(size); } } java.nio.ByteBuffer buff = java.nio.ByteBuffer.allocate(buffSize); buff.put(_redzone); buff.putInt(uidString.length); buff.put(uidString); buff.putInt(imageSize); buff.put(state.buffer()); synchronized (_lock) { ofile.seek(theLogEntry.offset); ofile.write(buff.array()); } } catch (SyncFailedException e) { unlockAndClose(fd, ofile); throw new ObjectStoreException( "ShadowingStore::write_state() - write failed to sync for " + fname, e); } catch (FileNotFoundException e) { unlockAndClose(fd, ofile); e.printStackTrace(); throw new ObjectStoreException( "ShadowingStore::write_state() - write failed to locate file " + fname + ": " + e, e); } catch (IOException e) { unlockAndClose(fd, ofile); e.printStackTrace(); throw new ObjectStoreException( "ShadowingStore::write_state() - write failed for " + fname + ": " + e, e); } finally { try { if (lock != null) lock.release(); } catch (IOException ex) { ex.printStackTrace(); } } } if (!unlockAndClose(fd, ofile)) { tsLogger.i18NLogger.warn_objectstore_ShadowingStore_19(fname); } super.addToCache(fname); return true; } else throw new ObjectStoreException( "ShadowStore::write_state - " + tsLogger.i18NLogger.get_objectstore_notypenameuid() + objUid); } /** * Shouldn't be called during normal execution only during recovery. */ protected InputObjectState read_state(Uid u, String tn, int s) throws ObjectStoreException { /* * In case of asynchronous removals of state, let's trigger the purger * thread to flush its cache now. Try to avoid false positives during * recovery wherever possible! */ _purger.trigger(); /* * It's possible that recovery got hold of a state id while it was * being deleted (marker written and pruning thread not yet active). * In which case when it comes to do a read it's not going to find * the state there any longer. Conversely it's possible that it could do * a read on a state that is about to be deleted. Recovery should be * able to cope with these edge cases. */ TransactionData td = getLogName(u, tn, -1); if (td == null) throw new ObjectStoreException(); ArrayList<InputObjectState> states = scanLog(td.container.getName(), tn); if ((states == null) || (states.size() == 0)) return null; for (int i = 0; i < states.size(); i++) { if (states.get(i).stateUid().equals(u)) return states.get(i); } /* * Not in the log, so probably removed by now. */ return null; } /** * Does nothing except indicate that this thread is finished with the log on * behalf of this transaction. */ protected boolean remove_state(Uid u, String tn, int s) throws ObjectStoreException { // maybe write a removal entry into the log. try { /* * If we don't add a removal entry then recovery has to work a * little harder to figure things out. But it has to cater for the * situation where a removal record write fails anyway, so this * shouldn't be a big deal. On the up side it improves performance * by 30% for this implementation, which is a 40% improvement over * the basic file-based log! */ /* * If we write a removal record as a separate entity to the original * data item then we cannot ensure that they will go into the same * log with a pre-set size for the log. Therefore, we have two * options: * * (i) find the old entry in the log and mark it as deleted. * (ii) increase the size of the log to accommodate the removal entry. * * We currently go for option (ii) as this is the quickest. */ if (_synchronousRemoval) { OutputObjectState removalState = new OutputObjectState(u, tn); removalState.packBytes(_removedState); if (!write_state(u, tn, removalState, s)) throw new ObjectStoreException(); } else _purger.addRemovedState(u, tn, s); } catch (IOException ex) { throw new ObjectStoreException(ex.toString(), ex); } catch (final Throwable ex) { ex.printStackTrace(); throw new ObjectStoreException(ex.toString(), ex); } finally { removeFromLog(u); } return true; } protected boolean lock(File fd, int lmode, boolean create) { return true; } protected boolean unlock(File fd) { return true; } protected String genPathName (Uid objUid, String tName, int ft) throws ObjectStoreException { String fname = super.genPathName(objUid, tName, ft); if (ft == StateStatus.OS_UNCOMMITTED) fname = fname + HIDDENCHAR; return fname; } boolean removeState(Uid u, String tn, int s) throws ObjectStoreException { try { OutputObjectState removalState = new OutputObjectState(u, tn); removalState.packBytes(_removedState); if (!write_state(u, tn, removalState, s)) throw new ObjectStoreException(); } catch (IOException ex) { throw new ObjectStoreException(ex.toString(), ex); } return true; } boolean truncateLogs () throws ObjectStoreException { return truncateLogs(false); } boolean truncateLogs (boolean force) throws ObjectStoreException { synchronized (_logNames) { Iterator<LogInstance> iter = _logNames.iterator(); /* * Only do this for logs that are full to save time, * except if we are terminating. */ while (iter.hasNext()) { boolean delete = false; LogInstance log = null; try { log = iter.next(); if (log.isFrozen() || force) delete = truncateLog(log, force); } catch (final Exception ex) { // TODO log } if (delete) iter.remove(); } } return true; } /* * Return true if the log needs to be deleted. */ private final boolean truncateLog(final LogInstance log, boolean force) throws ObjectStoreException { boolean delete = false; synchronized (_lock) { File fd = new File(genPathName(log.getName(), log.getTypeName(), StateStatus.OS_COMMITTED)); try { /* * Create a list of ObjectState entries. */ ArrayList<InputObjectState> objectStates = scanLog(log.getName(), log.getTypeName()); /* * At this stage we should now have a list of unique * entries. Write them back to the log. Do this * atomically! If the list is empty then delete the * file! */ if ((objectStates != null) && (objectStates.size() > 0)) { /* * If we are terminating then we can truncate the log to the * real size needed to contain the existing entries since we * will not use it again within another VM except for * recovery purposes. */ String fname = genPathName(log.getName(), log.getTypeName(), StateStatus.OS_UNCOMMITTED); File fd2 = openAndLock(fname, FileLock.F_WRLCK, true); RandomAccessFile oFile = new RandomAccessFile(fd2, FILE_MODE); int size = 0; oFile.setLength(_maxFileSize); for (int i = 0; i < objectStates.size(); i++) { byte[] uidString = objectStates.get(i).stateUid().stringForm().getBytes(StandardCharsets.UTF_8); int buffSize = _redzone.length + uidString.length + objectStates.get(i).buffer().length + 8; java.nio.ByteBuffer buff = java.nio.ByteBuffer.allocate(buffSize); size += buffSize; try { buff.put(_redzone); buff.putInt(uidString.length); buff.put(uidString); buff.putInt(objectStates.get(i).buffer().length); buff.put(objectStates.get(i).buffer(),0, objectStates.get(i).buffer().length); } catch (final Exception ex) { ex.printStackTrace(); // TODO log fd2.delete(); unlockAndClose(fd2, oFile); throw new ObjectStoreException(ex.toString(), ex); } } try { if (force) { oFile.setLength(size); log.freeze(); } fd2.renameTo(fd); } catch (final Exception ex) { ex.printStackTrace(); // TODO log throw new ObjectStoreException(ex.toString(), ex); } finally { unlockAndClose(fd2, oFile); } } else { /* * Delete the log if there are no states in it. We could * keep the file around and reuse it, but the advantage of * this is small compared to having to cope with reusing old * log instances. */ fd.delete(); /* * Remember to remove the information from the memory cache. */ delete = true; } } catch (final ObjectStoreException ex) { ex.printStackTrace(); throw ex; } catch (final Exception ex) { ex.printStackTrace(); throw new ObjectStoreException(ex.toString(), ex); } } return delete; } private final ArrayList<InputObjectState> scanLog (final Uid logName, final String typeName) throws ObjectStoreException { /* * Make sure no new entries can be created while we scan. */ synchronized (_lock) { try { String fname = genPathName(logName, typeName, StateStatus.OS_COMMITTED); File fd = openAndLock(fname, FileLock.F_WRLCK, true); RandomAccessFile iFile = new RandomAccessFile(fd, FILE_MODE); // iFile.getChannel().lock(); try { /* * Create a list of ObjectState entries. */ ArrayList<InputObjectState> objectStates = new ArrayList<InputObjectState>(); iFile.seek(0); // make sure we're at the start while (iFile.getFilePointer() < iFile.length()) { byte[] buff = new byte[_redzone.length]; iFile.read(buff); if (!redzoneProtected(buff)) { // end break; /* * TODO add an end-of-log entry and check for that. Currently just assume * that no RZ means end, rather than corruption. */ } else { int uidSize = iFile.readInt(); byte[] uidString = new byte[uidSize]; iFile.read(uidString); Uid txId = new Uid(new String(uidString, StandardCharsets.UTF_8)); int imageSize = iFile.readInt(); byte[] imageState = new byte[imageSize]; iFile.read(imageState); try { InputObjectState state = new InputObjectState( txId, "", imageState); objectStates.add(state); } catch (final Exception ex) { ex.printStackTrace(); throw new ObjectStoreException(ex.toString(), ex); } } } unlockAndClose(fd, iFile); iFile = null; /* * At this stage we now have a list of ObjectState entries. * Now we need to go through and prune the list. This is * complicated by the fact that there can be 1.. entries for * a specific transaction since we continually update the * log as we drive recovery. If an entry hasn't been deleted * then we will keep the latest one we find. */ /* * First search for those entries that have been deleted. */ ArrayList<InputObjectState> deletedLogs = new ArrayList<InputObjectState>(); for (int i = 0; i < objectStates.size(); i++) { InputObjectState curr = objectStates.get(i); try { if (Arrays.equals(curr.unpackBytes(), _removedState)) { deletedLogs.add(curr); } else curr.reread(); // don't forget to reset the read pointer! } catch (final Exception ex) { // if not a delete record then the first entry won't // be an the defined byte array. curr.reread(); // don't forget to reset the read pointer! } } if (deletedLogs.size() > 0) { /* * make sure we remove them from the first list to save time. */ objectStates.removeAll(deletedLogs); deleteEntries(objectStates, deletedLogs); /* * At this stage we should only have entries that refer * to in-flight transactions. Go through the list and * remove N-1 references for each transaction id. */ pruneEntries(objectStates); /* * Now return the list of committed entries. */ return objectStates; } else return objectStates; } finally { if (iFile != null) unlockAndClose(fd, iFile); } } catch (final ObjectStoreException ex) { ex.printStackTrace(); throw ex; } catch (final Exception ex) { ex.printStackTrace(); throw new ObjectStoreException(ex.toString(), ex); } } } private final boolean redzoneProtected(final byte[] buff) { for (int i = 0; i < _redzone.length; i++) { if (buff[i] != _redzone[i]) return false; } return true; } private final void deleteEntries(ArrayList<InputObjectState> allStates, ArrayList<InputObjectState> deletedStates) { /* * Look through the remaining states for entries that have been deleted. */ for (int i = 0; i < deletedStates.size(); i++) { Uid txId = deletedStates.get(i).stateUid(); for (int j = 0; j < allStates.size(); j++) { if (allStates.get(j).stateUid().equals(txId)) allStates.remove(j); } } deletedStates.clear(); } private final void pruneEntries(ArrayList<InputObjectState> allStates) { /* * The ArrayList is ordered with the earliest entries first. */ for (int j = allStates.size() - 1; j >= 0; j--) { Uid txId = allStates.get(j).stateUid(); for (int i = 0; i < j; i++) { if (allStates.get(i).stateUid().equals(txId)) allStates.remove(i); } } } /* * We maintain a list of log identifiers and the number of threads using * them. If a log size goes over the maximum allowed, then we swap all * threads to a new log with the exception of those that are currently using * the old log. * * We always add a new entry to the log even if one already exists. * * Because normally we are writing to the log we pass in the size that we need to * accommodate. However, during recovery we need to read the state yet still * need the log name. So if we pass a size of -1 this signifies only to * return the log data and not allocate space for a new instance. */ private final TransactionData getLogName (Uid txid, String tName, long size) throws ObjectStoreException { synchronized (_logNames) { Iterator<LogInstance> iter = _logNames.iterator(); LogInstance entry = null; /* * First check to see if the TxId is in an existing log. Always * return the same log instance for the same txid so we can * keep all data in the same location. This may mean that we have * to extend the size of the log over time to accommodate situations * where the log is modified but not deleted for a while, e.g., during * recovery. */ while (iter.hasNext()) { entry = (LogInstance) iter.next(); if (entry.present(txid)) { if (size == -1) // we are reading only return entry.getTxId(txid); else return entry.addTxId(txid, size); } } /* * If we get here then this TxId isn't in one of the * logs we are maintaining currently. So go back through * the list of logs and find one that is small enough * for us to use. The first one with room will do. */ iter = _logNames.iterator(); while (iter.hasNext()) { entry = (LogInstance) iter.next(); if (!entry.isFrozen()) { if (entry.remaining() > size) { return entry.addTxId(txid, size); } else { /* * TODO * * When can we remove the information about this * log from memory? If we do it too soon then it's possible * that delete entries will not go into the right log. If we * leave it too late then the memory footprint increases. Prune * the entry when we prune the log from disk? */ entry.freeze(); } } } // if we get here, then we need to create a new log entry = new LogInstance(tName, _maxFileSize); _logNames.add(entry); return entry.addTxId(txid, size); } } private final void removeFromLog(Uid txid) { if (_synchronousRemoval) { synchronized (_logNames) { Iterator<LogInstance> iter = _logNames.iterator(); LogInstance entry = null; while (iter.hasNext()) { entry = (LogInstance) iter.next(); if (entry.present(txid)) { //entry.removeTxId(txid); break; } } } } } private static Object _lock = new Object(); private static ArrayList<LogInstance> _logNames = new ArrayList<LogInstance>(); private final long _maxFileSize; private final long _purgeTime; private final LogPurger _purger; private final boolean _synchronousRemoval; private static final byte[] _redzone = { 0x2, 0x4, 0x6, 0x8 }; private static final byte[] _removedState = { 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf }; private static final char HIDDENCHAR = '~'; }