Skip to content

Commit 35ea647

Browse files
authored
Tolerate read-only managed dependencies files (#409)
Tolerate read-only managed dependencies files: - When updating the heartbeat (the timestamp on the .used file under a snapshot) fails because the file is read-only, log a warning but proceed. - Make sure the worker does not purge the currently used snapshot even if the heartbeat has not been updated for long time.
1 parent 5525dc8 commit 35ea647

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

src/DependencyManagement/DependencySnapshotPurger.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class DependencySnapshotPurger : IDependencySnapshotPurger, IDisposable
2121
private readonly TimeSpan _oldHeartbeatAgeMargin;
2222
private readonly int _minNumberOfSnapshotsToKeep;
2323

24+
private string _currentlyUsedSnapshotPath;
2425
private Timer _heartbeat;
2526

2627
public DependencySnapshotPurger(
@@ -42,6 +43,8 @@ public DependencySnapshotPurger(
4243
/// </summary>
4344
public void SetCurrentlyUsedSnapshot(string path, ILogger logger)
4445
{
46+
_currentlyUsedSnapshotPath = path;
47+
4548
Heartbeat(path, logger);
4649

4750
_heartbeat = new Timer(
@@ -65,6 +68,7 @@ public void Purge(ILogger logger)
6568
var threshold = DateTime.UtcNow - _heartbeatPeriod - _oldHeartbeatAgeMargin;
6669

6770
var pathSortedByAccessTime = allSnapshotPaths
71+
.Where(path => string.CompareOrdinal(path, _currentlyUsedSnapshotPath) != 0)
6872
.Select(path => Tuple.Create(path, GetSnapshotAccessTimeUtc(path, logger)))
6973
.OrderBy(entry => entry.Item2)
7074
.ToArray();
@@ -108,7 +112,22 @@ internal void Heartbeat(string path, ILogger logger)
108112

109113
if (_storage.SnapshotExists(path))
110114
{
111-
_storage.SetSnapshotAccessTimeToUtcNow(path);
115+
try
116+
{
117+
_storage.SetSnapshotAccessTimeToUtcNow(path);
118+
}
119+
// The files in the snapshot may be read-only in some scenarios, so updating
120+
// the timestamp may fail. However, the snapshot can still be used, and
121+
// we should not prevent function executions because of that.
122+
// So, just log and move on.
123+
catch (IOException e)
124+
{
125+
LogHeartbeatUpdateFailure(logger, path, e);
126+
}
127+
catch (UnauthorizedAccessException e)
128+
{
129+
LogHeartbeatUpdateFailure(logger, path, e);
130+
}
112131
}
113132
}
114133

@@ -140,5 +159,16 @@ private static int GetMinNumberOfSnapshotsToKeep()
140159
{
141160
return PowerShellWorkerConfiguration.GetInt("MDMinNumberOfSnapshotsToKeep") ?? 1;
142161
}
162+
163+
private static void LogHeartbeatUpdateFailure(ILogger logger, string path, Exception exception)
164+
{
165+
var message = string.Format(
166+
PowerShellWorkerStrings.FailedToUpdateManagedDependencySnapshotHeartbeat,
167+
path,
168+
exception.GetType().FullName,
169+
exception.Message);
170+
171+
logger.Log(isUserOnlyLog: false, LogLevel.Warning, message);
172+
}
143173
}
144174
}

src/resources/PowerShellWorkerStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@
256256
<data name="UpdatingManagedDependencySnapshotHeartbeat" xml:space="preserve">
257257
<value>Updating dependencies folder heartbeat for '{0}''.</value>
258258
</data>
259+
<data name="FailedToUpdateManagedDependencySnapshotHeartbeat" xml:space="preserve">
260+
<value>Failed to update dependencies folder heartbeat for '{0}'. Exception {1}: '{2}'.</value>
261+
</data>
259262
<data name="FailedToInstallDependenciesSnapshot" xml:space="preserve">
260263
<value>Failed to install dependencies into '{0}' (installation mode: {1}), removing the folder.</value>
261264
</data>

test/Unit/DependencyManagement/DependencySnapshotPurgerTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ public void DoesNotRemoveRecentlyUsedSnapshot()
7171
_mockStorage.Verify(_ => _.RemoveSnapshot(It.IsAny<string>()), Times.Never);
7272
}
7373

74+
[Fact]
75+
public void DoesNotRemoveCurrentlyUsedSnapshotEvenIfNotAccessedForLongTime()
76+
{
77+
_mockStorage.Setup(_ => _.GetInstalledAndInstallingSnapshots()).Returns(new[] { "snapshot" });
78+
_mockStorage.Setup(_ => _.SnapshotExists("snapshot")).Returns(true);
79+
_mockStorage.Setup(_ => _.GetSnapshotAccessTimeUtc("snapshot")).Returns(DateTime.UtcNow - TimeSpan.FromMinutes(300));
80+
_mockStorage.Setup(_ => _.SetSnapshotAccessTimeToUtcNow("snapshot"));
81+
82+
using (var purger = new DependencySnapshotPurger(
83+
_mockStorage.Object,
84+
heartbeatPeriod: TimeSpan.FromMinutes(10),
85+
oldHeartbeatAgeMargin: TimeSpan.FromMinutes(5),
86+
minNumberOfSnapshotsToKeep: 0))
87+
{
88+
purger.SetCurrentlyUsedSnapshot("snapshot", _mockLogger.Object);
89+
90+
purger.Purge(_mockLogger.Object);
91+
}
92+
93+
_mockStorage.Verify(_ => _.RemoveSnapshot(It.IsAny<string>()), Times.Never);
94+
}
95+
7496
[Fact]
7597
public void RemovesMultipleSnapshotNotUsedForLongTime()
7698
{
@@ -258,6 +280,32 @@ public void Heartbeat_DoesNotUpdateCurrentSnapshotAccessTime_WhenSnapshotDoesNot
258280
_mockStorage.Verify(_ => _.SetSnapshotAccessTimeToUtcNow(It.IsAny<string>()), Times.Never);
259281
}
260282

283+
[Theory]
284+
[MemberData(nameof(GetNonFatalFileSystemExceptions))]
285+
public void Heartbeat_Tolerates_FileAccessFailures(Exception exception)
286+
{
287+
const string snapshotPath = "FakeSnapshotPath";
288+
_mockStorage.Setup(_ => _.SnapshotExists(snapshotPath)).Returns(true);
289+
_mockStorage.Setup(_ => _.SetSnapshotAccessTimeToUtcNow(snapshotPath))
290+
.Throws(exception);
291+
292+
using (var purger = new DependencySnapshotPurger(_mockStorage.Object))
293+
{
294+
purger.Heartbeat(snapshotPath, _mockLogger.Object);
295+
}
296+
297+
_mockLogger.Verify(
298+
_ => _.Log(
299+
false,
300+
LogLevel.Warning,
301+
It.Is<string>(
302+
message => message.Contains(exception.GetType().FullName)
303+
&& message.Contains(exception.Message)
304+
&& message.Contains(snapshotPath)),
305+
null),
306+
Times.AtLeastOnce);
307+
}
308+
261309
[Fact]
262310
public void SetCurrentlyUsedSnapshot_UpdatesCurrentSnapshotAccessTimeImmediately()
263311
{

0 commit comments

Comments
 (0)