Skip to content

Commit 1aa1043

Browse files
committed
volumes: replace global lock with per-volume locking for concurrent mounts
The EFS volume plugin serializes all volume mount/unmount operations behind a single sync.RWMutex. When multiple ECS tasks start concurrently, each requiring EFS volume mounts (15-72s each), queued requests exceed Docker's plugin timeout (~60s), causing CannotCreateContainerError/TaskFailedToStart. This change introduces per-volume mutexes so that mount/unmount I/O for different volumes can proceed in parallel: - AmazonECSVolumePlugin: replace single `lock` with `mapLock` (short-lived, protects map access only) + `volLocks` (per-volume mutexes held during I/O) - ECSVolumeDriver: release driver lock before mount/unmount syscalls since the plugin layer already holds the per-volume lock - Add TestConcurrentMountsDifferentVolumes to verify parallel execution Operations on the same volume remain serialized for correctness. Metadata-only operations (Create, List, Get, Path) continue using the global map lock since they don't perform I/O. Fixes aws/containers-roadmap#2319
1 parent 024663e commit 1aa1043

3 files changed

Lines changed: 191 additions & 45 deletions

File tree

ecs-init/volumes/ecs_volume_driver.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ func (e *ECSVolumeDriver) Setup(name string, v *types.Volume) {
6161
e.volumeMounts[name] = mnt
6262
}
6363

64-
// Create implements ECSVolumeDriver's Create volume method
64+
// Create implements ECSVolumeDriver's Create volume method.
65+
// The lock is held only for map access and validation; the actual mount I/O
66+
// proceeds without the driver lock since the caller holds a per-volume lock.
6567
func (e *ECSVolumeDriver) Create(r *driver.CreateRequest) error {
66-
e.lock.Lock()
67-
defer e.lock.Unlock()
68-
6968
mnt := setOptions(r.Options)
7069
mnt.Target = r.Path
7170

@@ -74,12 +73,26 @@ func (e *ECSVolumeDriver) Create(r *driver.CreateRequest) error {
7473
return err
7574
}
7675

76+
// Check for duplicates under read lock
77+
e.lock.RLock()
78+
if _, exists := e.volumeMounts[r.Name]; exists {
79+
e.lock.RUnlock()
80+
return fmt.Errorf("volume %s already mounted", r.Name)
81+
}
82+
e.lock.RUnlock()
83+
84+
// Perform the mount I/O without holding the driver lock
7785
seelog.Infof("Mounting volume %s of type %s at path %s", r.Name, mnt.MountType, mnt.Target)
7886
err := mnt.Mount()
7987
if err != nil {
8088
return fmt.Errorf("mounting volume failed: %v", err)
8189
}
90+
91+
// Register the mount under write lock
92+
e.lock.Lock()
8293
e.volumeMounts[r.Name] = mnt
94+
e.lock.Unlock()
95+
8396
return nil
8497
}
8598

@@ -98,27 +111,37 @@ func setOptions(options map[string]string) *MountHelper {
98111
return mnt
99112
}
100113

101-
// Remove implements ECSVolumeDriver's Remove volume method
114+
// Remove implements ECSVolumeDriver's Remove volume method.
115+
// The unmount I/O proceeds without the driver lock since the caller holds a per-volume lock.
102116
func (e *ECSVolumeDriver) Remove(req *driver.RemoveRequest) error {
103-
e.lock.Lock()
104-
defer e.lock.Unlock()
117+
e.lock.RLock()
105118
mnt, ok := e.volumeMounts[req.Name]
119+
e.lock.RUnlock()
120+
106121
if !ok {
107122
return fmt.Errorf("volume not found")
108123
}
124+
125+
// Perform the unmount I/O without holding the driver lock
109126
err := mnt.Unmount()
110127
if err != nil {
111128
if strings.Contains(err.Error(), notMountedErrMsg) ||
112129
strings.Contains(err.Error(), noMountPointSpecifiedErrMsg) {
113130
seelog.Infof("Unmounting volume %s failed because it's not mounted.", req.Name)
131+
e.lock.Lock()
114132
delete(e.volumeMounts, req.Name)
133+
e.lock.Unlock()
115134
return nil
116135
}
117136
return fmt.Errorf("unmounting volume failed: %v", err)
118137
}
138+
139+
e.lock.Lock()
119140
delete(e.volumeMounts, req.Name)
141+
e.lock.Unlock()
142+
120143
seelog.Infof("Unmounted volume %s successfully.", req.Name)
121-
return err
144+
return nil
122145
}
123146

