+ All Categories
Home > Technology > Paris Tech Meetup talk : Troubles start at version 1.0

Paris Tech Meetup talk : Troubles start at version 1.0

Date post: 06-May-2015
Category:
Upload: laurent-cerveau
View: 450 times
Download: 5 times
Share this document with a friend
Description:
Presentation given at the Paris Tech Meetup 2013. A checklist (with code example for iOS development) to anticipate potential problems that can happen during the upgrade process of an application.
25
Troubles start at version 1 Laurent Cerveau [email protected]
Transcript

Before version 1.0

“You’ll never gonna die you gonna make it if you try they gonna love you”

Focus is naturally on this first version, get feature working and debugged

After version 1.0

“And in the end the love you take is equal to the love you make”

Some future changes may break what is already exist and _really_ annoy users

So the topic is...

• I am shipping a new version of a client , and its internal have changed.• I am deploying new server code I do not want to have application crash or behave weirdly• I need to ship a new client... in preparation for future server changes• I would like that users upgrade to a new version• I need to force users to upgrade to the latest version

Make an often heard question more useful

“Which version is it?”

Configuration

Code snippets and practice advices to manage evolution of versions on client and server

Client Native application

Server

API, push notification

1.0 1.0.1 1.1 2.0

v1 v2 v3 v4

2 main topics

• How to overuse versioning capabilities

• Structure tips for safe client server exchange

No complicated stuff, many little things to ease future developments

It’s all about versioning

Apple system

• Avoid going out of those conventions (1.2b)

CFBundleVersion “Int” e.g. 34 Development

CFBundleShortVersionString

“String” e.g.1.2.3 Marketing

• Apple environment provides 2 versions

• Use agvtool to modify them (in a build system)agvtool bump -all

agvtool new-marketing-version “1.0.1”

(For each build that is distributed)

• Here, each component stays below 10

In Code• Set up the XCode project

• Translate marketing version easily in integer for easy manipulation

int MMUtilityConvertMarketingVersionTo3Digit(NSString *version){ NSMutableArray *componentArray = [NSMutableArray arrayWithArray:[version componentsSeparatedByString:@"."]]; for(NSUInteger idx = [componentArray count]; idx < 3; idx++) { [componentArray addObject:@0]; } __block int result = 0; [componentArray enumerateObjectsUsingBlock:^(NSString *aComponent, NSUInteger idx, BOOL *stop) { result = 10*result+[aComponent intValue]; }]; return result;}

Centralized managementMMEnvironmentMonitor

singleton or on the application delegate, created early

@property @methodsfirstTime, firstTimeForCurrentVersion, previousVersion

applicationVariant

developmentStage

Tutorial of the app, present new features

When you have a lite and a full version

alpha, beta, final

-(void) runUpgradeScenario

-(void) detectBoundariesVersions(EndBlock)

-(BOOL) testRunability

Set up defaults, upgrade internal data structure

Time limit for beta, warn if wrong OS version

Read those parameters from the server

Actions• Detect first launch(es)

_current3DigitVersion = MMUtilityConvertMarketingVersionTo3Digit([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]);

id tmpObj = [[NSUserDefaults standardUserDefaults] objectForKey:kNEPVersionRunDefaultsKey];_firstTime = (nil == tmpObj);_firstTimeForCurrentVersion = (nil == [tmpObj objectForKey:[NSString stringWithFormat:@"%d",_current3DigitVersion]]);if(_firstTimeForCurrentVersion) { _previous3DigitVersion = [[[tmpObj keysSortedByValueUsingSelector:@selector(compare:)] lastObject] intValue]; [[NSUserDefaults standardUserDefaults] setObject:@{[NSString stringWithFormat:@"%d",_current3DigitVersion]:@YES} forKey:kNEPVersionRunDefaultsKey]; [[NSUserDefaults standardUserDefaults] synchronize];} else { _previous3DigitVersion = _current3DigitVersion;}

• Run Upgrade scenarios : convention naming/* Have all start and upgrade method named with the same scheme */- (void) _startAt100;

