RavenDB implementation frustrations

time to read 13 min | 2572 words

While working on the RavenDB server is usually a lot of fun, there is a part of RavenDB client that I absolutely abhor. Every time that I need to touch the part of the client API that talks to the server, it is a pain. Why is that?

Let us take a the simple example, loading a document by id. On the wire, it looks like this:

GET /docs/users/ayende

It can’t be simpler than that, except that internally in RavenDB, we have three implementation for that:

  • Standard sync impl
  • Standard async impl
  • Silverlight async impl

The problem is that each of those uses different API, and while we created a shared abstraction for async / sync, at least, it is a hard task making sure that they are all in sync with one another if we need to make a modification.

For example, let us look at the three implementation of Get:

public JsonDocument DirectGet(string serverUrl, string key)
{
    var metadata = new RavenJObject();
    AddTransactionInformation(metadata);
    var request = jsonRequestFactory.CreateHttpJsonRequest(this, serverUrl + "/docs/" + key, "GET", metadata, credentials, convention);
    request.AddOperationHeaders(OperationsHeaders);
    try
    {
        var requestString = request.ReadResponseString();
        RavenJObject meta = null;
        RavenJObject jsonData = null;
        try
        {
            jsonData = RavenJObject.Parse(requestString);
            meta = request.ResponseHeaders.FilterHeaders(isServerDocument: false);
        }
        catch (JsonReaderException jre)
        {
            var headers = "";
            foreach (string header in request.ResponseHeaders)
            {
                headers = headers + string.Format("\n\r{0}:{1}", header, request.ResponseHeaders[header]);
            }
            throw new JsonReaderException("Invalid Json Response: \n\rHeaders:\n\r" + headers + "\n\rBody:" + requestString, jre);
        }
        return new JsonDocument
        {
            DataAsJson = jsonData,
            NonAuthoritiveInformation = request.ResponseStatusCode == HttpStatusCode.NonAuthoritativeInformation,
            Key = key,
            Etag = new Guid(request.ResponseHeaders["ETag"]),
            LastModified = DateTime.ParseExact(request.ResponseHeaders["Last-Modified"], "r", CultureInfo.InvariantCulture).ToLocalTime(),
            Metadata = meta
        };
    }
    catch (WebException e)
    {
        var httpWebResponse = e.Response as HttpWebResponse;
        if (httpWebResponse == null)
            throw;
        if (httpWebResponse.StatusCode == HttpStatusCode.NotFound)
            return null;
        if (httpWebResponse.StatusCode == HttpStatusCode.Conflict)
        {
            var conflicts = new StreamReader(httpWebResponse.GetResponseStreamWithHttpDecompression());
            var conflictsDoc = RavenJObject.Load(new JsonTextReader(conflicts));
            var conflictIds = conflictsDoc.Value<RavenJArray>("Conflicts").Select(x => x.Value<string>()).ToArray();

            throw new ConflictException("Conflict detected on " + key +
                                        ", conflict must be resolved before the document will be accessible")
            {
                ConflictedVersionIds = conflictIds
            };
        }
        throw;
    }
}
 

This is the sync API, of course, next we will look at the same method, for the full .NET framework TPL:

public Task<JsonDocument> GetAsync(string key)
{
    EnsureIsNotNullOrEmpty(key, "key");

    var metadata = new RavenJObject();
    AddTransactionInformation(metadata);
    var request = jsonRequestFactory.CreateHttpJsonRequest(this, url + "/docs/" + key, "GET", metadata, credentials, convention);

    return Task.Factory.FromAsync<string>(request.BeginReadResponseString, request.EndReadResponseString, null)
        .ContinueWith(task =>
        {
            try
            {
                var responseString = task.Result;
                return new JsonDocument
                {
                    DataAsJson = RavenJObject.Parse(responseString),
                    NonAuthoritiveInformation = request.ResponseStatusCode == HttpStatusCode.NonAuthoritativeInformation,
                    Key = key,
                    LastModified = DateTime.ParseExact(request.ResponseHeaders["Last-Modified"], "r", CultureInfo.InvariantCulture).ToLocalTime(),
                    Etag = new Guid(request.ResponseHeaders["ETag"]),
                    Metadata = request.ResponseHeaders.FilterHeaders(isServerDocument: false)
                };
            }
            catch (WebException e)
            {
                var httpWebResponse = e.Response as HttpWebResponse;
                if (httpWebResponse == null)
                    throw;
                if (httpWebResponse.StatusCode == HttpStatusCode.NotFound)
                    return null;
                if (httpWebResponse.StatusCode == HttpStatusCode.Conflict)
                {
                    var conflicts = new StreamReader(httpWebResponse.GetResponseStreamWithHttpDecompression());
                    var conflictsDoc = RavenJObject.Load(new JsonTextReader(conflicts));
                    var conflictIds = conflictsDoc.Value<RavenJArray>("Conflicts").Select(x => x.Value<string>()).ToArray();

                    throw new ConflictException("Conflict detected on " + key +
                                                ", conflict must be resolved before the document will be accessible")
                    {
                        ConflictedVersionIds = conflictIds
                    };
                }
                throw;
            }
        });
}

And here is the Siliverlight version:

public Task<JsonDocument> GetAsync(string key)
{
    EnsureIsNotNullOrEmpty(key, "key");

    key = key.Replace("\\",@"/"); //NOTE: the present of \ causes the SL networking stack to barf, even though the Uri seemingly makes this translation itself

    var request = url.Docs(key)
        .ToJsonRequest(this, credentials, convention);

    return request
        .ReadResponseStringAsync()
        .ContinueWith(task =>
        {
            try
            {
                var responseString = task.Result;
                return new JsonDocument
                {
                    DataAsJson = RavenJObject.Parse(responseString),
                    NonAuthoritiveInformation = request.ResponseStatusCode == HttpStatusCode.NonAuthoritativeInformation,
                    Key = key,
                    LastModified = DateTime.ParseExact(request.ResponseHeaders["Last-Modified"].First(), "r", CultureInfo.InvariantCulture).ToLocalTime(),
                    Etag = new Guid(request.ResponseHeaders["ETag"].First()),
                    Metadata = request.ResponseHeaders.FilterHeaders(isServerDocument: false)
                };
            }
            catch (AggregateException e)
            {
                var webException = e.ExtractSingleInnerException() as WebException;
                if (webException != null)
                {
                    if (HandleWebExceptionForGetAsync(key, webException))
                        return null;
                }
                throw;
            }
            catch (WebException e)
            {
                if (HandleWebExceptionForGetAsync(key, e))
                    return null;
                throw;
            }
        });
}

Did I mention it is annoying?

All of those methods are doing the exact same thing, but I have to maintain 3 versions of them. I thought about dropping the sync version (it is easy to do sync on top of async), which would mean that I would have only 2 implementations, but I don’t like it. The error handling and debugging support for async is still way below what you can get for sync code.

I don’t know if there is a solution for this, I just know that this is a big pain point for me.