1
0
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:
Timo
2025-07-21 20:42:21 +02:00
committed by GitHub
parent 0ce944f3da
commit 11c9e39e5a
2 changed files with 44 additions and 8 deletions

View File

@@ -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 {

View File

@@ -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