Saturday, May 28, 2011

RavenDB: Conflict resolution when loading documents

The fun with RavenDB continues - if that sounds sarcastic, it's not meant to be - I'm rather fond of Raven, it certainly performs well, and I still find myself waxing lyrical about the LINQ provider - it's really very nice indeed.

One thing I did have to do though was work out how to deal with conflicts, especially in the face of a replication setup. It's not that difficult to do, but I thought I'd share (I like my presumption that anyone is actually going to read this!).

I have a wrapper class around Raven (I get bored typing RavenDB, so Raven it is), that provides some useful behaviour around core Raven features. One such wrapper feature is the 'recoverable' load - simply put, when a document is loaded from Raven and a conflict is detected, an attempt will be made to resolve the conflict.

The base implementation is as below - we use a Func<> to load an object, and if a Raven conflict exception occurs, we attempt to resolve the conflict, and return an object.

1:  public T RecoverableLoad<T>(string id, Func<RavenSessionWrapper, string, T> loader) where T : RavenBase {  
2:    DBC.AssertNotNull(loader, "Cannot exeucte a recoverable load with a null loader");  
3:    T result;  
4:    try {  
5:      LogFacade.LogInfo(this, string.Format("Recoverable load attempt for {0}", id));  
6:      result = loader(this, id);  
7:    }  
8:    catch (ConflictException ex) {  
9:      result = ResolveConflict(ex, id, loader);  
10:      DBC.AssertNotNull(result, "Null document even after conflict resolution tried");  
11:    }  
12:    return result;  
13:  }  
14:  private T ResolveConflict<T>(ConflictException ex, string id, Func<RavenSessionWrapper, string, T> loader) {  
15:    return ResolveConflict(ex.ConflictedVersionIds, id, loader);  
16:  }  

Lines 14-16 are just a convenience method to extract conflict ids from the actual conflict exception.

The actual conflict resolution behaviour is shown next:

1:  private T ResolveConflict<T>(IEnumerable<string> conflictIDs, string id, Func<RavenSessionWrapper, string, T> loader) {  
2:    LogFacade.LogWarning(this,   
3:     string.Format("Forced to resolve conflict for {0}. Conflict IDs:{1}{2}", id, Environment.NewLine, string.Join(Environment.NewLine, conflictIDs.ToArray())));  
4:    List<JsonDocument> conflicts =   
5:     conflictIDs.Select(s => RavenStore.DatabaseCommands.Get(s)).ToList();  
6:    Func<JsonDocument, DateTime> f = d =>  
7:     d.DataAsJson[TimestampProperty] == null ?   
8:      DateTime.MinValue :   
9:      JsonConvert.DeserializeObject<DateTime>(d.DataAsJson[TimestampProperty].ToString());  
10:    JsonDocument choice =   
11:       conflicts.Aggregate((curMax, x) => curMax == null || f(x) > f(curMax) ? x : curMax);  
12:    RavenStore.DatabaseCommands.Put(id, null, choice.DataAsJson, choice.Metadata);  
13:    LogFacade.LogWarning(this, string.Format("Resolved conflict for {0}, Marker: {1}", id, f(choice)));  
14:    return loader(this, id);  
15:  }  

So, what do we actually do:

  • Get a list of JSON documents that correspond to the conflict ids, using the base Raven'Get'  database command  (lines 4-5)
  • Define a Func<> that gets a timestamp property from the raw JSON document or provides a substitute value if non existent (lines 6-9)
  • Find the JSON document with the latest timestamp, via a peculiar use of the Aggregate LINQ method (lines 10-11)
  • Write the document back to Raven as definitive (line 12)
  • Now, load the document (line 14)
Sure, it is not that pretty, and it could do with some optimisation, but it works. The only implementation issue is really that a specific property is expected to exist in the JSON document. However, in my scenario, I have complete control over the construction of Raven documents, so there was no real need to make it more general.

No comments: