From 869fdcbdae61ec60be00ecf8f2d6caf2c752b15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20R=C3=A4tzel?= Date: Mon, 18 May 2020 15:14:11 +0000 Subject: [PATCH] Make `deploy-app-config` in `run-server` more robust Make the deployment of app configurations on the `run-server` command synchronous. The atomic deployment also fixes the issue of a process replicated from a production system crashing when attempting to acquire an SSL certificate. --- ...PersistentProcessVolatileRepresentation.cs | 29 +++++++++ .../ProcessStoreSupportingMigrations.cs | 15 +++++ .../PersistentProcess.WebHost/Program.cs | 2 +- .../StartupAdminInterface.cs | 60 ++++++------------ implement/elm-fullstack/Program.cs | 61 ++++++++++++++----- implement/elm-fullstack/elm-fullstack.csproj | 4 +- 6 files changed, 110 insertions(+), 61 deletions(-) diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs b/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs index b573d57b..2efbc9c0 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs @@ -455,6 +455,35 @@ public class Result public OkT Ok; } + static public Composition.Result filePath, byte[] fileContent)> projectedFiles, IFileStoreReader projectedReader)> + TestContinueWithCompositionEvent( + ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent compositionLogEvent, + IFileStoreReader fileStoreReader) + { + var projectionResult = IProcessStoreReader.ProjectFileStoreReaderForAppendedCompositionLogEvent( + originalFileStore: fileStoreReader, + compositionLogEvent: compositionLogEvent); + + using (var projectedProcess = + PersistentProcess.PersistentProcessVolatileRepresentation.Restore( + new ProcessStoreReaderInFileStore(projectionResult.projectedReader), + _ => { })) + { + if (compositionLogEvent.DeployAppConfigAndMigrateElmAppState != null || + compositionLogEvent.SetElmAppState != null) + { + if (projectedProcess.lastSetElmAppStateResult?.Ok == null) + { + return Composition.Result filePath, byte[] fileContent)> projectedFiles, IFileStoreReader projectedReader)>.err( + "Failed to migrate Elm app state for this deployment: " + projectedProcess.lastSetElmAppStateResult?.Err); + } + } + } + + return Composition.Result filePath, byte[] fileContent)> projectedFiles, IFileStoreReader projectedReader)>.ok( + projectionResult); + } + static Result>> PrepareMigrateSerializedValue( Composition.TreeComponent destinationAppConfigTree) { diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/ProcessStoreSupportingMigrations.cs b/implement/PersistentProcess/PersistentProcess.WebHost/ProcessStoreSupportingMigrations.cs index 68de4ee3..bdac0dea 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/ProcessStoreSupportingMigrations.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/ProcessStoreSupportingMigrations.cs @@ -153,6 +153,21 @@ public class CompositionEvent public ValueInFileStructure DeployAppConfigAndMigrateElmAppState; public ValueInFileStructure RevertProcessTo; + + static public CompositionEvent EventForDeployAppConfig( + ValueInFileStructure appConfigValueInFile, + bool initElmAppState) => + initElmAppState + ? + new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent + { + DeployAppConfigAndInitElmAppState = appConfigValueInFile, + } + : + new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent + { + DeployAppConfigAndMigrateElmAppState = appConfigValueInFile, + }; } } diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs b/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs index 8463957a..0c964a9d 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs @@ -2,6 +2,6 @@ namespace Kalmit.PersistentProcess.WebHost { public class Program { - static public string AppVersionId => "2020-05-16"; + static public string AppVersionId => "2020-05-18"; } } diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs b/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs index b042cb58..7c0dfc93 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs @@ -367,17 +367,9 @@ IWebHost buildWebHost() }; var compositionLogEvent = - requestPathIsDeployAppConfigAndInitElmAppState - ? - new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent - { - DeployAppConfigAndInitElmAppState = appConfigValueInFile, - } - : - new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent - { - DeployAppConfigAndMigrateElmAppState = appConfigValueInFile, - }; + ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent.EventForDeployAppConfig( + appConfigValueInFile: appConfigValueInFile, + initElmAppState: requestPathIsDeployAppConfigAndInitElmAppState); await attemptContinueWithCompositionEventAndSendHttpResponse(compositionLogEvent); return; @@ -611,11 +603,15 @@ TruncateProcessHistoryReport truncateProcessHistory(TimeSpan productionBlockDura { publicAppHost?.processVolatileRepresentation?.StoreReductionRecordForCurrentState(processStoreWriter); + var testContinueResult = PersistentProcess.PersistentProcessVolatileRepresentation.TestContinueWithCompositionEvent( + compositionLogEvent: compositionLogEvent, + fileStoreReader: processStoreFileStore); + var projectionResult = IProcessStoreReader.ProjectFileStoreReaderForAppendedCompositionLogEvent( originalFileStore: processStoreFileStore, compositionLogEvent: compositionLogEvent); - (int statusCode, AttemptContinueWithCompositionEventReport report) returnError(string errorMessage) + if (testContinueResult.Ok.projectedFiles == null) { return (statusCode: 400, new AttemptContinueWithCompositionEventReport { @@ -623,44 +619,24 @@ TruncateProcessHistoryReport truncateProcessHistory(TimeSpan productionBlockDura parentCompositionHashBase16 = projectionResult.parentHashBase16, compositionEvent = compositionLogEvent, totalTimeSpentMilli = (int)totalStopwatch.ElapsedMilliseconds, - result = Composition.Result.err(errorMessage), + result = Composition.Result.err(testContinueResult.Err), }); } - (int statusCode, AttemptContinueWithCompositionEventReport report) returnOk(string okMessage) - { - return (statusCode: 200, new AttemptContinueWithCompositionEventReport - { - beginTime = beginTime, - parentCompositionHashBase16 = projectionResult.parentHashBase16, - compositionEvent = compositionLogEvent, - totalTimeSpentMilli = (int)totalStopwatch.ElapsedMilliseconds, - result = Composition.Result.ok(okMessage), - }); - } - - using (var projectedProcess = - PersistentProcess.PersistentProcessVolatileRepresentation.Restore( - new ProcessStoreReaderInFileStore(projectionResult.projectedReader), - _ => { })) - { - if (compositionLogEvent.DeployAppConfigAndMigrateElmAppState != null || - compositionLogEvent.SetElmAppState != null) - { - if (projectedProcess.lastSetElmAppStateResult?.Ok == null) - { - return returnError("Failed to migrate Elm app state for this deployment: " + projectedProcess.lastSetElmAppStateResult?.Err); - } - } - } - - foreach (var projectedFilePathAndContent in projectionResult.projectedFiles) + foreach (var projectedFilePathAndContent in testContinueResult.Ok.projectedFiles) processStoreFileStore.SetFileContent( projectedFilePathAndContent.filePath, projectedFilePathAndContent.fileContent); startPublicApp(); - return returnOk("Successfully deployed this configuration and started the web server."); + return (statusCode: 200, new AttemptContinueWithCompositionEventReport + { + beginTime = beginTime, + parentCompositionHashBase16 = projectionResult.parentHashBase16, + compositionEvent = compositionLogEvent, + totalTimeSpentMilli = (int)totalStopwatch.ElapsedMilliseconds, + result = Composition.Result.ok("Successfully deployed this configuration and started the web server."), + }); } } diff --git a/implement/elm-fullstack/Program.cs b/implement/elm-fullstack/Program.cs index 2340fe22..edb5cc31 100644 --- a/implement/elm-fullstack/Program.cs +++ b/implement/elm-fullstack/Program.cs @@ -122,6 +122,51 @@ CommandOption verboseLogOptionFromCommand(CommandLineApplication command) => var adminInterfaceUrl = "http://*:" + adminInterfaceHttpPort.ToString(); + if (deployAppConfigOption.HasValue()) + { + Console.WriteLine("Loading app config to deploy..."); + + var appConfigZipArchive = + Kalmit.PersistentProcess.WebHost.BuildConfigurationFromArguments.BuildConfigurationZipArchive( + _ => { }).compileConfigZipArchive(); + + var appConfigTree = + Composition.TreeFromSetOfBlobsWithCommonFilePath( + ZipArchive.EntriesFromZipArchive(appConfigZipArchive)); + + var appConfigComponent = Composition.FromTree(appConfigTree); + + var processStoreWriter = + new Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations.ProcessStoreWriterInFileStore( + processStoreFileStore); + + processStoreWriter.StoreComponent(appConfigComponent); + + var appConfigValueInFile = + new Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations.ValueInFileStructure + { + HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(appConfigComponent)) + }; + + var compositionLogEvent = + Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent.EventForDeployAppConfig( + appConfigValueInFile: appConfigValueInFile, + initElmAppState: deletePreviousProcessOption.HasValue() && !replicateProcessFromOption.HasValue()); + + var testDeployResult = Kalmit.PersistentProcess.WebHost.PersistentProcess.PersistentProcessVolatileRepresentation.TestContinueWithCompositionEvent( + compositionLogEvent: compositionLogEvent, + fileStoreReader: processStoreFileStore); + + if (testDeployResult.Ok.projectedFiles == null) + { + throw new Exception("Attempt to deploy app config failed: " + testDeployResult.Err); + } + + foreach (var projectedFilePathAndContent in testDeployResult.Ok.projectedFiles) + processStoreFileStore.SetFileContent( + projectedFilePathAndContent.filePath, projectedFilePathAndContent.fileContent); + } + var webHostBuilder = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder() .ConfigureAppConfiguration(builder => builder.AddEnvironmentVariables("APPSETTING_")) @@ -141,22 +186,6 @@ CommandOption verboseLogOptionFromCommand(CommandLineApplication command) => Console.WriteLine("Completed starting the web server with the admin interface at '" + adminInterfaceUrl + "'."); - if (deployAppConfigOption.HasValue()) - { - System.Threading.Tasks.Task.Delay(1000).Wait(); - - /* - TODO: - Make deploy synchronous: Inject this in the host to be processed at startup, so that no public app is started for the pre-deploy state. - Also, to prevent confusion, we might want to fail starting the server completely in case the deployment fails. - */ - - deployAppConfig( - site: "http://localhost:" + adminInterfaceHttpPort, - sitePassword: adminRootPasswordOption.Value(), - initElmAppState: deletePreviousProcessOption.HasValue() && !replicateProcessFromOption.HasValue()); - } - Microsoft.AspNetCore.Hosting.WebHostExtensions.WaitForShutdown(webHost); }); }); diff --git a/implement/elm-fullstack/elm-fullstack.csproj b/implement/elm-fullstack/elm-fullstack.csproj index dcb69258..c9f023c7 100644 --- a/implement/elm-fullstack/elm-fullstack.csproj +++ b/implement/elm-fullstack/elm-fullstack.csproj @@ -5,8 +5,8 @@ netcoreapp3.1 elm_fullstack elm-fullstack - 2020.0516.0.0 - 2020.0516.0.0 + 2020.0518.0.0 + 2020.0518.0.0