You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Custom abort timeout logic for restarting delayed events that is compatible with the widget api (#4927)
* add custom local timout + add delay to 0 for normal local timeout. * consider retry limits for new custom error * mock the AbortError so we can reuse `actionUpdateFromErrors` * update comment
This commit is contained in:
@@ -79,7 +79,7 @@ export interface SessionConfig {
|
|||||||
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
||||||
// - we use membershipEvent if the option is related to the rtc member state event.
|
// - we use membershipEvent if the option is related to the rtc member state event.
|
||||||
// - we use the technical term expiry if the option is related to the expiry field of the membership state event.
|
// - we use the technical term expiry if the option is related to the expiry field of the membership state event.
|
||||||
// - we use a `MS` postfix if the option is a duration to avoid using words like:
|
// - we use a `Ms` postfix if the option is a duration to avoid using words like:
|
||||||
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
|
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
|
||||||
export interface MembershipConfig {
|
export interface MembershipConfig {
|
||||||
/**
|
/**
|
||||||
@@ -143,6 +143,7 @@ export interface MembershipConfig {
|
|||||||
* failed to send due to a network error. (send membership event, send delayed event, restart delayed event...)
|
* failed to send due to a network error. (send membership event, send delayed event, restart delayed event...)
|
||||||
*/
|
*/
|
||||||
networkErrorRetryMs?: number;
|
networkErrorRetryMs?: number;
|
||||||
|
|
||||||
/** @deprecated renamed to `networkErrorRetryMs`*/
|
/** @deprecated renamed to `networkErrorRetryMs`*/
|
||||||
callMemberEventRetryDelayMinimum?: number;
|
callMemberEventRetryDelayMinimum?: number;
|
||||||
|
|
||||||
@@ -150,6 +151,17 @@ export interface MembershipConfig {
|
|||||||
* If true, use the new to-device transport for sending encryption keys.
|
* If true, use the new to-device transport for sending encryption keys.
|
||||||
*/
|
*/
|
||||||
useExperimentalToDeviceTransport?: boolean;
|
useExperimentalToDeviceTransport?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time (in milliseconds) after which a we consider a delayed event restart http request to have failed.
|
||||||
|
* Setting this to a lower value will result in more frequent retries but also a higher chance of failiour.
|
||||||
|
*
|
||||||
|
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
|
||||||
|
* helps by keeping more delayed event reset candidates in flight,
|
||||||
|
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
|
||||||
|
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||||
|
*/
|
||||||
|
delayedLeaveEventRestartLocalTimeoutMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncryptionConfig {
|
export interface EncryptionConfig {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import { AbortError } from "p-retry";
|
||||||
|
|
||||||
import { EventType } from "../@types/event.ts";
|
import { EventType } from "../@types/event.ts";
|
||||||
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
||||||
@@ -85,19 +86,24 @@ export enum MembershipActionType {
|
|||||||
// -> MembershipActionType.SendJoinEvent if successful
|
// -> MembershipActionType.SendJoinEvent if successful
|
||||||
// -> DelayedLeaveActionType.SendDelayedEvent on error, retry sending the first delayed event.
|
// -> DelayedLeaveActionType.SendDelayedEvent on error, retry sending the first delayed event.
|
||||||
// -> DelayedLeaveActionType.RestartDelayedEvent on success start updating the delayed event
|
// -> DelayedLeaveActionType.RestartDelayedEvent on success start updating the delayed event
|
||||||
|
|
||||||
SendJoinEvent = "SendJoinEvent",
|
SendJoinEvent = "SendJoinEvent",
|
||||||
// -> MembershipActionType.SendJoinEvent if we run into a rate limit and need to retry
|
// -> MembershipActionType.SendJoinEvent if we run into a rate limit and need to retry
|
||||||
// -> MembershipActionType.Update if we successfully send the join event then schedule the expire event update
|
// -> MembershipActionType.Update if we successfully send the join event then schedule the expire event update
|
||||||
// -> DelayedLeaveActionType.RestartDelayedEvent to recheck the delayed event
|
// -> DelayedLeaveActionType.RestartDelayedEvent to recheck the delayed event
|
||||||
|
|
||||||
RestartDelayedEvent = "RestartDelayedEvent",
|
RestartDelayedEvent = "RestartDelayedEvent",
|
||||||
// -> DelayedLeaveActionType.SendMainDelayedEvent on missing delay id but there is a rtc state event
|
// -> DelayedLeaveActionType.SendMainDelayedEvent on missing delay id but there is a rtc state event
|
||||||
// -> DelayedLeaveActionType.SendDelayedEvent on missing delay id and there is no state event
|
// -> DelayedLeaveActionType.SendDelayedEvent on missing delay id and there is no state event
|
||||||
// -> DelayedLeaveActionType.RestartDelayedEvent on success we schedule the next restart
|
// -> DelayedLeaveActionType.RestartDelayedEvent on success we schedule the next restart
|
||||||
|
|
||||||
UpdateExpiry = "UpdateExpiry",
|
UpdateExpiry = "UpdateExpiry",
|
||||||
// -> MembershipActionType.Update if the timeout has passed so the next update is required.
|
// -> MembershipActionType.Update if the timeout has passed so the next update is required.
|
||||||
|
|
||||||
SendScheduledDelayedLeaveEvent = "SendScheduledDelayedLeaveEvent",
|
SendScheduledDelayedLeaveEvent = "SendScheduledDelayedLeaveEvent",
|
||||||
// -> MembershipActionType.SendLeaveEvent on failiour (not found) we need to send the leave manually and cannot use the scheduled delayed event
|
// -> MembershipActionType.SendLeaveEvent on failiour (not found) we need to send the leave manually and cannot use the scheduled delayed event
|
||||||
// -> DelayedLeaveActionType.SendScheduledDelayedLeaveEvent on error we try again.
|
// -> DelayedLeaveActionType.SendScheduledDelayedLeaveEvent on error we try again.
|
||||||
|
|
||||||
SendLeaveEvent = "SendLeaveEvent",
|
SendLeaveEvent = "SendLeaveEvent",
|
||||||
// -> MembershipActionType.SendLeaveEvent
|
// -> MembershipActionType.SendLeaveEvent
|
||||||
}
|
}
|
||||||
@@ -385,6 +391,9 @@ export class MembershipManager
|
|||||||
return this.joinConfig?.maximumNetworkErrorRetryCount ?? 10;
|
return this.joinConfig?.maximumNetworkErrorRetryCount ?? 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get delayedLeaveEventRestartLocalTimeoutMs(): number {
|
||||||
|
return this.joinConfig?.delayedLeaveEventRestartLocalTimeoutMs ?? 2000;
|
||||||
|
}
|
||||||
// LOOP HANDLER:
|
// LOOP HANDLER:
|
||||||
private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> {
|
private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -536,8 +545,18 @@ export class MembershipManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
|
private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
|
||||||
return await this.client
|
const abortPromise = new Promise((_, reject) => {
|
||||||
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart)
|
setTimeout(() => {
|
||||||
|
reject(new AbortError("Restart delayed event timed out before the HS responded"));
|
||||||
|
}, this.delayedLeaveEventRestartLocalTimeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
|
||||||
|
// to the widget driver this information would ge lost. That is why we mimic the AbortError using the race.
|
||||||
|
return await Promise.race([
|
||||||
|
this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart),
|
||||||
|
abortPromise,
|
||||||
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
|
this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
|
||||||
return createInsertActionUpdate(
|
return createInsertActionUpdate(
|
||||||
@@ -786,14 +805,19 @@ export class MembershipManager
|
|||||||
private actionUpdateFromNetworkErrorRetry(error: unknown, type: MembershipActionType): ActionUpdate | undefined {
|
private actionUpdateFromNetworkErrorRetry(error: unknown, type: MembershipActionType): ActionUpdate | undefined {
|
||||||
// "Is a network error"-boundary
|
// "Is a network error"-boundary
|
||||||
const retries = this.state.networkErrorRetries.get(type) ?? 0;
|
const retries = this.state.networkErrorRetries.get(type) ?? 0;
|
||||||
|
|
||||||
|
// Strings for error logging
|
||||||
const retryDurationString = this.networkErrorRetryMs / 1000 + "s";
|
const retryDurationString = this.networkErrorRetryMs / 1000 + "s";
|
||||||
const retryCounterString = "(" + retries + "/" + this.maximumNetworkErrorRetryCount + ")";
|
const retryCounterString = "(" + retries + "/" + this.maximumNetworkErrorRetryCount + ")";
|
||||||
|
|
||||||
|
// Variables for scheduling the new event
|
||||||
|
let retryDuration = this.networkErrorRetryMs;
|
||||||
|
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
// We do not wait for the timeout on local timeouts.
|
||||||
|
retryDuration = 0;
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
"Network local timeout error while sending event, retrying in " +
|
"Network local timeout error while sending event, immediate retry (" + retryCounterString + ")",
|
||||||
retryDurationString +
|
|
||||||
" " +
|
|
||||||
retryCounterString,
|
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
} else if (error instanceof Error && error.message.includes("updating delayed event")) {
|
} else if (error instanceof Error && error.message.includes("updating delayed event")) {
|
||||||
@@ -836,7 +860,7 @@ export class MembershipManager
|
|||||||
// retry boundary
|
// retry boundary
|
||||||
if (retries < this.maximumNetworkErrorRetryCount) {
|
if (retries < this.maximumNetworkErrorRetryCount) {
|
||||||
this.state.networkErrorRetries.set(type, retries + 1);
|
this.state.networkErrorRetries.set(type, retries + 1);
|
||||||
return createInsertActionUpdate(type, this.networkErrorRetryMs);
|
return createInsertActionUpdate(type, retryDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failure
|
// Failure
|
||||||
|
|||||||
Reference in New Issue
Block a user