Skip to content

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

Merged
merged 12 commits into from
Oct 7, 2016
Merged

Conversation

kristoiv
Copy link
Contributor

@kristoiv kristoiv commented Oct 6, 2016

Stab at resolving issue #13 . Looking for feedback.

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

Oh, and tests run fine.

Copy link
Member

@maxekman maxekman left a 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{}
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

@maxekman maxekman Oct 6, 2016

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.

Copy link
Contributor Author

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.

Copy link
Member

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()
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

What do we think now?

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

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.

@maxekman
Copy link
Member

maxekman commented Oct 6, 2016

A test case using goroutine for either the read or write together with the -race flag should trigger it. See https://golang.org/doc/articles/race_detector.html

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?

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

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.

@maxekman
Copy link
Member

maxekman commented Oct 6, 2016

Wonderful! Let me know when I should take a look.

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

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.

Copy link
Member

@maxekman maxekman left a 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.

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

Cool, you do that :)

Copy link
Member

@maxekman maxekman left a 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()
Copy link
Member

@maxekman maxekman Oct 6, 2016

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?

Copy link
Contributor Author

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!

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 6, 2016

Ops: I simplified my solution a little more

Copy link
Member

@maxekman maxekman left a 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?

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 7, 2016

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.

@maxekman
Copy link
Member

maxekman commented Oct 7, 2016

Definitely, I can be too picky sometimes!

Do you think we should merge this now?

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 7, 2016

Haha, it's a good looking syntax in general to keep locks and unlocks together, so why not 👍 .
I don't see why not, looks to be a good improvement.

@maxekman maxekman merged commit 21fc506 into looplab:master Oct 7, 2016
@maxekman
Copy link
Member

maxekman commented Oct 7, 2016

Thanks for all the work with this PR!

@kristoiv
Copy link
Contributor Author

kristoiv commented Oct 7, 2016

No problem. Thanks for all your feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants