11package ru.nsk.kstatemachine.persistence
22
33import ru.nsk.kstatemachine.event.DestroyEvent
4- import ru.nsk.kstatemachine.event.StartEvent
54import ru.nsk.kstatemachine.event.StopEvent
65import ru.nsk.kstatemachine.statemachine.*
76import ru.nsk.kstatemachine.statemachine.StateMachine.EventRecordingArguments
8- import ru.nsk.kstatemachine.statemachine.StateMachine.PendingEventHandler
97import ru.nsk.kstatemachine.transition.EventAndArgument
108import ru.nsk.kstatemachine.visitors.structureHashCode
119
@@ -54,183 +52,4 @@ internal class EventRecorderImpl(
5452 override fun getRecordedEvents (): RecordedEvents {
5553 return RecordedEvents (machine.structureHashCode, records)
5654 }
57- }
58-
59- data class RestorationResult (
60- val results : List <RestoredEventResult >,
61- val warnings : List <RestorationWarningException >,
62- )
63-
64- data class RestoredEventResult (
65- val record : Record ,
66- val processingResult : Result <ProcessingResult >,
67- val warnings : List <RestorationWarningException >,
68- )
69-
70- fun interface RestorationResultValidator {
71- /* *
72- * Throws if validation is not passed
73- */
74- fun validate (result : RestorationResult , recordedEvents : RecordedEvents , machine : StateMachine )
75- }
76-
77- /* *
78- * Completely skips validation
79- */
80- object EmptyValidator : RestorationResultValidator {
81- override fun validate (result : RestorationResult , recordedEvents : RecordedEvents , machine : StateMachine ) = Unit
82- }
83-
84- /* *
85- * Does not allow warnings or failed processing results
86- */
87- object StrictValidator : RestorationResultValidator {
88- override fun validate (result : RestorationResult , recordedEvents : RecordedEvents , machine : StateMachine ) {
89- if (result.warnings.isNotEmpty())
90- throw RestorationResultValidationException (
91- result,
92- " The ${RestorationResult ::class .simpleName} contains warnings" ,
93- result.warnings.first(),
94- )
95- result.results.forEach {
96- if (it.warnings.isNotEmpty()) {
97- throw RestorationResultValidationException (
98- result,
99- " The ${RestorationResult ::class .simpleName} contains warnings" ,
100- it.warnings.first(),
101- )
102- } else if (it.processingResult.isFailure) {
103- throw RestorationResultValidationException (
104- result,
105- " The ${RestorationResult ::class .simpleName} contains failed processing result" ,
106- it.processingResult.exceptionOrNull(),
107- )
108- }
109- }
110- }
111- }
112-
113- class RestorationResultValidationException (
114- val result : RestorationResult ,
115- message : String ,
116- cause : Throwable ? = null ,
117- ) : RuntimeException(message, cause)
118-
119- enum class WarningType {
120- ProcessingResultNotMatch ,
121- RecordedAndProcessedEventCountNotMatch ,
122- }
123-
124- class RestorationWarningException (
125- val warningType : WarningType ,
126- message : String ,
127- cause : Throwable ? = null ,
128- ) : RuntimeException(message, cause)
129-
130- /* *
131- * Processes [RecordedEvents] with purpose of restoring a [StateMachine] to a state configuration as it was before.
132- * Starts the [StateMachine] if necessary and returns [RestorationResult] allowing to inspect
133- * how the restoration was processed.
134- *
135- * There is no way on library side to decide if some exceptions during event processing are errors or not.
136- * For instance [StateMachine] may be configured with [throwingIgnoredEventHandler] so some exceptions might
137- * be expected and are not really errors.
138- *
139- * @param muteListeners listeners are not triggered by default,
140- * as we assume that client code reactions were already processed before.
141- * @param disableStructureHashCodeCheck allows to skip the machine structure check
142- * to force processing of [RecordedEvents]. Note that running the same event sequence on similar machines but having
143- * different structureHashCode value, may produce different results more likely.
144- */
145- suspend fun StateMachine.restoreByRecordedEvents (
146- recordedEvents : RecordedEvents ,
147- muteListeners : Boolean = true,
148- disableStructureHashCodeCheck : Boolean = false,
149- validator : RestorationResultValidator = StrictValidator ,
150- ): RestorationResult = coroutineAbstraction.withContext {
151- checkNotDestroyed()
152- if (isRunning) {
153- check(! hasProcessedEvents) {
154- " $this has already processed events, ${::restoreByRecordedEvents.name} () operation only makes " +
155- " sense on initially clear ${StateMachine ::class .simpleName} , please call it before " +
156- " processing any other events (or even before start - optionally)"
157- }
158- }
159-
160- if (! disableStructureHashCodeCheck)
161- check(structureHashCode == recordedEvents.structureHashCode) {
162- " $this structure seems to be different from recorded original one, you can disable this error by the " +
163- " disableStructureHashCodeCheck argument if you are sure that it is correct"
164- }
165-
166- this as InternalStateMachine
167- val results = mutableListOf<RestoredEventResult >()
168- val commonWarnings = mutableListOf<RestorationWarningException >()
169- val mutationSection = if (muteListeners) openListenersMutationSection() else EmptyListenersMutationSection
170- mutationSection.use {
171- recordedEvents.records.forEachIndexed iteration@{ index, record ->
172- val warnings = mutableListOf<RestorationWarningException >()
173- val (event, argument) = record.eventAndArgument
174- if (event is StartEvent ) {
175- if (isRunning) {
176- if (argument == null ) {
177- results + = RestoredEventResult (record, Result .success(ProcessingResult .PROCESSED ), warnings)
178- return @iteration // continue
179- } else {
180- if (index == 0 )
181- error(
182- " The ${StateMachine ::class .simpleName} is already started, but " +
183- " the ${RecordedEvents ::class .simpleName} contains an argument for " +
184- " ${StateMachine ::start.name} method. " +
185- " To restore such machine, " +
186- " do not start it before calling ${::restoreByRecordedEvents.name} "
187- )
188- else {
189- destroy()
190- error(" The machine should not be running here. Internal error. Never get here" )
191- }
192- }
193- } else {
194- start(argument)
195- results + = RestoredEventResult (record, Result .success(ProcessingResult .PROCESSED ), warnings)
196- }
197- } else {
198- val processingResult = runCatching { processEvent(event, argument) }
199- val actualResult = processingResult.getOrNull()
200- if (actualResult != null && actualResult != record.processingResult) {
201- warnings + = RestorationWarningException (
202- WarningType .ProcessingResultNotMatch ,
203- " Recorded (${record.processingResult} ) and actual ($actualResult ) processing results does not match" ,
204- )
205- }
206- results + = RestoredEventResult (record, processingResult, warnings)
207- }
208- }
209- }
210- if (results.size != recordedEvents.records.size)
211- commonWarnings + = RestorationWarningException (
212- WarningType .RecordedAndProcessedEventCountNotMatch ,
213- " Recorded event count is ${recordedEvents.records.size} but the actual processed event count is " +
214- " ${results.size} . They should not differ, this should never happen" ,
215- )
216- RestorationResult (results, commonWarnings).also {
217- validator.validate(it, recordedEvents, this )
218- }
219- }
220-
221- /* *
222- * Blocking [restoreByRecordedEvents] alternative
223- */
224- fun StateMachine.restoreByRecordedEventsBlocking (
225- recordedEvents : RecordedEvents ,
226- muteListeners : Boolean = true,
227- disableStructureHashCodeCheck : Boolean = false,
228- ) {
229- coroutineAbstraction.runBlocking {
230- restoreByRecordedEvents(recordedEvents, muteListeners, disableStructureHashCodeCheck)
231- }
232- }
233-
234- private object EmptyListenersMutationSection : ListenersMutationSection {
235- override fun close () = Unit
23655}
0 commit comments