-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exponentially backs off retry attempts for sending usage data #117955
Changes from 1 commit
10226eb
5ab66fa
8fdf14b
a81be14
7dc97b4
7024db1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,10 +17,10 @@ import type { EncryptedTelemetryPayload } from '../../common/types'; | |
|
||
export class TelemetrySender { | ||
private readonly telemetryService: TelemetryService; | ||
private isSending: boolean = false; | ||
private lastReported?: string; | ||
private readonly storage: Storage; | ||
private intervalId?: number; | ||
private intervalId: number = 0; // setInterval returns a positive integer, 0 means no interval is set | ||
private retryCount: number = 0; | ||
|
||
constructor(telemetryService: TelemetryService) { | ||
this.telemetryService = telemetryService; | ||
|
@@ -53,13 +53,22 @@ export class TelemetrySender { | |
return false; | ||
}; | ||
|
||
private delayRetrySend = () => { | ||
return 60 * (1000 * Math.min(Math.pow(2, this.retryCount), 64)); // 120s, 240s, 480s, 960s, 1920s, 3840s, 3840s, 3840s | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be written as We limit the backoff period to just over once per hour. |
||
}; | ||
|
||
private sendIfDue = async (): Promise<void> => { | ||
if (this.isSending || !this.shouldSendReport()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the removal of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (!this.shouldSendReport()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exit early if and only if the next report isn't due to be sent. This backs-off on the once-per minute fetch and send request calls we used to make. |
||
return; | ||
} | ||
// optimistically update the report date and reset the retry counter for a new time report interval window | ||
this.lastReported = `${Date.now()}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a behavior change here where we're now updating the report date before sending the data vs only after a successful send. We need to do this to ensure we're backing off the retry attempts. |
||
this.saveToBrowser(); | ||
this.retryCount = 0; | ||
await this.sendUsageData(); | ||
}; | ||
|
||
// mark that we are working so future requests are ignored until we're done | ||
this.isSending = true; | ||
private sendUsageData = async (): Promise<void> => { | ||
try { | ||
const telemetryUrl = this.telemetryService.getTelemetryUrl(); | ||
const telemetryPayload: EncryptedTelemetryPayload = | ||
|
@@ -80,17 +89,18 @@ export class TelemetrySender { | |
}) | ||
) | ||
); | ||
this.lastReported = `${Date.now()}`; | ||
this.saveToBrowser(); | ||
} catch (err) { | ||
// ignore err | ||
} finally { | ||
this.isSending = false; | ||
// ignore err and try again but after a longer wait period. | ||
this.retryCount = this.retryCount + 1; | ||
if (this.retryCount < 20) { | ||
// try once again at 60s, then exponentially backoff the time between subsequent retries | ||
TinaHeiligers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
window.setTimeout(this.sendUsageData, this.delayRetrySend()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT/optional: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent suggestion, thank you! It also made the testing much easier. |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: maybe we want to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done: https://github.com/elastic/kibana/pull/117955/files#diff-5a45ad5f1ca947b4b167609152746b1599cf51aac704cd7b181bb73cec63e208R100-R102 with tests updated. |
||
} | ||
}; | ||
|
||
public startChecking = () => { | ||
if (typeof this.intervalId === 'undefined') { | ||
if (this.intervalId === 0) { | ||
this.intervalId = window.setInterval(this.sendIfDue, 60000); | ||
} | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Thinking out loud, not related to the PR as it was already present) This is a code smell of bad isolation. Proper unit testing should never require to mock the blackboxed component.
e.g
shouldSendReport
mocking could be replaced withtelemetryService.canSendTelemetry
mocking (which would be the correct way to test the method's behavior btw). Here, we need to mock some of theTelemetrySender
's methods to test the behavior of other methods of the class, which is not unit testing the class.