Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
7da3d655
Unverified
Commit
7da3d655
authored
Jun 01, 2018
by
Jordan Liggitt
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Avoid deadlock in gc resync if available resources change during sync
parent
268dc570
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
130 additions
and
36 deletions
+130
-36
garbagecollector.go
pkg/controller/garbagecollector/garbagecollector.go
+82
-31
garbagecollector_test.go
pkg/controller/garbagecollector/garbagecollector_test.go
+45
-4
graph_builder.go
pkg/controller/garbagecollector/graph_builder.go
+3
-1
No files found.
pkg/controller/garbagecollector/garbagecollector.go
View file @
7da3d655
...
@@ -32,6 +32,7 @@ import (
...
@@ -32,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/types"
utilerrors
"k8s.io/apimachinery/pkg/util/errors"
utilerrors
"k8s.io/apimachinery/pkg/util/errors"
utilruntime
"k8s.io/apimachinery/pkg/util/runtime"
utilruntime
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic"
...
@@ -170,10 +171,8 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
...
@@ -170,10 +171,8 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
newResources
:=
GetDeletableResources
(
discoveryClient
)
newResources
:=
GetDeletableResources
(
discoveryClient
)
// This can occur if there is an internal error in GetDeletableResources.
// This can occur if there is an internal error in GetDeletableResources.
// If the gc attempts to sync with 0 resources it will block forever.
// TODO: Implement a more complete solution for the garbage collector hanging.
if
len
(
newResources
)
==
0
{
if
len
(
newResources
)
==
0
{
glog
.
V
(
5
)
.
Infof
(
"no resources reported by discovery, skipping garbage collector sync"
)
glog
.
V
(
2
)
.
Infof
(
"no resources reported by discovery, skipping garbage collector sync"
)
return
return
}
}
...
@@ -183,39 +182,61 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
...
@@ -183,39 +182,61 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
return
return
}
}
// Something has changed, time to sync.
glog
.
V
(
2
)
.
Infof
(
"syncing garbage collector with updated resources from discovery: %v"
,
newResources
)
// Ensure workers are paused to avoid processing events before informers
// Ensure workers are paused to avoid processing events before informers
// have resynced.
// have resynced.
gc
.
workerLock
.
Lock
()
gc
.
workerLock
.
Lock
()
defer
gc
.
workerLock
.
Unlock
()
defer
gc
.
workerLock
.
Unlock
()
// Resetting the REST mapper will also invalidate the underlying discovery
// Once we get here, we should not unpause workers until we've successfully synced
// client. This is a leaky abstraction and assumes behavior about the REST
attempt
:=
0
// mapper, but we'll deal with it for now.
wait
.
PollImmediateUntil
(
100
*
time
.
Millisecond
,
func
()
(
bool
,
error
)
{
gc
.
restMapper
.
Reset
()
attempt
++
// Perform the monitor resync and wait for controllers to report cache sync.
// On a reattempt, check if available resources have changed
//
if
attempt
>
1
{
// NOTE: It's possible that newResources will diverge from the resources
newResources
=
GetDeletableResources
(
discoveryClient
)
// discovered by restMapper during the call to Reset, since they are
if
len
(
newResources
)
==
0
{
// distinct discovery clients invalidated at different times. For example,
glog
.
V
(
2
)
.
Infof
(
"no resources reported by discovery (attempt %d)"
,
attempt
)
// newResources may contain resources not returned in the restMapper's
return
false
,
nil
// discovery call if the resources appeared in-between the calls. In that
}
// case, the restMapper will fail to map some of newResources until the next
}
// sync period.
if
err
:=
gc
.
resyncMonitors
(
newResources
);
err
!=
nil
{
glog
.
V
(
2
)
.
Infof
(
"syncing garbage collector with updated resources from discovery (attempt %d): %s"
,
attempt
,
printDiff
(
oldResources
,
newResources
))
utilruntime
.
HandleError
(
fmt
.
Errorf
(
"failed to sync resource monitors: %v"
,
err
))
return
// Resetting the REST mapper will also invalidate the underlying discovery
}
// client. This is a leaky abstraction and assumes behavior about the REST
// TODO: WaitForCacheSync can block forever during normal operation. Could
// mapper, but we'll deal with it for now.
// pass a timeout channel, but we have to consider the implications of
gc
.
restMapper
.
Reset
()
// un-pausing the GC with a partially synced graph builder.
glog
.
V
(
4
)
.
Infof
(
"reset restmapper"
)
if
!
controller
.
WaitForCacheSync
(
"garbage collector"
,
stopCh
,
gc
.
dependencyGraphBuilder
.
IsSynced
)
{
utilruntime
.
HandleError
(
fmt
.
Errorf
(
"timed out waiting for dependency graph builder sync during GC sync"
))
// Perform the monitor resync and wait for controllers to report cache sync.
return
//
}
// NOTE: It's possible that newResources will diverge from the resources
// discovered by restMapper during the call to Reset, since they are
// distinct discovery clients invalidated at different times. For example,
// newResources may contain resources not returned in the restMapper's
// discovery call if the resources appeared in-between the calls. In that
// case, the restMapper will fail to map some of newResources until the next
// attempt.
if
err
:=
gc
.
resyncMonitors
(
newResources
);
err
!=
nil
{
utilruntime
.
HandleError
(
fmt
.
Errorf
(
"failed to sync resource monitors (attempt %d): %v"
,
attempt
,
err
))
return
false
,
nil
}
glog
.
V
(
4
)
.
Infof
(
"resynced monitors"
)
// wait for caches to fill for a while (our sync period) before attempting to rediscover resources and retry syncing.
// this protects us from deadlocks where available resources changed and one of our informer caches will never fill.
// informers keep attempting to sync in the background, so retrying doesn't interrupt them.
// the call to resyncMonitors on the reattempt will no-op for resources that still exist.
// note that workers stay paused until we successfully resync.
if
!
controller
.
WaitForCacheSync
(
"garbage collector"
,
waitForStopOrTimeout
(
stopCh
,
period
),
gc
.
dependencyGraphBuilder
.
IsSynced
)
{
utilruntime
.
HandleError
(
fmt
.
Errorf
(
"timed out waiting for dependency graph builder sync during GC sync (attempt %d)"
,
attempt
))
return
false
,
nil
}
// success, break out of the loop
return
true
,
nil
},
stopCh
)
// Finally, keep track of our new state. Do this after all preceding steps
// Finally, keep track of our new state. Do this after all preceding steps
// have succeeded to ensure we'll retry on subsequent syncs if an error
// have succeeded to ensure we'll retry on subsequent syncs if an error
...
@@ -225,6 +246,36 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
...
@@ -225,6 +246,36 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterf
},
period
,
stopCh
)
},
period
,
stopCh
)
}
}
// printDiff returns a human-readable summary of what resources were added and removed
func
printDiff
(
oldResources
,
newResources
map
[
schema
.
GroupVersionResource
]
struct
{})
string
{
removed
:=
sets
.
NewString
()
for
oldResource
:=
range
oldResources
{
if
_
,
ok
:=
newResources
[
oldResource
];
!
ok
{
removed
.
Insert
(
fmt
.
Sprintf
(
"%+v"
,
oldResource
))
}
}
added
:=
sets
.
NewString
()
for
newResource
:=
range
newResources
{
if
_
,
ok
:=
oldResources
[
newResource
];
!
ok
{
added
.
Insert
(
fmt
.
Sprintf
(
"%+v"
,
newResource
))
}
}
return
fmt
.
Sprintf
(
"added: %v, removed: %v"
,
added
.
List
(),
removed
.
List
())
}
// waitForStopOrTimeout returns a stop channel that closes when the provided stop channel closes or when the specified timeout is reached
func
waitForStopOrTimeout
(
stopCh
<-
chan
struct
{},
timeout
time
.
Duration
)
<-
chan
struct
{}
{
stopChWithTimeout
:=
make
(
chan
struct
{})
go
func
()
{
select
{
case
<-
stopCh
:
case
<-
time
.
After
(
timeout
)
:
}
close
(
stopChWithTimeout
)
}()
return
stopChWithTimeout
}
func
(
gc
*
GarbageCollector
)
IsSynced
()
bool
{
func
(
gc
*
GarbageCollector
)
IsSynced
()
bool
{
return
gc
.
dependencyGraphBuilder
.
IsSynced
()
return
gc
.
dependencyGraphBuilder
.
IsSynced
()
}
}
...
...
pkg/controller/garbagecollector/garbagecollector_test.go
View file @
7da3d655
...
@@ -800,6 +800,15 @@ func TestGarbageCollectorSync(t *testing.T) {
...
@@ -800,6 +800,15 @@ func TestGarbageCollectorSync(t *testing.T) {
},
},
},
},
}
}
unsyncableServerResources
:=
[]
*
metav1
.
APIResourceList
{
{
GroupVersion
:
"v1"
,
APIResources
:
[]
metav1
.
APIResource
{
{
Name
:
"pods"
,
Namespaced
:
true
,
Kind
:
"Pod"
,
Verbs
:
metav1
.
Verbs
{
"delete"
,
"list"
,
"watch"
}},
{
Name
:
"secrets"
,
Namespaced
:
true
,
Kind
:
"Secret"
,
Verbs
:
metav1
.
Verbs
{
"delete"
,
"list"
,
"watch"
}},
},
},
}
fakeDiscoveryClient
:=
&
fakeServerResources
{
fakeDiscoveryClient
:=
&
fakeServerResources
{
PreferredResources
:
serverResources
,
PreferredResources
:
serverResources
,
Error
:
nil
,
Error
:
nil
,
...
@@ -813,6 +822,10 @@ func TestGarbageCollectorSync(t *testing.T) {
...
@@ -813,6 +822,10 @@ func TestGarbageCollectorSync(t *testing.T) {
200
,
200
,
[]
byte
(
"{}"
),
[]
byte
(
"{}"
),
},
},
"GET"
+
"/api/v1/secrets"
:
{
404
,
[]
byte
(
"{}"
),
},
},
},
}
}
srv
,
clientConfig
:=
testServerAndClientConfig
(
testHandler
.
ServeHTTP
)
srv
,
clientConfig
:=
testServerAndClientConfig
(
testHandler
.
ServeHTTP
)
...
@@ -849,7 +862,7 @@ func TestGarbageCollectorSync(t *testing.T) {
...
@@ -849,7 +862,7 @@ func TestGarbageCollectorSync(t *testing.T) {
fmt
.
Printf
(
"Test output"
)
fmt
.
Printf
(
"Test output"
)
time
.
Sleep
(
1
*
time
.
Second
)
time
.
Sleep
(
1
*
time
.
Second
)
err
=
expectSyncNotBlocked
(
fakeDiscoveryClient
)
err
=
expectSyncNotBlocked
(
fakeDiscoveryClient
,
&
gc
.
workerLock
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"Expected garbagecollector.Sync to be running but it is blocked: %v"
,
err
)
t
.
Fatalf
(
"Expected garbagecollector.Sync to be running but it is blocked: %v"
,
err
)
}
}
...
@@ -865,13 +878,29 @@ func TestGarbageCollectorSync(t *testing.T) {
...
@@ -865,13 +878,29 @@ func TestGarbageCollectorSync(t *testing.T) {
fakeDiscoveryClient
.
setPreferredResources
(
serverResources
)
fakeDiscoveryClient
.
setPreferredResources
(
serverResources
)
fakeDiscoveryClient
.
setError
(
nil
)
fakeDiscoveryClient
.
setError
(
nil
)
err
=
expectSyncNotBlocked
(
fakeDiscoveryClient
)
err
=
expectSyncNotBlocked
(
fakeDiscoveryClient
,
&
gc
.
workerLock
)
if
err
!=
nil
{
t
.
Fatalf
(
"Expected garbagecollector.Sync to still be running but it is blocked: %v"
,
err
)
}
// Simulate the discovery client returning a resource the restmapper can resolve, but will not sync caches
fakeDiscoveryClient
.
setPreferredResources
(
unsyncableServerResources
)
fakeDiscoveryClient
.
setError
(
nil
)
// Wait until sync discovers the change
time
.
Sleep
(
1
*
time
.
Second
)
// Put the resources back to normal and ensure garbage collector sync recovers
fakeDiscoveryClient
.
setPreferredResources
(
serverResources
)
fakeDiscoveryClient
.
setError
(
nil
)
err
=
expectSyncNotBlocked
(
fakeDiscoveryClient
,
&
gc
.
workerLock
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"Expected garbagecollector.Sync to still be running but it is blocked: %v"
,
err
)
t
.
Fatalf
(
"Expected garbagecollector.Sync to still be running but it is blocked: %v"
,
err
)
}
}
}
}
func
expectSyncNotBlocked
(
fakeDiscoveryClient
*
fakeServerResources
)
error
{
func
expectSyncNotBlocked
(
fakeDiscoveryClient
*
fakeServerResources
,
workerLock
*
sync
.
RWMutex
)
error
{
before
:=
fakeDiscoveryClient
.
getInterfaceUsedCount
()
before
:=
fakeDiscoveryClient
.
getInterfaceUsedCount
()
t
:=
1
*
time
.
Second
t
:=
1
*
time
.
Second
time
.
Sleep
(
t
)
time
.
Sleep
(
t
)
...
@@ -879,7 +908,19 @@ func expectSyncNotBlocked(fakeDiscoveryClient *fakeServerResources) error {
...
@@ -879,7 +908,19 @@ func expectSyncNotBlocked(fakeDiscoveryClient *fakeServerResources) error {
if
before
==
after
{
if
before
==
after
{
return
fmt
.
Errorf
(
"discoveryClient.ServerPreferredResources() called %d times over %v"
,
after
-
before
,
t
)
return
fmt
.
Errorf
(
"discoveryClient.ServerPreferredResources() called %d times over %v"
,
after
-
before
,
t
)
}
}
return
nil
workerLockAcquired
:=
make
(
chan
struct
{})
go
func
()
{
workerLock
.
Lock
()
workerLock
.
Unlock
()
close
(
workerLockAcquired
)
}()
select
{
case
<-
workerLockAcquired
:
return
nil
case
<-
time
.
After
(
t
)
:
return
fmt
.
Errorf
(
"workerLock blocked for at least %v"
,
t
)
}
}
}
type
fakeServerResources
struct
{
type
fakeServerResources
struct
{
...
...
pkg/controller/garbagecollector/graph_builder.go
View file @
7da3d655
...
@@ -288,11 +288,13 @@ func (gb *GraphBuilder) IsSynced() bool {
...
@@ -288,11 +288,13 @@ func (gb *GraphBuilder) IsSynced() bool {
defer
gb
.
monitorLock
.
Unlock
()
defer
gb
.
monitorLock
.
Unlock
()
if
len
(
gb
.
monitors
)
==
0
{
if
len
(
gb
.
monitors
)
==
0
{
glog
.
V
(
4
)
.
Info
(
"garbage controller monitor not synced: no monitors"
)
return
false
return
false
}
}
for
_
,
monitor
:=
range
gb
.
monitors
{
for
resource
,
monitor
:=
range
gb
.
monitors
{
if
!
monitor
.
controller
.
HasSynced
()
{
if
!
monitor
.
controller
.
HasSynced
()
{
glog
.
V
(
4
)
.
Infof
(
"garbage controller monitor not yet synced: %+v"
,
resource
)
return
false
return
false
}
}
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment