【问题标题】:App Engine Pull Queue tasks disappear before being properly handledApp Engine 拉取队列任务在正确处理之前消失
【发布时间】:2014-02-21 18:11:47
【问题描述】:

更新 4 - 为清楚起见重新表述问题

我正在使用拉取队列来为发送推送通知的后端工作人员任务提供反馈。我可以在日志中看到前端实例将任务排队。但是,该任务只是偶尔由后端处理。我看不到为什么任务在被处理并从队列中删除之前消失的迹象。

这可能与此有关:我在尝试从队列中租用任务时看到异常多的 TransientFailureExceptions - 尽管在尝试之间休眠。

在我的开发服务器上一切正常(早期版本已在生产中运行),但生产不再正常运行。一开始我以为是证书问题。但是,有时会在后端首次启动时发送通知。

当我在队列上调用leaseTasks 时,除了TransientFailureException 之外,没有任何迹象表明发生了错误。另外,我的日志似乎需要很长时间才能显示出来。

我可以根据需要提供更多信息和代码sn-ps。

感谢您的帮助。

更新 1:

应用程序使用 10 个拉取队列。它通常会使用 2,但队列标记仍被认为是实验性的。它们以标准方式声明:

<queue>
    <name>gcm-henchdist</name>
    <mode>pull</mode>
</queue>

租用任务函数是:

public boolean processBatchOfTasks()
{
    List< TaskHandle > tasks = attemptLeaseTasks();

    if( null == tasks || tasks.isEmpty() )
    {
        return false;
    }

    processLeasedTasks( tasks );
    return true;
}

private List< TaskHandle > attemptLeaseTasks()
{
    for( int attemptNnum = 1; !LifecycleManager.getInstance().isShuttingDown(); ++attemptNnum )
    {
        try
        {
            return m_taskQueue.leaseTasks( m_numLeaseTimeUnits, m_leaseTimeUnit, m_maxTasksPerLease );
        } catch( TransientFailureException exc )
        {
            LOG.warn( "TransientFailureException when leasing tasks from queue '{}'", m_taskQueue.getQueueName(), exc );
            ApiProxy.flushLogs();
        } catch( ApiDeadlineExceededException exc )
        {
            LOG.warn( "ApiDeadlineExceededException when when leasing tasks from queue '{}'",
                    m_taskQueue.getQueueName(), exc );
            ApiProxy.flushLogs();
        }

        if( !backOff( attemptNnum ) )
        {
            LOG.warn( "Failed to lease tasks." );
            break;
        }
    }

    return Collections.emptyList();
}

其中租赁变量分别为 30、TimeUnit.MINUTES、100

processBatchOfTasks 函数通过以下方式轮询:

private void startPollingForClient( EClientType clientType )
{
    InterimApnsCertificateConfig config = InterimApnsCertificateConfigMgr.getConfig( clientType );
    Queue notificationQueue = QueueFactory.getQueue( config.getQueueId().getName() );

    ApplePushNotificationWorker worker = new ApplePushNotificationWorker(
            notificationQueue,
            m_messageConverter.getObjectMapper(),
            config.getCertificateBytes(),
            config.getPassword(),
            config.isProduction() );

    LOG.info( "Started worker for {} polling queue {}", clientType, notificationQueue.getQueueName() );

    while ( !LifecycleManager.getInstance().isShuttingDown() )
    {
        boolean tasksProcessed = worker.processBatchOfTasks();
        ApiProxy.flushLogs();

        if ( !tasksProcessed )
        {
            // Wait before trying to lease tasks again.
            try
            {
                //LOG.info( "Going to sleep" );
                Thread.sleep( MILLISECONDS_TO_WAIT_WHEN_NO_TASKS_LEASED );
                //LOG.info( "Waking up" );
            } catch ( InterruptedException exc )
            {
                LOG.info( "Polling loop interrupted. Terminating loop.", exc );
                return;
            }
        }
    }

    LOG.info( "Instance is shutting down" );
}

线程是通过以下方式创建的:

Thread thread = ThreadManager.createBackgroundThread( new Runnable()
{
    @Override
    public void run()
    {
        startPollingForClient( clientType );
    }
} );

thread.start();

GCM 通知的处理方式类似。

更新 2

以下是退避函数。我已经在日志中验证了(使用 GAE 和我自己的时间戳)睡眠正在正确增加

private boolean backOff( int attemptNo )
{
    // Exponential back off between 2 seconds and 64 seconds with jitter
    // 0..1000 ms.
    attemptNo = Math.min( 6, attemptNo );
    int backOffTimeInSeconds = 1 << attemptNo;
    long backOffTimeInMilliseconds = backOffTimeInSeconds * 1000 + (int)( Math.random() * 1000 );

    LOG.info( "Backing off for {} milliseconds from queue '{}'", backOffTimeInMilliseconds, m_taskQueue.getQueueName() );
    ApiProxy.flushLogs();

    try
    {
        Thread.sleep( backOffTimeInMilliseconds );

    } catch( InterruptedException e )
    {
        return false;
    }

    LOG.info( "Waking up from {} milliseconds sleep for queue '{}'", backOffTimeInMilliseconds, m_taskQueue.getQueueName() );
    ApiProxy.flushLogs();

    return true;
}

更新 3

