Skip to content

Locking

Introduction

minimega is highly concurrent and uses many locks in order to avoid data races. This leads to many potential deadlocks which this article aims to prevent.

Locking conventions

In general, locks should only be used in the file where they are defined. Files typically include a type definition and some number of functions that operate on those types. Therefore, if a file defines a lock, most of the functions that are semantically related to the lock should be defined in the same file.

We are in the process of moving towards a naming convention within minimega -- if both an exported and internal function exist, the exported function acquires any necessary locks and then invokes the internal function. For example:

// FindVM finds a VM in the active namespace based on its ID, name, or UUID.
func (vms VMs) FindVM(s string) VM {
    vmLock.Lock()
    defer vmLock.Unlock()

    return vms.findVM(s)
}

// findVM assumes vmLock is held.
func (vms VMs) findVM(s string) VM {

Developers should read the function description to determine if the call site already holds the requisite locks. If the requisite locks are held, the developer should annotate the call sites with // LOCK: ... to make it clear that calling the internal function is indeed correct.

Locks in minimega

cmdChannel

cmdChannel is a channel that acts as a lock to serialize all commands from the CLI, meshage, domain socket, and other sources. All cli* handlers assume this channel is used for synchronization when they are invoked. runCommands, which wraps minicli.ProcessCommand, adds commands to the cmdChannel and should be used for all asynchronous tasks (e.g. handling meshage requests). minicli.ProcessCommand should only be called by runCommands.

This channel greatly reduces the overall locking in minimega.

vmLock

vmLock synchronizes all access to the global VMs map. All exported functions on the VMs type handle locking automatically. Developers should not range over the VMs map or access a VM by key -- these functionalities should only be performed by the exported functions.

VM.lock

VM.lock synchronizes all access to a single VM including performing lifecycle operations, updating attributes, and accessing tags.

Note: newly created VMs are returned in the locked state. This ensures that the only valid operation on a new VM is Launch.

meshageCommandLock

meshageCommandLock ensures that only one meshageSend operation can occur at a time. The lock is released once all the responses are read from the returned channel.

containerInitLock

containerInitLock ensures that we only initialize the container environment for minimega once, when we try to launch the first container.

namespaceLock

namespaceLock synchronizes all operations regarding namespaces including getting and setting the active namespace and creating a new namespace. The exported *Namespace functions acquire this lock automatically. We currently do not use this lock to synchronize access to the underlying Namespace structs -- these should be synchronized via the cmdChannel used by runCommands.

Hierarchy of locks

One way to prevent deadlocks in programs with multiple locks is to ensure that threads always acquire locks in the same order. We attempt to follow this idea and have defined the following hierarchy:

flowchart TD
    A[cmdChannel] --> B[vmLock]
    B --> C[VM.lock]
    C --> D[All other locks]

    subgraph M[minimega package]
        A
        B
        C
        D
    end

    M ==> O[Locks in other packages]

Developers must ensure that any blocking operations on channels do not implicitly pass locks to threads in violation with the hierarchy.

Locking in other packages

Other packages may contain their own locking mechanisms. We need to be careful about other packages using callbacks from minimega (or sending via goroutine) to ensure that we do not create a deadlock. Below we detail the packages where this may occur.

ipmac

We (incorrectly) allow a data race (but avoid a deadlock!).

Note: this should be fixed... (see #549).

ron

We register VMs with ron so that it can query a VM's tags, namespace, and set that CC is active. These operations all acquire the VM lock. In order to avoid a potential deadlock, VMs should not call any ron operations while holding their own lock (with the exception of ron.Server.RegisterVM -- the VM is not registered so it cannot cause a deadlock).