/
VersionStamp.cs
260 lines (215 loc) · 9.16 KB
/
VersionStamp.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
/// <summary>
/// VersionStamp should be only used to compare versions returned by same API.
/// </summary>
public struct VersionStamp : IEquatable<VersionStamp>, IObjectWritable
{
public static VersionStamp Default => default;
private const int GlobalVersionMarker = -1;
private const int InitialGlobalVersion = 10000;
/// <summary>
/// global counter to avoid collision within same session.
/// it starts with a big initial number just for a clarity in debugging
/// </summary>
private static int s_globalVersion = InitialGlobalVersion;
/// <summary>
/// time stamp
/// </summary>
private readonly DateTime _utcLastModified;
/// <summary>
/// indicate whether there was a collision on same item
/// </summary>
private readonly int _localIncrement;
/// <summary>
/// unique version in same session
/// </summary>
private readonly int _globalIncrement;
private VersionStamp(DateTime utcLastModified)
: this(utcLastModified, 0)
{
}
private VersionStamp(DateTime utcLastModified, int localIncrement)
: this(utcLastModified, localIncrement, GetNextGlobalVersion())
{
}
private VersionStamp(DateTime utcLastModified, int localIncrement, int globalIncrement)
{
if (utcLastModified != default && utcLastModified.Kind != DateTimeKind.Utc)
{
throw new ArgumentException(WorkspacesResources.DateTimeKind_must_be_Utc, nameof(utcLastModified));
}
_utcLastModified = utcLastModified;
_localIncrement = localIncrement;
_globalIncrement = globalIncrement;
}
/// <summary>
/// Creates a new instance of a VersionStamp.
/// </summary>
public static VersionStamp Create()
{
return new VersionStamp(DateTime.UtcNow);
}
/// <summary>
/// Creates a new instance of a version stamp based on the specified DateTime.
/// </summary>
public static VersionStamp Create(DateTime utcTimeLastModified)
{
return new VersionStamp(utcTimeLastModified);
}
/// <summary>
/// compare two different versions and return either one of the versions if there is no collision, otherwise, create a new version
/// that can be used later to compare versions between different items
/// </summary>
public VersionStamp GetNewerVersion(VersionStamp version)
{
// * NOTE *
// in current design/implementation, there are 4 possible ways for a version to be created.
//
// 1. created from a file stamp (most likely by starting a new session). "increment" will have 0 as value
// 2. created by modifying existing item (text changes, project changes etc).
// "increment" will have either 0 or previous increment + 1 if there was a collision.
// 3. created from deserialization (probably by using persistent service).
// 4. created by accumulating versions of multiple items.
//
// and this method is the one that is responsible for #4 case.
if (_utcLastModified > version._utcLastModified)
{
return this;
}
if (_utcLastModified == version._utcLastModified)
{
var thisGlobalVersion = GetGlobalVersion(this);
var thatGlobalVersion = GetGlobalVersion(version);
if (thisGlobalVersion == thatGlobalVersion)
{
// given versions are same one
return this;
}
// mark it as global version
// global version can't be moved to newer version.
return new VersionStamp(_utcLastModified, (thisGlobalVersion > thatGlobalVersion) ? thisGlobalVersion : thatGlobalVersion, GlobalVersionMarker);
}
return version;
}
/// <summary>
/// Gets a new VersionStamp that is guaranteed to be newer than its base one
/// this should only be used for same item to move it to newer version
/// </summary>
public VersionStamp GetNewerVersion()
{
// global version can't be moved to newer version
Debug.Assert(_globalIncrement != GlobalVersionMarker);
var now = DateTime.UtcNow;
var incr = (now == _utcLastModified) ? _localIncrement + 1 : 0;
return new VersionStamp(now, incr);
}
/// <summary>
/// Returns the serialized text form of the VersionStamp.
/// </summary>
public override string ToString()
{
// 'o' is the roundtrip format that captures the most detail.
return _utcLastModified.ToString("o") + "-" + _globalIncrement + "-" + _localIncrement;
}
public override int GetHashCode()
{
return Hash.Combine(_utcLastModified.GetHashCode(), _localIncrement);
}
public override bool Equals(object obj)
{
if (obj is VersionStamp v)
{
return this.Equals(v);
}
return false;
}
public bool Equals(VersionStamp version)
{
if (_utcLastModified == version._utcLastModified)
{
return GetGlobalVersion(this) == GetGlobalVersion(version);
}
return false;
}
public static bool operator ==(VersionStamp left, VersionStamp right)
{
return left.Equals(right);
}
public static bool operator !=(VersionStamp left, VersionStamp right)
{
return !left.Equals(right);
}
/// <summary>
/// check whether given persisted version is re-usable
/// </summary>
internal static bool CanReusePersistedVersion(VersionStamp baseVersion, VersionStamp persistedVersion)
{
if (baseVersion == persistedVersion)
{
return true;
}
// there was a collision, we can't use these
if (baseVersion._localIncrement != 0 || persistedVersion._localIncrement != 0)
{
return false;
}
return baseVersion._utcLastModified == persistedVersion._utcLastModified;
}
bool IObjectWritable.ShouldReuseInSerialization => true;
void IObjectWritable.WriteTo(ObjectWriter writer)
{
WriteTo(writer);
}
internal void WriteTo(ObjectWriter writer)
{
writer.WriteInt64(_utcLastModified.ToBinary());
writer.WriteInt32(_localIncrement);
writer.WriteInt32(_globalIncrement);
}
internal static VersionStamp ReadFrom(ObjectReader reader)
{
var raw = reader.ReadInt64();
var localIncrement = reader.ReadInt32();
var globalIncrement = reader.ReadInt32();
return new VersionStamp(DateTime.FromBinary(raw), localIncrement, globalIncrement);
}
private static int GetGlobalVersion(VersionStamp version)
{
// global increment < 0 means it is a global version which has its global increment in local increment
return version._globalIncrement >= 0 ? version._globalIncrement : version._localIncrement;
}
private static int GetNextGlobalVersion()
{
// REVIEW: not sure what is best way to wrap it when it overflows. should I just throw or don't care.
// with 50ms (typing) as an interval for a new version, it gives more than 1 year before int32 to overflow.
// with 5ms as an interval, it gives more than 120 days before it overflows.
// since global version is only for per VS session, I think we don't need to worry about overflow.
// or we could use Int64 which will give more than a million years turn around even on 1ms interval.
// this will let versions to be compared safely between multiple items
// without worrying about collision within same session
var globalVersion = Interlocked.Increment(ref VersionStamp.s_globalVersion);
return globalVersion;
}
/// <summary>
/// True if this VersionStamp is newer than the specified one.
/// </summary>
internal bool TestOnly_IsNewerThan(VersionStamp version)
{
if (_utcLastModified > version._utcLastModified)
{
return true;
}
if (_utcLastModified == version._utcLastModified)
{
return GetGlobalVersion(this) > GetGlobalVersion(version);
}
return false;
}
}
}