Skip to content

Commit 06f8c42

Browse files
authored
Merge pull request #72 from Doxense/dev/versionstamps
Add initial support for VersionStamps
2 parents 9e832a0 + 7919499 commit 06f8c42

20 files changed

+3407
-2
lines changed

FoundationDB.Client/Core/IFdbTransactionHandler.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public interface IFdbTransactionHandler : IDisposable
6161
/// </remarks>
6262
long GetCommittedVersion();
6363

64+
/// <summary>Returns the <see cref="VersionStamp"/> which was used by versionstamps operations in this transaction.</summary>
65+
Task<VersionStamp> GetVersionStampAsync(CancellationToken ct);
66+
6467
/// <summary>Sets the snapshot read version used by a transaction. This is not needed in simple cases.</summary>
6568
/// <param name="version">Read version to use in this transaction</param>
6669
/// <remarks>

FoundationDB.Client/FdbMutationType.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ public enum FdbMutationType
9191
/// If ``param`` is shorter than the existing value in the database, the existing value is truncated to match the length of ``param``.
9292
/// The smaller of the two values is then stored in the database.
9393
/// </summary>
94-
Min = 13
94+
Min = 13,
95+
96+
//TODO: XML Comments!
97+
VersionStampedKey = 14,
98+
99+
//TODO: XML Comments!
100+
VersionStampedValue = 15,
95101

96102
}
97103

FoundationDB.Client/FdbTransaction.cs

Lines changed: 100 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...
@@ -284,6 +287,81 @@ public void SetReadVersion(long version)
284287
m_handler.SetReadVersion(version);
285288
}
286289

290+
/// <summary>Returns the <see cref="VersionStamp"/> which was used by versionstamps operations in this transaction.</summary>
291+
/// <remarks>
292+
/// The Task will be ready only after the successful completion of a call to <see cref="CommitAsync"/> on this transaction.
293+
/// Read-only transactions do not modify the database when committed and will result in the Task completing with an error.
294+
/// Keep in mind that a transaction which reads keys and then sets them to their current values may be optimized to a read-only transaction.
295+
/// </remarks>
296+
public Task<VersionStamp> GetVersionStampAsync()
297+
{
298+
EnsureNotFailedOrDisposed();
299+
300+
return m_handler.GetVersionStampAsync(m_cancellation);
301+
}
302+
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+
287365
#endregion
288366

289367
#region Get...
@@ -513,6 +591,23 @@ private static void EnsureMutationTypeIsSupported(FdbMutationType mutation, int
513591
return;
514592
}
515593

594+
if (mutation == FdbMutationType.VersionStampedKey || mutation == FdbMutationType.VersionStampedValue)
595+
{
596+
if (selectedApiVersion < 400)
597+
{
598+
if (Fdb.GetMaxApiVersion() >= 400)
599+
{
600+
throw new FdbException(FdbError.InvalidMutationType, "Atomic mutations for VersionStamps are only supported starting from API level 400. You need to select API level 400 or more at the start of your process.");
601+
}
602+
else
603+
{
604+
throw new FdbException(FdbError.InvalidMutationType, "Atomic mutations Max and Min are only supported starting from client version 4.x. You need to update the version of the client, and select API level 400 or more at the start of your process..");
605+
}
606+
}
607+
// ok!
608+
return;
609+
}
610+
516611
// this could be a new mutation type, or an invalid value.
517612
throw new FdbException(FdbError.InvalidMutationType, "An invalid mutation type was issued. If you are attempting to call a new mutation type, you will need to update the version of this assembly, and select the latest API level.");
518613
}
@@ -755,6 +850,11 @@ private void RestoreDefaultSettings()
755850
{
756851
this.Timeout = m_database.DefaultTimeout;
757852
}
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;
758858
}
759859

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

