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
6 changes: 6 additions & 0 deletions GVFS/GVFS.Common/Enlistment.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using System;
using System.IO;

Expand Down Expand Up @@ -117,5 +118,10 @@ public virtual GitProcess CreateGitProcess()
{
return new GitProcess(this);
}

public virtual GitProcess CreateGitProcess(ITracer tracer)
{
return new GitProcess(this, tracer);
}
}
}
30 changes: 29 additions & 1 deletion GVFS/GVFS.Common/Git/GitConfigHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace GVFS.Common.Git
{
/// <summary>
/// Helper methods for git config-style file reading and parsing.
/// Helper methods for reading Git config files and parsing config values.
/// </summary>
public static class GitConfigHelper
{
Expand All @@ -30,6 +30,34 @@ public static bool TrySanitizeConfigFileLine(string fileLine, out string sanitiz
return !string.IsNullOrWhiteSpace(sanitizedLine);
}

/// <summary>
/// Parses an integer setting. A null or blank value selects <paramref name="defaultValue"/>.
/// </summary>
public static bool TryParseIntSetting(string configName, string valueString, int defaultValue, int minValue, out int value, out string error)
{
value = defaultValue;
error = string.Empty;

if (string.IsNullOrWhiteSpace(valueString))
{
return true;
}

if (!int.TryParse(valueString, out value))
{
error = string.Format("Misconfigured config setting {0}, could not parse value `{1}` as an int", configName, valueString);
return false;
}

if (value < minValue)
{
error = string.Format("Invalid value {0} for setting {1}, value must be greater than or equal to {2}", value, configName, minValue);
return false;
}

return true;
}

/// <summary>
/// Get the settings for a section in a given config file.
/// </summary>
Expand Down
172 changes: 172 additions & 0 deletions GVFS/GVFS.Common/Git/GitConfigReadRetry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using GVFS.Common.Tracing;
using System;
using System.Threading;

namespace GVFS.Common.Git
{
internal static class GitConfigReadRetry
{
internal const int MaxAttempts = 5;
internal const int InitialDelayMilliseconds = 20;

private static readonly string[] TransientReasons =
{
"Permission denied",
"Device or resource busy",
"being used by another process",
"sharing violation",
};

internal static T InvokeResult<T>(
string configName,
Func<T> readConfig,
Func<T, bool> isTransientFailure,
ITracer tracer,
Action<TimeSpan> delay,
int maxAttempts = MaxAttempts)
{
for (int attempt = 1; ; ++attempt)
{
T result = readConfig();
if (!isTransientFailure(result))
{
return result;
}

if (attempt >= maxAttempts)
{
TraceExhausted(tracer, configName, attempt);
return result;
}

TimeSpan retryDelay = GetRetryDelay(attempt);
TraceRetry(tracer, configName, attempt, maxAttempts, retryDelay);
delay(retryDelay);
}
}

internal static T InvokeCatching<T>(
string configName,
Func<T> readConfig,
Func<Exception, bool> isTransientFailure,
ITracer tracer,
Action<TimeSpan> delay,
int maxAttempts = MaxAttempts)
{
for (int attempt = 1; ; ++attempt)
{
try
{
return readConfig();
}
catch (Exception e) when (isTransientFailure(e))
{
if (attempt >= maxAttempts)
{
TraceExhausted(tracer, configName, attempt);
throw;
}

TimeSpan retryDelay = GetRetryDelay(attempt);
TraceRetry(tracer, configName, attempt, maxAttempts, retryDelay);
delay(retryDelay);
}
}
}

internal static bool IsTransientLibGit2Error(Exception exception)
{
return
exception is LibGit2Exception &&
IsTransientFileAccessError(exception.Message);
}

/// <summary>
/// Both git.exe and libgit2 report a file failure as <c>... '&lt;path&gt;': &lt;reason&gt;</c>.
/// Only the reason after the final <c>': </c> is tested, so a path or key that contains
/// a transient phrase does not match.
/// </summary>
internal static bool IsTransientFileAccessError(string error, string lineMustContain = null)
{
if (string.IsNullOrEmpty(error))
{
return false;
}

foreach (string line in error.Split('\n'))
{
if (lineMustContain != null && !ContainsOrdinalIgnoreCase(line, lineMustContain))
{
continue;
}

int reasonSeparator = line.LastIndexOf("': ", StringComparison.Ordinal);
if (reasonSeparator < 0)
{
continue;
}

string reason = line.Substring(reasonSeparator + 3);
foreach (string transientReason in TransientReasons)
{
if (ContainsOrdinalIgnoreCase(reason, transientReason))
{
return true;
}
}
}

return false;
}

internal static bool ContainsOrdinalIgnoreCase(string value, string expected)
{
return
value != null &&
value.IndexOf(expected, StringComparison.OrdinalIgnoreCase) >= 0;
}

internal static void Delay(TimeSpan delay)
{
Thread.Sleep(delay);
}

private static TimeSpan GetRetryDelay(int failedAttempt)
{
return TimeSpan.FromMilliseconds(InitialDelayMilliseconds * (1 << (failedAttempt - 1)));
}

// A repository URL with embedded credentials can become part of a deprecated cache
// server config key name.
private static string GetTraceableConfigName(string configName)
{
return configName != null && configName.IndexOf('@') >= 0
? "<redacted>"
: configName;
}

private static void TraceRetry(ITracer tracer, string configName, int failedAttempt, int maxAttempts, TimeSpan retryDelay)
{
EventMetadata metadata = new EventMetadata();
metadata.Add("ConfigName", GetTraceableConfigName(configName));
metadata.Add("AttemptNumber", failedAttempt);
metadata.Add("MaxAttempts", maxAttempts);
metadata.Add("RetryDelayMilliseconds", retryDelay.TotalMilliseconds);
tracer.RelatedWarning(
metadata,
"Transient failure while reading git config. Retrying.",
Keywords.Telemetry);
}

private static void TraceExhausted(ITracer tracer, string configName, int attempts)
{
EventMetadata metadata = new EventMetadata();
metadata.Add("ConfigName", GetTraceableConfigName(configName));
metadata.Add("Attempts", attempts);
tracer.RelatedWarning(
metadata,
"Transient failure while reading git config persisted after all attempts.",
Keywords.Telemetry);
}
}
}
Loading
Loading