Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions GVFS/GVFS.Common/Maintenance/PrefetchStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ public class PrefetchStep : GitMaintenanceStep
private const int NoExistingPrefetchPacks = -1;
private readonly TimeSpan timeBetweenPrefetches = TimeSpan.FromMinutes(70);

private readonly Action<List<string>> postFetchCallback;

public PrefetchStep(GVFSContext context, GitObjects gitObjects, bool requireCacheLock = true)
: this(context, gitObjects, requireCacheLock, postFetchCallback: null)
{
}

public PrefetchStep(GVFSContext context, GitObjects gitObjects, bool requireCacheLock, Action<List<string>> postFetchCallback)
: base(context, requireCacheLock)
{
this.GitObjects = gitObjects;
this.postFetchCallback = postFetchCallback;
}

public override string Area => "PrefetchStep";
Expand Down Expand Up @@ -283,6 +291,14 @@ private void SchedulePostFetchJob(List<string> packIndexes)
return;
}

// When running inside the mount process, use the injected callback to
// enqueue the post-fetch step directly (avoids re-entrant named pipe IPC).
if (this.postFetchCallback != null)
{
this.postFetchCallback(packIndexes);
return;
}

// We make a best-effort request to run MIDX and commit-graph writes
using (NamedPipeClient pipeClient = new NamedPipeClient(this.Context.Enlistment.NamedPipeName))
{
Expand Down
67 changes: 67 additions & 0 deletions GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,73 @@ public Message CreateMessage()
}
}

public static class PrefetchCommits
{
public const string Request = "PrefetchCommits";
public const string CompleteResult = "PrefetchCommitsComplete";
public const string MountNotReadyResult = "MountNotReady";

public class Response
{
public bool Success { get; set; }
public string Error { get; set; }

public static Response FromMessage(Message message)
{
return GVFSJsonOptions.Deserialize<Response>(message.Body);
}

public Message CreateMessage()
{
return new Message(CompleteResult, GVFSJsonOptions.Serialize(this));
}
}
}

public static class PrefetchBlobs
{
public const string RequestHeader = "PrefetchBlobs";
public const string CompleteResult = "PrefetchBlobsComplete";
public const string MountNotReadyResult = "MountNotReady";

public class Request
{
public List<string> Files { get; set; }
public List<string> Folders { get; set; }
public string HeadCommitId { get; set; }
public bool HydrateFiles { get; set; }

public static Request FromMessage(Message message)
{
return GVFSJsonOptions.Deserialize<Request>(message.Body);
}

public Message CreateMessage()
{
return new Message(RequestHeader, GVFSJsonOptions.Serialize(this));
}
}

public class Response
{
public bool Success { get; set; }
public string Error { get; set; }
public int MatchedBlobCount { get; set; }
public int DownloadedBlobCount { get; set; }
public int HydratedFileCount { get; set; }

public static Response FromMessage(Message message)
{
return GVFSJsonOptions.Deserialize<Response>(message.Body);
}

public Message CreateMessage()
{
return new Message(CompleteResult, GVFSJsonOptions.Serialize(this));
}
}
}

public static class Notification
{
public class Request
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
[TestFixture]
public class PrefetchBlobsOffloadTests : TestsWithEnlistmentPerFixture
{
private FileSystemRunner fileSystem;

public PrefetchBlobsOffloadTests()
{
this.fileSystem = new SystemIORunner();
}

[TestCase, Order(1)]
public void PrefetchBlobsMountedUsesOffload()
{
// With the enlistment mounted, blob prefetch should succeed
// by offloading to the mount process (using its warm auth).
string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}");
output.ShouldContain("Matched blobs:");
output.ShouldContain("Downloaded:");
}

[TestCase, Order(2)]
public void PrefetchBlobsMountedReportsStats()
{
// Prefetch multiple files and verify stats are reported
string output = this.Enlistment.Prefetch(
$"--files {Path.Combine("GVFS", "GVFS", "Program.cs")};{Path.Combine("GVFS", "GVFS.FunctionalTests", "GVFS.FunctionalTests.csproj")}");
output.ShouldContain("Matched blobs:");
output.ShouldContain("Already cached:");
output.ShouldContain("Downloaded:");
}

[TestCase, Order(3)]
public void PrefetchBlobsUnmountedFallsBackToDirectAuth()
{
// Unmount, then blob prefetch should fall back to direct auth
// and still succeed.
this.Enlistment.UnmountGVFS();

try
{
string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}");
output.ShouldContain("Matched blobs:");
output.ShouldContain("Downloaded:");
}
finally
{
this.Enlistment.MountGVFS();
}
}

