Permalink
Browse files

Detect errors in TouchDB replications

* Check nonstandard "error" property in _active_tasks entry to get replication error from TouchDB.
* Listen for TouchDB replication-stopped notification and re-poll _active_tasks in response.

Change-Id: I8dfbf8980cb3d1f6596dbd02f34d77cd418ed865
  • Loading branch information...
1 parent b3e01c7 commit 45d66634222215cd487e9537f920fc46ee40b0bf @snej snej committed Jan 28, 2012
Showing with 62 additions and 17 deletions.
  1. +1 −1 Couch/CouchQuery.m
  2. +29 −5 Couch/CouchReplication.m
  3. +10 −0 Couch/CouchTouchDBServer.m
  4. +3 −0 REST/RESTInternal.h
  5. +19 −11 REST/RESTOperation.m
View
2 Couch/CouchQuery.m
@@ -150,7 +150,7 @@ - (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)erro
result: result] autorelease];
} else {
Warn(@"Couldn't parse rows from CouchDB view response");
- error = [NSError errorWithDomain: CouchHTTPErrorDomain code: 500 userInfo:nil];
+ error = [RESTOperation errorWithHTTPStatus: 502 message: nil URL: self.URL];
}
}
return error;
View
34 Couch/CouchReplication.m
@@ -53,9 +53,10 @@ - (id) initWithDatabase: (CouchDatabase*)database
- (void)dealloc {
- [self stopped];
+ COUCHLOG2(@"%@: dealloc", self);
[_remote release];
[_database release];
+ [_status release];
[_error release];
[_filter release];
[_filterParams release];
@@ -125,6 +126,7 @@ - (RESTOperation*) start {
if (_taskID) {
// Successfully started:
COUCHLOG(@"%@: task ID = '%@'", self, _taskID);
+ [self retain]; // so I don't go away while active; see [self release] in -stopped
[_database.server registerActiveTask: [NSDictionary dictionaryWithObjectsAndKeys:
@"Replication", @"type",
_taskID, @"task", nil]];
@@ -134,8 +136,7 @@ - (RESTOperation*) start {
// Huh, something's wrong.
Warn(@"%@ couldn't find _local_id in response: %@", self, response);
self.running = NO;
- self.error = [NSError errorWithDomain: CouchHTTPErrorDomain
- code: 599 userInfo: nil]; // TODO: Real err
+ self.error = [RESTOperation errorWithHTTPStatus: 599 message: nil URL: _remote]; // TODO: Real err
}
}
}];
@@ -149,6 +150,7 @@ - (void) stopped {
[_taskID release];
_taskID = nil;
[_database.server removeObserver: self forKeyPath: @"activeTasks"];
+ [self autorelease]; // balances [self retain] when successfully started
}
self.running = NO;
}
@@ -175,6 +177,13 @@ - (void) setStatus: (NSString*)status {
[_status autorelease];
_status = [status copy];
+ if ([status isEqualToString: @"Stopped"]) {
+ // TouchDB only
+ COUCHLOG(@"%@: Status changed to 'Stopped'", self);
+ [self stopped];
+ return;
+ }
+
int completed = 0, total = 0;
if (status) {
// Current format of status is "Processed \d+ / \d+ changes".
@@ -207,13 +216,15 @@ - (void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object
// Server's activeTasks changed:
BOOL active = NO;
NSString* status = nil;
+ NSArray* error = nil;
for (NSDictionary* task in _database.server.activeTasks) {
if ([[task objectForKey:@"type"] isEqualToString:@"Replication"]) {
// Can't look up the task ID directly because it's part of a longer string like
// "`6390525ac52bd8b5437ab0a118993d0a+continuous`: ..."
if ([[task objectForKey: @"task"] rangeOfString: _taskID].length > 0) {
active = YES;
- status = [task objectForKey: @"status"];
+ status = $castIf(NSString, [task objectForKey: @"status"]);
+ error = $castIf(NSArray, [task objectForKey: @"error"]);
break;
}
}
@@ -222,7 +233,20 @@ - (void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object
if (!active) {
COUCHLOG(@"%@: No longer an active task", self);
[self stopped];
- } else if (!$equal(status, _status)) {
+ return;
+ }
+
+ // Interpret .error property. This is nonstandard; only TouchDB supports it.
+ if (error.count >= 1) {
+ COUCHLOG(@"%@: error %@", self, error);
+ int status = [$castIf(NSNumber, [error objectAtIndex: 0]) intValue];
+ NSString* message = nil;
+ if (error.count >= 2)
+ message = $castIf(NSString, [error objectAtIndex: 1]);
+ self.error = [RESTOperation errorWithHTTPStatus: status message: message URL: _remote];
+ }
+
+ if (!$equal(status, _status)) {
self.status = status;
}
}
View
10 Couch/CouchTouchDBServer.m
@@ -12,9 +12,11 @@
#if TARGET_OS_IPHONE
extern NSString* const TDReplicatorProgressChangedNotification;
+extern NSString* const TDReplicatorStoppedNotification;
#else
// Copied from TouchDB's TDReplicator.m.
static NSString* TDReplicatorProgressChangedNotification = @"TDReplicatorProgressChanged";
+static NSString* TDReplicatorStoppedNotification = @"TDReplicatorStopped";
#endif
@@ -89,6 +91,7 @@ - (id) initWithURL:(NSURL *)url {
- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
[_touchServer release];
[_error release];
[super dealloc];
@@ -112,10 +115,17 @@ - (void) setActivityPollInterval: (NSTimeInterval)interval {
selector: @selector(replicationProgressChanged:)
name: TDReplicatorProgressChangedNotification
object: nil];
+ [[NSNotificationCenter defaultCenter] addObserver: self
+ selector: @selector(replicationProgressChanged:)
+ name: TDReplicatorStoppedNotification
+ object: nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:TDReplicatorProgressChangedNotification
object:nil];
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:TDReplicatorStoppedNotification
+ object:nil];
}
_observing = observe;
}
View
3 REST/RESTInternal.h
@@ -36,6 +36,9 @@ static inline BOOL $equal(id a, id b) {return a==b || [a isEqual: b];}
@interface RESTOperation ()
++ (NSError*) errorWithHTTPStatus: (int)httpStatus
+ message: (NSString*)message
+ URL: (NSURL*)url;
@property (nonatomic, readonly) UInt8 retryCount;
@end
View
30 REST/RESTOperation.m
@@ -424,7 +424,7 @@ - (void)connection: (NSURLConnection*)connection didReceiveData: (NSData*)data {
- (void)connectionDidFinishLoading: (NSURLConnection*)connection {
- NSInteger httpStatus = [_response statusCode];
+ int httpStatus = (int) [_response statusCode];
if (gRESTLogLevel >= kRESTLogRequestURLs) {
NSLog(@"REST: << %ld for %@ %@ (%lu bytes)",
@@ -440,21 +440,29 @@ - (void)connectionDidFinishLoading: (NSURLConnection*)connection {
[self completedWithError: nil];
} else {
// Escalate HTTP error to a connection error:
- NSString* message = [NSHTTPURLResponse localizedStringForStatusCode:httpStatus];
- NSDictionary* info = [NSDictionary dictionaryWithObjectsAndKeys:
- message, NSLocalizedFailureReasonErrorKey,
- [NSString stringWithFormat: @"%i %@", httpStatus, message],
- NSLocalizedDescriptionKey,
- self.URL, NSURLErrorKey,
- nil];
- NSError* error = [NSError errorWithDomain: CouchHTTPErrorDomain
- code: httpStatus
- userInfo: info];
+ NSError* error = [[self class] errorWithHTTPStatus: httpStatus message: nil URL: self.URL];
[self completedWithError: error];
}
}
++ (NSError*) errorWithHTTPStatus: (int)httpStatus
+ message: (NSString*)message
+ URL: (NSURL*)url
+{
+ if (!message)
+ message = [NSHTTPURLResponse localizedStringForStatusCode:httpStatus];
+ NSDictionary* info = [NSDictionary dictionaryWithObjectsAndKeys:
+ message, NSLocalizedDescriptionKey,
+ message, NSLocalizedFailureReasonErrorKey,
+ url, NSURLErrorKey,
+ nil];
+ return [NSError errorWithDomain: CouchHTTPErrorDomain
+ code: httpStatus
+ userInfo: info];
+}
+
+
- (void)connection: (NSURLConnection*)connection didFailWithError: (NSError*)error {
[self completedWithError: error];
}

0 comments on commit 45d6663

Please sign in to comment.