Skip to content

Subscription: add topic owner epoch fencing - #17780

Merged
jt2594838 merged 15 commits into
masterfrom
subscription-topic-owner-fencing
Jun 12, 2026
Merged

jt2594838 merged 15 commits into
masterfrom
subscription-topic-owner-fencing

Conversation

@VGalaxies

@VGalaxies VGalaxies commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add owner-epoch fencing for subscription topics so that, after an owner
failover/transfer, stale owners can no longer mutate a topic's consumption
progress.

  • Owner identity + epoch metadata. A topic may carry owner-id and
    owner-epoch. Consumers propagate their owner-id/owner-epoch, and the
    DataNode fences stale owners on heartbeat, subscribe, poll, commit, and
    seek (before any position-mutating operation).
  • Globally monotonic, never-reused epochs. A new max-owner-epoch
    attribute records the highest epoch ever granted and survives clearOwner,
    so an owner change can never reuse an epoch <= any epoch previously
    granted, even across a clear -> re-enable cycle.
  • Owner lease as a liveness bound. The lease is pushed by the ConfigNode
    over a dedicated, fine-grained SubscriptionOwnerLeaseSyncer (independent of
    the node heartbeat) carrying a relative remaining duration. The absolute
    expiry is derived and held DataNode-local at runtime only -- it is never a
    topic attribute, never serialized, and not part of equals/hashCode. This
    avoids comparing absolute timestamps across ConfigNode/DataNode clocks. The
    ConfigNode persists only owner-id, owner-epoch, max-owner-epoch, and
    owner-lease-duration-ms.
  • Transfer drain via ConfigNode admission-wait. On an owner transfer the
    ConfigNode stops renewing the previous owner's lease and waits
    blockSince + lease-duration + heartbeat-interval on its own clock before
    installing the new owner. Epoch fencing on reachable DataNodes guarantees
    correctness during the wait.

Notable design points

  • The lease is a liveness mechanism layered on top of epoch fencing, not the
    primary fence: if no lease has been established yet (e.g. right after a
    meta push), owner-id/epoch matching still fences correctly.
  • No absolute lease timestamp is ever propagated between nodes.

Tests

Unit tests (pass):

mvn test -pl iotdb-core/confignode -am \
  -Dtest=TopicDeSerTest,SubscriptionInfoTest \
  -Dsurefire.failIfNoSpecifiedTests=false

TopicDeSerTest covers serialization, owner transfer, cross-clear epoch-reuse
rejection, and the DataNode-local-only lease expiry behavior;
SubscriptionInfoTest covers epoch-rollback rejection, owner transfer via
updated attributes, the read-only relative-duration lease collector, and
SHOW TOPICS output.

Compilation / build validation:

mvn compile -pl iotdb-core/datanode,iotdb-core/confignode -am
mvn test-compile -pl integration-test -am -P with-integration-tests

Integration tests added but not run under MiniCluster in this change:
IoTDBSubscriptionTopicOwnerIT (relative-lease drain) and
IoTDBSubscriptionTopicOwnerPartitionIT (multi-DataNode fencing, ConfigNode
failover epoch persistence, DataNode restart).

@Caideyipi

Copy link
Copy Markdown
Collaborator

Findings

  • P1: TopicMeta 的新尾部字段破坏旧格式兼容。deserialize(InputStream) 用 inputStream.available() 判断是否还有 owner
    字段,但 TopicMeta 在 snapshot 里是连续写入的;旧版本数据没有 owner flag,多个 topic 时这里会把下一个 topic
    的首字节当成 owner flag 消费,导致后续反序列化错位。ByteBuffer.hasRemaining() 在旧 procedure/plan
    列表里也有同类问题。见 TopicMeta.java#L260

