Skip to content

Add generic buffer.TypedRingGrowing and shrinkable buffer.Ring #323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

nojnhuh
Copy link

@nojnhuh nojnhuh commented Mar 25, 2025

What type of PR is this?
/kind feature

What this PR does / why we need it:

This PR adds a new type, buffer.Ring which is a buffer.RingGrowing with a type parameter (generics) and the capability to shrink its underlying buffer when all elements have been read. It should be able to replace the scheduler's use of queue.FIFO (and its copy in k8s.io/dynamic-resource-allocation/internal/queue). Behavior of the existing buffer.RingGrowing should be unchanged.

Which issue(s) this PR fixes:

First step of kubernetes/kubernetes#131032

Special notes for your reviewer:

I verified that this new implementation passes the existing unit tests for k8s.io/client-go/tools/cache where buffer.RingGrowing is currently in use without any changes to that package, and for k8s.io/kubernetes/pkg/scheduler/util/assumecache and k8s.io/dynamic-resource-allocation/resourceslice/tracker replacing its use of queue.FIFO: kubernetes/kubernetes@master...nojnhuh:kubernetes:typed-ring-buffer.

Release note:

Add new, generic buffer.TypedRingGrowing type to succeed buffer.RingGrowing which is now deprecated. Add new buffer.Ring type, which is equivalent to buffer.TypedRingGrowing, but shrinks to its original size after its elements have been totally consumed.

@k8s-ci-robot k8s-ci-robot added the kind/feature Categorizes issue or PR as related to a new feature. label Mar 25, 2025
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: nojnhuh
Once this PR has been reviewed and has the lgtm label, please assign thockin for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Mar 25, 2025
// Ring is a dynamically-sized ring buffer which can grow and shrink as-needed.
// Not thread safe.
type Ring[T any] struct {
TypedRingGrowing[T]
Copy link
Author

@nojnhuh nojnhuh Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embedding this type here is convenient to "inherit" its methods, but I'm not sure we want this in the public API. Or maybe that's actually a feature? If users have a Ring and want to do a read such that the buffer definitely will not shrink, they could do r.TypedRingGrowing.ReadOne().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That particular usage seems a bit far-fetched and not like something that developers will discover if they need it - we would need to explicitly document the possibility.

I understand the convenience, but I have been bitten by wrappers embedding the type that they wrap when extending the type with new methods. Everything compiles fine even if the wrapper would have to be updated to also wrap the new methods.

It's cleaner to not embed the underlying type and instead repeat all methods as calls of the underlying method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, done.

type Ring[T any] struct {
TypedRingGrowing[T]

normalSize int // Limits the size of the buffer that is kept for reuse. Read-only.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queue.FIFO is set up to not require any constructors like this, so users can initialize one directly as the zero value and get a valid, empty queue. One detail I couldn't quite iron out to enable that here is how to make the normalSize configurable. There it's hardcoded to 4 which seems reasonable for where that's used, but I see informers' notification buffers using RingGrowing are initialized to 1024, so shrinking all the way down to 4 could result in significantly more allocations. So if there isn't a single golden value for every use case, I think that value probably has to be fed in through a constructor (or possibly a method).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that a constructor parameter would be good. Two int parameters with no clear obvious ordering will be hard to read at call sites, so I prefer the config struct pattern.

We could use optional With* methods, but that raises the question whether they return a deep copy of the instance or completely new ones - let's not go there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the zero value Ring[T]: let's document it as NewRing() with zero initial size and zero normal size. Perhaps not very useful, but at least it's then documented.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added options structs and mentioned zero values in doc strings.

@nojnhuh
Copy link
Author

nojnhuh commented Mar 25, 2025

/cc @pohly

@k8s-ci-robot k8s-ci-robot requested a review from pohly March 25, 2025 03:27
@nojnhuh
Copy link
Author

nojnhuh commented Apr 14, 2025

AFAICT the apidiff failure is a false-positive, maybe it doesn't fully understand generics? I've at least checked that client-go will still compile without any changes with this PR.

@apelisse @cheftako If you could PTAL that would be great, thanks!


// ReadOne reads (consumes) first item from the buffer if it is available,
// otherwise returns false. When the buffer has been totally consumed and has
// grown in size, it shrinks down to its initial size.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// grown in size, it shrinks down to its initial size.
// grown in size beyond its normal size, it shrinks down to its normal size again.

I was wondering whether two different options are needed, but I can imaging that RingOptions{InitialSize: 8, NormalSize: 32} could make sense: we get some memory reuse for small problems with less than 8 entries max. If the problem space is larger, we hang on to more memory to deal with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/feature Categorizes issue or PR as related to a new feature. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants