//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "stdafx.h" #include "sharedobjecttransaction.h" #include "sqlaccess/sqlaccess.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" namespace GCSDK { //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- class CTrustedHelper_OutputAndSetErrorState { public: CTrustedHelper_OutputAndSetErrorState( CSharedObjectTransactionEx& SharedObjectTransaction, const char *pszFunctionContext, const CSteamID& CacheOwnerSteamID, const CSharedObject *pObject ) : m_SharedObjectTransaction( SharedObjectTransaction ) , m_pszFunctionContext( pszFunctionContext ) , m_CacheOwnerSteamID( CacheOwnerSteamID ) , m_pObject( pObject ) { } void operator()( const bool bExpResult, const char *pszExp ) const { if ( !bExpResult ) { AssertMsg4( bExpResult, "Failed verification: %s (context '%s'; owner '%s'; object '%s')", pszExp, m_pszFunctionContext, m_CacheOwnerSteamID.Render(), m_pObject ? m_pObject->GetDebugString().String() : "[none]" ); m_SharedObjectTransaction.SetErrorState(); } } private: CSharedObjectTransactionEx& m_SharedObjectTransaction; const char *m_pszFunctionContext; const CSteamID& m_CacheOwnerSteamID; const CSharedObject *m_pObject; }; #define CSOTVerifyBase( exp_, obj_ ) \ CVerifyIfTrustedHelper( CTrustedHelper_OutputAndSetErrorState( *this, __FUNCTION__, m_pLockedSOCache->GetOwner(), obj_ ), (exp_), #exp_ ).GetResult() #define CSOTVerify( exp_ ) \ CSOTVerifyBase( exp_, NULL ) #define CSOTVerifyObj( exp_ ) \ CSOTVerifyBase( exp_, pObject ) //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CSharedObjectTransactionEx::CSharedObjectTransactionEx( CGCSharedObjectCache *pLockedSOCache, const char *pszTransactionName ) : m_pLockedSOCache( pLockedSOCache ) , m_pSQLAccess( &m_sqlAccessInternal ) { Assert( pszTransactionName ); Assert( pszTransactionName[0] ); Assert( m_pLockedSOCache ); // We have to start a SQL transaction no matter what. If we don't do this, then // any code internally or externally that tries to add operations to the open transaction // will instead run immediately. We Verify() here because we know the only thing that // can fail beginning a new transaction is to already be in a transaction, and that // can't be the case because we just made this object. m_bTransactionBuildSuccess = m_pSQLAccess->BBeginTransaction( pszTransactionName ); Verify( m_bTransactionBuildSuccess ); // Grab another lock on our user that owns the cache we got passed in. This means that, // barring any maliscious or really terrible code happening on the outside, even if our // calling code unlocks *their* lock, we'll still have ours and can access the cache safely. Verify( GGCBase()->BLockSteamIDImmediate( m_pLockedSOCache->GetOwner() ) ); // Because we just constructed a fresh object, we want to guarantee that our internal state // is consistent. This way, either we're guaranteed to fail construction or we know moving // forward that we started in a good place so anything that's wrong since then is some kind // of real programmer error. Verify( BIsValidInternalState() ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CSharedObjectTransactionEx::~CSharedObjectTransactionEx() { // If we fall off the stack and we haven't been submitted, manually rollback whatever work // we queued up in SQL and free up our local storage. This isn't an error. if ( m_pSQLAccess->BInTransaction() ) { Rollback(); } // We're finally done manipulating this user's cache. If we're practicing best // practices for locking, this will still leave us locked in our outer scope. If we're // not, at least we'll be safe. GGCBase()->UnlockSteamID( m_pLockedSOCache->GetOwner() ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BIsValidInternalState() { // Sanity check basic internal data. Assert( m_pSQLAccess ); // If we've done something bad with our transaction (ie., committed from outside, or // tried to submit through this object) and we're still doing work, that's a case we // can't handle. if ( !CSOTVerify( m_pSQLAccess->BInTransaction() ) ) return false; // Verify we have a cache and it's a cache we understand how to manipulate. if ( !CSOTVerify( m_pLockedSOCache ) || !CSOTVerify( m_pLockedSOCache->GetOwner().IsValid() ) || !CSOTVerify( m_pLockedSOCache->GetOwner().BIndividualAccount() ) ) return false; // Transactions can yield when trying to commit, so we need to be running a job. We also want // to be paranoid and make sure that the lock is actively held by our current job. AssertRunningJob(); if ( !CSOTVerify( GGCBase()->IsSteamIDLockedByJob( m_pLockedSOCache->GetOwner(), &GJobCur() ) ) ) return false; // Internal state is such that we can at least try to perform operations, at least. return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BIsValidInput( const CSharedObject *pObject ) { if ( !BIsValidInternalState() ) return false; if ( !CSOTVerifyObj( pObject ) ) return false; // Make sure we have a valid type ID for this object. There aren't any objects that return negative // IDs, but if we pass in a deleted or bogus pointer, we'll probably crash here when we hit the vtable. if ( !CSOTVerifyObj( pObject->GetTypeID() > 0 ) ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BTrackModifiedObjectInternal( CSharedObject *pObject, CSharedObject **out_ppWritableObject ) { if ( !BIsValidInput( pObject ) ) return false; if ( !CSOTVerifyObj( out_ppWritableObject ) ) return false; // If an object is in the added list, we're going to do a full write so modifying it at this point adds nothing // new. We'll return the current only version of it as our writable object. { const CreateOrDestroyCommitInfo_t *pInfo = InternalFindCommitInfo( pObject, m_vecObjects_Added ); if ( pInfo ) { *out_ppWritableObject = pInfo->m_pObject; return true; } } // If an object is in the modified list, we've already made a copy that we can make modifications to, so we'll // return that copy. { const ModifyCommitInfo_t *pInfo = InternalFindCommitInfo( pObject, m_vecObjects_Modified ); if ( pInfo ) { *out_ppWritableObject = pInfo->m_pWriteableObject; return true; } } // We aren't already tracking this object. We're acting as if we've never seen it before, so first make sure that // we're in the cache we think we're in. if ( !CSOTVerify( m_pLockedSOCache->FindSharedObject( *pObject ) ) ) return false; // Make a copy of our current state that we can make modifications to and track the association. *out_ppWritableObject = CSharedObject::Create( pObject->GetTypeID() ); (*out_ppWritableObject)->Copy( *pObject ); m_vecObjects_Modified.AddToTail( ModifyCommitInfo_t( *out_ppWritableObject, pObject ) ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BAddNewObjectInternal( CSharedObject *pObject ) { // Make sure we pass basic sanity check measures for our inputs (ie., we have inputs, we have appropriate // locks to manipulate those inputs, etc.). if ( !BIsValidInput( pObject ) ) return false; // Make sure this object isn't already in this cache. This can cause problems during rollback if we didn't // really add it as part of the transaction and remove it when a transaction fails. if ( !CSOTVerifyObj( m_pLockedSOCache->FindSharedObject( *pObject ) == NULL ) ) return false; // Make sure this object isn't already in the list of objects we're adding as part of this transaction. // Having the object in the list multiple times is potentially harmless, but it probably indicates some // calling code is doing something we don't expect. if ( !CSOTVerifyObj( InternalFindCommitInfo( pObject, m_vecObjects_Added ) == NULL ) ) return false; // Success. m_vecObjects_Added.AddToTail( CreateOrDestroyCommitInfo_t( pObject ) ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BRemoveObjectInternal( CSharedObject *pObject ) { // Make sure we pass basic sanity check measures for our inputs (ie., we have inputs, we have appropriate // locks to manipulate those inputs, etc.). if ( !BIsValidInput( pObject ) ) return false; // Make sure the object we're removing is in the cache we're trying to remove it from. if ( !CSOTVerifyObj( m_pLockedSOCache->FindSharedObject( *pObject ) ) ) return false; // Look through our lists of objects that we're adding and objects that we're modifying. If we're // removing an object that we're adding in the same transaction through the same pointer, this will // result in a broken SO cache. Removing a modified object may or may not be safe but it's probably // indicative of a higher-level logic bug regardless. if ( !CSOTVerifyObj( InternalFindCommitInfo( pObject, m_vecObjects_Added ) == NULL ) ) return false; if ( !CSOTVerifyObj( InternalFindCommitInfo( pObject, m_vecObjects_Modified ) == NULL ) ) return false; // Success. m_vecObjects_Removed.AddToTail( CreateOrDestroyCommitInfo_t( pObject ) ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const CSharedObject *CSharedObjectTransactionEx::InternalFindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject& soIndex ) const { FOR_EACH_VEC( m_vecObjects_Modified, i ) { auto& info = m_vecObjects_Modified[i]; if ( info.m_pObject->BIsKeyEqual( soIndex ) ) return info.m_pObject; } FOR_EACH_VEC( m_vecObjects_Added, i ) { auto& info = m_vecObjects_Added[i]; if ( info.m_pObject->BIsKeyEqual( soIndex ) ) return info.m_pObject; } return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CSharedObjectTransactionEx::InternalPreCommit() { // ... if ( !BIsValidInternalState() ) return "invalid internal state"; // ... if ( !CSOTVerify( m_pSQLAccess->BInTransaction() ) ) return "transaction closed before commit"; // Did we run into some error internally building this transaction? This doesn't assert because it // could be a SQL error or something else that doesn't necessarily indicate a problem with the way // we're using this class. It *probably* indicates the calling code has a problem, but it isn't // guaranteed so we don't assert. if ( !m_bTransactionBuildSuccess ) return "error(s) building transaction"; // Nothing wrong so far. return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CSharedObjectTransactionEx::BYieldingCommit() { const char *pszPreCommitFailureDesc = InternalPreCommit(); if ( pszPreCommitFailureDesc ) { // We can't spit out information about which cache we're dealing with here because it's possible at // this point that the error we're displaying is "we don't have the cache anymore!" which means pulling // memory is probably unsafe. EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s': %s!\n", GetInternalTransactionDesc(), pszPreCommitFailureDesc ); Rollback(); return false; } bool bNetworkRelevantObjectsChanged = false; bool bDBRelevantObjectsChanged = false; bool bGeneratedCommitSQL = true; // Add insert statements to SQL access instance for everything that needs to be written to the // database. Some types only exist in memory and have no database backing. FOR_EACH_VEC( m_vecObjects_Added, i ) { CreateOrDestroyCommitInfo_t& info = m_vecObjects_Added[i]; if ( info.m_pObject->BIsDatabaseBacked() ) { bGeneratedCommitSQL &= info.m_pObject->BYieldingAddInsertToTransaction( *m_pSQLAccess ); bDBRelevantObjectsChanged |= true; } bNetworkRelevantObjectsChanged |= info.m_pObject->BIsNetworked(); } // For every object that we've changed state on, we've also been tracking information on which // fields we modified. Here we ask each of the modified objects (which aren't currently in the // SO cache, but are clones living outside of it) to queue up their updates to the SQL transaction. { CUtlVector< int > vecDirtyFields; FOR_EACH_VEC( m_vecObjects_Modified, i ) { const ModifyCommitInfo_t& info = m_vecObjects_Modified[i]; Assert( info.m_pObject ); Assert( info.m_pWriteableObject ); Assert( info.m_pObject != info.m_pWriteableObject ); if ( info.m_pWriteableObject->BIsDatabaseBacked() ) { vecDirtyFields.RemoveAll(); m_SODirtyList.GetDirtyFieldSetByObj( info.m_pWriteableObject, vecDirtyFields ); bGeneratedCommitSQL &= info.m_pWriteableObject->BYieldingAddWriteToTransaction( *m_pSQLAccess, vecDirtyFields ); bDBRelevantObjectsChanged |= true; } bNetworkRelevantObjectsChanged |= info.m_pWriteableObject->BIsNetworked(); AssertMsg4( info.m_pWriteableObject->BIsDatabaseBacked() == info.m_pObject->BIsDatabaseBacked(), "Disagreement over DB backing state between SOs '%s' and '%s' in transaction '%s' for user '%s'!", info.m_pObject->GetDebugString().String(), info.m_pWriteableObject->GetDebugString().String(), GetInternalTransactionDesc(), m_pLockedSOCache->GetOwner().Render() ); AssertMsg4( info.m_pWriteableObject->BIsNetworked() == info.m_pObject->BIsNetworked(), "Disagreement over network state between SOs '%s' and '%s' in transaction '%s' for user '%s'!", info.m_pObject->GetDebugString().String(), info.m_pWriteableObject->GetDebugString().String(), GetInternalTransactionDesc(), m_pLockedSOCache->GetOwner().Render() ); } } // Have each object that we'd like to remove queue up the SQL work necessary to do so. FOR_EACH_VEC( m_vecObjects_Removed, i ) { CreateOrDestroyCommitInfo_t& info = m_vecObjects_Removed[i]; if ( info.m_pObject->BIsDatabaseBacked() ) { bGeneratedCommitSQL &= info.m_pObject->BYieldingAddRemoveToTransaction( *m_pSQLAccess ); bDBRelevantObjectsChanged |= true; } // We don't have to update network state here as removes are sent immediately to the client // before we've even flushed our dirty updates. } // Our "did we generate the SQL to do this work in the DB" variable starts off true, so the only // way we'd expect it to be false here is if we attempted to do work above and ran into some errors. // If we don't have any SQL work to do at all, for example if we're only adding/modifying/removing // memory-only items, then "bGeneratedCommitSQL" will be true and "bDBRelevantObjectsChanged" will // be false. if ( !bGeneratedCommitSQL ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s' for user '%s': failed to add inserts/writes.\n", GetInternalTransactionDesc(), m_pLockedSOCache->GetOwner().Render() ); Rollback(); return false; } // Try to commit DB transaction. We don't know for sure whether we're the only code that's adding commands // to this transaction, so we can't completely skip this if we didn't do any SQL work internally. We can // say "did we do anything internally?; if not, maybe we'll be empty". if ( !m_pSQLAccess->BCommitTransaction( !bDBRelevantObjectsChanged ) ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s' for user '%s': SQL transaction failure.\n", GetInternalTransactionDesc(), m_pLockedSOCache->GetOwner().Render() ); Rollback(); return false; } // The database work committed successfully, so we update our memory state to match the state we just wrote // to the DB. FOR_EACH_VEC( m_vecObjects_Added, i ) { CreateOrDestroyCommitInfo_t& info = m_vecObjects_Added[i]; m_pLockedSOCache->AddObject( info.m_pObject ); // internally will assert if cache isn't locked } FOR_EACH_VEC( m_vecObjects_Modified, i ) { ModifyCommitInfo_t& info = m_vecObjects_Modified[i]; info.m_pObject->Copy( *info.m_pWriteableObject ); // stomp the version that's already in the cache with the properties from where we've been writing delete info.m_pWriteableObject; } FOR_EACH_VEC( m_vecObjects_Removed, i ) { CreateOrDestroyCommitInfo_t &info = m_vecObjects_Removed[i]; Verify( m_pLockedSOCache->BDestroyObject( *info.m_pObject, false ) ); // internally will assert if cache isn't locked } // Did we change anything that affected network state? If so, tell our cache to flush the dirty // state list. if ( bNetworkRelevantObjectsChanged ) { // Our cache is responsible for sending all the network updates, including creates from the // AddObject() calls above, so now that our commit has succeeded we copy over our list of // dirty objects from inside this transaction. for ( const ModifyCommitInfo_t& info : m_vecObjects_Modified ) { m_pLockedSOCache->DirtyNetworkObject( info.m_pObject ); } m_pLockedSOCache->SendAllNetworkUpdates(); } // Cleanup. m_vecObjects_Added.RemoveAll(); m_vecObjects_Modified.RemoveAll(); m_vecObjects_Removed.RemoveAll(); return true; } void CSharedObjectTransactionEx::Rollback() { // Clean up any memory allocated to handle any database work that is outstanding but not yet // committed. m_pSQLAccess->RollbackTransaction(); Assert( !m_pSQLAccess->BInTransaction() ); // Clean up any in-memory changes that are currently outstanding. // We made new objects for our adds, so we need to free up that memory. FOR_EACH_VEC( m_vecObjects_Added, i ) { delete m_vecObjects_Added[i].m_pObject; } m_vecObjects_Added.RemoveAll(); // For our modifies, we haven't done any work on the versions that are in the cache, so all we have // to do is delete the temp memory that we allocated for a writable version. FOR_EACH_VEC( m_vecObjects_Modified, i ) { ModifyCommitInfo_t& info = m_vecObjects_Modified[i]; delete info.m_pWriteableObject; } m_vecObjects_Modified.RemoveAll(); // We didn't actually do any in-memory work for our items that were set to be deleted, so we // can just free the memory for our tracking state. m_vecObjects_Removed.RemoveAll(); } CSharedObjectTransaction::CSharedObjectTransaction( CSQLAccess &sqlAccess, const char *pName ) : m_sqlAccess( sqlAccess ) { if ( m_sqlAccess.BInTransaction() == false ) { m_sqlAccess.BBeginTransaction( pName ); } } CSharedObjectTransaction::~CSharedObjectTransaction() { Rollback(); } CSharedObjectTransaction::undoinfo_t *CSharedObjectTransaction::FindObjectInVector( const CSharedObject *pObject, CUtlVector &vec ) const { FOR_EACH_VEC( vec, i ) { if ( vec[i].pObject == pObject ) return &vec[i]; } return NULL; } bool CSharedObjectTransaction::AssertValidInput( const CGCSharedObjectCache *pSOCache, const CSharedObject *pObject, const char *pszContext ) { Assert( pszContext ); Assert( pSOCache ); Assert( pObject ); if ( pSOCache == NULL || pObject == NULL ) { SetError( CFmtStr( "%s: attempt to manipulate invalid SO cache %s, object %s", pszContext, pSOCache ? pSOCache->GetOwner().Render() : "[none]", pObject ? pObject->GetDebugString().String() : "[none]" ) ); return false; } const bool bSOCachedLocked = GGCBase()->IsSteamIDLockedByCurJob( pSOCache->GetOwner() ); Assert( bSOCachedLocked ); if ( !bSOCachedLocked ) { SetError( CFmtStr( "%s: attempt to manipulate non-locked SO cache %s to add object %s", pszContext, pSOCache->GetOwner().Render(), pObject->GetDebugString().String() ) ); return false; } return pSOCache != NULL && pObject != NULL && bSOCachedLocked; } void CSharedObjectTransaction::AddManagedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ) { if ( !AssertValidInput( pSOCache, pObject, "AddManagedObject()" ) ) return; // if an object is in the added list, we're going to do a full write so modifying it at this point adds nothing // new; if an object is in the modified list, we already tracked the initial state; either way we have no new // data to track here if ( FindObjectInVector( pObject, m_vecObjects_Added ) || FindObjectInVector( pObject, m_vecObjects_Modified ) ) return; undoinfo_t info = { pObject, pSOCache, NULL }; info.pOriginalCopy = CSharedObject::Create( pObject->GetTypeID() ); info.pOriginalCopy->Copy( *pObject ); m_vecObjects_Modified.AddToTail( info ); } void CSharedObjectTransaction::AddNewObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ) { if ( !AssertValidInput( pSOCache, pObject, "AddNewObject()" ) ) return; if ( FindObjectInVector( pObject, m_vecObjects_Added ) ) return; undoinfo_t info = { pObject, pSOCache, NULL }; m_vecObjects_Added.AddToTail( info ); } void CSharedObjectTransaction::RemoveObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ) { if ( !AssertValidInput( pSOCache, pObject, "RemoveObject()" ) ) return; // make sure the object we're removing is in the cache we're trying to remove it from. AssertMsg1( pSOCache->FindSharedObject( *pObject ), "Attempting to remove object '%s' from non-owning cache!", pObject->GetDebugString().Get() ); // look through our lists of objects that we're adding and objects that we're modifying. If we're // removing an object that we're adding in the same transaction through the same pointer, this will // result in a broken SO cache. Removing a modified object may or may not be safe but it's probably // indicative of a higher-level logic bug regardless. if ( undoinfo_t *pInfo = FindObjectInVector( pObject, m_vecObjects_Added ) ) { EmitError( SPEW_GC, "Attempting to add and remove the same object from the same CSharedObjectTransaction! Object: %s", pInfo->pObject->GetDebugString().Get() ); Assert( !"Attempting to add and remove the same object from the same CSharedObjectTransaction!" ); m_vecObjects_Added.FindAndFastRemove( *pInfo ); } if ( undoinfo_t *pInfo = FindObjectInVector( pObject, m_vecObjects_Modified ) ) { EmitError( SPEW_GC, "Attempting to modify and remove the same object from the same CSharedObjectTransaction! Object: %s", pInfo->pObject->GetDebugString().Get() ); Assert( !"Attempting to modify and remove the same object from the same CSharedObjectTransaction!" ); m_vecObjects_Modified.FindAndFastRemove( *pInfo ); } // @note Tom Bui: the act of removing an item may change the object, so to roll that back, // we need the original version undoinfo_t info = { pObject, pSOCache, NULL }; info.pOriginalCopy = CSharedObject::Create( pObject->GetTypeID() ); info.pOriginalCopy->Copy( *pObject ); m_vecObjects_Removed.AddToTail( info ); if ( !pObject->BYieldingAddRemoveToTransaction( m_sqlAccess ) ) { SetError( "RemoveObject(): BYieldingAddRemoveToTransaction() failed" ); return; } } void CSharedObjectTransaction::ModifiedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject, uint32 unFieldIdx ) { if ( !AssertValidInput( pSOCache, pObject, "ModifiedObject()" ) ) return; // look for an object in the transaction -- this might be a new object created for this // transaction or it might be an object we've already tagged for modification; we don't // use FindSharedObject() for this because we might not be in an SO cache yet // if we're in the add list, we aren't intended to be in a cache yet, and so we also don't // have to dirty any fields -- we don't exist for real yet so when we finalize this transaction, // effectively *everything* is dirty if ( FindObjectInVector( pObject, m_vecObjects_Added ) ) return; // make sure the object we're removing is in the cache we think it is. This check has to happen // after the "is in the added list?" check above because we won't actually put items in the cache // for real until after the SQL transaction succeeds AssertMsg1( pSOCache->FindSharedObject( *pObject ), "Attempting to modify object '%s' in non-owning cache!", pObject->GetDebugString().Get() ); undoinfo_t *pInfo = FindObjectInVector( pObject, m_vecObjects_Modified ); if ( pInfo ) { pInfo->pSOCache->DirtyObjectField( pObject, unFieldIdx ); return; } Assert( !"Attempt to modify an unmanaged object in CSharedObjectTransaction!" ); SetError( CFmtStr( "ModifiedObject(): attempt to modify an unmanaged object %s", pObject->GetDebugString().String() ) ); } CSharedObject *CSharedObjectTransaction::FindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex ) { // search in modified objects FOR_EACH_VEC( m_vecObjects_Modified, i ) { undoinfo_t &info = m_vecObjects_Modified[i]; if ( info.pSOCache == pSOCache && info.pObject->BIsKeyEqual( soIndex ) ) return info.pObject; } // search in new objects FOR_EACH_VEC( m_vecObjects_Added, i ) { undoinfo_t &info = m_vecObjects_Added[i]; if ( info.pSOCache == pSOCache && info.pObject->BIsKeyEqual( soIndex ) ) return info.pObject; } return NULL; } void CSharedObjectTransaction::Rollback() { if ( m_sqlAccess.BInTransaction() ) { m_sqlAccess.RollbackTransaction(); } Undo(); } bool CSharedObjectTransaction::BYieldingCommit( bool bAllowEmpty ) { const char *pszPreExistingError = GetError(); if ( pszPreExistingError ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s': %s.\n", PchName(), pszPreExistingError ); Undo(); return false; } Assert( m_sqlAccess.BInTransaction() ); if ( !m_sqlAccess.BInTransaction() ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s': transaction closed before commit!\n", PchName() ); Undo(); return false; } bool bSuccess = true; bool bDBRelevantObjectsChanged = false; // add insert statements to sql access FOR_EACH_VEC( m_vecObjects_Added, i ) { undoinfo_t &info = m_vecObjects_Added[i]; if ( info.pObject->BIsDatabaseBacked() ) { bSuccess &= info.pObject->BYieldingAddInsertToTransaction( m_sqlAccess ); bDBRelevantObjectsChanged = true; } } // add update statements to sql access FOR_EACH_VEC( m_vecObjects_Modified, i ) { undoinfo_t &info = m_vecObjects_Modified[i]; if ( info.pObject->BIsDatabaseBacked() ) { bSuccess &= info.pSOCache->BYieldingAddWriteToTransaction( info.pObject, m_sqlAccess ); bDBRelevantObjectsChanged = true; } } if ( bSuccess == false ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s': failed to add inserts/writes.\n", PchName() ); Undo(); return false; } // try to commit db transaction. if ( m_sqlAccess.BCommitTransaction( bAllowEmpty && !bDBRelevantObjectsChanged ) == false ) { EmitError( SPEW_GC, "Failed to commit CSharedObjectTransaction '%s': SQL transaction failure.\n", PchName() ); Undo(); return false; } // remove objects from SO cache FOR_EACH_VEC( m_vecObjects_Removed, i ) { undoinfo_t &info = m_vecObjects_Removed[i]; DbgVerify( info.pSOCache->BDestroyObject( *info.pObject, false ) ); // internally will assert if cache isn't locked delete info.pOriginalCopy; } m_vecObjects_Removed.RemoveAll(); // add new objects to SO cache FOR_EACH_VEC( m_vecObjects_Added, i ) { undoinfo_t &info = m_vecObjects_Added[i]; info.pSOCache->AddObject( info.pObject ); // internally will assert if cache isn't locked Assert( info.pOriginalCopy == NULL ); } // free up memory for original state of modified objects FOR_EACH_VEC( m_vecObjects_Modified, i ) { undoinfo_t &info = m_vecObjects_Modified[i]; info.pSOCache->DirtyNetworkObject( info.pObject ); delete info.pOriginalCopy; } // send network updates for objects that were added or modified // this is OK to call more than once on a CGCSharedObjectCache, because internally // it keeps a list of things that were marked dirty FOR_EACH_VEC( m_vecObjects_Added, i ) { m_vecObjects_Added[i].pSOCache->SendAllNetworkUpdates(); } FOR_EACH_VEC( m_vecObjects_Modified, i ) { m_vecObjects_Modified[i].pSOCache->SendAllNetworkUpdates(); } m_vecObjects_Added.RemoveAll(); m_vecObjects_Modified.RemoveAll(); return true; } void CSharedObjectTransaction::Undo() { FOR_EACH_VEC( m_vecObjects_Added, i ) { undoinfo_t &info = m_vecObjects_Added[i]; delete info.pObject; } m_vecObjects_Added.RemoveAll(); FOR_EACH_VEC( m_vecObjects_Removed, i ) { undoinfo_t &info = m_vecObjects_Removed[i]; AssertMsg1( GGCBase()->IsSteamIDLockedByCurJob( info.pSOCache->GetOwner() ), "Attempt to modify in-memory object '%s' during CSharedObjectTransaction removal rollback.", info.pObject->GetDebugString().Get() ); info.pObject->Copy( *info.pOriginalCopy ); delete info.pOriginalCopy; } m_vecObjects_Removed.RemoveAll(); FOR_EACH_VEC( m_vecObjects_Modified, i ) { undoinfo_t &info = m_vecObjects_Modified[i]; AssertMsg1( GGCBase()->IsSteamIDLockedByCurJob( info.pSOCache->GetOwner() ), "Attempt to modify in-memory object '%s' during CSharedObjectTransaction modify rollback.", info.pObject->GetDebugString().Get() ); info.pObject->Copy( *info.pOriginalCopy ); delete info.pOriginalCopy; } m_vecObjects_Modified.RemoveAll(); ClearError(); } const char *CSharedObjectTransaction::PchName() const { return m_sqlAccess.PchTransactionName(); } };