任务被添加到前端实例的事务中的队列中:

if( null != queueType )
{
    String deviceName;
    int numDevices = deviceList.size();
    for ( int iDevice = 0; iDevice < numDevices; ++iDevice )
    {
        deviceName = deviceList.get( iDevice ).getName();
        LOG.info( "Queueing Your-Turn notification for user: {} device: {} queue: {}", user.getId(), deviceName, queueType.getName() );
        Queue queue = QueueFactory.getQueue( queueType.getName() );

        queue.addAsync( TaskOptions.Builder.withMethod( TaskOptions.Method.PULL )
                .param( "alertLocKey", "NOTIF_YOUR_TURN" ).param( "device", deviceName ) );
    }
}

我知道事务成功是因为数据库更新正确。

在日志中,我看到“正在排队轮到你的通知...”条目,但我看不到后端日志中出现任何内容。

在管理面板中,我看到任务队列 API 调用增加 1 以及任务队列存储的任务计数增加 1。但是,写入的队列在队列中的任务和最后一分钟租用的任务中都显示为零字段。

【问题讨论】:

  • 是的,请提供更多信息。
  • @MartinBerends 已更新!如果您有什么特别想看的,请告诉我。
  • @MartinBerends 我已将leaseTasks 函数的名称更改为attemptLeaseTasks,以便更容易按照您的建议评论代码。另外,我在 catch 语句和 backoff 调用之间放置了额外的间距,这样它就不会融入其中。

标签: java google-app-engine


【解决方案1】:

TransientFailureException JavaDoc 表示“如果再次尝试,请求的操作可能会成功”(因为失败是暂时的)。因此,当抛出此异常时,您的代码应循环返回并重复 leaseTasks 调用。此外,AppEngine 不必自己重做请求,因为它通过异常通知您应该这样做。

很遗憾,您将方法名称leaseTasks 重复为您自己的方法名称,因为现在还不清楚当我提到leaseTasks 时我指的是哪一个。尽管如此,将对 m_taskQueue.leaseTasks 的内部调用包装在一个 while 循环和一个额外的 try 块中以仅捕获 TransientFailureException。仅当未引发异常时才使用标志结束 while 循环。

解释够了吗,还是需要完整的源代码清单?

【讨论】:

  • 这就是退避函数的作用,我在日志中验证了它会休眠规定的时间,然后再试一次。即使在尝试之间等待几秒钟,失败也会频繁发生。这遵循 Google 在以下位置创建的示例:github.com/GoogleCloudPlatform/…
  • 我按下回车键腾出一个空间,但提交了评论:令人沮丧的是代码在生产中正常工作,但是当我扩大它并添加更多队列时,它停止运行。这尤其令人沮丧,因为它在开发服务器上正常运行。
  • 我还应该评论说我的日志从不显示被删除的任务。因此,一旦它的租约到期,它应该返回到再次尝试的队列中。但是,任务似乎正在消失而没有被删除。
  • 以上三个cmets等于拒绝了我的建议。你没有考虑过或没有表现出尝试的意愿。目前的退避逻辑是有缺陷的。执行后,外部 for 循环继续执行队列中的下一个任务,并且忘记先前的瞬态失败任务!如果您希望我继续尝试帮助您,请添加一个内部 while 循环和一个内部 catch。
  • CloudPushSample for iOS - Java Backend 中的逻辑存在相同的错误。
【解决方案2】:

看来罪魁祸首可能是我在排队任务时调用了 addAsync 而不是仅仅调用 add。

我更换了电话,现在似乎一切正常。我想知道为什么这会有所不同,并会在找到原因后更新答案。

【讨论】:

  • 使用 addAsync 应该没有什么区别,因为它只影响入队操作的流量控制。也许 m_taskQueue.leaseTasks() 抛出的瞬时失败异常现在不那么瞬时了。日志可能会说明问题。
  • @MartinBerends 我也是这么想的。但是,例外情况与以往一样普遍。考虑到错误的性质,我觉得一个无害的改变会解决问题。我希望我有一位 Google 工程师可以解释原因。
  • 在使用 addAsync 时异常是否仍然普遍存在?
  • @MartinBerends 是的,它们经常出现在 add 和 addAsync 中。我认为错误不在于任务的检索和处理,而在于入队。当我们考虑更新 3(上图)时,这是有道理的。尽管 API 调用号增加,“队列中的任务”字段和“最后一分钟租用”字段仍为零。这也可以解释缺少与处理相关的日志以及未找到任务(即使它们从未被删除)。因此,它们可能并没有消失,只是从未被正确地创造出来。
  • 哎哟。然后,您基于代码的链接示例可能会在大量使用下出现相同的问题。我最近获得了使用 iPhone 进行测试的机会,所以当我得到一个轮训时,我将尝试部署那个原始项目。 Round tuits 很少见,所以没有关于什么时候的承诺,但是这个问题已经变成了一个挑战。对你来说,我想它已经变成了更多的烦恼。如果原始代码丢失任务,则应将其报告为平台错误。
猜你喜欢
  • 2012-08-09
  • 1970-01-01
  • 1970-01-01
  • 2011-06-22
  • 1970-01-01
  • 2011-04-29
  • 2019-03-04
  • 1970-01-01
相关资源
最近更新 更多