From ec01107b027024655eb92d8e97572a94f58d8cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 21 Feb 2024 13:48:53 +0100 Subject: [PATCH 1/3] tmp: try to reproduce extension method load instability --- src/Samples/AspNetCore/Startup.cs | 13 +- .../DotvvmServiceConfigurator.cs | 2 +- src/Samples/AspNetCoreLatest/Startup.cs | 15 +- .../Common/DotVVM.Samples.Common.csproj | 158 +----------------- src/Tools/AppStartupInstabilityTester.py | 49 ++++++ 5 files changed, 76 insertions(+), 161 deletions(-) create mode 100644 src/Tools/AppStartupInstabilityTester.py diff --git a/src/Samples/AspNetCore/Startup.cs b/src/Samples/AspNetCore/Startup.cs index 4c569954b1..097957df9d 100644 --- a/src/Samples/AspNetCore/Startup.cs +++ b/src/Samples/AspNetCore/Startup.cs @@ -94,6 +94,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF } private string GetApplicationPath(IWebHostEnvironment env) - => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + { + var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + if (Directory.Exists(common)) + { + return common; + } + if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml"))) + { + return env.ContentRootPath; + } + throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root."); + } } } diff --git a/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs b/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs index eef93199fe..32bb31cbc5 100644 --- a/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs +++ b/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs @@ -10,7 +10,7 @@ public void ConfigureServices(IDotvvmServiceCollection services) { CommonConfiguration.ConfigureServices(services); services.AddDefaultTempStorages("Temp"); - services.AddHotReload(); + // services.AddHotReload(); } } } diff --git a/src/Samples/AspNetCoreLatest/Startup.cs b/src/Samples/AspNetCoreLatest/Startup.cs index cb24824f6f..b308c9cbb6 100644 --- a/src/Samples/AspNetCoreLatest/Startup.cs +++ b/src/Samples/AspNetCoreLatest/Startup.cs @@ -99,12 +99,23 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseStaticFiles(); app.UseEndpoints(endpoints => { - endpoints.MapDotvvmHotReload(); + // endpoints.MapDotvvmHotReload(); endpoints.MapMetrics(); // prometheus metrics on /metrics }); } private string GetApplicationPath(IWebHostEnvironment env) - => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + { + if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml"))) + { + return env.ContentRootPath; + } + var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + if (File.Exists(Path.Combine(common, "Views/Default.dothtml"))) + { + return common; + } + throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root."); + } } } diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 216f55da77..41bcf2a7ef 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -19,167 +19,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Tools/AppStartupInstabilityTester.py b/src/Tools/AppStartupInstabilityTester.py new file mode 100644 index 0000000000..80e6f77f32 --- /dev/null +++ b/src/Tools/AppStartupInstabilityTester.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import subprocess, requests, os, time, argparse + +parser = argparse.ArgumentParser(description="Repeatedly starts the server and every time checks if some pages are working, use to find startup-time race condition bugs") +parser.add_argument("--port", type=int, default=16017, help="Port to run the server on") +parser.add_argument("--working-directory", type=str, default=".", help="Working directory to run the server in") +parser.add_argument("--server-path", type=str, default="bin/Debug/net8.0/DotVVM.Samples.BasicSamples.AspNetCoreLatest", help="Path to the server executable") +parser.add_argument("--environment", type=str, default="Development", help="Asp.Net Core environment (Development, Production)") +args = parser.parse_args() + +port = args.port + +def server_start() -> subprocess.Popen: + """Starts the server and returns the process object""" + server = subprocess.Popen([ + args.server_path, "--environment", args.environment, "--urls", f"http://localhost:{port}"], + cwd=args.working_directory, + ) + return server + +def req(path): + try: + response = requests.get(f"http://localhost:{port}{path}") + return response.status_code + except requests.exceptions.ConnectionError: + return None + +iteration = 0 +while True: + iteration += 1 + print(f"Starting iteration {iteration}") + server = server_start() + time.sleep(0.1) + while req("/") is None: + time.sleep(0.1) + + probes = [ + req("/"), + req("/FeatureSamples/LambdaExpressions/StaticCommands"), + req("/FeatureSamples/LambdaExpressions/ClientSideFiltering"), + req("/FeatureSamples/LambdaExpressions/LambdaExpressions") + ] + if set(probes) != {200}: + print(f"Iteration {iteration} failed: {probes}") + time.sleep(100000000) + + server.terminate() + server.wait() From 4cd20f5be8a045d57cbef639c0805c06cd549f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 25 Feb 2024 11:58:16 +0100 Subject: [PATCH 2/3] Added dump of extension methods to the test --- src/Samples/Common/DotvvmStartup.cs | 3 ++ .../DumpExtensionMethodsPresenter.cs | 45 +++++++++++++++++++ .../Tests/Tests/Feature/StaticCommandTests.cs | 25 ++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index fc32374ecc..9172d44334 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -31,6 +31,7 @@ using DotVVM.Samples.Common.ViewModels.FeatureSamples.BindingVariables; using DotVVM.Samples.Common.Views.ControlSamples.TemplateHost; using DotVVM.Framework.ResourceManagement; +using DotVVM.Samples.Common.Presenters; using DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; namespace DotVVM.Samples.BasicSamples @@ -251,6 +252,8 @@ private static void AddRoutes(DotvvmConfiguration config) config.RouteTable.Add("FeatureSamples_PostBack_PostBackHandlers_Localized", "FeatureSamples/PostBack/PostBackHandlers_Localized", "Views/FeatureSamples/PostBack/ConfirmPostBackHandler.dothtml", presenterFactory: LocalizablePresenter.BasedOnQuery("lang")); config.RouteTable.Add("Errors_UndefinedRouteLinkParameters-PageDetail", "Erros/UndefinedRouteLinkParameters/{Id}", "Views/Errors/UndefinedRouteLinkParameters.dothtml", new { Id = 0 }); + + config.RouteTable.Add("DumpExtensionsMethods", "dump-extension-methods", _ => new DumpExtensionMethodsPresenter()); } private static void AddControls(DotvvmConfiguration config) diff --git a/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs new file mode 100644 index 0000000000..bb6308547e --- /dev/null +++ b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace DotVVM.Samples.Common.Presenters +{ + public class DumpExtensionMethodsPresenter : IDotvvmPresenter + { + public async Task ProcessRequest(IDotvvmRequestContext context) + { + var cache = context.Configuration.ServiceProvider.GetService(); + + var contents = typeof(ExtensionMethodsCache) + .GetField("methodsCache", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(cache) as ConcurrentDictionary>; + + var dump = contents.SelectMany(p => p.Value.Select(m => new { + Namespace = p.Key, + m.Name, + m.DeclaringType!.FullName, + Params = m.GetParameters().Select(p => new { + p.Name, + Type = p.ParameterType!.FullName + }), + m.IsGenericMethodDefinition, + GenericParameters = m.IsGenericMethodDefinition ? m.GetGenericArguments().Select(a => new { + a.Name + }) : null + })) + .OrderBy(m => m.Namespace).ThenBy(m => m.Name); + + await context.HttpContext.Response.WriteAsync("ExtensionMethodsCache dump: " + JsonConvert.SerializeObject(dump)); + } + } +} diff --git a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs index 164bf1fa71..3900fcf6d5 100644 --- a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs +++ b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; +using System.Threading.Tasks; using DotVVM.Samples.Tests.Base; using DotVVM.Testing.Abstractions; using OpenQA.Selenium; @@ -816,16 +819,34 @@ public void Feature_List_Translation_Remove_Range() } [Fact] - public void Feature_List_Translation_Remove_Reverse() + public async Task Feature_List_Translation_Remove_Reverse() { + var wasError = false; + RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_JavascriptTranslation_ListMethodTranslations); + browser.Wait(1000); + if (browser.FindElements(".exceptionMessage").Count > 0) + { + wasError = true; + return; + } + var rows = GetSortedRow(browser, "Reverse"); var column = GetColumnContent(rows, 0); browser.WaitFor(() => Assert.Equal(10, column.Count), 500); Assert.Equal(new List { "10", "9", "8", "7", "6", "5", "4", "3", "2", "1" }, column); }); + + if (wasError) + { + // error page - extension methods not found + var client = new HttpClient(); + var json = await client.GetStringAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/dump-extension-methods"); + TestOutput.WriteLine(json); + throw new Exception("Test failed"); + } } protected IElementWrapperCollection GetSortedRow(IBrowserWrapper browser, string btn) From e0fd59dac8f0d3ab81ab9cce2e07b1d6c928f59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 23 Mar 2024 17:27:13 +0100 Subject: [PATCH 3/3] Isolated test case and output if the error returns --- .../DotvvmServiceConfigurator.cs | 2 +- src/Samples/AspNetCoreLatest/Startup.cs | 2 +- .../DumpExtensionMethodsPresenter.cs | 2 +- .../Tests/Tests/Feature/StaticCommandTests.cs | 36 ++++++++++--------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs b/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs index 32bb31cbc5..eef93199fe 100644 --- a/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs +++ b/src/Samples/AspNetCoreLatest/DotvvmServiceConfigurator.cs @@ -10,7 +10,7 @@ public void ConfigureServices(IDotvvmServiceCollection services) { CommonConfiguration.ConfigureServices(services); services.AddDefaultTempStorages("Temp"); - // services.AddHotReload(); + services.AddHotReload(); } } } diff --git a/src/Samples/AspNetCoreLatest/Startup.cs b/src/Samples/AspNetCoreLatest/Startup.cs index b308c9cbb6..4342847d57 100644 --- a/src/Samples/AspNetCoreLatest/Startup.cs +++ b/src/Samples/AspNetCoreLatest/Startup.cs @@ -99,7 +99,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseStaticFiles(); app.UseEndpoints(endpoints => { - // endpoints.MapDotvvmHotReload(); + endpoints.MapDotvvmHotReload(); endpoints.MapMetrics(); // prometheus metrics on /metrics }); } diff --git a/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs index bb6308547e..6b418fb287 100644 --- a/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs +++ b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs @@ -39,7 +39,7 @@ public async Task ProcessRequest(IDotvvmRequestContext context) })) .OrderBy(m => m.Namespace).ThenBy(m => m.Name); - await context.HttpContext.Response.WriteAsync("ExtensionMethodsCache dump: " + JsonConvert.SerializeObject(dump)); + await context.HttpContext.Response.WriteAsync("ExtensionMethodsCache dump: " + JsonConvert.SerializeObject(dump, Formatting.Indented)); } } } diff --git a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs index 3900fcf6d5..3f9807656a 100644 --- a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs +++ b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -817,35 +818,38 @@ public void Feature_List_Translation_Remove_Range() Assert.Equal(new List { "1", "2", "8", "9", "10" }, column); }); } - + [Fact] - public async Task Feature_List_Translation_Remove_Reverse() + public void Feature_List_Translation_Remove_Reverse() { - var wasError = false; - RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_JavascriptTranslation_ListMethodTranslations); - browser.Wait(1000); - if (browser.FindElements(".exceptionMessage").Count > 0) - { - wasError = true; - return; - } - var rows = GetSortedRow(browser, "Reverse"); var column = GetColumnContent(rows, 0); browser.WaitFor(() => Assert.Equal(10, column.Count), 500); Assert.Equal(new List { "10", "9", "8", "7", "6", "5", "4", "3", "2", "1" }, column); }); + } + + [Fact] + public async Task Feature_ExtensionMethodsNotResolvedOnStartup() + { + var client = new HttpClient(); + + // try to visit the page + var pageResponse = await client.GetAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/" + SamplesRouteUrls.FeatureSamples_JavascriptTranslation_ListMethodTranslations); + TestOutput.WriteLine($"Page response: {(int)pageResponse.StatusCode}"); + var wasError = pageResponse.StatusCode != HttpStatusCode.OK; + // dump extension methods on the output + var json = await client.GetStringAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/dump-extension-methods"); + TestOutput.WriteLine(json); + if (wasError) { - // error page - extension methods not found - var client = new HttpClient(); - var json = await client.GetStringAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/dump-extension-methods"); - TestOutput.WriteLine(json); - throw new Exception("Test failed"); + // fail the test on error + throw new Exception("Extension methods were not resolved on application startup."); } }