diff options
Diffstat (limited to 'public/gcsdk/sharedobjecttransaction.h')
| -rw-r--r-- | public/gcsdk/sharedobjecttransaction.h | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/public/gcsdk/sharedobjecttransaction.h b/public/gcsdk/sharedobjecttransaction.h new file mode 100644 index 0000000..da16c74 --- /dev/null +++ b/public/gcsdk/sharedobjecttransaction.h @@ -0,0 +1,499 @@ +//========= 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 <functional> + +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<TSharedObject>( 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<TSharedObject, CSharedObject>::kValue) ); + COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::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<TSharedObject, CSharedObject>::kValue) ); + COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::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<TSharedObject>::ModifyFunctionType& funcModifyAndAudit ) + { + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::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<TSharedObject *>( 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<TSharedObject, CSharedObject>::kValue) ); + COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::kValue ); + + m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject ); + m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject ); + } + + template < typename TSharedObject > + void RemoveObject( CSharedObject *pObject ) + { + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); + COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::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<TSchType, CSchItem>::kValue) ); + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) ); + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::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<TSchType, CSchItem>::kValue) ); + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) ); + COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::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<TSharedObject *>( 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<tCommitInfo>& 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<CreateOrDestroyCommitInfo_t> m_vecObjects_Added; + CUtlVector<CreateOrDestroyCommitInfo_t> m_vecObjects_Removed; + CUtlVector<ModifyCommitInfo_t> 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<T *>( 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<undoinfo_t> &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 |