xiangfu0 commented on code in PR #20058:
URL: https://github.com/apache/pinot/pull/20058#discussion_r4381923763


##########
pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BaseServerStarter.java:
##########
@@ -530,6 +530,8 @@ private void updateInstanceConfigIfNeeded(ServerConf 
serverConf) {
           serverConf.getMultiStageMailboxPort());
     }
     updated |= HelixHelper.updatePinotVersion(instanceConfig);
+    updated |= HelixHelper.updateCapabilities(instanceConfig,
+        org.apache.pinot.spi.config.instance.InstanceType.SERVER);

Review Comment:
   Same minor nit as in HelixHelper: this inline FQN could be an import for 
readability per the repo convention. Purely stylistic.



##########
pinot-common/src/main/java/org/apache/pinot/common/utils/helix/InstanceCapability.java:
##########
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.utils.helix;
+
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.spi.config.instance.InstanceType;
+
+
+/// A feature that a Pinot instance advertises in its instance config, with a 
revision.
+///
+/// Any component (server, broker, controller, minion) may advertise 
capabilities. Each capability names the area of
+/// the feature in its own name (for example `MSE_` for the multi-stage 
engine) and declares the instance type that
+/// advertises it. At startup an instance writes the map field `capabilities`
+/// (`CommonConstants.Helix.Instance.CAPABILITIES_KEY`) of its instance config 
with
+/// `HelixHelper.updateCapabilities`: the key is the name of a capability of 
its instance type and the value is its
+/// [#getRevision()] as a decimal integer, for example `{"MSE_FEATURE": "1"}`. 
Another component reads the map,
+/// for example with [InstanceCapabilityTracker], to decide whether it can use 
a feature that instances of an older
+/// version do not support. A missing map, a missing key or a malformed value 
means revision 0: not supported.
+///
+/// The map also holds the reserved entry `pinotVersion`, the Pinot version 
that wrote it. A reader ignores the map
+/// when that entry is not string-equal to the simple field `pinotVersion` of 
the same instance config, so the
+/// capabilities of a version do not survive a rollback to a version that 
predates them. See
+/// [InstanceCapabilityTracker].
+///
+/// The capabilities of this version are the constants of this class, listed 
in [#values()]. Contract:
+/// - The revision of a capability only grows. Revision `n` implies support 
for everything that revisions below `n`
+///   added. A change that old instances with the same capability cannot 
handle increments the revision.
+/// - A revision is not a Pinot version. Nothing compares Pinot version 
strings.
+/// - A new feature that old instances cannot handle adds a constant with 
revision 1, and adds it to [#values()] in
+///   the same change as the code that supports the feature. An instance must 
never advertise a feature it cannot run.
+/// - The names are persisted in ZooKeeper: never rename or remove a 
capability.
+///
+/// Two capabilities are equal when their names are equal. Instances are 
immutable and thread-safe.
+public final class InstanceCapability {
+  private static final List<InstanceCapability> VALUES = List.of();
+
+  private final String _name;
+  private final InstanceType _instanceType;
+  private final int _revision;
+
+  /// Package-private: production capabilities are the constants of this 
class. Tests create their own.
+  InstanceCapability(String name, InstanceType instanceType, int revision) {
+    // Upper case keeps the name apart from the reserved entry pinotVersion of 
the capabilities map.
+    Preconditions.checkArgument(name.matches("[A-Z][A-Z0-9_]*"), "Capability 
name must be upper case: %s", name);
+    Preconditions.checkArgument(revision > 0, "Revision of capability %s must 
be positive: %s", name, revision);
+    _name = name;
+    _instanceType = instanceType;
+    _revision = revision;
+  }
+
+  /// Every capability of this version.
+  public static List<InstanceCapability> values() {

Review Comment:
   Optional: since equality is by name and the contract is 
never-rename/never-duplicate, an assertion that `VALUES` has no two 
capabilities with the same name would catch a future constant-definition 
mistake early. Not needed while the registry is empty, but cheap insurance when 
capabilities start landing.



##########
pinot-common/src/main/java/org/apache/pinot/common/utils/helix/MultiClusterInstanceCapabilityTracker.java:
##########
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.utils.helix;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.apache.helix.HelixManager;
+import org.apache.helix.PropertyKey;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Tracks the capabilities of the instances of one type across several Helix 
clusters, for example the local cluster
+/// of a broker and the remote clusters that it federates. It holds one 
[InstanceCapabilityTracker] for each cluster
+/// that it watches.
+///
+/// [#getMinRevision] returns the minimum revision across all the clusters. A 
cluster that the caller adds with
+/// [#addUnavailableCluster] (for example a remote cluster that failed to 
connect), or whose tracker failed to
+/// register, counts as revision 0. This is fail-safe: a lower minimum only 
keeps the plan that every version can run.
+/// Without any cluster, the minimum is 0 too.
+///
+/// An unavailable cluster stays at revision 0 for the life of this instance: 
nothing retries the connection or the
+/// registration. For a broker, a remote cluster that fails to connect at 
startup pins the minimum to 0 until the
+/// broker restarts. Both cases are logged at WARN or ERROR with the cluster 
name.
+///
+/// Lifecycle: add the clusters with [#addCluster] and 
[#addUnavailableCluster] while the Helix managers are
+/// connected, then share the instance. [#close] removes the Helix listeners. 
Call it before the Helix managers
+/// disconnect.
+///
+/// Thread safety: the clusters are kept in copy-on-write collections, so 
query threads may call [#getMinRevision]
+/// at any time, also while clusters are added. [#getMinRevision] never blocks.
+public class MultiClusterInstanceCapabilityTracker {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MultiClusterInstanceCapabilityTracker.class);
+
+  private final InstanceType _instanceType;
+  private final List<InstanceCapability> _capabilities;
+  private final List<InstanceCapabilityTracker> _trackers = new 
CopyOnWriteArrayList<>();
+  private final Set<String> _unavailableClusters = new CopyOnWriteArraySet<>();
+
+  /// Tracks the capabilities of [InstanceCapability#values()] for the 
instances of `instanceType`.
+  public MultiClusterInstanceCapabilityTracker(InstanceType instanceType) {
+    this(instanceType, InstanceCapability.values());
+  }
+
+  /// Tracks the given capabilities instead of [InstanceCapability#values()].
+  @VisibleForTesting
+  MultiClusterInstanceCapabilityTracker(InstanceType instanceType, 
Collection<InstanceCapability> capabilities) {
+    _instanceType = instanceType;
+    _capabilities = List.copyOf(capabilities);
+  }
+
+  /// Watches the cluster of the given connected Helix manager: creates an 
[InstanceCapabilityTracker] and registers it
+  /// as an instance config change listener. When the registration fails, the 
cluster counts as revision 0.
+  public void addCluster(HelixManager helixManager) {
+    addCluster(new InstanceCapabilityTracker(helixManager, _instanceType, 
_capabilities));
+  }
+
+  @VisibleForTesting
+  void addCluster(InstanceCapabilityTracker tracker) {
+    String clusterName = tracker.getClusterName();
+    try {
+      tracker.getHelixManager().addInstanceConfigChangeListener(tracker);
+      _trackers.add(tracker);
+      LOGGER.info("Tracking the capabilities of the {} instances of cluster: 
{}", _instanceType, clusterName);
+    } catch (Exception e) {
+      LOGGER.error("Failed to register the InstanceCapabilityTracker for 
cluster: {}, treating the cluster as "
+          + "supporting no capability until restart", clusterName, e);
+      _unavailableClusters.add(clusterName);
+    }
+  }
+
+  /// Adds a cluster that cannot be watched, for example a remote cluster that 
failed to connect. It counts as
+  /// revision 0 for every capability.
+  public void addUnavailableCluster(String clusterName) {
+    LOGGER.warn("Cluster: {} is unavailable, treating it as supporting no 
capability until restart", clusterName);
+    _unavailableClusters.add(clusterName);
+  }
+
+  /// The minimum revision of the capability across the instances of all the 
clusters. 0 when a cluster is
+  /// unavailable, when an instance does not support the capability, or when 
no cluster is tracked. Never blocks.
+  public int getMinRevision(InstanceCapability capability) {
+    if (!_unavailableClusters.isEmpty() || _trackers.isEmpty()) {

Review Comment:
   Minor: this guard reads `_unavailableClusters` and `_trackers` separately, 
so if `addUnavailableCluster` runs concurrently with a `getMinRevision` call 
during startup, the loop can briefly return a higher-than-0 revision before 
converging to 0 on the next call. It's self-correcting and the class documents 
add-then-share as single-threaded setup, so this is fine as-is — just flagging 
it. If you want strict safety during concurrent startup, re-checking 
`_unavailableClusters.isEmpty()` after the loop would close it.



##########
pinot-common/src/main/java/org/apache/pinot/common/utils/helix/HelixHelper.java:
##########
@@ -719,6 +721,33 @@ public static boolean updatePinotVersion(InstanceConfig 
instanceConfig) {
     return false;
   }
 
+  /// Writes the capabilities that an instance of the given type advertises in 
this version
+  /// ([InstanceCapability#values()]) to the map field `capabilities` of the 
instance config, together with the
+  /// reserved entry `pinotVersion` 
([CommonConstants.Helix.Instance#CAPABILITIES_PINOT_VERSION_KEY]) set to the 
Pinot
+  /// version of this build. Readers ignore the capabilities when that entry 
differs from the simple field
+  /// `pinotVersion`, which [#updatePinotVersion] writes. Any instance type 
can call it. Returns whether the field
+  /// changed.
+  public static boolean updateCapabilities(InstanceConfig instanceConfig,
+      org.apache.pinot.spi.config.instance.InstanceType instanceType) {

Review Comment:
   Minor/nit: CLAUDE.md prefers an import over an inline fully-qualified name. 
Since `InstanceType` isn't imported here yet, consider adding `import 
org.apache.pinot.spi.config.instance.InstanceType;` and using the short name in 
both overloads (and the `[#updateCapabilities(...)]` javadoc reference). Not 
blocking.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to