arcs
arcs copied to clipboard
[WIP] Adds a type-safe StateMachine builder to be used for life cycles.
Adds a declarative type-safe StateMachine immutable builder DSL to be used for life cycles. This is inspired by various frameworks I've used before in games and protocols (e.g. Fettle).
Each state may have onEntry, onExit, onError handlers attached, as well as event->newstate transition mappings.
All legal states are defined, as well as event-driven transitions, with attached event and error handlers, in the DSL.
a PlantUML or GraphViz dump of the structure can be obtained.
The machine uses an internal queue to manage any events triggered by handlers. A sample implementation of AbstractArcHost.performParticleLifecycle() has been ported.
I'm still working on how the queue will be processed or drained, and whether I'll use another thread (e.g. like a browser microtasks queue). Additionally, its currently not thread safe (no mutexes around the mutate state field or eventQueue)
Comments welcome.
Added Mick as a replacement reviewer, since he's been working on the particle lifecycle a lot.
I like the idea of having a proper reusable state machine implementation, though to be honest I'm not completely sold on the DSL aspect of it, it seems like it might be harder to read and debug.
I'm still working on how the queue will be processed or drained, and whether I'll use another thread (e.g. like a browser microtasks queue). Additionally, its currently not thread safe (no mutexes around the mutate state field or eventQueue)
I think it's ok leaving this single threaded, without mutexes and without a separate processing thread. That seems simpler, and fits better with our proposed threading model going forward.
This seems very cool, I'm curious why so many of the methods are suspending functions though? What reason do they need to suspend? (KDoc explaining under what scenarios they suspend execution would be awesome)
I tend to agree with Cameron; not sure about the DSL approach. I'm also not sure about how the events fit in. For example: in ParticleStateMachine
, a CreateEvent
is triggered that doesn't appear to do anything and the process for MaxFailedEvent
seems a little convoluted.
I like the idea of having a proper reusable state machine implementation, though to be honest I'm not completely sold on the DSL aspect of it, it seems like it might be harder to read and debug.
The DSL idiom is a standard Kotlin idiom for doing this (https://kotlinlang.org/docs/reference/type-safe-builders.html)
You can see Tinder is doing a similar technique here (https://github.com/Tinder/Scarlet/blob/5457f827884269a7e1a7c1c60d057d0ed5fa26b1/scarlet/src/main/java/com/tinder/scarlet/internal/connection/Connection.kt)
Or a similar pattern in Haskell (https://wickstrom.tech/finite-state-machines/2017/11/10/finite-state-machines-part-1-modeling-with-haskell.html)
The alternative is the boilerplatey Java approach used in Fettle, e.g.
val stateMachine.newBuilder().addState(Foo).onEntry(...).addTransition(...).build()
I'd argue laying out the states and events -> transitions helps readability once you get used to it.
I tend to agree with Cameron; not sure about the DSL approach. I'm also not sure about how the events fit in. For example: in
ParticleStateMachine
, aCreateEvent
is triggered that doesn't appear to do anything and the process forMaxFailedEvent
seems a little convoluted.
It's an omission. 'CreateEvent' is supposed to have a on(CreateEvent, Created) that creates a valid transition edge from Instantiated -> Created state, after onCreate() is called. 'Created'
The reason it uses an exception is because the current implementation is that onError() clears pending transition events. The exception could be removed just by adding a 'clearPendingEvents()' call and then transitioning to the MaxFailed state.
Basically, code needs to be able to short-circuit a sequence of transitions if an error occurs.
This seems very cool, I'm curious why so many of the methods are suspending functions though? What reason do they need to suspend? (KDoc explaining under what scenarios they suspend execution would be awesome)
Mostly laziness. Since the handle APIs are suspend functions, as are the particle APIs, calling them from handlers means launching coroutines everytime, but once your big PR lands that removes the suspend keywords, all these can go away.
This seems very cool, I'm curious why so many of the methods are suspending functions though? What reason do they need to suspend? (KDoc explaining under what scenarios they suspend execution would be awesome)
Mostly laziness. Since the handle APIs are suspend functions, as are the particle APIs, calling them from handlers means launching coroutines everytime, but once your big PR lands that removes the suspend keywords, all these can go away.
Actually, I believe if I use inline functions, I might be able to have my cake and eat it too, by letting you use either? (similar to the way many Kotlin stdlib inline functions on collections work?)
The DSL idiom is a standard Kotlin idiom for doing this (https://kotlinlang.org/docs/reference/type-safe-builders.html)
Fair enough. Definitely better than the boilerplate.
I'd argue laying out the states and events -> transitions helps readability once you get used to it.
Yes, that's probably true.
This seems very cool, I'm curious why so many of the methods are suspending functions though? What reason do they need to suspend? (KDoc explaining under what scenarios they suspend execution would be awesome)
Mostly laziness. Since the handle APIs are suspend functions, as are the particle APIs, calling them from handlers means launching coroutines everytime, but once your big PR lands that removes the suspend keywords, all these can go away.
Actually, I believe if I use inline functions, I might be able to have my cake and eat it too, by letting you use either? (similar to the way many Kotlin stdlib inline functions on collections work?)
That could work!