Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -727,19 +727,44 @@ public void destroy() {
}

private void initMetaManager(HugeConfig conf) {
if (conf.get(ServerOptions.META_USE_CA)) {
this.ca = new K8sDriver.CA(conf.get(ServerOptions.META_CA),
conf.get(ServerOptions.META_CLIENT_CA),
conf.get(ServerOptions.META_CLIENT_KEY));
}
connectMetaManager(conf);
}

/**
* Connect the MetaManager under the cluster name of rest-server.properties
* (option 'cluster'). Idempotent, and it fails when the MetaManager was
* connected earlier under another name: the meta keys are prefixed with
* the cluster, so the server would otherwise read an empty tree. Called
* from HugeGraphServer before any graph is opened, because opening an
* hstore graph connects the MetaManager with the graph's 'pd.cluster'
* (default 'hg') if nothing connected it yet. With usePD=false the server
* has no cluster of its own and the graph-level binding is the only one,
* so this is a no-op there: the check must never apply to a prefix the
* server did not bind.
*/
public static void connectMetaManager(HugeConfig conf) {
if (!conf.get(ServerOptions.USE_PD)) {
return;
}
String cluster = conf.get(ServerOptions.CLUSTER);
String endpoints = conf.get(ServerOptions.PD_PEERS);
boolean useCa = conf.get(ServerOptions.META_USE_CA);
String ca = null;
String clientCa = null;
String clientKey = null;
if (useCa) {
if (conf.get(ServerOptions.META_USE_CA)) {
ca = conf.get(ServerOptions.META_CA);
clientCa = conf.get(ServerOptions.META_CLIENT_CA);
clientKey = conf.get(ServerOptions.META_CLIENT_KEY);
this.ca = new K8sDriver.CA(ca, clientCa, clientKey);
}
this.metaManager.connect(this.cluster, MetaManager.MetaDriverType.PD,
ca, clientCa, clientKey, endpoints);
MetaManager manager = MetaManager.instance();
manager.connect(cluster, MetaManager.MetaDriverType.PD,
ca, clientCa, clientKey, endpoints);
manager.ensureCluster(cluster);

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.

‼️ Critical. In a usePD=false HStore startup, HugeGraphServer skips the pre-bind at its usePD guard, while HugeGremlinServer.prepare() opens the graph before HugeRestServer.start(); the graph then binds MetaManager through the pd.cluster fallback (default hg). GraphManager.initMetaManager() subsequently calls this method unconditionally, and the default server cluster is hg-test, so ensureCluster(cluster) throws instead of preserving the graph-level binding. This regresses the usePD=false compatibility path described by the PR. Please gate this check to the explicit usePD=true server bind, or otherwise use the already-bound graph cluster for usePD=false, and add a startup test covering that path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for tracing this path; I checked it two ways and would like to be precise, because in the described form it does not occur. ensureCluster() is called only from connectMetaManager(), which has two callers: HugeGraphServer (explicitly behind the usePD guard) and GraphManager.initMetaManager(). initMetaManager() has a single caller, loadMetaFromPD() (line 376), and loadMetaFromPD() runs only in the constructor under if (PDExist) (lines 271-274), i.e. only with usePD=true. With usePD=false the server never reaches ensureCluster at all; StandardHugeGraph binds pd.cluster and that is the end of it, exactly as in 1.7.0.

Measured on the lab (1 PD + 3 stores, data written by the official 1.7.0 release, a build of this head), with usePD and pd.peers removed from rest-server.properties: with the default pd.cluster the server starts, the graph opens, 0 property keys (the data lives under hg-test, which is expected), and zero ensureCluster/IllegalStateException lines in the log; with pd.cluster=hg-test in the graph file the server starts and sees all 13 keys, log equally clean. With usePD=true restored: 13 keys and Meta cluster bound to 'hg-test'. Log: results/upgrade-170-to-master/logs/usepd-false-round2.txt in the validation repo.

I added the gate anyway in 8e914e6, since it is cheap and protects against a future caller: connectMetaManager() returns without binding or checking when usePD=false, with a comment that the graph-level binding is then the only one and no server-side check may apply to it. Two tests: testUsePdFalseLeavesTheGraphLevelBindingAlone (a graph bound hg, calling the helper with usePD=false does not throw and the cluster stays hg) and testGraphManagerStartupWithUsePdFalseKeepsTheGraphBinding (a GraphManager constructed with usePD=false after such a binding completes without an exception, cluster still hg). MetaManagerClusterTest 9/9.

}

private void initK8sManagerIfNeeded(HugeConfig conf) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,25 @@ public StandardHugeGraph(HugeConfig config) {
}

if (isHstore()) {
// TODO: parameterize the remaining configurations
MetaManager.instance().connect("hg", MetaManager.MetaDriverType.PD,
"ca", "ca", "ca",
config.get(CoreOptions.PD_PEERS));
MetaManager meta = MetaManager.instance();
String cluster = config.get(CoreOptions.PD_CLUSTER);
if (!meta.isReady()) {
// Fallback for usePD=false: with usePD=true the server has
// already connected the MetaManager under ServerOptions.CLUSTER
// (the meta keys are prefixed with the cluster name)
// TODO: parameterize the remaining configurations
meta.connect(cluster, MetaManager.MetaDriverType.PD,
"ca", "ca", "ca", config.get(CoreOptions.PD_PEERS));
} else if (config.containsKey(CoreOptions.PD_CLUSTER.name()) &&
!cluster.equals(meta.cluster())) {
// The prefix is bound once per process: the server's 'cluster'
// or the first hstore graph opened wins, a later different
// 'pd.cluster' would otherwise be dropped silently
LOG.warn("Graph '{}' sets pd.cluster='{}' but the meta cluster is " +
"already bound to '{}' (keys under HUGEGRAPH/{}/); the " +
"graph's value is ignored", this.name(), cluster,
meta.cluster(), meta.cluster());
}
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,18 @@ public class CoreOptions extends OptionHolder {
disallowEmpty(),
"127.0.0.1:8686"
);
public static final ConfigOption<String> PD_CLUSTER = new ConfigOption<>(
"pd.cluster",
"The cluster name prefixing the meta keys in PD " +
"('HUGEGRAPH/<cluster>/...') when the graph itself connects the " +
"MetaManager, i.e. the server runs with usePD=false. The prefix " +
"is bound once per process: with usePD=true the server binds its " +
"own 'cluster' option first, otherwise the first hstore graph " +
"opened wins, and a different value on a later graph is ignored " +
"with a warning.",
disallowEmpty(),
"hg"
);
public static final ConfigOption<String> MEMORY_MODE = new ConfigOption<>(
"memory.mode",
"The memory mode used for query in HugeGraph.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,13 @@ public synchronized void connect(String cluster, MetaDriverType type,
String clientKeyFile, Object... args) {
E.checkArgument(cluster != null && !cluster.isEmpty(),
"The cluster can't be null or empty");
if (this.metaDriver == null) {
if (this.metaDriver != null) {
if (!cluster.equals(this.cluster)) {
LOG.warn("MetaManager is already connected to cluster '{}', " +
"ignoring the connect request for cluster '{}'",
this.cluster, cluster);
}
} else {
this.cluster = cluster;

switch (type) {
Expand All @@ -187,6 +193,24 @@ public synchronized void connect(String cluster, MetaDriverType type,
this.initManagers(this.cluster);
}

/**
* Fail fast when the MetaManager was connected earlier under another
* cluster name: every meta key is prefixed with the cluster, so a server
* that expects `expected` would otherwise silently read an empty tree.
*/
public synchronized void ensureCluster(String expected) {
E.checkState(this.metaDriver != null,
"The MetaManager is not connected yet");
if (!expected.equals(this.cluster)) {
throw new IllegalStateException(String.format(
"The MetaManager is connected to cluster '%s', but the " +
"configured cluster is '%s'; the meta keys live under " +
"'HUGEGRAPH/%s/', set the same cluster name for the " +
"server ('cluster') and the graphs ('pd.cluster')",
this.cluster, expected, this.cluster));
}
}

private void initManagers(String cluster) {
this.authMetaManager = new AuthMetaManager(this.metaDriver, cluster);
this.graphMetaManager = new GraphMetaManager(this.metaDriver, cluster);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.ServerOptions;
import org.apache.hugegraph.constant.ServiceConstant;
import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.event.EventHub;
import org.apache.hugegraph.meta.MetaManager;
import org.apache.hugegraph.meta.PdMetaDriver;
Expand Down Expand Up @@ -68,6 +69,18 @@ public HugeGraphServer(String gremlinServerConf, String restServerConf)
ServiceConstant.SERVICE_NAME,
ServiceConstant.AUTHORITY);

// Bind the meta cluster name ('cluster' in rest-server.properties)
// before any graph is opened: prepare() below opens every graph
// in conf/graphs, and an hstore graph would otherwise connect the
// MetaManager first under its own 'pd.cluster' (default 'hg'),
// hiding the meta written under the configured cluster
if (restServerConfig.get(ServerOptions.USE_PD)) {
GraphManager.connectMetaManager(restServerConfig);

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.

🧹 Minor. This moves the prefix back to hg-test for every usePD=true server that has been running a master build since #3008 (merged 2026-07-10). Those servers wrote their schema, graph spaces and users under HUGEGRAPH/hg/. After this change they come up with an empty schema, and ensureCluster passes, so the log gives no hint. docker/docker-compose-hstore.yml:92 sets HG_SERVER_USE_PD: "true" and no HG_SERVER_CLUSTER, so anyone on the compose setup from master gets the hg-test default (ServerOptions.CLUSTER).

Restoring 1.7.0 behaviour is the right call, and the migration tool is fine as a follow-up. Could you log the bound prefix at INFO here (for example Meta cluster bound to '{}' (keys under HUGEGRAPH/{}/)) and add a line to the upgrade notes for snapshot users? Then an empty schema after upgrading shows its cause in the first lines of the log.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 68b616a. After connectMetaManager() the server logs Meta cluster bound to 'hg-test' (keys under HUGEGRAPH/hg-test/) at INFO, in the first lines of the startup (line 5 of the log on the lab). I added a paragraph to hugegraph-store/docs/operations-guide.md, step "Upgrade Server Nodes": 1.7.0 bound cluster (hg-test), master builds between #3008 and this PR bound the literal hg, from this PR on cluster again; whoever sees an empty schema after an upgrade compares the log line with the prefix their data lives under and sets cluster in rest-server.properties. I did not find a dedicated upgrade-notes file in the repo, so if there is a better place I'll move it. The prefix migration tool stays a follow-up, as agreed.

String cluster = MetaManager.instance().cluster();
LOG.info("Meta cluster bound to '{}' (keys under HUGEGRAPH/{}/)",
cluster, cluster);
}

// Prepare GremlinServer (registers GRAPH_CREATE listener) BEFORE
// RestServer starts loading graphs from PD/meta. This ensures that
// graphs loaded during RestServer initialization are captured by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.apache.hugegraph.unit.core.DataTypeTest;
import org.apache.hugegraph.unit.core.GraphSpaceInfoLocaleTest;
import org.apache.hugegraph.unit.core.GraphManagerStoresWaitTest;
import org.apache.hugegraph.unit.core.MetaManagerClusterTest;
import org.apache.hugegraph.unit.core.DirectionsTest;
import org.apache.hugegraph.unit.core.ExceptionTest;
import org.apache.hugegraph.unit.core.GraphManagerAdminInitTest;
Expand Down Expand Up @@ -141,6 +142,7 @@
DataTypeTest.class,
GraphSpaceInfoLocaleTest.class,
GraphManagerStoresWaitTest.class,
MetaManagerClusterTest.class,
DirectionsTest.class,
SerialEnumTest.class,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* 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.hugegraph.unit.core;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.hugegraph.config.CoreOptions;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.ServerOptions;
import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.event.EventHub;
import org.apache.hugegraph.meta.MetaDriver;
import org.apache.hugegraph.meta.MetaManager;
import org.apache.hugegraph.testutil.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

/**
* The meta keys in PD are prefixed with the cluster name, and
* MetaManager.connect() binds that name once per process. A server that
* connects under the configured 'cluster' must not be preceded by a graph
* connecting under its own 'pd.cluster', otherwise every later lookup reads
* an empty tree (1.7.0 wrote the schema under 'hg-test', master looked
* under 'hg').
*/
public class MetaManagerClusterTest {

/*
* connect() also rebuilds every sub-manager of the singleton, so the
* whole instance state is snapshotted and restored, not just the driver
* and the cluster; the suite must find the singleton as it left it.
*/
private final Map<Field, Object> snapshot = new HashMap<>();

@Before
public void setup() throws Exception {
for (Field f : MetaManager.class.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
f.setAccessible(true);
this.snapshot.put(f, f.get(MetaManager.instance()));
}
swapField("metaDriver", null);
swapField("cluster", null);
}

@After
public void teardown() throws Exception {
for (Map.Entry<Field, Object> e : this.snapshot.entrySet()) {
e.getKey().set(MetaManager.instance(), e.getValue());
}
}

@Test
public void testFirstConnectWinsAndLaterNamesAreIgnored() throws Exception {
connectWithMockDriver("hg-test");
Assert.assertEquals("hg-test", MetaManager.instance().cluster());

// the graph-level fallback ('pd.cluster', default 'hg') is a no-op
MetaManager.instance().connect("hg", MetaManager.MetaDriverType.PD,
"ca", "ca", "ca", "127.0.0.1:8686");
Assert.assertEquals("hg-test", MetaManager.instance().cluster());
MetaManager.instance().ensureCluster("hg-test");
}

@Test
public void testEnsureClusterFailsOnAnotherName() throws Exception {
connectWithMockDriver("hg");

Assert.assertThrows(IllegalStateException.class, () -> {
MetaManager.instance().ensureCluster("hg-test");
}, e -> {
Assert.assertContains("connected to cluster 'hg'", e.getMessage());
Assert.assertContains("configured cluster is 'hg-test'",
e.getMessage());
Assert.assertContains("HUGEGRAPH/hg/", e.getMessage());
});
}

@Test
public void testEnsureClusterRequiresAConnection() {
Assert.assertThrows(IllegalStateException.class, () -> {
MetaManager.instance().ensureCluster("hg-test");
});
}

/**
* The server binds the configured cluster before any graph is opened;
* a graph that got there first under another name is a startup error,
* not a silently empty schema.
*/
@Test
public void testServerConnectDetectsGraphLevelCluster() throws Exception {
connectWithMockDriver(CoreOptions.PD_CLUSTER.defaultValue());

HugeConfig conf = serverConfig(true);
Assert.assertEquals("hg-test", conf.get(ServerOptions.CLUSTER));
Assert.assertThrows(IllegalStateException.class, () -> {
GraphManager.connectMetaManager(conf);
}, e -> {
Assert.assertContains("configured cluster is 'hg-test'",
e.getMessage());
});
}

@Test
public void testServerConnectIsIdempotentUnderTheConfiguredCluster()
throws Exception {
connectWithMockDriver("hg-test");
HugeConfig conf = serverConfig(true);
GraphManager.connectMetaManager(conf);
GraphManager.connectMetaManager(conf);
Assert.assertEquals("hg-test", MetaManager.instance().cluster());
}

/**
* With usePD=false the server binds no cluster of its own: an hstore graph
* opened by HugeGremlinServer.prepare() binds the MetaManager under its
* 'pd.cluster' (default 'hg'), and nothing on the server side may check
* that binding against the server's 'cluster' default ('hg-test').
*/
@Test
public void testUsePdFalseLeavesTheGraphLevelBindingAlone() throws Exception {
connectWithMockDriver(CoreOptions.PD_CLUSTER.defaultValue());

HugeConfig conf = serverConfig(false);
Assert.assertFalse(conf.get(ServerOptions.USE_PD));
Assert.assertEquals("hg-test", conf.get(ServerOptions.CLUSTER));
GraphManager.connectMetaManager(conf);
Assert.assertEquals("hg", MetaManager.instance().cluster());
}

/**
* The same on the real startup path: a GraphManager built with usePD=false
* after a graph already bound 'hg' must construct without touching the
* binding (initMetaManager() only runs from loadMetaFromPD(), i.e. with
* usePD=true).
*/
@Test
public void testGraphManagerStartupWithUsePdFalseKeepsTheGraphBinding()
throws Exception {
connectWithMockDriver(CoreOptions.PD_CLUSTER.defaultValue());

HugeConfig conf = serverConfig(false);
GraphManager manager = new GraphManager(conf, new EventHub("cluster-test"));
try {
Assert.assertEquals("hg", MetaManager.instance().cluster());
} finally {
manager.close();
}
}

@Test
public void testTeardownRestoresTheSubManagers() throws Exception {
Field f = MetaManager.class.getDeclaredField("authMetaManager");
f.setAccessible(true);
Object before = this.snapshot.get(f);
connectWithMockDriver("hg-test");
Assert.assertNotSame(before, f.get(MetaManager.instance()));
teardown();
Assert.assertSame(before, f.get(MetaManager.instance()));
}

@Test
public void testGraphLevelDefaultStaysHg() {
Assert.assertEquals("hg", CoreOptions.PD_CLUSTER.defaultValue());
}

/** rest-server.properties as the server sees it, with usePD set explicitly. */
private static HugeConfig serverConfig(boolean usePd) {
PropertiesConfiguration props = new PropertiesConfiguration();
// HugeConfig.get() casts stored values, it does not parse strings here
props.setProperty(ServerOptions.USE_PD.name(), Boolean.valueOf(usePd));
return new HugeConfig(props);
}

private static void connectWithMockDriver(String cluster) throws Exception {
// connect() builds a real driver, so pre-bind a mock one and the
// cluster the same way the first successful connect() would
swapField("metaDriver", Mockito.mock(MetaDriver.class));
swapField("cluster", cluster);
MetaManager.instance().connect(cluster, MetaManager.MetaDriverType.PD,
null, null, null, "127.0.0.1:8686");
}

private static void swapField(String field, Object replacement)
throws Exception {
Field f = MetaManager.class.getDeclaredField(field);
f.setAccessible(true);
f.set(MetaManager.instance(), replacement);
}
}
Loading
Loading