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

Commit ff66ead

Browse files
committed
Fixes #46 - adds pseudo high res timestamp generator.
This adds a util to generate timestamps from DateTime.UtcNow + a sequential tick. This will get around the issue where DateTime.UtcNow may not update frequently enough for it to be used in multitheaded environments. Only applies if the caller doesn't supply a timestamp.
1 parent 4283ea3 commit ff66ead

File tree

4 files changed

+248
-1
lines changed

4 files changed

+248
-1
lines changed

src/InfluxDB.Collector/MetricsCollector.cs

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

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