Skip to content

Commit 3b9e7de

Browse files
committed
Filter out late file system events caused by API push (#72)
Un-suppress file system events before releasing the lock. Add two configuration values which allow control of automatic cache rebuild. Add a concurrency integration test to ensure cache rebuilds don't occur from concurrent pushes. Address NuGet/NuGetGallery#6960.
1 parent c595803 commit 3b9e7de

15 files changed

Lines changed: 452 additions & 33 deletions

File tree

samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ namespace NuGet.Server.V2.Samples.OwinHost
88
{
99
public class DictionarySettingsProvider : ISettingsProvider
1010
{
11-
readonly Dictionary<string, object> _settings;
11+
private readonly Dictionary<string, object> _settings;
1212

1313
public DictionarySettingsProvider(Dictionary<string, object> settings)
1414
{
1515
_settings = settings;
1616
}
1717

18-
1918
public bool GetBoolSetting(string key, bool defaultValue)
2019
{
21-
System.Diagnostics.Debug.WriteLine("getSetting: " + key);
2220
return _settings.ContainsKey(key) ? Convert.ToBoolean(_settings[key]) : defaultValue;
21+
}
2322

23+
public int GetIntSetting(string key, int defaultValue)
24+
{
25+
return _settings.ContainsKey(key) ? Convert.ToInt32(_settings[key]) : defaultValue;
2426
}
2527

2628
public string GetStringSetting(string key, string defaultValue)

src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ public bool GetBoolSetting(string key, bool defaultValue)
99
return defaultValue;
1010
}
1111

12+
public int GetIntSetting(string key, int defaultValue)
13+
{
14+
return defaultValue;
15+
}
16+
1217
public string GetStringSetting(string key, string defaultValue)
1318
{
1419
return defaultValue;

src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public interface ISettingsProvider
66
{
77
bool GetBoolSetting(string key, bool defaultValue);
88
string GetStringSetting(string key, string defaultValue);
9+
int GetIntSetting(string key, int defaultValue);
910
}
1011
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
6+
namespace NuGet.Server.Core.Infrastructure
7+
{
8+
public static class KnownPathUtility
9+
{
10+
/// <summary>
11+
/// Determines if a relative file path could have been generated by <see cref="ExpandedPackageRepository"/>.
12+
/// The path is assumed to be relative to the base of the package directory.
13+
/// </summary>
14+
/// <param name="path">The file path to parse.</param>
15+
/// <param name="id">The package ID found.</param>
16+
/// <param name="version">The package version found.</param>
17+
/// <returns>True if the file name is known.</returns>
18+
public static bool TryParseFileName(string path, out string id, out SemanticVersion version)
19+
{
20+
id = null;
21+
version = null;
22+
23+
if (path == null || Path.IsPathRooted(path))
24+
{
25+
return false;
26+
}
27+
28+
var pathPieces = path.Split(Path.DirectorySeparatorChar);
29+
if (pathPieces.Length != 3) // {id}\{version}\{file name}
30+
{
31+
return false;
32+
}
33+
34+
id = pathPieces[pathPieces.Length - 3];
35+
var unparsedVersion = pathPieces[pathPieces.Length - 2];
36+
var fileName = pathPieces[pathPieces.Length - 1];
37+
38+
if (!SemanticVersion.TryParse(unparsedVersion, out version)
39+
|| version.ToNormalizedString() != unparsedVersion)
40+
{
41+
return false;
42+
}
43+
44+
string expectedFileName;
45+
if (fileName.EndsWith(NuGet.Constants.PackageExtension))
46+
{
47+
expectedFileName = $"{id}.{version}{NuGet.Constants.PackageExtension}";
48+
}
49+
else if (fileName.EndsWith(NuGet.Constants.HashFileExtension))
50+
{
51+
expectedFileName = $"{id}.{version}{NuGet.Constants.HashFileExtension}";
52+
}
53+
else if (fileName.EndsWith(NuGet.Constants.ManifestExtension))
54+
{
55+
expectedFileName = $"{id}{NuGet.Constants.ManifestExtension}";
56+
}
57+
else
58+
{
59+
return false;
60+
}
61+
62+
return expectedFileName == fileName;
63+
}
64+
}
65+
}

src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,35 @@ internal ServerPackageRepository(
108108

109109
private string CacheFileName => _settingsProvider.GetStringSetting("cacheFileName", null);
110110

111+
private TimeSpan InitialCacheRebuildAfter
112+
{
113+
get
114+
{
115+
var value = GetPositiveIntSetting("initialCacheRebuildAfterSeconds", 15);
116+
return TimeSpan.FromSeconds(value);
117+
}
118+
}
119+
120+
private TimeSpan CacheRebuildFrequency
121+
{
122+
get
123+
{
124+
int value = GetPositiveIntSetting("cacheRebuildFrequencyInMinutes", 60);
125+
return TimeSpan.FromMinutes(value);
126+
}
127+
}
128+
129+
private int GetPositiveIntSetting(string name, int defaultValue)
130+
{
131+
var value = _settingsProvider.GetIntSetting(name, defaultValue);
132+
if (value <= 0)
133+
{
134+
value = defaultValue;
135+
}
136+
137+
return value;
138+
}
139+
111140
private ServerPackageCache InitializeServerPackageCache()
112141
{
113142
return new ServerPackageCache(_fileSystem, ResolveCacheFileName());
@@ -529,18 +558,21 @@ private void SetupBackgroundJobs()
529558
_logger.Log(LogLevel.Info, "Registering background jobs...");
530559

531560
// Persist to package store at given interval (when dirty)
561+
_logger.Log(LogLevel.Info, "Persisting the cache file every 1 minute.");
532562
_persistenceTimer = new Timer(
533563
callback: state => _serverPackageCache.PersistIfDirty(),
534564
state: null,
535565
dueTime: TimeSpan.FromMinutes(1),
536566
period: TimeSpan.FromMinutes(1));
537567

538-
// Rebuild the package store in the background (every hour)
568+
// Rebuild the package store in the background
569+
_logger.Log(LogLevel.Info, "Rebuilding the cache file for the first time after {0} second(s).", InitialCacheRebuildAfter.TotalSeconds);
570+
_logger.Log(LogLevel.Info, "Rebuilding the cache file every {0} hour(s).", CacheRebuildFrequency.TotalHours);
539571
_rebuildTimer = new Timer(
540572
callback: state => RebuildPackageStoreAsync(CancellationToken.None),
541573
state: null,
542-
dueTime: TimeSpan.FromSeconds(15),
543-
period: TimeSpan.FromHours(1));
574+
dueTime: InitialCacheRebuildAfter,
575+
period: CacheRebuildFrequency);
544576

545577
_logger.Log(LogLevel.Info, "Finished registering background jobs.");
546578
}
@@ -595,7 +627,6 @@ private void UnregisterFileSystemWatcher()
595627
_watchDirectory = null;
596628
}
597629

598-
599630
/// <summary>
600631
/// This is an event handler for background work. Therefore, it should never throw exceptions.
601632
/// </summary>
@@ -608,6 +639,12 @@ private async void FileSystemChangedAsync(object sender, FileSystemEventArgs e)
608639
return;
609640
}
610641

642+
if (ShouldIgnoreFileSystemEvent(e))
643+
{
644+
_logger.Log(LogLevel.Verbose, "File system event ignored. File: {0} - Change: {1}", e.Name, e.ChangeType);
645+
return;
646+
}
647+
611648
_logger.Log(LogLevel.Verbose, "File system changed. File: {0} - Change: {1}", e.Name, e.ChangeType);
612649

613650
var changedDirectory = Path.GetDirectoryName(e.FullPath);
@@ -642,6 +679,59 @@ private async void FileSystemChangedAsync(object sender, FileSystemEventArgs e)
642679
}
643680
}
644681

682+
private bool ShouldIgnoreFileSystemEvent(FileSystemEventArgs e)
683+
{
684+
// We can only ignore Created or Changed events. All other types are always processed. Eventually we could
685+
// try to ignore some Deleted events in the case of API package delete, but this is harder.
686+
if (e.ChangeType != WatcherChangeTypes.Created
687+
&& e.ChangeType != WatcherChangeTypes.Changed)
688+
{
689+
_logger.Log(LogLevel.Verbose, "The file system event change type is not ignorable.");
690+
return false;
691+
}
692+
693+
/// We can only ignore events related to file paths changed by the
694+
/// <see cref="ExpandedPackageRepository"/>. If the file system event is representing a known file path
695+
/// extracted during package push, we can ignore the event. File system events are supressed during package
696+
/// push but this is still necessary since file system events can come some time after the suppression
697+
/// window has ended.
698+
if (!KnownPathUtility.TryParseFileName(e.Name, out var id, out var version))
699+
{
700+
_logger.Log(LogLevel.Verbose, "The file system event is not related to a known package path.");
701+
return false;
702+
}
703+
704+
/// The file path could have been generated by <see cref="ExpandedPackageRepository"/>. Now
705+
/// determine if the package is in the cache.
706+
var matchingPackage = _serverPackageCache
707+
.GetAll()
708+
.Where(p => StringComparer.OrdinalIgnoreCase.Equals(p.Id, id))
709+
.Where(p => version.Equals(p.Version))
710+
.FirstOrDefault();
711+
712+
if (matchingPackage == null)
713+
{
714+
_logger.Log(LogLevel.Verbose, "The file system event is not related to a known package.");
715+
return false;
716+
}
717+
718+
var fileInfo = new FileInfo(e.FullPath);
719+
if (!fileInfo.Exists)
720+
{
721+
_logger.Log(LogLevel.Verbose, "The package file is missing.");
722+
return false;
723+
}
724+
725+
var minimumCreationTime = DateTimeOffset.UtcNow.AddMinutes(-1);
726+
if (fileInfo.CreationTimeUtc < minimumCreationTime)
727+
{
728+
_logger.Log(LogLevel.Verbose, "The package file was not created recently.");
729+
return false;
730+
}
731+
732+
return true;
733+
}
734+
645735
private async Task<Lock> LockAsync(CancellationToken token)
646736
{
647737
var handle = new Lock(_syncLock);
@@ -710,8 +800,8 @@ public void Dispose()
710800
{
711801
if (_lockHandle != null && _lockHandle.LockTaken)
712802
{
713-
_lockHandle.Dispose();
714803
_repository._isFileSystemWatcherSuppressed = false;
804+
_lockHandle.Dispose();
715805
}
716806
}
717807
}

