Skip to content
This repository was archived by the owner on Jul 18, 2023. It is now read-only.

Commit 041ec80

Browse files
authored
Merge pull request #47 from trevhunter/fix-timestamp
Fixes #46 - adds pseudo high res timestamp generator.
2 parents 4283ea3 + 054e90e commit 041ec80

File tree

4 files changed

+218
-1
lines changed

4 files changed

+218
-1
lines changed

src/InfluxDB.Collector/MetricsCollector.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace InfluxDB.Collector
77
{
88
public abstract class MetricsCollector : IPointEmitter, IDisposable
99
{
10+
readonly Util.ITimestampSource _timestampSource = new Util.PseudoHighResTimestampSource();
11+
1012
public void Increment(string measurement, long count = 1, IReadOnlyDictionary<string, string> tags = null)
1113
{
1214
Write(measurement, new Dictionary<string, object> { { "count", count } }, tags);
@@ -38,7 +40,7 @@ public void Write(string measurement, IReadOnlyDictionary<string, object> fields
3840
{
3941
try
4042
{
41-
var point = new PointData(measurement, fields, tags, timestamp ?? DateTime.UtcNow);
43+
var point = new PointData(measurement, fields, tags, timestamp ?? _timestampSource.GetUtcNow());
4244
Emit(new[] { point });
4345
}
4446
catch (Exception ex)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
3+
namespace InfluxDB.Collector.Util
4+
{
5+
/// <summary>
6+
/// Supplier of timestamps for metrics
7+
/// </summary>
8+
interface ITimestampSource
9+
{
10+
DateTime GetUtcNow();
11+
}
12+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
3+
namespace InfluxDB.Collector.Util
4+
{
5+
/// <summary>
6+
/// Implements <see cref="ITimestampSource"/>
7+
/// in a way that combines the low-ish resolution DateTime.UtcNow
8+
/// with a sequence number added to the ticks to provide
9+
/// pseudo-tick precision timestamps
10+
/// </summary>
11+
/// <remarks>
12+
/// See https://github.com/influxdata/influxdb-csharp/issues/46 for why this is necessary.
13+
/// Long story short:
14+
/// a) InfluxDB has a "LastWriteWins" policy for points that have the same timestamp and tags.
15+
/// b) The normal <see cref="System.DateTime.UtcNow"/> only supplies timestamps that change not as often as you think.
16+
/// c) In a web server, it's entirely possible for more than one thread to get the same UtcNow value
17+
///
18+
/// As a remediation for this, we infuse DateTime.UtcNow with a sequence number until it ticks over.
19+
/// </remarks>
20+
class PseudoHighResTimestampSource : ITimestampSource
21+
{
22+
private long _lastUtcNowTicks = 0;
23+
private long _sequence = 0;
24+
private readonly object lockObj = new object();
25+
26+
public DateTime GetUtcNow()
27+
{
28+
DateTime utcNow = DateTime.UtcNow;
29+
30+
lock (lockObj)
31+
{
32+
if (utcNow.Ticks == _lastUtcNowTicks)
33+
{
34+
// UtcNow hasn't rolled over yet, so
35+
// add a sequence number to it
36+
_sequence++;
37+
long pseudoTicks = utcNow.Ticks + _sequence;
38+
return new DateTime(pseudoTicks, DateTimeKind.Utc);
39+
}
40+
else
41+
{
42+
// Reset as UtcNow has rolled over
43+
_sequence = 0;
44+
_lastUtcNowTicks = utcNow.Ticks;
45+
return utcNow;
46+
}
47+
}
48+
}
49+
}
50+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System;
2+
using Xunit;
3+
using System.Threading.Tasks;
4+
using Xunit.Abstractions;
5+
using InfluxDB.Collector.Util;
6+
7+
namespace InfluxDB.LineProtocol.Tests.Collector.Util
8+
{
9+
public class PseudoHighResTimeStampSourceTests
10+
{
11+
private readonly ITestOutputHelper output;
12+
13+
public PseudoHighResTimeStampSourceTests(ITestOutputHelper output)
14+
{
15+
this.output = output;
16+
}
17+
18+
[Fact]
19+
public void CanSupplyHighResTimestamps()
20+
{
21+
const int iterations = 100000;
22+
23+
// Even if we call inside a very tight loop,
24+
// we should get better (pseudo) resolution than just DateTime.UtcNow
25+
long dateTimeCollisions = 0;
26+
DateTime previousDateTime = DateTime.UtcNow;
27+
for (int i = 0; i < iterations; i++)
28+
{
29+
// Attempt with date time directly
30+
DateTime current = DateTime.UtcNow;
31+
32+
if (previousDateTime == current)
33+
{
34+
dateTimeCollisions++;
35+
}
36+
previousDateTime = current;
37+
}
38+
39+
if (0 == dateTimeCollisions)
40+
{
41+
// Warn because we do expect that datetime.now has collisions in such a tight loop
42+
output.WriteLine("Warning! Expected DateTime.UtcNow to have some collisions, but seemed to be high resolution already.");
43+
}
44+
45+
// Now attempt to use our pseudo high precision provider that includes a sequence number to ensure that it
46+
// never collides
47+
ITimestampSource target = new PseudoHighResTimestampSource();
48+
long highResTotalCollisions = 0;
49+
previousDateTime = target.GetUtcNow();
50+
for (int i = 0; i < iterations; i++)
51+
{
52+
// Attempt with date time directly
53+
DateTime current = target.GetUtcNow();
54+
55+
if (previousDateTime == current)
56+
{
57+
highResTotalCollisions++;
58+
}
59+
previousDateTime = current;
60+
}
61+
62+
Assert.Equal(0, highResTotalCollisions);
63+
output.WriteLine($"No collisions detected with high resolution source, compared to {dateTimeCollisions} for DateTime.UtcNow.");
64+
}
65+
66+
[Fact]
67+
public void CanSupplyHighResTimestampsInParallel()
68+
{
69+
const int iterations = 100000;
70+
71+
// Even if we call inside a very tight loop, we should get better (pseudo) resolution than just DateTime.UtcNow
72+
int dateTimeCollisions = 0;
73+
DateTime previousDateTime = DateTime.UtcNow;
74+
Parallel.For(0, iterations, (i) => // (int i = 0; i < 100000; i++)
75+
{
76+
// Attempt with date time directly
77+
DateTime current = DateTime.UtcNow;
78+
79+
if (previousDateTime == current)
80+
{
81+
System.Threading.Interlocked.Increment(ref dateTimeCollisions);
82+
}
83+
previousDateTime = current;
84+
});
85+
86+
if (0 == dateTimeCollisions)
87+
{
88+
// Warn because we do expect that datetime.now has collisions in such a tight loop
89+
output.WriteLine("Warning! Expected DateTime.UtcNow to have some collisions, but seemed to be high resolution already.");
90+
}
91+
92+
// Now attempt to use our pseudo high precision provider that includes a sequence number to ensure that it
93+
// never collides
94+
ITimestampSource target = new PseudoHighResTimestampSource();
95+
int highResTotalCollisions = 0;
96+
previousDateTime = target.GetUtcNow();
97+
Parallel.For(0, iterations, (i) => // (int i = 0; i < 100000; i++)
98+
{
99+
// Attempt with date time directly
100+
DateTime current = target.GetUtcNow();
101+
102+
if (previousDateTime == current)
103+
{
104+
System.Threading.Interlocked.Increment(ref highResTotalCollisions);
105+
}
106+
previousDateTime = current;
107+
});
108+
109+
Assert.Equal(0, highResTotalCollisions);
110+
output.WriteLine($"No collisions detected with high resolution source, compared to {dateTimeCollisions} for DateTime.UtcNow.");
111+
}
112+
113+
[Fact]
114+
public void WillGiveUtcDateTimeKind()
115+
{
116+
ITimestampSource target = new PseudoHighResTimestampSource();
117+
118+
DateTime result = target.GetUtcNow();
119+
Assert.Equal(DateTimeKind.Utc, result.Kind);
120+
}
121+
122+
[Fact]
123+
public void WillNotDriftTooFarFromUtcNow()
124+
{
125+
ITimestampSource target = new PseudoHighResTimestampSource();
126+
const int MAX_DRIFT_MS = 10;
127+
128+
// Average over 10000 iterations and get the average drift
129+
decimal totalDrift = 0;
130+
const int iterations = 10000;
131+
for (int i = 0; i < iterations; i++)
132+
{
133+
DateTime current = DateTime.UtcNow;
134+
DateTime result = target.GetUtcNow();
135+
136+
totalDrift += Convert.ToDecimal((result - current).TotalMilliseconds);
137+
}
138+
Decimal averageDrift = totalDrift / iterations;
139+
140+
if (averageDrift > MAX_DRIFT_MS)
141+
{
142+
output.WriteLine($"Expected times were more than {MAX_DRIFT_MS}ms apart. Instead they were {averageDrift}ms apart.");
143+
Assert.True(false); // Force fail.
144+
}
145+
else
146+
{
147+
output.WriteLine ($"Total Drift over {iterations} iterations: {totalDrift}ms. Average {averageDrift}ms");
148+
}
149+
}
150+
}
151+
}
152+
153+

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy