//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Base class for transactions that modify a CGCSharedObjectCache and the database // //============================================================================= #ifndef SHAREDOBJECTTRANSACTION_H #define SHAREDOBJECTTRANSACTION_H #ifdef _WIN32 #pragma once #endif #include namespace GCSDK { template < typename TSharedObject > struct SharedObjectContainsAuditEntryType { typedef char t_Yes[1]; typedef char t_No[2]; template < typename T > static t_Yes& Test( typename T::CAuditEntry * ); template < typename T > static t_No& Test( ... ); enum { kValue = sizeof( Test( NULL ) ) == sizeof( t_Yes ) }; }; //----------------------------------------------------------------------------- // Purpose: Let's stop writing transactional code by hand everywhere! // // Core usage: // // - make a new instance, either starting a new SQL transaction or hooking // into an existing open transaction. // // - call some combination of AddNewObject/RemoveObject to add newly-allocated // objects or remove existing objects. If the types of objects being added/ // removed contain a linked audit data class, you're required to pass in // a filled-out instance with the add/remove request. // // - modify some existing objects. You can either call ModifyObject directly // or call one of the helper functions/macros (ie., ModifyObjectSch). // Whatever changes get made here won't be visible anywhere outside this // transaction object until a commit succeeds. // // - try to do a commit. If this succeeds, everything worked and memory has // been updated to reflect DB changes (if any). If this fails, or if the // transaction object gets destroyed with an open transaction, all queued // SQL work will be dropped and no potential memory changes will happen. // // That was too long. What's a short version?: // // { // CSharedObjectTransactionEx transaction( pSomeLockedSOCache, "Sample Tool Transaction" ); // transaction.RemoveObject( pToolItem, CEconItem::CAuditEntry( ... ) ); // transaction.RemoveObject( pToolTargetItem, CEconItem::CAuditEntry( ... ) ); // transaction.AddNewObject( pNewResultItem, CEconItem::CAuditEntry( ... ) ); // transaction.ModifyObjectSch( pEconGameAccount, unNumToolsUsed, pEconGameAccount->Obj().m_pEconGameAccount + 1 ); // Verify( transaction.BYieldingCommit() ); // } // // Guarantees this class makes: // // - if the initial state is correct and client code isn't malicious, a // lock on the SO cache passed in will be maintained for the lifetime // of the class. // // - externally, no changes will be visible on the GC or on cients until a // commit attempt takes place. If the commit failures, no changes will // take place in memory. If the commit succeeds, all memory changes will // become visible "simultaneously" (no yields, but not atomic from a // threading perspective. // // - this class will do the minimum amount of work possible to guarantee // correct behavior. If you don't touch any networked objects, no network // updates will be sent. If you don't touch any DB-backed objects, no DB // work will be done. //----------------------------------------------------------------------------- class CSharedObjectTransactionEx { public: CSharedObjectTransactionEx( CGCSharedObjectCache *pLockedSOCache, const char *pszTransactionName ); ~CSharedObjectTransactionEx(); // AddNewObject: // Add a new object to this user's SO cache. If the type of this object supports audit entries, you're // required to pass in a CAuditEntry of the appropriate type (ie., CEconItem::CAuditEntry). If your type // doesn't support audit entries, and you're really sure that not auditing in the correct behavior, you // call the single-argument version below. In both cases, it's illegal to pass in an object that's // a raw CSharedObject pointer because then this code doesn't know what type of audit data to look for. // // It's possible to set up these functions using SFINAE so that the compiler will only see the appropriate // version, but this way produces prettier error messages. // Add an object to this user's cache, conditional on the SQL transaction succeeding, including writing an // audit entry to SQL explaining where this object came from. template < typename TSharedObject > void AddNewObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit ) { COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType::kValue ); m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject ); m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject ); } // Add an object to this user's cache, conditional on the SQL transaction succeeding, but don't write any // audit data. In general, this is probably the wrong thing to do and you want to be making sure this object // type supports writing audit data and then writing it. template < typename TSharedObject > void AddNewObject( TSharedObject *pObject ) { COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType::kValue ); m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject ); } // This is a helper class purely to help VC will template type deduction. The real signature we want for the // base ModifyObject function is: // // template < typename TSharedObject > // void ModifyObject( TSharedObject *pObject, const std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) >& funcModifyAndAudit ) // // ...but if we do do that, VC will complain that it doesn't know how to deduce the type for TSharedObject // because of the second parameter. Instead, we make it deduce the type based on the first parameter, and then // feed that type into this helper class to get the type for the second parameter. template < typename TSharedObject > struct CSharedObjectModifyAndAuditFunction { typedef std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) > ModifyFunctionType; }; // ModifyObject: takes in template < typename TSharedObject > void ModifyObject( TSharedObject *pObject, const typename CSharedObjectModifyAndAuditFunction::ModifyFunctionType& funcModifyAndAudit ) { COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); CSharedObject *pWritableObject = NULL; m_bTransactionBuildSuccess &= BTrackModifiedObjectInternal( pObject, &pWritableObject ); AssertMsg( !m_bTransactionBuildSuccess || pWritableObject != NULL, "Cannot be tracking state for an object but not having a writable version!" ); m_bTransactionBuildSuccess &= funcModifyAndAudit( *m_pSQLAccess, m_SODirtyList, assert_cast( pWritableObject ) ); } // ModifyObjectL() is a helper macro designed to behave like a function. It takes as parameters the original // object you want to modify and the code you want to run on it, expressed as a lambda. #define ModifyObjectL( obj_, modifyfunc_ ) \ ModifyObject( obj_, [=] ( CSQLAccess& sqlAccess, CSharedObjectDirtyList& SODirtyList, decltype( obj_ ) pWritableObject ) -> bool { modifyfunc_ } ) // ModifyObjectSch() is a helper macro designed to behave like a function. It takes as parameters the original // object you want to modify, an identifier for the field you want to change, and the new value you want to // change it to. Ex.: // // transaction.ModifyObjectSch( pLockedSOCache->GetGameAccount(), // type of internal object is used to look up field IDs // unNextHalloweenGiftTime, // turns into "(obj).m_unNextHalloweenGiftTime" and "(type)::k_iField_unNextHalloweenGiftTime" // CRTime::RTime32DateAdd( CRTime::RTime32TimeCur(), tf_halloween_min_minutes_between_drops_per_player.GetInt(), k_ETimeUnitMinute ) ); #define ModifyObjectSch( obj_, field_, newvalue_ ) \ ModifyObjectL( obj_, \ { \ pWritableObject->Obj().m_ ## field_ = (newvalue_); \ SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k_iField_ ## field_ ); \ return true; \ } ) // ModifyObjectProto() is a helper macro designed to behave like a function. It takes as parameters the original // object you want to modify, the field name in the message or the field you want to change, the identifier field // ID (will be identical except for case/underscores), and the new value. Ex.: // // soTrans.ModifyObjectProto( pGameAccountClient, // type of internal object is used to look up field IDs // preview_item_def, // turns into "(obj)->set_preview_item_def" // PreviewItemDef, // turns into "(type)::kPreviewItemDefFieldNumber" // 0 ); #define ModifyObjectProto( obj_, fieldfunc_, fieldname_, newvalue_ ) \ ModifyObjectL( obj_, \ { \ pWritableObject->Obj().set_ ## fieldfunc_( newvalue_ ); \ SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k ## fieldname_ ## FieldNumber ); \ return true; \ } ) // RemoveObject template < typename TSharedObject > void RemoveObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit ) { COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType::kValue ); m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject ); m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject ); } template < typename TSharedObject > void RemoveObject( CSharedObject *pObject ) { COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType::kValue ); m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject ); } template < class TSchType > void AddSQLRecord( const TSchType& sch ) { // We can't test for all types here because there's no compile-time list. This is // mostly to demonstrate "seriously please don't call this with types that have // CSharedObject wrappers". COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); // We're about to call a function with "Yielding" in the name and we aren't in a // function with "Yielding" in the name, but we *are* in a transaction so we know // we don't yield. We verify that assumption here. DO_NOT_YIELD_THIS_SCOPE(); // Queue up the work for SQL as long as we're in a good state to do so. m_bTransactionBuildSuccess &= BIsValidInternalState() ? m_pSQLAccess->BYieldingInsertRecord( &sch ) : false; } template < class TSchType > void AddOrUpdateSQLRecord( TSchType& sch ) { // We can't test for all types here because there's no compile-time list. This is // mostly to demonstrate "seriously please don't call this with types that have // CSharedObject wrappers". COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); COMPILE_TIME_ASSERT( (!AreTypesIdentical::kValue) ); // We're about to call a function with "Yielding" in the name and we aren't in a // function with "Yielding" in the name, but we *are* in a transaction so we know // we don't yield. We verify that assumption here. DO_NOT_YIELD_THIS_SCOPE(); // Queue up the work for SQL as long as we're in a good state to do so. m_bTransactionBuildSuccess &= BIsValidInternalState() ? m_pSQLAccess->BYieldingInsertOrUpdateOnPK( &sch ) : false; } // This is meant purely for interop with the SQL message queue and even then only as a temporary // measure until we have a CSQLTransaction object we can pass around instead. CSQLAccess& GetSQLTransactionForSQLMsgQueue() { return *m_pSQLAccess; } // slow! but fine for current uses template < class TSharedObject > const TSharedObject *FindTypedSharedObject( const CSharedObject &soIndex ) const { return assert_cast( InternalFindSharedObject( pSOCache, soIndex ) ); } // Take all the work we queued up for SQL and try to commit it to the DB. If that works, take all // of our memory changes and copy them over. From the outside, this will either move *all* memory // changes to our cache over at once, or not touch any in-memory structures at all. MUST_CHECK_RETURN bool BYieldingCommit(); // Cancel this transaction completely -- this will empty the queue of whatever SQL work we may have // done and also free up the memory we used to track modified object state, etc. Once this function // gets called, it's illegal to call any other functions on this transaction object except the // destructor. void Rollback(); private: // State validation. Non-const because may set internal error state. bool BIsValidInternalState(); bool BIsValidInput( const CSharedObject *pObject ); const char *GetInternalTransactionDesc() const { return m_pSQLAccess->PchTransactionName(); } template < typename tCommitInfo > const tCommitInfo *InternalFindCommitInfo( const CSharedObject *pObject, const CUtlVector& vec ) const { FOR_EACH_VEC( vec, i ) { if ( vec[i].m_pObject == pObject ) return &vec[i]; } return NULL; } const CSharedObject *InternalFindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject& soIndex ) const; // Will return NULL if pre-commit operations/checks were successful, or a pointer to a descriptive error string if // pre-commit failed. const char *InternalPreCommit(); bool BAddNewObjectInternal( CSharedObject *pObject ); bool BTrackModifiedObjectInternal( CSharedObject *pObject, CSharedObject **out_ppWritableObject ); bool BRemoveObjectInternal( CSharedObject *pObject ); friend class CTrustedHelper_OutputAndSetErrorState; void SetErrorState() { m_bTransactionBuildSuccess = false; } private: CGCSharedObjectCache *m_pLockedSOCache; // we don't do any locking ourself, but verify that the lock is held during construction/modification CSharedObjectDirtyList m_SODirtyList; CSQLAccess *m_pSQLAccess; // our access to SQL -- may point to our inline instance or may point to an external object if we're hitching on an already-existing transaction bool m_bTransactionBuildSuccess; CSQLAccess m_sqlAccessInternal; struct CreateOrDestroyCommitInfo_t { CreateOrDestroyCommitInfo_t( CSharedObject *pObject ) : m_pObject( pObject ) { Assert( m_pObject ); } CSharedObject *m_pObject; }; struct ModifyCommitInfo_t { ModifyCommitInfo_t( CSharedObject *pWriteableObject, CSharedObject *pOriginalCopy ) : m_pWriteableObject( pWriteableObject ) , m_pObject( pOriginalCopy ) { } CSharedObject *m_pWriteableObject; // scratch/memory-writable copy while transaction is open CSharedObject *m_pObject; // original copy }; CUtlVector m_vecObjects_Added; CUtlVector m_vecObjects_Removed; CUtlVector m_vecObjects_Modified; }; class CSharedObjectTransaction { public: /** * Constructor that will begin a transaction * @param sqlAccess * @param pName */ CSharedObjectTransaction( CSQLAccess &sqlAccess, const char *pName ); /** * Destructor */ ~CSharedObjectTransaction(); /** * Adds an object that exists in the given CGCSharedObjectCache to be managed in this transaction. * Call this before making any modifications to the object * @param pSOCache the owner CGCSharedObjectCache * @param pObject the object that will be modified */ void AddManagedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); /** * Adds a brand new object to the given CGCSharedObjectCache * @param pSOCache the owner CGCSharedObjectCache * @param pObject the newly created object */ void AddNewObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); /** * Removes an existing object from the CGCSharedObjectCache * @param pSOCache the owner CGCSharedObjectCache * @param pObject the object to be removed from the CGCSharedObjectCache */ void RemoveObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); /** * Marks in the transaction that the object was modified. The object must have been previously added via * the AddManagedObject() call in order for the object to be marked dirty. If the object is new to the * CGCSharedObjectCache, then calling this will return false (which is not necessarily an error) * * @param pObject the object that will be modified * @param unFieldIdx the field that was changed */ void ModifiedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject, uint32 unFieldIdx ); /** * @param pSOCache * @param soIndex * @return the CSharedObject that matches either in the CGCSharedObjectCache or to be added */ template < class T > T *FindTypedSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex ) { return assert_cast( FindSharedObject( pSOCache, soIndex ) ); } /** * Rolls back any changes made to the objects in-memory and in the database * * This function should not be made virtual -- it's called from within the destructor. */ void Rollback(); /** * Commits any changes to the database and also to memory * @return true if successful, false otherwise */ bool BYieldingCommit( bool bAllowEmpty = false ); /** * @return GCSDK::CSQLAccess associated with this transaction */ CSQLAccess &GetSQLAccess() { return m_sqlAccess; } /** * Fetch name of transaction for debugging purposes * @return the string passed to the constructor */ const char *PchName() const; private: /** * @param pSOCache * @param soIndex * @return the CSharedObject that matches either in the list of currently-modified objects or the list * or of new objects we added; this will not search in the base SO cache */ CSharedObject *FindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex ); /** * Reverts all in-memory modifications and deletes all newly created objects. * * This function should not be made virtual -- it's called from within the destructor. */ void Undo(); /** * Set an error string to describe an error we encountered building this transaction. Setting this * will cause the transaction to fail. */ void SetError( const char *pszNewError ) { AssertMsg( pszNewError && ( pszNewError[0] != '\0' ), "Invalid NULL/empty error set in CSharedObjectTransaction::SetError()! This will have the effect of clearing the error state, which is unsupported." ); m_sErrorDesc = pszNewError; } /** * Clear our error string if we have one. This will allow transactions to succeed and so is only intended * to be done when the transaction itself has either completed (no either to begin with) or emptied (clean * slate). */ void ClearError() { Assert( m_vecObjects_Added.Count() == 0 ); Assert( m_vecObjects_Removed.Count() == 0 ); Assert( m_vecObjects_Modified.Count() == 0 ); m_sErrorDesc.Clear(); } /** * Get access to the string describing what, if any, the last error we encountered was. Will return * NULL if no errors have been encountered. */ const char *GetError() const { return m_sErrorDesc.IsEmpty() ? NULL : m_sErrorDesc.String(); } struct undoinfo_t { CSharedObject *pObject; CGCSharedObjectCache *pSOCache; CSharedObject *pOriginalCopy; bool operator==( const undoinfo_t& other ) const { return other.pObject == pObject && other.pSOCache == pSOCache && other.pOriginalCopy == pOriginalCopy; } }; // Wraps the common check to make sure these pointers are valid and that the cache is locked. This is non-const // because it can call SetError(). bool AssertValidInput( const CGCSharedObjectCache *pSOCache, const CSharedObject *pObject, const char *pszContext ); // Finds the object in the given vector (using simple pointer compare) undoinfo_t *FindObjectInVector( const CSharedObject *pObject, CUtlVector &vec ) const; // variables CUtlVector< undoinfo_t > m_vecObjects_Added; CUtlVector< undoinfo_t > m_vecObjects_Removed; CUtlVector< undoinfo_t > m_vecObjects_Modified; CSQLAccess &m_sqlAccess; // internal error state CUtlString m_sErrorDesc; // will be non-empty if we've encountered an error at some point building this transaction }; // class CSharedObjectTransaction }; // namespace GCSDK #endif // SHAREDOBJECTTRANSACTION_H