SignalR and/or 计时器问题,因为 Chrome 88

SignalR and/or timer issues since Chrome 88

我们有一个 ASP.Net WebForms 应用程序,它使用 SignalR (v2.4.1) 在服务器和客户端之间进行一些双向通信。它多年来一直运行良好:连接稳定,数百名用户使用它,等等。

但是,我们已经开始从我们的客户群中收到关于连接问题的零星报告,所有报告的内容都是一样的:如果浏览器 (Chrome) 会话空闲超过 5 分钟,则后台连接断开。页面中的所有计时器定期停止运行,这(除其他外)停止发送“keepalives”,最终连接失败并出现客户端错误:

The client has been inactive since <date> and it has exceeded the inactivity timeout of 50000 ms. Stopping the connection.

此后的标准程序是自动重新启动连接,但这没有任何作用。 If/when 用户重新激活页面(例如通过切换到选项卡),一切开始 spring 恢复正常,尽管 SignalR 连接已关闭。

经过大量调查,我们似乎受到了 Chrome v88 中引入的 this change 的影响,如果 [=19=,计时器 (setTimeouts) 受到严格限制]

5 minutes/30 秒条件符合我们收到的报告。然而,我们的页面上 运行ning 非常基本 Javascript:在我们自己的代码中只有两次使用 setTimeout,它们都不能“链接”(递归)到他们自己。我们也无法重现该问题:它在我们的测试中发生过,但我们无法使它可靠地发生。通过 chrome://flags/#intensive-wake-up-throttling 禁用此功能似乎可以缓解此问题 - 但当然,我们不能将此作为使用我们网站的要求。

站点上唯一的其他 Javascript 运行ning 是 jquery.signalR-2.4.1.js,并且从 SignalR 来源来看,那里有很多 setTimeout。 Chrome 中的这一变化会影响 SignalR 吗?也许当它在临时网络问题或其他一些不可预测的事件后尝试静默重新连接时?

如果没有,有没有办法在任何浏览器或 IDE 中跟踪启动了哪些计时器(更重要的是,“链接”),这样我们就可以看到是什么触发了这个限制?

我们的 signalR(WebSockets 作为传输)也面临着问题。我们无法在我们的实验室中重现它。我们客户的 HAR 文件和扩展日志记录仅向我们提供了以下信息:客户端“仅在关注感兴趣的组后才消费”未在保持连接所需的默认 30 秒内发送 ping。因此服务器关闭连接。我们在 signalR 客户端库中添加了日志,只看到 ping 计时器没有按时命中。没有错误,什么都没有。 (客户端是 JavaScript,问题发生在 chrome 87 的客户网站上(那里已经对一半的 chrome 用户实施了限制 - https://support.google.com/chrome/a/answer/7679408#87))

世界正在慢慢意识到“一个问题”:https://github.com/SignalR/SignalR/issues/4536

我们为客户提供的快速帮助是从服务器站点创建一个具有手动广播乒乓机制的 ET,每个客户都必须回答。在提供“更好”的解决方案或修复之前,避免依赖于 signalR 库中的 JavaScript ping。

我知道它并不能完全解决 chrome 的问题,但是,使用 chromium 引擎的新边缘添加了一些新设置来控制超时(因为它也受到了变化的影响).有一个新的白名单选项,它至少赋予用户决定哪些页面被排除在这种行为之外的权力。老实说,我相信 google 迟早会添加这些设置。在此之前,如果我们的客户受到影响,我们建议他们切换到边缘。

您可以在 settings\system 中找到它:

作为解决方法,java可以修改执行 ping 的脚本库,以稍微改变它使用计时器的方式。密集节流的条件之一是setTimeout()/setInterval()链数为5+。通过使用 web worker,可以避免重复调用。主线程可以 post 向 web worker 发送一条虚拟消息,而 web worker 除了 post 将一条虚拟消息返回主线程外什么都不做。随后的 setTimeout() 调用可以在来自 web worker 的消息事件上进行。

  1. main_thread_ping_function :- doPing() -> post_CallMeBack_ToWebWorker()
  2. web_worker :- onmessage -> post_CallingYouBack_ToMainThread()
  3. main_thread :- web_worker.onmessage -> setTimeout(main_thread_ping_function, timeoutValue)

由于 setTimeout() 是在来自 web worker 的消息上调用的,而不是来自 setTimout() 执行流程的消息,因此链长度保持为一,因此 [= 不会进行密集的节流50=] 88+.

请注意,Web Worker 中的链式 setTimeout() 调用目前不受 chrome 的限制,因此在 Web Worker 中定义计时器功能,并对消息进行操作(到从 web worker 执行 ping),也解决了问题。但是,如果 chrome 开发人员也决定限制 web worker 中的计时器,将来它会再次损坏。

一个实用程序(类似于 java 调度执行程序),它允许使用 Web 工作者调度回调,以避免通过上下文切换进行节流:

class NonThrottledScheduledExecutor {


  constructor(callbackFn, initialDelay, delay) {
    this.running = false;
    this.callback = callbackFn;
    this.initialDelay = initialDelay;
    this.delay = delay;
  };

  start() {
    if (this.running) {
      return;
    }
    this.running = true;
    // Code in worker.
    let workerFunction = "onmessage = function(e) { postMessage('fireTimer'); }";
    this.worker = new Worker(URL.createObjectURL(new Blob([workerFunction], {
      type: 'text/javascript'
    })));
    // On a message from worker, schedule the next round.
    this.worker.onmessage = (e) => setTimeout(this.fireTimerNow.bind(this), this.delay);
    // Start the first round.
    setTimeout(this.fireTimerNow.bind(this), this.initialDelay);

  };

  fireTimerNow() {
    if (this.running) {
      this.callback();
      // dummy message to be posted to web worker.
      this.worker.postMessage('callBackNow');
    }
  };

  stop() {
    if (this.running) {
      this.running = false;
      this.worker.terminate();
      this.worker = undefined;
    }
  };

};
<button onclick="startExecutor()">Start Executor</button>
<button onclick="stopExecutor()">Stop Executor</button>
<div id="op"></div>
<script>
  var executor;
  
  function startExecutor() {
    if (typeof(executor) == 'undefined') {
      // Schedules execution of 'doThis' function every 2seconds, after an intial delay of 1 sec
      executor = new NonThrottledScheduledExecutor(doThis, 1000, 2000);
      executor.start();
      console.log("Started scheduled executor");
    }
  }

  function stopExecutor() {
    if (typeof(executor) != 'undefined') {
      executor.stop();
      executor = undefined;
      document.getElementById("op").innerHTML = "Executor stopped at " + l;
    }
  }

  var l = 0;

  function doThis() {
    l = l + 1;
    document.getElementById("op").innerHTML = "Executor running... I will run even when the my window is hidden.. counter: " + l;
  }
</script>

Microsoft 已经发布了 SignalR 2.4.2,它应该可以在本地解决这个问题并且不需要任何手动解决方法。

Nuget 包可用here, and the list of fixed issues is here