Skip to content

Commit 6ae022a

Browse files
committed
Add the logic on transactions to generate custom placeholder stamps (using a random token)
- Each transaction generate a random token (and on each retry). - tr.CreateVersionStamp() can be used to get a stamp specific to this transaction
1 parent 5051b06 commit 6ae022a

File tree

5 files changed

+232
-15
lines changed

5 files changed

+232
-15
lines changed

FoundationDB.Client/FdbTransaction.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ public sealed partial class FdbTransaction : IFdbTransaction
9090
/// <summary>CancellationToken that should be used for all async operations executing inside this transaction</summary>
9191
private CancellationToken m_cancellation;
9292

93+
/// <summary>Random token (but constant per transaction retry) used to generate incomplete VersionStamps</summary>
94+
private ulong m_versionStampToken;
95+
9396
#endregion
9497

9598
#region Constructors...
@@ -297,6 +300,68 @@ public Task<VersionStamp> GetVersionStampAsync()
297300
return m_handler.GetVersionStampAsync(m_cancellation);
298301
}
299302

303+
private ulong GenerateNewVersionStampToken()
304+
{
305+
// We need to generate a 80-bits stamp, and also need to mark it as 'incomplete' by forcing the highest bit to 1.
306+
// Since this is supposed to be a version number with a ~1M tickrate per seconds, we will play it safe, and force the 8 highest bits to 1,
307+
// meaning that we only reduce the database potential lifetime but 1/256th, before getting into trouble.
308+
//
309+
// By doing some empirical testing, it also seems that the last 16 bits are a transction batch order which is usually a low number.
310+
// Again, we will force the 4 highest bit to 1 to reduce the change of collision with a complete version stamp.
311+
//
312+
// So the final token will look like: 'FF xx xx xx xx xx xx xx Fy yy', were 'x' is the random token, and 'y' will lowest 12 bits of the transaction retry count
313+
314+
var rnd = new Random(); //TODO: singleton? (need locking!!)
315+
ulong x;
316+
unsafe
317+
{
318+
double r = rnd.NextDouble();
319+
x = *(ulong*) &r;
320+
}
321+
x |= 0xFF00000000000000UL;
322+
323+
lock (this)
324+
{
325+
ulong token = m_versionStampToken;
326+
if (token == 0)
327+
{
328+
token = x;
329+
m_versionStampToken = x;
330+
}
331+
return token;
332+
}
333+
}
334+
335+
/// <summary>Return a place-holder 80-bit VersionStamp, whose value is not yet known, but will be filled by the database at commit time.</summary>
336+
/// <returns>This value can used to generate temporary keys or value, for use with the <see cref="FdbMutationType.VersionStampedKey"/> or <see cref="FdbMutationType.VersionStampedValue"/> mutations</returns>
337+
/// <remarks>
338+
/// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
339+
/// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
340+
/// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
341+
/// </remarks>
342+
[Pure]
343+
public VersionStamp CreateVersionStamp()
344+
{
345+
var token = m_versionStampToken;
346+
if (token == 0) token = GenerateNewVersionStampToken();
347+
return VersionStamp.Custom(token, (ushort) (m_context.Retries | 0xF000), incomplete: true);
348+
}
349+
350+
/// <summary>Return a place-holder 96-bit VersionStamp with an attached user version, whose value is not yet known, but will be filled by the database at commit time.</summary>
351+
/// <returns>This value can used to generate temporary keys or value, for use with the <see cref="FdbMutationType.VersionStampedKey"/> or <see cref="FdbMutationType.VersionStampedValue"/> mutations</returns>
352+
/// <remarks>
353+
/// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
354+
/// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
355+
/// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
356+
/// </remarks>
357+
public VersionStamp CreateVersionStamp(int userVersion)
358+
{
359+
var token = m_versionStampToken;
360+
if (token == 0) token = GenerateNewVersionStampToken();
361+
362+
return VersionStamp.Custom(token, (ushort) (m_context.Retries | 0xF000), userVersion, incomplete: true);
363+
}
364+
300365
#endregion
301366

