Skip to content

Commit 563dc59

Browse files
Fix Timer leakage in TimerBasedCredentialRefresher by maintaining only one registration across all connection recoveries
1 parent 5b45d25 commit 563dc59

File tree

1 file changed

+100
-38
lines changed

1 file changed

+100
-38
lines changed

projects/RabbitMQ.Client/client/api/ICredentialsRefresher.cs

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131

3232
using System;
3333
using System.Collections.Concurrent;
34-
using System.Diagnostics.CodeAnalysis;
34+
using System.Collections.Generic;
3535
using System.Diagnostics.Tracing;
3636
using System.Threading.Tasks;
37+
using System.Timers;
38+
3739
namespace RabbitMQ.Client
3840
{
3941
public interface ICredentialsRefresher
@@ -71,74 +73,134 @@ public class TimerBasedCredentialRefresherEventSource : EventSource
7173

7274
public class TimerBasedCredentialRefresher : ICredentialsRefresher
7375
{
74-
private readonly ConcurrentDictionary<ICredentialsProvider, System.Timers.Timer> _registrations = new ConcurrentDictionary<ICredentialsProvider, System.Timers.Timer>();
75-
76+
private readonly IDictionary<ICredentialsProvider, TimerRegistration> _registrations =
77+
new Dictionary<ICredentialsProvider, TimerRegistration>();
78+
private readonly object _lockObj = new();
79+
7680
public ICredentialsProvider Register(ICredentialsProvider provider, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback)
7781
{
7882
if (!provider.ValidUntil.HasValue || provider.ValidUntil.Value.Equals(TimeSpan.Zero))
7983
{
8084
return provider;
8185
}
8286

83-
if (_registrations.TryAdd(provider, scheduleTimer(provider, callback)))
87+
lock (_lockObj)
8488
{
89+
if (_registrations.TryGetValue(provider, out var registration))
90+
{
91+
registration.Callback = callback;
92+
TimerBasedCredentialRefresherEventSource.Log.AlreadyRegistered(provider.Name);
93+
return provider;
94+
}
95+
96+
registration = new TimerRegistration(_lockObj, callback);
97+
_registrations.Add(provider, registration);
98+
registration.ScheduleTimer(provider);
99+
85100
TimerBasedCredentialRefresherEventSource.Log.Registered(provider.Name);
86101
}
87-
else
88-
{
89-
TimerBasedCredentialRefresherEventSource.Log.AlreadyRegistered(provider.Name);
90-
}
91102

92103
return provider;
93104
}
94105

95106
public bool Unregister(ICredentialsProvider provider)
96107
{
97-
if (_registrations.TryRemove(provider, out System.Timers.Timer? timer))
108+
lock (_lockObj)
98109
{
99-
try
110+
if (_registrations.TryGetValue(provider, out var registration))
100111
{
112+
_registrations.Remove(provider);
113+
101114
TimerBasedCredentialRefresherEventSource.Log.Unregistered(provider.Name);
102-
timer.Stop();
103-
}
104-
finally
105-
{
106-
timer.Dispose();
107-
}
108-
return true;
109-
}
110-
else
111-
{
112-
return false;
115+
registration.Dispose();
116+
return true;
117+
}
113118
}
119+
120+
return false;
114121
}
115122

116-
private System.Timers.Timer scheduleTimer(ICredentialsProvider provider, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback)
123+
private class TimerRegistration : IDisposable
117124
{
118-
System.Timers.Timer timer = new System.Timers.Timer();
119-
timer.Interval = provider.ValidUntil!.Value.TotalMilliseconds * (1.0 - (1 / 3.0));
120-
timer.Elapsed += (o, e) =>
125+
126+
private readonly object _lockObj;
127+
private System.Timers.Timer? _timer;
128+
private bool _disposed;
129+
130+
public ICredentialsRefresher.NotifyCredentialRefreshedAsync Callback { get; set; }
131+
132+
public TimerRegistration(object lockObj, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback)
133+
{
134+
_lockObj = lockObj;
135+
Callback = callback;
136+
}
137+
138+
public void ScheduleTimer(ICredentialsProvider provider)
139+
{
140+
if (provider.ValidUntil == null)
141+
{
142+
throw new ArgumentNullException("ValidUntil of " + nameof(provider) + " was null");
143+
}
144+
if (_disposed)
145+
{
146+
throw new InvalidOperationException("Registration already disposed");
147+
}
148+
149+
var newTimer = new Timer();
150+
newTimer.Interval = provider.ValidUntil.Value.TotalMilliseconds * (1.0 - 1 / 3.0);
151+
newTimer.Elapsed += (o, e) =>
152+
{
153+
TimerBasedCredentialRefresherEventSource.Log.TriggeredTimer(provider.Name);
154+
155+
lock (_lockObj)
156+
{
157+
try
158+
{
159+
if (_disposed)
160+
{
161+
// We were waiting and the registration has been disposed in meanwhile
162+
return;
163+
}
164+
165+
provider.Refresh();
166+
ScheduleTimer(provider);
167+
Callback.Invoke(provider.Password != null);
168+
TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, true);
169+
}
170+
catch (Exception)
171+
{
172+
Callback.Invoke(false);
173+
TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, false);
174+
}
175+
}
176+
};
177+
newTimer.Enabled = true;
178+
newTimer.AutoReset = false;
179+
TimerBasedCredentialRefresherEventSource.Log.ScheduledTimer(provider.Name, newTimer.Interval);
180+
_timer = newTimer;
181+
}
182+
183+
public void Dispose()
121184
{
122-
TimerBasedCredentialRefresherEventSource.Log.TriggeredTimer(provider.Name);
185+
if (_disposed)
186+
{
187+
throw new InvalidOperationException("registration already disposed");
188+
}
189+
123190
try
124191
{
125-
provider.Refresh();
126-
scheduleTimer(provider, callback);
127-
callback.Invoke(provider.Password != null);
128-
TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, true);
192+
_timer?.Stop();
193+
_disposed = true;
129194
}
130-
catch (Exception)
195+
finally
131196
{
132-
callback.Invoke(false);
133-
TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, false);
197+
_timer?.Dispose();
198+
_timer = null;
134199
}
200+
}
135201

136-
};
137-
timer.Enabled = true;
138-
timer.AutoReset = false;
139-
TimerBasedCredentialRefresherEventSource.Log.ScheduledTimer(provider.Name, timer.Interval);
140-
return timer;
141202
}
203+
142204
}
143205

144206
class NoOpCredentialsRefresher : ICredentialsRefresher

0 commit comments

Comments
 (0)