[TestCase, Order(4)]
public void PrefetchBlobsMountedWithFolders()
{
// Prefetch a folder while mounted
string output = this.Enlistment.Prefetch("--folders GVFS/GVFS");
output.ShouldContain("Matched blobs:");
}

[TestCase, Order(5)]
public void PrefetchBlobsMountedAfterRemount()
{
// After unmount + remount, blob prefetch should work via
// the mount process again.
this.Enlistment.UnmountGVFS();
this.Enlistment.MountGVFS();

string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}");
output.ShouldContain("Matched blobs:");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
[TestFixture]
public class PrefetchCommitsOffloadTests : TestsWithEnlistmentPerFixture
{
private const string PrefetchPackPrefix = "prefetch";

private FileSystemRunner fileSystem;

public PrefetchCommitsOffloadTests()
: base(forcePerRepoObjectCache: true, skipPrefetchDuringClone: true)
{
this.fileSystem = new SystemIORunner();
}

private string PackRoot
{
get
{
return this.Enlistment.GetPackRoot(this.fileSystem);
}
}

[TestCase, Order(1)]
public void PrefetchCommitsMountedUsesOffload()
{
// With the enlistment mounted, prefetch --commits should succeed
// by offloading to the mount process (using its warm auth).
this.Enlistment.Prefetch("--commits");
this.PostFetchJobShouldComplete();

string[] prefetchPacks = this.ReadPrefetchPackFileNames();
prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after mounted prefetch");
this.AllPrefetchPacksShouldHaveIdx(prefetchPacks);
}

[TestCase, Order(2)]
public void PrefetchCommitsMountedIsIdempotent()
{
// Running prefetch --commits again while mounted should succeed
// (may be a no-op if packs are already up to date).
string[] packsBefore = this.ReadPrefetchPackFileNames();

this.Enlistment.Prefetch("--commits");
this.PostFetchJobShouldComplete();

string[] packsAfter = this.ReadPrefetchPackFileNames();
packsAfter.Length.ShouldBeAtLeast(packsBefore.Length, "Pack count should not decrease after idempotent prefetch");
this.AllPrefetchPacksShouldHaveIdx(packsAfter);
}

[TestCase, Order(3)]
public void PrefetchCommitsUnmountedFallsBackToDirectAuth()
{
// Unmount, then prefetch --commits should fall back to direct auth
// and still succeed.
this.Enlistment.UnmountGVFS();

try
{
this.Enlistment.Prefetch("--commits");

string[] prefetchPacks = this.ReadPrefetchPackFileNames();
prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after unmounted prefetch");
this.AllPrefetchPacksShouldHaveIdx(prefetchPacks);
}
finally
{
this.Enlistment.MountGVFS();
}
}

[TestCase, Order(4)]
public void PrefetchCommitsMountedAfterRemount()
{
// After unmount + remount, prefetch --commits should work via
// the mount process again.
this.Enlistment.UnmountGVFS();
this.Enlistment.MountGVFS();

this.Enlistment.Prefetch("--commits");
this.PostFetchJobShouldComplete();

string[] prefetchPacks = this.ReadPrefetchPackFileNames();
prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after remount prefetch");
this.AllPrefetchPacksShouldHaveIdx(prefetchPacks);
}

private string[] ReadPrefetchPackFileNames()
{
return Directory.GetFiles(this.PackRoot, $"{PrefetchPackPrefix}*.pack");
}

private void AllPrefetchPacksShouldHaveIdx(string[] prefetchPacks)
{
foreach (string prefetchPack in prefetchPacks)
{
string idxPath = Path.ChangeExtension(prefetchPack, ".idx");
idxPath.ShouldBeAFile(this.fileSystem);
}
}

private void PostFetchJobShouldComplete()
{
string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem);
string postFetchLock = Path.Combine(objectDir, "git-maintenance-step.lock");

System.Diagnostics.Stopwatch timeout = System.Diagnostics.Stopwatch.StartNew();
while (this.fileSystem.FileExists(postFetchLock))
{
timeout.Elapsed.TotalSeconds.ShouldBeAtMost(60, "Post-fetch lock file was not released within 60 seconds");
System.Threading.Thread.Sleep(500);
}

ProcessResult graphResult = GitProcess.InvokeProcess(
this.Enlistment.RepoRoot,
"commit-graph verify --shallow --object-dir=\"" + objectDir + "\"");
graphResult.ExitCode.ShouldEqual(0);
}
}
}
Loading
Loading