-
Notifications
You must be signed in to change notification settings - Fork 320
Implemented first stab at thread safety #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Oh, and tests run fine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, looks nice. It would be really nice if you could add a test case as the first commit that would trigger the concurrent issue and see it pass with your changes.
@@ -125,6 +129,7 @@ type Callbacks map[string]Callback | |||
// currently performed. | |||
func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM { | |||
var f FSM | |||
f.mutex = sync.RWMutex{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed, it is ready to go as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -42,6 +43,9 @@ type transitionerStruct struct { | |||
// | |||
// It has to be created with NewFSM to function properly. | |||
type FSM struct { | |||
// mutex is used for synchronization to achieve thread safety | |||
mutex sync.RWMutex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be cleaner to have it as a anonymous field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was anonymous, wouldn't the Lock/RLock/Unlock/RUnlock methods be public? Thus external access would be dangerous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are correct! Keep the current approach.
@@ -199,23 +204,31 @@ func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) * | |||
|
|||
// Current returns the current state of the FSM. | |||
func (f *FSM) Current() string { | |||
f.mutex.RLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With a anonymous field this would simply be f.RLock()
.
} | ||
|
||
// transitionInternal wraps transitioner.transition. | ||
func (f *FSM) transitionInternal() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think doTransition()
would be a better name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
What do we think now? |
Ah, I didn't remember your first comment. Creating a test-case may be difficult since it is a race condition. One problem I see with the new implementation is access from callbacks will block if they use the public API of their own *FSM object. |
A test case using goroutine for either the read or write together with the Yes, I was also thinking a bit by the possible case of deadlocking within callbacks. Not sure how to prevent that right now. Do you have ideas? |
I have written a testcase that results in a deadlock, trying to figure out a clean way of doing it. But there will be a lot more lock/unlock calls during Event(...)'s. |
Wonderful! Let me know when I should take a look. |
There! I went back to before I added thread safety and added a test proving the lack of thread safety, then I merged it to my current HEAD and confirmed the problem to be solved. I also fixed the deadlock problem as well as I could. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Let me just check out the code a bit later and try it out before we merge.
Cool, you do that :) |
…r locking public api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice tests! I had a comment/idea regarding readability and maintainability of the code.
@@ -306,13 +326,17 @@ func (t transitionerStruct) transition(f *FSM) error { | |||
// general version. | |||
func (f *FSM) beforeEventCallbacks(e *Event) error { | |||
if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok { | |||
f.mutex.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be less repetitive and error prone in these cases to wrap the callbacks in NewFSM
at line 193 with a function that does the unlocking and locking:
f.callbacks[cKey{target, callbackType}] = func(e *Event) {
// This always happens in a locked condition, but needs to be unlocked
// to not risk a deadlock. The reversed order is not a mistake!
f.mutex.Unlock()
defer f.mutex.Lock()
c(e)
}
If you do this, could you also change var f FSM
in the beginning to pointer form:
f := &FSM{
transitionerObj: &transitionerStruct{},
current: initial,
transitions: make(map[eKey]string),
callbacks: make(map[cKey]Callback),
}
And return it with out dereferencing.
Do you think this would be good?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes definitely. I fixed both. Good call!
…cking to a single location in the code
Ops: I simplified my solution a little more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, I was just going to suggest that. 😊
Could you also leave a comment similar to my suggestion in there?
Another minor change could be to use defer for the lock, what do you think about that?
I added your comment :) In our case defering or not doesn't make any difference, but I changed it as you suggest. Arguably if the code is ever changed, whether or not it should be run in a lock or not depends on its function. |
Definitely, I can be too picky sometimes! Do you think we should merge this now? |
Haha, it's a good looking syntax in general to keep locks and unlocks together, so why not 👍 . |
Thanks for all the work with this PR! |
No problem. Thanks for all your feedback! |
Stab at resolving issue #13 . Looking for feedback.