diff --git a/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs b/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs index b2a0633..28e530f 100644 --- a/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs +++ b/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs @@ -1254,6 +1254,41 @@ public void CreateRightCompositeStringChildrenWithoutExtraLeaf_ShouldSucceed() }); } + [Fact] + public void SelfReferencingLinkSubstitution_ShouldNotCauseInfiniteLoop() + { + RunTestWithLinks(links => + { + // This test case reproduces the issue from GitHub issue #20 + // The query '((($i: 1 21)) (($i: $s $t) ($i 20)))' was causing OutOfMemoryException + // due to infinite recursion in link creation + + // Act & Assert - this should not throw OutOfMemoryException + var exception = Record.Exception(() => + { + ProcessQuery(links, "((($i: 1 21)) (($i: $s $t) ($i 20)))"); + }); + + // The fix should either: + // 1. Complete successfully without infinite loop, or + // 2. Throw a controlled InvalidOperationException instead of OutOfMemoryException + if (exception != null) + { + Assert.IsType(exception); + Assert.Contains("infinite", exception.Message.ToLower()); + } + + // If no exception was thrown, verify the links were created properly + if (exception == null) + { + var allLinks = GetAllLinks(links); + // We expect some links to be created, but not an infinite number + Assert.True(allLinks.Count > 0 && allLinks.Count < 1000, + $"Expected reasonable number of links (1-999), but got {allLinks.Count}"); + } + }); + } + // Helper methods private static void RunTestWithLinks(Action> testAction, bool enableTracing = false) { diff --git a/Foundation.Data.Doublets.Cli/LinksExtensions.cs b/Foundation.Data.Doublets.Cli/LinksExtensions.cs index c1fda7c..e4f8605 100644 --- a/Foundation.Data.Doublets.Cli/LinksExtensions.cs +++ b/Foundation.Data.Doublets.Cli/LinksExtensions.cs @@ -19,13 +19,42 @@ public static void EnsureCreated(this ILinks links, var max = nonExistentAddresses.Max()!; max = uInt64ToAddressConverter.Convert(TLinkAddress.CreateTruncating(Math.Min(ulong.CreateTruncating(max), ulong.CreateTruncating(links.Constants.InternalReferencesRange.Maximum)))); var createdLinks = new List(); + var seenAddresses = new HashSet(); TLinkAddress createdLink; + var maxIterations = 10000; // More conservative limit to prevent infinite loops + var iterations = 0; + do { createdLink = creator(); + + // Check for infinite loop conditions first + if (iterations++ > maxIterations) + { + throw new InvalidOperationException($"Link creation exceeded maximum iterations ({maxIterations}). This may indicate a circular reference or infinite recursion in the link creation process."); + } + + // Early break if we're in an obvious cycle + if (createdLinks.Count > 0 && seenAddresses.Contains(createdLink) && createdLink != max) + { + // If we've created many links and started seeing repeats (but not the target), likely infinite loop + if (createdLinks.Count > 50) + { + throw new InvalidOperationException($"Link creation appears to be in an infinite loop. Created {createdLinks.Count} links, seeing repeated address {createdLink}, but target {max} not reached."); + } + } + + seenAddresses.Add(createdLink); createdLinks.Add(createdLink); + + // Additional safety: if we've created far more links than the target ID suggests, something is wrong + if (createdLinks.Count > Math.Max(100, (int)(ulong.CreateTruncating(max) * 2))) + { + throw new InvalidOperationException($"Link creation created {createdLinks.Count} links while trying to reach {max}. This suggests infinite recursion."); + } } while (createdLink != max); + for (var i = 0; i < createdLinks.Count; i++) { if (!nonExistentAddresses.Contains(createdLinks[i])) diff --git a/examples/Program.cs b/examples/Program.cs new file mode 100644 index 0000000..d399738 --- /dev/null +++ b/examples/Program.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +class MockLinks +{ + private uint counter = 1; + private Dictionary existingLinks = new Dictionary(); + + public uint Create() + { + // This simulates the infinite loop by always returning the same value + // that never matches the 'max' target + return counter; // Always return 1, never reaching higher values + } + + public bool Exists(uint id) => existingLinks.ContainsKey(id); +} + +class Program +{ + static void Main() + { + Console.WriteLine("Testing LinksExtensions fix for infinite loop..."); + + var mockLinks = new MockLinks(); + var addresses = new uint[] { 5, 10, 15 }; // Try to create these addresses + + try + { + // This would previously cause an infinite loop because mockLinks.Create() + // always returns 1, never reaching the max target of 15 + TestEnsureCreated(mockLinks, addresses); + Console.WriteLine("Test failed - expected InvalidOperationException"); + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"SUCCESS: Caught expected exception: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"FAILURE: Unexpected exception type: {ex.GetType()}: {ex.Message}"); + } + } + + static void TestEnsureCreated(MockLinks mockLinks, uint[] addresses) + { + // Simplified version of the LinksExtensions.EnsureCreated logic + var nonExistentAddresses = new HashSet(); + foreach (var addr in addresses) + { + if (!mockLinks.Exists(addr)) + { + nonExistentAddresses.Add(addr); + } + } + + if (nonExistentAddresses.Count > 0) + { + var max = nonExistentAddresses.Max(); + var createdLinks = new List(); + var seenAddresses = new HashSet(); + uint createdLink; + var maxIterations = 10000; + var iterations = 0; + + do + { + createdLink = mockLinks.Create(); + + // Check for infinite loop conditions first + if (iterations++ > maxIterations) + { + throw new InvalidOperationException($"Link creation exceeded maximum iterations ({maxIterations}). This may indicate a circular reference or infinite recursion in the link creation process."); + } + + // Early break if we're in an obvious cycle + if (createdLinks.Count > 0 && seenAddresses.Contains(createdLink) && createdLink != max) + { + // If we've created many links and started seeing repeats (but not the target), likely infinite loop + if (createdLinks.Count > 50) + { + throw new InvalidOperationException($"Link creation appears to be in an infinite loop. Created {createdLinks.Count} links, seeing repeated address {createdLink}, but target {max} not reached."); + } + } + + seenAddresses.Add(createdLink); + createdLinks.Add(createdLink); + + // Additional safety: if we've created far more links than the target ID suggests, something is wrong + if (createdLinks.Count > Math.Max(100, (int)(max * 2))) + { + throw new InvalidOperationException($"Link creation created {createdLinks.Count} links while trying to reach {max}. This suggests infinite recursion."); + } + } + while (createdLink != max); + } + } +} \ No newline at end of file diff --git a/examples/SimpleTest.csproj b/examples/SimpleTest.csproj new file mode 100644 index 0000000..c76b054 --- /dev/null +++ b/examples/SimpleTest.csproj @@ -0,0 +1,8 @@ + + + Exe + net8.0 + enable + enable + + \ No newline at end of file