summaryrefslogtreecommitdiff
path: root/public/gcsdk/sharedobjecttransaction.h
diff options
context:
space:
mode:
Diffstat (limited to 'public/gcsdk/sharedobjecttransaction.h')
-rw-r--r--public/gcsdk/sharedobjecttransaction.h499
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