Search code examples
mongodbgostructmutexmgo

Does the Golang MGO module lock or unlock Go objects?


I am trying to understand whether Mongo locks Go objects.

The first function works fine with the json encoder, however the second function fails fatal error: sync: Unlock of unlocked RWMutex. Is this because mongo.Find is already trying to lock/unlock the state object? Do I need to externally handle race competition for my go objects or does MGO take care of it? I tried reading the source code but I haven't been able to reach a conclusion.

Any would be much appreciated!

import (
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"io"
"sync"
"encoding/json"
)

type ApplicationState struct {
    FileStates map[string]FileState     `json:"fileStates" bson:"fileStates"` 
    lock       sync.RWMutex             `json:"-" bson:"-"`
}

func (state *ApplicationState) ReadState(reader io.Reader) error {
    state.lock.Lock()
    defer state.lock.Unlock()
    return json.NewDecoder(reader).Decode(state)}

func (state *ApplicationState) ReadStateMGO(c *mgo.Collection) error {
    state.lock.Lock()
    defer state.lock.Unlock()
    return c.Find( bson.M{} ).Select( bson.M{"_id": 0} ).One(state)}

Note: to test it you can just replace the Filestate field with a string map.


Solution

  • First, drop gopkg.in/mgo.v2, it's obsolte, unmaintained. Instead use the community supported fork: github.com/globalsign/mgo.

    Next, you should first read the public, package documentation, and only "revert" to reading the source to clear such things up if the documentation does not offer the answers. But drawing conclusions from the source is always dangerous, as the implementation may change at any time, only what is documented is guaranteed. Documentation of mgo.Session states:

    All Session methods are concurrency-safe and may be called from multiple goroutines.

    This is all the guarantee you have, and all you should depend on. Using methods of mgo.Collection may or may not be safe for concurrent use, so do not do that. When needed, always obtain a "new" collection from the session, as that is safe to be accessed from multiple goroutines.

    And now on to your actual problem.

    Your ApplicationState struct type contains a lock (sync.RWMutex), and you unmarshal your query result into the same ApplicationState value that holds the lock:

    func (state *ApplicationState) ReadStateMGO(c *mgo.Collection) error {
        state.lock.Lock()
        defer state.lock.Unlock()
        return c.Find( bson.M{} ).Select( bson.M{"_id": 0} ).One(state)
    }
    

    This is a red flag! Don't do this! Unmarshaling into a value may clear any fields, including the lock!

    Yes, you may say it's an unexported field, so the mgo package should not / could not change it. This is true, but the mgo package may decide to create a new ApplicationState value to unmarshal into, and the new value may be assigned to the value pointed by the passed pointer (state).

    If this happens, the newly created ApplicationState will have a lock field being its zero-value, which is an unlocked mutex. And once this happens, the subsequent unlock will obviously fail (panic).

    Solution? Move locks outside of struct values you intend to serialize / deserialize. Or at least don't expect the lock state to transfer along with other fields.