src/NuGet.Server.Core/NuGet.Server.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
<Compile Include="Infrastructure\ClientCompatibility.cs" />
7676
<Compile Include="Infrastructure\ClientCompatibilityFactory.cs" />
7777
<Compile Include="Infrastructure\DefaultSettingsProvider.cs" />
78+
<Compile Include="Infrastructure\KnownPathUtility.cs" />
7879
<Compile Include="Infrastructure\SerializedServerPackages.cs" />
7980
<Compile Include="Infrastructure\ServerPackageStore.cs" />
8081
<Compile Include="Infrastructure\IServerPackageStore.cs" />

src/NuGet.Server/Core/DefaultServiceResolver.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace NuGet.Server
1313
public sealed class DefaultServiceResolver
1414
: IServiceResolver, IDisposable
1515
{
16+
private readonly Core.Logging.ILogger _logger;
1617
private readonly CryptoHashProvider _hashProvider;
1718
private readonly ServerPackageRepository _packageRepository;
1819
private readonly PackageAuthenticationService _packageAuthenticationService;
@@ -24,20 +25,33 @@ public DefaultServiceResolver() : this(
2425
{
2526
}
2627

27-
public DefaultServiceResolver(string packagePath, NameValueCollection settings)
28+
public DefaultServiceResolver(string packagePath, NameValueCollection settings) : this(
29+
packagePath,
30+
settings,
31+
new TraceLogger())
2832
{
33+
}
34+
35+
public DefaultServiceResolver(string packagePath, NameValueCollection settings, Core.Logging.ILogger logger)
36+
{
37+
_logger = logger;
38+
2939
_hashProvider = new CryptoHashProvider(Core.Constants.HashAlgorithm);
3040

3141
_settingsProvider = new WebConfigSettingsProvider(settings);
3242

33-
_packageRepository = new ServerPackageRepository(packagePath, _hashProvider, _settingsProvider, new TraceLogger());
43+
_packageRepository = new ServerPackageRepository(packagePath, _hashProvider, _settingsProvider, _logger);
3444

3545
_packageAuthenticationService = new PackageAuthenticationService(settings);
36-
3746
}
3847

3948
public object Resolve(Type type)
4049
{
50+
if (type == typeof(Core.Logging.ILogger))
51+
{
52+
return _logger;
53+
}
54+
4155
if (type == typeof(IHashProvider))
4256
{
4357
return _hashProvider;

src/NuGet.Server/Infrastructure/WebConfigSettingsProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public bool GetBoolSetting(string key, bool defaultValue)
2929
return !bool.TryParse(settings[key], out value) ? defaultValue : value;
3030
}
3131

32+
public int GetIntSetting(string key, int defaultValue)
33+
{
34+
var settings = _getSettings();
35+
int value;
36+
return !int.TryParse(settings[key], out value) ? defaultValue : value;
37+
}
38+
3239
public string GetStringSetting(string key, string defaultValue)
3340
{
3441
var settings = _getSettings();

src/NuGet.Server/Web.config

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,31 @@
6262
Uncomment the following configuration entry to enable NAT support.
6363
-->
6464
<!-- <add key="aspnet:UseHostHeaderForRequestUrl" value="true" /> -->
65+
6566
<!--
6667
Set enableFileSystemMonitoring to true (default) to enable file system monitoring (which will update the package cache appropriately on file system changes).
6768
Set it to false to disable file system monitoring.
6869
NOTE: Disabling file system monitoring may result in increased storage capacity requirements as package cache may only be purged by a background job running
6970
on a fixed 1-hour interval.
7071
-->
7172
<add key="enableFileSystemMonitoring" value="true" />
72-
<!--Set allowRemoteCacheManagement to true to enable the "clear cache" and other cache operations initiated via requests originating from remote hosts.-->
73+
74+
<!--
75+
Set allowRemoteCacheManagement to true to enable the "clear cache" and other cache operations initiated via requests originating from remote hosts.
76+
-->
7377
<add key="allowRemoteCacheManagement" value="false" />
78+
79+
<!--
80+
Set initialCacheRebuildAfterSeconds to the number of seconds to wait before starting the cache rebuild timer.
81+
Defaults to 15 seconds if excluded or an invalid value.
82+
-->
83+
<add key="initialCacheRebuildAfterSeconds" value="15" />
84+
85+
<!--
86+
Set cacheRebuildFrequencyInMinutes to the frequency in minutes to rebuild the cache. Defaults to 60 minutes if
87+
excluded or an invalid value.
88+
-->
89+
<add key="cacheRebuildFrequencyInMinutes" value="60" />
7490
</appSettings>
7591
<!--
7692
For a description of web.config changes see http://go.microsoft.com/fwlink/?LinkId=235367.

test/NuGet.Server.Core.Tests/Infrastructure/FuncSettingsProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public bool GetBoolSetting(string key, bool defaultValue)
2323
return Convert.ToBoolean(_getSetting(key, defaultValue));
2424
}
2525

26+
public int GetIntSetting(string key, int defaultValue)
27+
{
28+
return Convert.ToInt32(_getSetting(key, defaultValue));
29+
}
30+
2631
public string GetStringSetting(string key, string defaultValue)
2732
{
2833
return Convert.ToString(_getSetting(key, defaultValue));

0 commit comments

Comments
 (0)