[Feature] Re-try mechanism for CodePush Rollbacks (#1467)
This commit is contained in:
Родитель
ac5472ee2a
Коммит
693b769ba6
74
CodePush.js
74
CodePush.js
|
@ -197,6 +197,7 @@ async function tryReportStatus(statusReport, resumeListener) {
|
|||
log(`Reporting CodePush update success (${label})`);
|
||||
} else {
|
||||
log(`Reporting CodePush update rollback (${label})`);
|
||||
await NativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash);
|
||||
}
|
||||
|
||||
config.deploymentKey = statusReport.package.deploymentKey;
|
||||
|
@ -225,6 +226,71 @@ async function tryReportStatus(statusReport, resumeListener) {
|
|||
}
|
||||
}
|
||||
|
||||
async function shouldUpdateBeIgnored(remotePackage, syncOptions) {
|
||||
let { rollbackRetryOptions } = syncOptions;
|
||||
|
||||
const isFailedPackage = remotePackage && remotePackage.failedInstall;
|
||||
if (!isFailedPackage || !syncOptions.ignoreFailedUpdates) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!rollbackRetryOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof rollbackRetryOptions !== "object") {
|
||||
rollbackRetryOptions = CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS;
|
||||
} else {
|
||||
rollbackRetryOptions = { ...CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS, ...rollbackRetryOptions };
|
||||
}
|
||||
|
||||
if (!validateRollbackRetryOptions(rollbackRetryOptions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const latestRollbackInfo = await NativeCodePush.getLatestRollbackInfo();
|
||||
if (!validateLatestRollbackInfo(latestRollbackInfo, remotePackage.packageHash)) {
|
||||
log("The latest rollback info is not valid.");
|
||||
return true;
|
||||
}
|
||||
|
||||
const { delayInHours, maxRetryAttempts } = rollbackRetryOptions;
|
||||
const hoursSinceLatestRollback = (Date.now() - latestRollbackInfo.time) / (1000 * 60 * 60);
|
||||
if (hoursSinceLatestRollback >= delayInHours && maxRetryAttempts >= latestRollbackInfo.count) {
|
||||
log("Previous rollback should be ignored due to rollback retry options.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateLatestRollbackInfo(latestRollbackInfo, packageHash) {
|
||||
return latestRollbackInfo &&
|
||||
latestRollbackInfo.time &&
|
||||
latestRollbackInfo.count &&
|
||||
latestRollbackInfo.packageHash &&
|
||||
latestRollbackInfo.packageHash === packageHash;
|
||||
}
|
||||
|
||||
function validateRollbackRetryOptions(rollbackRetryOptions) {
|
||||
if (typeof rollbackRetryOptions.delayInHours !== "number") {
|
||||
log("The 'delayInHours' rollback retry parameter must be a number.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof rollbackRetryOptions.maxRetryAttempts !== "number") {
|
||||
log("The 'maxRetryAttempts' rollback retry parameter must be a number.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rollbackRetryOptions.maxRetryAttempts < 1) {
|
||||
log("The 'maxRetryAttempts' rollback retry parameter cannot be less then 1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var testConfig;
|
||||
|
||||
// This function is only used for tests. Replaces the default SDK, configuration and native bridge
|
||||
|
@ -293,6 +359,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|||
const syncOptions = {
|
||||
deploymentKey: null,
|
||||
ignoreFailedUpdates: true,
|
||||
rollbackRetryOptions: null,
|
||||
installMode: CodePush.InstallMode.ON_NEXT_RESTART,
|
||||
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
|
||||
minimumBackgroundDuration: 0,
|
||||
|
@ -360,7 +427,8 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|||
return CodePush.SyncStatus.UPDATE_INSTALLED;
|
||||
};
|
||||
|
||||
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
|
||||
const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);
|
||||
|
||||
if (!remotePackage || updateShouldBeIgnored) {
|
||||
if (updateShouldBeIgnored) {
|
||||
log("An update is available, but it is being ignored due to having been previously rolled back.");
|
||||
|
@ -585,6 +653,10 @@ if (NativeCodePush) {
|
|||
optionalInstallButtonLabel: "Install",
|
||||
optionalUpdateMessage: "An update is available. Would you like to install it?",
|
||||
title: "Update available"
|
||||
},
|
||||
DEFAULT_ROLLBACK_RETRY_OPTIONS: {
|
||||
delayInHours: 24,
|
||||
maxRetryAttempts: 1
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -27,4 +27,8 @@ public class CodePushConstants {
|
|||
public static final String UNZIPPED_FOLDER_NAME = "unzipped";
|
||||
public static final String CODE_PUSH_APK_BUILD_TIME_KEY = "CODE_PUSH_APK_BUILD_TIME";
|
||||
public static final String BUNDLE_JWT_FILE = ".codepushrelease";
|
||||
public static final String LATEST_ROLLBACK_INFO_KEY = "LATEST_ROLLBACK_INFO";
|
||||
public static final String LATEST_ROLLBACK_PACKAGE_HASH_KEY = "packageHash";
|
||||
public static final String LATEST_ROLLBACK_TIME_KEY = "time";
|
||||
public static final String LATEST_ROLLBACK_COUNT_KEY = "count";
|
||||
}
|
||||
|
|
|
@ -510,7 +510,33 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
|
|||
public void isFailedUpdate(String packageHash, Promise promise) {
|
||||
try {
|
||||
promise.resolve(mSettingsManager.isFailedHash(packageHash));
|
||||
} catch(CodePushUnknownException e) {
|
||||
} catch (CodePushUnknownException e) {
|
||||
CodePushUtils.log(e);
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getLatestRollbackInfo(Promise promise) {
|
||||
try {
|
||||
JSONObject latestRollbackInfo = mSettingsManager.getLatestRollbackInfo();
|
||||
if (latestRollbackInfo != null) {
|
||||
promise.resolve(CodePushUtils.convertJsonObjectToWritable(latestRollbackInfo));
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
}
|
||||
} catch (CodePushUnknownException e) {
|
||||
CodePushUtils.log(e);
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setLatestRollbackInfo(String packageHash, Promise promise) {
|
||||
try {
|
||||
mSettingsManager.setLatestRollbackInfo(packageHash);
|
||||
promise.resolve(null);
|
||||
} catch (CodePushUnknownException e) {
|
||||
CodePushUtils.log(e);
|
||||
promise.reject(e);
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ public class CodePushUtils {
|
|||
map.putString(key, (String) obj);
|
||||
else if (obj instanceof Double)
|
||||
map.putDouble(key, (Double) obj);
|
||||
else if (obj instanceof Long)
|
||||
map.putDouble(key, ((Long) obj).doubleValue());
|
||||
else if (obj instanceof Integer)
|
||||
map.putInt(key, (Integer) obj);
|
||||
else if (obj instanceof Boolean)
|
||||
|
|
|
@ -74,8 +74,7 @@ public class SettingsManager {
|
|||
return pendingUpdate != null &&
|
||||
!pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY) &&
|
||||
(packageHash == null || pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY).equals(packageHash));
|
||||
}
|
||||
catch (JSONException e) {
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e);
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +88,15 @@ public class SettingsManager {
|
|||
}
|
||||
|
||||
public void saveFailedUpdate(JSONObject failedPackage) {
|
||||
try {
|
||||
if (isFailedHash(failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY))) {
|
||||
// Do not need to add the package if it is already in the failedUpdates.
|
||||
return;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read package hash from package.", e);
|
||||
}
|
||||
|
||||
String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null);
|
||||
JSONArray failedUpdates;
|
||||
if (failedUpdatesString == null) {
|
||||
|
@ -107,6 +115,49 @@ public class SettingsManager {
|
|||
mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
|
||||
}
|
||||
|
||||
public JSONObject getLatestRollbackInfo() {
|
||||
String latestRollbackInfoString = mSettings.getString(CodePushConstants.LATEST_ROLLBACK_INFO_KEY, null);
|
||||
if (latestRollbackInfoString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONObject(latestRollbackInfoString);
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
CodePushUtils.log("Unable to parse latest rollback metadata " + latestRollbackInfoString +
|
||||
" stored in SharedPreferences");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLatestRollbackInfo(String packageHash) {
|
||||
JSONObject latestRollbackInfo = getLatestRollbackInfo();
|
||||
int count = 0;
|
||||
|
||||
if (latestRollbackInfo != null) {
|
||||
try {
|
||||
String latestRollbackPackageHash = latestRollbackInfo.getString(CodePushConstants.LATEST_ROLLBACK_PACKAGE_HASH_KEY);
|
||||
if (latestRollbackPackageHash.equals(packageHash)) {
|
||||
count = latestRollbackInfo.getInt(CodePushConstants.LATEST_ROLLBACK_COUNT_KEY);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
CodePushUtils.log("Unable to parse latest rollback info.");
|
||||
}
|
||||
} else {
|
||||
latestRollbackInfo = new JSONObject();
|
||||
}
|
||||
|
||||
try {
|
||||
latestRollbackInfo.put(CodePushConstants.LATEST_ROLLBACK_PACKAGE_HASH_KEY, packageHash);
|
||||
latestRollbackInfo.put(CodePushConstants.LATEST_ROLLBACK_TIME_KEY, System.currentTimeMillis());
|
||||
latestRollbackInfo.put(CodePushConstants.LATEST_ROLLBACK_COUNT_KEY, count + 1);
|
||||
mSettings.edit().putString(CodePushConstants.LATEST_ROLLBACK_INFO_KEY, latestRollbackInfo.toString()).commit();
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to save latest rollback info.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void savePendingUpdate(String packageHash, boolean isLoading) {
|
||||
JSONObject pendingUpdate = new JSONObject();
|
||||
try {
|
||||
|
|
|
@ -148,6 +148,14 @@ The `codePush` decorator accepts an "options" object that allows you to customiz
|
|||
|
||||
* __title__ *(String)* - The text used as the header of an update notification that is displayed to the end user. Defaults to `"Update available"`.
|
||||
|
||||
* __rollbackRetryOptions__ *(RollbackRetryOptions)* - An "options" object used to determine whether a rollback retry mechanism should be enabled, and if so, what settings to use. Defaults to `null`, which has the effect of disabling the retry mechanism completely. Setting this to any truthy value will enable the retry mechanism with the default settings, and passing an object to this parameter allows enabling the retry mechanism as well as overriding one or more of the default values. The rollback retry mechanism allows the application to attempt to reinstall an update that was previously rolled back (with the restrictions specified in the options).
|
||||
|
||||
The following list represents the available options and their defaults:
|
||||
|
||||
* __delayInHours__ *(Number)* - Specifies the minimum time in hours that the app will wait after the latest rollback before attempting to reinstall the same rolled-back package. Defaults to `24`.
|
||||
|
||||
* __maxRetryAttempts__ *(Number)* - Specifies the maximum number of retry attempts that the app can make before it stops trying. Cannot be less than `1`. Defaults to `1`.
|
||||
|
||||
##### codePushStatusDidChange (event hook)
|
||||
|
||||
Called when the sync process moves from one stage to another in the overall update process. The event hook is called with a status code which represents the current state, and can be any of the [`SyncStatus`](#syncstatus) values.
|
||||
|
|
|
@ -62,6 +62,25 @@
|
|||
*/
|
||||
+ (BOOL)isFailedHash:(NSString*)packageHash;
|
||||
|
||||
|
||||
/*
|
||||
* This method is used to get information about the latest rollback.
|
||||
* This information will be used to decide whether the application
|
||||
* should ignore the update or not.
|
||||
*/
|
||||
+ (NSDictionary*)getRollbackInfo;
|
||||
/*
|
||||
* This method is used to save information about the latest rollback.
|
||||
* This information will be used to decide whether the application
|
||||
* should ignore the update or not.
|
||||
*/
|
||||
+ (void)setLatestRollbackInfo:(NSString*)packageHash;
|
||||
/*
|
||||
* This method is used to get the count of rollback for the package
|
||||
* using the latest rollback information.
|
||||
*/
|
||||
+ (int)getRollbackCountForPackage:(NSString*) packageHash fromLatestRollbackInfo:(NSMutableDictionary*) latestRollbackInfo;
|
||||
|
||||
/*
|
||||
* This method checks to see whether a specific package hash
|
||||
* represents a downloaded and installed update, that hasn't
|
||||
|
|
|
@ -73,6 +73,12 @@ static NSString *bundleResourceExtension = @"jsbundle";
|
|||
static NSString *bundleResourceName = @"main";
|
||||
static NSString *bundleResourceSubdirectory = nil;
|
||||
|
||||
// These keys represent the names we use to store information about the latest rollback
|
||||
static NSString *const LatestRollbackInfoKey = @"LATEST_ROLLBACK_INFO";
|
||||
static NSString *const LatestRollbackPackageHashKey = @"packageHash";
|
||||
static NSString *const LatestRollbackTimeKey = @"time";
|
||||
static NSString *const LatestRollbackCountKey = @"count";
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[super initialize];
|
||||
|
@ -403,6 +409,64 @@ static NSString *bundleResourceSubdirectory = nil;
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is used to get information about the latest rollback.
|
||||
* This information will be used to decide whether the application
|
||||
* should ignore the update or not.
|
||||
*/
|
||||
+ (NSDictionary *)getLatestRollbackInfo
|
||||
{
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
NSDictionary *latestRollbackInfo = [preferences objectForKey:LatestRollbackInfoKey];
|
||||
return latestRollbackInfo;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is used to save information about the latest rollback.
|
||||
* This information will be used to decide whether the application
|
||||
* should ignore the update or not.
|
||||
*/
|
||||
+ (void)setLatestRollbackInfo:(NSString*)packageHash
|
||||
{
|
||||
if (packageHash == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
NSMutableDictionary *latestRollbackInfo = [preferences objectForKey:LatestRollbackInfoKey];
|
||||
if (latestRollbackInfo == nil) {
|
||||
latestRollbackInfo = [[NSMutableDictionary alloc] init];
|
||||
} else {
|
||||
latestRollbackInfo = [latestRollbackInfo mutableCopy];
|
||||
}
|
||||
|
||||
int initialRollbackCount = [self getRollbackCountForPackage: packageHash fromLatestRollbackInfo: latestRollbackInfo];
|
||||
NSNumber *count = [NSNumber numberWithInt: initialRollbackCount + 1];
|
||||
NSNumber *currentTimeMillis = [NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970] * 1000];
|
||||
|
||||
[latestRollbackInfo setValue:count forKey:LatestRollbackCountKey];
|
||||
[latestRollbackInfo setValue:currentTimeMillis forKey:LatestRollbackTimeKey];
|
||||
[latestRollbackInfo setValue:packageHash forKey:LatestRollbackPackageHashKey];
|
||||
|
||||
[preferences setObject:latestRollbackInfo forKey:LatestRollbackInfoKey];
|
||||
[preferences synchronize];
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is used to get the count of rollback for the package
|
||||
* using the latest rollback information.
|
||||
*/
|
||||
+ (int)getRollbackCountForPackage:(NSString*) packageHash fromLatestRollbackInfo:(NSMutableDictionary*) latestRollbackInfo
|
||||
{
|
||||
NSString *oldPackageHash = [latestRollbackInfo objectForKey:LatestRollbackPackageHashKey];
|
||||
if ([packageHash isEqualToString: oldPackageHash]) {
|
||||
NSNumber *oldCount = [latestRollbackInfo objectForKey:LatestRollbackCountKey];
|
||||
return [oldCount intValue];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This method checks to see whether a specific package hash
|
||||
* has previously failed installation.
|
||||
|
@ -508,6 +572,10 @@ static NSString *bundleResourceSubdirectory = nil;
|
|||
*/
|
||||
- (void)saveFailedUpdate:(NSDictionary *)failedPackage
|
||||
{
|
||||
if ([[self class] isFailedHash:[failedPackage objectForKey:PackageHashKey]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey];
|
||||
if (failedUpdates == nil) {
|
||||
|
@ -822,6 +890,21 @@ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash
|
|||
resolve(@(isFailedHash));
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setLatestRollbackInfo:(NSString *)packageHash
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
[[self class] setLatestRollbackInfo:packageHash];
|
||||
}
|
||||
|
||||
|
||||
RCT_EXPORT_METHOD(getLatestRollbackInfo:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSDictionary *latestRollbackInfo = [[self class] getLatestRollbackInfo];
|
||||
resolve(latestRollbackInfo);
|
||||
}
|
||||
|
||||
/*
|
||||
* This method isn't publicly exposed via the "react-native-code-push"
|
||||
* module, and is only used internally to populate the LocalPackage.isFirstRun property.
|
||||
|
|
|
@ -135,6 +135,15 @@ export interface SyncOptions {
|
|||
* overriding one or more of the default strings.
|
||||
*/
|
||||
updateDialog?: UpdateDialog;
|
||||
|
||||
/**
|
||||
* An "options" object used to determine whether a rollback retry mechanism should be enabled, and if so, what settings to use.
|
||||
* Defaults to `null`, which has the effect of disabling the retry mechanism completely. Setting this to any truthy value will enable
|
||||
* the retry mechanism with the default settings, and passing an object to this parameter allows enabling the retry mechanism as well
|
||||
* as overriding one or more of the default values. The rollback retry mechanism allows the application to attempt to reinstall
|
||||
* an update that was previously rolled back (with the restrictions specified in the options).
|
||||
*/
|
||||
rollbackRetryOptions?: RollbackRetryOptions;
|
||||
}
|
||||
|
||||
export interface UpdateDialog {
|
||||
|
@ -182,6 +191,20 @@ export interface UpdateDialog {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
export interface RollbackRetryOptions {
|
||||
/**
|
||||
* Specifies the minimum time in hours that the app will wait after the latest rollback
|
||||
* before attempting to reinstall same rolled-back package. Defaults to `24`.
|
||||
*/
|
||||
delayInHours?: number;
|
||||
|
||||
/**
|
||||
* Specifies the maximum number of retry attempts that the app can make before it stops trying.
|
||||
* Cannot be less than `1`. Defaults to `1`.
|
||||
*/
|
||||
maxRetryAttempts?: number;
|
||||
}
|
||||
|
||||
export interface StatusReport {
|
||||
/**
|
||||
* Whether the deployment succeeded or failed.
|
||||
|
|
Загрузка…
Ссылка в новой задаче