302367
#region Get...
@@ -785,6 +850,11 @@ private void RestoreDefaultSettings()
785850
{
786851
this.Timeout = m_database.DefaultTimeout;
787852
}
853+
854+
// if we have used a random token for versionstamps, we need to clear it (and generate a new one)
855+
// => this ensure that if the error was due to a collision between the token and another part of the key,
856+
// a transaction retry will hopefully use a different token that does not collide.
857+
m_versionStampToken = 0;
788858
}
789859

790860
/// <summary>Reset the transaction to its initial state.</summary>

FoundationDB.Client/Filters/FdbTransactionFilter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ public virtual Task<VersionStamp> GetVersionStampAsync()
257257
return m_transaction.GetVersionStampAsync();
258258
}
259259

260+
public virtual VersionStamp CreateVersionStamp()
261+
{
262+
ThrowIfDisposed();
263+
return m_transaction.CreateVersionStamp();
264+
}
265+
266+
public virtual VersionStamp CreateVersionStamp(int userVersion)
267+
{
268+
ThrowIfDisposed();
269+
return m_transaction.CreateVersionStamp(userVersion);
270+
}
271+
260272
public virtual void SetReadVersion(long version)
261273
{
262274
ThrowIfDisposed();

FoundationDB.Client/IFdbTransaction.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ public interface IFdbTransaction : IFdbReadOnlyTransaction
120120
/// </remarks>
121121
Task<VersionStamp> GetVersionStampAsync();
122122

123+
/// <summary>Return a place-holder 80-bit VersionStamp, whose value is not yet known, but will be filled by the database at commit time.</summary>
124+
/// <returns>This value can used to generate temporary keys or value, for use with the <see cref="FdbMutationType.VersionStampedKey"/> or <see cref="FdbMutationType.VersionStampedValue"/> mutations</returns>
125+
/// <remarks>
126+
/// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
127+
/// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
128+
/// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
129+
/// </remarks>
130+
VersionStamp CreateVersionStamp();
131+
132+
/// <summary>Return a place-holder 96-bit VersionStamp with an attached user version, whose value is not yet known, but will be filled by the database at commit time.</summary>
133+
/// <returns>This value can used to generate temporary keys or value, for use with the <see cref="FdbMutationType.VersionStampedKey"/> or <see cref="FdbMutationType.VersionStampedValue"/> mutations</returns>
134+
/// <remarks>
135+
/// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
136+
/// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
137+
/// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
138+
/// </remarks>
139+
VersionStamp CreateVersionStamp(int userVersion);
140+
123141
/// <summary>
124142
/// Watch a key for any change in the database.
125143
/// </summary>

FoundationDB.Client/VersionStamp.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ public static VersionStamp Incomplete(ushort userVersion)
115115
return new VersionStamp(PLACEHOLDER_VERSION, PLACEHOLDER_ORDER, userVersion, FLAGS_IS_INCOMPLETE | FLAGS_HAS_VERSION);
116116
}
117117

118+
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
119+
internal static VersionStamp Custom(ulong version, ushort order, bool incomplete)
120+
{
121+
return new VersionStamp(version, order, NO_USER_VERSION, incomplete ? FLAGS_IS_INCOMPLETE : FLAGS_NONE);
122+
}
123+
124+
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
125+
internal static VersionStamp Custom(ulong version, ushort order, int userVersion, bool incomplete)
126+
{
127+
Contract.Between(userVersion, 0, 0xFFFF, nameof(userVersion), "Local version must fit in 16-bits.");
128+
return new VersionStamp(version, order, (ushort) userVersion, incomplete ? (ushort) (FLAGS_IS_INCOMPLETE | FLAGS_HAS_VERSION) : FLAGS_HAS_VERSION);
129+
}
130+
118131
/// <summary>Creates a 80-bit <see cref="VersionStamp"/>, obtained from the database.</summary>
119132
/// <returns>Complete stamp, without user version.</returns>
120133
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]

FoundationDB.Tests/TransactionFacts.cs

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace FoundationDB.Client.Tests
3737
using System.Threading;
3838
using System.Threading.Tasks;
3939
using Doxense.Collections.Tuples;
40+
using Doxense.Memory;
4041

