我们在Azure Service Bus队列上处理长时间运行的消息时会看到一些奇怪的行为。当我们在一个相当大的数据集上运行时,我们正在更新锁定,这个特定的消息通常需要大约1个小时才能完成。该过程运行良好并一直运行完成, 除非 它在午夜UTC运行。
在UTC午夜时分,我们的进程抛出异常:
Microsoft.ServiceBus.Messaging.MessageLockLostException:提供的锁无效。锁定已过期,或者消息已从队列中删除。
我们能够一夜又一夜地重现这种情况,如果我们不在午夜进行这个过程,就不会发生这种情况。
可能是消息锁定" ExpiresAtUtc"时间戳计算没有非常优雅地处理从一天到另一天的交叉?
----更新----
可能有用的更多信息:
长时间运行的进程通过调用RenewLock
来阻止消息再次在队列中可见。当此处理在午夜继续时,我们注意到该消息在队列中变得可见并且重新开始处理。消息不会被删除,也不会移动到死信队列。它的锁定只是过期,消息再次在队列中可见,因此它被处理器拾取并重新启动进程。只要该过程不跨越午夜UTC的边界,它就会成功完成。
以下是我们用于连接/排队/从队列中出列的代码片段:
连接到队列:
private QueueClient GetQueue<TMessage>() => QueueClient.CreateFromConnectionString(this.configSection.Value.ConnectionString, typeof(TMessage).Name, ReceiveMode.PeekLock);
排队留言:
using (var brokeredMessage = new BrokeredMessage(message) {ContentType = "application/json"})
{
await GetQueue<TMessage>().SendAsync(brokeredMessage).ConfigureAwait(false);
}
出队消息:
GetQueue<TMessage>().OnMessageAsync(
async msg =>
{
TMessage body = null;
try
{
body = msg.GetBody<TMessage>();
await handler.HandleMessageAsync(body, msg.RenewLockAsync).ConfigureAwait(false);
await msg.CompleteAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
await msg.AbandonAsync().ConfigureAwait(false);
}
},
new OnMessageOptions
{
AutoComplete = false
}
);
下面是Azure服务总线指标的屏幕截图 - 成功请求,在午夜UTC(我在UTC + 1时区,以及我在上午1:00)我们在午夜时分如何显示发生影响队列的某些:
下面是我们内部日志记录的屏幕截图,处理只是停止,然后在超过一分钟后再次启动 - 当锁定到期并且消息再次在队列中可见时由处理器接收:
答案 0 :(得分:1)
我来自Azure Service Bus团队。 LockDuration只是持续时间,它永远不会在内部表示为时间戳。从接收的瞬间开始,技术上锁定了LockDuration的消息,直到将来某个时刻才锁定。例如,无论何时,消息都会在收到消息之后锁定30秒。所以午夜UTC从来不是特例或任何此类事情。我们的自动化测试每晚都在进行,之前我们没有这种情况。 但是既然你每晚都要复制它,那么一定会有一些有趣的事情发生。我还不知道它是什么。我想知道你正在复制它的地区。我们的团队成员之一将很快与您联系。
答案 1 :(得分:0)
我更习惯使用ASB中的主题/订阅(大规模),但据我所知,队列和主题共享相同的内部基础架构。
基本上,在我们的软件中,我们使用BrokeredMessage.RenewLock()作为长时间运行进程锁定消息的方法。
为了完成工作,并保持消息锁定直到进程结束,我们启动一个单独的Task,每隔30秒左右(取决于你的队列“Lock Duration”配置)我们执行brokeredMessage.RenewLock(),也就是说,我们一直在ping这个锁,所以最终不会丢失。
您报告的错误不是本地的,当您尝试“完成”消息,使其失效或尝试其他需要您对消息进行锁定的其他操作时,ASB会抛出此错误,您很可能丢失了超时锁定。
我们用来保持锁定活动的代码看起来非常相似:
<?php
include 'init.php';
session_start();
try{
if( empty( $_SESSION['cart_array'] ) ){
throw new Exception('<h2 align="center">Your shopping cart is empty</h2>');
} else {
if( empty( $_SESSION['user_name'] ) ){
$book = rand( 1000000, 2000000 );
/* Prepare SQL once outside the loop */
$sql = 'insert into `books` ( `book`,`item_name`, `quantity`, `msg` ) values ( :book, :item, :qty, :msg )';
$stmt=$conn->prepare( $sql );
if( $stmt ){
/* bind placholders to variables */
$stmt->bindParam(':book', $book );
$stmt->bindParam(':item', $id );
$stmt->bindParam(':qty', $qty );
$stmt->bindParam(':msg', $msg );
/* assign variables and execute inside loop */
foreach( $_SESSION['cart_array'] as $item ) {
$id = $item['item_id'];
$qty = $item['quantity'];
$msg = '';
if( $id == 'sms' ) {
$msg = $item['msg'];
$qty = 1;
}
$stmt->execute();
}
$stmt->closeCursor();
echo "
<div class='info_post'>
YOUR SHOPPING BOOKED CODE IS ' . $book . ' KINDLY COPY TO ANY DEALER NEAR YOU TO COMFIRM
<br/ >
</div>
<form action='mail.php' method='POST'><b> Mail me:</b><br/ >
<input type='text' name='book' size='23'>
<input type='submit' name='submit' value='SEND EMAIL' />
</form>";
unset( $_SESSION['cart_array'] );
} else {
throw new Exception('Failed to prepare sql statement',1);
}
} else {
/* create and prepare sql */
$sql='select * from `users` where `username`=:book';
$stmt=$conn->prepare( $sql );
/* bind parameters */
if( $stmt ){
$stmt->bindParam(':book', $username );
$username = $_SESSION['user_name'];
$result = $stmt->execute();
if( $result ){
$row = $stmt->fetch( PDO::FETCH_BOTH );
$stmt->closeCursor();
if( !$row ) throw new Exception('bad foo',3);
/* assign vars */
$id = $row['id'];
$username = $row['username'];
$ip = $row['ip'];
$ban = $row['validated'];
$balance = $row['balance'];
if( $ban != "0" ) {
echo "<div class='info_post'><b>$buy $balance $ban</div>";
}
if( $buy <= $balance) {
$redut = $balance - $buy;
$sql='update `users` set `balance`=:redut where `id`=:id;';
$stmt=$conn->prepare( $sql );
if( $stmt ){
$stmt->bindParam(':redut', $redut );
$stmt->bindParam(':id', $id );
$result = $stmt->execute();
$stmt->closeCursor();
if( $result ){
$book = rand( 1000000, 2000000 );
$sql_insert_1='insert into `books` ( `book`, `item_name`, `quantity` ) values ( :book, :name, :qty )';
$stmt_insert_1=$conn->prepare( $sql );
$sql_insert_2='insert into `details` ( `poster`, `message`, `date` ) values ( :username, :msg, :time )';
$stmt_insert_2=$conn->prepare( $sql );
if( $stmt_insert_1 ){
$stmt_insert_1->bindParam(':book', $book );
$stmt_insert_1->bindParam(':name', $name );
$stmt_insert_1->bindParam(':qty', $qty );
} else {
throw new Exception('Failed to prepare sql statement',5);
}
if( $stmt_insert_2 ){
$stmt_insert_2->bindParam(':username', $username );
$stmt_insert_2->bindParam(':msg', $msg );
$stmt_insert_2->bindParam(':time', $time );
} else {
throw new Exception('Failed to prepare sql statement',6);
}
foreach( $_SESSION['cart_array'] as $item ) {
/* $book defined above - rand() */
$name = $item['item_id'];
$qty = $item['quantity'];
/* $username defined earlier */
$msg = "Transation of $totalquantity products cost of $cartTotal occur on your account with ticket id $book";
$time = date('Y-m-d H:i:s');
$result = $stmt_insert_1->execute();
if( !$result )throw new Exception('insert failed',7);
$result = $stmt_insert_2->execute();
if( !$result )throw new Exception('insert failed',8);
}
$stmt_insert_1->closeCursor();
$stmt_insert_2->closeCursor();
unset( $_SESSION['cart_array'] );
}
} else {
throw new Exception('Failed to prepare sql statement',4);
}
}
}
} else {
throw new Exception('Failed to prepare sql statement',2);
}
}
}
} catch( Exception $e ){
printf( 'Error: Code %d Message %s', $e->getCode(), $e->getMessage() );
}
?>