Skip to content

feat(scheduler): pool pattern aware scheduler helpers - #739

Open
krishnaGajabi wants to merge 3 commits into
openebs:developfrom
krishnaGajabi:poolpattern-schd-helpers
Open

feat(scheduler): pool pattern aware scheduler helpers#739
krishnaGajabi wants to merge 3 commits into
openebs:developfrom
krishnaGajabi:poolpattern-schd-helpers

Conversation

@krishnaGajabi

@krishnaGajabi krishnaGajabi commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworks the scheduler helpers in pkg/driver/schd_helper.go so that a pool is
selected by a regular expression rather than an exact name, and adds a third
scheduling algorithm, SpaceWeighted. This is the first step towards the
poolpattern StorageClass parameter (OEP-4268); the parameter itself is not read
yet, see Left unwired.

What this PR does

One matching path for poolname and poolpattern. Both fold into a single
*regexp.Regexp via compilePoolPattern, so every helper matches the same way.
poolpattern compiles as given (unanchored RE2, the same semantics as
lvm-localpv's vgpattern); poolname compiles as an anchored, QuoteMeta-quoted
exact match. The quoting is required for correctness: legal ZFS pool names contain
regex metacharacters, so an unescaped tank.prod would also match tankXprod and
select the wrong pool. Names are matched against the pool root, since
poolname may be a pool/dataset path while the ZFSNode CR advertises
pool-level names only.

CapacityWeighted weighs by real pool usage. It now reads the ZFSNode CRs
and sums the on-disk Used of a node's matching pools, instead of summing the
capacity of the volumes this driver provisioned — so the ordering also accounts
for data that did not come through the driver. Every node with a matching pool
gets an entry even when its pools are empty, because lib-csi moves a node missing
from the weight map to the front of the list rather than dropping it.
VolumeWeighted still counts ZFSVolumes (the ZFSNode CR carries no volume
count) and matches on the pool root like everything else.

New SpaceWeighted algorithm, opt-in. VolumeWeighted and
CapacityWeighted both order by what has already been put into a pool, which
says nothing about what is left: a node with a small untouched pool outranks a
node with a large, moderately used one that has far more room. SpaceWeighted
orders by the free capacity of a node's roomiest matching pool — the same pool the
volume would land in. lib-csi prefers the least-weighted node, so free capacity is
inverted into a weight (math.MaxInt64 - free). Select it with
scheduler: "SpaceWeighted"; CapacityWeighted remains the default.

Unlike lvm-localpv's SpaceWeighted, a node whose matching pool is full keeps its
map entry (weight math.MaxInt64, sorting last) instead of being dropped —
omission promotes a node to the front under lib-csi, which would prefer exactly
the nodes with no room. Deciding whether a volume fits stays the job of the
suitable-node intersection.

Weight maps are keyed by node name, not node id. The map handed to lib-csi was
keyed from ZFSVolume.Spec.OwnerNodeID, while the scheduler looks nodes up by
their Kubernetes node name. Those coincide only when no custom node id is
configured; with one, no key ever matched, so the weighting silently stopped
having any effect and nodes came back in listing order. k8sNodeName reads the
owner reference the node agent maintains on the ZFSNode CR, falling back to the
CR name. This matters more for what comes next: the suitable-node set is going to
be intersected with the scheduler's output, and a set keyed by node id would
intersect to nothing and fail every space-reserving volume on a cluster with
plenty of room.

Three helpers for the controller to consume next.

  • reservesSpace — whether a volume gets a ZFS reservation and so has to fit in a
    pool's free space: a zvol does unless it is sparse; a dataset only when
    thinprovision: "no", since its quota is a limit rather than a reservation.
  • getSuitableNodes — the nodes having a matching pool with more than the
    requested bytes free (a single pool must hold the whole reservation), plus
    whether any pool matched the pattern at all, so the controller can tell an
    exhausted pool from a StorageClass naming a pool that exists nowhere.
  • resolvePool — the matching pool with the most free space on a given node,
    turning a pattern into the concrete pool stored in ZFSVolume.Spec.PoolName.

All three, and SpaceWeighted, share one maxFreePool helper so the three uses of
"the node's roomiest matching pool" cannot drift apart.

Left unwired

Deliberately out of scope here, so this PR reviews as a self-contained helper
change:

  • the poolpattern StorageClass parameter is not read; CreateZFSVolume is
    touched only as far as compiling the exact-name pattern for the changed
    getNodeMap signature
  • getSuitableNodes, resolvePool and reservesSpace have no callers yet
  • GetCapacity, the clone/snapshot-restore pool guard, and
    validateVolumeCreateReq are untouched
  • user-facing docs still describe two scheduling algorithms

Next in the series

The controller task wires the fresh-create path: read poolpattern, compute
reservesSpace once, intersect the scheduler's ordered node list with
getSuitableNodes for reserving volumes, and replace the generic
codes.Internal, "scheduler failed, node list is empty" with a capacity-aware
verdict — ResourceExhausted when a matching pool exists but nothing fits
(transient; external-provisioner retries with backoff and, with capacity
tracking, reschedules), FailedPrecondition when the pattern matches no pool
anywhere (a misconfigured StorageClass that retrying will never fix). It then
calls resolvePool per candidate node in the provisioning loop so
Spec.PoolName always names a pool present on the assigned node.

Then, as separate tasks: GetCapacity under poolpattern; the clone/snapshot
guard, which today rejects every clone under a pattern StorageClass because
poolname is empty; StorageClass validation; and docs plus samples.

Behaviour change — needs a release note

Applying the Used-based CapacityWeighted metric uniformly means existing
poolname StorageClasses change behaviour on upgrade with no opt-in
: node
ordering under the default scheduler shifts from driver-provisioned-sum to the
pool's real Used. OEP-4268 accepts this as an improvement — the scheduler now
reflects real capacity — but records that it must be called out in the release
notes.

SpaceWeighted is additive and opt-in, so it changes nothing for existing
StorageClasses.

Testing

pkg/driver/schd_helper_test.go is new and covers: pool-root matching;
QuoteMeta anchoring for poolname (a . in a pool name must not over-match);
both-set and neither-set rejection; Used-based weighting under both existing
algorithms; reservesSpace across the zvol/dataset × yes/no/unset matrix;
maxFreePool selection including a full matching pool; suitable-node computation
including the exhausted-pool and no-match-anywhere cases; and for SpaceWeighted,
that the inverted weight sorts most-free-first under an ascending sort, that a
node's largest matching pool decides rather than the sum of its pools, and that a
node with a full matching pool keeps its entry instead of being front-loaded.
Node-name keying is asserted for every map via custom-node-id cases.

make test and go vet ./pkg/... are clean.

Worth running the BDD suite with make ci profile=custom-node-id (or
profile=all): the regular profile cannot exercise the node-name fix, since node
name and node id are equal there — only the custom-node-id profile relabels the
nodes.

Rework the scheduler helpers so that a pool is selected by a regular
expression, the first step towards the poolpattern StorageClass parameter
(OEP-4268). No behaviour is wired to the new parameter yet.

The poolname and poolpattern parameters fold into a single *regexp.Regexp so
the helpers have one matching path: poolpattern compiles as given (unanchored
RE2, like lvm-localpv's vgpattern), while poolname compiles as an anchored,
quoted exact match. Quoting is required for correctness, since legal ZFS pool
names carry regex metacharacters and an unescaped "tank.prod" would also match
"tankXprod". Pool names are matched against the pool root, as the poolname
parameter may be a pool/dataset path while the ZFSNode CR only advertises
pool level names.

CapacityWeighted now reads the ZFSNode CRs and weights a node by the real on
disk usage of its matching pools, instead of summing the capacity of the
volumes this driver provisioned, so the ordering also accounts for data that
did not come through the driver. Every node with a matching pool gets an entry
even when the pools are empty, since lib-csi's scheduler moves the nodes
missing from the map to the front of the list rather than dropping them.
VolumeWeighted keeps counting ZFSVolumes, as the ZFSNode CR has no volume
count, and matches on the pool root like everything else.

Three helpers are added for the controller to use next:

  - reservesSpace tells whether a volume gets a ZFS reservation and so has to
    fit in a pool's free space: a zvol does unless it is sparse, a dataset only
    when thinprovision is "no", since its quota is a limit and not a
    reservation.
  - getSuitableNodes returns the nodes having a matching pool with more than
    the requested bytes free, a single pool having to hold the whole
    reservation, along with whether any pool matched the pattern at all, so the
    controller can tell an exhausted pool from a StorageClass naming a pool
    that exists nowhere.
  - resolvePool returns the matching pool with the most free space on a node,
    turning a pattern into the concrete pool stored in ZFSVolume.Spec.PoolName.

The list reading helpers are shells over pure functions taking the CR slice,
which is what the new unit tests exercise.

CreateZFSVolume is updated only as far as compiling the exact name pattern for
the changed getNodeMap signature; reading the poolpattern parameter, filtering
the scheduled nodes and resolving the pool per node follow in the next change.

Signed-off-by: krishnaGajabi <gajbikrishna23@gmail.com>
@krishnaGajabi
krishnaGajabi requested a review from a team as a code owner July 28, 2026 07:02
@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

The weight map handed to lib-csi's scheduler was keyed by the node id, taken
from ZFSVolume.Spec.OwnerNodeID, while the scheduler looks the nodes up by
their kubernetes node name. The two are the same only as long as no custom
node id is configured; with one, no key ever matches, and since lib-csi treats
a node missing from the map as least loaded and moves it to the front of the
list instead of dropping it, the weighting silently stops having any effect and
the nodes end up in listing order.

Key the maps by the node name instead. A ZFSNode CR is named after the node id,
but the node agent also gives it an owner reference to the node object and keeps
that up to date, so the name is available without asking the API server for it;
k8sNodeName falls back to the CR name, which is the node name as long as no
custom node id is set. The volume weighted map needs the same translation the
other way round, as a volume records only the node id of its node, so it now
reads the ZFSNode CRs as well.

This matters more than the lost ordering for what comes next: the suitable node
set is going to be intersected with the scheduler's output, and a set keyed by
node id would intersect to nothing, failing every space reserving volume on a
cluster that has plenty of room. resolvePool keeps taking the node id, since it
looks the CR up by name and the provisioning loop resolves the id anyway for
ZFSVolume.Spec.OwnerNodeID.

The regular CI profile cannot catch any of this, as the node name and the node
id are equal there; only the custom-node-id profile relabels the nodes.

Signed-off-by: krishnaGajabi <gajbikrishna23@gmail.com>
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.64%. Comparing base (6c395e6) to head (271abf1).
⚠️ Report is 2 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #739   +/-   ##
========================================
  Coverage    96.64%   96.64%           
========================================
  Files            1        1           
  Lines          656      656           
========================================
  Hits           634      634           
  Misses          17       17           
  Partials         5        5           
Flag Coverage Δ
bddtests 96.64% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Abhinandan-Purkait

Abhinandan-Purkait commented Jul 29, 2026

Copy link
Copy Markdown
Member

@copilot Review this PR. Things to look for.

  1. Change in existing behavior against the complete codebase.
  2. Any breaking changes, that might surface on upgrades.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds pool-pattern-aware scheduler helpers and corrects custom node-ID weighting.

Changes:

  • Compiles exact pool names and regex patterns consistently.
  • Adds pool capacity, suitability, reservation, and resolution helpers.
  • Adds comprehensive unit tests and updates controller integration.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
pkg/driver/schd_helper.go Implements pattern-aware scheduling helpers.
pkg/driver/schd_helper_test.go Tests matching, weighting, suitability, and node IDs.
pkg/driver/controller.go Compiles the fixed pool name before scheduling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tiagolobocastro tiagolobocastro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The OEP is not approved yet @krishnaGajabi
You need to propose changing it to implementable and request maintainer review

@Abhinandan-Purkait

Copy link
Copy Markdown
Member

Framing: only getNodeMap + compilePoolPattern are actually wired in (controller.go:279,284); reservesSpace/getSuitableNodes/resolvePool/poolForNode are dormant helpers for the next PR, so the live behavior surface is small.

  1. Existing-behavior changes
    CapacityWeighted now weights by real pool Used (whole pool root, incl. non-driver data/snapshots, from zfs list -d 1) instead of summed driver-provisioned volume sizes — affects all existing poolname SCs on the default scheduler.
    Exact Spec.PoolName match → pool-root regex also broadens VolumeWeighted to cross-count volumes in sibling datasets sharing a pool root — a quieter change not fully flagged in the PR notes.
    Node-id → node-name keying fix verified correct against lib-csi's nmap[node.Name] and the controller's GetNodeID(node); makes weighting actually take effect on custom-node-ID clusters.
    Empty poolname now hard-fails early with InvalidArgument (poolname isn't validated elsewhere) instead of silently hanging in Pending — an improvement, but a timing/error-code change.
  2. Upgrade/breaking
    No CRD/API/StorageClass/go.mod changes; existing SCs keep working; changes are runtime ordering shifts, not hard breaks; rollback-safe.

Minor, non-blocking
CapacityWeighted front-loading corner case (a pool-less node can rank ahead of an empty-pool node → one extra zfs create retry); pool-root-level free checks are optimistic for child-dataset quotas (relevant to tasks 2/4); reservesSpace confirmed to match the real zvol/dataset create-arg logic (zfs_util.go:164,297).

Only concrete ask: also surface changes (b) pool-root VolumeWeighted cross-dataset counting and (d) empty-poolname early-error in the release notes.

@krishnaGajabi

krishnaGajabi commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

The OEP is not approved yet @krishnaGajabi You need to propose changing it to implementable and request maintainer review

I have raised the PR to mark the OEP as implementable. PTAL
openebs/openebs#4277

cc @Abhinandan-Purkait

@krishnaGajabi

Copy link
Copy Markdown
Contributor Author

The OEP is not approved yet @krishnaGajabi You need to propose changing it to implementable and request maintainer review

I have raised the PR to mark the OEP as implementable. PTAL openebs/openebs#4277

cc @Abhinandan-Purkait

The PR is merged

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

pkg/driver/schd_helper.go:219

  • runScheduler prepends nodes missing from nmap, but this map adds explicit zero entries only for nodes with a matching empty pool. Because the controller in this PR does not yet filter or skip non-matching nodes, every topology-eligible node without the requested pool is now ordered ahead of a valid empty-pool node and provisioning is attempted there first. Please either defer this active weighting change or wire the pool-membership filtering/skip into the same PR.
	for _, node := range nodelist {
		for _, pool := range node.Pools {
			if pattern.MatchString(pool.Name) {
				nmap[k8sNodeName(node)] += pool.Used.Value()

VolumeWeighted and CapacityWeighted both order the nodes by what has
already been put into a pool, by volume count and by used capacity. That
says nothing about what is left: a node with a small untouched pool
outranks a node with a large, moderately used one, even though the latter
has far more room for the volume.

Add SpaceWeighted, which orders the nodes by the free capacity of their
roomiest pool matching the pattern, the same pool the volume would
actually land in. lib-csi's scheduler prefers the least weighted node, so
the free capacity is inverted into a weight (math.MaxInt64 - free): the
more space a node has left, the less loaded it looks. It is opt-in via
the scheduler storageclass parameter, CapacityWeighted stays the default.

Unlike lvm-localpv's SpaceWeighted, a node whose matching pool is full
keeps its entry in the map instead of being dropped. An omitted node is
not excluded by lib-csi, it is treated as least loaded and moved to the
front of the list, which would prefer exactly the nodes with no room.
Fitting the volume stays the job of the suitable node intersection.

The "roomiest matching pool of a node" lookup is now shared by the three
places that need it, as maxFreePool: the pool the volume is created in,
the capacity it has to fit into, and the space the node is weighted by.

Signed-off-by: krishnaGajabi <gajbikrishna23@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants