diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs b/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs index c033297d..5c56ab01 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/PersistentProcessVolatileRepresentation.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text; +using System.Text; using System.Text.RegularExpressions; using Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations; using Newtonsoft.Json; @@ -47,7 +47,18 @@ public class PersistentProcessVolatileRepresentation : IPersistentProcess, IDisp public readonly Result lastSetElmAppStateResult; - class LoadedReduction + public struct CompositionLogRecordWithLoadedDependencies + { + public CompositionLogRecordInFile compositionRecord; + + public string compositionRecordHashBase16; + + public ReductionWithLoadedDependencies? reduction; + + public CompositionEventWithLoadedDependencies? composition; + } + + public struct ReductionWithLoadedDependencies { public byte[] elmAppState; @@ -56,6 +67,17 @@ class LoadedReduction public Composition.TreeWithStringPath appConfigAsTree; } + public struct CompositionEventWithLoadedDependencies + { + public byte[] UpdateElmAppStateForEvent; + + public byte[] SetElmAppState; + + public Composition.TreeWithStringPath DeployAppConfigAndInitElmAppState; + + public Composition.TreeWithStringPath DeployAppConfigAndMigrateElmAppState; + } + static public (IDisposableProcessWithStringInterface process, (string javascriptFromElmMake, string javascriptPreparedToRun) buildArtifacts, IReadOnlyList log) @@ -120,21 +142,17 @@ static public (IImmutableDictionary, IImmutableList } }; - /* - Following part can be made less expensive when we have an implementation that evaluates all readings but skips the - rest of the restoring. Something like an extension of `EnumerateCompositionLogRecordsForRestoreProcess` resolving all - dependencies on the store. - */ - using (var restoredProcess = Restore(new ProcessStoreReaderInFileStore(recordingReader), _ => { })) - { - return ( - files: filesForProcessRestore.ToImmutableDictionary(EnumerableExtension.EqualityComparer()), - lastCompositionLogRecordHashBase16: restoredProcess.lastCompositionLogRecordHashBase16); - } + var compositionLogRecords = + EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(new ProcessStoreReaderInFileStore(recordingReader)) + .ToImmutableList(); + + return ( + files: filesForProcessRestore.ToImmutableDictionary(EnumerableExtension.EqualityComparer()), + lastCompositionLogRecordHashBase16: compositionLogRecords.LastOrDefault().compositionRecordHashBase16); } - static IEnumerable<(CompositionLogRecordInFile compositionRecord, string compositionRecordHashBase16, LoadedReduction reduction)> - EnumerateCompositionLogRecordsForRestoreProcess(IProcessStoreReader storeReader) => + static IEnumerable + EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(IProcessStoreReader storeReader) => storeReader .EnumerateSerializedCompositionLogRecordsReverse() .Select(serializedCompositionLogRecord => @@ -147,7 +165,7 @@ dependencies on the store. var reductionRecord = storeReader.LoadProvisionalReduction(compositionRecordHashBase16); - LoadedReduction loadedReduction = null; + ReductionWithLoadedDependencies? reduction = null; if (reductionRecord?.appConfig?.HashBase16 != null && reductionRecord?.elmAppState?.HashBase16 != null) { @@ -169,7 +187,7 @@ dependencies on the store. throw new Exception("Unexpected content of elmAppStateComponent " + reductionRecord.elmAppState?.HashBase16 + ": This is not a blob."); } - loadedReduction = new LoadedReduction + reduction = new ReductionWithLoadedDependencies { appConfig = appConfigComponent, appConfigAsTree = parseAppConfigAsTree.Ok, @@ -178,14 +196,18 @@ dependencies on the store. } } - return ( - compositionRecord: compositionRecord, - compositionRecordHashBase16: compositionRecordHashBase16, - reduction: loadedReduction); + return new CompositionLogRecordWithLoadedDependencies + { + compositionRecord = compositionRecord, + compositionRecordHashBase16 = compositionRecordHashBase16, + composition = LoadCompositionEventDependencies(compositionRecord.compositionEvent, storeReader), + reduction = reduction, + }; }) - .TakeUntil(compositionAndReduction => compositionAndReduction.reduction != null); + .TakeUntil(compositionAndReduction => compositionAndReduction.reduction != null) + .Reverse(); - static public PersistentProcessVolatileRepresentation Restore( + static public PersistentProcessVolatileRepresentation LoadFromStoreAndRestoreProcess( IProcessStoreReader storeReader, Action logger, ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null) @@ -194,11 +216,11 @@ static public PersistentProcessVolatileRepresentation Restore( logger?.Invoke("Begin to restore the process state."); - var compositionEventsToLatestReductionReversed = - EnumerateCompositionLogRecordsForRestoreProcess(storeReader) + var compositionEventsFromLatestReduction = + EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(storeReader) .ToImmutableList(); - if (!compositionEventsToLatestReductionReversed.Any()) + if (!compositionEventsFromLatestReduction.Any()) { logger?.Invoke("Found no composition record, default to initial state."); @@ -209,15 +231,28 @@ static public PersistentProcessVolatileRepresentation Restore( lastSetElmAppStateResult: null); } - logger?.Invoke("Found " + compositionEventsToLatestReductionReversed.Count + " composition log records to use for restore."); + logger?.Invoke("Found " + compositionEventsFromLatestReduction.Count + " composition log records to use for restore."); + + var processVolatileRepresentation = RestoreFromCompositionEventSequence( + compositionEventsFromLatestReduction, + overrideElmAppInterfaceConfig); + + logger?.Invoke("Restored the process state in " + ((int)restoreStopwatch.Elapsed.TotalSeconds) + " seconds."); + + return processVolatileRepresentation; + } - var firstCompositionEventRecord = - compositionEventsToLatestReductionReversed.LastOrDefault(); + static public PersistentProcessVolatileRepresentation RestoreFromCompositionEventSequence( + IEnumerable compositionLogRecords, + ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null) + { + var firstCompositionLogRecord = + compositionLogRecords.FirstOrDefault(); - if (firstCompositionEventRecord.reduction == null && - firstCompositionEventRecord.compositionRecord.parentHashBase16 != CompositionLogRecordInFile.compositionLogFirstRecordParentHashBase16) + if (firstCompositionLogRecord.reduction == null && + firstCompositionLogRecord.compositionRecord.parentHashBase16 != CompositionLogRecordInFile.compositionLogFirstRecordParentHashBase16) { - throw new Exception("Failed to get sufficient history: Composition log record points to parent " + firstCompositionEventRecord.compositionRecord.parentHashBase16); + throw new Exception("Failed to get sufficient history: Composition log record points to parent " + firstCompositionLogRecord.compositionRecord.parentHashBase16); } string lastCompositionLogRecordHashBase16 = null; @@ -227,7 +262,7 @@ static public PersistentProcessVolatileRepresentation Restore( lastElmAppVolatileProcess: null, lastSetElmAppStateResult: null); - foreach (var compositionLogRecord in compositionEventsToLatestReductionReversed.Reverse()) + foreach (var compositionLogRecord in compositionLogRecords) { try { @@ -237,17 +272,17 @@ static public PersistentProcessVolatileRepresentation Restore( { var (newElmAppProcess, (javascriptFromElmMake, javascriptPreparedToRun), _) = ProcessFromWebAppConfig( - compositionLogRecord.reduction.appConfigAsTree, + compositionLogRecord.reduction.Value.appConfigAsTree, overrideElmAppInterfaceConfig: overrideElmAppInterfaceConfig); - var elmAppStateAsString = Encoding.UTF8.GetString(compositionLogRecord.reduction.elmAppState); + var elmAppStateAsString = Encoding.UTF8.GetString(compositionLogRecord.reduction.Value.elmAppState); newElmAppProcess.SetSerializedState(elmAppStateAsString); processRepresentationDuringRestore?.lastElmAppVolatileProcess?.Dispose(); processRepresentationDuringRestore = new PersistentProcessVolatileRepresentationDuringRestore( - lastAppConfig: (compositionLogRecord.reduction.appConfig, (javascriptFromElmMake, javascriptPreparedToRun)), + lastAppConfig: (compositionLogRecord.reduction.Value.appConfig, (javascriptFromElmMake, javascriptPreparedToRun)), lastElmAppVolatileProcess: newElmAppProcess, lastSetElmAppStateResult: null); @@ -269,9 +304,8 @@ static public PersistentProcessVolatileRepresentation Restore( processRepresentationDuringRestore = ApplyCompositionEvent( - compositionEvent, + compositionLogRecord.composition.Value, processRepresentationDuringRestore, - storeReader, overrideElmAppInterfaceConfig); } finally @@ -280,8 +314,6 @@ static public PersistentProcessVolatileRepresentation Restore( } } - logger?.Invoke("Restored the process state in " + ((int)restoreStopwatch.Elapsed.TotalSeconds) + " seconds."); - return new PersistentProcessVolatileRepresentation( lastCompositionLogRecordHashBase16: lastCompositionLogRecordHashBase16, lastAppConfig: processRepresentationDuringRestore.lastAppConfig, @@ -316,47 +348,17 @@ public PersistentProcessVolatileRepresentationDuringRestore WithLastSetElmAppSta } static PersistentProcessVolatileRepresentationDuringRestore ApplyCompositionEvent( - CompositionLogRecordInFile.CompositionEvent compositionEvent, + CompositionEventWithLoadedDependencies compositionEvent, PersistentProcessVolatileRepresentationDuringRestore processBefore, - IProcessStoreReader storeReader, ElmAppInterfaceConfig? overrideElmAppInterfaceConfig) { - IImmutableList loadComponentFromStoreAndAssertIsBlob(string componentHash) - { - var component = storeReader.LoadComponent(componentHash); - - if (component == null) - throw new Exception("Failed to load component " + componentHash + ": Not found in store."); - - if (component.BlobContent == null) - throw new Exception("Failed to load component " + componentHash + " as blob: This is not a blob."); - - return component.BlobContent; - } - - Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string componentHash) - { - var component = storeReader.LoadComponent(componentHash); - - if (component == null) - throw new Exception("Failed to load component " + componentHash + ": Not found in store."); - - var parseAsTreeResult = Composition.ParseAsTreeWithStringPath(component); - - if (parseAsTreeResult.Ok == null) - throw new Exception("Failed to load component " + componentHash + " as tree: Failed to parse as tree."); - - return parseAsTreeResult.Ok; - } - if (compositionEvent.UpdateElmAppStateForEvent != null) { if (processBefore.lastElmAppVolatileProcess == null) return processBefore; processBefore.lastElmAppVolatileProcess.ProcessEvent( - Encoding.UTF8.GetString(loadComponentFromStoreAndAssertIsBlob( - compositionEvent.UpdateElmAppStateForEvent.HashBase16).ToArray())); + Encoding.UTF8.GetString(compositionEvent.UpdateElmAppStateForEvent)); return processBefore; } @@ -374,8 +376,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp } var projectedElmAppState = - Encoding.UTF8.GetString(loadComponentFromStoreAndAssertIsBlob( - compositionEvent.SetElmAppState.HashBase16).ToArray()); + Encoding.UTF8.GetString(compositionEvent.SetElmAppState); processBefore.lastElmAppVolatileProcess.SetSerializedState(projectedElmAppState); @@ -403,8 +404,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp { var elmAppStateBefore = processBefore.lastElmAppVolatileProcess?.GetSerializedState(); - var appConfig = loadComponentFromStoreAndAssertIsTree( - compositionEvent.DeployAppConfigAndMigrateElmAppState.HashBase16); + var appConfig = compositionEvent.DeployAppConfigAndMigrateElmAppState; var prepareMigrateResult = PrepareMigrateSerializedValue(destinationAppConfigTree: appConfig); @@ -450,8 +450,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp if (compositionEvent.DeployAppConfigAndInitElmAppState != null) { - var appConfig = loadComponentFromStoreAndAssertIsTree( - compositionEvent.DeployAppConfigAndInitElmAppState.HashBase16); + var appConfig = compositionEvent.DeployAppConfigAndInitElmAppState; var (newElmAppProcess, buildArtifacts, _) = ProcessFromWebAppConfig( @@ -469,6 +468,80 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp throw new Exception("Unexpected shape of composition event: " + JsonConvert.SerializeObject(compositionEvent)); } + static CompositionEventWithLoadedDependencies? LoadCompositionEventDependencies( + CompositionLogRecordInFile.CompositionEvent compositionEvent, + IProcessStoreReader storeReader) + { + IImmutableList loadComponentFromStoreAndAssertIsBlob(string componentHash) + { + var component = storeReader.LoadComponent(componentHash); + + if (component == null) + throw new Exception("Failed to load component " + componentHash + ": Not found in store."); + + if (component.BlobContent == null) + throw new Exception("Failed to load component " + componentHash + " as blob: This is not a blob."); + + return component.BlobContent; + } + + Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string componentHash) + { + var component = storeReader.LoadComponent(componentHash); + + if (component == null) + throw new Exception("Failed to load component " + componentHash + ": Not found in store."); + + var parseAsTreeResult = Composition.ParseAsTreeWithStringPath(component); + + if (parseAsTreeResult.Ok == null) + throw new Exception("Failed to load component " + componentHash + " as tree: Failed to parse as tree."); + + return parseAsTreeResult.Ok; + } + + if (compositionEvent.UpdateElmAppStateForEvent != null) + { + return new CompositionEventWithLoadedDependencies + { + UpdateElmAppStateForEvent = loadComponentFromStoreAndAssertIsBlob( + compositionEvent.UpdateElmAppStateForEvent.HashBase16).ToArray(), + }; + } + + if (compositionEvent.SetElmAppState != null) + { + return new CompositionEventWithLoadedDependencies + { + SetElmAppState = loadComponentFromStoreAndAssertIsBlob( + compositionEvent.SetElmAppState.HashBase16).ToArray(), + }; + } + + if (compositionEvent.DeployAppConfigAndMigrateElmAppState != null) + { + return new CompositionEventWithLoadedDependencies + { + DeployAppConfigAndMigrateElmAppState = loadComponentFromStoreAndAssertIsTree( + compositionEvent.DeployAppConfigAndMigrateElmAppState.HashBase16), + }; + } + + if (compositionEvent.DeployAppConfigAndInitElmAppState != null) + { + return new CompositionEventWithLoadedDependencies + { + DeployAppConfigAndInitElmAppState = loadComponentFromStoreAndAssertIsTree( + compositionEvent.DeployAppConfigAndInitElmAppState.HashBase16), + }; + } + + if (compositionEvent.RevertProcessTo != null) + return null; + + throw new Exception("Unexpected shape of composition event: " + JsonConvert.SerializeObject(compositionEvent)); + } + public class Result { public ErrT Err; @@ -486,7 +559,7 @@ public class Result compositionLogEvent: compositionLogEvent); using (var projectedProcess = - Restore(new ProcessStoreReaderInFileStore(projectionResult.projectedReader), _ => { })) + LoadFromStoreAndRestoreProcess(new ProcessStoreReaderInFileStore(projectionResult.projectedReader), _ => { })) { if (compositionLogEvent.DeployAppConfigAndMigrateElmAppState != null || compositionLogEvent.SetElmAppState != null) diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs b/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs index 40cba8d4..eab85b52 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/StartupAdminInterface.cs @@ -126,7 +126,7 @@ void startPublicApp() logger.LogInformation("Begin to build the process volatile representation."); var processVolatileRepresentation = - PersistentProcess.PersistentProcessVolatileRepresentation.Restore( + PersistentProcess.PersistentProcessVolatileRepresentation.LoadFromStoreAndRestoreProcess( new ProcessStoreSupportingMigrations.ProcessStoreReaderInFileStore(processStoreFileStore), logger: logEntry => logger.LogInformation(logEntry)); diff --git a/implement/test-elm-fullstack/TestWebHost.cs b/implement/test-elm-fullstack/TestWebHost.cs index cec2c6d2..ac4bba8b 100644 --- a/implement/test-elm-fullstack/TestWebHost.cs +++ b/implement/test-elm-fullstack/TestWebHost.cs @@ -1166,7 +1166,7 @@ public void tooling_supports_deploy_app_directly_on_process_store() promptForPasswordOnConsole: false); using (var restoredProcess = - Kalmit.PersistentProcess.WebHost.PersistentProcess.PersistentProcessVolatileRepresentation.Restore( + Kalmit.PersistentProcess.WebHost.PersistentProcess.PersistentProcessVolatileRepresentation.LoadFromStoreAndRestoreProcess( new Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations.ProcessStoreReaderInFileStore( new FileStoreFromSystemIOFile(testDirectory)), _ => { }))