diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /gcsdk/sharedobjecttransaction.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'gcsdk/sharedobjecttransaction.cpp')
| -rw-r--r-- | gcsdk/sharedobjecttransaction.cpp | 819 |
1 files changed, 819 insertions, 0 deletions
diff --git a/gcsdk/sharedobjecttransaction.cpp b/gcsdk/sharedobjecttransaction.cpp new file mode 100644 index 0000000..38d9ca7 --- /dev/null +++ b/gcsdk/sharedobjecttransaction.cpp @@ -0,0 +1,819 @@ +//========= 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<undoinfo_t> &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(); + } +}; |