Azure Service Bus队列消息锁定于UTC午夜

时间:2017-12-30 00:33:43

标签: c# azureservicebus

我们在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)我们在午夜时分如何显示发生影响队列的某些

enter image description here

下面是我们内部日志记录的屏幕截图,处理只是停止,然后在超过一分钟后再次启动 - 当锁定到期并且消息再次在队列中可见时由处理器接收:

enter image description here

2 个答案:

答案 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() );
    }

?>