mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
cc9a9e8503
This commit makes a few changes to the Action Cable client to prevent a "thundering herd" of client reconnects after server connectivity loss: * The client will wait a random amount between 1x and 3x of the stale threshold after the server's last ping before making the first reconnection attempt. * Subsequent reconnection attempts now use exponential backoff instead of logarithmic backoff. To allow the delay between reconnection attempts to increase slowly at first, the default exponentiation base is < 2. * Random jitter is applied to each delay between reconnection attempts. Co-authored-by: John Williams <john@veloshots.com>
125 lines
3.5 KiB
JavaScript
125 lines
3.5 KiB
JavaScript
import logger from "./logger"
|
|
|
|
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
|
|
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
|
|
|
|
const now = () => new Date().getTime()
|
|
|
|
const secondsSince = time => (now() - time) / 1000
|
|
|
|
class ConnectionMonitor {
|
|
constructor(connection) {
|
|
this.visibilityDidChange = this.visibilityDidChange.bind(this)
|
|
this.connection = connection
|
|
this.reconnectAttempts = 0
|
|
}
|
|
|
|
start() {
|
|
if (!this.isRunning()) {
|
|
this.startedAt = now()
|
|
delete this.stoppedAt
|
|
this.startPolling()
|
|
addEventListener("visibilitychange", this.visibilityDidChange)
|
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`)
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
if (this.isRunning()) {
|
|
this.stoppedAt = now()
|
|
this.stopPolling()
|
|
removeEventListener("visibilitychange", this.visibilityDidChange)
|
|
logger.log("ConnectionMonitor stopped")
|
|
}
|
|
}
|
|
|
|
isRunning() {
|
|
return this.startedAt && !this.stoppedAt
|
|
}
|
|
|
|
recordPing() {
|
|
this.pingedAt = now()
|
|
}
|
|
|
|
recordConnect() {
|
|
this.reconnectAttempts = 0
|
|
this.recordPing()
|
|
delete this.disconnectedAt
|
|
logger.log("ConnectionMonitor recorded connect")
|
|
}
|
|
|
|
recordDisconnect() {
|
|
this.disconnectedAt = now()
|
|
logger.log("ConnectionMonitor recorded disconnect")
|
|
}
|
|
|
|
// Private
|
|
|
|
startPolling() {
|
|
this.stopPolling()
|
|
this.poll()
|
|
}
|
|
|
|
stopPolling() {
|
|
clearTimeout(this.pollTimeout)
|
|
}
|
|
|
|
poll() {
|
|
this.pollTimeout = setTimeout(() => {
|
|
this.reconnectIfStale()
|
|
this.poll()
|
|
}
|
|
, this.getPollInterval())
|
|
}
|
|
|
|
getPollInterval() {
|
|
const { staleThreshold, reconnectionBackoffRate } = this.constructor
|
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10))
|
|
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate
|
|
const jitter = jitterMax * Math.random()
|
|
return staleThreshold * 1000 * backoff * (1 + jitter)
|
|
}
|
|
|
|
reconnectIfStale() {
|
|
if (this.connectionIsStale()) {
|
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
|
|
this.reconnectAttempts++
|
|
if (this.disconnectedRecently()) {
|
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`)
|
|
} else {
|
|
logger.log("ConnectionMonitor reopening")
|
|
this.connection.reopen()
|
|
}
|
|
}
|
|
}
|
|
|
|
get refreshedAt() {
|
|
return this.pingedAt ? this.pingedAt : this.startedAt
|
|
}
|
|
|
|
connectionIsStale() {
|
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold
|
|
}
|
|
|
|
disconnectedRecently() {
|
|
return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold)
|
|
}
|
|
|
|
visibilityDidChange() {
|
|
if (document.visibilityState === "visible") {
|
|
setTimeout(() => {
|
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`)
|
|
this.connection.reopen()
|
|
}
|
|
}
|
|
, 200)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
|
ConnectionMonitor.reconnectionBackoffRate = 0.15
|
|
|
|
export default ConnectionMonitor
|