@@ -36,18 +36,12 @@ type usage struct {
36
36
subtree v1.ResourceList
37
37
}
38
38
39
- // TryUseResources checks resource limits in the namespace and its ancestors
40
- // when given proposed absolute (not delta) resource usages in the namespace. If
41
- // the proposed resource usages are the same as the current usages, of course
42
- // it's allowed and we do nothing. If there are any changes in the usages, we
43
- // only check to see if the proposed changing usages are allowed. If any of them
44
- // exceed resource limits, it returns an error; otherwise, it returns nil.
45
- // Callers of this method are responsible for updating resource usage status of
46
- // the HierarchicalResourceQuota objects.
47
- //
48
- // If the proposed resource usages changes are allowed, the method also updates
49
- // in-memory local resource usages of the current namespace and the subtree
50
- // resource usages of its ancestors (including itself).
39
+ // TryUseResources checks resource limits in the namespace and its ancestors when given proposed
40
+ // absolute (not delta) resource usages in the namespace. If there are any changes in the usages, we
41
+ // only check to see if any proposed increases take us over any limits. If any of them exceed
42
+ // resource limits, it returns an error suitable to display to end users; otherwise, it updates the
43
+ // in-memory usages of both this namespace as well as all its ancestors. Callers of this method are
44
+ // responsible for updating resource usage status of the HierarchicalResourceQuota objects.
51
45
//
52
46
// TryUseResources is called by the HRQ admission controller to decide if a ResourceQuota.Status
53
47
// update issued by the K8s ResourceQuota admission controller is allowed. Since UseResources()
@@ -62,38 +56,21 @@ type usage struct {
62
56
// etcd) but the apiserver runs a cleanup process that occasionally syncs up actual usage with the
63
57
// usage recorded in RQs. When the RQs are changed, we'll be updated too.
64
58
//
65
- // Based on observations, the K8s ResourceQuota admission controller is called only
66
- // when a resource is consumed, not when a resource is released. Therefore, in most cases,
67
- // the proposed resource usages that the HRQ admission controller received should
68
- // be larger than in-memory resource usages.
69
- //
70
- // The proposed usage can be smaller when in-memory resource usages are out of sync
71
- // with ResourceQuota.Status.Used. In this case, TryUseResources will still update
72
- // in-memory usages.
73
- //
74
- // For example, when a user deletes a pod that consumes
75
- // 100m cpu and immediately creates another pod that consumes 1m cpu in a namespace,
76
- // the HRQ ResourceQuota reconciler will be triggered by ResourceQuota.Status changes twice:
77
- // 1) when pod is deleted.
78
- // 2) when pod is created.
79
- // The HRQ ResourceQuota reconciler will compare `local` with ResourceQuota.Status.Used
80
- // and will update `local` (and `subtree` in namespace and its ancestors) if it
81
- // sees `local` is different from ResourceQuota.Status.Used.
59
+ // Based on observations, the K8s ResourceQuota admission controller is called only when a resource
60
+ // is consumed, not when a resource is released. Therefore, in most cases, the proposed resource
61
+ // usages that the HRQ admission controller received should be larger than in-memory resource
62
+ // usages. However, this function is robust to (that is, always allows) decreases as well, mainly
63
+ // because it's easier to test - plus, who knows, the K8s behaviour may change in the future.
82
64
//
83
- // The HRQ admission coontroller will be triggered once when the pod is created.
84
- // If the HRQ admission coontroller locks the in-memory forest before step 1)
85
- // of the HRQ ResourceQuota reconciler, the HRQ admission controller will see proposed
86
- // usages are smaller than in-memory usages and will update in-memory usages.
87
- // When ResourceQuota reconciler receives the lock later, it will notice the
88
- // ResourceQuota.Status.Used is the same as in-memory usages and will not
89
- // update in-memory usages.
65
+ // This may allow one weird case where a user may be allowed to use something they weren't supposed
66
+ // to. Let's say you're well over your limit, and then in quick succession, some resources are
67
+ // deleted, and some _fewer_ are added, but enough to still go over the limit. In that case, there's
68
+ // a race condition between this function being called, and the RQ reconciler updating the baseline
69
+ // resource usage. If this function wins, it will look like resource usage is decreasing, and will
70
+ // be incorrectly allowed. If the RQ reconciler runs first, we'll see that the usage is incorrectly
71
+ // _increasing_ and it will be disallowed. However, I think the simplicity of not trying to prevent
72
+ // this (hopefully very unlikely) corner case is more valuable than trying to catch it.
90
73
func (n * Namespace ) TryUseResources (rl v1.ResourceList ) error {
91
- // The proposed resource usages are allowed if they are the same as current
92
- // resource usages in the namespace.
93
- if utils .Equals (rl , n .quotas .used .local ) {
94
- return nil
95
- }
96
-
97
74
if err := n .canUseResources (rl ); err != nil {
98
75
// At least one of the proposed usage exceeds resource limits.
99
76
return err
@@ -114,28 +91,32 @@ func (n *Namespace) TryUseResources(rl v1.ResourceList) error {
114
91
func (n * Namespace ) canUseResources (u v1.ResourceList ) error {
115
92
// For each resource, delta = proposed usage - current usage.
116
93
delta := utils .Subtract (u , n .quotas .used .local )
117
- delta = utils .OmitZeroQuantity (delta )
94
+ // Only consider *increasing* deltas; see comments to TryUseResources for details.
95
+ increases := utils .OmitLTEZero (delta )
118
96
119
97
for _ , nsnm := range n .AncestryNames () {
120
98
ns := n .forest .Get (nsnm )
121
- r := utils .Copy (ns .quotas .used .subtree )
122
- r = utils .AddIfExists (delta , r )
123
- allowed , nm , exceeded := checkLimits (ns .quotas .limits , r )
124
- if ! allowed {
125
- // Construct the error message similar to the RQ exceeded quota error message -
126
- // "exceeded quota: gke-hc-hrq, requested: configmaps=1, used: configmaps=2, limited: configmaps=2"
127
- msg := fmt .Sprintf ("exceeded hierarchical quota in namespace %q: %q" , ns .name , nm )
128
- for _ , er := range exceeded {
129
- rnm := er .String ()
130
- // Get the requested, used, limited quantity of the exceeded resource.
131
- rq := delta [er ]
132
- uq := ns .quotas .used .subtree [er ]
133
- lq := ns .quotas .limits [nm ][er ]
134
- msg += fmt .Sprintf (", requested: %s=%v, used: %s=%v, limited: %s=%v" ,
135
- rnm , & rq , rnm , & uq , rnm , & lq )
136
- }
137
- return fmt .Errorf (msg )
99
+ // Use AddIfExists (not Add) because we want to ignore any resources that aren't increasing when
100
+ // checking against the limits.
101
+ proposed := utils .AddIfExists (increases , ns .quotas .used .subtree )
102
+ allowed , nm , exceeded := checkLimits (ns .quotas .limits , proposed )
103
+ if allowed {
104
+ continue
138
105
}
106
+
107
+ // Construct the error message similar to the RQ exceeded quota error message -
108
+ // "exceeded quota: gke-hc-hrq, requested: configmaps=1, used: configmaps=2, limited: configmaps=2"
109
+ msg := fmt .Sprintf ("exceeded hierarchical quota in namespace %q: %q" , ns .name , nm )
110
+ for _ , er := range exceeded {
111
+ rnm := er .String ()
112
+ // Get the requested, used, limited quantity of the exceeded resource.
113
+ rq := increases [er ]
114
+ uq := ns .quotas .used .subtree [er ]
115
+ lq := ns .quotas .limits [nm ][er ]
116
+ msg += fmt .Sprintf (", requested: %s=%v, used: %s=%v, limited: %s=%v" ,
117
+ rnm , & rq , rnm , & uq , rnm , & lq )
118
+ }
119
+ return fmt .Errorf (msg )
139
120
}
140
121
141
122
return nil
@@ -159,8 +140,10 @@ func (n *Namespace) canUseResources(u v1.ResourceList) error {
159
140
// usages to the new ancestors following a parent update
160
141
func (n * Namespace ) UseResources (newUsage v1.ResourceList ) {
161
142
oldUsage := n .quotas .used .local
162
- // We only consider limited usages.
163
- newUsage = utils .CleanupUnneeded (newUsage , n .Limits ())
143
+
144
+ // We only store the usages we care about
145
+ newUsage = utils .FilterUnlimited (newUsage , n .Limits ())
146
+
164
147
// Early exit if there's no usages change. It's safe because the forest would
165
148
// remain unchanged and the caller would always enqueue all ancestor HRQs.
166
149
if utils .Equals (oldUsage , newUsage ) {
@@ -181,7 +164,7 @@ func (n *Namespace) UseResources(newUsage v1.ResourceList) {
181
164
182
165
// Get the new subtree usage and remove no longer limited usages.
183
166
newSubUsg := utils .Add (delta , ns .quotas .used .subtree )
184
- ns .UpdateSubtreeUsages (newSubUsg )
167
+ ns .quotas . used . subtree = utils . FilterUnlimited (newSubUsg , ns . Limits () )
185
168
}
186
169
}
187
170
@@ -236,9 +219,13 @@ func (n *Namespace) GetSubtreeUsages() v1.ResourceList {
236
219
return u
237
220
}
238
221
239
- // UpdateSubtreeUsages sets the subtree resource usages.
240
- func (n * Namespace ) UpdateSubtreeUsages (rl v1.ResourceList ) {
241
- n .quotas .used .subtree = utils .CleanupUnneeded (rl , n .Limits ())
222
+ // TestOnlySetSubtreeUsage overwrites the actual, calculated subtree usages and replaces them with
223
+ // arbitrary garbage. Needless to say, you should never call this, unless you're testing HNC's
224
+ // ability to recover from arbitrary garbage.
225
+ //
226
+ // The passed-in arg is used as-is, not copied. This is test code, so deal with it 😎
227
+ func (n * Namespace ) TestOnlySetSubtreeUsage (rl v1.ResourceList ) {
228
+ n .quotas .used .subtree = rl
242
229
}
243
230
244
231
// RemoveLimits removes limits specified by the HierarchicalResourceQuota object
0 commit comments