- (void) _startAt101;

- (void) _upgradeFrom100To101;

- (void) _upgradeFrom102To110;

/* Use the Objective-C runtime */- (BOOL) runUpgradeScenario{#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks" __block BOOL result = NO;

if(NO == self.firstTimeForCurrentVersion && NO == self.firstTime) return result;

}

• Run Upgrade scenarios : apply the upgrade or start

NSMutableDictionary *allUpgrades= [NSMutableDictionary dictionary]; NSMutableDictionary *allStarts= [NSMutableDictionary dictionary]; //Find all upgrade methods unsigned int outCount; Method * allMethods = class_copyMethodList([self class], &outCount); for(unsigned int idx = 0; idx < outCount; idx++) { Method aMethod = allMethods[idx]; NSString *aMethodName = NSStringFromSelector(method_getName(aMethod)); if([aMethodName hasPrefix:@"_upgradeFrom"]) { NSString *upgradeVersionString = [aMethodName substringWithRange:NSMakeRange([@"_upgradeFrom" length], 3)]; [allUpgrades setObject:aMethodName forKey:upgradeVersionString]; } else if ([aMethodName hasPrefix:@"_startAt"]) { NSString *startVersionString = [aMethodName substringWithRange:NSMakeRange([@"_startAt" length], 3)]; [allStarts setObject:aMethodName forKey:startVersionString]; } } if(allMethods) free(allMethods);

if(self.firstTime) { //sort them and perform the most "recent" one SEL startSelector = NSSelectorFromString([allStarts[[[allStarts keysSortedByValueUsingSelector:@selector(compare:)]lastObject]]]); [self performSelector:startSelector withObject:nil]; result = YES; } else if(self.firstTimeForCurrentVersion) { //Sort them and apply the one that needs to be applied [[allUpgrades keysSortedByValueUsingSelector:@selector(compare:)] enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) { if([obj intValue] > _previous3DigitVersion) { result = YES; [self performSelector:NSSelectorFromString([allUpgrades objectForKey:obj]) withObject:nil]; } }]; } #pragma clang diagnostic pop return result;

Runability

• Beta lock : generate a .m file with limit date at each build. Put it in a Run script phase

tmp_path = os.path.join(os.getcwd(),'Sources/frontend/NEPDevelopmentStage.m')tmp_now = time.strftime('%Y-%m-%d %X +0000', time.localtime(time.time()+24*3600*20))f.write('NSString *const kMMLimitTimeForNonFinalVersion =@"')f.write(tmp_now)f.write('";')f.close()

• Be nice and say goodbye

Server can help• Have a server call returns information

{ last_version_in_apple_store:”1.2.3”, minimal_client_version:”1.0.1”}

Run limited

2 Small server/client tips

• It is always good to deal with HTTP 503 (service unavailable) instead of nothing happening. Useful for big (or non mastered) changes

• Client limitation can be done with extra HTTP header or user agent change (be cautious) and send back 403

/* Change user agent */userAgent = [NSString stringWithFormat:@"MyApp/%@ iOS CFNetwork/%@ Darwin/%s", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], cfNetworkVersion, kernelVersion];[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];

Data structure

• Whatever I send you, you should not crash on data (Obj-C nil insertion, receiving HTML instead of JSON...)• The user is not aware of what is not visible - spread the changes• The user may forgive missing data (if she/he has been prepared)• The user should always find back its environment

Rules of thumb

ConsequencesSelf-describing uuid

{ __class_name:person, uuid:person_123456, firstname:laurent, lastname:cerveau, age:...}

{ __class_name:person, uuid:person_123456, data_version:1, object_version:2 firstname:laurent...}

or

The topic of uuid• use directly “id” of DB table

unique only in one table, dangerous in case of DB technology change

• use full uuidgen “B238BC15-DF27-4538-9FDA-2F972FE24B59”

• use something helpful in debugging“video_12345”

• use something with a meaning (FQDN)“video.tv_episode.12345”

NB: server may forget UUID in case of non persistent data

