다음을 통해 공유


Poison Message Handling in MSMQ 3.0

이 샘플에서는 서비스에서 포이즌 메시지 처리를 수행하는 방법을 보여 줍니다. 이 샘플은 Transacted MSMQ Binding 샘플을 기반으로 합니다. 이 샘플에서는 netMsmqBinding을 사용합니다. 이 서비스는 자체적으로 호스팅되는 콘솔 응용 프로그램으로서 이를 사용하여 서비스에서 대기된 메시지를 받는 것을 볼 수 있습니다.

대기 중인 통신에서 클라이언트는 큐를 사용하여 서비스와 통신합니다. 좀더 정확하게 말하면 클라이언트가 큐에 메시지를 보내고, 서비스는 큐에서 보낸 메시지를 받습니다. 따라서 서비스와 클라이언트가 동시에 실행되고 있지 않더라도 큐를 사용하여 통신할 수 있습니다.

포이즌 메시지는 메시지를 읽는 서비스가 메시지를 처리할 수 없어 메시지를 읽는 트랜잭션을 종료할 때 큐에서 반복적으로 읽는 메시지입니다. 그러한 경우 메시지는 다시 시도됩니다. 메시지에 문제가 있는 경우 이론적으로 이 현상이 끝없이 계속될 수 있습니다. 트랜잭션을 사용하여 큐로부터 읽고 서비스 작업을 호출하는 경우에만 이러한 상황이 발생할 수 있습니다.

MSMQ 버전에 따라 NetMsmqBinding은 포이즌 메시지의 제한적 검색부터 완전한 검색까지 지원합니다. 포이즌 메시지로 검색된 메시지는 몇 가지 방법으로 처리할 수 있습니다.

이 샘플에서는 Windows Server 2003 및 Windows XP 플랫폼에서 제공하는 제한적 포이즌 기능과 Windows Vista에서 제공하는 완전한 포이즌 기능을 보여 줍니다. 두 샘플 모두 어떤 큐에서 포이즌 메시지 서비스가 수행될 수 있는 다른 큐로 포이즌 메시지를 이동하는 데 목적이 있습니다.

MSMQ v3.0(Windows Server 2003 및 Windows XP) 포이즌 처리 샘플

MSMQ v3.0에서는 포이즌 하위 큐를 지원하지 않습니다. 따라서 이 샘플에서는 포이즌 메시지가 위치할 큐를 만듭니다. 이는 데이터 손실을 방지하기 위해 MSMQ v3.0에서 포이즌 메시지를 처리할 때 선호하는 방법입니다.

Windows Server 2003 및 Windows XP에서의 포이즌 메시지 검색은 단일 속성 ReceiveRetryCount를 구성하는 것으로 제한됩니다. ReceiveRetryCount는 큐로부터 메시지를 반복하여 읽을 수 있는 횟수입니다. WCF(Windows Communication Foundation)에서는 이 카운트를 메모리에 유지 관리합니다. 이 카운트에 도달하면, 즉 포이즌 메시지가 되면 포이즌 메시지 처리 방법으로 진행합니다. 바인딩의 열거형 ReceiveErrorHandling 속성은 받아들일 수 있는 값을 제공합니다. 열거형 값은 다음과 같습니다.

  • Fault(기본값): 수신기와 서비스 호스트에 오류를 발생시킵니다.
  • Drop: 메시지를 제거합니다.
  • Move: 메시지를 포이즌 메시지 하위 큐로 이동합니다. 이 값은 Windows Vista에서만 사용할 수 있습니다.
  • Reject: 메시지를 발신자의 배달 못 한 편지 큐로 돌려보내는 방법으로 메시지를 거부합니다. 이 값은 Windows Vista에서만 사용할 수 있습니다.

MSMQ v3.0을 사용할 경우 그 중에서 Fault 및 Drop만 유효한 값입니다. 이 샘플에서는 MSMQ v3.0을 사용하여 포이즌 메시지를 처리하는 방법을 보여 줍니다. 또한 이 샘플은 MSMQ v4.0에서도 실행 가능하며 MSMQ v3.0에서처럼 작동합니다.

서비스 계약은 IOrderProcessor이며, 이는 큐에 사용하기에 적합한 단방향 서비스를 정의합니다.

