我最近一直在从事与一些MSMQ消息队列接口的项目。 System.Messaging
API十分悲惨,尤其是如果您希望像对待可观察的消息序列一样使用队列,因此我整理了一个包装器API以使其更可口,但是我仍然要解决的问题之一具有代表接收消息时发生的错误。
有些表示为MessageQueueException
的错误实际上只是预期的结果,例如在调用Peek
或Receive
超时之前没有消息到达队列。 。由于这些阻塞了MSMQ中永远无法返回的调用,因此基本上需要使用超时来调用它们,因此我想处理某些消息队列异常,然后继续等待下一条消息(如果未取消),但是我不这样做希望我的AsyncSeq仅在出现更严重的错误(例如DTC故障)时永远等待。
有问题的函数是下面的Msmq.subscribe
函数(仅包括相关代码):
#r "System.Messaging"
#r "System.Transactions"
#r "FSharp.Control.AsyncSeq.dll"
open FSharp.Control
open System
open System.IO
open System.Messaging
open System.Text
open System.Threading
open System.Transactions
type MsmqFormatName =
| MsmqDirect of string
type MsmqQueue =
private {
MsmqFormatName: MsmqFormatName
MessageQueue: MessageQueue
} member queue.FormatName =
match queue.MsmqFormatName with
| MsmqDirect queuePath -> sprintf "DIRECT=OS:%s" queuePath
member queue.Queue = queue.MessageQueue
type MsmqTransaction =
| Local of MessageQueueTransaction
| Distributed
| Ambient
| AutoCommit
type MsmqMode =
| NonTransactional
| Transactional of MsmqTransaction
type MsmqError =
| MsmqReceiveTimedOut
| MsmqException of MessageQueueException
| NonMsmqException of exn
type ICommittable =
abstract member Commit: unit -> Result<unit, MsmqError>
abstract member Abort: unit -> Result<unit, MsmqError>
type MsmqMessage =
| Committed of Message
| Uncommitted of Message * ICommittable
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
[<RequireQualifiedAccess>]
module Msmq =
let private tryAsync f =
async {
try
let! message = f
return Ok message
with
| :? MessageQueueException as ex ->
return
if ex.MessageQueueErrorCode <> MessageQueueErrorCode.IOTimeout
then Error MsmqReceiveTimedOut
else Error (MsmqException ex)
| ex ->
return Error (NonMsmqException ex)
}
let receive timeout mode (msmq: MsmqQueue) =
let queue = msmq.Queue
match mode with
| NonTransactional ->
Async.FromBeginEnd ((fun _ -> queue.BeginReceive(timeout)), queue.EndReceive)
| Transactional tx ->
match tx with
| Local transaction ->
async { return queue.Receive(timeout, transaction) }
| Distributed ->
async {
use transaction = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)
let message = queue.Receive(timeout, MessageQueueTransactionType.Automatic)
transaction.Complete()
return message
}
| Ambient ->
async { return queue.Receive(timeout, MessageQueueTransactionType.Automatic) }
| AutoCommit ->
async { return queue.Receive(timeout, MessageQueueTransactionType.Single) }
|> tryAsync
let private tryMsmqOp f =
try f() |> Ok
with | :? MessageQueueException as ex -> Error (if ex.MessageQueueErrorCode = MessageQueueErrorCode.IOTimeout then MsmqReceiveTimedOut else MsmqException ex)
| ex -> Error (NonMsmqException ex)
let subscribe (cancellation: CancellationToken) mode msmq =
let timeout = TimeSpan.FromSeconds 10.0
let rec getMessages () =
asyncSeq {
let! result =
match mode with
| NonTransactional -> async.Bind(receive timeout mode msmq, Result.bind (Committed >> Ok) >> async.Return)
| Transactional tx ->
match tx with
| Local transaction ->
let committable =
{ new ICommittable with
member __.Commit () = tryMsmqOp transaction.Commit
member __.Abort () = tryMsmqOp transaction.Abort
}
async.Bind(receive timeout mode msmq, Result.bind (fun message -> Ok (Uncommitted (message, committable))) >> async.Return)
| Distributed ->
async {
let transaction = new TransactionScope(TransactionScopeOption.RequiresNew, TransactionScopeAsyncFlowOption.Enabled)
let! result = receive timeout (Transactional Ambient) msmq
match result with
| Ok message ->
let committable =
{new ICommittable with
member __.Commit () = tryMsmqOp (transaction.Complete >> transaction.Dispose)
member __.Abort () = tryMsmqOp (transaction.Dispose)
}
return Ok <| Uncommitted (message, committable)
| Error e ->
return Error e
}
| Ambient ->
async {
use transaction =
let tx = new MessageQueueTransaction()
tx.Begin()
tx
let! result = receive timeout (Transactional Ambient) msmq
match result with
| Ok message ->
let committable =
{new ICommittable with
member __.Commit () = tryMsmqOp transaction.Commit
member __.Abort () = tryMsmqOp transaction.Dispose
}
return Ok <| Uncommitted (message, committable)
| Error e ->
return Error e
}
| AutoCommit -> async.Bind(receive timeout mode msmq, Result.bind (Committed >> Ok) >> async.Return)
match result with
| Ok message -> yield message
| Error _ -> () // Note: Swallows Errors
if not cancellation.IsCancellationRequested
then yield! getMessages ()
}
getMessages ()
我的MSMQ客户端的完整代码可用here。
对于单个receive
操作,我返回一个Result<Message, MsmqError>
,其中MsmqError
类型表示类似超时的事物与其他异常不同。在这种情况下,事务管理由调用方完成。但是,对于返回subscribe
的{{1}}操作,我使用了一种称为AsyncSeq
的类型,它表示消息及其关联事务的组合,因为通常具有每个消息的事务处理,在许多情况下,直到消费者完全处理了消息后才应提交事务处理。我也可以将MsmqMessage
包装在MsmqMessage
中,但是将实际消息埋在Result
中的MsmqMessage
中的元组中似乎很不方便。另外,我可以检查Result
生成器中的错误并引发异常,但是我真的不喜欢在我的单操作函数返回asyncSeq
类型时抛出基于序列的函数的想法。
客户端的想法是像这样使用它:
Result
在延迟计算的序列中生成下一个元素期间,尤其是在使用""".\private$\test.msmq"""
|> FormatName.direct
|> Msmq.connect AllProperties
|> Msmq.subscribe cancellation.Token (Transactional Distributed)
|> AsyncSeq.iterAsyncParallel (fun message ->
async {
match message with
| Committed msg ->
printfn "%A: Received Committed Message %A" DateTime.Now msg.Id
| Uncommitted (msg, tx) ->
printfn "%A: Received Uncommitted Message %A" DateTime.Now msg.Id
// This is where you'd actually process the message
match tx.Commit() with
| Ok _ -> printfn "%A: Committed Message %A" DateTime.Now msg.Id
| Error e -> printfn "%A: Error Committing Message %A -- %A" DateTime.Now msg.Id e
})
库时,在这种情况下如何处理错误?