Base object@interface MMBaseObject : NSObject{ NSString *_uuid; int _objectVersion; int _dataVersion; MMObjectType _type;}

• Every object created with server data derives from such• Parsing of data instantiates all objects, ...if understood• Each object is responsible to be defensive in its parsing• Base object class methods allows creation of a factory• Use of a enum-based type can be convenient

Step 1 Registration• Each subclass register itself at load time

/* Register towards to the base class */+ (void)load{ [MMBaseObject registerClass:NSStringFromClass([self class]) forType:kMMObjectTypePerson JSONClassName:@"person"];}

/* Class registration: to be called by subclasses */+ (void) registerClass:(NSString *)className forType:(MMObjectType)type JSONClassName:(NSString *)jsonClassName{ if(nil == _allObjectClasses) _allObjectClasses = [[NSMutableDictionary alloc] init]; if(nil == _jsonClassToObjectClass) _jsonClassToObjectClass = [[NSMutableDictionary alloc] init];

@autoreleasepool { [_allObjectClasses setObject:[NSNumber numberWithUnsignedInteger:type] forKey:className]; [_jsonClassToObjectClass setObject:className forKey:jsonClassName]; }}

• Registration maintains the mapping

• Class method on the Base class allows to retrieve class from JSON class name and so on...

Step 2 Parsing• Starts on the base class

/* Entry point for JSON parsing and MMObject instantiations */+ (void) createMMObjectsFromJSONResult:(id)jsonResult{ _ParseAPIObjectWithExecutionBlock(jsonResult); return ;}

/* Transform a Cocoa object JSON inspired representation into a real object */void _ParseAPIObjectWithExecutionBlock(id inputObj) { if([inputObj isKindOfClass:[NSArray class]]) { [inputObj enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { _ParseAPIObjectWithExecutionBlock(obj); }]; } else if([inputObj isKindOfClass:[NSDictionary class]]) { NSDictionary *tmpDictionary = (NSDictionary *)inputObj; NSString *objectAPIType = tmpDictionary[@"__class_name"]; NSString *objectUUID = tmpDictionary[@"uuid"] ;

if(objectUUID) { MMBaseObject *tmpObject = [_dataStore objectWithUUID :objectUUID]; if(tmpObject) { [tmpObject updateWithJSONContent:tmpDictionary]; } else { if(nil == objectAPIType) return; NSString *objectClass = [BOXBaseObject classNameForStringAPIType:objectAPIType]; if(nil == objectClass) return result; tmpObject = [[NSClassFromString(objectClass) alloc] initWithJSONContent:tmpDictionary]; [_dataStore addObject:tmpObject replace:NO]; } [tmpDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]) { _ParseAPIObjectWithExecutionBlock(obj,provider, task, block, objectUUID, key); } }]; } }}

• And inside calls a recursive function

Step 3 Object creation• Base class does the basics

/* Designated initializer. Possible Variation:if uuid is nil one can be generated */- (id)initWithUUID:(NSString *)uuid{ self = [super init]; if(self) { NSString *tmpClassString = NSStringFromClass([self class]); self.uuid = uuid; self.type = [_allObjectClasses[tmpClassString] unsignedIntegerValue]; } return self;}

/* JSON object initialization : first time */- (id)initWithJSONContent:(NSDictionary *)contentObject{ self = [super initWithUUID:contentObject[@"uuid"]];! if (self != nil) {! ! [self updateWithJSONContent:contentObject]; }! return self;}

• And subclass are defensive/* JSON update */- (void)updateWithJSONContent:(NSDictionary *)contentObject{ if([contentObject[@”object_version”] intValue] > _objectVersion) { id tmpObj = JSONContent[@"title"]; if(tmpObj && [tmpObj isKindOfClass:[NSString class]]) { self.title = tmpObj; } }}

Down the road• Deal with a field based API (incomplete download)• Deal with sliced data • Gather all objects created in a HTTP call• Handle relationship between objects• Apply in an external framework (e.g. RestKit...)

Thank You!


Recommended