You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
819 lines
30 KiB
819 lines
30 KiB
//========= 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(); |
|
} |
|
};
|
|
|