Paris Tech Meetup talk : Troubles start at version 1.0


Published on

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.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

No Downloads
Total views
On SlideShare
From Embeds
Number of Embeds
Embeds 0
No embeds

No notes for slide

Paris Tech Meetup talk : Troubles start at version 1.0

  1. 1. Troubles start at version 1 Laurent Cerveau
  2. 2. 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
  3. 3. 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
  4. 4. 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?”
  5. 5. 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
  6. 6. 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
  7. 7. It’s all about versioning
  8. 8. Apple system •Avoid going out of those conventions (1.2b) CFBundleVersion “Int” e.g. 34 Development CFBundleShortVersion String “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
  9. 9. 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; }
  10. 10. Centralized management MMEnvironmentMonitor singleton or on the application delegate, created early @property @methods firstTime, 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
  11. 11. 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;
  12. 12. /* 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;
  13. 13. 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
  14. 14. 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
  15. 15. 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"];
  16. 16. Data structure
  17. 17. •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
  18. 18. Consequences Self-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
  19. 19. 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
  20. 20. 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
  21. 21. 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...
  22. 22. 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
  23. 23. 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; } } }
  24. 24. 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...)
  25. 25. ThankYou!