Snapshot "Read Your Writes" behavior changes since API 200

I have this test that fails when using API version 300 or greater. The default API version selected was 200 up until recently, so this is probably an old change that went unnoticed by me?

The test checks that a Snapshot read does not see the writes made by the transaction as long as it is not committed. I remember that things where changed in the past regarding this, but I thought that I already handled it. Maybe I missed something?

[Test]
public async Task Test_Read_Isolation_From_Writes()
{
	// By default:
	// - Regular reads see the writes made by the transaction itself, but not the writes made by other transactions that committed in between
	// - Snapshot reads never see the writes made since the transaction read version, including the writes made by the transaction itself

	//Fdb.Start(200); // <-- the test passes
	//Fdb.Start(300); // <-- the test fails

	using (var db = await OpenTestPartitionAsync())
	{
		var location = db.Partition.ByKey("test");
		await db.ClearRangeAsync(location, this.Cancellation);

		var A = location.Keys.Encode("A");
		var B = location.Keys.Encode("B");
		var C = location.Keys.Encode("C");
		var D = location.Keys.Encode("D");

		// Reads (before and after):
		// - A and B will use regular reads
		// - C and D will use snapshot reads
		// Writes:
		// - A and C will be modified by the transaction itself
		// - B and D will be modified by a different transaction

		await db.WriteAsync((tr) =>
		{
			tr.Set(A, Slice.FromString("a"));
			tr.Set(B, Slice.FromString("b"));
			tr.Set(C, Slice.FromString("c"));
			tr.Set(D, Slice.FromString("d"));
		}, this.Cancellation);

		Log("Initial db state:");
		await DumpSubspace(db, location);

		using (var tr = db.BeginTransaction(this.Cancellation))
		{
			// check initial state
			Assert.That((await tr.GetAsync(A)).ToStringUtf8(), Is.EqualTo("a"));
			Assert.That((await tr.GetAsync(B)).ToStringUtf8(), Is.EqualTo("b"));
			Assert.That((await tr.Snapshot.GetAsync(C)).ToStringUtf8(), Is.EqualTo("c"));
			Assert.That((await tr.Snapshot.GetAsync(D)).ToStringUtf8(), Is.EqualTo("d"));

			// mutate (not yet comitted)
			tr.Set(A, Slice.FromString("aa"));
			tr.Set(C, Slice.FromString("cc"));
			await db.WriteAsync((tr2) =>
			{ // have another transaction change B and D under our nose
				tr2.Set(B, Slice.FromString("bb"));
				tr2.Set(D, Slice.FromString("dd"));
			}, this.Cancellation);

			// check what the transaction sees
			Assert.That((await tr.GetAsync(A)).ToStringUtf8(), Is.EqualTo("aa"), "The transaction own writes should change the value of regular reads");
			Assert.That((await tr.GetAsync(B)).ToStringUtf8(), Is.EqualTo("b"), "Other transaction writes should not change the value of regular reads");
			//FAIL: test fails here because we read "CC" ??
			Assert.That((await tr.Snapshot.GetAsync(C)).ToStringUtf8(), Is.EqualTo("c"), "The transaction own writes should not change the value of snapshot reads");
			Assert.That((await tr.Snapshot.GetAsync(D)).ToStringUtf8(), Is.EqualTo("d"), "Other transaction writes should not change the value of snapshot reads");

			//note: committing here would conflict
		}
	}
}
  • With API version 200: PASS
  • With API version 300 or more: FAIL
=== FoundationDB.Client.Tests.TransactionFacts.Test_Read_Isolation_From_Writes() === 11:32:53.9300588
Initial db state:
Dumping content of subspace Subspace(<15>.<02>test<00>) :
- ("A",) = a
- ("B",) = b
- ("C",) = c
- ("D",) = d
> Found 4 values
Test 'FoundationDB.Client.Tests.TransactionFacts.Test_Read_Isolation_From_Writes' failed: 
  The transaction own writes should not change the value of snapshot reads
  Expected string length 1 but was 2. Strings differ at index 1.
  Expected: "c"
  But was:  "cc"
  ------------^
	TransactionFacts.cs(1309,0): at FoundationDB.Client.Tests.TransactionFacts.<Test_Read_Isolation_From_Writes>d__27.MoveNext()
	at NUnit.Framework.AsyncInvocationRegion.AsyncTaskInvocationRegion.WaitForPendingOperationsToComplete(Object invocationResult)
	at NUnit.Core.NUnitAsyncTestMethod.RunTestMethod()

This was changed in API version 300 so that snapshot reads do see prior writes in the same transaction. See:

It’s also worth noting that there is a transaction option, set_snapshot_ryw_disable, that allows one to use the old behavior on a transaction-by-transaction basis, if that’s useful.

Yes, I have updated the code to test for both the old and new behavior (by setting this option). I’ve never really relied on this behavior, so hopefully this should not break anything.

The new behavior is much better because it makes transactional functions composable, even if they happen to use relaxed concurrency. I don’t think the option should normally be used; it was intended primarly for code that had been written before the change (so that it wouldn’t be stuck at a low API version).

Does this solve the issue with the Directory Layer only able to create one folder per transaction due to snapshot reads? (or am I mis-remembering something?).

I believe that was fixed in 3.0.7 by the bug fixes related to atomic ops and snapshot reads.

Actually, it looks like while the fix to the directory layer depended on the 3.0.7 fix, according to the release notes it didn’t actually land until 4.0.1.