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:
Post a Comment