124147
// Method to check if a volume is currently mounted.

ecs-init/volumes/ecs_volume_plugin.go

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ type AmazonECSVolumePlugin struct {
3838
volumeDrivers map[string]driver.VolumeDriver
3939
volumes map[string]*types.Volume
4040
state *StateManager
41-
lock sync.RWMutex
41+
// mapLock protects the volumes map for short lookups and metadata-only mutations.
42+
// It must NOT be held during I/O operations (mount/unmount syscalls).
43+
mapLock sync.RWMutex
44+
// volLocks provides per-volume mutexes so that mount/unmount I/O for
45+
// different volumes can proceed concurrently.
46+
volLocks map[string]*sync.Mutex
4247
}
4348

4449
// NewAmazonECSVolumePlugin initiates the volume drivers
@@ -48,16 +53,28 @@ func NewAmazonECSVolumePlugin() *AmazonECSVolumePlugin {
4853
"efs": NewECSVolumeDriver(),
4954
"s3files": NewECSVolumeDriver(),
5055
},
51-
volumes: make(map[string]*types.Volume),
52-
state: NewStateManager(),
56+
volumes: make(map[string]*types.Volume),
57+
state: NewStateManager(),
58+
volLocks: make(map[string]*sync.Mutex),
5359
}
5460
return plugin
5561
}
5662

