/* GASH 2 DBEditSet.java The GANYMEDE object storage system. Created: 2 July 1996 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program 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 2 of the License, or (at your option) any later version. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.Vector; import arlut.csd.Util.NamedStack; import arlut.csd.Util.TranslationService; import arlut.csd.Util.VectorUtils; import arlut.csd.ganymede.common.ErrorTypeEnum; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.ObjectStatus; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.scheduleHandle; import arlut.csd.ganymede.common.SchemaConstants; /*------------------------------------------------------------------------------ class DBEditSet ------------------------------------------------------------------------------*/ /** * <p>DBEditSet is the basic transactional unit. All changes to the * database during normal operations are made in the context of a * DBEditSet, which may then be committed or rolled back as an atomic * operation. Each {@link arlut.csd.ganymede.server.DBSession * DBSession} will have at most one DBEditSet transaction object * active at any time.</p> * * <p>A DBEditSet tracks several things for the server, including * instances of {@link arlut.csd.ganymede.server.DBEditObject * DBEditObject}'s that were created or checked-out from the {@link * arlut.csd.ganymede.server.DBStore DBStore}, {@link * arlut.csd.ganymede.server.DBNameSpace DBNameSpace} values that were * reserved during the course of the transaction, and {@link * arlut.csd.ganymede.server.DBLogEvent DBLogEvent} objects to be * recorded in the {@link arlut.csd.ganymede.server.DBLog DBLog} * and/or mailed out to various interested parties when the * transaction is committed.</p> * * <p>DBEditSet's transaction logic is based on a two-phase commit * protocol, where all DBEditObject's involved in the transaction are * given an initial opportunity to approve or reject the transaction's * commit before the DBEditSet commit method goes back and 'locks-in' * the changes. DBEditObjects are able to initiate changes external * to the Ganymede database in their {@link * arlut.csd.ganymede.server.DBEditObject#commitPhase2() * commitPhase2()} methods, if needed.</p> * * <p>When a DBEditSet is committed, a {@link * arlut.csd.ganymede.server.DBWriteLock DBWriteLock} is established * on all {@link arlut.csd.ganymede.server.DBObjectBase * DBObjectBase}'s involved in the transaction. All objects checked * out by that transaction are then updated in the DBStore, and a * summary of changes is recorded to the DBStore {@link * arlut.csd.ganymede.server.DBJournal DBJournal}. The database as a * whole will not be dumped to disk unless and until the {@link * arlut.csd.ganymede.server.dumpTask dumpTask} is run, or until the * server undergoes a formal shutdown.</p> * * <p>Typically, the {@link * arlut.csd.ganymede.server.DBEditSet#commit(java.lang.String) * commit()} method is called by the {@link * arlut.csd.ganymede.server.GanymedeSession#commitTransaction(boolean) * GanymedeSession.commitTransaction()} method, which will induce the * server to schedule any commit-time build tasks registered with the * {@link arlut.csd.ganymede.server.GanymedeScheduler * GanymedeScheduler}.</p> * * <p>If a DBEditSet commit() operation fails catastrophically, or if * {@link arlut.csd.ganymede.server.DBEditSet#abort() abort()} is * called, all DBEditObjects created or checked out during the course * of the transaction will be discarded, all {@link * arlut.csd.ganymede.server.DBNameSpace DBNameSpace} values allocated * will be relinquished, and any logging information for the abandoned * transaction will be forgotten.</p> * * <p>As if all that wasn't enough, the DBEditSet class also maintains * a stack of {@link arlut.csd.ganymede.server.DBCheckPoint * DBCheckPoint} objects to enable users to set checkpoints during the * course of a transaction. These objects are basically a snapshot of * the transaction's state at the moment of the checkpoint, and are * used to rollback the transaction to a known state if a series of * linked operations within a transaction cannot all be completed.</p> * * <p>Finally, note that the DBEditSet class does not actually track * namespace value allocations.. instead, the {@link * arlut.csd.ganymede.server.DBNameSpace DBNameSpace} class is * responsible for recording a list of values allocated by each active * DBEditSet. When a DBEditSet commits or releases, all {@link * arlut.csd.ganymede.server.DBNameSpace DBNameSpace} objects in the * server are informed of this, whereupon they do their own * cleanup.</p> */ public final class DBEditSet { /** * <p>TranslationService object for handling string localization in * the Ganymede server.</p> */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBEditSet"); /** * <p>Maps Invids to {@link arlut.csd.ganymede.server.DBEditObject * DBEditObject}s checked out in care of this transaction.</p> */ private Map<Invid, DBEditObject> objects = null; /** * <p>A List of {@link arlut.csd.ganymede.server.DBLogEvent DBLogEvent}'s * to be written to the Ganymede logfile and/or mailed out when * this transaction commits.</p> */ private List<DBLogEvent> logEvents = null; /** * <p>A record of the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase}'s * touched by this transaction. These DBObjectBase's will be locked * when this transaction is committed.</p> */ private Set<DBObjectBase> basesModified = null; /** * Who's our daddy? */ DBStore dbStore; /** * A reference to the DBSession that this transaction is attached to. */ DBSession session; /** * A brief description of the client associated with this * transaction, used in logging to identify what was done by the * main client, what by a password-changing utility, etc. */ String description; /** * Will be true if we are running this transaction on behalf of an * XML client and the server was started with the -historyOverride * command line parameter. */ private boolean allowXMLHistoryOverride = false; /** * A stack of {@link arlut.csd.ganymede.server.DBCheckPoint DBCheckPoint} objects * to keep track of check points performed during the course of this transaction. */ private NamedStack<DBCheckPoint> checkpoints = new NamedStack<DBCheckPoint>(); /** * <p>The writelock acquired during the course of a commit attempt. We keep * this around as a DBEditSet field so that we can use the handy * {@link arlut.csd.ganymede.server.DBEditSet#releaseWriteLock() releaseWriteLock()} * method, but wLock should really never be non-null outside of the * context of the commit() call.</p> * * <p>As long as the writeLock is held, no other transactions can * proceed into commit on the same object bases.</p> */ private DBWriteLock wLock = null; /** * <p>True if this DBEditSet is operating in interactive mode.</p> */ private boolean interactive; /** * <p>True if this DBEditSet is operating in non-interactive mode * and a rollback was ordered. In such cases, the server skipped * doing the checkpoint and so has no choice but to condemn the * whole transaction.</p> */ private boolean mustAbort = false; /** * <p>This DBJournalTransaction is used to remember information * about a transaction that we have persisted to the on-disk * journal, but which we have not yet finalized.</p> */ private DBJournalTransaction persistedTransaction = null; /** * A comment to attach to logging and email generated in response to * this transaction. */ private String comment = null; /* -- */ /** * Constructor for DBEditSet * * @param dbStore The owning DBStore object. * @param session The DBStore session owning this transaction. * @param description An optional string to identify this transaction * @param interactive If false, this transaction will operate in * non-interactive mode. Certain Invid operations will be optimized * to avoid doing choice list queries and bind checkpoint * operations. When a transaction is operating in non-interactive mode, * any failure that cannot be handled cleanly due to the optimizations will * result in the transaction refusing to commit when commitTransaction() * is attempted. This mode is intended for batch operations. * */ public DBEditSet(DBStore dbStore, DBSession session, String description, boolean interactive) { this.session = session; this.dbStore = dbStore; this.description = description; this.interactive = interactive; this.objects = Collections.synchronizedMap(new HashMap<Invid, DBEditObject>()); this.logEvents = Collections.synchronizedList(new ArrayList<DBLogEvent>()); this.basesModified = new HashSet<DBObjectBase>(dbStore.bases().size()); if (session.GSession != null && session.GSession.isXMLSession() && Ganymede.allowMagicImport) { this.allowXMLHistoryOverride = true; } } /** * <p>Method to return the DBSession handle owning this * transaction.</p> * * <p>Semi-deprecated. Use getDBSession() instead for clarity.</p> */ public DBSession getSession() { return session; } /** * <p>Method to return the DBSession handle owning this * transaction.</p> */ public DBSession getDBSession() { return session; } /** * <p>Returns the user-level GanymedeSession object associated with this * transaction, or null if there is no GanymedeSession associated.</p> */ public final GanymedeSession getGSession() { if (this.session == null) { return null; } return session.getGSession(); } /** * <p>Returns the descriptive string passed to us when this * transaction was opened.</p> */ public final String getDescription() { return this.description; } /** * <p>Returns the name of the GanymedeSession user who created this * transaction.</p> * * <p>If this transaction was created by an admin persona, we will * return that persona name, otherwise we'll return the user * name.</p> */ public final String getUsername() { GanymedeSession gSession = this.getGSession(); if (gSession == null) { return null; } return gSession.getPermManager().getIdentity(); } /** * <p>Returns true if the GanymedeSession associated with this * transaction has oversight turned on.</p> */ public final boolean isOversightOn() { return (getGSession() != null && getGSession().enableOversight); } /** * <p>This method returns true if this transaction is being carried * out by an interactive client.</p> */ public boolean isInteractive() { return interactive; } /** * <p>To allow the GanymedeSession to get a copy of our object hash.</p> */ public Map<Invid, DBEditObject> getObjectHashClone() { synchronized (objects) { return new HashMap<Invid,DBEditObject>(objects); } } /** * <p>Return a list of objects that we are currently working on.</p> */ public DBEditObject[] getObjectList() { return this.objects.values().toArray(new DBEditObject[0]); } /** * <p>Method to find a DBObject / DBEditObject if it has * previously been checked out to this EditSet in some * fashion.</p> */ public DBEditObject findObject(DBObject object) { return findObject(object.getInvid()); } /** * <p>Method to find a DBObject / DBEditObject if it has * previously been checked out to this EditSet in some * fashion. This method is used to allow consistency * check code in the DBEditObjects to get a transaction * consistent view of the system as it stands with the * transaction's changes made.</p> */ public DBEditObject findObject(Invid invid) { return this.objects.get(invid); } /** * <p>This method returns true if the invid parameter * has been checked out for editing by this transaction.</p> */ public boolean isEditingObject(Invid invid) { return this.objects.containsKey(invid); } /** * <p>Method to associate a DBEditObject with this transaction.</p> * * <p>This method is called by the createDBObject and editDBObject * methods in {@link arlut.csd.ganymede.server.DBSession DBSession}.</p> * * @param object The newly created DBEditObject. */ public synchronized boolean addObject(DBEditObject object) { // if this transaction is in the middle of commit(), don't let the // programmer try to corrupt the transaction by adding new objects // to this transaction. Gaurav, this means you. ;-) if (wLock != null) { throw new RuntimeException(ts.l("addObject.cant_add")); } // remember that we are not allowing objects that this object is // pointing to via an asymmetric link to be deleted.. make sure // that no one has already deleted an object we're pointing to, or // else we can't check this object out if (!DBDeletionManager.addSessionInvids(session, object.getASymmetricTargets())) { return false; } if (!this.objects.containsKey(object.getInvid())) { this.objects.put(object.getInvid(), object); // just need something to mark the slot in the hash table, to // indicate that this object's base is involved in the // transaction. this.basesModified.add(object.objectBase); } return true; } /** * <p>This method is used to register a log event with this transaction. * If the transaction successfully commits, the provided log event * will be recorded in the Ganymede log file and mail notification will * be sent out if appropriate.</p> * * @param eventClassToken a short string specifying a DBObject record describing * the general category for the event * @param description Descriptive text to be entered in the record of the event * @param admin Invid pointing to the adminPersona that fired the event, if any * @param adminName String containing the name of the adminPersona that fired the event, if any * @param objects A List of invids of objects involved in this event. * @param notifyList A List of Strings listing email addresses to send notification * of this event to. */ public void logEvent(String eventClassToken, String description, Invid admin, String adminName, List<Invid> objects, List<String> notifyList) { DBLogEvent event = new DBLogEvent(eventClassToken, description, admin, adminName, objects, notifyList); logEvents.add(event); } /** * <p>This method is used to register a log event with this transaction. * If the transaction successfully commits, the provided log event * will be recorded in the Ganymede log file and mail notification will * be sent out if appropriate.</p> * * @param event A pre-formed log event to register with this transaction. */ public void logEvent(DBLogEvent event) { logEvents.add(event); } /** * <p>This method is used to record a message to be sent out when * the transaction is committed.</p> * * @param addresses Vector of Strings, the address list * @param subject The subject line of the message * @param message The body of the message * @param admin The invid of the admin whose action resulted in the mail * @param adminName The name of the admin whose actin resulted in the mail * @param objects A vector of invids of objects involved in the mail */ public void logMail(Collection<String> addresses, String subject, String message, Invid admin, String adminName, Vector<Invid> objects) { logEvents.add(new DBLogEvent(addresses, subject, message, admin, adminName, objects)); } /** * <p>This method is used to record a message to be sent out when * the transaction is committed.</p> * * @param addresses Vector of Strings, the address list * @param subject The subject line of the message * @param message The body of the message */ public void logMail(Collection<String> addresses, String subject, String message) { logEvents.add(new DBLogEvent(addresses, subject, message, null, null, null)); } /** * This method is used to record a message to be sent out when * the transaction is committed. * * @param toAddress The email address to send this message to. * @param subject The subject line of the message * @param message The body of the message */ public void logMail(String toAddress, String subject, String message) { Vector<String> addresses = new Vector<String>(); addresses.add(toAddress); logEvents.add(new DBLogEvent(addresses, subject, message, null, null, null)); } /** * <p>This method is used to transmit a log event during successful * transaction commit. The provided log event is recorded in the * Ganymede log file and mail notification is sent out if * appropriate.</p> * * @param eventClassToken a short string specifying a DBObject record describing * the general category for the event * @param description Descriptive text to be entered in the record of the event * @param admin Invid pointing to the adminPersona that fired the event, if any * @param adminName String containing the name of the adminPersona that fired the event, if any * @param objects A vector of invids of objects involved in this event. * @param notifyList A vector of Strings listing email addresses to send notification * of this event to. */ private void streamLogEvent(String eventClassToken, String description, Invid admin, String adminName, List<Invid> objects, List<String> notifyList) { DBLogEvent event = new DBLogEvent(eventClassToken, description, admin, adminName, objects, notifyList); Ganymede.log.streamEvent(event, this); } /** * <p>This method is used to transmit a log event during successful * transaction commit. The provided log event is recorded in the * Ganymede log file and mail notification is sent out if * appropriate.</p> * * @param event A pre-formed log event to register with this transaction. */ private void streamLogEvent(DBLogEvent event) { Ganymede.log.streamEvent(event, this); } /** * <p>This method is used to transmit a message during transaction commit.</p> * * @param addresses Vector of Strings, the address list * @param subject The subject line of the message * @param message The body of the message * @param admin The invid of the admin whose action resulted in the mail * @param adminName The name of the admin whose actin resulted in the mail * @param objects A vector of invids of objects involved in the mail */ private void streamLogMail(Vector<String> addresses, String subject, String message, Invid admin, String adminName, Vector<Invid> objects) { Ganymede.log.streamEvent(new DBLogEvent(addresses, subject, message, admin, adminName, objects), this); } /** * <p>This method is used to transmit a message during transaction commit.</p> * * @param addresses Vector of Strings, the address list * @param subject The subject line of the message * @param message The body of the message */ private void streamLogMail(Vector<String> addresses, String subject, String message) { Ganymede.log.streamEvent(new DBLogEvent(addresses, subject, message, null, null, null), this); } /** * <p>This method checkpoints the current transaction at its current * state. If need be, this transaction can later be rolled back * to this point by calling the rollback() method.</p> * * <p>Once a thread checkpoints a transaction, no other thread * can checkpoint a transaction until some thread clears the * checkpoint, either by doing a rollback() or a popCheckpoint(). * checkpoint() will block any threads that try to establish a checkpoint() * until the prior thread's checkpoint is resolved.</p> * * <p>See DBSession.deleteDBObject() and DBSesssion.createDBObject() * for instances of this.</p> * * @param name An identifier for this checkpoint */ public synchronized void checkpoint(String name) { // if we're not interactive, we'll disregard any checkpointing, in // favor of just failing the transaction at commit time if a // rollback is later attempted if (!interactive) { return; } // checkpoint our objects, logEvents, and deletion locks checkpoints.push(name, new DBCheckPoint(logEvents, getObjectList(), session)); // and our namespaces // we don't synchronize on dbStore, the odds are zip that a // namespace will be created or deleted while we are in the middle // of a transaction, since that is only done during schema editing for (DBNameSpace space: dbStore.nameSpaces) { space.checkpoint(this, name); } Ganymede.db.aSymLinkTracker.checkpoint(session, name); } /** * <p>This method is used to pop a checkpoint off the checkpoint * stack without making any other changes to the edit set. This * method is equivalent to a rollback where the checkpoint * information is taken off the stack, but this DBEditSet's state is * not reverted.</p> * * <p>Any checkpoints that were placed on the stack after * the checkpoint matching <name> will also be * removed from the checkpoint stack.</p> * * @param name An identifier for the checkpoint to take off * the checkpoint stack. * * @return null if the checkpoint could not be found on the * stack, or the DBCheckPoint object representing the state * of the transaction at the checkpoint time if the checkpoint * could be found. */ public DBCheckPoint popCheckpoint(String name) { return popCheckpoint(name, false); } /** * <p>This method is used to pop a checkpoint off the checkpoint * stack without making any other changes to the edit set. This * method is equivalent to a rollback where the checkpoint * information is taken off the stack, but this DBEditSet's state is * not reverted.</p> * * <p>Any checkpoints that were placed on the stack after * the checkpoint matching <name> will also be * removed from the checkpoint stack.</p> * * @param name An identifier for the checkpoint to take off * the checkpoint stack. * * @param inRollback If true, popCheckpoint will not actually * pop the checkpoint states out of the DBStore namespaces, * leaving that for rollback() to finish up. * * @return null if the checkpoint could not be found on the * stack, or the DBCheckPoint object representing the state * of the transaction at the checkpoint time if the checkpoint * could be found. */ public synchronized DBCheckPoint popCheckpoint(String name, boolean inRollback) { DBCheckPoint point = null; /* -- */ // if we're not an interactive transaction, we disregard all checkpoints if (!interactive) { return null; } // see if we can find the checkpoint point = checkpoints.pop(name); if (point == null) { System.err.println(ts.l("popCheckpoint.no_checkpoint", name)); System.err.println(checkpoints.toString()); return null; } // DBEditSet.rollback() calls us to take care of getting our // transactional checkpoint in the rollback case, but it doesn't // want us to pop the namespace checkpoint for it, since it will // want to do a rollback on the namespace checkpoint instead. if (!inRollback) { // we don't synchronize on dbStore, the odds are zip that a // namespace will be created or deleted while we are in the middle // of a transaction. Go ahead and clear out the namespace checkpoint. for (DBNameSpace space: dbStore.nameSpaces) { space.popCheckpoint(this, name); } } Ganymede.db.aSymLinkTracker.popCheckpoint(session, name); // if we've cleared the last checkpoint stacked, wake up any // threads that are blocking to create new checkpoints if (checkpoints.empty()) { this.notifyAll(); } return point; } /** * <p>This brings this transaction back to the state it was at at * the time of the matching checkPoint() call. Any objects that * were checked out in care of this transaction since the * checkPoint() will be checked back into the database and made * available for other transactions to access. All namespace * changes made by this transaction will likewise be rolled back to * their state at the checkpoint.</p> * * @param name An identifier for the checkpoint to be rolled back to. * @return true if the rollback could be performed, false otherwise */ public synchronized boolean rollback(String name) { DBCheckPoint point = null; /* -- */ if (!interactive) { // oops, we're non-interactive and we didn't actually do the // checkpoint we're being asked to go back to.. set the // mustAbort flag so the transaction will never commit this.mustAbort = true; if (false) { try { // "rollback() called in non-interactive transaction" throw new RuntimeException(ts.l("rollback.non_interactive")); } catch (RuntimeException ex) { Ganymede.logError(ex); } } return false; } point = popCheckpoint(name, true); // this may wake up blocking checkpointers if (point == null) { return false; } // rollback our mail/log events logEvents = point.logEvents; // restore the noDeleteLocks we had DBDeletionManager.revertSessionCheckpoint(session, point.invidDeleteLocks); // and our objects // first, take care of all the objects that were in the transaction at // the time of this checkpoint.. we want to revert these objects to // their checkpoint-time status for (DBCheckPointObj objck: point.objects) { DBEditObject obj = findObject(objck.invid); if (obj != null) { obj.rollback(objck.fields); obj.setStatus(objck.status); } else { // huh? this shouldn't ever happen, unless maybe we have a rollback order // error or something. Complain. throw new RuntimeException("DBEditSet.rollback error.. we lost checked out objects in midstream?"); } } // now, we have to sweep out any objects that are in the transaction now // that weren't in the transaction at the checkpoint. // note that we need a drop temp vector because it confuses things // if we remove elements from objects while we are iterating over it // calculate what DBEditObjects we have in the transaction at the // present time that we didn't have in the checkpoint HashSet<Invid> oldvalues = new HashSet<Invid>(); for (DBCheckPointObj obj: point.objects) { oldvalues.add(obj.invid); } ArrayList<DBEditObject> drop = new ArrayList<DBEditObject>(); for (DBEditObject eobjRef: this.objects.values()) { Invid tmpvid = eobjRef.getInvid(); if (!oldvalues.contains(tmpvid)) { drop.add(eobjRef); } } // and now we get rid of DBEditObjects we need to drop for (DBEditObject eObj: drop) { eObj.release(true); switch (eObj.getStatus()) { case ObjectStatus.CREATING: case ObjectStatus.DROPPING: eObj.getBase().releaseId(eObj.getID()); // relinquish the unused invid session.GSession.checkIn(); // XXX *synchronized* on GanymedeSession eObj.getBase().getStore().checkIn(); // update checked out count break; case ObjectStatus.EDITING: case ObjectStatus.DELETING: // note that clearShadow updates the checked out count for us. if (!eObj.original.clearShadow(this)) { throw new RuntimeException("editset ownership synchronization error"); } break; } } // now go ahead and clean out the dropped objects for (DBEditObject eObj: drop) { this.objects.remove(eObj.getInvid()); } // and our namespaces boolean success = true; // we don't synchronize on dbStore, the odds are zip that a // namespace will be created or deleted while we are in the middle // of a transaction, since that is only done during schema editing for (DBNameSpace space: dbStore.nameSpaces) { if (!space.rollback(this, name)) { success = false; } } Ganymede.db.aSymLinkTracker.rollback(session, name); return success; } /** * <p>commit is used to cause all changes in association with this * DBEditSet to be performed. If commit() cannot make the changes * for any reason, commit() will return a ReturnVal indicating * failure and the cause of the commit failure. Depending on the * source of the failure, the transaction may be left open for a * subsequent transaction commit or release.</p> * * <p>The returned {@link arlut.csd.ganymede.common.ReturnVal ReturnVal} * will have doNormalProcessing set to false if the transaction was * completely aborted. Both {@link arlut.csd.ganymede.server.DBSession * DBSession} and the client should take a false doNormalProcessing * boolean as an indicator that the transaction was simply wiped out * and a new transaction should be opened for subsequent activity. * A true doNormalProcessing value indicates that the client can try * the commit again at a later time, perhaps after making changes to * fix the commit problem.</p> * * <p>This method is synchronized and calls a synchronized method on * the DBSession which contains this DBEditSet. Because of this, * this method should really only be called by way of the * DBSession.commitTransaction() method, to avoid the possibility of * nested monitor deadlock.</p> * * @param comment If not null, a comment to attach to logging and * email generated in response to this transaction. * * @return a ReturnVal indicating success or failure, or null on success * without comment. */ public synchronized ReturnVal commit(String comment) { if (this.objects == null) { throw new RuntimeException(ts.l("global.already")); } if (mustAbort) { release(); // "Forced Transaction Abort" // "The server ran into a non-reversible error while processing this transaction and forced an abort." return Ganymede.createErrorDialog(this.getGSession(), ts.l("commit.forced_abort"), ts.l("commit.forced_abort_text")); } this.comment = comment; try { commit_run_precommit_hooks(); commit_lockBases(); // may block commit_verifyNamespaces(); commit_handlePhase1(); commit_recordModificationDates(); commit_integrateChanges(); releaseWriteLock(); return null; } catch (CommitNonFatalException ex) { return ex.getReturnVal(); } catch (CommitFatalException ex) { releaseWriteLock(); release(); return ex.getReturnVal(); } catch (Throwable ex) { Ganymede.debug(Ganymede.stackTrace(ex)); releaseWriteLock(); release(); // "Transaction commit failure" // "Couldn''t commit transaction, exception caught: {0}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("commit.commit_failure"), ts.l("commit.commit_failure_text", Ganymede.stackTrace(ex))); } finally { // just to be sure we don't leave a write lock hanging somehow. // if we successfully released before, this is a no-op releaseWriteLock(); } } /** * <p>This hook is run before we lock the bases, so we're still able * to make changes to our objects in transaction.</p> * * <p>The main use of this hook will be to allow DBEditObject * subclasses to refresh their hidden unique label fields, if any. * If such a hook has a problem, it should return a ReturnVal * indicating the problem.</p> */ private final void commit_run_precommit_hooks() throws CommitException { ReturnVal retVal; String checkpointKey = description + " precommit hook"; /* -- */ // make a copy of the object references currently in the // transaction. // // XXX right now, if an object's preCommitHook() causes // another object to be pulled into the transaction (as by doing // invid linkage operations or discrete remote object editing), // those new objects will not have their preCommitHook() run. // // this shouldn't be a big concern since the main purpose of // preCommitHook() is to update hidden label fields. /XXX DBEditObject[] myObjects = getObjectList(); checkpoint(checkpointKey); try { for (DBEditObject eObj: myObjects) { try { retVal = eObj.preCommitHook(); if (!ReturnVal.didSucceed(retVal)) { retVal.setErrorType(ErrorTypeEnum.SHOWOBJECT); retVal.setInvid(eObj.getInvid()); throw new CommitNonFatalException(retVal); } } catch (CommitNonFatalException ex) { throw ex; } catch (Throwable ex) { retVal = Ganymede.createErrorDialog(this.getGSession(), null, Ganymede.stackTrace(ex)); throw new CommitNonFatalException(retVal); } } } catch (CommitNonFatalException ex) { if (!rollback(checkpointKey)) { throw new CommitFatalException(ex.getReturnVal()); } throw ex; } popCheckpoint(checkpointKey); } /** * <p>Obtain a write lock on all bases modified by this transaction. * This method may block indefinitely, waiting on other transactions * which are in the process of modifying the DBStore hashes.</p> * * <p>Returns a List of DBObjectBases that we have locked if we * succeed.</p> * * <p>Throws a CommitNonFatalException if we can't get the lock.</p> */ private final List<DBObjectBase> commit_lockBases() throws CommitNonFatalException { List<DBObjectBase> baseSet = new ArrayList<DBObjectBase>(); /* -- */ for (DBObjectBase base: this.basesModified) { baseSet.add(base); } // and try to lock the bases down. // There should be NO WAY we can have a non-null wLock at this point. if (wLock != null) { // "Error! DBEditSet {0} commit already has writeLock established!" throw new Error(ts.l("commit_lockBases.wLock", description)); } // Create the lock on the bases changed and establish. This // openWriteLock() call will cause our thread to block until we // can get all the bases we need locked. try { wLock = session.openWriteLock(baseSet); // wait for write lock *synchronized* } catch (InterruptedException ex) { Ganymede.debug(ts.l("commit_lockBases.interrupted", String.valueOf(session.getKey()))); releaseWriteLock(); ReturnVal retVal = Ganymede.createErrorDialog(this.getGSession(), ts.l("commit.commit_failure"), ts.l("commit_lockBases.wLock_refused")); throw new CommitNonFatalException(retVal); } return baseSet; } /** * <p>If this transaction was carried out by an xml session, we will * have allowed namespace operations (in particular, moving values * from one field in a namespace to another) to be done * out-of-order. In such cases, we have to verify that the xml * transaction ultimately put things back right. This method does * that for us, throwing a CommitNonFatalException if we were not * successful.</p> * * <p>Of course, since the xmlclient has no way of interactively * fixing a problem, the CommitNonFatalException winds up causing * the transaction to be aborted by the xmlclient after all.</p> */ private final void commit_verifyNamespaces() throws CommitNonFatalException { // interactive transactions aren't allowed to get out of sync in // namespace if (isInteractive()) { return; } // we don't synchronize on dbStore.nameSpaces, the nameSpaces // vector should never have elements added or deleted while we are // in the middle of a transaction, since that is only done during // schema editing Set<String> totalConflicts = new TreeSet<String>(); for (DBNameSpace space: dbStore.nameSpaces) { List<String> conflicts = space.verify_noninteractive(this); if (conflicts != null) { totalConflicts.addAll(conflicts); } } if (totalConflicts.size() > 0) { // "Error, namespace conflicts remaining at transaction commit time. // The following values are in namespace conflict:\n\t{0}" ReturnVal retVal = Ganymede.createErrorDialog(this.getGSession(), null, ts.l("commit_verifyNamespaces.conflicts", VectorUtils.vectorString(totalConflicts, ",\n\t"))); throw new CommitNonFatalException(retVal); } } /** * <p>This private helper method for the commit() method handles * phase 1 of transaction commit.</p> * * <p>If an object refuses transaction commit, we'll throw a * CommitNonFatalException with ReturnVal information encoded.</p> */ private final void commit_handlePhase1() throws CommitNonFatalException { ReturnVal retVal; List<DBEditObject> committedObjects = new ArrayList<DBEditObject>(); /* -- */ for (DBEditObject eObj: this.objects.values()) { try { retVal = eObj.commitPhase1(); } catch (Throwable ex) { retVal = Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_handlePhase1.exception"), Ganymede.stackTrace(ex)); eObj.release(false); for (DBEditObject eObj2: committedObjects) { eObj2.release(false); // unlock commit mode } // let DBSession/the client know they can retry // things.. but if we've got an error in an object's // commitPhase1, committing again will probably just // repeat the problem. throw new CommitNonFatalException(retVal); } // the object has now been locked to commit mode, and will not // allow further modifications from the client if (ReturnVal.didSucceed(retVal)) { try { commit_checkObjectMissingFields(eObj); } catch (CommitNonFatalException ex) { retVal = ex.getReturnVal(); } } // retVal could be set by either eObj.commitPhase1() or // by commit_checkObjectMissingFields() if (!ReturnVal.didSucceed(retVal)) { retVal.setErrorType(ErrorTypeEnum.SHOWOBJECT); retVal.setInvid(eObj.getInvid()); eObj.release(false); for (DBEditObject eObj2: committedObjects) { eObj2.release(false); // unlock commit mode } // let DBSession/the client know they can retry things. throw new CommitNonFatalException(retVal); } else { committedObjects.add(eObj); } } } /** * <p>This private helper method for the commit() method runs a * check looking for missing mandatory fields on an object * involved with this transaction.</p> * * <p>If the object is missing fields, a CommitNonFatalException * will be thrown.</p> */ private final void commit_checkObjectMissingFields(DBEditObject eObj) throws CommitNonFatalException { ReturnVal retVal; Set<String> missingFields = new TreeSet<String>(); /* -- */ // if we're deleting or dropping an object, we won't ever require // fields to be set if (eObj.getStatus() == ObjectStatus.DELETING || eObj.getStatus() == ObjectStatus.DROPPING) { return; } // otherwise, we always insist on the label field being present. // We'll check that up front. DBField labelField = eObj.getField(eObj.getLabelFieldID()); if (labelField == null || !labelField.isDefined()) { // the label field is missing. look it up. DBObjectBaseField fieldDef = eObj.getBase().getField(eObj.getLabelFieldID()); missingFields.add(fieldDef.getName()); } if (isOversightOn()) { Vector<String> missingRequiredFields = eObj.checkRequiredFields(); if (missingRequiredFields != null) { missingFields.addAll(missingRequiredFields); } } if (missingFields.size() > 0) { StringBuilder errorBuf = new StringBuilder(); if (!eObj.isEmbedded()) { // "Error, {0} object {1} has not been completely filled out. The following fields need to be filled in before this transaction can be committed:\n\n" errorBuf.append(ts.l("commit_checkObjectMissingFields.missing_fields_text", eObj.getTypeName(), eObj.getLabel())); } else { DBObject topContainerObj = this.session.getContainingObj(eObj); // "Error, {0} object {1} contained within {2} object {3} has not been completely filled out. // // The following fields need to be filled in before this transaction can be committed:\n\n" errorBuf.append(ts.l("commit_checkObjectMissingFields.embedded_missing_fields_text", eObj.getTypeName(), eObj.getLabel(), topContainerObj.getTypeName(), topContainerObj.getLabel())); } for (String fieldName: missingFields) { errorBuf.append(fieldName); errorBuf.append("\n"); } // "Error, required fields not filled in" retVal = Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_checkObjectMissingFields.missing_fields"), errorBuf.toString()); // put a reference to the object that tripped us up so that // the client can bring the problematic window forward. retVal.setErrorType(ErrorTypeEnum.SHOWOBJECT); retVal.setInvid(eObj.getInvid()); // let DBSession/the client know they can retry things. throw new CommitNonFatalException(retVal); } } /** * <p>This private helper method for the commit() method records * the creation/modification timestamp for the vector of * committed objects.</p> */ private final void commit_recordModificationDates() { DateDBField df; StringDBField sf; String result = this.session.getID() + " [" + this.description + "]"; Date modDate = new Date(); /* -- */ // intern the result string so that we don't have multiple // copies of common strings in our heap result = result.intern(); for (DBEditObject eObj: this.objects.values()) { // force a change of date and modifier information // into the object without using the normal field // modification methods.. this lets us set field // values at a time when the object would reject // changes from the user because the committing flag // is set. switch (eObj.getStatus()) { case ObjectStatus.CREATING: if (!eObj.isEmbedded()) { df = eObj.getDateField(SchemaConstants.CreationDateField); // If we're processing an XML transaction and the XML // transaction already specified a creation date (the // df field is not undefined), we'll go ahead and // leave it alone. // this behavior is intended to allow data to be // dumped from a Ganymede 1.0 server, manually // massaged to bring it into compliance with a // Ganymede 2.0 server's schema, and then reloaded // without losing the original creation information. if (!allowXMLHistoryOverride || !df.isDefined()) { df.value = modDate; } // ditto for the creator info field. sf = eObj.getStringField(SchemaConstants.CreatorField); if (!allowXMLHistoryOverride || !sf.isDefined()) { sf.value = result; } } // * fall-through * case ObjectStatus.EDITING: if (!eObj.isEmbedded()) { df = eObj.getDateField(SchemaConstants.ModificationDateField); if (!allowXMLHistoryOverride || !df.isDefined()) { df.value = modDate; } sf = eObj.getStringField(SchemaConstants.ModifierField); if (!allowXMLHistoryOverride || !sf.isDefined()) { sf.value = result; } } } } } /** * <p>This private helper method for commit() integrates all * committed objects back into the DBStore, handling on-disk change * journaling, transaction logging, namespaces, and more.</p> */ private final void commit_integrateChanges() throws CommitFatalException { Set<DBObjectBaseField> fieldsTouched = new HashSet<DBObjectBaseField>(); /* -- */ // we serialize transaction commits here synchronized (dbStore.journal) { commit_persistTransaction(); // on first run we're initializing an empty database, and we // won't have any sync channels registered, nor yet a // scheduler to scan for them. if (!Ganymede.firstrun) { commit_writeSyncChannels(); } commit_finalizeTransaction(); } // we've successfully persisted the transaction, written the // transaction to the sync channels, and finalized the // transaction.. we can proceed // // note that we still have our write lock established, even though // we are giving up synchronization on the journal. try { // we sync on Ganymede global objects in the following, but we // don't keep external sync for more than one step, so // multiple transactions on non-overlapping DBObjectBases can // proceed through this section concurrently commit_handlePhase2(); commit_logTransaction(fieldsTouched); // *sync* Ganymede.log commit_replace_objects(); commit_updateNamespaces(); // *sync* over each namespace in Ganymede.db.nameSpaces DBDeletionManager.releaseSession(session); // *sync* static DBDeletionManager Ganymede.db.aSymLinkTracker.commit(session); // *sync* Ganymede.db.aSymLinkTracker commit_updateBases(fieldsTouched); } catch (Throwable ex) { // If we throw up here, we've got real problems throw new CommitError("Critical error: Intolerable exception during commit_integrateChanges().", ex); } } /** * <p>This private helper method for commit() writes the transaction * to the on-disk transactions journal, which will persist our * transaction's changes.</p> * * <p>Will throw a CommitException if a failure was detected.</p> */ private final void commit_persistTransaction() throws CommitFatalException { try { persistedTransaction = dbStore.journal.writeTransaction(this); if (persistedTransaction == null) { // "Couldn''t commit transaction, couldn''t write transaction to disk" // "Couldn''t commit transaction, the server may have run out of disk space. Couldn''t write transaction to disk." throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_persistTransaction.error"), ts.l("commit_persistTransaction.error_text"))); } } catch (Throwable ex) { if (ex instanceof IOException) { // "Couldn''t commit transaction, Exception caught writing journal" // "Couldn''t commit transaction, the server may have run out of disk space.\n\n{0}" throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_persistTransaction.exception"), ts.l("commit_persistTransaction.ioexception_text", Ganymede.stackTrace(ex)))); } else { // "Couldn''t commit transaction, Exception caught writing journal" // "Couldn''t commit transaction, an exception was caught persisting to the journal.\n\n{0}" throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_persistTransaction.exception"), ts.l("commit_persistTransaction.exception_text", Ganymede.stackTrace(ex)))); } } } /** * <p>This private helper method for commit() writes the transaction * to all of the builder queue sync channels.</p> */ private final void commit_writeSyncChannels() throws CommitFatalException { DBEditObject[] objectList = getObjectList(); try { for (scheduleHandle handle: Ganymede.scheduler.getTasksByClass(SyncRunner.class)) { SyncRunner sync = (SyncRunner) handle.task; try { if (sync.isIncremental()) { sync.writeIncrementalSync(persistedTransaction, objectList, this); } else if (sync.isFullState()) { sync.checkBuildNeeded(persistedTransaction, objectList, this); } } catch (java.io.FileNotFoundException in_ex) { // "Couldn''t write transaction to sync channel. Exception caught writing to sync channel." // "Couldn''t write transaction to sync channel {0} due to a FileNotFoundException. // // This sync channel is configured to write to {1}, but this directory does not exist or is not writable. // // Transaction Cancelled." throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_writeSyncChannels.exception"), ts.l("commit_writeSyncChannels.no_sync_found", sync.getName(), sync.getDirectory()))); } } } catch (Throwable ex) { undoSyncChannels(); try { dbStore.journal.undoTransaction(persistedTransaction); } catch (IOException inex) { // This *really* shouldn't happen, since there's no writes involved // in truncating the journal. If it did, we're kind of screwed, though. // *** // *** Error in commit_writeSyncChannels()! Couldn''t undo a transaction in the // *** journal file after catching an exception! // *** // *** The journal may not be completely recoverable! // *** // // {0} Ganymede.debug(ts.l("commit_writeSyncChannels.badundo", Ganymede.stackTrace(ex))); } if (ex instanceof CommitFatalException) { throw (CommitFatalException) ex; } else if (ex instanceof IOException) { // "Couldn''t write transaction to sync channel. Exception caught writing to sync channel." // "Couldn''t write transaction to sync channels due to an IOException. The server may have run out of disk space.\n\n{0}" throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_writeSyncChannels.exception"), ts.l("commit_writeSyncChannels.ioexception_text", Ganymede.stackTrace(ex)))); } else { // "Couldn''t write transaction to sync channel. Exception caught writing to sync channel." // "Exception caught while writing to sync channels. Sync channels write aborted.\n\n{0}" throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_writeSyncChannels.exception"), ts.l("commit_writeSyncChannels.exception_text", Ganymede.stackTrace(ex)))); } } } /** * <p>This private helper method scrubs the sync channels of the * persistedTransaction, so that we can avoid having bits of the * transaction sync'ed to the channels when we ultimately wind up * undoing the transaction.</p> */ private final void undoSyncChannels() { for (scheduleHandle handle: Ganymede.scheduler.getTasksByClass(SyncRunner.class)) { SyncRunner sync = (SyncRunner) handle.task; if (sync.isIncremental()) { try { sync.unSync(persistedTransaction); } catch (Throwable inex) { // what can we do? keep clearing them out as best we // can Ganymede.logError(inex); } } } } /** * <p>This private helper method for commit() writes a finalized * token to the on-disk transactions journal, so that we'll know * upon restart that we don't need to scrub the transaction from the * sync channels.</p> * * <p>Will throw a CommitException if a failure was detected.</p> */ private final void commit_finalizeTransaction() throws CommitFatalException { try { dbStore.journal.finalizeTransaction(persistedTransaction); } catch (IOException ex) { try { undoSyncChannels(); dbStore.journal.undoTransaction(persistedTransaction); } catch (IOException inex) { // This *really* shouldn't happen, since there's no writes involved // in truncating the journal. If it did, we're kind of screwed, though. // *** // *** Error in commit_finalizeTransaction()! Couldn''t undo a transaction in the // *** journal file after catching an exception! // *** // *** The journal may not be completely recoverable! // *** // // {0} Ganymede.debug(ts.l("commit_finalizeTransaction.badundo", Ganymede.stackTrace(ex))); } // "Couldn''t finalize transaction to journal. IOException caught writing to journal." // "Couldn''t finalize transaction to journal, the server may have run out of disk space.\n\n{0}" throw new CommitFatalException(Ganymede.createErrorDialog(this.getGSession(), ts.l("commit_finalizeTransaction.exception"), ts.l("commit_finalizeTransaction.exception_text", Ganymede.stackTrace(ex)))); } finally { persistedTransaction = null; } } /** * <p>This private helper method calls {@link * arlut.csd.ganymede.server.DBEditObject#commitPhase2()} on the * DBEditObjects in this transaction, after we have successfully * finalized the transaction to disk.</p> */ private final void commit_handlePhase2() { for (DBEditObject eObj: this.objects.values()) { // tell the object to go ahead and do any external // commit actions. try { eObj.commitPhase2(); } catch (Throwable ex) { // if we get a runtime exception here, there's nothing to // do for it.. we're locked on course. log the trace, but // stay on target.. stay on target.. Ganymede.debug(Ganymede.stackTrace(ex)); } } } /** * <p>This private helper method is executed in the middle of the * commit() method, and handles logging for any changes made to * objects during the committed transaction.</p> * * <p>While this method is examining each object in the transaction * to determine the diffs, we'll also take the opportunity to track * the identity of all fields in all object bases that have * themselves been touched. We use the fieldsTouched Set for this * purpose, storing the DBObjectBaseFields that were touched.</p> */ private final void commit_log_events(Set<DBObjectBaseField> fieldsTouched) { for (DBEditObject eObj: this.objects.values()) { try { commit_log_event(eObj, fieldsTouched); } catch (Throwable ex) { // we're already committed, so we just warn about the log // failure and move on // "Error! Problem occured while writing log entry, continuing with transaction commit.\n{0}" Ganymede.debug(ts.l("commit_log_events.log_failure", Ganymede.stackTrace(ex))); } } } /** * <p>This private helper method is executed in the middle of the * commit() method, and handles logging for any changes made to a * DBEditObject during the committed transaction.</p> * * <p>While this method is examining each object in the transaction * to determine the diffs, we'll also take the opportunity to track * the identity of all fields in all object bases that have * themselves been touched. We use the fieldsTouched hashtable for * this purpose, storing identity maps for the DBObjectBaseFields * that were touched.</p> */ private final void commit_log_event(DBEditObject eObj, Set<DBObjectBaseField> fieldsTouched) { if (Ganymede.log == null) { return; } Vector<Invid> invids; String diff; Invid responsibleInvid = null; String responsibleName = null; /* -- */ if (getGSession() != null) { responsibleInvid = getGSession().getPermManager().getResponsibleInvid(); responsibleName = getGSession().getPermManager().getIdentity(); } else { responsibleInvid = null; responsibleName = "system"; } switch (eObj.getStatus()) { case ObjectStatus.EDITING: invids = new Vector<Invid>(); invids.add(eObj.getInvid()); // if we changed an embedded object, make a // note that the containing object was // involved so that we show changes to // embedded objects in a log search for the // containing object if (eObj.isEmbedded()) { DBObject container = session.getContainingObj(eObj); if (container != null) { invids.add(container.getInvid()); } } diff = eObj.diff(fieldsTouched); if (diff != null) { boolean logNormal = true; if (eObj.isEmbedded()) { try { DBObject parentObj = session.getContainingObj(eObj); // "{0} {1}''s {2} ''{3}'', <{4}> was modified.\n\n{5}" streamLogEvent("objectchanged", ts.l("commit_createLogEvent.embedded_modified", parentObj.getTypeName(), parentObj.getLabel(), eObj.getTypeName(), eObj.getLabel(), eObj.getInvid(), diff), responsibleInvid, responsibleName, invids, (List<String>) VectorUtils.union(eObj.getEmailTargets(), parentObj.getEmailTargets())); logNormal = false; } catch (IntegrityConstraintException ex) { // We might catch this from // getContainingObj if the // embedded object doesn't have a // proper container. Won't be // fatal, as we'll just leave // logNormal true and handle it below Ganymede.debug(Ganymede.stackTrace(ex)); } } if (logNormal) { streamLogEvent("objectchanged", ts.l("commit_createLogEvent.modified", eObj.getTypeName(), eObj.getLabel(), String.valueOf(eObj.getInvid()), diff), responsibleInvid, responsibleName, invids, (List<String>) eObj.getEmailTargets()); } } break; case ObjectStatus.CREATING: invids = new Vector<Invid>(); invids.add(eObj.getInvid()); // if we created an embedded object, make a // note that the containing object was // involved so that we show changes to // embedded objects in a log search for the // containing object if (eObj.isEmbedded()) { DBObject container = session.getContainingObj(eObj); if (container != null) { invids.add(container.getInvid()); } } // We'll call diff() to update the fieldsTouched hashtable, // but we won't use the string generated, since // getPrintString() does a better job of describing the // contents of (newly-created) embedded objects. // // This forced use of diff() isn't elegant, but as DBField was // originally defined, it's only through the use of the diff // strings that we have a unified way to determine change, and // we don't want to have to re-do that work in all the DBField // subclasses. eObj.diff(fieldsTouched); diff = eObj.getPrintString(); if (diff != null) { boolean logNormal = true; if (eObj.isEmbedded()) { try { DBObject parentObj = session.getContainingObj(eObj); // "{0} {1}''s {2} ''{3}'', <{4}> was created.\n\n{5}\n" streamLogEvent("objectcreated", ts.l("commit_createLogEvent.embedded_created", parentObj.getTypeName(), parentObj.getLabel(), eObj.getTypeName(), eObj.getLabel(), eObj.getInvid(), diff), responsibleInvid, responsibleName, invids, (List<String>) VectorUtils.union(eObj.getEmailTargets(), parentObj.getEmailTargets())); logNormal = false; } catch (IntegrityConstraintException ex) { // We might catch this from // getContainingObj if the // embedded object doesn't have a // proper container. Won't be // fatal, as we'll just leave // logNormal true and handle it below Ganymede.debug(Ganymede.stackTrace(ex)); } } if (logNormal) { // "{0} {1}, <{2}> was created.\n\n{3}\n" streamLogEvent("objectcreated", ts.l("commit_createLogEvent.created", eObj.getTypeName(), eObj.getLabel(), String.valueOf(eObj.getInvid()), diff), responsibleInvid, responsibleName, invids, eObj.getEmailTargets()); } } break; case ObjectStatus.DELETING: invids = new Vector<Invid>(); invids.add(eObj.getInvid()); // if we deleted an embedded object, make a // note that the containing object was // involved so that we show changes to // embedded objects in a log search for the // containing object if (eObj.isEmbedded()) { try { DBObject container = session.getContainingObj(eObj); if (container != null) { invids.add(container.getInvid()); } } catch (IntegrityConstraintException ex) { // GanymedeServer.sweepEmbeddedObjects() may need to // delete an embedded object with no container // registered if an error condition elsewhere in the // Ganymede server left a dangling embedded object. // So we'll ignore this here for the sake of getting // the dangling embedded object properly flushed. } } String oldVals = null; try { oldVals = eObj.getOriginal().getPrintString(); } catch (NullPointerException ex) { Ganymede.debug(Ganymede.stackTrace(ex)); } if (oldVals != null) { boolean logNormal = true; if (eObj.isEmbedded()) { try { DBObject parentObj = session.getContainingObj(eObj); streamLogEvent("deleteobject", ts.l("commit_createLogEvent.embedded_deleted", parentObj.getTypeName(), parentObj.getLabel(), eObj.getTypeName(), eObj.getLabel(), eObj.getInvid(), oldVals), responsibleInvid, responsibleName, invids, (List<String>) VectorUtils.union(eObj.getEmailTargets(), parentObj.getEmailTargets())); logNormal = false; } catch (IntegrityConstraintException ex) { // We might catch this from // getContainingObj if the // embedded object doesn't have a // proper container. Won't be // fatal, as we'll just leave // logNormal true and handle it below Ganymede.debug(Ganymede.stackTrace(ex)); } } if (logNormal) { streamLogEvent("deleteobject", ts.l("commit_createLogEvent.deleted", eObj.getTypeName(), eObj.getLabel(), String.valueOf(eObj.getInvid()), oldVals), responsibleInvid, responsibleName, invids, eObj.getEmailTargets()); } // and calculate the fields that we touched by losing them DBObject origObj = eObj.getOriginal(); for (DBObjectBaseField fieldDef: origObj.objectBase.getFieldsInFieldOrder()) { // we don't care if certain fields change if (fieldDef.getID() == SchemaConstants.CreationDateField || fieldDef.getID() == SchemaConstants.CreatorField || fieldDef.getID() == SchemaConstants.ModificationDateField || fieldDef.getID() == SchemaConstants.ModifierField) { continue; } DBField origField = origObj.getField(fieldDef.getID()); if (origField != null && origField.isDefined()) { fieldsTouched.add(fieldDef); } } } else { boolean logNormal = true; if (eObj.isEmbedded()) { try { DBObject parentObj = session.getContainingObj(eObj); streamLogEvent("deleteobject", ts.l("commit_createLogEvent.embedded_deleted_nodiff", parentObj.getTypeName(), parentObj.getLabel(), eObj.getTypeName(), eObj.getLabel(), eObj.getInvid()), responsibleInvid, responsibleName, invids, (List<String>) VectorUtils.union(eObj.getEmailTargets(), parentObj.getEmailTargets())); logNormal = false; } catch (IntegrityConstraintException ex) { // We might catch this from // getContainingObj if the // embedded object doesn't have a // proper container. Won't be // fatal, as we'll just leave // logNormal true and handle it below Ganymede.debug(Ganymede.stackTrace(ex)); } } if (logNormal) { streamLogEvent("deleteobject", ts.l("commit_createLogEvent.deleted_nodiff", eObj.getTypeName(), eObj.getLabel(), String.valueOf(eObj.getInvid())), responsibleInvid, responsibleName, invids, eObj.getEmailTargets()); } } break; } } /** * <p>This method handles the on-disk and email logging for events * that have built up over the course of this transaction.</p> */ private final void commit_logTransaction(Set<DBObjectBaseField> fieldsTouched) { Invid responsibleInvid; String responsibleName; /* -- */ if (Ganymede.log == null) { // if our log is null, as it is with DBStore bootstrap, then // we don't need to do any logging.. we'll just return. // // note that this means that when we have a log, we also won't // be updating the per-field-type timestamps in // DBObjectBaseField. // // This is acceptable, since the DBStore bootstrap is only for // creating the mandatory Ganymede objects, which we shouldn't // care about in a GanymedeBuilderTask context (the only thing // that cares about the DBObjectBase/DBObjectBaseField // timestamps). return; } if (getGSession() != null) { responsibleName = getGSession().getPermManager().getIdentity(); responsibleInvid = getGSession().getPermManager().getResponsibleInvid(); } else { responsibleName = "system"; responsibleInvid = null; } // collect the list of invids that we know were touched in this // transaction for the start transaction log record Vector<Invid> invids = new Vector<Invid>(this.objects.keySet()); synchronized (Ganymede.log) { try { Ganymede.log.startTransactionLog(invids, responsibleName, responsibleInvid, comment, this); // then transmit/log any pre-recorded log events that we // have accumulated during the user's session/transaction for (DBLogEvent event: logEvents) { streamLogEvent(event); } // for garbage collection logEvents.clear(); // then create and stream log events describing the // objects that are in this transaction at the time of // commit commit_log_events(fieldsTouched); // finish the transaction to disk and send out any email // that we need to send Ganymede.log.endTransactionLog(invids, responsibleName, responsibleInvid, this); } catch (Throwable ex) { // exceptions during logging aren't important enough to break a // transaction commit in progress, but we do want to record any // such Ganymede.debug(Ganymede.stackTrace(ex)); } finally { logEvents = null; Ganymede.log.cleanupTransaction(); } } } /** * This method integrates commiteted objects back into our in-memory * DBStore data structures, and clears the transaction of objects as * it does so. */ private final void commit_replace_objects() { for (DBEditObject eObj: this.objects.values()) { commit_replace_object(eObj); } this.objects.clear(); } /** * <p>Private helper method for commit() that integrates committed * objects back into the DBStore hashes.</p> * * <p>If this method throws an exception, we will be pretty screwed, * as it means we're not able to replace an object that we've * already committed to our logs. This essentially boils down to * meaning that we're screwed if we can't create a new DBObject with * a copy constructor from a DBEditObject.</p> */ private final void commit_replace_object(DBEditObject eObj) { DBObjectBase base; /* -- */ base = eObj.getBase(); // Create a new DBObject from our DBEditObject and insert // into the object hash switch (eObj.getStatus()) { case ObjectStatus.CREATING: case ObjectStatus.EDITING: // Create a read-only version of eObj, with all fields // reset to checked-in status, put it into our object hash // note that this new DBObject will not include any // transient fields which self-identify as undefined base.put(new DBObject(eObj)); // note that we can't use a no-sync put above, since // we don't prevent asynchronous viewDBObject(). if (getGSession() != null) { getGSession().checkIn(); } base.getStore().checkIn(); // update checkout count break; case ObjectStatus.DELETING: // Deleted objects had their deletion finalization done before // we ever got to this point. // Note that we don't try to release the id for previously // registered objects.. the base.releaseId() method really // can only handle popping object id's off of a stack, and // can't do anything for object id's unless the id was the // last one allocated in that base, which is unlikely // enough that we don't worry about it here. base.remove(eObj.getID()); // note that we can't use a no-sync remove above, since // we don't prevent asynchronous viewDBObject(). session.GSession.checkIn(); // *synchronized* base.getStore().checkIn(); // count it as checked in once it's deleted break; case ObjectStatus.DROPPING: // dropped objects had their deletion finalization done before // we ever got to this point.. base.releaseId(eObj.getID()); // relinquish the unused invid if (getGSession() != null) { getGSession().checkIn(); } base.getStore().checkIn(); // count it as checked in once it's deleted break; } } /** * <p>Private helper method for commit() which causes all namespaces to update * themselves in conjunction with a commit.</p> */ private final void commit_updateNamespaces() { // we don't synchronize on dbStore.nameSpaces, the nameSpaces // vector should never have elements added or deleted while we are // in the middle of a transaction, since that is only done during // schema editing for (DBNameSpace space: dbStore.nameSpaces) { space.commit(this); } } /** * <p>Private helper method for commit() which causes all bases that * were touched by this transaction to be updated.</p> * * <p>This should be run as late as possible in the commit() * sequence to minimize the chance that a previously scheduled * builder task completes and updates its lastRunTime field after we * have touched the timestamps on the changed bases.</p> * * @param fieldsTouched Set of field types that were edited during * this transaction */ private final void commit_updateBases(Set<DBObjectBaseField> fieldsTouched) { for (DBObjectBase base: this.basesModified) { base.updateTimeStamp(); // and, very important, update the base's snapshot vector // so that any new queries that are issued will proceed // against the new state of objects in this base base.updateIterationSet(); } // And in addition to updating the time stamps on the object // bases, update the time stamps on each field. for (DBObjectBaseField fieldDef: fieldsTouched) { fieldDef.updateTimeStamp(); } } /** * <p>This method is intended for use by DBSession's * abortTransaction() method, and returns true if the transaction * could be aborted, false otherwise.</p> */ public synchronized boolean abort() { // if we are called while we are waiting on a write lock in order // to commit() on another thread, try to kill it off. We // synchronize on Ganymede.db.lockSync here because we are using // that as a monitor for all lock operations, and we need the // wLock.inEstablish check to be sync'ed so that we don't force an // abort after we have gotten our lock established and are busy // mucking with the server's DBObjectTables. synchronized (Ganymede.db.lockSync) { if (wLock != null) { if (wLock.isEstablishing()) { return false; } wLock.abort(); } } release(); return true; } /** * <p>release is used to abandon all changes made in association * with this DBEditSet. All DBObjects created, deleted, or * modified, and all unique values allocated and freed during the * course of actions on this transaction will be reverted to their * state when this transaction was created.</p> * * <p>Note that this does not mean that the entire DBStore will * revert to its state at the beginning of this transaction; any * changes not relating to objects and namespace values connected to * this transaction will not be affected by this transaction's * release.</p> */ private final void release() { if (this.objects == null) { throw new RuntimeException(ts.l("global.already")); } for (DBEditObject eObj: this.objects.values()) { eObj.release(true); switch (eObj.getStatus()) { case ObjectStatus.CREATING: case ObjectStatus.DROPPING: eObj.getBase().releaseId(eObj.getID()); // relinquish the unused invid if (getGSession() != null) { getGSession().checkIn(); } eObj.getBase().getStore().checkIn(); // update checked out count break; case ObjectStatus.EDITING: case ObjectStatus.DELETING: // note that clearShadow updates the checked out count for us. if (!eObj.original.clearShadow(this)) { throw new RuntimeException("editset ownership synchronization error"); } break; } } // undo all namespace modifications associated with this editset // we don't synchronize on dbStore, the odds are zip that a // namespace will be created or deleted while we are in the middle // of a transaction, since that is only done during schema editing for (DBNameSpace space: dbStore.nameSpaces) { space.abort(this); } // release any deletion locks we have asserted DBDeletionManager.releaseSession(session); // and scrub any link tracking data for the session Ganymede.db.aSymLinkTracker.abort(session); // make sure that we haven't somehow left a write lock // hanging.. and let's do it before we deconstruct. This is a // no-op if we don't have a write lock open. releaseWriteLock(); // and help out garbage collection some this.deconstruct(); } /** * <p>Private helper method for commit() and release(), which breaks apart * and nulls references to data structures maintained for this transaction * to aid GC.</p> * * <p>This method also does a notifyAll() to wake up any checkpoint threads * that are blocking waiting for the ability to checkpoint.</p> */ private void deconstruct() { if (this.objects != null) { this.objects.clear(); this.objects = null; } if (this.logEvents != null) { this.logEvents.clear(); this.logEvents = null; } if (this.basesModified != null) { this.basesModified.clear(); this.basesModified = null; } this.dbStore = null; this.session = null; this.description = null; if (this.checkpoints != null) { this.checkpoints.clear(); this.checkpoints = null; } this.notifyAll(); // wake up any late checkpointers } /** * <p>This is a dinky little private helper method to keep things * clean. It's essential that wLock be released if things go * wrong, else next time this session tries to commit a transaction, * it'll wind up waiting forever for the old lock to be released.</p> * * <p>Note that this method will fail if called after deconstruct().</p> */ private void releaseWriteLock() { if (wLock != null) { session.releaseLock(wLock); wLock = null; } } } /*------------------------------------------------------------------------------ class CommitException ------------------------------------------------------------------------------*/ /** * <p>This is a Ganymede-specific Exception that can be thrown by code in * the server during a transactional commit.</p> */ class CommitException extends Exception { public CommitException() { super(); } public CommitException(String s) { super(s); } } /*------------------------------------------------------------------------------ class CommitNonFatalException ------------------------------------------------------------------------------*/ /** * <p>This is a Ganymede-specific Exception that can be thrown by code in * the server during a transactional commit.</p> * * <p>The ReturnVal encapsulated by this exception class will be coded so * that upstream code can re-try the transaction commit once the problems * that caused the CommitNonFatalException to be thrown are fixed.</p> */ class CommitNonFatalException extends CommitException { private ReturnVal retVal; /* -- */ public CommitNonFatalException(ReturnVal retVal) { super(); this.retVal = retVal; retVal.doNormalProcessing = true; } public CommitNonFatalException(String s, ReturnVal retVal) { super(s); this.retVal = retVal; retVal.doNormalProcessing = true; } public ReturnVal getReturnVal() { return retVal; } } /*------------------------------------------------------------------------------ class CommitFatalException ------------------------------------------------------------------------------*/ /** * <p>This is a Ganymede-specific Exception that can be thrown by code in * the server during a transactional commit.</p> * * <p>The ReturnVal encapsulated by a CommitFatalException will cause all * upstream code to treat the transaction as fatally compromised, and a * transaction cancel will be triggered.</p> */ class CommitFatalException extends CommitException { private ReturnVal retVal; /* -- */ public CommitFatalException(ReturnVal retVal) { super(); this.retVal = retVal; retVal.doNormalProcessing = false; } public CommitFatalException(String s, ReturnVal retVal) { super(s); this.retVal = retVal; retVal.doNormalProcessing = false; } public ReturnVal getReturnVal() { return retVal; } } /*------------------------------------------------------------------------------ class CommitError ------------------------------------------------------------------------------*/ /** * <p>This is a Ganymede-specific Error that can be thrown by code in * the server during a transactional commit, signifying that a commit * failed in a possibly non-recoverable way.</p> */ class CommitError extends Error { public CommitError() { super(); } public CommitError(String s) { super(s); } public CommitError(String s, Throwable t) { super(s, t); } }