[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
public interface IOrderProcessor
{
    [OperationContract(IsOneWay = true)]
    void SubmitPurchaseOrder(PurchaseOrder po);
}

서비스 작업은 해당 주문을 처리 중이라는 메시지를 표시합니다. 포이즌 메시지 기능을 보여 주기 위해 SubmitPurchaseOrder 서비스 작업은 일부 임의의 서비스 호출에 대해 트랜잭션을 롤백하고자 예외를 throw합니다. 그러면 메시지는 큐로 돌아가서, 포이즌 메시지로 표시됩니다. 이 구성은 포이즌 메시지에 대해 오류를 발생시키도록 설정되었습니다. 포이즌 메시지가 발생하면 대기 중인 채널은 포이즌 메시지의 MessageLookupId를 포함하는 MsmqPoisonMessageException을 throw하고 ServiceHost에 오류를 발생시킵니다. 오류 처리기가 연결되어 이 예외를 모니터링하고 적절한 조치를 취합니다. 오류 처리기를 추가하기 위해 PoisonErrorBehaviorAttribute를 만들고 이 동작으로 OrderProcessorService에 주석을 추가합니다.

    // Service class that implements the service contract.
    // Added code to write output to the console window.
    [PoisonErrorBehavior]
    public class OrderProcessorService : IOrderProcessor
    {
        static Random r = new Random(137);
        public static string QueueName;
        public static string PoisonQueueName;

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public void SubmitPurchaseOrder(PurchaseOrder po)
        {

            int randomNumber = r.Next(10);

            if (randomNumber % 2 == 0)
            {
                Orders.Add(po);
                Console.WriteLine("Processing {0} ", po);
            }
            else
            {
                Console.WriteLine("Aborting transaction, cannot process purchase order: " + po.PONumber);
                Console.WriteLine();
                throw new Exception("Cannot process purchase order: " + po.PONumber);
            }
        }

        public static void StartThreadProc(object stateInfo)
        {
            StartService();
        }

        public static void StartService()
        {
            // Get MSMQ queue name from app settings in configuration
            QueueName = ConfigurationManager.AppSettings["queueName"];

            // Get MSMQ queue name for the final poison queue
            PoisonQueueName = ConfigurationManager.AppSettings["poisonQueueName"];

            // Create the transacted MSMQ queue if necessary.
            if (!System.Messaging.MessageQueue.Exists(QueueName))
                System.Messaging.MessageQueue.Create(QueueName, true);

            // Create the transacted poison message MSMQ queue if necessary.
            if (!System.Messaging.MessageQueue.Exists(PoisonQueueName))
                System.Messaging.MessageQueue.Create(PoisonQueueName, true);


            // Get the base address that is used to listen for WS-MetaDataExchange requests
            // This is useful to generate a proxy for the client
            string baseAddress = ConfigurationManager.AppSettings["baseAddress"];

            // Create a ServiceHost for the OrderProcessorService type.
            ServiceHost serviceHost = new ServiceHost(typeof(OrderProcessorService), new Uri(baseAddress));

            // Hook on to the service host faulted events
            serviceHost.Faulted += new EventHandler(OnServiceFaulted);

            // Open the ServiceHostBase to create listeners and start listening for messages.
            serviceHost.Open();
            
        }


        public static void OnServiceFaulted(object sender, EventArgs e) 
        {
            Console.WriteLine("Service Faulted");
        }
        

        // Host the service within this EXE console application.
        public static void Main()
        {

            OrderProcessorService.StartService();

            // The service can now be accessed.
            Console.WriteLine("The service is ready.");
            Console.WriteLine("Press <ENTER> to stop the service");
            Console.ReadLine();
            Console.WriteLine("Exiting service");

        }
    }

PoisonErrorBehaviorAttributePoisonErrorHandler를 설치합니다. PoisonErrorHandlerIErrorHandler의 구현입니다. 시스템에서 예외가 throw될 때마다 IErrorHandler가 호출됩니다. 이 구현에서는 포이즌 메시지가 발견되었음을 나타내는 MsmqPoisonMessageException을 찾습니다. 예외로부터 MSMQ 메시지 조회 ID를 검색하고 System.Messaging API를 사용하여 큐로부터 포이즌 메시지를 수신한 다음 나중에 처리할 수 있도록 특정 포이즌 메시지 큐로 보냅니다. 메시지가 여전히 ServiceModel 트랜잭션에서 잠겨 있을 수 있으므로 읽기에 실패했음을 확인할 때까지 기다렸다가 다시 시도합니다. 메시지가 다른 큐로 이동했으면 해당 큐의 나머지 메시지는 소비될 수 있습니다. 이를 처리하기 위해 새로운 ServiceHost가 만들어지고 열립니다. 다음 코드 예제에서는 이 동작을 보여 줍니다.

    public class PoisonErrorHandler : IErrorHandler
    {
        static WaitCallback orderProcessingCallback = new WaitCallback(OrderProcessorService.StartThreadProc);

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            // no-op -we are not interested in this.
        }
        
        // Handle poison message exception by moving the offending message out of the way for regular processing to go on.
        public bool HandleError(Exception error)
        {
            MsmqPoisonMessageException poisonException = error as MsmqPoisonMessageException;
            if (null != poisonException)
            {
                long lookupId = poisonException.MessageLookupId;
                Console.WriteLine(" Poisoned message -message look up id = {0}", lookupId);

                // Get MSMQ queue name from app settings in configuration.

                System.Messaging.MessageQueue orderQueue = new System.Messaging.MessageQueue(OrderProcessorService.QueueName);
                System.Messaging.MessageQueue poisonMessageQueue = new System.Messaging.MessageQueue(OrderProcessorService.PoisonQueueName);
                System.Messaging.Message message = null;
                
                // Use a new transaction scope to remove the message from the main application queue and add it to the poison queue.
                // The poison message service processes messages from the poison queue.
                using(TransactionScope txScope= new TransactionScope(TransactionScopeOption.RequiresNew))
                {
                    int retryCount = 0;
                    while(retryCount < 3) 
                    {
                        retryCount++;
                    
                        try 
                        {
                            // Look up the poison message using the look up id.
                            message = orderQueue.ReceiveByLookupId(lookupId);
                            if(message != null) 
                            {
                                // Send the message to the poison message queue.
                                poisonMessageQueue.Send(message, System.Messaging.MessageQueueTransactionType.Automatic);
                                
                                // complete transaction scope
                                txScope.Complete();

                                Console.WriteLine("Moved poisoned message with look up id: {0} to poison queue: {1} ", lookupId, OrderProcessorService.PoisonQueueName);
                                break;
                            }
                        
                        }
                        catch(InvalidOperationException ) 
                        {
                            //Code for the case when the message may still not be available in the queue because of a race condition in transaction or 
                            //another node in the farm may actually have taken the message.
                            if (retryCount < 3) 
                            {
                                Console.WriteLine("Trying to move poison message but message is not available, sleeping for 10 seconds before retrying");
                                Thread.Sleep(TimeSpan.FromSeconds(10));
                            }
                            else 
                            {
                                Console.WriteLine("Giving up on trying to move the message");
                            }
                        }
                    
                    }
                }

                // Restart the service host.
                Console.WriteLine();
                Console.WriteLine("Restarting the service to process rest of the messages in the queue");
                Console.WriteLine("Press <ENTER> to stop the service");
                ThreadPool.QueueUserWorkItem(orderProcessingCallback);
                return true;
            }

            return false;
        }
    }

