//----------------------------------------------------------------------------- // // // File: destmsgq.cpp // // Description: Implementation of the CDestMsgQueue class // // Author: mikeswa // // Copyright (C) 1997 Microsoft Corporation // //----------------------------------------------------------------------------- #include "aqprecmp.h" #include "fifoqimp.h" #include "qwiklist.h" #include "aqutil.h" //---[ DEST_QUEUE_CONTEXT ]---------------------------------------------------- // // // Description: // Context used for DSN generation. This is created by // GenerateDSNsIfNecessary() and passed to the DMQ iterator function // HrWalkDMQForDSN(). // Hungarian: // dqcontext, pdqcontext // //----------------------------------------------------------------------------- class DEST_QUEUE_CONTEXT { private: friend HRESULT CDestMsgQueue::HrWalkDMQForDSN(IN CMsgRef *pmsgref, IN PVOID pvContext, OUT BOOL *pfContinue, OUT BOOL *pfDelete); CDestMsgQueue *m_pdmq; CQuickList *m_pql; HRESULT m_hrConnectionStatus; DWORD m_cMsgsSeenThisQueue; DWORD m_cDSNsGeneratedThisQueue; DWORD m_dwTickCountStart; BOOL m_bRetryQueue; public: //Contructor.... initializes and updates DWORD context DEST_QUEUE_CONTEXT(IN OUT DWORD *pdwContext, IN CDestMsgQueue *pdmq, IN CQuickList *pql, IN HRESULT hr) { _ASSERT(pdwContext); m_pdmq = pdmq; m_pql = pql; m_hrConnectionStatus = hr; m_cMsgsSeenThisQueue = 0; m_cDSNsGeneratedThisQueue = 0; m_bRetryQueue = FALSE; //Initialize/Update context if it has not been initialized if (!*pdwContext) *pdwContext = GetTickCount(); m_dwTickCountStart = *pdwContext; } ~DEST_QUEUE_CONTEXT() { TraceFunctEnterEx((LPARAM) this, "DEST_QUEUE_CONTEXT::~DEST_QUEUE_CONTEXT"); DWORD dwTickDiff = GetTickCount() - m_dwTickCountStart; DebugTrace((LPARAM) this, "DSN summary: %d milliseconds - %d msgs - %d DSNs", dwTickDiff, m_cMsgsSeenThisQueue, m_cDSNsGeneratedThisQueue); _ASSERT(m_cMsgsSeenThisQueue >= m_cDSNsGeneratedThisQueue); TraceFunctLeave(); } void SetRetry(BOOL bRetry) { m_bRetryQueue = bRetry; } BOOL fPastTimeLimit() { DWORD dwTickCountDiff = GetTickCount() - m_dwTickCountStart; if (dwTickCountDiff >= g_cMaxSecondsPerDSNsGenerationPass*1000) return TRUE; else return FALSE; } }; //---[ CDestMsgRetryQueue::CDestMsgRetryQueue ]-------------------------------- // // // Description: // Constructor for CDestMsgRetryQueue. // Parameters: // - // Returns: // - // History: // 10/25/1999 - MikeSwa Created // //----------------------------------------------------------------------------- CDestMsgRetryQueue::CDestMsgRetryQueue() { m_dwSignature = DESTMSGRETRYQ_SIG; m_cRetryReferenceCount = 0; m_pdmq = NULL; } //---[ CDestMsgRetryQueue::HrRetryMsg ]---------------------------------------- // // // Description: // Puts a message into the retry queue // Parameters: // pmsgref Message to put into retry queue // Returns: // S_OK on success // E_INVALIDARG if no refcount (asserts in DBG) // History: // 10/25/1999 - MikeSwa Created // //----------------------------------------------------------------------------- HRESULT CDestMsgRetryQueue::HrRetryMsg(IN CMsgRef *pmsgref) { _ASSERT(m_pdmq); _ASSERT(m_cRetryReferenceCount); _ASSERT(DESTMSGRETRYQ_SIG == m_dwSignature); if (!m_pdmq || !m_cRetryReferenceCount) { return E_INVALIDARG; } m_pdmq->AssertSignature(); return (m_pdmq->HrRetryMsg(pmsgref)); } //---[ CDestMsgQueueHrWalkDMQForDSN ]------------------------------------------ // // // Description: // Example default function to use with HrMapFn... will always return TRUE // to continue and delete the current queued data // Parameters: // IN CMsgRef pmsgref, //ptr to data on queue // IN PVOID pvContext //list of queues to prepare for DSN // OUT BOOL *pfContinue, //TRUE if we should continue // OUT BOOL *pfDelete); //TRUE if item should be deleted // Returns: // S_OK // History: // 7/13/98 - MikeSwa Created //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrWalkDMQForDSN(IN CMsgRef *pmsgref, IN PVOID pvContext, OUT BOOL *pfContinue, OUT BOOL *pfDelete) { TraceFunctEnterEx((LPARAM) pmsgref, "CDestMsgQueue::HrWalkDMQForDSN"); Assert(pfContinue); Assert(pfDelete); HRESULT hr = S_OK; HRESULT hrReason = S_OK; DWORD dwDSNFlags = 0; DWORD dwMsgRefDSNOptions = CMsgRef::MSGREF_DSN_SEND_DELAY | CMsgRef::MSGREF_DSN_HAS_ROUTING_LOCK; DEST_QUEUE_CONTEXT *pdqcontext = (DEST_QUEUE_CONTEXT *) pvContext; CLinkMsgQueue *plmq = NULL; CQuickList quicklist; CQuickList *pql; DWORD dwIndex; _ASSERT(pdqcontext->m_pdmq); _ASSERT(pdqcontext->m_pql); *pfContinue = TRUE; *pfDelete = FALSE; //See if we got the shutdown hint... if so bail if (pdqcontext->m_pdmq->m_paqinst->fShutdownSignaled()) { *pfContinue = FALSE; goto Exit; } //Check and make sure that a routing change is not pending if (!pdqcontext->m_pdmq->m_paqinst || !pdqcontext->m_pdmq->m_paqinst->fTryRoutingShareLock()) { *pfContinue = FALSE; goto Exit; } pdqcontext->m_pdmq->m_paqinst->RoutingShareUnlock(); //Check and see if we have hit our limit. We force ourselves //to generate at least one DSN, so we make some forward progress //each pass. if (pdqcontext->m_cDSNsGeneratedThisQueue && pdqcontext->fPastTimeLimit()) { *pfContinue = FALSE; hr = HRESULT_FROM_WIN32(E_PENDING); goto Exit; } //Avoid holding the lock while making external calls other than AddRef etc pdqcontext->m_pdmq->m_slPrivateData.ShareLock(); plmq = pdqcontext->m_pdmq->m_plmq; if (plmq) plmq->AddRef(); pdqcontext->m_pdmq->m_slPrivateData.ShareUnlock(); //$$REVIEW: Holding the lock for such a short time is something of a moot //point here, since a Sharelock is held at the link level to assure that //the qwiklist passed in as part of our context does not change if (plmq) { //We should not send delay DSNs to TURN/ETRN domains if (plmq->dwGetLinkState() & LINK_STATE_PRIV_CONFIG_TURN_ETRN) dwMsgRefDSNOptions &= ~CMsgRef::MSGREF_DSN_SEND_DELAY; plmq->Release(); } if(pdqcontext->m_pdmq->m_hrRoutingDiag != S_OK) { // // This message is being NDR'ed because routing had a problem with // it. Pass in the HRESULT from routing so that we can use it during // DSN generation. Pass in a quicklist with only the CDestMsgQueue // on which the routing error occured. // hrReason = pdqcontext->m_pdmq->m_hrRoutingDiag; DebugTrace((LPARAM)pmsgref, "Generating DSN due to routing, hr - %08x", hrReason); hr = quicklist.HrAppendItem(pdqcontext->m_pdmq, &dwIndex); if(FAILED(hr)) { ErrorTrace((LPARAM)pmsgref, "Unable to generate DSN for msg"); goto Exit; } pql = &quicklist; } else { hrReason = pdqcontext->m_hrConnectionStatus; pql = pdqcontext->m_pql; } if (pdqcontext->m_pdmq->m_dwFlags & DMQ_CHECK_FOR_STALE_MSGS) { DebugTrace((LPARAM) pmsgref, "Enabling checking for stale messages"); dwMsgRefDSNOptions |= CMsgRef::MSGREF_DSN_CHECK_IF_STALE; } hr = pmsgref->HrSendDelayOrNDR(dwMsgRefDSNOptions, pql, hrReason, &dwDSNFlags); if (FAILED(hr)) goto Exit; //NOTE: Although it would be tempting to return *pfContinue as FALSE if //MSGREF_HAS_NOT_EXPIRED was set, it would be wrong since queues may be //out of order on startup... and some sink may modify the expiration time //(for example... routing may want to expire low-priority messages earlier). //We need to remove this message from the queue if ((CMsgRef::MSGREF_DSN_SENT_NDR | CMsgRef::MSGREF_HANDLED) & dwDSNFlags) { *pfDelete = TRUE; if (pdqcontext->m_bRetryQueue) pdqcontext->m_pdmq->UpdateRetryStats(FALSE); else pdqcontext->m_pdmq->UpdateMsgStats(pmsgref, FALSE); } //Update counts in context pdqcontext->m_cMsgsSeenThisQueue++; if ((CMsgRef::MSGREF_DSN_SENT_NDR | CMsgRef::MSGREF_DSN_SENT_DELAY) & dwDSNFlags) { pdqcontext->m_cDSNsGeneratedThisQueue++; } Exit: if (AQUEUE_E_SHUTDOWN == hr) { *pfContinue = FALSE; hr = S_OK; } TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::HrWalkQueueForShutdown ]-------------------------------- // // // Description: // Static function to walk a queue containing msgrefs at shutdown and // clear out all of the IMailMsgs // Parameters: // IN CMsgRef pmsgref, ptr to data on queue // IN PVOID pvContext Pointer to CDestMsgQueue we are walking for // shutdown. // OUT BOOL *pfContinue, TRUE if we should continue // OUT BOOL *pfDelete); TRUE if item should be deleted // Returns: // S_OK - *always* // History: // 11/18/98 - MikeSwa Created //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrWalkQueueForShutdown(IN CMsgRef *pmsgref, IN PVOID pvContext, OUT BOOL *pfContinue, OUT BOOL *pfDelete) { TraceFunctEnterEx((LPARAM) pmsgref, "HrWalkMsgRefQueueForShutdown"); Assert(pfContinue); Assert(pfDelete); CDestMsgQueue *pdmq = (CDestMsgQueue *) pvContext; _ASSERT(pmsgref); _ASSERT(pdmq); _ASSERT(DESTMSGQ_SIG == pdmq->m_dwSignature); *pfContinue = TRUE; *pfDelete = TRUE; //call server stop hint function if (pdmq->m_paqinst) pdmq->m_paqinst->ServerStopHintFunction(); //Update stats pdmq->UpdateMsgStats(pmsgref, FALSE); pmsgref->AddRef(); pdmq->m_paqinst->HrQueueWorkItem(pmsgref, fMsgRefShutdownCompletion); TraceFunctLeave(); return S_OK; } //---[ CDestMsgQueue::HrWalkRetryQueueForShutdown ]-------------------------------- // // // Description: // Static function to walk a retry queue containing msgrefs at shutdown and // clear out all of the IMailMsgs // Parameters: // IN CMsgRef pmsgref, ptr to data on queue // IN PVOID pvContext Pointer to CDestMsgQueue we are walking for // shutdown. // OUT BOOL *pfContinue, TRUE if we should continue // OUT BOOL *pfDelete); TRUE if item should be deleted // Returns: // S_OK - *always* //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrWalkRetryQueueForShutdown(IN CMsgRef *pmsgref, IN PVOID pvContext, OUT BOOL *pfContinue, OUT BOOL *pfDelete) { TraceFunctEnterEx((LPARAM) pmsgref, "HrWalkMsgRefQueueForShutdown"); Assert(pfContinue); Assert(pfDelete); CDestMsgQueue *pdmq = (CDestMsgQueue *) pvContext; _ASSERT(pmsgref); _ASSERT(pdmq); _ASSERT(DESTMSGQ_SIG == pdmq->m_dwSignature); *pfContinue = TRUE; *pfDelete = TRUE; //call server stop hint function if (pdmq->m_paqinst) pdmq->m_paqinst->ServerStopHintFunction(); //Update retry stats pdmq->UpdateRetryStats(FALSE); pmsgref->AddRef(); pdmq->m_paqinst->HrQueueWorkItem(pmsgref, fMsgRefShutdownCompletion); TraceFunctLeave(); return S_OK; } //---[ CDestMsgQueue::CDestMsgQueue() ]---------------------------------------- // // // Description: // Class constructor // Parameters: // IN paqinst AQ virtual server object // IN paqmtMessageType Message type for this queue // IN pIMessageRouter IMessageRouter interface for this queue // Returns: // - //----------------------------------------------------------------------------- CDestMsgQueue::CDestMsgQueue(CAQSvrInst *paqinst, CAQMessageType *paqmtMessageType, IMessageRouter *pIMessageRouter) : m_aqmt(paqmtMessageType) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::CDestMsgQueue"); _ASSERT(paqinst); _ASSERT(pIMessageRouter); m_dwSignature = DESTMSGQ_SIG; m_dwFlags = DMQ_EMPTY; m_pIMessageRouter = pIMessageRouter; m_plmq = NULL; m_paqinst = paqinst; m_cMessageTypeRefs = 0; m_pvLinkContext = NULL; m_cCurrentThreadsEnqueuing = 0; m_hrRoutingDiag = S_OK; m_pIMessageRouter->AddRef(); m_paqinst->AddRef(); m_paqinst->IncDestQueueCount(); m_liDomainEntryDMQs.Flink = NULL; m_liDomainEntryDMQs.Blink = NULL; m_liEmptyDMQs.Flink = NULL; m_liEmptyDMQs.Blink = NULL; m_cRemovedFromEmptyList = 0; ZeroMemory(m_rgpfqQueues, NUM_PRIORITIES*sizeof(CFifoQueue **)); ZeroMemory(&m_ftOldest, sizeof (FILETIME)); m_dmrq.m_pdmq = this; TraceFunctLeave(); } //---[ CDestMsgQueue::~CDestMsgQueue() ]--------------------------------------- // // // Description: // Default destructor // Parameters: // - // Returns: // - //----------------------------------------------------------------------------- CDestMsgQueue::~CDestMsgQueue() { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::~CDestMsgQueue"); for (int i = 0; i < NUM_PRIORITIES; i++) { if (NULL != m_rgpfqQueues[i]) delete m_rgpfqQueues[i]; } //Make sure we clean up the link even if HrDeinitialize wasn't called if (m_plmq) { m_plmq->HrDeinitialize(); m_plmq->Release(); m_plmq = NULL; } if (m_pIMessageRouter) { _ASSERT((!m_cMessageTypeRefs) && "Message Type references in destructor"); m_pIMessageRouter->Release(); m_pIMessageRouter = NULL; } if (m_paqinst) { m_paqinst->DecDestQueueCount(); m_paqinst->Release(); m_paqinst = NULL; } _ASSERT(NULL == m_liDomainEntryDMQs.Flink); _ASSERT(NULL == m_liDomainEntryDMQs.Blink); _ASSERT(!m_cCurrentThreadsEnqueuing); MARK_SIG_AS_DELETED(m_dwSignature); TraceFunctLeave(); } //---[ CDestMsgQueue::HrInitialize() ]----------------------------------------- // // // Description: // Performs initialization that may require allocation // Parameters: // IN CDomainMapping *pdmap //array of domain mappings to use // Returns: // S_OK on success // E_OUTOFMEMORY if allocations fail //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrInitialize(IN CDomainMapping *pdmap) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::HrInitialize"); HRESULT hr = S_OK; DWORD i = 0; //loop counter _ASSERT(pdmap); if (!pdmap) { hr = E_INVALIDARG; goto Exit; } //If the queues are compressed (more than one domain name per destination), //then each queue will contain multiple domain mappings (1 for each domain) m_dmap.Clone(pdmap); Exit: TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::HrDeinitialize ]----------------------------------------- // // // Description: // Deinitialize object // Parameters: // - // Returns: // S_OK on success // //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrDeinitialize() { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::HrDeinitialize"); HRESULT hr = S_OK; HRESULT hrTmp = S_OK; DWORD cMsgsRemoved =0; dwInterlockedSetBits(&m_dwFlags, DMQ_SHUTDOWN_SIGNALED); for (int i = 0; i < NUM_PRIORITIES; i++) { if (NULL != m_rgpfqQueues[i]) { hrTmp = m_rgpfqQueues[i]->HrMapFn(CDestMsgQueue::HrWalkQueueForShutdown, this, &cMsgsRemoved); //This should really never fail, since HrMapFn will only return errors from //the function walking the queues (which in this case never fails) _ASSERT(SUCCEEDED(hrTmp)); //This *should* have removed all msgs _ASSERT(!m_aqstats.m_cMsgs && "Still msgs in queue after Deinit"); } } m_fqRetryQueue.HrMapFn(CDestMsgQueue::HrWalkRetryQueueForShutdown, this, NULL); if (m_pIMessageRouter) { if (m_cMessageTypeRefs) { hr = m_pIMessageRouter->ReleaseMessageType(m_aqmt.dwGetMessageType(), m_cMessageTypeRefs); _ASSERT(SUCCEEDED(hr) && "Release Message Type Failed"); m_cMessageTypeRefs = 0; } m_pIMessageRouter->Release(); m_pIMessageRouter = NULL; } else { _ASSERT((!m_cMessageTypeRefs) && "We're leaking message type references"); } if (m_paqinst) { m_paqinst->DecDestQueueCount(); m_paqinst->Release(); m_paqinst = NULL; } if (m_plmq) { m_plmq->Release(); m_plmq = NULL; } TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::HrAddMsg ]---------------------------------------------- // // // Description: // Enqueues or Requeues a message to the appropriate priority queue, // allocating queue if not present. // // A notification will be sent if needed (& requested) to the associated // link object. The fNotify argument was originally included to prevent // messages from the retry queue causing notifications. // Parameters: // IN CMsgRef *pmsgref - the message ref to enqueue // IN BOOL fEnqueue - TRUE => enqueue and FALSE => requeue // IN BOOL fNotify - TRUE => send notification if necessary. // Returns: // S_OK on success // //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrAddMsg(IN CMsgRef *pmsgref, IN BOOL fEnqueue, IN BOOL fNotify) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::HrAddMsg"); HRESULT hr = S_OK; DWORD dwFlags = 0; DWORD iQueueIndex = (DWORD) eEffPriNormal; CFifoQueue *pfqQueue = NULL; CFifoQueue *pfqQueueNew= NULL; _ASSERT(pmsgref); _ASSERT(m_aqmt.fIsEqual(pmsgref->paqmtGetMessageType())); _ASSERT(!(m_dwFlags & (DMQ_INVALID | DMQ_SHUTDOWN_SIGNALED))); //get the priority from the message reference iQueueIndex = (DWORD) pmsgref->PriGetPriority(); // // If the message is marked as problem... treat with lower priority. // if (iQueueIndex && pmsgref->fIsProblemMsg()) iQueueIndex--; //use priority to get to get ptr to correct queue _ASSERT(iQueueIndex < NUM_PRIORITIES); pfqQueue = m_rgpfqQueues[iQueueIndex]; if (NULL == pfqQueue) //we must allocate a queue { pfqQueueNew = new CFifoQueue(); if (NULL != pfqQueueNew) { pfqQueue = (CFifoQueue *) InterlockedCompareExchangePointer( (VOID **) &(m_rgpfqQueues[iQueueIndex]), (VOID *) pfqQueueNew, NULL); if (NULL != pfqQueue) { //someone else updated first delete pfqQueueNew; } else { //Our updated worked pfqQueue = pfqQueueNew; } pfqQueueNew = NULL; } else //allocation failed { hr = E_OUTOFMEMORY; goto Exit; } } //at this point queue ptr should be good _ASSERT(pfqQueue); //Assume enqueue will work - optimize to avoid dealing with negative stats //Mark queue as non-empty dwFlags = dwInterlockedUnsetBits(&m_dwFlags, DMQ_EMPTY); if (DMQ_EMPTY & dwFlags) UpdateOldest(pmsgref->pftGetAge()); // // Keep track of how many threads are enqueuing so that we know what // is the most we can expect to be off in our message count. // InterlockedIncrement((PLONG) &m_cCurrentThreadsEnqueuing); //Add the msg to the appropriate queue if (fEnqueue) hr = pfqQueue->HrEnqueue(pmsgref); else hr = pfqQueue->HrRequeue(pmsgref); //If the enqueue/requeue succeeded, update the stats. DO NOT update the //stats before the enqueue/requeue attempt. This will cause the link to //wake up and start spinning off connections before the msg is enqueued. //See bug 88931 if (SUCCEEDED(hr)) UpdateMsgStats(pmsgref, TRUE); InterlockedDecrement((PLONG) &m_cCurrentThreadsEnqueuing); Exit: TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::HrDequeueMsg ]------------------------------------------- // // // Description: // Finds and dequeues the next message. All OUT parameters are // ref-counted. The call is responsible for there release // Parameters: // IN priLowestPriority - Lowest priority message allowed // OUT ppmsgref - MsgRef dequeued // OUT ppdmrq - Ptr to retry interface (can be NULL) // Returns: // NO_ERROR if successful // AQUEUE_E_QUEUE_EMPTY if no messages in queue //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrDequeueMsg( IN DWORD priLowestPriority, OUT CMsgRef **ppmsgref, OUT CDestMsgRetryQueue **ppdmrq) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::HrDequeueMsg"); Assert(ppmsgref); HRESULT hr = S_OK; DWORD priCurrent = eEffPriHigh; //start at highest priority *ppmsgref = NULL; Assert(priCurrent < NUM_PRIORITIES); hr = AQUEUE_E_QUEUE_EMPTY; while (TRUE) { if (NULL != m_rgpfqQueues[priCurrent]) { hr = m_rgpfqQueues[priCurrent]->HrDequeue(ppmsgref); if (SUCCEEDED(hr)) { if ((*ppmsgref)->fIsMsgFrozen()) { //Msg is frozen, we need to put it in //the retry queue and get the next one //We must call UpdateMsgStats,because //MergeRetryQueue will re-add it. UpdateMsgStats(*ppmsgref, FALSE); hr = HrRetryMsg(*ppmsgref); if (FAILED(hr)) goto Exit; (*ppmsgref)->Release(); *ppmsgref = NULL; continue; } else { break; } } else if (hr != AQUEUE_E_QUEUE_EMPTY) { //some unexpected error has occured goto Exit; } } //otherwise decrement the priority if (priCurrent == eEffPriLow) break; // // If there are no messages at the current lowest priority, // then treat this queue as empty // if (priCurrent <= priLowestPriority) { hr = AQUEUE_E_QUEUE_EMPTY; goto Exit; } Assert(eEffPriLow < priCurrent); priCurrent--; } if (FAILED(hr)) goto Exit; Assert(*ppmsgref); //Before we update stats. AddRef the retry interface so there is //no timing window where the queue is erroniously marked as empty if (ppdmrq) { *ppdmrq = &m_dmrq; m_dmrq.AddRef(); } UpdateMsgStats(*ppmsgref, FALSE); //approximate oldest UpdateOldest((*ppmsgref)->pftGetAge()); Exit: TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::UpdateMsgStats ]--------------------------------------- // // // Description: // Updates stats. A shared lock must be aquired before calling into this. // Parameters: // IN pmsgref - message reference added or removed // IN fAdd - TRUE => msgref is being added the queue // FALSE => msgref is being removed from the queue // Returns: // - // //----------------------------------------------------------------------------- void CDestMsgQueue::UpdateMsgStats(IN CMsgRef *pmsgref, IN BOOL fAdd) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::UpdateMsgStats"); Assert(pmsgref); CAQStats aqstats; if (fAdd) { m_paqinst->IncQueueMsgInstances(); } else { m_paqinst->DecQueueMsgInstances(); } aqstats.m_cMsgs = 1; aqstats.m_rgcMsgPriorities[pmsgref->PriGetPriority()] = 1; aqstats.m_uliVolume.QuadPart = (ULONGLONG) pmsgref->dwGetMsgSize(); aqstats.m_pdmq = this; aqstats.m_dwNotifyType = NotifyTypeDestMsgQueue; aqstats.m_dwHighestPri = pmsgref->PriGetPriority(); //Keep track of the number of *other* domains this is being sent to, so //that we can make an accurate guess when to create connections aqstats.m_cOtherDomainsMsgSpread = pmsgref->cGetNumDomains()-1; // // Make sure that our stats are within reason. We expect to be negative // for short periods of time, but never more negative than the // number of threads currently enqueueing. // _ASSERT(m_aqstats.m_cMsgs+m_cCurrentThreadsEnqueuing < 0xFFFFFFF0); m_slPrivateData.ShareLock(); m_aqstats.UpdateStats(&aqstats, fAdd); //send notification off to link if (m_plmq) { //Caller does not care about success of notification... only //about updating stats m_plmq->HrNotify(&aqstats, fAdd); } m_slPrivateData.ShareUnlock(); TraceFunctLeave(); } //---[ CDestMsgQueue::UpdateRetryStats ]--------------------------------------- // // // Description: // Updates retry stats. A shared lock must be aquired before calling into this. // Parameters: // IN fAdd - TRUE => msgref is being added the queue // FALSE => msgref is being removed from the queue // Returns: // - // //----------------------------------------------------------------------------- void CDestMsgQueue::UpdateRetryStats(IN BOOL fAdd) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::UpdateRetryStats"); if (fAdd) { m_paqinst->IncQueueMsgInstances(); } else { m_paqinst->DecQueueMsgInstances(); } m_slPrivateData.ShareLock(); m_aqstats.UpdateRetryStats(fAdd); if (m_plmq) { //Caller does not care about success of notification... only //about updating stats m_plmq->HrNotifyRetryStatChange(fAdd); } m_slPrivateData.ShareUnlock(); TraceFunctLeave(); } //---[ CDestMsgQueue::HrRetryMsg ]--------------------------------------------- // // // Description: // Add an message to the queue for retry. This will put a message in // a retry queue (that is not usually checked during HrDequeueMessage) // Parameters: // IN pmsgref Message to add to the queue for retry // Returns: // S_OK on success // //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrRetryMsg(IN CMsgRef *pmsgref) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::HrRetryMsg"); HRESULT hr = S_OK; _ASSERT(pmsgref); hr = m_fqRetryQueue.HrRequeue(pmsgref); //If we couldn't put it in retry queue... retry when all references //have been released if (FAILED(hr)) pmsgref->RetryOnDelete(); else UpdateRetryStats(true); hr = S_OK; TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::MarkQueueEmptyIfNecessary ]------------------------------ // // // Description: // Checks and sees if it is OK to mark the queue as empty. Will // insert it in the empty list if needed. // If queue is now empty (and not tagged as empty), then we need to put // it in the empty queue list. If it is already tagged as empty, then // it is already in the empty queue list with the appropirate expire time. // Parameters: // - // Returns: // - // History: // 10/25/1999 - MikeSwa Created (separated from MergeRetryQueue()) // //----------------------------------------------------------------------------- void CDestMsgQueue::MarkQueueEmptyIfNecessary() { //A queue cannot be considered empty if any of the following conditions // - There are messages queued up for delivery // - There are messages pending ack (someone has a reference to the // retry interface) // - There are messages pending retry // if we can't get the shutdown lock then there is no reason to mark the // queue as empty, since it will go away when we shutdown if (m_paqinst->fTryShutdownLock()) { //To be thread safe we should check in the opposite order that they //are set/unset. On dequeue, we add a ref count, then update stats. On //retry we update the retry stats, and then release. // if (!m_aqstats.m_cMsgs && !m_dmrq.m_cRetryReferenceCount && !m_fqRetryQueue.cGetCount() && !(m_dwFlags & DMQ_EMPTY)) { m_paqinst->pdmtGetDMT()->AddDMQToEmptyList(this); } m_paqinst->ShutdownUnlock(); } } //---[ CDestMsgQueue::HrGenerateDSNsIfNecessary ]----------------------------- // // // Description: // Merge Messages from retry queue into main priority queues and // generates DSNs if neccessary. // Parameters: // IN pqlQueues List of queues to pass to DSN code // IN hrConnectionStatus HRESULT that should be passed to DSN generation // code. // IN OUT pdwContext Context that is used to throttle // DSN generation. Should be initialzed to // 0 on first call. Actually used to store // the tick count when we started DSN generation // Returns: // Failures will be handled internally // S_OK - success, and all messages processed // HRESULT_FROM_WIN32(E_PENDING) - more messages left to processes // History: // 11/10/1999 - MikeSwa Modified to return pending error // //----------------------------------------------------------------------------- HRESULT CDestMsgQueue::HrGenerateDSNsIfNecessary(IN CQuickList *pqlQueues, IN HRESULT hrConnectionStatus, IN OUT DWORD *pdwContext) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::GenerateDSNsIfNecessary"); HRESULT hr = S_OK; int i = 0; DEST_QUEUE_CONTEXT dqcontext(pdwContext, this, pqlQueues, hrConnectionStatus); //Re-merge retry queue MergeRetryQueue(); //Check re-try queue as well since we may have frozen messages that need to //be NDR'd or DSN'd // this is on a retry queue, HrWalkDMQForDSN should call UpdateMsgStats dqcontext.SetRetry(TRUE); hr = m_fqRetryQueue.HrMapFn(CDestMsgQueue::HrWalkDMQForDSN, &dqcontext, NULL); if (FAILED(hr)) { if (HRESULT_FROM_WIN32(E_PENDING) == hr) { DebugTrace((LPARAM) this, "Hit DSN generation limit, must continue DSN genration later"); goto Exit; } ErrorTrace((LPARAM) this, "ERROR: Unable to Check Queues for DSNs - hr 0x%08X", hr); hr = S_OK; } dqcontext.SetRetry(FALSE); for (i = 0; i < NUM_PRIORITIES; i++) { if (NULL != m_rgpfqQueues[i]) { hr = m_rgpfqQueues[i]->HrMapFn(CDestMsgQueue::HrWalkDMQForDSN, &dqcontext, NULL); if (FAILED(hr)) { if (HRESULT_FROM_WIN32(E_PENDING) == hr) { DebugTrace((LPARAM) this, "Hit msg limit, must continue DSN genration later"); goto Exit; } ErrorTrace((LPARAM) this, "ERROR: Unable to Check Queues for DSNs - hr 0x%08X", hr); hr = S_OK; } } } // // If we where checking for stale messages, we should stop until we // hit another stale message on a message ack // dwInterlockedUnsetBits(&m_dwFlags, DMQ_CHECK_FOR_STALE_MSGS); Exit: MarkQueueEmptyIfNecessary(); TraceFunctLeave(); return hr; } //---[ CDestMsgQueue::MergeRetryQueue ]------------------------------------------ // // // Description: // Merges retry queues with normal queues. Will keep frozen msgs in // retry queue. // Parameters: // - // Returns: // - // History: // 12/13/98 - MikeSwa split from original MergeRetryQueue // (now called GenerateDSNsIfNecessary) // //----------------------------------------------------------------------------- void CDestMsgQueue::MergeRetryQueue() { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::MergeRetryQueue"); HRESULT hr = S_OK; CMsgRef *pmsgref = NULL; CMsgRef *pmsgrefFirstFrozen = NULL; DWORD cMsgsInRetry = m_fqRetryQueue.cGetCount(); DWORD cMsgsProcessed = 0; while (SUCCEEDED(hr)) { //While we have a mechanism to loop through the queue only once by //checking the pmsgrefFirstFrozen pointer. It is possible that another //thread will remove that from the queue (unfreeze, NDR, etc), so it //is important that we have a failsafe mechanism. Worst case here, is //that we will see every message twice, but lets us handle extra //messages added to the retry queue. if (cMsgsProcessed++ > 2*cMsgsInRetry) break; hr = m_fqRetryQueue.HrDequeue(&pmsgref); if (FAILED(hr)) break; UpdateRetryStats(false); //Handle frozen messages sitting in the retry queue if (pmsgref->fIsMsgFrozen()) { //Message is frozen, we are keeping it in the retry queue hr = m_fqRetryQueue.HrEnqueue(pmsgref); if (FAILED(hr)) { //Mark Msgref as retry pmsgref->RetryOnDelete(); ErrorTrace((LPARAM) this, "ERROR: Unable to add frozen msg to retry queue - msg 0x%X", pmsgref); } else { UpdateRetryStats(true); } pmsgref->Release(); //See if we've made it all the way through the retry queue if (!pmsgrefFirstFrozen) pmsgrefFirstFrozen = pmsgref; else if (pmsgref == pmsgrefFirstFrozen) break; } else { //Re-queue non-frozen message for delivery hr = HrAddMsg(pmsgref, FALSE, FALSE); if (FAILED(hr)) { pmsgref->RetryOnDelete(); ErrorTrace((LPARAM) this, "ERROR: Unable to merge retry queue - msg 0x%X", pmsgref); } pmsgref->Release(); } } MarkQueueEmptyIfNecessary(); TraceFunctLeave(); } //---[ CDestMsgQueue::RemoveDMQFromLink ]-------------------------------------- // // // Description: // Removes this DMQ from its associated link // Parameters: // fNotifyLink TRUE if not being called by owning link, and link needs // to be notified // Returns: // - // History: // 9/14/98 - MikeSwa Created // 11/6/98 - MikeSwa Modified to allow changes to routing info // //----------------------------------------------------------------------------- void CDestMsgQueue::RemoveDMQFromLink(BOOL fNotifyLink) { _ASSERT(DESTMSGQ_SIG == m_dwSignature); CLinkMsgQueue *plmq = NULL; CAQStats aqstats; m_slPrivateData.ExclusiveLock(); plmq = m_plmq; m_plmq = NULL; if (plmq && fNotifyLink) memcpy(&aqstats, &m_aqstats, sizeof(CAQStats)); m_slPrivateData.ExclusiveUnlock(); if (plmq) { if (fNotifyLink) plmq->RemoveQueue(this, &aqstats); plmq->Release(); } } //---[ CDestMsgQueue::SetRouteInfo ]------------------------------------------- // // // Description: // Sets the routing information for this domain. Will blow away any // previous routing info. // Parameters: // IN plmq Link to associate with this domain. // Returns: // - // History: // 11/6/98 - MikeSwa Created // //----------------------------------------------------------------------------- void CDestMsgQueue::SetRouteInfo(CLinkMsgQueue *plmq) { TraceFunctEnterEx((LPARAM) this, "CDestMsgQueue::SetRouteInfo"); HRESULT hr = S_OK; CAQStats aqstats; //First blow-away old routing info RemoveDMQFromLink(TRUE); //Grab lock and update routing info m_slPrivateData.ExclusiveLock(); m_plmq = plmq; if (plmq) { plmq->AddRef(); memcpy(&aqstats, &m_aqstats, sizeof(CAQStats)); aqstats.m_dwNotifyType |= NotifyTypeDestMsgQueue; hr = plmq->HrNotify(&aqstats, TRUE); if (FAILED(hr)) { //nothing really we can do ErrorTrace((LPARAM) this, "ERROR: Unable to update link stats - hr 0x%08X", hr); } } m_slPrivateData.ExclusiveUnlock(); TraceFunctLeave(); } //---[ CDestMsgQueue::plmqGetLink ]-------------------------------------------- // // // Description: // Returns the Addref'd Link for the Queue. // Parameters: // - // Returns: // Ptr to CLinkMsgQueue // History: // 5/14/99 - MikeSwa Created // //----------------------------------------------------------------------------- CLinkMsgQueue *CDestMsgQueue::plmqGetLink() { CLinkMsgQueue *plmq = NULL; m_slPrivateData.ShareLock(); plmq = m_plmq; if (plmq) plmq->AddRef(); m_slPrivateData.ShareUnlock(); return plmq; } //---[ CDestMsgQueue::HrGetQueueID ]-------------------------------------------- // // // Description: // Gets the QueueID for this DMQ. Used by Queue Admin // Parameters: // IN OUT pQueueID QUEUELINK_ID struct to fill in // Returns: // S_OK on success // E_OUTOFMEMORY if unable to allocate memory for queue name. // History: // 12/3/98 - MikeSwa Created // 2/23/99 - MikeSwa Modified to be part of IQueueAdminQueue interface // //----------------------------------------------------------------------------- STDMETHODIMP CDestMsgQueue::HrGetQueueID(QUEUELINK_ID *pQueueID) { DWORD cbDomainName = m_dmap.pdentryGetQueueEntry()->cbGetDomainNameLength(); LPSTR szDomainName = m_dmap.pdentryGetQueueEntry()->szGetDomainName(); pQueueID->qltType = QLT_QUEUE; pQueueID->dwId = m_aqmt.dwGetMessageType(); m_aqmt.GetGUID(&pQueueID->uuid); pQueueID->szName = wszQueueAdminConvertToUnicode(szDomainName, cbDomainName); if (!pQueueID->szName) return E_OUTOFMEMORY; return S_OK; } //---[ CDestMsgQueue::HrGetQueueInfo ]------------------------------------------ // // // Description: // Gets the Queue Admin infor for this Queue // Parameters: // IN OUT pqiQueueInfo Ptr to Queue Info Stucture to fill // Returns: // S_OK on success // E_OUTOFMEMORY if unable to allocate memory for queue name. // History: // 12/5/98 - MikeSwa Created // 2/22/99 - MikeSwa changed to COM function // //----------------------------------------------------------------------------- STDMETHODIMP CDestMsgQueue::HrGetQueueInfo(QUEUE_INFO *pqiQueueInfo) { DWORD cbDomainName = m_dmap.pdentryGetQueueEntry()->cbGetDomainNameLength(); LPSTR szDomainName = m_dmap.pdentryGetQueueEntry()->szGetDomainName(); HRESULT hr = S_OK; //Get # of messages = # in queue + failed msgs pqiQueueInfo->cMessages = m_aqstats.m_cMsgs + cGetFailedMsgs(); //Get DMQ name pqiQueueInfo->szQueueName = wszQueueAdminConvertToUnicode(szDomainName, cbDomainName); if (!pqiQueueInfo->szQueueName) { hr = E_OUTOFMEMORY; goto Exit; } pqiQueueInfo->cbQueueVolume.QuadPart = m_aqstats.m_uliVolume.QuadPart; pqiQueueInfo->dwMsgEnumFlagsSupported = AQUEUE_DEFAULT_SUPPORTED_ENUM_FILTERS; //Get Link name m_slPrivateData.ShareLock(); if (m_plmq && !m_plmq->fRPCCopyName(&pqiQueueInfo->szLinkName)) hr = E_OUTOFMEMORY; m_slPrivateData.ShareUnlock(); Exit: return hr; } //---[ CDestMsgQueue::UpdateOldest ]------------------------------------------- // // // Description: // Updates the age of the "oldest" message in the queue // Parameters: // pft Ptr to filetime of oldest nessage // Returns: // - // History: // 12/13/98 - MikeSwa Created // //----------------------------------------------------------------------------- void CDestMsgQueue::UpdateOldest(FILETIME *pft) { DWORD dwFlags = dwInterlockedSetBits(&m_dwFlags, DMQ_UPDATING_OLDEST_TIME); if (!(DMQ_UPDATING_OLDEST_TIME & dwFlags)) { //we got the spin lock memcpy(&m_ftOldest, pft, sizeof(FILETIME)); dwInterlockedUnsetBits(&m_dwFlags, DMQ_UPDATING_OLDEST_TIME); } } //---[ CDestMsgQueue::QueryInterface ]----------------------------------------- // // // Description: // QueryInterface for CDestMsgQueue that supports: // - IQueueAdminAction // - IUnknown // - IQueueAdminQueue // Parameters: // // Returns: // // History: // 2/21/99 - MikeSwa Created // //----------------------------------------------------------------------------- STDMETHODIMP CDestMsgQueue::QueryInterface(REFIID riid, LPVOID *ppvObj) { HRESULT hr = S_OK; if (!ppvObj) { hr = E_POINTER; goto Exit; } if (IID_IUnknown == riid) { *ppvObj = static_cast(this); } else if (IID_IQueueAdminAction == riid) { *ppvObj = static_cast(this); } else if (IID_IQueueAdminQueue == riid) { *ppvObj = static_cast(this); } else { *ppvObj = NULL; hr = E_NOINTERFACE; goto Exit; } static_cast(*ppvObj)->AddRef(); Exit: return hr; } //---[ CDestMsgQueue::HrApplyQueueAdminFunction ]------------------------------ // // // Description: // Will call the IQueueAdminMessageFilter::Process message for every // message in this queue. If the message passes the filter, then // HrApplyActionToMessage on this object will be called. // Parameters: // IN pIQueueAdminMessageFilter // Returns: // S_OK on success // History: // 2/21/99 - MikeSwa Created // 4/1/99 - MikeSwa Merged implementations of ApplyQueueAdminFunction // //----------------------------------------------------------------------------- STDMETHODIMP CDestMsgQueue::HrApplyQueueAdminFunction( IQueueAdminMessageFilter *pIQueueAdminMessageFilter) { HRESULT hr = S_OK; DWORD i = 0; DWORD dwFlags = 0; PVOID pvOldContext = NULL; CQueueAdminContext qapictx(NULL, m_paqinst); _ASSERT(pIQueueAdminMessageFilter); hr = pIQueueAdminMessageFilter->HrSetQueueAdminAction( (IQueueAdminAction *) this); //This is an internal interface that should not fail _ASSERT(SUCCEEDED(hr) && "HrSetQueueAdminAction"); if (FAILED(hr)) goto Exit; pIQueueAdminMessageFilter->HrGetCurrentUserContext(&pvOldContext); pIQueueAdminMessageFilter->HrSetCurrentUserContext(&qapictx); //Apply action to every queue in DMQ dwInterlockedSetBits(&m_dwFlags, DMQ_QUEUE_ADMIN_OP_PENDING); //Map function on retry queue first, because that will make display //order more consistant, since messages that where at the front of //the queue, will be in the retry queue for retry errors. qapictx.SetQueueState(LI_RETRY); hr = m_fqRetryQueue.HrMapFn(QueueAdminApplyActionToMessages, pIQueueAdminMessageFilter, NULL); qapictx.SetQueueState(LI_READY); if (FAILED(hr)) goto Exit; for (i = 0; i < NUM_PRIORITIES; i++) { if (NULL != m_rgpfqQueues[i]) { hr = m_rgpfqQueues[i]->HrMapFn(QueueAdminApplyActionToMessages, pIQueueAdminMessageFilter, NULL); if (FAILED(hr)) { goto Exit; } } } Exit: dwFlags = dwInterlockedUnsetBits(&m_dwFlags, DMQ_QUEUE_ADMIN_OP_PENDING); // // NOTE - By doing this here, we will only check DMQ's for which // explicit queue admin actions have happened. Other DMQ's will wait // for retry or another QAPI action. // if (qapictx.cGetNumThawedMsgs()) { //We need to walk the retry queue for thawed msgs. We have to do //it here, because otherwise we might deadlock if this thread called //MergeRetryQueue() from within the HrMapFn MergeRetryQueue(); } //Restore inital context pIQueueAdminMessageFilter->HrSetCurrentUserContext(pvOldContext); return hr; } //---[ CDestMsgQueue::HrApplyActionToMessage ]--------------------------------- // // // Description: // Applies an action to this message for this queue. This will be called // by the IQueueAdminMessageFilter during a queue enumeration function. // Parameters: // IN *pIUnknownMsg ptr to message abstraction // IN ma Message action to perform // IN pvContext Context set on IQueueAdminFilter // OUT pfShouldDelete TRUE if the message should be deleted // Returns: // S_OK on success // History: // 2/21/99 - MikeSwa Created // 4/2/99 - MikeSwa Added context // //----------------------------------------------------------------------------- STDMETHODIMP CDestMsgQueue::HrApplyActionToMessage( IUnknown *pIUnknownMsg, MESSAGE_ACTION ma, PVOID pvContext, BOOL *pfShouldDelete) { HRESULT hr = S_OK; CMsgRef *pmsgref = (CMsgRef *)pIUnknownMsg; CQueueAdminContext *pqapictx = (CQueueAdminContext *)pvContext; BOOL fUpdateStats = TRUE; _ASSERT(pmsgref); _ASSERT(pfShouldDelete); _ASSERT(pqapictx); _ASSERT(pqapictx && pqapictx->fIsValid()); if (pqapictx && !pqapictx->fIsValid()) pqapictx = NULL; if (pqapictx && (LI_RETRY == pqapictx->lfGetQueueState())) { //We should not update stats if we are working on the retry queue //instead we should call UpdateRetryStats if this is a retry queue fUpdateStats = FALSE; } *pfShouldDelete = FALSE; switch (ma) { case MA_DELETE: hr = pmsgref->HrQueueAdminNDRMessage((CDestMsgQueue *)this); *pfShouldDelete = TRUE; break; case MA_DELETE_SILENT: hr = pmsgref->HrRemoveMessageFromQueue((CDestMsgQueue *)this); *pfShouldDelete = TRUE; break; case MA_FREEZE_GLOBAL: pmsgref->GlobalFreezeMessage(); break; case MA_THAW_GLOBAL: pmsgref->GlobalThawMessage(); // // Mark this queue as one to check for thawed messages. // if (pqapictx) pqapictx->IncThawedMsgs(); break; case MA_COUNT: default: //do nothing for counting and default break; } if (*pfShouldDelete && SUCCEEDED(hr) ) { if (fUpdateStats) { UpdateMsgStats(pmsgref, FALSE); MarkQueueEmptyIfNecessary(); } else { UpdateRetryStats(FALSE); } } return hr; } //---[ CDestMsgQueue::fMatchesID ]--------------------------------------------- // // // Description: // Used to determine if this link matches a given scheduleID/link pair // Parameters: // IN QueueLinkID ID to match against // Returns: // TRUE if it matches // FALSE if it does not // History: // 2/23/99 - MikeSwa Created // //----------------------------------------------------------------------------- BOOL STDMETHODCALLTYPE CDestMsgQueue::fMatchesID(QUEUELINK_ID *pQueueLinkID) { //This is not used at the DMQ level _ASSERT(0 && "Not implemented"); return E_NOTIMPL; } //---[ CDestMsgQueue::SendLinkStateNotification ]------------------------------ // // // Description: // Sends link state notification saying that the link was created. // Parameters: // - // Returns: // - // History: // 8/18/99 - AWetmore Created // //----------------------------------------------------------------------------- void CDestMsgQueue::SendLinkStateNotification(void) { CLinkMsgQueue *plmq = plmqGetLink(); if (plmq) { plmq->SendLinkStateNotificationIfNew(); plmq->Release(); } } //---[ CDestMsgQueue::fIsRemote ]---------------------------------------------- // // // Description: // Determines if queue is routed remotely. Caller should have routing // share lock. // Parameters: // - // Returns: // TRUE if link is routed remotely // FALSE otherwise // History: // 11/29/1999 - MikeSwa Created // //----------------------------------------------------------------------------- BOOL CDestMsgQueue::fIsRemote() { CLinkMsgQueue *plmq = plmqGetLink(); BOOL fIsRemote = FALSE; if (plmq) { fIsRemote = plmq->fIsRemote(); plmq->Release(); } return fIsRemote; } //---[ CDestMsgRetryQueue::CheckForStaleMsgsNextDSNGenerationPass ]------------ // // // Description: // Marks the queue as so that we will do the (expensive) check for // stale messages during the next DSN generation pass. // Parameters: // - // Returns: // - // History: // 4/18/2000 - MikeSwa Created // //----------------------------------------------------------------------------- VOID CDestMsgRetryQueue::CheckForStaleMsgsNextDSNGenerationPass() { _ASSERT(m_pdmq); dwInterlockedSetBits(&(m_pdmq->m_dwFlags), CDestMsgQueue::DMQ_CHECK_FOR_STALE_MSGS); }