(

if (inputStream.available() > 0 && ReadWriteIOUtils.readBool(inputStream)) {
)
和 TopicMetaKeeper.java#L112

(https://github.com/apache/iotdb/blob/76f2a882411ba790f7db290740f0232a1e5c4022/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMetaKeeper.java#L112)。建议改成版本化或长度前缀格式,或只从
config 属性恢复 owner,不在非自描述对象尾部追加可选字段。

  • P1: owner epoch 单调性没有在真实元数据替换路径上保证。transferOwner() 只在同一个对象上检查递增,但 AlterTopicPlan /
    metadata replace 路径直接 remove + add 新 TopicMeta,新对象从 -1 初始化,所以较小 epoch 也能覆盖当前 epoch,使旧
    owner 重新合法。见 TopicMeta.java#L135

(

if (isOwnerFencingEnabled() && ownerEpoch <= this.ownerEpoch) {
)
和 SubscriptionInfo.java#L340

(https://github.com/apache/iotdb/blob/76f2a882411ba790f7db290740f0232a1e5c4022/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java#L340)。建议在
ConfigNode alter/handle meta change 时对比 existing owner epoch 并拒绝回退。

  • P2: 新增的 owner status code 没有接入客户端错误分类。1913-1916 会落到 default,变成 generic critical
    exception;heartbeat 还会把 provider 标成 unavailable,导致 stale owner 场景反复重连/重试且错误信息不稳定。见
    TSStatusCode.java#L319

(https://github.com/apache/iotdb/blob/76f2a882411ba790f7db290740f0232a1e5c4022/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TSStatusCode.java#L319)、AbstractSubscriptionProvider.java#L445

(https://github.com/apache/iotdb/blob/76f2a882411ba790f7db290740f0232a1e5c4022/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java#L445)。建议显式处理
owner fenced/required/lease expired,映射成明确的不可重试业务异常。

@VGalaxies
VGalaxies force-pushed the subscription-topic-owner-fencing branch from 76f2a88 to b6086b2 Compare May 28, 2026 10:35
@VGalaxies

Copy link
Copy Markdown
Contributor Author

Thanks @Caideyipi, addressed the three findings in b6086b2.

  • P1 serialization compatibility: removed the optional owner fields from the non-self-describing TopicMeta serialization tail. Owner state is now restored from topic attributes after deserialization, so sequential TopicMeta snapshot/procedure streams are not consumed out of boundary. Added a sequential deserialization regression test.
  • P1 epoch rollback: added TopicMeta.validateOwnerProgression(...) and applied it on both ConfigNode alter-topic metadata replacement and DataNode topic-meta update handling, rejecting owner clears/rollbacks/stale same-epoch owner changes. Added ConfigNode rollback coverage and extended the network-partition old-SN test to verify stale topic meta cannot make the old owner valid again.
  • P2 client classification: mapped 1913-1917 explicitly to SubscriptionOwnerFencedException, a SubscriptionRuntimeNonCriticalException subclass, so stale-owner/business fencing errors no longer fall into the generic critical default path.

Local verification passed:

  • mvn spotless:apply -pl iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/datanode,iotdb-core/confignode -DskipTests
  • mvn test -pl iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons -Dtest=TopicDeSerTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn test -pl iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/confignode -Dtest=SubscriptionInfoTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn test -pl iotdb-core/relational-grammar,iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/datanode -Dtest=SubscriptionReceiverV1Test -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn test -pl iotdb-client/service-rpc,iotdb-client/subscription -Dtest=TSStatusCodeTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • git diff --check

The Sonar duplication check is queued again on the new commit; I will follow up if the rerun still fails.

@codecov

codecov Bot commented May 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.69397% with 429 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.99%. Comparing base (07b9cb0) to head (55b9568).
⚠️ Report is 21 commits behind head on master.

Files with missing lines Patch % Lines
.../manager/subscription/SubscriptionCoordinator.java 5.66% 50 Missing ⚠️
...sion/subscription/AbstractSubscriptionSession.java 0.00% 47 Missing ⚠️
.../subscription/receiver/SubscriptionReceiverV1.java 0.00% 43 Missing ⚠️
...che/iotdb/confignode/manager/ProcedureManager.java 0.00% 29 Missing ⚠️
...tdb/commons/subscription/meta/topic/TopicMeta.java 78.83% 29 Missing ⚠️
...ger/subscription/SubscriptionOwnerLeaseSyncer.java 24.00% 19 Missing ⚠️
.../db/subscription/agent/SubscriptionTopicAgent.java 56.81% 19 Missing ⚠️
...ueryengine/plan/relational/sql/ast/AlterTopic.java 30.76% 18 Missing ⚠️
...ion/config/executor/ClusterConfigTaskExecutor.java 0.00% 17 Missing ⚠️
...plan/relational/sql/util/DataNodeSqlFormatter.java 0.00% 15 Missing ⚠️
... and 28 more
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #17780      +/-   ##
============================================
+ Coverage     40.68%   40.99%   +0.30%     
+ Complexity     2620      318    -2302     
============================================
  Files          5244     5253       +9     
  Lines        362374   364620    +2246     
  Branches      46653    47110     +457     
============================================
+ Hits         147419   149460    +2041     
- Misses       214955   215160     +205     

☔ 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.

@jt2594838 jt2594838 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add an IT to show a complete use case.

@VGalaxies
VGalaxies force-pushed the subscription-topic-owner-fencing branch from b6086b2 to 25c34c2 Compare June 2, 2026 09:46
@VGalaxies

Copy link
Copy Markdown
Contributor Author

Addressed the latest review comments in 25c34c2.

  • Added IoTDBSubscriptionTopicOwnerIT as a complete local subscription use case: a stale owner (sn1/epoch 5) is fenced after the topic is owned by sn2/epoch 6, while the current owner can subscribe, poll data, commit, and unsubscribe.
  • Kept the new IT aligned with the current subscription IT convention: it is @ignore because SubscriptionConfig#getSubscriptionEnabled() is currently hard-coded false in this codebase, but the class compiles and is discovered under the with-integration-tests profile.
  • Preserved SubscriptionOwnerFencedException through subscribe redirection so the stale-owner path remains a business fencing error.
  • Merged the SubscriptionTopicAgent contains/get check into one getTopicMeta(...) null check.

Local verification:

  • mvn spotless:apply -pl integration-test -P with-integration-tests -DskipTests
  • mvn verify -DskipUTs -Dit.test=IoTDBSubscriptionTopicOwnerIT -DfailIfNoTests=false -Dfailsafe.failIfNoSpecifiedTests=false -pl integration-test -am -P with-integration-tests
  • mvn test -pl iotdb-client/service-rpc,iotdb-client/subscription -Dtest=TSStatusCodeTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn test -pl iotdb-core/relational-grammar,iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/datanode -Dtest=SubscriptionReceiverV1Test -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • git diff --check

@jt2594838 jt2594838 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Need one more test to show how to update the ownership and how to show the ownership.

@jt2594838 jt2594838 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you waited for all leases to expire before transferring the ownership?

@VGalaxies

Copy link
Copy Markdown
Contributor Author

Addressed the lease-transfer review comment in 82049d7.

Topic ownership transfer now checks the current topic owner lease before replacing owner metadata: if the current owner has an unexpired owner-lease-expire-time-ms and the incoming owner id is different, the transfer is rejected. Once the lease has expired, the transfer can proceed. Added SubscriptionInfoTest coverage for both paths.

Also addressed the inline comments in the same commit:

  • public session alterTopic(...) rejects owner attributes; alterTopicOwner(...) is the session API entry for owner updates.
  • removed the extra deep copy from the alter-topic metadata build path.
  • moved the two hard-coded messages to en/zh i18n constants.

Local verification passed:

  • mvn spotless:apply -pl iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/confignode,iotdb-core/datanode -DskipTests
  • mvn test -pl iotdb-protocol/thrift-confignode,iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/confignode -Dtest=SubscriptionInfoTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn test -pl iotdb-protocol/thrift-confignode,iotdb-core/relational-grammar,iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/node-commons,iotdb-core/datanode -Dtest=StatementGeneratorTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
  • mvn compile -pl iotdb-protocol/thrift-confignode,iotdb-client/service-rpc,iotdb-client/subscription,iotdb-core/relational-grammar,iotdb-core/node-commons,iotdb-core/confignode,iotdb-core/datanode -P with-zh-locale -DskipTests
  • git diff --check

codex added 5 commits June 11, 2026 05:21
Land the technical-review corrections together:

- Seek fencing in SubscriptionReceiverV1 before any position-mutating op.
- Move the owner lease off the node heartbeat onto a dedicated, fine-grained
  SubscriptionOwnerLeaseSyncer pushing relative remaining durations.
- Make the absolute lease expiry DataNode-local runtime state only: it is set
  solely from the lease heartbeat, never an attribute, never serialized, and
  not part of equals/hashCode. CN persists only owner-id, owner-epoch,
  max-owner-epoch and owner-lease-duration-ms.
- Add globally-monotonic, never-reused owner epochs via max-owner-epoch that
  survives clearOwner.
- Drain owner transfers via a ConfigNode admission-wait (blockSince + L + H)
  instead of CN-side lease-not-expired rejection/retry.
- Remove the dead absolute-timestamp lease machinery (renewOwnerLease*,
  getOwnerLeaseRemainingTimeMs, isActiveOwnerLease/isOwnerTransfer, the
  owner-lease-expire-time-ms attribute key, the alterTopic retry loop) and
  update the unit tests accordingly.
Address the i18n review comments: the owner-validation exception messages in TopicMeta now use SubscriptionMessages (en/zh), and the alter-topic warn in SubscriptionCoordinator uses ManagerMessages. The previously flagged renewTopicOwnerLease message no longer exists after the lease rework.
AbstractSubscriptionLocalIT already enables subscription, so the end-to-end stale-owner-fencing + current-owner-consume scenario runs; verified locally via mvn verify -Dit.test=IoTDBSubscriptionTopicOwnerIT (tests=2, failures=0, errors=0, skipped=0).
@VGalaxies
VGalaxies requested a review from jt2594838 June 11, 2026 16:39
Drop 'SN' wording from the PR's new code: owner-id values sn1/sn2 -> owner1/owner2, consumer ids and vars stale_sn/current_sn/oldSnConsumer/newSnConsumer -> owner-based names, and the SN/DN/CN comment -> owner/DataNode/ConfigNode. Pure rename; all affected unit tests still pass.
codex added 2 commits June 12, 2026 03:42
Reject owner-lease-duration-ms below a configurable floor at topic create/alter admission, so the lease stays well above the ~5s renewal heartbeat and a missed beat cannot falsely fence a healthy owner.

- New config subscription_owner_lease_duration_ms_min (CommonConfig, default 60000), loaded by CommonDescriptor, exposed via SubscriptionConfig.
- Enforced in SubscriptionInfo.validateTopicConfig (create + alter admission), with an en/zh ManagerMessages constant. Deserialize stays lenient (>0) so already-persisted topics still load if the floor is later raised.
- IT framework: setSubscriptionOwnerLeaseDurationMsMin on the CommonConfig interface + Mpp/MppShared/Remote impls; the two owner ITs lower the floor in setUp to keep their short leases (and stay fast). Verified the local IT passes (tests=2, failures=0) end-to-end.
Address review feedback: the ConfigNode admission wait is (lease duration + one renewal/heartbeat interval) measured on its own clock, not the lease duration alone, since a renewal may have been in flight to a DataNode just before renewal was stopped. Updated the drain test comment to reflect this; the >= lease-duration lower bound still holds because the actual wait is duration + one interval.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@jt2594838
jt2594838 merged commit abb9ef9 into master Jun 12, 2026
42 of 43 checks passed
@HTHou
HTHou deleted the subscription-topic-owner-fencing branch June 12, 2026 07:54
MileaRobertStefan pushed a commit to MileaRobertStefan/iotdb that referenced this pull request Jun 26, 2026
* Subscription: add topic owner epoch fencing

* Implement alter topic owner entrypoints

* Add topic owner update show test

* Subscription: address topic owner review comments

* Implement subscription topic owner lease renewal

* Use duration-based subscription owner lease renewal

* Renew subscription owner leases in ConfigNode heartbeat

* Rework subscription topic owner epoch fencing and lease

Land the technical-review corrections together:

- Seek fencing in SubscriptionReceiverV1 before any position-mutating op.
- Move the owner lease off the node heartbeat onto a dedicated, fine-grained
  SubscriptionOwnerLeaseSyncer pushing relative remaining durations.
- Make the absolute lease expiry DataNode-local runtime state only: it is set
  solely from the lease heartbeat, never an attribute, never serialized, and
  not part of equals/hashCode. CN persists only owner-id, owner-epoch,
  max-owner-epoch and owner-lease-duration-ms.
- Add globally-monotonic, never-reused owner epochs via max-owner-epoch that
  survives clearOwner.
- Drain owner transfers via a ConfigNode admission-wait (blockSince + L + H)
  instead of CN-side lease-not-expired rejection/retry.
- Remove the dead absolute-timestamp lease machinery (renewOwnerLease*,
  getOwnerLeaseRemainingTimeMs, isActiveOwnerLease/isOwnerTransfer, the
  owner-lease-expire-time-ms attribute key, the alterTopic retry loop) and
  update the unit tests accordingly.

* Move subscription owner fencing messages into i18n constants

Address the i18n review comments: the owner-validation exception messages in TopicMeta now use SubscriptionMessages (en/zh), and the alter-topic warn in SubscriptionCoordinator uses ManagerMessages. The previously flagged renewTopicOwnerLease message no longer exists after the lease rework.

* Reword owner-lease syncer TODO to satisfy the todo-check CI gate

* Enable the local topic-owner fencing IT (drop stale @ignore)

AbstractSubscriptionLocalIT already enables subscription, so the end-to-end stale-owner-fencing + current-owner-consume scenario runs; verified locally via mvn verify -Dit.test=IoTDBSubscriptionTopicOwnerIT (tests=2, failures=0, errors=0, skipped=0).

* Rename SN owner identifiers to neutral owner names in tests

Drop 'SN' wording from the PR's new code: owner-id values sn1/sn2 -> owner1/owner2, consumer ids and vars stale_sn/current_sn/oldSnConsumer/newSnConsumer -> owner-based names, and the SN/DN/CN comment -> owner/DataNode/ConfigNode. Pure rename; all affected unit tests still pass.

* Add configurable minimum owner-lease-duration floor (default 1 min)

Reject owner-lease-duration-ms below a configurable floor at topic create/alter admission, so the lease stays well above the ~5s renewal heartbeat and a missed beat cannot falsely fence a healthy owner.

- New config subscription_owner_lease_duration_ms_min (CommonConfig, default 60000), loaded by CommonDescriptor, exposed via SubscriptionConfig.
- Enforced in SubscriptionInfo.validateTopicConfig (create + alter admission), with an en/zh ManagerMessages constant. Deserialize stays lenient (>0) so already-persisted topics still load if the floor is later raised.
- IT framework: setSubscriptionOwnerLeaseDurationMsMin on the CommonConfig interface + Mpp/MppShared/Remote impls; the two owner ITs lower the floor in setUp to keep their short leases (and stay fast). Verified the local IT passes (tests=2, failures=0) end-to-end.

* Clarify owner-lease drain wait depends on the heartbeat interval too

Address review feedback: the ConfigNode admission wait is (lease duration + one renewal/heartbeat interval) measured on its own clock, not the lease duration alone, since a renewal may have been in flight to a DataNode just before renewal was stopped. Updated the drain test comment to reflect this; the >= lease-duration lower bound still holds because the actual wait is duration + one interval.

---------

Co-authored-by: OpenAI Codex <codex@openai.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