서비스 구성은 다음 구성 파일에서와 같이 receiveRetryCountreceiveErrorHandling이라는 포이즌 메시지 속성을 포함합니다. Windows Server 2003 및 Windows XP에서 실행될 때 maxRetryCyclesretryCycleDelay 속성이 무시됩니다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <!-- Use appSetting to configure the MSMQ queue name. -->
    <add key="queueName" value=".\private$\ServiceModelSamplesPoison" />
    <add key="poisonQueueName" value=".\private$\OrderProcessorPoisonQueue" />
    <add key="baseAddress" value="https://localhost:8000/orderProcessor/poisonSample"/>
  </appSettings>
  <system.serviceModel>
    <services>
      <service 
              name="Microsoft.ServiceModel.Samples.OrderProcessorService">
        <!-- Define NetMsmqEndpoint -->
        <endpoint address="net.msmq://localhost/private/ServiceModelSamplesPoison"
                  binding="netMsmqBinding"
                  bindingConfiguration="PoisonBinding" 
                  contract="Microsoft.ServiceModel.Samples.IOrderProcessor" />
      </service>
    </services>

    <bindings>
      <netMsmqBinding>
        <binding name="PoisonBinding" 
                 receiveRetryCount="0"
                 maxRetryCycles="1"
                 retryCycleDelay="00:00:05"                      
                 receiveErrorHandling="Fault">
        </binding>
      </netMsmqBinding>
    </bindings>
  </system.serviceModel>