63+
// getOrCreateVolLock returns the per-volume mutex, creating one if it doesn't exist.
64+
// Caller must hold mapLock (at least RLock for read, Lock for create).
65+
func (a *AmazonECSVolumePlugin) getOrCreateVolLock(name string) *sync.Mutex {
66+
if mu, ok := a.volLocks[name]; ok {
67+
return mu
68+
}
69+
mu := &sync.Mutex{}
70+
a.volLocks[name] = mu
71+
return mu
72+
}
73+
5774
// LoadState loads past state information of the plugin
5875
func (a *AmazonECSVolumePlugin) LoadState() error {
59-
a.lock.Lock()
60-
defer a.lock.Unlock()
76+
a.mapLock.Lock()
77+
defer a.mapLock.Unlock()
6178
seelog.Info("Loading plugin state information")
6279
oldState := &VolumeState{}
6380
if !fileExists(PluginStateFileAbsPath) {
@@ -96,6 +113,7 @@ func (a *AmazonECSVolumePlugin) LoadState() error {
96113
Mounts: vol.Mounts,
97114
}
98115
a.volumes[volName] = volume
116+
a.getOrCreateVolLock(volName)
99117
voldriver.Setup(volName, volume)
100118
}
101119
a.state.VolState = oldState
@@ -112,10 +130,11 @@ func (a *AmazonECSVolumePlugin) getVolumeDriver(driverType string) (driver.Volum
112130
return a.volumeDrivers[driverType], nil
113131
}
114132

115-
// Create implements Docker volume plugin's Create Method
133+
// Create implements Docker volume plugin's Create Method.
134+
// Create is metadata-only (no I/O), so the global lock is acceptable here.
116135
func (a *AmazonECSVolumePlugin) Create(r *volume.CreateRequest) error {
117-
a.lock.Lock()
118-
defer a.lock.Unlock()
136+
a.mapLock.Lock()
137+
defer a.mapLock.Unlock()
119138

120139
seelog.Infof("Creating new volume %s", r.Name)
121140
_, ok := a.volumes[r.Name]
@@ -162,6 +181,7 @@ func (a *AmazonECSVolumePlugin) Create(r *volume.CreateRequest) error {
162181
}
163182
// record the volume information
164183
a.volumes[r.Name] = vol
184+
a.getOrCreateVolLock(r.Name)
165185
seelog.Infof("Saving state of new volume %s", r.Name)
166186
// save the state of new volume
167187
err = a.state.recordVolume(r.Name, vol)
@@ -198,7 +218,8 @@ func deleteMountPath(path string) error {
198218
return os.Remove(path)
199219
}
200220

201-
// Mount implements Docker volume plugin's Mount Method
221+
// Mount implements Docker volume plugin's Mount Method.
222+
// Uses per-volume locking so that mounts for different volumes proceed concurrently.
202223
func (a *AmazonECSVolumePlugin) Mount(r *volume.MountRequest) (*volume.MountResponse, error) {
203224
seelog.Infof("Received mount request %+v", r)
204225

@@ -210,27 +231,30 @@ func (a *AmazonECSVolumePlugin) Mount(r *volume.MountRequest) (*volume.MountResp
210231
return nil, fmt.Errorf("no mount ID in the request")
211232
}
212233

213-
// Acquire write lock
214-
a.lock.Lock()
215-
defer a.lock.Unlock()
216-
217-
// Find the volume
234+
// Phase 1: short global lock to look up volume metadata and per-volume lock
235+
a.mapLock.RLock()
218236
vol, ok := a.volumes[r.Name]
219237
if !ok {
238+
a.mapLock.RUnlock()
220239
seelog.Errorf("Volume %s to mount is not found", r.Name)
221240
return nil, fmt.Errorf("volume %s not found", r.Name)
222241
}
223-
224-
// Find the volume driver
225242
volDriver, err := a.getVolumeDriver(vol.Type)
226243
if err != nil {
244+
a.mapLock.RUnlock()
227245
seelog.Errorf("Volume %s's driver type %s not supported: %v", r.Name, vol.Type, err)
228246
return nil, fmt.Errorf("Volume %s's driver type %s not supported: %w", r.Name, vol.Type, err)
229247
}
230248
if volDriver == nil {
231-
// This case shouldn't happen normally
249+
a.mapLock.RUnlock()
232250
return nil, fmt.Errorf("no volume driver found for type %s", vol.Type)
233251
}
252+
volMu := a.getOrCreateVolLock(r.Name)
253+
a.mapLock.RUnlock()
254+
255+
// Phase 2: per-volume lock — only this volume is blocked, others proceed freely
256+
volMu.Lock()
257+
defer volMu.Unlock()
234258

235259
// Mount the volume on the host if there are no active mounts for the volume.
236260
if len(vol.Mounts) == 0 {
@@ -264,7 +288,8 @@ func (a *AmazonECSVolumePlugin) Mount(r *volume.MountRequest) (*volume.MountResp
264288
return &volume.MountResponse{Mountpoint: vol.Path}, nil
265289
}
266290

267-
// Unmount implements Docker volume plugin's Unmount Method
291+
// Unmount implements Docker volume plugin's Unmount Method.
292+
// Uses per-volume locking so that unmounts for different volumes proceed concurrently.
268293
func (a *AmazonECSVolumePlugin) Unmount(r *volume.UnmountRequest) error {
269294
seelog.Infof("Received unmount request %+v", r)
270295

@@ -276,27 +301,30 @@ func (a *AmazonECSVolumePlugin) Unmount(r *volume.UnmountRequest) error {
276301
return fmt.Errorf("no mount ID in the request")
277302
}
278303

279-
// Acquire write lock
280-
a.lock.Lock()
281-
defer a.lock.Unlock()
282-
283-
// Find the volume
304+
// Phase 1: short global lock to look up volume metadata and per-volume lock
305+
a.mapLock.RLock()
284306
vol, ok := a.volumes[r.Name]
285307
if !ok {
308+
a.mapLock.RUnlock()
286309
seelog.Errorf("Volume %s to unmount is not found", r.Name)
287310
return fmt.Errorf("volume %s not found", r.Name)
288311
}
289-
290-
// Get the corresponding volume driver
291312
volDriver, err := a.getVolumeDriver(vol.Type)
292313
if err != nil {
314+
a.mapLock.RUnlock()
293315
seelog.Errorf("Volume %s removal failure: %v", r.Name, err)
294316
return fmt.Errorf("volume %v of type %s is unsupported: %w", r.Name, vol.Type, err)
295317
}
296318
if volDriver == nil {
297-
// this case should not happen normally
319+
a.mapLock.RUnlock()
298320
return fmt.Errorf("no corresponding volume driver found for type %s", vol.Type)
299321
}
322+
volMu := a.getOrCreateVolLock(r.Name)
323+
a.mapLock.RUnlock()
324+
325+
// Phase 2: per-volume lock
326+
volMu.Lock()
327+
defer volMu.Unlock()
300328

301329
// Remove the mount from the volume
302330
seelog.Infof("Removing mount %s from volume %s", r.ID, r.Name)
@@ -324,12 +352,13 @@ func (a *AmazonECSVolumePlugin) Unmount(r *volume.UnmountRequest) error {
324352
return nil
325353
}
326354

327-
// Remove implements Docker volume plugin's Remove Method
355+
// Remove implements Docker volume plugin's Remove Method.
356+
// Uses global write lock since it mutates the volumes map.
328357
func (a *AmazonECSVolumePlugin) Remove(r *volume.RemoveRequest) error {
329358
seelog.Infof("Received Remove request %+v", r)
330359

331-
a.lock.Lock()
332-
defer a.lock.Unlock()
360+
a.mapLock.Lock()
361+
defer a.mapLock.Unlock()
333362

334363
seelog.Infof("Removing volume %s", r.Name)
335364
vol, ok := a.volumes[r.Name]
@@ -362,6 +391,7 @@ func (a *AmazonECSVolumePlugin) Remove(r *volume.RemoveRequest) error {
362391

363392
// remove the volume information
364393
delete(a.volumes, r.Name)
394+
delete(a.volLocks, r.Name)
365395
// cleanup the volume's host mount path
366396
err = a.CleanupMountPath(vol.Path)
367397
if err != nil {
@@ -378,8 +408,8 @@ func (a *AmazonECSVolumePlugin) Remove(r *volume.RemoveRequest) error {
378408

379409
// List implements Docker volume plugin's List Method
380410
func (a *AmazonECSVolumePlugin) List() (*volume.ListResponse, error) {
381-
a.lock.RLock()
382-
defer a.lock.RUnlock()
411+
a.mapLock.RLock()
412+
defer a.mapLock.RUnlock()
383413
vols := make([]*volume.Volume, len(a.volumes))
384414
i := 0
385415
for volName := range a.volumes {
@@ -396,8 +426,8 @@ func (a *AmazonECSVolumePlugin) List() (*volume.ListResponse, error) {
396426

397427
// Get implements Docker volume plugin's Get Method
398428
func (a *AmazonECSVolumePlugin) Get(r *volume.GetRequest) (*volume.GetResponse, error) {
399-
a.lock.RLock()
400-
defer a.lock.RUnlock()
429+
a.mapLock.RLock()
430+
defer a.mapLock.RUnlock()
401431
vol, ok := a.volumes[r.Name]
402432
if !ok {
403433
return nil, fmt.Errorf("volume %s not found", r.Name)
@@ -413,8 +443,8 @@ func (a *AmazonECSVolumePlugin) Get(r *volume.GetRequest) (*volume.GetResponse,
413443

414444
// Path implements Docker volume plugin's Path Method
415445
func (a *AmazonECSVolumePlugin) Path(r *volume.PathRequest) (*volume.PathResponse, error) {
416-
a.lock.RLock()
417-
defer a.lock.RUnlock()
446+
a.mapLock.RLock()
447+
defer a.mapLock.RUnlock()
418448
vol, ok := a.volumes[r.Name]
419449
if !ok {
420450
seelog.Errorf("Could not find mount path for volume %s", r.Name)

0 commit comments

Comments
 (0)