4142
[TestFixture]
4243
public class TransactionFacts : FdbTest
@@ -1993,37 +1994,140 @@ public async Task Test_Simple_Read_Transaction()
19931994
[Test]
19941995
public async Task Test_VersionStamp_Operations()
19951996
{
1996-
Fdb.Start(510);
1997+
// Veryify that we can set versionstamped keys inside a transaction
1998+
19971999
using (var db = await OpenTestDatabaseAsync())
19982000
{
1999-
Log("API Version: " + Fdb.ApiVersion);
2000-
20012001
var location = db.Partition.ByKey("versionstamps");
20022002

20032003
await db.ClearRangeAsync(location, this.Cancellation);
20042004

2005+
VersionStamp vsActual; // will contain the actual version stamp used by the database
2006+
2007+
Log("Inserting keys with version stamps:");
20052008
using (var tr = db.BeginTransaction(this.Cancellation))
20062009
{
2007-
Slice HACKHACK_Packify(VersionStamp stamp)
2010+
//TODO: HACKACK: until we add support to he transaction itself, we have to 'patch' the versionstamps by hand!
2011+
Slice HACKHACK_Stampify(Slice key)
20082012
{
2009-
var x = location.Keys.Encode(stamp);
2010-
x = x.Concat(Slice.FromFixed16((short) (location.GetPrefix().Count + 1)));
2011-
Log(x.ToHexaString(' ') + " | " + location.Keys.Dump(x));
2012-
return x;
2013+
// find the stamp byte sequence in the key
2014+
var x = tr.CreateVersionStamp().ToSlice();
2015+
int p = key.IndexOf(x);
2016+
Assert.That(p, Is.GreaterThan(0), "Stamp pattern was not found in the key!");
2017+
2018+
// append the offset at the end
2019+
var writer = new SliceWriter(key.Count + 2);
2020+
writer.WriteBytes(key);
2021+
writer.WriteFixed16((ushort) p); //note: the offset is Little Endian!
2022+
var y = writer.ToSlice();
2023+
2024+
//Log(y.ToHexaString(' ') + " | " + location.Keys.Dump(y));
2025+
return y;
20132026
}
20142027

2015-
tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete()), Slice.FromString("Hello, World!"));
2016-
tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(0)), Slice.FromString("Zero"));
2017-
tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(1)), Slice.FromString("One"));
2018-
tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(2)), Slice.FromString("Two"));
2019-
2028+
var vs = tr.CreateVersionStamp();
2029+
Log($"> placeholder stamp: {vs} with token '{vs.ToSlice():X}'");
2030+
Assert.That(vs.IsIncomplete, Is.True, "Placeholder token should be incomplete");
2031+
Assert.That(vs.HasUserVersion, Is.False);
2032+
Assert.That(vs.UserVersion, Is.Zero);
2033+
Assert.That(vs.TransactionVersion >> 56, Is.EqualTo(0xFF), "Highest 8 bit of Transaction Version should be set to 1");
2034+
Assert.That(vs.TransactionOrder >> 12, Is.EqualTo(0xF), "Hight 4 bits of Transaction Order should be set to 1");
2035+
2036+
var vs0 = tr.CreateVersionStamp(0);
2037+
Assert.That(vs0.IsIncomplete, Is.True, "Placeholder token should be incomplete");
2038+
Assert.That(vs0.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
2039+
Assert.That(vs0.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
2040+
Assert.That(vs0.HasUserVersion, Is.True);
2041+
Assert.That(vs0.UserVersion, Is.EqualTo(0));
2042+
2043+
var vs1 = tr.CreateVersionStamp(1);
2044+
Assert.That(vs1.IsIncomplete, Is.True, "Placeholder token should be incomplete");
2045+
Assert.That(vs1.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
2046+
Assert.That(vs1.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
2047+
Assert.That(vs1.HasUserVersion, Is.True);
2048+
Assert.That(vs1.UserVersion, Is.EqualTo(1));
2049+
2050+
var vs42 = tr.CreateVersionStamp(42);
2051+
Assert.That(vs42.IsIncomplete, Is.True, "Placeholder token should be incomplete");
2052+
Assert.That(vs42.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
2053+
Assert.That(vs42.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
2054+
Assert.That(vs42.HasUserVersion, Is.True);
2055+
Assert.That(vs42.UserVersion, Is.EqualTo(42));
2056+
2057+
// a single key using the 80-bit stamp
2058+
tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("foo", vs, 123)), Slice.FromString("Hello, World!"));
2059+
2060+
// simulate a batch of 3 keys, using 96-bits stamps
2061+
tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs0)), Slice.FromString("Zero"));
2062+
tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs1)), Slice.FromString("One"));
2063+
tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs42)), Slice.FromString("FortyTwo"));
2064+
2065+
// need to be request BEFORE the commit
20202066
var vsTask = tr.GetVersionStampAsync();
20212067

