GramJs: Fix upload hanging (#3475)
This commit is contained in:
parent
332ae408a4
commit
44b3f4d8dd
@ -72,6 +72,7 @@ import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnl
|
|||||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||||
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||||
import { updateChannelState } from '../updateManager';
|
import { updateChannelState } from '../updateManager';
|
||||||
|
import { compact } from '../../../util/iteratees';
|
||||||
|
|
||||||
const FAST_SEND_TIMEOUT = 1000;
|
const FAST_SEND_TIMEOUT = 1000;
|
||||||
const INPUT_WAVEFORM_LENGTH = 63;
|
const INPUT_WAVEFORM_LENGTH = 63;
|
||||||
@ -620,11 +621,18 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const file = await fetchFile(blobUrl, filename);
|
const fetchAndUpload = async (url: string) => {
|
||||||
const inputFile = await uploadFile(file, patchedOnProgress);
|
const file = await fetchFile(url, filename);
|
||||||
|
return uploadFile(file, patchedOnProgress);
|
||||||
|
};
|
||||||
|
|
||||||
const thumbFile = previewBlobUrl && await fetchFile(previewBlobUrl, filename);
|
const isVideo = SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType);
|
||||||
const thumb = thumbFile ? await uploadFile(thumbFile) : undefined;
|
const shouldUploadThumb = audio || isVideo || shouldSendAsFile;
|
||||||
|
|
||||||
|
const [inputFile, thumb] = await Promise.all(compact([
|
||||||
|
fetchAndUpload(blobUrl),
|
||||||
|
shouldUploadThumb && previewBlobUrl && fetchAndUpload(previewBlobUrl),
|
||||||
|
]));
|
||||||
|
|
||||||
const attributes: GramJs.TypeDocumentAttribute[] = [new GramJs.DocumentAttributeFilename({ fileName: filename })];
|
const attributes: GramJs.TypeDocumentAttribute[] = [new GramJs.DocumentAttributeFilename({ fileName: filename })];
|
||||||
if (!shouldSendAsFile) {
|
if (!shouldSendAsFile) {
|
||||||
@ -636,7 +644,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
|
if (isVideo) {
|
||||||
const { width, height, duration } = quick;
|
const { width, height, duration } = quick;
|
||||||
if (duration !== undefined) {
|
if (duration !== undefined) {
|
||||||
attributes.push(new GramJs.DocumentAttributeVideo({
|
attributes.push(new GramJs.DocumentAttributeVideo({
|
||||||
|
|||||||
@ -27,12 +27,21 @@ const {
|
|||||||
getTmpPassword,
|
getTmpPassword,
|
||||||
} = require('./2fa');
|
} = require('./2fa');
|
||||||
const RequestState = require('../network/RequestState');
|
const RequestState = require('../network/RequestState');
|
||||||
|
const withAbortCheck = require('../../../util/withAbortCheck').default;
|
||||||
|
const { AbortError } = require('../../../util/withAbortCheck');
|
||||||
|
|
||||||
|
class ConnectTimeoutError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Connection Timeout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_DC_ID = 2;
|
const DEFAULT_DC_ID = 2;
|
||||||
const WEBDOCUMENT_DC_ID = 4;
|
const WEBDOCUMENT_DC_ID = 4;
|
||||||
const EXPORTED_SENDER_RECONNECT_TIMEOUT = 1000; // 1 sec
|
const EXPORTED_SENDER_RECONNECT_TIMEOUT = 1000; // 1 sec
|
||||||
const EXPORTED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec
|
const EXPORTED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec
|
||||||
const WEBDOCUMENT_REQUEST_PART_SIZE = 131072; // 128kb
|
const WEBDOCUMENT_REQUEST_PART_SIZE = 131072; // 128kb
|
||||||
|
const EXPORTED_SENDER_CONNECT_TIMEOUT = 8000; // 8 sec
|
||||||
|
|
||||||
const PING_INTERVAL = 3000; // 3 sec
|
const PING_INTERVAL = 3000; // 3 sec
|
||||||
const PING_TIMEOUT = 5000; // 5 sec
|
const PING_TIMEOUT = 5000; // 5 sec
|
||||||
@ -156,6 +165,7 @@ class TelegramClient {
|
|||||||
this._config = undefined;
|
this._config = undefined;
|
||||||
this.phoneCodeHashes = [];
|
this.phoneCodeHashes = [];
|
||||||
this._exportedSenderPromises = {};
|
this._exportedSenderPromises = {};
|
||||||
|
this._exportedSenderAbortControllers = {};
|
||||||
this._waitingForAuthKey = {};
|
this._waitingForAuthKey = {};
|
||||||
this._exportedSenderReleaseTimeouts = {};
|
this._exportedSenderReleaseTimeouts = {};
|
||||||
this._additionalDcsDisabled = args.additionalDcsDisabled;
|
this._additionalDcsDisabled = args.additionalDcsDisabled;
|
||||||
@ -328,6 +338,7 @@ class TelegramClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this._exportedSenderPromises = {};
|
this._exportedSenderPromises = {};
|
||||||
|
this._exportedSenderAbortControllers = {};
|
||||||
this._waitingForAuthKey = {};
|
this._waitingForAuthKey = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,6 +385,7 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
const sender = await this._exportedSenderPromises[dcId][index];
|
const sender = await this._exportedSenderPromises[dcId][index];
|
||||||
delete this._exportedSenderPromises[dcId][index];
|
delete this._exportedSenderPromises[dcId][index];
|
||||||
|
delete this._exportedSenderAbortControllers[dcId][index];
|
||||||
await sender.disconnect();
|
await sender.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,6 +400,7 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._exportedSenderPromises[dcId] = {};
|
this._exportedSenderPromises[dcId] = {};
|
||||||
|
this._exportedSenderAbortControllers[dcId] = {};
|
||||||
|
|
||||||
await Promise.all(promises.map(async (promise) => {
|
await Promise.all(promises.map(async (promise) => {
|
||||||
const sender = await promise;
|
const sender = await promise;
|
||||||
@ -395,7 +408,7 @@ class TelegramClient {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _connectSender(sender, dcId, isPremium = false) {
|
async _connectSender(sender, dcId, index, isPremium = false, abortSignal) {
|
||||||
// if we don't already have an auth key we want to use normal DCs not -1
|
// if we don't already have an auth key we want to use normal DCs not -1
|
||||||
let hasAuthKey = Boolean(sender.authKey.getKey());
|
let hasAuthKey = Boolean(sender.authKey.getKey());
|
||||||
let firstConnectResolver;
|
let firstConnectResolver;
|
||||||
@ -419,7 +432,7 @@ class TelegramClient {
|
|||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
await sender.connect(new this._connection(
|
await withAbortCheck(abortSignal, sender.connect(new this._connection(
|
||||||
dc.ipAddress,
|
dc.ipAddress,
|
||||||
dc.port,
|
dc.port,
|
||||||
dcId,
|
dcId,
|
||||||
@ -427,18 +440,23 @@ class TelegramClient {
|
|||||||
this._args.testServers,
|
this._args.testServers,
|
||||||
// Premium DCs are not stable for obtaining auth keys, so need to we first connect to regular ones
|
// Premium DCs are not stable for obtaining auth keys, so need to we first connect to regular ones
|
||||||
hasAuthKey ? isPremium : false,
|
hasAuthKey ? isPremium : false,
|
||||||
));
|
)));
|
||||||
|
|
||||||
if (this.session.dcId !== dcId && !sender._authenticated) {
|
if (this.session.dcId !== dcId && !sender._authenticated) {
|
||||||
this._log.info(`Exporting authorization for data center ${dc.ipAddress}`);
|
this._log.info(`Exporting authorization for data center ${dc.ipAddress}`);
|
||||||
const auth = await this.invoke(new requests.auth.ExportAuthorization({ dcId }));
|
const auth = await withAbortCheck(
|
||||||
|
abortSignal,
|
||||||
|
this.invoke(new requests.auth.ExportAuthorization({ dcId })),
|
||||||
|
);
|
||||||
|
|
||||||
const req = this._initWith(new requests.auth.ImportAuthorization({
|
const req = this._initWith(new requests.auth.ImportAuthorization({
|
||||||
id: auth.id,
|
id: auth.id,
|
||||||
bytes: auth.bytes,
|
bytes: auth.bytes,
|
||||||
}));
|
}));
|
||||||
await sender.send(req);
|
await withAbortCheck(abortSignal, sender.send(req));
|
||||||
sender._authenticated = true;
|
sender._authenticated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.dcId = dcId;
|
sender.dcId = dcId;
|
||||||
sender.userDisconnected = false;
|
sender.userDisconnected = false;
|
||||||
|
|
||||||
@ -449,6 +467,12 @@ class TelegramClient {
|
|||||||
|
|
||||||
return sender;
|
return sender;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof AbortError) {
|
||||||
|
delete this._exportedSenderPromises[dcId][index];
|
||||||
|
delete this._exportedSenderAbortControllers[dcId][index];
|
||||||
|
sender.disconnect();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -458,26 +482,54 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConnectedExportedSenderIndex(dcId, i) {
|
||||||
|
const index = Object.keys(this._exportedSenderReleaseTimeouts[dcId] || {})[0];
|
||||||
|
const firstIndex = Number(index ?? 0);
|
||||||
|
return {
|
||||||
|
newIndex: firstIndex === i ? i + 1 : firstIndex,
|
||||||
|
noConnectedExportedSenders: index === undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async _borrowExportedSender(dcId, shouldReconnect, existingSender, index, isPremium) {
|
async _borrowExportedSender(dcId, shouldReconnect, existingSender, index, isPremium) {
|
||||||
if (this._additionalDcsDisabled) {
|
if (this._additionalDcsDisabled) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i = index || 0;
|
let i = index || 0;
|
||||||
|
|
||||||
if (!this._exportedSenderPromises[dcId]) this._exportedSenderPromises[dcId] = {};
|
if (!this._exportedSenderPromises[dcId]) this._exportedSenderPromises[dcId] = {};
|
||||||
|
if (!this._exportedSenderAbortControllers[dcId]) this._exportedSenderAbortControllers[dcId] = {};
|
||||||
|
|
||||||
|
if (this._exportedSenderAbortControllers[dcId][i]?.signal.aborted) {
|
||||||
|
const { newIndex } = this.getConnectedExportedSenderIndex(dcId, i);
|
||||||
|
i = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._exportedSenderPromises[dcId][i] || shouldReconnect) {
|
if (!this._exportedSenderPromises[dcId][i] || shouldReconnect) {
|
||||||
|
this._exportedSenderAbortControllers[dcId][i]?.abort();
|
||||||
|
this._exportedSenderAbortControllers[dcId][i] = new AbortController();
|
||||||
this._exportedSenderPromises[dcId][i] = this._connectSender(
|
this._exportedSenderPromises[dcId][i] = this._connectSender(
|
||||||
existingSender || this._createExportedSender(dcId, i),
|
existingSender || this._createExportedSender(dcId, i),
|
||||||
dcId,
|
dcId,
|
||||||
|
index,
|
||||||
isPremium,
|
isPremium,
|
||||||
|
this._exportedSenderAbortControllers[dcId][i].signal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sender;
|
let sender;
|
||||||
try {
|
try {
|
||||||
sender = await this._exportedSenderPromises[dcId][i];
|
sender = await Promise.race([
|
||||||
|
this._exportedSenderPromises[dcId][i],
|
||||||
|
Helpers.sleep(EXPORTED_SENDER_CONNECT_TIMEOUT).then(() => {
|
||||||
|
return Promise.reject(new ConnectTimeoutError());
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!sender) {
|
||||||
|
throw new ConnectTimeoutError();
|
||||||
|
}
|
||||||
|
|
||||||
if (!sender.isConnected()) {
|
if (!sender.isConnected()) {
|
||||||
if (sender.isConnecting) {
|
if (sender.isConnecting) {
|
||||||
@ -488,6 +540,11 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof ConnectTimeoutError) {
|
||||||
|
this._exportedSenderAbortControllers[dcId][i]?.abort();
|
||||||
|
const { newIndex, noConnectedExportedSenders } = this.getConnectedExportedSenderIndex(dcId, i);
|
||||||
|
return this._borrowExportedSender(dcId, noConnectedExportedSenders, undefined, newIndex, isPremium);
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
15
src/util/withAbortCheck.ts
Normal file
15
src/util/withAbortCheck.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export class AbortError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Aborted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function withAbortCheck<T>(abortSignal: AbortSignal, cb: Promise<T>): Promise<T> {
|
||||||
|
const result = await cb;
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
throw new AbortError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user