AnswerModifying execution approaches
In RavenDB, we had this piece of code:
internal T[] LoadInternal<T>(string[] ids, string[] includes) { if(ids.Length == 0) return new T[0]; IncrementRequestCount(); Debug.WriteLine(string.Format("Bulk loading ids [{0}] from {1}", string.Join(", ", ids), StoreIdentifier)); MultiLoadResult multiLoadResult; JsonDocument[] includeResults; JsonDocument[] results; #if !SILVERLIGHT var sp = Stopwatch.StartNew(); #else var startTime = DateTime.Now; #endif bool firstRequest = true; do { IDisposable disposable = null; if (firstRequest == false) // if this is a repeated request, we mustn't use the cached result, but have to re-query the server disposable = DatabaseCommands.DisableAllCaching(); using (disposable) multiLoadResult = DatabaseCommands.Get(ids, includes); firstRequest = false; includeResults = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Includes).ToArray(); results = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Results).ToArray(); } while ( AllowNonAuthoritiveInformation == false && results.Any(x => x.NonAuthoritiveInformation ?? false) && #if !SILVERLIGHT sp.Elapsed < NonAuthoritiveInformationTimeout #else (DateTime.Now - startTime) < NonAuthoritiveInformationTimeout #endif ); foreach (var include in includeResults) { TrackEntity<object>(include); } return results .Select(TrackEntity<T>) .ToArray(); }
And we needed to take this same piece of code and execute it in:
- Async fashion
- As part of a batch of queries (sending multiple requests to RavenDB in a single HTTP call).
Everything else is the same, but in each case the marked line is completely different.
I chose to address this by doing a Method Object refactoring. I create a new class, and moved all the local variables to fields, and moved each part of the method to its own method. I also explicitly gave up control on executing, deferring that to whoever it calling us. We ended up with this:
public class MultiLoadOperation { private static readonly Logger log = LogManager.GetCurrentClassLogger(); private readonly InMemoryDocumentSessionOperations sessionOperations; private readonly Func<IDisposable> disableAllCaching; private string[] ids; private string[] includes; bool firstRequest = true; IDisposable disposable = null; JsonDocument[] results; JsonDocument[] includeResults; #if !SILVERLIGHT private Stopwatch sp; #else private DateTime startTime; #endif public MultiLoadOperation(InMemoryDocumentSessionOperations sessionOperations, Func<IDisposable> disableAllCaching, string[] ids, string[] includes) { this.sessionOperations = sessionOperations; this.disableAllCaching = disableAllCaching; this.ids = ids; this.includes = includes; sessionOperations.IncrementRequestCount(); log.Debug("Bulk loading ids [{0}] from {1}", string.Join(", ", ids), sessionOperations.StoreIdentifier); #if !SILVERLIGHT sp = Stopwatch.StartNew(); #else startTime = DateTime.Now; #endif } public IDisposable EnterMultiLoadContext() { if (firstRequest == false) // if this is a repeated request, we mustn't use the cached result, but have to re-query the server disposable = disableAllCaching(); return disposable; } public bool SetResult(MultiLoadResult multiLoadResult) { firstRequest = false; includeResults = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Includes).ToArray(); results = SerializationHelper.RavenJObjectsToJsonDocuments(multiLoadResult.Results).ToArray(); return sessionOperations.AllowNonAuthoritiveInformation == false && results.Any(x => x.NonAuthoritiveInformation ?? false) && #if !SILVERLIGHT sp.Elapsed < sessionOperations.NonAuthoritiveInformationTimeout #else (DateTime.Now - startTime) < sessionOperations.NonAuthoritiveInformationTimeout #endif ; } public T[] Complete<T>() { foreach (var include in includeResults) { sessionOperations.TrackEntity<object>(include); } return results .Select(sessionOperations.TrackEntity<T>) .ToArray(); } }
Note that this class doesn’t contain two very important things:
- The actual call to the database, we gave up control on that.
- The execution order for the methods, we don’t control that either.
That was ugly, and I decided that since I have to write another implementation as well, I might as well do the right thing and have a shared implementation. The key was to extract everything away except for the call to get the actual value. So I did just that, and we got a new class, that does all of the functionality above, except control where the actual call to the server is made and how.
Now, for the sync version, we have this code:
internal T[] LoadInternal<T>(string[] ids, string[] includes) { if(ids.Length == 0) return new T[0]; var multiLoadOperation = new MultiLoadOperation(this, DatabaseCommands.DisableAllCaching, ids, includes); MultiLoadResult multiLoadResult; do { using(multiLoadOperation.EnterMultiLoadContext()) { multiLoadResult = DatabaseCommands.Get(ids, includes); } } while (multiLoadOperation.SetResult(multiLoadResult)); return multiLoadOperation.Complete<T>(); }
This isn’t the most trivial of methods, I’ll admit, but it is ever so much better than the alternative, especially since now the async version looks like:
/// <summary> /// Begins the async multi load operation /// </summary> public Task<T[]> LoadAsyncInternal<T>(string[] ids, string[] includes) { var multiLoadOperation = new MultiLoadOperation(this,AsyncDatabaseCommands.DisableAllCaching, ids, includes); return LoadAsyncInternal<T>(ids, includes, multiLoadOperation); } private Task<T[]> LoadAsyncInternal<T>(string[] ids, string[] includes, MultiLoadOperation multiLoadOperation) { using (multiLoadOperation.EnterMultiLoadContext()) { return AsyncDatabaseCommands.MultiGetAsync(ids, includes) .ContinueWith(t => { if (multiLoadOperation.SetResult(t.Result) == false) return Task.Factory.StartNew(() => multiLoadOperation.Complete<T>()); return LoadAsyncInternal<T>(ids, includes, multiLoadOperation); }) .Unwrap(); } }
Again, it isn’t trivial, but at least the core stuff, the actual logic that isn’t related to how we execute the code is shared.
More posts in "Answer" series:
- (22 Jan 2025) What does this code do?
- (05 Jan 2023) what does this code print?
- (15 Dec 2022) What does this code print?
- (07 Apr 2022) Why is this code broken?
- (20 Jan 2017) What does this code do?
- (16 Aug 2011) Modifying execution approaches
- (30 Apr 2011) Stopping the leaks
- (24 Dec 2010) This code should never hit production
- (21 Dec 2010) Your own ThreadLocal
- (11 Feb 2010) Debugging a resource leak
- (03 Sep 2009) The lazy loaded inheritance many to one association OR/M conundrum
- (04 Sep 2008) Don't stop with the first DSL abstraction
- (12 Jun 2008) How many tests?
Comments
Hm, the using statement in line
using (multiLoadOperation.EnterMultiLoadContext())
will be exited immediately after setting up the async call. Is this intended?
You should abstract the timing mechanism into its own class that way you don't have to pepper your entire codebase with silverlight #includes.
+1 to @Bryan's comment
Tobi, Yes, the affect is used only for starting the async op, not for the duration of it.
Bryan, Agreed, and that was done already
I do like this old-style-Ayende post!
Comment preview