20222068
await tr.CommitAsync();
20232069
Log(tr.GetCommittedVersion());
20242070

2025-
var vs = await vsTask;
2026-
Log(vs);
2071+
// need to be resolved AFTER the commit
2072+
vsActual = await vsTask;
2073+
Log($"> actual stamp: {vsActual} with token '{vsActual.ToSlice():X}'");
2074+
}
2075+
2076+
Log("Checking database content:");
2077+
using (var tr = db.BeginReadOnlyTransaction(this.Cancellation))
2078+
{
2079+
{
2080+
var foo = await tr.GetRange(location.Keys.ToKeyRange("foo")).SingleAsync();
2081+
Log("> Found 1 result under (foo,)");
2082+
Log($"- {location.ExtractKey(foo.Key):K} = {foo.Value:V}");
2083+
Assert.That(foo.Value.ToString(), Is.EqualTo("Hello, World!"));
2084+
2085+
var t = location.Keys.Unpack(foo.Key);
2086+
Assert.That(t.Get<string>(0), Is.EqualTo("foo"));
2087+
Assert.That(t.Get<int>(2), Is.EqualTo(123));
2088+
2089+
var vs = t.Get<VersionStamp>(1);
2090+
Assert.That(vs.IsIncomplete, Is.False);
2091+
Assert.That(vs.HasUserVersion, Is.False);
2092+
Assert.That(vs.UserVersion, Is.Zero);
2093+
Assert.That(vs.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
2094+
Assert.That(vs.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
2095+
}
2096+
2097+
{
2098+
var items = await tr.GetRange(location.Keys.ToKeyRange("bar")).ToListAsync();
2099+
Log($"> Found {items.Count} results under (bar,)");
2100+
foreach (var item in items)
2101+
{
2102+
Log($"- {location.ExtractKey(item.Key):K} = {item.Value:V}");
2103+
}
2104+
2105+
Assert.That(items.Count, Is.EqualTo(3), "Should have found 3 keys under 'foo'");
2106+
2107+
Assert.That(items[0].Value.ToString(), Is.EqualTo("Zero"));
2108+
var vs0 = location.Keys.DecodeLast<VersionStamp>(items[0].Key);
2109+
Assert.That(vs0.IsIncomplete, Is.False);
2110+
Assert.That(vs0.HasUserVersion, Is.True);
2111+
Assert.That(vs0.UserVersion, Is.EqualTo(0));
2112+
Assert.That(vs0.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
2113+
Assert.That(vs0.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
2114+
2115+
Assert.That(items[1].Value.ToString(), Is.EqualTo("One"));
2116+
var vs1 = location.Keys.DecodeLast<VersionStamp>(items[1].Key);
2117+
Assert.That(vs1.IsIncomplete, Is.False);
2118+
Assert.That(vs1.HasUserVersion, Is.True);
2119+
Assert.That(vs1.UserVersion, Is.EqualTo(1));
2120+
Assert.That(vs1.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
2121+
Assert.That(vs1.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
2122+
2123+
Assert.That(items[2].Value.ToString(), Is.EqualTo("FortyTwo"));
2124+
var vs42 = location.Keys.DecodeLast<VersionStamp>(items[2].Key);
2125+
Assert.That(vs42.IsIncomplete, Is.False);
2126+
Assert.That(vs42.HasUserVersion, Is.True);
2127+
Assert.That(vs42.UserVersion, Is.EqualTo(42));
2128+
Assert.That(vs42.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
2129+
Assert.That(vs42.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
2130+
}
20272131
}
20282132

20292133
await DumpSubspace(db, location);

0 commit comments

Comments
 (0)