Skip to content

fix(namespace): converge bookie affinity group instead of blind-writing - #421

Open
david-streamlio wants to merge 1 commit into
streamnative:mainfrom
david-streamlio:fix/bookie-affinity-group-converge
Open

fix(namespace): converge bookie affinity group instead of blind-writing#421
david-streamlio wants to merge 1 commit into
streamnative:mainfrom
david-streamlio:fix/bookie-affinity-group-converge

Conversation

@david-streamlio

Copy link
Copy Markdown

Fixes #420.

spec.bookieAffinityGroup already existed on PulsarNamespace and was already reconciled, so this is not a new feature — it is a fix to the reconcile path, plus the validation and docs the field was missing. Rationale for the reframing is in this issue comment.

The bug

Every PulsarNamespace that did not declare bookieAffinityGroup issued an unconditional DELETE .../persistence/bookieAffinity on each reconcile, with no IsNotFound tolerance — unlike the neighbouring RemoveTopicAutoCreation and RemoveInactiveTopicPolicies branches in the same function:

} else {
    err = p.adminClient.Namespaces().DeleteBookieAffinityGroup(completeNSName)
    if err != nil {
        return err
    }
}

Pulsar implements that delete as a set of a null group (NamespacesBase#internalDeleteBookieAffinityGroupAsyncinternalSetBookieAffinityGroupAsync(null)), and both the setter and the getter call validateSuperUserAccessAsync(). Consequences:

  1. A tenant-admin connection cannot reconcile any namespace. It gets 401/403 on that DELETE, ApplyNamespace fails, and the namespace never reaches Ready — for users who never touched bookie affinity at all.
  2. Spurious metadata writes. setLocalPoliciesWithCreate creates the local-policies znode with default bundle data even when there was nothing to delete.
  3. No read of current state, so "removing the setting removes the policy" was assumed rather than verified.

The fix

applyBookieAffinityGroup reads the current group first and writes only on an actual diff. An unset-to-unset transition now touches Pulsar not at all, which removes the superuser dependency for namespaces that don't use the feature. Both of Pulsar's "no group" answers are normalized to nil: a 404 "Namespace local-policies does not exist" when the namespace has no local policies, and a 200 carrying an empty group when they were cleared.

A non-404 read failure propagates rather than being mistaken for "unset" — otherwise a 403 would silently skip a required write.

Also in this PR

  • Corrected the BookieAffinityGroup godoc. It read "is the name of the namespace isolation policy to apply to the namespace", which describes PulsarNSIsolationPolicy — a different feature. That wording is why Expose bookie affinity groups (namespace-level bookie isolation) as a managed resource #420 was filed as "not exposed through any CRD".
  • MinLength=1 on both group names. An empty primary passed CRD validation and was pushed to Pulsar, where BookieRackAffinityMapping cannot place ledgers. Only "" becomes invalid, so no working configuration is rejected.
  • Documented the end-to-end carve-out — new "Broker and Bookie Isolation" section in docs/pulsar_namespace.md pairing PulsarNSIsolationPolicy (brokers) with bookieAffinityGroup (bookies), including the rack-metadata prerequisite and the superuser requirement. Cross-linked from docs/pulsar_ns_isolation_policy.md.

Tests

  • pkg/admin/bookie_affinity_group_test.go — 7 table cases over an httptest server in the style of namespace_backlog_quota_test.go: unset stays unset without writing, cleared group is not deleted again, removal deletes, set when absent, matching group left alone, changed group rewritten, dropped secondary rewritten. Plus read-failure propagation.
  • tests/operator/resources_test.go — a PulsarNamespace Bookie Affinity Group context covering create → update → empty-primary rejection → field removal, mirroring the offload-policies coverage from feat(namespace): add offload policies support #413.

Verified: go build ./..., go test ./pkg/... ./api/... (including the existing applyNamespacePolicies backlog-quota tests, which now traverse the new read path), make fmt vet, make license-check (479 files, 0 invalid). E2E not executed — needs a live cluster via ADMIN_SERVICE_URL.

Note for reviewers

The CRD schema was updated by hand, deliberately. make manifests pulls controller-gen v0.17.0 (pinned at Makefile:201) but the committed CRDs were generated with v0.15.0. Running it rewrites all 19 CRDs — ~1300 lines of unrelated churn — and strips the Apache license header from every YAML. Only the bookieAffinityGroup block was applied here, byte-matching the generator's output. The Makefile/CRD version skew is pre-existing and wants its own PR.

The sn-operator half of #420 (declarative bookie-to-isolation-group membership on BookKeeperCluster) is a genuinely separate gap and belongs on that repo — nothing here can express it.

🤖 Generated with Claude Code

Every PulsarNamespace that did not declare `bookieAffinityGroup` issued an
unconditional DELETE .../persistence/bookieAffinity on each reconcile, with no
IsNotFound tolerance -- unlike the neighbouring RemoveTopicAutoCreation and
RemoveInactiveTopicPolicies branches in the same function.

Pulsar implements the delete as a set of a null group
(NamespacesBase#internalDeleteBookieAffinityGroupAsync), and both the setter and
the getter call validateSuperUserAccessAsync(). So a tenant-admin connection got
401/403 on that DELETE, ApplyNamespace failed, and the namespace never reached
Ready -- for users who never touched bookie affinity at all. It also created the
local-policies znode with default bundle data when there was nothing to delete.

Read the current group first and write only on an actual diff. An unset-to-unset
transition now touches Pulsar not at all, which removes the superuser dependency
for namespaces that do not use the feature. Both of Pulsar's "no group" answers
are normalized: 404 "Namespace local-policies does not exist" when the namespace
has no local policies, and 200 with an empty group when they were cleared.

Also:

- Correct the BookieAffinityGroup godoc, which described PulsarNSIsolationPolicy
  -- a different feature -- and made the field read as something it is not.
- Require MinLength=1 on both group names. An empty primary passed CRD validation
  and was pushed to Pulsar, where BookieRackAffinityMapping cannot place ledgers.
- Document the end-to-end carve-out: PulsarNSIsolationPolicy for brokers paired
  with bookieAffinityGroup for bookies, including the rack-metadata prerequisite
  and the superuser requirement.

The CRD schema was updated by hand rather than via `make manifests`: the Makefile
pins controller-gen v0.17.0 but the committed CRDs were generated with v0.15.0,
so regenerating rewrites all 19 CRDs and strips their license headers. The
bookieAffinityGroup block matches the generator output exactly. The version skew
is pre-existing and needs its own change.

Fixes streamnative#420

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

@david-streamlio:Thanks for your contribution. For this PR, do we need to update docs?
(The PR template contains info about doc, which helps others know more about the changes. Can you provide doc-related info in this and future PR descriptions? Thanks)

@github-actions github-actions Bot added the doc-info-missing This pr needs to mark a document option in description label Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-info-missing This pr needs to mark a document option in description

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose bookie affinity groups (namespace-level bookie isolation) as a managed resource

1 participant