using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Text.RegularExpressions;
namespace XFESystem.Collections;
/// <summary>
/// Retries only pure regex operations interrupted by a measured managed GC pause.
/// Active time already spent is deducted from the next match's timeout. Callback
/// replacements and lazy match collections are deliberately never replayed.
/// </summary>
public static class RegexPauseRecovery
{
private static long recoveries, lastPauseTicks, lastActiveTicks;
public static long RecoveryCount => Interlocked.Read(ref recoveries);
public static TimeSpan LastRecoveredPause => TimeSpan.FromTicks(Interlocked.Read(ref lastPauseTicks));
public static TimeSpan LastRecoveredActiveTime => TimeSpan.FromTicks(Interlocked.Read(ref lastActiveTicks));
public static Match Match(Regex regex, string input) => Match(regex, input, regex.RightToLeft ? input.Length : 0);
public static Match Match(Regex regex, string input, int start)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.Match(input, start); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.Match(input, start)); }
}
public static bool IsMatch(Regex regex, string input)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.IsMatch(input); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.IsMatch(input)); }
}
public static string Replace(Regex regex, string input, string replacement, int count)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.Replace(input, replacement, count); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.Replace(input, replacement, count)); }
}
private static T Retry<T>(Regex original, int inputLength, long began, TimeSpan paused, RegexMatchTimeoutException error, Func<Regex, T> execute)
{
var remaining = original.MatchTimeout; TimeSpan totalPause = default, totalActive = default;
for (int attempt = 0; attempt < 2; attempt++)
{
var wall = Stopwatch.GetElapsedTime(began); var pause = GC.GetTotalPauseDuration() - paused;
var active = wall > pause ? wall - pause : TimeSpan.Zero;
totalPause += pause; totalActive += active; remaining -= active;
error.Data["XfeObservedGcPauseMilliseconds"] = pause.TotalMilliseconds;
error.Data["XfeObservedActiveMilliseconds"] = active.TotalMilliseconds;
if (pause <= TimeSpan.Zero || remaining <= TimeSpan.Zero || original.ToString().Length > 4096 || inputLength > 131_072 ||
(original.Options & RegexOptions.IgnoreCase) != 0 && (original.Options & RegexOptions.CultureInvariant) == 0) break;
// The retry receives only the unspent active-time budget, never a
// fresh full timeout. A finite attempt cap also bounds wall time.
began = Stopwatch.GetTimestamp(); paused = GC.GetTotalPauseDuration();
try
{
var result = execute(new Regex(original.ToString(), original.Options, remaining));
Interlocked.Exchange(ref lastPauseTicks, totalPause.Ticks);
Interlocked.Exchange(ref lastActiveTicks, totalActive.Ticks + Stopwatch.GetElapsedTime(began).Ticks);
Interlocked.Increment(ref recoveries);
return result;
}
catch (RegexMatchTimeoutException next) { error = next; }
}
ExceptionDispatchInfo.Capture(error).Throw(); return default!;
}
}
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Text.RegularExpressions;
namespace XFESystem.Collections;
/// <summary>
/// Retries only pure regex operations interrupted by a measured managed GC pause.
/// Active time already spent is deducted from the next match's timeout. Callback
/// replacements and lazy match collections are deliberately never replayed.
/// </summary>
public static class RegexPauseRecovery
{
private static long recoveries, lastPauseTicks, lastActiveTicks;
public static long RecoveryCount => Interlocked.Read(ref recoveries);
public static TimeSpan LastRecoveredPause => TimeSpan.FromTicks(Interlocked.Read(ref lastPauseTicks));
public static TimeSpan LastRecoveredActiveTime => TimeSpan.FromTicks(Interlocked.Read(ref lastActiveTicks));
public static Match Match(Regex regex, string input) => Match(regex, input, regex.RightToLeft ? input.Length : 0);
public static Match Match(Regex regex, string input, int start)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.Match(input, start); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.Match(input, start)); }
}
public static bool IsMatch(Regex regex, string input)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.IsMatch(input); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.IsMatch(input)); }
}
public static string Replace(Regex regex, string input, string replacement, int count)
{
long began = Stopwatch.GetTimestamp(); var pause = GC.GetTotalPauseDuration();
try { return regex.Replace(input, replacement, count); }
catch (RegexMatchTimeoutException error) { return Retry(regex, input.Length, began, pause, error, next => next.Replace(input, replacement, count)); }
}
private static T Retry<T>(Regex original, int inputLength, long began, TimeSpan paused, RegexMatchTimeoutException error, Func<Regex, T> execute)
{
var remaining = original.MatchTimeout; TimeSpan totalPause = default, totalActive = default;
for (int attempt = 0; attempt < 2; attempt++)
{
var wall = Stopwatch.GetElapsedTime(began); var pause = GC.GetTotalPauseDuration() - paused;
var active = wall > pause ? wall - pause : TimeSpan.Zero;
totalPause += pause; totalActive += active; remaining -= active;
error.Data["XfeObservedGcPauseMilliseconds"] = pause.TotalMilliseconds;
error.Data["XfeObservedActiveMilliseconds"] = active.TotalMilliseconds;
if (pause <= TimeSpan.Zero || remaining <= TimeSpan.Zero || original.ToString().Length > 4096 || inputLength > 131_072 ||
(original.Options & RegexOptions.IgnoreCase) != 0 && (original.Options & RegexOptions.CultureInvariant) == 0) break;
// The retry receives only the unspent active-time budget, never a
// fresh full timeout. A finite attempt cap also bounds wall time.
began = Stopwatch.GetTimestamp(); paused = GC.GetTotalPauseDuration();
try
{
var result = execute(new Regex(original.ToString(), original.Options, remaining));
Interlocked.Exchange(ref lastPauseTicks, totalPause.Ticks);
Interlocked.Exchange(ref lastActiveTicks, totalActive.Ticks + Stopwatch.GetElapsedTime(began).Ticks);
Interlocked.Increment(ref recoveries);
return result;
}
catch (RegexMatchTimeoutException next) { error = next; }
}
ExceptionDispatchInfo.Capture(error).Throw(); return default!;
}
}