@@ -19,51 +19,102 @@ package cached
19
19
import (
20
20
"errors"
21
21
"fmt"
22
+ "net"
23
+ "net/url"
22
24
"sync"
25
+ "syscall"
23
26
24
27
"github.com/googleapis/gnostic/OpenAPIv2"
25
28
29
+ errorsutil "k8s.io/apimachinery/pkg/api/errors"
26
30
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27
31
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
28
32
"k8s.io/apimachinery/pkg/version"
29
33
"k8s.io/client-go/discovery"
30
34
restclient "k8s.io/client-go/rest"
31
35
)
32
36
37
+ type cacheEntry struct {
38
+ resourceList * metav1.APIResourceList
39
+ err error
40
+ }
41
+
33
42
// memCacheClient can Invalidate() to stay up-to-date with discovery
34
43
// information.
35
44
//
36
- // TODO: Switch to a watch interface. Right now it will poll anytime
37
- // Invalidate() is called .
45
+ // TODO: Switch to a watch interface. Right now it will poll after each
46
+ // Invalidate() call .
38
47
type memCacheClient struct {
39
48
delegate discovery.DiscoveryInterface
40
49
41
50
lock sync.RWMutex
42
- groupToServerResources map [string ]* metav1. APIResourceList
51
+ groupToServerResources map [string ]* cacheEntry
43
52
groupList * metav1.APIGroupList
44
53
cacheValid bool
45
54
}
46
55
47
56
// Error Constants
48
57
var (
49
- ErrCacheEmpty = errors .New ("the cache has not been filled yet" )
50
58
ErrCacheNotFound = errors .New ("not found" )
51
59
)
52
60
53
61
var _ discovery.CachedDiscoveryInterface = & memCacheClient {}
54
62
63
+ // isTransientConnectionError checks whether given error is "Connection refused" or
64
+ // "Connection reset" error which usually means that apiserver is temporarily
65
+ // unavailable.
66
+ func isTransientConnectionError (err error ) bool {
67
+ urlError , ok := err .(* url.Error )
68
+ if ! ok {
69
+ return false
70
+ }
71
+ opError , ok := urlError .Err .(* net.OpError )
72
+ if ! ok {
73
+ return false
74
+ }
75
+ errno , ok := opError .Err .(syscall.Errno )
76
+ if ! ok {
77
+ return false
78
+ }
79
+ return errno == syscall .ECONNREFUSED || errno == syscall .ECONNRESET
80
+ }
81
+
82
+ func isTransientError (err error ) bool {
83
+ if isTransientConnectionError (err ) {
84
+ return true
85
+ }
86
+
87
+ if t , ok := err .(errorsutil.APIStatus ); ok && t .Status ().Code >= 500 {
88
+ return true
89
+ }
90
+
91
+ return errorsutil .IsTooManyRequests (err )
92
+ }
93
+
55
94
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
56
95
func (d * memCacheClient ) ServerResourcesForGroupVersion (groupVersion string ) (* metav1.APIResourceList , error ) {
57
- d .lock .RLock ()
58
- defer d .lock .RUnlock ()
96
+ d .lock .Lock ()
97
+ defer d .lock .Unlock ()
59
98
if ! d .cacheValid {
60
- return nil , ErrCacheEmpty
99
+ if err := d .refreshLocked (); err != nil {
100
+ return nil , err
101
+ }
61
102
}
62
103
cachedVal , ok := d .groupToServerResources [groupVersion ]
63
104
if ! ok {
64
105
return nil , ErrCacheNotFound
65
106
}
66
- return cachedVal , nil
107
+
108
+ if cachedVal .err != nil && isTransientError (cachedVal .err ) {
109
+ r , err := d .serverResourcesForGroupVersion (groupVersion )
110
+ if err != nil {
111
+ utilruntime .HandleError (fmt .Errorf ("couldn't get resource list for %v: %v" , groupVersion , err ))
112
+ }
113
+ cachedVal = & cacheEntry {r , err }
114
+ d .groupToServerResources [groupVersion ] = cachedVal
115
+ }
116
+
117
+ return cachedVal .resourceList , cachedVal .err
67
118
}
68
119
69
120
// ServerResources returns the supported resources for all groups and versions.
@@ -72,10 +123,12 @@ func (d *memCacheClient) ServerResources() ([]*metav1.APIResourceList, error) {
72
123
}
73
124
74
125
func (d * memCacheClient ) ServerGroups () (* metav1.APIGroupList , error ) {
75
- d .lock .RLock ()
76
- defer d .lock .RUnlock ()
77
- if d .groupList == nil {
78
- return nil , ErrCacheEmpty
126
+ d .lock .Lock ()
127
+ defer d .lock .Unlock ()
128
+ if ! d .cacheValid {
129
+ if err := d .refreshLocked (); err != nil {
130
+ return nil , err
131
+ }
79
132
}
80
133
return d .groupList , nil
81
134
}
@@ -103,49 +156,59 @@ func (d *memCacheClient) OpenAPISchema() (*openapi_v2.Document, error) {
103
156
func (d * memCacheClient ) Fresh () bool {
104
157
d .lock .RLock ()
105
158
defer d .lock .RUnlock ()
106
- // Fresh is supposed to tell the caller whether or not to retry if the cache
107
- // fails to find something. The idea here is that Invalidate will be called
108
- // periodically and therefore we'll always be returning the latest data. (And
109
- // in the future we can watch and stay even more up-to-date.) So we only
110
- // return false if the cache has never been filled.
159
+ // Return whether the cache is populated at all. It is still possible that
160
+ // a single entry is missing due to transient errors and the attempt to read
161
+ // that entry will trigger retry.
111
162
return d .cacheValid
112
163
}
113
164
114
- // Invalidate refreshes the cache, blocking calls until the cache has been
115
- // refreshed. It would be trivial to make a version that does this in the
116
- // background while continuing to respond to requests if needed.
165
+ // Invalidate enforces that no cached data that is older than the current time
166
+ // is used.
117
167
func (d * memCacheClient ) Invalidate () {
118
168
d .lock .Lock ()
119
169
defer d .lock .Unlock ()
170
+ d .cacheValid = false
171
+ d .groupToServerResources = nil
172
+ d .groupList = nil
173
+ }
120
174
175
+ // refreshLocked refreshes the state of cache. The caller must hold d.lock for
176
+ // writing.
177
+ func (d * memCacheClient ) refreshLocked () error {
121
178
// TODO: Could this multiplicative set of calls be replaced by a single call
122
179
// to ServerResources? If it's possible for more than one resulting
123
180
// APIResourceList to have the same GroupVersion, the lists would need merged.
124
181
gl , err := d .delegate .ServerGroups ()
125
182
if err != nil || len (gl .Groups ) == 0 {
126
- utilruntime .HandleError (fmt .Errorf ("couldn't get current server API group list; will keep using cached value. (%v) " , err ))
127
- return
183
+ utilruntime .HandleError (fmt .Errorf ("couldn't get current server API group list: %v " , err ))
184
+ return err
128
185
}
129
186
130
- rl := map [string ]* metav1. APIResourceList {}
187
+ rl := map [string ]* cacheEntry {}
131
188
for _ , g := range gl .Groups {
132
189
for _ , v := range g .Versions {
133
- r , err := d .delegate .ServerResourcesForGroupVersion (v .GroupVersion )
134
- if err != nil || len (r .APIResources ) == 0 {
190
+ r , err := d .serverResourcesForGroupVersion (v .GroupVersion )
191
+ rl [v .GroupVersion ] = & cacheEntry {r , err }
192
+ if err != nil {
135
193
utilruntime .HandleError (fmt .Errorf ("couldn't get resource list for %v: %v" , v .GroupVersion , err ))
136
- if cur , ok := d .groupToServerResources [v .GroupVersion ]; ok {
137
- // retain the existing list, if we had it.
138
- r = cur
139
- } else {
140
- continue
141
- }
142
194
}
143
- rl [v .GroupVersion ] = r
144
195
}
145
196
}
146
197
147
198
d .groupToServerResources , d .groupList = rl , gl
148
199
d .cacheValid = true
200
+ return nil
201
+ }
202
+
203
+ func (d * memCacheClient ) serverResourcesForGroupVersion (groupVersion string ) (* metav1.APIResourceList , error ) {
204
+ r , err := d .delegate .ServerResourcesForGroupVersion (groupVersion )
205
+ if err != nil {
206
+ return r , err
207
+ }
208
+ if len (r .APIResources ) == 0 {
209
+ return r , fmt .Errorf ("Got empty response for: %v" , groupVersion )
210
+ }
211
+ return r , nil
149
212
}
150
213
151
214
// NewMemCacheClient creates a new CachedDiscoveryInterface which caches
@@ -156,6 +219,6 @@ func (d *memCacheClient) Invalidate() {
156
219
func NewMemCacheClient (delegate discovery.DiscoveryInterface ) discovery.CachedDiscoveryInterface {
157
220
return & memCacheClient {
158
221
delegate : delegate ,
159
- groupToServerResources : map [string ]* metav1. APIResourceList {},
222
+ groupToServerResources : map [string ]* cacheEntry {},
160
223
}
161
224
}
0 commit comments