-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ClusterCacheTracker: non-blocking per-cluster locking
Co-authored-by: Florian Gutmann <[email protected]>
- Loading branch information
1 parent
ebbd333
commit 37a60e5
Showing
5 changed files
with
247 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
Copyright 2021 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package remote | ||
|
||
import "sync" | ||
|
||
// keyedMutex is a mutex locking on the key provided to the Lock function. | ||
// Only one caller can hold the lock for a specific key at a time. | ||
// A second Lock call if the lock is already held for a key returns false. | ||
type keyedMutex struct { | ||
locksMtx sync.Mutex | ||
locks map[interface{}]*sync.Mutex | ||
} | ||
|
||
// newKeyedMutex creates a new keyed mutex ready for use. | ||
func newKeyedMutex() *keyedMutex { | ||
return &keyedMutex{ | ||
locks: make(map[interface{}]*sync.Mutex), | ||
} | ||
} | ||
|
||
// unlock unlocks a currently locked key. | ||
type unlock func() | ||
|
||
// TryLock locks the passed in key if it's not already locked. | ||
// Returns the unlock function to release the lock on the key. | ||
// A second Lock call if the lock is already held for a key returns false. | ||
// In the ClusterCacheTracker case the key is the ObjectKey for a cluster. | ||
func (k *keyedMutex) TryLock(key interface{}) (unlock, bool) { | ||
// Get the lock if it doesn't exist already. | ||
// If it does exist, return false. | ||
l, ok := func() (*sync.Mutex, bool) { | ||
k.locksMtx.Lock() | ||
defer k.locksMtx.Unlock() | ||
|
||
l, ok := k.locks[key] | ||
if !ok { | ||
// Lock doesn't exist yet, create one and return it. | ||
l = &sync.Mutex{} | ||
k.locks[key] = l | ||
return l, true | ||
} | ||
|
||
// Lock already exists, return false. | ||
return nil, false | ||
}() | ||
|
||
// Return false if another go routine already holds the lock for this key (e.g. Cluster). | ||
if !ok { | ||
return nil, false | ||
} | ||
|
||
// Lock for the current key (e.g. Cluster). | ||
l.Lock() | ||
|
||
// Unlock the key (e.g. Cluster) and remove it from the lock map. | ||
return func() { | ||
k.locksMtx.Lock() | ||
defer k.locksMtx.Unlock() | ||
|
||
l.Unlock() | ||
delete(k.locks, key) | ||
}, true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
Copyright 2021 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package remote | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/gomega" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
func TestKeyedMutex(t *testing.T) { | ||
t.Run("Lock a Cluster and ensures the second Lock on the same Cluster returns false", func(t *testing.T) { | ||
t.Parallel() | ||
g := NewWithT(t) | ||
|
||
cluster1 := client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: "Cluster1"} | ||
km := newKeyedMutex() | ||
|
||
// Try to lock cluster1. | ||
// Should work as nobody currently holds the lock for cluster1. | ||
unlock, ok := km.TryLock(cluster1) | ||
g.Expect(ok).To(BeTrue()) | ||
|
||
// Try to lock cluster1 again. | ||
// Shouldn't work as cluster1 is already locked. | ||
_, ok = km.TryLock(cluster1) | ||
g.Expect(ok).To(BeFalse()) | ||
|
||
// Unlock cluster1. | ||
unlock() | ||
|
||
// Ensure that the lock was cleaned up from the internal map. | ||
g.Expect(km.locks).To(HaveLen(0)) | ||
}) | ||
|
||
t.Run("Can lock different Clusters in parallel but each one only once", func(t *testing.T) { | ||
g := NewWithT(t) | ||
km := newKeyedMutex() | ||
clusters := []client.ObjectKey{ | ||
{Namespace: metav1.NamespaceDefault, Name: "Cluster1"}, | ||
{Namespace: metav1.NamespaceDefault, Name: "Cluster2"}, | ||
{Namespace: metav1.NamespaceDefault, Name: "Cluster3"}, | ||
{Namespace: metav1.NamespaceDefault, Name: "Cluster4"}, | ||
} | ||
|
||
// Run this twice to ensure Clusters can be locked again | ||
// after they have been unlocked. | ||
for i := 0; i < 2; i++ { | ||
unlocks := make([]unlock, 0, len(clusters)) | ||
|
||
// Lock all Clusters (should work). | ||
for _, key := range clusters { | ||
unlock, ok := km.TryLock(key) | ||
g.Expect(ok).To(BeTrue()) | ||
unlocks = append(unlocks, unlock) | ||
} | ||
|
||
// Ensure Clusters can't be locked again. | ||
for _, key := range clusters { | ||
_, ok := km.TryLock(key) | ||
g.Expect(ok).To(BeFalse()) | ||
} | ||
|
||
// Unlock all Clusters. | ||
for _, unlock := range unlocks { | ||
unlock() | ||
} | ||
} | ||
|
||
// Ensure that the lock was cleaned up from the internal map. | ||
g.Expect(km.locks).To(HaveLen(0)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters