@@ -36,18 +36,12 @@ type usage struct {
3636 subtree v1.ResourceList
3737}
3838
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.
5145//
5246// TryUseResources is called by the HRQ admission controller to decide if a ResourceQuota.Status
5347// update issued by the K8s ResourceQuota admission controller is allowed. Since UseResources()
@@ -62,38 +56,21 @@ type usage struct {
6256// etcd) but the apiserver runs a cleanup process that occasionally syncs up actual usage with the
6357// usage recorded in RQs. When the RQs are changed, we'll be updated too.
6458//
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.
8264//
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.
9073func (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-
9774 if err := n .canUseResources (rl ); err != nil {
9875 // At least one of the proposed usage exceeds resource limits.
9976 return err
@@ -114,28 +91,32 @@ func (n *Namespace) TryUseResources(rl v1.ResourceList) error {
11491func (n * Namespace ) canUseResources (u v1.ResourceList ) error {
11592 // For each resource, delta = proposed usage - current usage.
11693 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 )
11896
11997 for _ , nsnm := range n .AncestryNames () {
12098 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
138105 }
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 )
139120 }
140121
141122 return nil
@@ -159,8 +140,10 @@ func (n *Namespace) canUseResources(u v1.ResourceList) error {
159140// usages to the new ancestors following a parent update
160141func (n * Namespace ) UseResources (newUsage v1.ResourceList ) {
161142 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+
164147 // Early exit if there's no usages change. It's safe because the forest would
165148 // remain unchanged and the caller would always enqueue all ancestor HRQs.
166149 if utils .Equals (oldUsage , newUsage ) {
@@ -181,7 +164,7 @@ func (n *Namespace) UseResources(newUsage v1.ResourceList) {
181164
182165 // Get the new subtree usage and remove no longer limited usages.
183166 newSubUsg := utils .Add (delta , ns .quotas .used .subtree )
184- ns .UpdateSubtreeUsages (newSubUsg )
167+ ns .quotas . used . subtree = utils . FilterUnlimited (newSubUsg , ns . Limits () )
185168 }
186169}
187170
@@ -236,9 +219,13 @@ func (n *Namespace) GetSubtreeUsages() v1.ResourceList {
236219 return u
237220}
238221
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
242229}
243230
244231// RemoveLimits removes limits specified by the HierarchicalResourceQuota object
0 commit comments