From 846f17fa47f7d01749c6a1e1f296d83e0d1fd20a Mon Sep 17 00:00:00 2001 From: Kevin Gosse Date: Tue, 21 Nov 2017 10:08:21 +0100 Subject: [PATCH 1/2] Automatically recreate the UDP client after a given time It makes the UDP pipe more resilient to DNS changes or socket errors --- source/Graphite/ChannelFactory.cs | 28 ++++----- .../ConnectionStringConfigurationBase.cs | 18 +++++- .../Graphite/Configuration/GraphiteElement.cs | 18 +++++- .../Configuration/IGraphiteConfiguration.cs | 6 ++ .../Configuration/IStatsDConfiguration.cs | 7 +++ .../Graphite/Configuration/StatsDElement.cs | 18 +++++- source/Graphite/Graphite.csproj | 1 + .../Infrastructure/AutoRefreshPipe.cs | 58 +++++++++++++++++++ source/Graphite/Infrastructure/UdpPipe.cs | 7 +-- source/MSBuild.Graphite/Tasks/Graphite.cs | 12 ++-- source/MSBuild.Graphite/Tasks/StatsD.cs | 2 + 11 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 source/Graphite/Infrastructure/AutoRefreshPipe.cs diff --git a/source/Graphite/ChannelFactory.cs b/source/Graphite/ChannelFactory.cs index c20f4b8..6953c61 100644 --- a/source/Graphite/ChannelFactory.cs +++ b/source/Graphite/ChannelFactory.cs @@ -11,7 +11,7 @@ namespace Graphite /// public class ChannelFactory : IDisposable { - private static readonly Func buildKey = (prefix, key) => + private static readonly Func buildKey = (prefix, key) => !string.IsNullOrEmpty(prefix) ? prefix + "." + key : key; private readonly FormatterFactory formatters; @@ -55,8 +55,8 @@ public IMonitoringChannel CreateChannel(string type, string target) throw new InvalidOperationException("graphite pipe is not configured."); return new MonitoringChannel( - k => buildKey(this.graphitePrefix, k), - formatter, + k => buildKey(this.graphitePrefix, k), + formatter, this.graphitePipe); } else if (string.Equals(target, "statsd", StringComparison.OrdinalIgnoreCase)) @@ -65,8 +65,8 @@ public IMonitoringChannel CreateChannel(string type, string target) throw new InvalidOperationException("statsd pipe is not configured."); return new MonitoringChannel( - k => buildKey(this.statsdPrefix, k), - formatter, + k => buildKey(this.statsdPrefix, k), + formatter, this.statsdPipe); } @@ -92,9 +92,9 @@ public IMonitoringChannel CreateChannel(string type, string target, float sampli throw new InvalidOperationException("statsd pipe is not configured."); return new SamplingMonitoringChannel( - k => buildKey(this.statsdPrefix, k), - formatter, - this.statsdPipe, + k => buildKey(this.statsdPrefix, k), + formatter, + this.statsdPipe, sampling); } else if (string.Equals(target, "graphite", StringComparison.OrdinalIgnoreCase)) @@ -155,13 +155,12 @@ private void SetupPipes(IGraphiteConfiguration graphite, IStatsDConfiguration st } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Objekte verwerfen, bevor Bereich verloren geht", Justification="Ownership transferred to outer pipe.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Objekte verwerfen, bevor Bereich verloren geht", Justification = "Ownership transferred to outer pipe.")] private void SetupStatsD(IStatsDConfiguration configuration) { - IPAddress address = Helpers.ParseAddress(configuration.Address); + Func pipeFactory = () => new UdpPipe(configuration.Address, configuration.Port); - // Initialize with ip address. - IPipe inner = new UdpPipe(address, configuration.Port); + var inner = new AutoRefreshPipe(pipeFactory, configuration.Lifetime); this.statsdPipe = new SamplingPipe(inner); } @@ -178,8 +177,9 @@ private void SetupGraphite(IGraphiteConfiguration configuration) } else if (configuration.Transport == TransportType.Udp) { - // Initialize with ip address. - this.graphitePipe = new UdpPipe(address, configuration.Port); + Func pipeFactory = () => new UdpPipe(configuration.Address, configuration.Port); + + this.graphitePipe = new AutoRefreshPipe(pipeFactory, configuration.Lifetime); } else { diff --git a/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs b/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs index 1a2f411..3554deb 100644 --- a/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs +++ b/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Graphite.Configuration @@ -23,6 +24,11 @@ public abstract class ConnectionStringConfigurationBase /// public string PrefixKey { get; private set; } + /// + /// Gets the time before renewing the socket, when using UDP protocol. + /// + public TimeSpan Lifetime { get; private set; } + /// /// Parses connection string to properties. /// @@ -37,7 +43,7 @@ protected virtual IDictionary Parse(string connectionString) .Split(';') .Select(x => x.Split(new[] { '=' }, 2)) .ToDictionary( - x => x.First().ToLowerInvariant(), + x => x.First().ToLowerInvariant(), x => x.Skip(1).FirstOrDefault()); string value; @@ -59,6 +65,14 @@ protected virtual IDictionary Parse(string connectionString) { this.PrefixKey = value; } + + if (values.TryGetValue("lifetime", out value)) + { + if (TimeSpan.TryParse(value, out TimeSpan temp)) + { + this.Lifetime = temp; + } + } } return values; diff --git a/source/Graphite/Configuration/GraphiteElement.cs b/source/Graphite/Configuration/GraphiteElement.cs index 94f0bb6..dee6684 100644 --- a/source/Graphite/Configuration/GraphiteElement.cs +++ b/source/Graphite/Configuration/GraphiteElement.cs @@ -1,4 +1,5 @@ -using System.Configuration; +using System; +using System.Configuration; using System.Net; namespace Graphite.Configuration @@ -28,6 +29,11 @@ public class GraphiteElement : ConfigurationElement, IGraphiteConfiguration /// internal const string PrefixKeyPropertyName = "prefixKey"; + /// + /// The XML name of the property + /// + internal const string LifetimePropertyName = "lifetime"; + /// /// Gets or sets the port number. /// @@ -67,5 +73,15 @@ public string PrefixKey get { return (string)base[PrefixKeyPropertyName]; } set { base[PrefixKeyPropertyName] = value; } } + + /// + /// When using UDP protocol, the time before renewing the socket + /// + [ConfigurationProperty(LifetimePropertyName, IsRequired = false)] + public TimeSpan Lifetime + { + get { return (TimeSpan)base[LifetimePropertyName]; } + set { base[LifetimePropertyName] = value; } + } } } diff --git a/source/Graphite/Configuration/IGraphiteConfiguration.cs b/source/Graphite/Configuration/IGraphiteConfiguration.cs index b259f3c..03eb87f 100644 --- a/source/Graphite/Configuration/IGraphiteConfiguration.cs +++ b/source/Graphite/Configuration/IGraphiteConfiguration.cs @@ -1,3 +1,4 @@ +using System; using System.Net; namespace Graphite.Configuration @@ -26,5 +27,10 @@ public interface IGraphiteConfiguration /// Gets the common prefix key. /// string PrefixKey { get; } + + /// + /// Gets the time before renewing the socket, when using UDP protocol + /// + TimeSpan Lifetime { get; } } } \ No newline at end of file diff --git a/source/Graphite/Configuration/IStatsDConfiguration.cs b/source/Graphite/Configuration/IStatsDConfiguration.cs index fa7767b..db14677 100644 --- a/source/Graphite/Configuration/IStatsDConfiguration.cs +++ b/source/Graphite/Configuration/IStatsDConfiguration.cs @@ -1,3 +1,5 @@ +using System; + namespace Graphite.Configuration { /// @@ -19,5 +21,10 @@ public interface IStatsDConfiguration /// Gets the common prefix key. /// string PrefixKey { get; } + + /// + /// Gets the time before renewing the socket, when using UDP protocol + /// + TimeSpan Lifetime { get; } } } \ No newline at end of file diff --git a/source/Graphite/Configuration/StatsDElement.cs b/source/Graphite/Configuration/StatsDElement.cs index 33edd05..f0aa78f 100644 --- a/source/Graphite/Configuration/StatsDElement.cs +++ b/source/Graphite/Configuration/StatsDElement.cs @@ -1,4 +1,5 @@ -using System.Configuration; +using System; +using System.Configuration; namespace Graphite.Configuration { @@ -22,6 +23,11 @@ public class StatsDElement : ConfigurationElement, IStatsDConfiguration /// internal const string PrefixKeyPropertyName = "prefixKey"; + /// + /// The XML name of the property + /// + internal const string LifetimePropertyName = "lifetime"; + /// /// Gets or sets the port number. /// @@ -51,5 +57,15 @@ public string PrefixKey get { return (string)base[PrefixKeyPropertyName]; } set { base[PrefixKeyPropertyName] = value; } } + + /// + /// The time before renewing the socket + /// + [ConfigurationProperty(LifetimePropertyName, IsRequired = false)] + public TimeSpan Lifetime + { + get { return (TimeSpan)base[LifetimePropertyName]; } + set { base[LifetimePropertyName] = value; } + } } } diff --git a/source/Graphite/Graphite.csproj b/source/Graphite/Graphite.csproj index f4993ef..b53d5be 100644 --- a/source/Graphite/Graphite.csproj +++ b/source/Graphite/Graphite.csproj @@ -111,6 +111,7 @@ + diff --git a/source/Graphite/Infrastructure/AutoRefreshPipe.cs b/source/Graphite/Infrastructure/AutoRefreshPipe.cs new file mode 100644 index 0000000..571de58 --- /dev/null +++ b/source/Graphite/Infrastructure/AutoRefreshPipe.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; + +namespace Graphite.Infrastructure +{ + internal class AutoRefreshPipe : IPipe, IDisposable + { + private readonly Func pipeFactory; + private readonly TimeSpan delay; + + private DateTime lastUpdate; + private IPipe innerPipe; + + public AutoRefreshPipe(Func pipeFactory, TimeSpan delay) + { + this.pipeFactory = pipeFactory; + this.innerPipe = pipeFactory(); + this.delay = delay; + this.lastUpdate = DateTime.UtcNow; + } + + public bool Send(string message) + { + this.RefreshPipeIfNeeded(); + + return this.innerPipe.Send(message); + } + + public bool Send(string[] messages) + { + this.RefreshPipeIfNeeded(); + + return this.innerPipe.Send(messages); + } + + public void Dispose() + { + this.DisposePipe(this.innerPipe); + } + + private void RefreshPipeIfNeeded() + { + if (this.delay > TimeSpan.Zero && DateTime.UtcNow - this.lastUpdate >= this.delay) + { + var oldPipe = Interlocked.Exchange(ref this.innerPipe, this.pipeFactory()); + this.lastUpdate = DateTime.UtcNow; + this.DisposePipe(oldPipe); + } + } + + private void DisposePipe(IPipe pipe) + { + var disposablePipe = pipe as IDisposable; + + disposablePipe?.Dispose(); + } + } +} diff --git a/source/Graphite/Infrastructure/UdpPipe.cs b/source/Graphite/Infrastructure/UdpPipe.cs index 92d6e38..cc4c366 100644 --- a/source/Graphite/Infrastructure/UdpPipe.cs +++ b/source/Graphite/Infrastructure/UdpPipe.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Net; using System.Net.Sockets; using System.Text; @@ -12,11 +11,11 @@ internal class UdpPipe : IPipe, IDisposable private bool disposed; - public UdpPipe(IPAddress address, int port) + public UdpPipe(string address, int port) { this.udpClient = new UdpClient(); - this.udpClient.Connect(new IPEndPoint(address, port)); + this.udpClient.Connect(address, port); } public bool Send(string message) @@ -36,7 +35,7 @@ public bool Send(string[] messages) return this.CoreSend(data); } - + private bool CoreSend(byte[] data) { try diff --git a/source/MSBuild.Graphite/Tasks/Graphite.cs b/source/MSBuild.Graphite/Tasks/Graphite.cs index 3a19b63..88e04eb 100644 --- a/source/MSBuild.Graphite/Tasks/Graphite.cs +++ b/source/MSBuild.Graphite/Tasks/Graphite.cs @@ -24,15 +24,15 @@ public Graphite() public int Port { get; set; } [Required] - public string Transport + public string Transport { get { return this.transport.ToString(); } set { this.transport = (TransportType)Enum.Parse(typeof(TransportType), value); } } - TransportType IGraphiteConfiguration.Transport - { - get { return this.transport; } + TransportType IGraphiteConfiguration.Transport + { + get { return this.transport; } } public string PrefixKey { get; set; } @@ -42,12 +42,14 @@ TransportType IGraphiteConfiguration.Transport public int Value { get; set; } + public TimeSpan Lifetime { get; set; } + public override bool Execute() { using (var channelFactory = new ChannelFactory(this, null)) { IMonitoringChannel channel = channelFactory.CreateChannel( - "gauge", + "gauge", "graphite"); channel.Report(this.Key, this.Value); diff --git a/source/MSBuild.Graphite/Tasks/StatsD.cs b/source/MSBuild.Graphite/Tasks/StatsD.cs index 27f0c70..3a385f6 100644 --- a/source/MSBuild.Graphite/Tasks/StatsD.cs +++ b/source/MSBuild.Graphite/Tasks/StatsD.cs @@ -30,6 +30,8 @@ public StatsD() [Required] public MetricType Type { get; set; } + public TimeSpan Lifetime { get; set; } + public override bool Execute() { using (var channelFactory = new ChannelFactory(null, this)) From e27a2512fb356511bb950726a6d5cdcc61b324f0 Mon Sep 17 00:00:00 2001 From: Kevin Gosse Date: Tue, 21 Nov 2017 10:31:25 +0100 Subject: [PATCH 2/2] Remove C# 7 syntax --- .../Configuration/ConnectionStringConfigurationBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs b/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs index 3554deb..74853ef 100644 --- a/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs +++ b/source/Graphite/Configuration/ConnectionStringConfigurationBase.cs @@ -68,7 +68,9 @@ protected virtual IDictionary Parse(string connectionString) if (values.TryGetValue("lifetime", out value)) { - if (TimeSpan.TryParse(value, out TimeSpan temp)) + TimeSpan temp; + + if (TimeSpan.TryParse(value, out temp)) { this.Lifetime = temp; }