Spread the update downloads over 6 hours
This commit is contained in:
parent
dcac698631
commit
16e877ece4
4 changed files with 194 additions and 5 deletions
17
ts/scripts/prepare-no-delay-release.ts
Normal file
17
ts/scripts/prepare-no-delay-release.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const PACKAGE_FILE = join(__dirname, '..', '..', 'package.json');
|
||||
|
||||
const json = JSON.parse(fs.readFileSync(PACKAGE_FILE, { encoding: 'utf8' }));
|
||||
|
||||
json.build.mac.releaseInfo.vendor.noDelay = true;
|
||||
json.build.win.releaseInfo.vendor.noDelay = true;
|
||||
|
||||
fs.writeFileSync(PACKAGE_FILE, JSON.stringify(json, null, ' '));
|
||||
93
ts/test-node/updater/util_test.ts
Normal file
93
ts/test-node/updater/util_test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isTimeToUpdate } from '../../updater/util';
|
||||
import { HOUR } from '../../util/durations';
|
||||
import * as logger from '../../logging/log';
|
||||
|
||||
describe('updater/util', () => {
|
||||
const now = 1745522337601;
|
||||
describe('isTimeToUpdate', () => {
|
||||
it('should update immediately if update is too far in the past', () => {
|
||||
assert.isTrue(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abc',
|
||||
releasedAt: new Date(0).getTime(),
|
||||
now,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update immediately if release date invalid', () => {
|
||||
assert.isTrue(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abc',
|
||||
releasedAt: NaN,
|
||||
now,
|
||||
})
|
||||
);
|
||||
|
||||
assert.isTrue(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abc',
|
||||
releasedAt: Infinity,
|
||||
now,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should delay the update', () => {
|
||||
assert.isFalse(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abcd',
|
||||
releasedAt: now,
|
||||
now,
|
||||
})
|
||||
);
|
||||
|
||||
assert.isFalse(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abcd',
|
||||
releasedAt: now,
|
||||
now: now + HOUR,
|
||||
})
|
||||
);
|
||||
|
||||
assert.isTrue(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abcd',
|
||||
releasedAt: now,
|
||||
now: now + 2 * HOUR,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should compute the delay based on pollId', () => {
|
||||
assert.isFalse(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abc',
|
||||
releasedAt: now,
|
||||
now,
|
||||
})
|
||||
);
|
||||
|
||||
assert.isTrue(
|
||||
isTimeToUpdate({
|
||||
logger,
|
||||
pollId: 'abc',
|
||||
releasedAt: now,
|
||||
now: now + HOUR,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -53,7 +53,12 @@ import {
|
|||
prepareDownload as prepareDifferentialDownload,
|
||||
} from './differential';
|
||||
import { getGotOptions } from './got';
|
||||
import { checkIntegrity, gracefulRename, gracefulRmRecursive } from './util';
|
||||
import {
|
||||
checkIntegrity,
|
||||
gracefulRename,
|
||||
gracefulRmRecursive,
|
||||
isTimeToUpdate,
|
||||
} from './util';
|
||||
|
||||
const POLL_INTERVAL = 30 * durations.MINUTE;
|
||||
|
||||
|
|
@ -61,6 +66,11 @@ type JSONVendorSchema = {
|
|||
minOSVersion?: string;
|
||||
requireManualUpdate?: 'true' | 'false';
|
||||
requireUserConfirmation?: 'true' | 'false';
|
||||
|
||||
// If 'true' - the update will be autodownloaded as soon as it becomes
|
||||
// available. Otherwise a delay up to 6h might be applied. See
|
||||
// `isTimeToUpdate`.
|
||||
noDelay?: 'true' | 'false';
|
||||
};
|
||||
|
||||
type JSONUpdateSchema = {
|
||||
|
|
@ -142,6 +152,10 @@ export abstract class Updater {
|
|||
#autoRetryAttempts = 0;
|
||||
#autoRetryAfter: number | undefined;
|
||||
|
||||
// Just a stable randomness that is used for determining the update time. The
|
||||
// value does not have to be consistent across restarts.
|
||||
#pollId = getGuid();
|
||||
|
||||
constructor({
|
||||
settingsChannel,
|
||||
logger,
|
||||
|
|
@ -580,6 +594,26 @@ export abstract class Updater {
|
|||
return;
|
||||
}
|
||||
|
||||
if (checkType === CheckType.Normal && vendor?.noDelay !== 'true') {
|
||||
try {
|
||||
const releasedAt = new Date(parsedYaml.releaseDate).getTime();
|
||||
|
||||
if (
|
||||
!isTimeToUpdate({
|
||||
logger: this.logger,
|
||||
pollId: this.#pollId,
|
||||
releasedAt,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`checkForUpdates: failed to compute delay for ${parsedYaml.releaseDate}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`checkForUpdates: found newer version ${version} ` +
|
||||
`checkType=${checkType}`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
|
|
@ -8,7 +8,7 @@ import { pipeline } from 'stream/promises';
|
|||
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as durations from '../util/durations';
|
||||
import { SECOND, MINUTE, HOUR } from '../util/durations';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { isOlderThan } from '../util/timestamp';
|
||||
|
||||
|
|
@ -55,8 +55,8 @@ async function doGracefulFSOperation<Args extends ReadonlyArray<unknown>>({
|
|||
logger,
|
||||
startedAt,
|
||||
retryCount,
|
||||
retryAfter = 5 * durations.SECOND,
|
||||
timeout = 5 * durations.MINUTE,
|
||||
retryAfter = 5 * SECOND,
|
||||
timeout = 5 * MINUTE,
|
||||
}: {
|
||||
name: string;
|
||||
operation: (...args: Args) => Promise<void>;
|
||||
|
|
@ -138,3 +138,48 @@ export async function gracefulRmRecursive(
|
|||
retryCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const MAX_UPDATE_DELAY = 6 * HOUR;
|
||||
|
||||
export function isTimeToUpdate({
|
||||
logger,
|
||||
pollId,
|
||||
releasedAt,
|
||||
now = Date.now(),
|
||||
maxDelay = MAX_UPDATE_DELAY,
|
||||
}: {
|
||||
logger: LoggerType;
|
||||
pollId: string;
|
||||
releasedAt: number;
|
||||
now?: number;
|
||||
maxDelay?: number;
|
||||
}): boolean {
|
||||
// Check that the release date is a proper number
|
||||
if (!Number.isFinite(releasedAt) || Number.isNaN(releasedAt)) {
|
||||
logger.warn('updater/isTimeToUpdate: invalid releasedAt');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check that the release date is not too far in the future
|
||||
if (releasedAt - HOUR > now) {
|
||||
logger.warn('updater/isTimeToUpdate: releasedAt too far in the future');
|
||||
return true;
|
||||
}
|
||||
|
||||
const digest = createHash('sha512')
|
||||
.update(pollId)
|
||||
.update(Buffer.alloc(1))
|
||||
.update(new Date(releasedAt).toJSON())
|
||||
.digest();
|
||||
|
||||
const delay = maxDelay * (digest.readUInt32LE(0) / 0xffffffff);
|
||||
const updateAt = releasedAt + delay;
|
||||
|
||||
if (now >= updateAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const remaining = Math.round((updateAt - now) / MINUTE);
|
||||
logger.info(`updater/isTimeToUpdate: updating in ${remaining} minutes`);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue