• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ThreeMammals / Ocelot / 26761608736

01 Jun 2026 02:34PM UTC coverage: 0.0% (-95.4%) from 95.403%
26761608736

Pull #2394

github

web-flow
Merge e39fc0db2 into e4022a7d8
Pull Request #2394: Harden `FileConfigurationPoller` against timer reentrancy and callback thread leaks, and stabilize `TimeoutDelegatingHandler` timeout test

0 of 7112 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs
1
using Microsoft.Extensions.Hosting;
2
using Newtonsoft.Json;
3
using Ocelot.Configuration.Creator;
4
using Ocelot.Configuration.File;
5
using Ocelot.DependencyInjection;
6
using Ocelot.Logging;
7

8
namespace Ocelot.Configuration.Repository;
9

10
/// <summary>
11
/// This hosted service pools periodically (~1 sec) configuration data from the <see cref="IFileConfigurationRepository"/> and propagates data into the <see cref="IInternalConfigurationRepository"/> to be reused across Ocelot services.
12
/// Thus, this service is responsible for getting actual configuration from anywhere and reflect the state to internal Ocelot repo.
13
/// </summary>
14
/// <remarks>
15
/// Feature: <see cref="IOcelotBuilder.AddConfigurationPoller()"/>.<br/>
16
/// Feature PR: <see href="https://github.com/ThreeMammals/Ocelot/pull/157/">157</see>.<br/>
17
/// Note, that the service is reused in Consul service discovery provider.
18
/// </remarks>
19
public class FileConfigurationPoller : IFileConfigurationPoller, IHostedService, IDisposable
20
{
21
    private readonly IOcelotLogger _logger;
22
    private readonly IFileConfigurationRepository _repo;
23
    private string _previousAsJson;
24
    private Timer _timer;
25
    private int _isPolling;
26
    private readonly IFileConfigurationPollerOptions _options;
27
    private readonly IInternalConfigurationRepository _internalConfigRepo;
28
    private readonly IInternalConfigurationCreator _internalConfigCreator;
29

30
    public FileConfigurationPoller(
×
31
        IOcelotLoggerFactory factory,
×
32
        IFileConfigurationRepository repo,
×
33
        IFileConfigurationPollerOptions options,
×
34
        IInternalConfigurationRepository internalConfigRepo,
×
35
        IInternalConfigurationCreator internalConfigCreator)
×
36
    {
×
37
        _internalConfigRepo = internalConfigRepo;
×
38
        _internalConfigCreator = internalConfigCreator;
×
39
        _options = options;
×
40
        _logger = factory.CreateLogger<FileConfigurationPoller>();
×
41
        _repo = repo;
×
42
        _previousAsJson = string.Empty;
×
43
    }
×
44

45
    private void OnTimer(object state)
46
    {
×
47
        Poll(); // PollAsync().GetAwaiter().GetResult(); // TODO This is not good, TimerCallback must be synchronous
×
48
    }
×
49

50
    public async Task StartAsync(CancellationToken cancellationToken)
51
    {
×
52
        if (_timer is not null)
×
53
            return;
×
54

55
        _logger.LogInformation(() => $"{nameof(FileConfigurationPoller)} is starting.");
×
56
        int delay = await _options.DelayAsync(cancellationToken);
×
57
        _timer = new(OnTimer, null, delay, delay); // TODO state could be CancellationToken?
×
58
    }
×
59

60
    public async Task StopAsync(CancellationToken cancellationToken)
61
    {
×
62
        var timer = Interlocked.Exchange(ref _timer, null);
×
63
        if (timer is null)
×
64
            return;
×
65

66
        _logger.LogInformation(() => $"{nameof(FileConfigurationPoller)} is stopping.");
×
67
        timer.Change(Timeout.Infinite, Timeout.Infinite);
×
68
        await DisposeTimerAsync(timer, cancellationToken);
×
69
    }
×
70

71
    public void Poll()
72
    {
×
73
        if (!TryEnterPolling())
×
74
            return;
×
75
        try
76
        {
×
77
            _logger.LogInformation(() => $"{nameof(Poll)}: Started polling");
×
78

79
            FileConfiguration configuration;
80
            try
81
            {
×
82
                configuration = _repo.Get();
×
83
            }
×
84
            catch (Exception e)
×
85
            {
×
86
                _logger.LogWarning(() => $"{nameof(Poll)}: Error getting {nameof(FileConfiguration)} -> {e}.");
×
87
                return;
×
88
            }
89

90
            if (configuration is null)
×
91
            {
×
92
                _logger.LogWarning(() => $"{nameof(Poll)}: Null object while getting {nameof(FileConfiguration)} via the {_repo.GetType().Name} service.");
×
93
                return;
×
94
            }
95

96
            var asJson = ToJson(configuration);
×
97
            if (asJson != _previousAsJson)
×
98
            {
×
99
                var config = _internalConfigCreator.Create(configuration).GetAwaiter().GetResult(); // TODO Extend interface with sync version
×
100
                if (!config.IsError)
×
101
                    _internalConfigRepo.AddOrReplace(config.Data);
×
102

103
                _previousAsJson = asJson;
×
104
            }
×
105

106
            _logger.LogInformation(() => $"{nameof(Poll)}: Finished polling");
×
107
        }
×
108
        finally
109
        {
×
110
            ExitPolling();
×
111
        }
×
112
    }
×
113

114
    public async Task PollAsync(CancellationToken cancellationToken = default)
115
    {
×
116
        if (!TryEnterPolling())
×
117
            return;
×
118
        try
119
        {
×
120
            _logger.LogInformation(() => $"{nameof(PollAsync)}: Started polling");
×
121

122
            FileConfiguration configuration;
123
            try
124
            {
×
125
                configuration = await _repo.GetAsync(cancellationToken);
×
126
            }
×
127
            catch (Exception e)
×
128
            {
×
129
                _logger.LogWarning(() => $"{nameof(PollAsync)}: Error getting {nameof(FileConfiguration)} -> {e}.");
×
130
                return;
×
131
            }
132

133
            if (configuration is null)
×
134
            {
×
135
                _logger.LogWarning(() => $"{nameof(PollAsync)}: Null object while getting {nameof(FileConfiguration)} via the {_repo.GetType().Name} service.");
136
                return;
×
137
            }
138

139
            var asJson = ToJson(configuration);
×
140
            if (asJson != _previousAsJson)
×
141
            {
×
142
                var config = await _internalConfigCreator.Create(configuration);
×
143
                if (!config.IsError)
×
144
                    _internalConfigRepo.AddOrReplace(config.Data);
×
145

146
                _previousAsJson = asJson;
×
147
            }
×
148

149
            _logger.LogInformation(() => $"{nameof(PollAsync)}: Finished polling");
×
150
        }
×
151
        finally
152
        {
×
153
            ExitPolling();
×
154
        }
×
155
    }
×
156

157
    /// <summary>
158
    /// We could do object comparison here but performance isnt really a problem. This might be an issue one day!.
159
    /// </summary>
160
    /// <returns>hash of the config.</returns>
161
    private static string ToJson(FileConfiguration config)
162
    {
×
163
        var currentHash = JsonConvert.SerializeObject(config); // TODO WTF?
×
164
        return currentHash;
×
165
    }
×
166

167
    public void Dispose()
168
    {
×
169
        var timer = Interlocked.Exchange(ref _timer, null);
×
170
        if (timer is not null)
×
171
            DisposeTimer(timer);
×
172
        GC.SuppressFinalize(this);
×
173
    }
×
174

175
    private bool TryEnterPolling() => Interlocked.CompareExchange(ref _isPolling, 1, 0) == 0;
×
176

177
    private void ExitPolling() => Volatile.Write(ref _isPolling, 0);
×
178

179
    private static Task DisposeTimerAsync(Timer timer, CancellationToken cancellationToken)
180
    {
×
181
        using var disposed = new ManualResetEvent(false);
×
182
        if (!timer.Dispose(disposed))
×
183
            return Task.CompletedTask;
×
184

185
        while (!disposed.WaitOne(10))
×
186
            cancellationToken.ThrowIfCancellationRequested();
×
187

188
        return Task.CompletedTask;
×
189
    }
×
190

191
    private static void DisposeTimer(Timer timer)
192
    {
×
193
        using var disposed = new ManualResetEvent(false);
×
194
        if (!timer.Dispose(disposed))
×
195
            return;
×
196

197
        disposed.WaitOne();
×
198
    }
×
199
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc