Skip to content

Commit 4ad085b

Browse files
committed
Rearrange Event recording feature code
1 parent 20e65eb commit 4ad085b

3 files changed

Lines changed: 196 additions & 181 deletions

File tree

Lines changed: 0 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package ru.nsk.kstatemachine.persistence
22

33
import ru.nsk.kstatemachine.event.DestroyEvent
4-
import ru.nsk.kstatemachine.event.StartEvent
54
import ru.nsk.kstatemachine.event.StopEvent
65
import ru.nsk.kstatemachine.statemachine.*
76
import ru.nsk.kstatemachine.statemachine.StateMachine.EventRecordingArguments
8-
import ru.nsk.kstatemachine.statemachine.StateMachine.PendingEventHandler
97
import ru.nsk.kstatemachine.transition.EventAndArgument
108
import 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
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package ru.nsk.kstatemachine.persistence
2+
3+
import ru.nsk.kstatemachine.statemachine.StateMachine
4+
5+
fun interface RestorationResultValidator {
6+
/**
7+
* Throws if validation is not passed
8+
*/
9+
fun validate(result: RestorationResult, recordedEvents: RecordedEvents, machine: StateMachine)
10+
}
11+
12+
class RestorationResultValidationException(
13+
val result: RestorationResult,
14+
message: String,
15+
cause: Throwable? = null,
16+
) : RuntimeException(message, cause)
17+
18+
/**
19+
* Completely skips validation
20+
*/
21+
object EmptyValidator : RestorationResultValidator {
22+
override fun validate(result: RestorationResult, recordedEvents: RecordedEvents, machine: StateMachine) = Unit
23+
}
24+
25+
/**
26+
* Does not allow warnings or failed processing results
27+
*/
28+
object StrictValidator : RestorationResultValidator {
29+
/**
30+
* @throws RestorationResultValidationException to indicate validation errors.
31+
*/
32+
override fun validate(result: RestorationResult, recordedEvents: RecordedEvents, machine: StateMachine) {
33+
if (result.warnings.isNotEmpty())
34+
throw RestorationResultValidationException(
35+
result,
36+
"The ${RestorationResult::class.simpleName} contains warnings",
37+
result.warnings.first(),
38+
)
39+
result.results.forEach {
40+
if (it.warnings.isNotEmpty()) {
41+
throw RestorationResultValidationException(
42+
result,
43+
"The ${RestorationResult::class.simpleName} contains warnings",
44+
it.warnings.first(),
45+
)
46+
} else if (it.processingResult.isFailure) {
47+
throw RestorationResultValidationException(
48+
result,
49+
"The ${RestorationResult::class.simpleName} contains failed processing result",
50+
it.processingResult.exceptionOrNull(),
51+
)
52+
}
53+
}
54+
}
55+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package ru.nsk.kstatemachine.persistence
2+
3+
import ru.nsk.kstatemachine.event.StartEvent
4+
import ru.nsk.kstatemachine.statemachine.*
5+
import ru.nsk.kstatemachine.visitors.structureHashCode
6+
7+
data class RestorationResult(
8+
val results: List<RestoredEventResult>,
9+
val warnings: List<RestorationWarningException>,
10+
)
11+
12+
data class RestoredEventResult(
13+
val record: Record,
14+
val processingResult: Result<ProcessingResult>,
15+
val warnings: List<RestorationWarningException>,
16+
)
17+
18+
enum class WarningType {
19+
ProcessingResultNotMatch,
20+
RecordedAndProcessedEventCountNotMatch,
21+
}
22+
23+
class RestorationWarningException(
24+
val warningType: WarningType,
25+
message: String,
26+
cause: Throwable? = null,
27+
) : RuntimeException(message, cause)
28+
29+
/**
30+
* Processes [RecordedEvents] with purpose of restoring a [StateMachine] to a state configuration as it was before
31+
* (when the record was made).
32+
* Starts the [StateMachine] if necessary and returns [RestorationResult] allowing to inspect
33+
* how the restoration was processed. Specified [RestorationResultValidator] will be called to validate the result so
34+
* do not have to remember to perform validation of the [RestorationResult].
35+
*
36+
* There is no way on library side to strictly decide if some exceptions during event processing are errors or not.
37+
* For instance [StateMachine] may be configured with [throwingIgnoredEventHandler] so some exceptions might
38+
* be expected and are not really errors.
39+
* So this method collects all such warnings into [RestorationResult] object in a form of
40+
* [RestorationWarningException] collections, delegating the responsibility for them to
41+
* [RestorationResultValidator] object. [StrictValidator] is used by default and the library provides
42+
* standard [EmptyValidator] to explicitly skip the validation step if necessary.
43+
*
44+
* @param muteListeners listeners are not triggered by default,
45+
* as we assume that client code reactions were already processed before.
46+
* @param disableStructureHashCodeCheck allows to skip the machine structure check
47+
* to force processing of [RecordedEvents]. Note that running the same event sequence on similar machines but having
48+
* different structureHashCode value, may produce different results more likely.
49+
*/
50+
suspend fun StateMachine.restoreByRecordedEvents(
51+
recordedEvents: RecordedEvents,
52+
muteListeners: Boolean = true,
53+
disableStructureHashCodeCheck: Boolean = false,
54+
validator: RestorationResultValidator = StrictValidator,
55+
): RestorationResult = coroutineAbstraction.withContext {
56+
checkNotDestroyed()
57+
if (isRunning) {
58+
check(!hasProcessedEvents) {
59+
"$this has already processed events, ${::restoreByRecordedEvents.name}() operation only makes " +
60+
"sense on initially clear ${StateMachine::class.simpleName}, please call it before " +
61+
"processing any other events (or even before start - optionally)"
62+
}
63+
}
64+
65+
if (!disableStructureHashCodeCheck)
66+
check(structureHashCode == recordedEvents.structureHashCode) {
67+
"$this structure seems to be different from recorded original one, you can disable this error by the " +
68+
"disableStructureHashCodeCheck argument if you are sure that it is correct"
69+
}
70+
71+
this as InternalStateMachine
72+
val results = mutableListOf<RestoredEventResult>()
73+
val commonWarnings = mutableListOf<RestorationWarningException>()
74+
val mutationSection = if (muteListeners) openListenersMutationSection() else EmptyListenersMutationSection
75+
mutationSection.use {
76+
recordedEvents.records.forEachIndexed iteration@{ index, record ->
77+
val warnings = mutableListOf<RestorationWarningException>()
78+
val (event, argument) = record.eventAndArgument
79+
if (event is StartEvent) {
80+
if (isRunning) {
81+
if (argument == null) {
82+
results += RestoredEventResult(record, Result.success(ProcessingResult.PROCESSED), warnings)
83+
return@iteration // continue
84+
} else {
85+
if (index == 0)
86+
error(
87+
"The ${StateMachine::class.simpleName} is already started, but " +
88+
"the ${RecordedEvents::class.simpleName} contains an argument for " +
89+
"${StateMachine::start.name} method. " +
90+
"To restore such machine, " +
91+
"do not start it before calling ${::restoreByRecordedEvents.name}"
92+
)
93+
else {
94+
destroy()
95+
error("The machine should not be running here. Internal error. Never get here")
96+
}
97+
}
98+
} else {
99+
start(argument)
100+
results += RestoredEventResult(record, Result.success(ProcessingResult.PROCESSED), warnings)
101+
}
102+
} else {
103+
val processingResult = runCatching { processEvent(event, argument) }
104+
val actualResult = processingResult.getOrNull()
105+
if (actualResult != null && actualResult != record.processingResult) {
106+
warnings += RestorationWarningException(
107+
WarningType.ProcessingResultNotMatch,
108+
"Recorded (${record.processingResult}) and actual ($actualResult) processing results does not match",
109+
)
110+
}
111+
results += RestoredEventResult(record, processingResult, warnings)
112+
}
113+
}
114+
}
115+
if (results.size != recordedEvents.records.size)
116+
commonWarnings += RestorationWarningException(
117+
WarningType.RecordedAndProcessedEventCountNotMatch,
118+
"Recorded event count is ${recordedEvents.records.size} but the actual processed event count is " +
119+
"${results.size}. They should not differ, this should never happen",
120+
)
121+
RestorationResult(results, commonWarnings).also {
122+
validator.validate(it, recordedEvents, this)
123+
}
124+
}
125+
126+
/**
127+
* Blocking [restoreByRecordedEvents] alternative
128+
*/
129+
fun StateMachine.restoreByRecordedEventsBlocking(
130+
recordedEvents: RecordedEvents,
131+
muteListeners: Boolean = true,
132+
disableStructureHashCodeCheck: Boolean = false,
133+
) {
134+
coroutineAbstraction.runBlocking {
135+
restoreByRecordedEvents(recordedEvents, muteListeners, disableStructureHashCodeCheck)
136+
}
137+
}
138+
139+
private object EmptyListenersMutationSection : ListenersMutationSection {
140+
override fun close() = Unit
141+
}

0 commit comments

Comments
 (0)