FoundationDB.Client/FdbTransactionExtensions.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ namespace FoundationDB.Client
3636
using System.Threading.Tasks;
3737
using Doxense.Diagnostics.Contracts;
3838
using Doxense.Linq;
39+
using Doxense.Memory;
3940
using Doxense.Serialization.Encoders;
4041
using JetBrains.Annotations;
4142

@@ -425,6 +426,89 @@ public static void AtomicMin([NotNull] this IFdbTransaction trans, Slice key, Sl
425426
trans.Atomic(key, value, FdbMutationType.Min);
426427
}
427428

429+
private static int GetVersionStampOffset(Slice buffer, Slice token, string argName)
430+
{
431+
// the buffer MUST contain one incomplete stamp, either the random token of the current transsaction or the default token (all-FF)
432+
433+
int p = token.HasValue ? buffer.IndexOf(token) : -1;
434+
if (p >= 0)
435+
{ // found a candidate spot, we have to make sure that it is only present once in the key!
436+
437+
if (buffer.IndexOf(token, p + token.Count) >= 0)
438+
{
439+
if (argName == "key")
440+
throw new ArgumentException("The key should only contain one occurrence of a VersionStamp.", argName);
441+
else
442+
throw new ArgumentException("The value should only contain one occurrence of a VersionStamp.", argName);
443+
}
444+
}
445+
else
446+
{ // not found, maybe it is using the default incomplete stamp (all FF) ?
447+
p = buffer.IndexOf(VersionStamp.IncompleteToken);
448+
if (p < 0)
449+
{
450+
if (argName == "key")
451+
throw new ArgumentException("The key should contain at least one VersionStamp.", argName);
452+
else
453+
throw new ArgumentException("The value should contain at least one VersionStamp.", argName);
454+
}
455+
}
456+
Contract.Assert(p >= 0 && p + token.Count <= buffer.Count);
457+
458+
return p;
459+
}
460+
461+
/// <summary>Set the <paramref name="value"/> of the <paramref name="key"/> in the database, with the <see cref="VersionStamp"/> replaced by the resolved version at commit time.</summary>
462+
/// <param name="trans">Transaction to use for the operation</param>
463+
/// <param name="key">Name of the key whose value is to be mutated. This key must contain a single <see cref="VersionStamp"/>, whose position will be automatically detected.</param>
464+
/// <param name="value">New value for this key.</param>
465+
public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, Slice value)
466+
{
467+
Contract.NotNull(trans, nameof(trans));
468+
469+
//TODO: PERF: optimize this to not have to allocate!
470+
var token = trans.CreateVersionStamp().ToSlice();
471+
var offset = GetVersionStampOffset(key, token, nameof(key));
472+
473+
var writer = new SliceWriter(key.Count + 2);
474+
writer.WriteBytes(key);
475+
writer.WriteFixed16(checked((ushort) offset)); //note: currently stored as 16-bits in Little Endian
476+
477+
trans.Atomic(writer.ToSlice(), value, FdbMutationType.VersionStampedKey);
478+
}
479+
480+
/// <summary>Set the <paramref name="value"/> of the <paramref name="key"/> in the database, with the <see cref="VersionStamp"/> replaced by the resolved version at commit time.</summary>
481+
/// <param name="trans">Transaction to use for the operation</param>
482+
/// <param name="key">Name of the key whose value is to be mutated. This key must contain a single <see cref="VersionStamp"/>, whose start is defined by <paramref name="stampOffset"/>.</param>
483+
/// <param name="stampOffset">Offset within <paramref name="key"/> of the start of the 80-bit VersionStamp.</param>
484+
/// <param name="value">New value for this key.</param>
485+
public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, int stampOffset, Slice value)
486+
{
487+
Contract.NotNull(trans, nameof(trans));
488+
489+
if (stampOffset > key.Count - 10) throw new ArgumentException("The VersionStamp overflows past the end of the key.", nameof(stampOffset));
490+
if (stampOffset > 0xFFFF) throw new ArgumentException("The offset is too large to fit within 16-bits.");
491+
492+
var writer = new SliceWriter(key.Count + 2);
493+
writer.WriteBytes(key);
494+
writer.WriteFixed16(checked((ushort) stampOffset)); //note: currently stored as 16-bits in Little Endian
495+
496+
trans.Atomic(writer.ToSlice(), value, FdbMutationType.VersionStampedKey);
497+
}
498+
499+
/// <summary>Set the <paramref name="value"/> of the <paramref name="key"/> in the database, with the first 10 bytes overwritten with the transaction's <see cref="VersionStamp"/>.</summary>
500+
/// <param name="trans">Transaction to use for the operation</param>
501+
/// <param name="key">Name of the key whose value is to be mutated.</param>
502+
/// <param name="value">Value whose first 10 bytes will be overwritten by the database with the resolved VersionStamp at commit time. The rest of the value will be untouched.</param>
503+
public static void SetVersionStampedValue([NotNull] this IFdbTransaction trans, Slice key, Slice value)
504+
{
505+
Contract.NotNull(trans, nameof(trans));
506+
507+
if (value.Count < 10) throw new ArgumentException("The value must be at least 10 bytes long.", nameof(value));
508+
509+
trans.Atomic(key, value, FdbMutationType.VersionStampedValue);
510+
}
511+
428512
#endregion
429513

