异步发送消息,同时保留顺序



因此,由于在家工作,我们的QA工程师在与我们分开的地方工作。有时,作为开发人员,我们很难调试他们的发现,因为我们无法直接访问他们持有的设备(有些错误只能在他们的设备中复制),因此无法访问日志(我说的是Android应用程序,所以这些设备是移动电话)。

因此,我们想出了一个想法来设置远程调试,基本上是通过互联网将日志发送到我们的开发服务器,这样我们就可以对其进行检查

长话短说,我们提出了这个天真的实现,在它的帮助下,我们异步发送日志

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {

}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
}
});

然而,我们很快发现了一个问题,即以这种方式发送日志并不能保证日志的顺序,这可能会使调试变得混乱。

有没有什么干净的方法可以在异步发送日志的同时保持日志的顺序?(我们希望网络部分是异步的,因为我们不希望测试人员在发送日志时出现抖动)。

我正在避免任何复杂的服务器端逻辑与区块ID或其他什么。希望订单可以在客户端处理。在发送另一个日志之前等待发送日志不是问题,我们并不真正关心日志性能,因为它在生产构建中无论如何都会被关闭。只要日志记录不影响用户界面,就没问题。

还有一个要求是,当发生崩溃时,我们希望在应用程序终止之前刷新日志并发送stacktrace。

我知道我可以实现一种消息队列,并且为了稍后设置defaultUnaughtExceptionHandler,但我能想到的每一种方法都有点混乱。我想知道是否有一个简单的方法我可以做?或者我可以使用一个框架?

提前谢谢。

您需要转移异步的责任。现在您将"jobs"交给http异步处理器。

这不是你真正想要的,原因有很多。这不仅仅是因为这意味着你的日志消息最终会出错,还因为这可能会淹没你的http端点。

你想要这样的东西听起来更明智:

  • 只有一个线程。它的工作是获取下一个需要发送的日志并将其发送。它将不断地这样做,直到没有更多的日志可发送,这时线程将休眠,直到一个日志到达。这个线程是同步的,因为它最多只有一个活动的HTTP连接
  • 系统中的其他代码只是将日志作业扔到堆上,因为它们知道一个日志线程可以发送它们。在堆上抛出日志作业的任务必须很快(以避免抖动)

这并不难实现,但在我们到达那里之前,这个设计中有一个问题:想象一下你的日志服务关闭了,或者真的很慢,应用程序堆积"记录这个"作业的速度比线程将它们发送到HTTP端点的速度快。现在该怎么办?

对于这种公认的奇异场景,没有明显的正确答案,你必须提供一个答案。你有3个广泛的选择:

  1. 只需开始完全忽略日志消息,和/或将打开的日志堆发送:将当前1000个排队的日志全部删除,然后以"由于拥塞而忽略的1000个日志"作为第一条日志消息重新启动队列,现在您有999个房间。

  2. 开始紧张:放慢应用程序的速度——应用程序的logThis调用只需等待,直到堆上有空间。

  3. 崩溃应用程序:logThis调用应该会导致应用程序显示错误,可能是通过抛出异常并确保该异常适当地出现。

无论你最终选择什么,java.util.concurrent.BlockingQueue都可能是你想要的。

如果队列已满,我想阻止

这是一个简单的例子,BlockingQueue正是您所需要的,开箱即用:

BlockingQueue<LogJob> logQueue = new ArrayBlockingQueue<>(200);
public void log(LogJob job) {
logQueue.put(job);
}
public void run() {
// This is the thread's primary loop
while (!Thread.interrupted()) {
LogJob job = logQueue.take();
job.sendToServerUsingSynchronousHttpLibrary();
}
}

压缩日志

需要对log方法进行轻微更新:

public void log(LogJob job) {
while (true) {
if (logQueue.offer(job)) return;
var sink = new ArrayList<LogJob>();
logQueue.drainTo(sink);
if (sink.size() == 0) continue;
if (!logQueue.offer(LogJob.warning("Omitted " + sink.size() + " logs due to congestion"))) throw new IllegalStateException("Feels like you have an infinite loop piling on endless logs somewhere");
}
}

使应用程序崩溃

public void log(LogJob job) {
if (!logQueue.offer(job)) throw new TheExceptionYouWant();
}

如果队列已满,则静默不发送任何内容

public void log(LogJob job) {
logQueue.offer(job);
}

如果你想增加一点趣味性,让log方法检查线程是否真的在运行可能会很好,如果没有,抛出一些东西,或者启动它

第二个问题是最后会发生什么:你的应用程序的测试人员生成一个日志,然后应用程序硬崩溃。此日志不太可能到达您的HTTP端点。这是基本的日志二分法,没有简单的解决方案可以绕过它:

  • 您不希望日志语句具有显著的时间效应(您不希望仅日志语句的存在就改变应用程序的运行方式或使其导致显著的速度减慢)
  • 您不希望"错过"在重大崩溃(如硬崩溃或内存错误)之前立即出现的重要日志

这两个要求最终是相互排斥的,除非你开始采取一些激烈的措施,不能完全解决上述二分法:

  • 总是等待日志"成功"的确认,无论是到磁盘还是到终点,但这会导致您试图避免的抖动
  • 登录到速度相当快的第二个进程,然后让该进程发送日志,这样可以防止该进程(创建日志的进程)发生硬崩溃:第二进程将继续运行并发送日志。但这需要第二个完全独立的应用程序,还需要主进程和辅助进程之间的通信速度如此之快,以至于不会发现应用程序的显著放缓。这是可能的,但并不容易
  • 不要像这个答案所解释的那样等待并使用队列解决方案。。。但这确实意味着,如果安卓操作系统最终硬杀了你的应用程序,那么一大堆日志就会丢失。您可能希望继续在设备上进行本地日志记录,这样,如果开发团队确定没有一堆关键日志,您至少可以向测试人员询问他们的本地日志

最新更新