/* * Copyright 2009-2016 Tilmann Zaeschke. All rights reserved. * * This file is part of ZooDB. * * ZooDB 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, either version 3 of the License, or * (at your option) any later version. * * ZooDB 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 ZooDB. If not, see <http://www.gnu.org/licenses/>. * * See the README and COPYING files for further information. */ package org.zoodb.internal.server; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.zoodb.internal.util.Pair; import org.zoodb.internal.util.PrimLongMapLI; /** * Here we keep a history of modified objects in order to quickly detect conflicts. * * Deletes: * We consider double-deletion of objects as conflict (TODO check JDO spec) * Reasons: The object may have been recreated, for example by creating an object * with a dedicated OID or by changing the OID of an object. * * See: * http://etutorials.org/Programming/Java+data+objects/Chapter+15.+Optimistic+Transactions/15.1+Verification+at+Commit/ * --> They claim that * - Double delete should not fail * - a failure causes automatic rollback(), however a refresh() is still required ?!?!? * --> Depending on the restoreValues() flag. * * * Conflicting transactions: * Potentially conflicting transactions with a transaction X are any transactions that: * a) began before X committed * AND * b) committed after X began * In other words: a transaction can be removed if it ended before any CURRENTLY ACTIVE transaction * started. * * * @author Tilmann Zaeschke */ class TxManager { /** This stores a history of all objects that were modified or deleted in a transaction. */ private final PrimLongMapLI<ArrayList<TxObjInfo>> updateHistory = new PrimLongMapLI<>(); private final PrimLongMapLI<TxObjInfo> updateSummary = new PrimLongMapLI<>(); //TODO use CritBit tree? //Maps tx-end to tx-ID //Transactions are added in the order that they end. This means they are ordered by txEnd. private final LinkedList<Pair<Long, Long>> endedTx = new LinkedList<>(); private boolean isSingleSession = true; private final LinkedList<Long> activeTXs = new LinkedList<>(); private long latestTxId = -1; public TxManager(long txId) { this.latestTxId = txId; } /** * Add updates of a transaction to the history tree. * To be called when starting a new commit(). * @param txId * @param txContext * @return A list of conflicting objects or {@code null} if there are no conflicts */ synchronized List<Long> addUpdates(long txId, TxContext txContext, boolean isTrialRun) { if (isSingleSession) { //no need to record history return null; } ArrayList<TxObjInfo> updatesAndDeletes = txContext.getUpdatesAndDeletes(); //first, check for conflicts ArrayList<Long> conflicts = null; for (TxObjInfo clientInfo: updatesAndDeletes) { long oid = clientInfo.getOid(); long ots = clientInfo.getTS(); //At this point we should not ignore objects that are apparently new! //Why? Even if the object appears new, the OID may be in use, which //may present a conflict. TxObjInfo serverInfo = updateSummary.get(oid); //OLD: //Did the current transaction begin before the other was committed? //I.e. is the updateTimeStamp higher than the readTimeStamp of the current TX? //NEW: We just check whether the cached TS equals the expected TS. //If not, we have a conflict. Note, that the new timestamp may be LOWER than the //cached timestamp if the object was updated AFTER the current TX started, but //before the current TX first accessed the object. //System.out.println("TxM-au: " + oid + " ots=" + ots + " txTS=" + (serverInfo != null?serverInfo.getTS():"null")); if (serverInfo != null && serverInfo.getTxId() != ots) { if (clientInfo.isDeleted() && serverInfo.isDeleted()) { //okay, ignore //continue; //TODO For now we don't ignore these. To ignore these, we have to report back //so that there will be no attempts on updating any indexes. Furthermore, //it is not obvious that this is the right thing to do, because the TX may //semantically rely on having deleted an object, however the object is already //gone (for example if the number of deleted objects counts). } if (conflicts == null) { conflicts = new ArrayList<>(); } conflicts.add(oid); } } if (conflicts != null || isTrialRun) { return conflicts; } //apply updates updateHistory.put(txId, updatesAndDeletes); for (TxObjInfo info: updatesAndDeletes) { // +1 to ensure conflicts even with latest transaction info.setTxId(txId); updateSummary.put(info.getOid(), info); } //not very clean: 'null' indicates no conflicts. return null; } /** * Deregister the transaction. * To be called after commit() or rollback(). * @param txId */ synchronized void deRegisterTx(long txId) { activeTXs.remove(txId); if (isSingleSession) { //no need to record history return; } //cache only if there are active TXs if (activeTXs.isEmpty()) { updateHistory.clear(); updateSummary.clear(); return; } endedTx.add(new Pair<>(latestTxId, txId)); //drop all tx with END < min(active_start), i.e. drop all that ended before any of the //remaining active transactions started. long minOpenTx = activeTXs.getFirst(); while (!endedTx.isEmpty() && endedTx.getFirst().getA() < minOpenTx) { long beginID = endedTx.getFirst().getB(); ArrayList<TxObjInfo> allUpdates = updateHistory.get(beginID); if (allUpdates != null) { for (TxObjInfo info: allUpdates) { long oid = info.getOid(); long tx = updateSummary.get(oid).getTxId(); if (tx == beginID) { updateSummary.remove(oid); } } updateHistory.remove(beginID); } endedTx.removeFirst(); } } /** * To be called when opening a new transaction. * @return ID for the new TX */ synchronized long getNextTxId() { //this is synchronized to ensure correct ordering of the list. //alternatively we could use a sorted list, but this is probably not cheaper... ? latestTxId++; activeTXs.add(latestTxId); return latestTxId; } synchronized void setMultiSession() { isSingleSession = false; } synchronized int statsGetBufferedTxCount() { return updateHistory.size(); } synchronized int statsGetBufferedOidCount() { return updateSummary.size(); } }