430514
#region GetRange...

FoundationDB.Client/Filters/FdbTransactionFilter.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ public virtual long GetCommittedVersion()
251251
return m_transaction.GetCommittedVersion();
252252
}
253253

254+
public virtual Task<VersionStamp> GetVersionStampAsync()
255+
{
256+
ThrowIfDisposed();
257+
return m_transaction.GetVersionStampAsync();
258+
}
259+
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+
254272
public virtual void SetReadVersion(long version)
255273
{
256274
ThrowIfDisposed();

FoundationDB.Client/Filters/Logging/FdbLoggedTransaction.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ public override Task OnErrorAsync(FdbError code)
312312
);
313313
}
314314

315+
public override Task<VersionStamp> GetVersionStampAsync()
316+
{
317+
return ExecuteAsync(
318+
new FdbTransactionLog.GetVersionStampCommand(),
319+
(tr, cmd) => tr.GetVersionStampAsync()
320+
);
321+
}
322+
315323
public override void Set(Slice key, Slice value)
316324
{
317325
Execute(

FoundationDB.Client/Filters/Logging/FdbTransactionLog.Commands.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,12 @@ public override string GetResult(KeyResolver resolver)
832832

833833
}
834834

835+
public sealed class GetVersionStampCommand : Command<VersionStamp>
836+
{
837+
public override Operation Op { get { return Operation.GetVersionStamp; } }
838+
839+
}
840+
835841
public sealed class GetReadVersionCommand : Command<long>
836842
{
837843
public override Operation Op { get { return Operation.GetReadVersion; } }

FoundationDB.Client/Filters/Logging/FdbTransactionLog.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ public enum Operation
552552
Reset,
553553
OnError,
554554
SetOption,
555+
GetVersionStamp,
555556

556557
Log,
557558
}

FoundationDB.Client/IFdbTransaction.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@ public interface IFdbTransaction : IFdbReadOnlyTransaction
112112
/// </remarks>
113113
long GetCommittedVersion();
114114

115+
/// <summary>Returns the <see cref="VersionStamp"/> which was used by versionstamps operations in this transaction.</summary>
116+
/// <remarks>
117+
/// The Task will be ready only after the successful completion of a call to <see cref="CommitAsync"/> on this transaction.
118+
/// Read-only transactions do not modify the database when committed and will result in the Task completing with an error.
119+
/// Keep in mind that a transaction which reads keys and then sets them to their current values may be optimized to a read-only transaction.
120+
/// </remarks>
121+
Task<VersionStamp> GetVersionStampAsync();
122+
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+
115141
/// <summary>
116142
/// Watch a key for any change in the database.
117143
/// </summary>

0 commit comments

Comments
 (0)