arcs icon indicating copy to clipboard operation
arcs copied to clipboard

[WIP] Adds a type-safe StateMachine builder to be used for life cycles.

Open cromwellian opened this issue 4 years ago • 9 comments

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.

cromwellian avatar Apr 27 '20 07:04 cromwellian

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.

csilvestrini avatar Apr 28 '20 01:04 csilvestrini

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)

jasonwyatt avatar Apr 28 '20 18:04 jasonwyatt

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.

mykmartin avatar Apr 29 '20 01:04 mykmartin

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.

cromwellian avatar Apr 29 '20 19:04 cromwellian

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.

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.

cromwellian avatar Apr 29 '20 20:04 cromwellian

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.

cromwellian avatar Apr 29 '20 20:04 cromwellian

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?)

cromwellian avatar Apr 29 '20 20:04 cromwellian

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.

mykmartin avatar Apr 30 '20 00:04 mykmartin

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!

jasonwyatt avatar Apr 30 '20 17:04 jasonwyatt