</configuration>

포이즌 메시지 큐의 메시지 처리

포이즌 메시지 서비스는 최종 포이즌 메시지 큐로부터 메시지를 읽어 처리합니다.

포이즌 메시지 큐의 메시지는 메시지를 처리 중인 서비스로 주소 지정된 메시지입니다. 이 서비스가 포이즌 메시지 서비스 끝점과 다를 수도 있습니다. 따라서 포이즌 메시지 서비스가 큐로부터 메시지를 읽을 때 WCF 채널 계층은 끝점에서 불일치를 발견하고 메시지를 디스패치하지 않습니다. 이 경우 메시지는 주문 처리 서비스로 주소 지정되지만 포이즌 메시지 서비스에서 이를 수신합니다. 메시지의 주소가 다른 끝점으로 지정되는 경우에도 메시지를 계속 받으려면 ServiceBehavior를 추가하여 메시지 주소가 지정된 서비스 끝점과 일치하는 주소를 필터링해야 합니다. 포이즌 메시지 큐로부터 읽는 메시지를 성공적으로 처리하려면 이 작업이 필요합니다.

포이즌 메시지 서비스 구현 자체는 서비스 구현과 매우 비슷합니다. 계약을 구현하고 주문을 처리합니다. 코드 예제는 다음과 같습니다.

    // Service class that implements the service contract.
    // Added code to write output to the console window.
    [ServiceBehavior(AddressFilterMode=AddressFilterMode.Any)]
    public class OrderProcessorService : IOrderProcessor
    {
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public void SubmitPurchaseOrder(PurchaseOrder po)
        {
            Orders.Add(po);
            Console.WriteLine("Processing {0} ", po);
        }

        public static void OnServiceFaulted(object sender, EventArgs e) 
        {
            Console.WriteLine("Service Faulted...exiting app");
            Environment.Exit(1);
        }


        // Host the service within this EXE console application.
        public static void Main()
        {
   
            // Create a ServiceHost for the OrderProcessorService type.
            ServiceHost serviceHost = new ServiceHost(typeof(OrderProcessorService));

            // Hook on to the service host faulted events.
            serviceHost.Faulted += new EventHandler(OnServiceFaulted);
            
            serviceHost.Open();

            // The service can now be accessed.
            Console.WriteLine("The poison message service is ready.");
            Console.WriteLine("Press <ENTER> to terminate service.");
            Console.WriteLine();
            Console.ReadLine();

            // Close the ServiceHostBase to shutdown the service.
            if(serviceHost.State != CommunicationState.Faulted)
            {
                serviceHost.Close();
            }
        }

주문 큐로부터 메시지를 읽는 주문 처리 서비스와 달리 포이즌 메시지 서비스는 포이즌 하위 큐로부터 메시지를 읽습니다. 포이즌 큐는 기본 큐의 하위 큐로서 "poison"이라는 이름이 지정되고 MSMQ에서 자동으로 생성합니다. 이 큐에 액세스하려면 다음 샘플 구성처럼 기본 큐 이름, ";" 및 하위 큐 이름(여기서는 -"poison")을 차례로 입력합니다.

참고

MSMQ v3.0 샘플에서는 포이즌 큐 이름이 하위 큐가 아니며 메시지를 옮겨 놓은 큐입니다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Microsoft.ServiceModel.Samples.OrderProcessorService">
        <!-- Define NetMsmqEndpoint -->
        <endpoint address="net.msmq://localhost/private/ServiceModelSamplesPoison;poison"
                  binding="netMsmqBinding"
                  contract="Microsoft.ServiceModel.Samples.IOrderProcessor" >
        </endpoint>
      </service>
    </services>

  </system.serviceModel>
</configuration> 

샘플을 실행하면 클라이언트, 서비스 및 포이즌 메시지 서비스 동작이 콘솔 창에 표시됩니다. 서비스에서 클라이언트가 보내는 메시지를 받는 것을 볼 수 있습니다. 서비스를 종료하려면 각 콘솔 창에서 Enter 키를 누릅니다.

서비스가 실행되기 시작하면서 주문을 처리하고 임의로 처리 종료에 착수합니다. 주문을 처리했다는 메시지가 나타나면 클라이언트를 다시 실행하여 서비스가 실제로 메시지를 종료했음을 확인할 때까지 다른 메시지를 보낼 수 있습니다. 구성된 포이즌 설정에 따라 메시지가 최종 포이즌 큐로 이동하기 전에 한 번 메시지 처리가 시도됩니다.

The service is ready.
Press <ENTER> to terminate service.

Processing Purchase Order: 0f063b71-93e0-42a1-aa3b-bca6c7a89546
        Customer: somecustomer.com
        OrderDetails
                Order LineItem: 54 of Blue Widget @unit price: $29.99
                Order LineItem: 890 of Red Widget @unit price: $45.89
        Total cost of this order: $42461.56
        Order status: Pending

Processing Purchase Order: 5ef9a4fa-5a30-4175-b455-2fb1396095fa
        Customer: somecustomer.com
        OrderDetails
                Order LineItem: 54 of Blue Widget @unit price: $29.99
                Order LineItem: 890 of Red Widget @unit price: $45.89
        Total cost of this order: $42461.56
        Order status: Pending

Aborting transaction, cannot process purchase order: 23e0b991-fbf9-4438-a0e2-20adf93a4f89

포이즌 메시지 서비스를 시작하여 포이즌 큐로부터 포이즌 메시지를 읽습니다. 이 예제에서는 포이즌 메시지 서비스가 메시지를 읽고 처리합니다. 종료되고 포이즌 메시지가 된 구매 주문을 포이즌 메시지 서비스에서 읽는 것을 확인할 수 있습니다.

The service is ready.
Press <ENTER> to terminate service.

Processing Purchase Order: 23e0b991-fbf9-4438-a0e2-20adf93a4f89
        Customer: somecustomer.com
        OrderDetails
                Order LineItem: 54 of Blue Widget @unit price: $29.99
                Order LineItem: 890 of Red Widget @unit price: $45.89
        Total cost of this order: $42461.56
        Order status: Pending

샘플을 설치, 빌드 및 실행하려면

  1. Windows Communication Foundation 샘플의 일회 설치 절차를 수행했는지 확인합니다.

  2. C# 또는 Visual Basic .NET 버전의 솔루션을 빌드하려면 Windows Communication Foundation 샘플 빌드의 지침을 따릅니다.

  3. 단일 컴퓨터 또는 다중 컴퓨터 구성에서 샘플을 실행하려면 localhost 대신 실제 호스트 이름을 반영하도록 큐 이름을 변경하고 Windows Communication Foundation 샘플 실행의 지침을 따릅니다.

기본적으로 netMsmqBinding 바인딩 전송을 사용하여 보안이 설정됩니다. MsmqAuthenticationModeMsmqProtectionLevel 속성은 모두 전송 보안의 형식을 결정합니다. 기본적으로 인증 모드는 Windows로 설정되고 보호 수준은 Sign으로 설정됩니다. MSMQ는 인증 및 서명 기능을 제공하려면 도메인의 일부여야 합니다. 도메인의 일부가 아닌 컴퓨터에서 이 샘플을 실행할 경우 "사용자의 내부 메시지 큐 인증서가 없습니다."라는 오류가 발생합니다.

작업 그룹에 가입된 컴퓨터에서 샘플을 실행하려면

  1. 컴퓨터가 도메인의 일부가 아닌 경우 다음 샘플 구성과 같이 인증 모드와 보호 수준을 None으로 설정하여 전송 보안을 해제합니다.

    <bindings>
        <netMsmqBinding>
            <binding name="TransactedBinding">
                <security mode="None"/>
            </binding>
        </netMsmqBinding>
    </bindings>
    

    끝점의 bindingConfiguration 특성을 설정하여 끝점이 바인딩과 연결되게 합니다.

  2. 샘플을 실행하기 전에 PoisonMessageServer, 서버 및 클라이언트에서 구성을 변경해야 합니다.

    참고

    security modeNone으로 설정하는 것은 MsmqAuthenticationMode, MsmqProtectionLevelMessage 보안을 None으로 설정하는 것과 같습니다.

Send comments about this topic to Microsoft.
© 2007 Microsoft Corporation. All rights reserved.