Implementing VersionStamps in bindings

Ok so this is a bit confusing. I’m calling the getVersionstamp method AFTER the commit has completed, but I’m getting an error Operation issued while a commit was outstanding which seems wrong to me: The commit was already completed, so it was not “while” and there was no “outstanding” commit? Maybe the wording of the error message is wrong?

[…] but you have to call the future before it is committed, and then that future will be ready only after the commit

I’m having some issues with this API, because it will seem a little bit weird - from a .NET coder’s point of view - when dealing with tasks, AND it does not appear to compose well with retry loops and multiple layers.

My test creates and commits the transaction manually, but typical application will never do that and go through one of the retry loops (the db.run(...) in Java):


Task<IActionReuslt> SomeControllerMethod(....)
{
  //... check args ...
  await db.WriteAsync((tr) =>
  {   
     // traditional write operations
     tr.ClearRange(....);
     tr.Set(..., ...);
     // new Versionstamp API
     tr.SetVersionstampedKey(MAKE_KEY_WITH_VERSION_STAMP(), ....);

  }, HttpContext.Cancel);
  //...
  return View(....);
}

obviously, the db code would be inside some Business Logic class, and not inlined in the controller!

If the code wanted to obtain the actual Versionstamp, as well as some other result extracted from the database, both at the same time, it will look very ugly:

//...
Task<Versionstamp> stampTask; // out of scope
Slice result = await db.ReadWriteAsync((tr) =>
{
     var val = await tr.Get('SOME_KEY', ....);

     tr.SetVersionstampedKey(MAKE_KEY_WITH_VERSION_STAMP(), ..);
     stampTask = tr.GetVersionstamp();

     return val;
}, cancel);
Versionstamp stamp = await stampTask; // need an extra await here!
//...
var data = DoSomethingWithIt(result, stamp);
return View(new SpomeViewModel { Data = data, ... });

Having to hoist a task outside the scope and do an additional await looks weird in moden .NET code.

The retry loop could return the Versionstamp task alongside the result like this, but again it does not look nice:

(Slice result, Task<Versionstamp> stampTask) = await db.ReadWriteAsync((tr) =>
{
     Slice val = await tr.Get('SOME_KEY', ....);

     tr.SetVersionstampedKey(MAKE_KEY_WITH_VERSION_STAMP(), ..);

     return (val, tr.GetVersionstamp());
}, cancel);

Versionstamp stamp = await stampTask; // still need an extra await here

I think what the user would expect to happen is that the retry loops returns the Versionstamp directly not a Future:

(Slice result, Versionstamp stamp) = await db.ReadWriteAsync((tr) =>
{
     Slice val = await tr.Get('SOME_KEY', ....);

     tr.SetVersionstampedKey(MAKE_KEY_WITH_VERSION_STAMP(), ..);

     return (val, tr.GetVersionstamp());
}, cancel);

The inner lambda would have signature Func<IFdbTransaction, Task<TResult, Task<VersionStamp>>>, which is a mouthfull, The retryloop method wants to return a Task<TResult, VersionStamp>, and not a Task<TResult, Task<Versionstamp>> so some generic magic needs to be done.

It may have some effects on the overall API because, in .NET, you cannot have overloads whose signature only differ by the return value. So it would not be easy to have an overload of ReadWriteAsync(...) that returns plain results, and another one which also return a tuple with the resolved timestamp. You’d probably have to change the method name, or add some arguments to disambiguate.

I could have a ReadWriteWithVersionStampAsync<TResult>() => Task<(TResult, VersionStamp>) overload, but then it may cause issue when composing multiple libraries:

Let’s say in an HTTP request controller, the outer scope starts a retry loop, and then pass along the transaction to some business logic, which then calls into other Layers (Document, Blob, Index, …). If one layer is refactored somehow, and starts using versionstamps. It would have an impact on the outerscope (in the HTTP controller code) because it needs to know to call GetVersionstamp, before the commit, and extract the value after it has suceeded, and outside the scope of the retry loop. How do I pass back the actual versionstamp into the original layer (which has long since returned and be garbage collected).

It looks like you need dedicated transactions that are fully handled by the Layer code, and will not be able to compose with other layer inside a single transaction ?