Skip to content

[feat] PIP-468: segment-aware admin endpoints for cursor lifecycle#25717

Merged
merlimat merged 3 commits intoapache:masterfrom
merlimat:st-v5-segment-subscription-admin
May 8, 2026
Merged

[feat] PIP-468: segment-aware admin endpoints for cursor lifecycle#25717
merlimat merged 3 commits intoapache:masterfrom
merlimat:st-v5-segment-subscription-admin

Conversation

@merlimat
Copy link
Copy Markdown
Contributor

@merlimat merlimat commented May 7, 2026

Summary

Supersedes #25709, which had the right intent but produced an invalid persistent://t/n/parent/{descriptor} URL that TopicName parses as a legacy V1 topic name and rejects synchronously — making the admin call hard-fail before any HTTP request goes out.

createSubscription / deleteSubscription on a scalable topic was silently broken before either change. ScalableTopicController.createSubscriptionOnSegment built a persistent://t/n/parent URL (no descriptor) and handed it to the generic topics().createSubscriptionAsync admin API. That URL addresses the parent scalable topic, which has no managed-ledger backing, so the admin call hit Subscription Busy / NotFound and the .exceptionally block swallowed it. Per-segment cursors were never pre-created. Lazy cursor creation by the V5 client on first consumer subscribe masked the bug in the happy path; admin createSubscription had no observable effect beyond writing the SubscriptionMetadata to the metadata store. The same shape of bug existed in ScalableTopics.deleteSegmentTopics (whole-scalable-topic teardown).

The clean fix is segment-specific admin endpoints, all the way down — no persistent:// conversion.

New REST endpoints (Segments.java, super-user only, segment-domain only)

  • PUT /segments/{tenant}/{namespace}/{topic}/{descriptor}/subscription/{subscription} — create a cursor at earliest position
  • DELETE /segments/{tenant}/{namespace}/{topic}/{descriptor}/subscription/{subscription} — delete the cursor

Same validateSuperUserAccessAsyncvalidateTopicOwnershipAsyncgetOrCreateTopic (or getTopicIfExists for delete) pattern as the existing /segments/.../subscription/{sub}/backlog, /seek, /skip-all.

New admin client methods (ScalableTopics)

  • createSegmentSubscriptionAsync(segmentTopic, subscription)
  • deleteSegmentSubscriptionAsync(segmentTopic, subscription)

Both take a segment://... URI directly. ScalableTopicsImpl builds the REST path from TopicName parts — same pattern as the existing getSegmentSubscriptionBacklogAsync / seekSegmentSubscriptionAsync / clearSegmentSubscriptionBacklogAsync helpers.

Caller updates

  • ScalableTopicController.createSubscriptionOnSegment / deleteSubscriptionOnSegment now call scalableTopics().createSegmentSubscriptionAsync / deleteSegmentSubscriptionAsync. Removed the broken toSegmentUnderlyingPersistentName helper.
  • ScalableTopics.deleteSegmentTopics (whole-scalable-topic teardown) now uses admin.scalableTopics().deleteSegmentAsync(segmentTopicName, force) — the existing segment-aware admin endpoint already used for split-segment teardown — instead of fabricating a persistent:// URL.

Test plan

  • V5ScalableSubscriptionAdminTest (new, 1 test) — behavioral round trip: create scalable topic → admin.scalableTopics().createSubscription → produce 30 messages with no consumer attached → subscribe and assert all 30 are received. Fails on master, passes here.
  • ScalableTopicControllerTest 31/31 — updated mocks: topics.createSubscriptionAsync / deleteSubscriptionAsyncscalableTopics.createSegmentSubscriptionAsync / deleteSegmentSubscriptionAsync. Verifies the controller calls the new admin path.
  • ScalableTopicServiceTest 16/16 — unchanged, no regressions.
  • Full V5 broker test suite green (153/153).
  • pulsar-client-admin-api, pulsar-client-admin-original, pulsar-broker checkstyle clean.

…ycle

createSubscription / deleteSubscription on a scalable topic was silently
broken: ScalableTopicController.createSubscriptionOnSegment built a
"persistent://t/n/parent" URL (no descriptor) and handed it to the
generic topics().createSubscriptionAsync admin API. That URL addresses
the parent scalable topic, which has no managed-ledger backing, so the
admin call hit "Subscription Busy" / NotFound and the .exceptionally
block swallowed it. Per-segment cursors were never pre-created. Lazy
creation by the V5 client on first consumer subscribe masked the bug
in the happy path; admin "createSubscription" had no observable effect
beyond writing the SubscriptionMetadata to the metadata store.

PR 25709 attempted to fix this by appending the descriptor to the
persistent URL ("persistent://t/n/parent/0000-ffff-0"), but TopicName's
non-scalable-domain parser uses splitLimit=4 and rejects 4-segment
paths as legacy V1 — making the admin call hard-fail synchronously
before any HTTP request goes out, so the user-facing breakage is now
loud instead of silent. Cleaned-up form here uses segment-specific
admin endpoints all the way down, no persistent-URL conversion.

New REST endpoints (Segments.java, super-user only, segment-domain only):
- PUT  /segments/{t}/{n}/{topic}/{descriptor}/subscription/{sub}
- DELETE /segments/{t}/{n}/{topic}/{descriptor}/subscription/{sub}

New admin client methods (ScalableTopics admin API):
- createSegmentSubscriptionAsync(segmentTopic, subscription)
- deleteSegmentSubscriptionAsync(segmentTopic, subscription)

Both take a segment:// URI directly; ScalableTopicsImpl builds the
REST path from TopicName parts (same pattern as the existing
getSegmentSubscriptionBacklogAsync / seekSegmentSubscriptionAsync /
clearSegmentSubscriptionBacklogAsync helpers).

Updated ScalableTopicController.createSubscriptionOnSegment /
deleteSubscriptionOnSegment to call the new admin methods, removing
the broken toSegmentUnderlyingPersistentName helper.

Updated ScalableTopics.deleteSegmentTopics (the whole-scalable-topic
teardown path) to use admin.scalableTopics().deleteSegmentAsync(
segmentTopicName, force) — same segment-aware admin endpoint already
used for split-segment teardown — instead of trying to address the
segment via a fabricated persistent:// URL.

V5ScalableSubscriptionAdminTest (new) is a behavioral round trip:
create scalable topic, admin.scalableTopics().createSubscription,
produce 30 messages with no consumer attached, then subscribe and
assert every message is received. Fails on master, passes here.

ScalableTopicControllerTest updated to mock the new admin methods
and verify the controller invokes them.
@merlimat merlimat changed the title PIP-468: segment-aware admin endpoints for cursor lifecycle [feat] PIP-468: segment-aware admin endpoints for cursor lifecycle May 7, 2026
@lhotari lhotari self-requested a review May 8, 2026 17:11
Copy link
Copy Markdown
Member

@lhotari lhotari left a comment

Choose a reason for hiding this comment

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

LGTM

@merlimat merlimat merged commit 5b97483 into apache:master May 8, 2026
80 of 82 checks passed
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.

2 participants