Skip to content

fix(hugegraph-dist): gate init-store on a dedicated init_store.enabled option#3119

Open
bitflicker64 wants to merge 5 commits into
apache:masterfrom
bitflicker64:fix/no-init
Open

fix(hugegraph-dist): gate init-store on a dedicated init_store.enabled option#3119
bitflicker64 wants to merge 5 commits into
apache:masterfrom
bitflicker64:fix/no-init

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #3118

Kept in sync with hugegraph#171

Problem

InitStore always runs full local initialization: scan ./conf/graphs, init
non-hstore backends, create the built-in admin account. That is correct for
standalone / tarball users and must remain the default.

In PD / HStore deployments the storage side already owns metadata, so there
is nothing for init-store to do.

Why the Docker guard is not enough

docker-entrypoint.sh guards init with a flag file, docker/init_complete.
That file lives in the container's writable layer at
/hugegraph-server/docker/init_complete. No compose file mounts a volume over
it, and the server services declare no volumes at all.

That guard works for Docker and does not work for Kubernetes:

Writable layer after a restart Effect
docker restart preserved flag survives, init-store runs once ever
K8s container restart / pod reschedule new container from the image flag is gone, full init-store runs on every restart

So on Helm the flag file never accumulates: every Server pod restart re-runs
the complete local init against a PD/HStore cluster that already owns its
metadata. Working around that is what pushed the chart toward one-shot init
Jobs and HG_SERVER_SKIP_INIT-style switches. A file-based guard cannot fix
this, because the problem is precisely that the file does not persist. The
decision has to come from configuration instead.

This is the whole motivation for the change: it is a distributed-deployment
concern, not a Docker one.

Solution

A dedicated option, init_store.enabled, defaulting to true:

init_store.enabled in rest-server.properties init-store
Unset (shipped defaults) Full init, same as master, standalone safe
true Full init
false No-op, WARN, exit 0, the distributed opt-out

A dedicated option rather than reusing graph.load_from_local_config: that
property already means "whether GraphManager loads graph definitions from the
local directory", its declared default is false, and existing configs may
materialize that default explicitly. Giving it a second contract would make an
explicit false silently skip backend and admin init for standalone
RocksDB/HBase installs.

Docker maps HG_SERVER_INIT_STORE_ENABLED onto the property in
rest-server.properties, the file init-store.sh passes to InitStore.

Auth bootstrap in skip mode

When init-store is skipped, StandardAuthenticator.initAdminUserIfNeeded()
does not run, so a Docker PASSWORD piped into init-store.sh would be read
and discarded, so the server would then come up with the auth.admin_pa default
of pa instead of the requested credential.

The entrypoint therefore writes PASSWORD to auth.admin_pa in skip mode
instead of piping it to init-store.sh, so the account the server creates on
startup uses the requested password.

That startup path has a precondition. GraphManager.initAdminUserIfNeeded() is
reached only from loadMetaFromPD(), which runs only when usePD is true, and
usePD defaults to false. With init-store skipped and usePD false, no
component creates the built-in admin at all, so enabling auth would start a
server that enforces authentication against an empty account list. The
entrypoint refuses that combination and exits with an explanation, and
InitStore logs the same condition for non-Docker installs.

usePD=true is the intended setting for distributed deployments: the
cluster-test rest-server.properties.template already pairs it with
auth.authenticator and auth.admin_pa. The shipped compose files not setting
it is a pre-existing gap, and is left to a follow-up because enabling it also
switches graph loading from local config to PD metadata.

The entrypoint also does not write docker/init_complete in skip mode: nothing
was initialized, so a later run with init-store enabled must still be able to
perform the real initialization.

Files changed

File Change
.../config/ServerOptions.java New INIT_STORE_ENABLED option, default true
.../cmd/InitStore.java Early exit when init_store.enabled=false
.../docker/docker-entrypoint.sh Env → property; PASSWORDauth.admin_pa in skip mode; refuse auth + skip without usePD; no init flag when nothing was initialized
.../unit/cmd/InitStoreConfigTest.java Option defaults + main()-level proof of the early exit
.../unit/UnitTestSuite.java Suite entry
.../docker/test/test-docker-entrypoint.sh Entrypoint lifecycle smoke tests
.github/workflows/docker-build-ci.yml Run the entrypoint tests, and Docker CI on hugegraph-dist/docker/**
docker/README.md Server env var reference

Test plan

mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test -Dtest=InitStoreConfigTest
mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test

InitStoreConfigTest points the temporary rest-server.properties at a
graphs directory that does not exist, then calls InitStore.main() with
init_store.enabled=false. A companion test asserts that the same directory
does make ConfigUtil.scanGraphsDir fail, so the skip test proves the gate is
applied before any graph or admin work rather than being a tautology.

The entrypoint lifecycle is covered separately, with no JVM, backend or Docker
needed. The entrypoint runs against a throwaway install tree whose ./bin
scripts are stubs that record their own invocation:

hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh

Cases: default init, default + PASSWORD, skip via env, skip + PASSWORD
(asserts the password reaches auth.admin_pa and never init-store.sh stdin),
skip via a mounted property with no env var, env overriding a conflicting
property, false → true restart, flag-file suppression of re-init, boolean
parsing parity with the server (FALSE, off, no, and fail-fast on
non-booleans), the : property separator, a backslash password round trip, and
refusal of auth plus skip without usePD.

Manual smoke (optional):

  1. Shipped conf/rest-server.properties (no flag): bin/init-store.sh → full
    init, same as master.
  2. Set init_store.enabled=false, run again → WARN and exit 0 without backend
    init.

Out of scope

  • util.sh / check_port / lsof
  • Helm chart Job / SKIP_INIT removal (the chart sets the env, and usePD=true when it enables auth)
  • Setting usePD=true in the shipped compose files, which also switches graph
    loading from local config to PD metadata
  • GraphManager behavior and the graph.load_from_local_config default

Compatibility

No breaking change for tarball / bare init-store. The option defaults to
true, so unset config keeps the full init path.

Distributed deployments set init_store.enabled=false (or
HG_SERVER_INIT_STORE_ENABLED=false).

…g is false

Honor the existing ServerOptions flag in InitStore so distributed/HStore
can opt out of local backend and admin init. Unset keeps full init for
standalone. Docker maps HG_SERVER_LOAD_FROM_LOCAL_CONFIG to
rest-server.properties.

Fixes apache#3118
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. tests Add or improve test cases labels Jul 26, 2026

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: Disabling local graph loading also bypasses the Docker password-based admin bootstrap, so the requested credential is never installed. Evidence: exact-head static trace through docker-entrypoint.sh, InitStore.java, and GraphManager.java; 3/3 focused tests passed; visible checks are green.

+ "backend/admin init; distributed/Helm sets false.",
ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(),
restConf);
return;

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.

⚠️ This early return also skips StandardAuthenticator.initAdminUserIfNeeded(restConf) below. The Docker entrypoint still handles a documented PASSWORD by enabling auth, piping that password to init-store.sh, and then writing docker/init_complete; with HG_SERVER_LOAD_FROM_LOCAL_CONFIG=false, the password is therefore consumed without creating the admin. In PD mode startup instead uses auth.admin_pa (default pa), so it still ignores the requested Docker credential. Please preserve the authentication bootstrap while skipping only local backend initialization, or explicitly reject/map this configuration combination, and add coverage for PASSWORD together with HG_SERVER_LOAD_FROM_LOCAL_CONFIG=false.

@bitflicker64 bitflicker64 Jul 26, 2026

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.

Fixed in 69ac25e.

I took the "map the configuration combination" option rather than preserving the bootstrap, because initAdminUserIfNeeded() calls setup(), which scans the graphs directory and opens the auth graph store locally. That is the work the skip exists to avoid.

So in skip mode the entrypoint writes PASSWORD to auth.admin_pa instead of piping it into init-store.sh, and the server creates the admin from that on startup. It also no longer writes docker/init_complete, since nothing was initialized and a later run with init-store enabled must still be able to perform the real initialization.

PASSWORD together with skip is covered in hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh, which asserts the password reaches auth.admin_pa and never reaches init-store.sh stdin.

Two caveats I documented rather than fixed, both pre-existing properties of auth.admin_pa:

  • it only applies when the admin account is first created, so changing PASSWORD on a later restart silently keeps the old one, without an error;
  • it leaves the password at rest in rest-server.properties, whereas the enabled path only passes it over stdin.

Both are noted at the line that causes them and in docker/README.md. Happy to take them on here if you would rather they were fixed than documented.

To be explicit about the limit of the testing: the smoke tests prove the entrypoint wiring, not that the server then creates a working admin from auth.admin_pa under PD. I have not run the 3pd-3store cluster to confirm that end to end.

…d option

Replace the graph.load_from_local_config gate with a dedicated
init_store.enabled option defaulting to true. That property already means
whether GraphManager loads graph definitions from the local directory, and
existing configs may materialize its declared false default, so reusing it
made an explicit false skip backend and admin initialization for standalone
RocksDB/HBase installs.

The early return also skipped StandardAuthenticator.initAdminUserIfNeeded,
so a Docker PASSWORD was piped into init-store.sh and discarded without
creating the admin, leaving the server on the auth.admin_pa default. In skip
mode the entrypoint now writes PASSWORD to auth.admin_pa instead, and does
not create docker/init_complete since nothing was initialized. The gate is
read back from rest-server.properties so a config mounted without the env
var behaves the same way.

Cover the lifecycle with a main-level test whose graphs directory would fail
if the early exit were missed, and entrypoint smoke tests for default init,
PASSWORD, skip, skip with PASSWORD, mounted property, and false to true
restart. Run those in Docker CI and extend its path filter to the dist
docker directory.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 26, 2026
@bitflicker64 bitflicker64 changed the title fix(hugegraph-dist): skip init-store when graph.load_from_local_config is false fix(hugegraph-dist): gate init-store on a dedicated init_store.enabled option Jul 26, 2026
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.08%. Comparing base (89b648a) to head (3cb4fda).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
.../main/java/org/apache/hugegraph/cmd/InitStore.java 0.00% 24 Missing ⚠️
...ava/org/apache/hugegraph/config/ServerOptions.java 0.00% 3 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (89b648a) and HEAD (3cb4fda). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (89b648a) HEAD (3cb4fda)
4 1
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #3119       +/-   ##
============================================
- Coverage     37.43%   0.08%   -37.36%     
+ Complexity      520      22      -498     
============================================
  Files           808     748       -60     
  Lines         69581   63271     -6310     
  Branches       9165    8280      -885     
============================================
- Hits          26048      51    -25997     
- Misses        40750   63218    +22468     
+ Partials       2783       2     -2781     

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

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The skip path still fails admin bootstrap for the shipped HStore configuration and has inconsistent option/password handling; backend and plugin registration also occurs before the gate. Evidence: six independent review lanes, exact-head static traces, an uppercase FALSE regression test failing four assertions, and all visible latest-head checks passing.

# ── Init store (once) ─────────────────────────────────────────────────
# With `init_store.enabled=false` (distributed PD/HStore) init-store is a no-op:
# storage owns the metadata and the admin account is created on server startup
# from `auth.admin_pa`. A requested PASSWORD is therefore written to that

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.

‼️ The claim that server startup creates the admin is not true for the shipped HStore Docker configurations. GraphManager calls initAdminUserIfNeeded() only inside usePD=true, but ServerOptions.USE_PD defaults to false and the repository's HStore compose files set only the HStore backend and PD peers. With this skip option plus PASSWORD, InitStore no longer creates the admin and normal startup does not take the PD metadata path, so authentication can start without the requested admin account. Please either make bootstrap work for every advertised HStore skip configuration or reject/avoid this mode unless usePD=true, and add an integration test that authenticates with the supplied password.

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.

Confirmed, and worse than a mismatch: with usePD false nothing creates the admin at all. StandardAuthenticator.initAdminUserIfNeeded is called only from InitStore, and GraphManager.initAdminUserIfNeeded only from loadMetaFromPD(), which is gated on usePD.

Took the reject option in a62f6ff. The entrypoint refuses skip + auth unless usePD=true and exits with the reason; InitStore logs the same for non-Docker installs. Skip without auth is unchanged. usePD=true is already the pairing in the cluster-test rest-server.properties.template.

The shipped compose files not setting usePD is pre-existing and left alone, since enabling it also moves graph loading to PD metadata.

# the same as `HG_SERVER_INIT_STORE_ENABLED` (the env mapping above has already
# been applied, so env still wins).
INIT_STORE_ENABLED=$(get_prop "init_store.enabled" "${REST_SERVER_CONF}")
if [[ "${INIT_STORE_ENABLED:-true}" == "false" ]]; then

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.

⚠️ Shell and Java use different Boolean semantics here. The shell skips only for exact lowercase false, while HugeConfig accepts case-insensitive false values (and Java-properties syntax also permits separators other than =). With HG_SERVER_INIT_STORE_ENABLED=FALSE and PASSWORD, the shell pipes the password to init-store.sh and later writes init_complete, but Java parses the option as false and returns before creating the admin. A focused exact-head test reproduced this with four failed assertions. Please canonicalize or validate the value consistently across both layers and cover uppercase and mounted-property variants.

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.

Confirmed and fixed in a62f6ff. to_bool now mirrors PropertyConverter.toBoolean (BooleanUtils: y/t/on/yes/true, n/f/no/off/false, case-insensitive), canonicalizes before writing so the file only ever holds true or false, and exits non-zero on anything else instead of diverging silently.

get_prop also accepts the : and whitespace separators a properties file allows, not just =. Covered for FALSE, off, no, a non-boolean, and a mounted : separator.

# created, so changing PASSWORD on a later restart silently keeps the
# old one. It also leaves the password at rest in rest-server.properties,
# unlike the enabled path where it only travels over stdin.
set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"

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.

⚠️ set_prop() escapes for sed, not for Java-properties serialization. A valid password containing a backslash, such as abc\def, is written with a single backslash and is read back by the properties parser as abcdef; the old stdin path preserved it. Please serialize the secret using Java-properties escaping (or avoid storing it in this file) and add a round-trip test containing backslashes and other properties metacharacters.

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.

Confirmed and fixed in a62f6ff. props_escape doubles backslashes and escapes a leading space before the value reaches set_prop, so abc\def round-trips instead of arriving as abcdef. Test asserts the escaped form is what lands in the file.

* every Server pod restart, since the entrypoint's init flag file does
* not survive one.
*/
if (!restServerConfig.get(ServerOptions.INIT_STORE_ENABLED)) {

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.

⚠️ This gate runs only after registerBackends() and registerPlugins(). Plugin registration invokes every discovered plugin.register() and propagates failures, so disabled mode can still fail or trigger plugin side effects before reaching the documented no-op/exit-0 path. Please register only the server options first, evaluate this flag, and move backend/plugin registration to the enabled path; add coverage proving disabled mode bypasses those calls.

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.

Confirmed and fixed in a62f6ff. Order is now registerServer(), build the config, evaluate the gate, then registerBackends() and registerPlugins() on the enabled path only.

Verified no default-path change: every key in the shipped rest-server.properties is declared in ServerOptions, which registerServer() covers. Added a test asserting rocksdb.data_path never enters OptionSpace in disabled mode.

… is true

With init_store.enabled=false the built-in admin account is not created by
init-store, and the only other component that creates it is
GraphManager.initAdminUserIfNeeded(), reached from loadMetaFromPD() and so
gated on usePD. That option defaults to false and the shipped HStore compose
files do not set it, so enabling auth in skip mode would start a server that
enforces authentication with no account to authenticate against.

The entrypoint now refuses that combination and exits with an explanation,
and InitStore logs the same condition for non-Docker installs. Skipping
without auth is unaffected. usePD=true is already the documented pairing for
distributed deployments in the cluster-test rest-server.properties template.

Also align the shell with the server's boolean parsing, which is
case-insensitive, so HG_SERVER_INIT_STORE_ENABLED=FALSE no longer means skip
to Java and run to the entrypoint; read the property through the separators a
properties file allows; escape the password for properties serialization so a
backslash survives; and register backends and plugins only on the enabled
path, since plugin registration runs every plugin's register() and propagates
its failures.

Add entrypoint coverage for these plus a test that disabled mode leaves
backend options unregistered. Note a pre-existing arthas config key mismatch
found while checking the option table.
@bitflicker64

bitflicker64 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Unrelated to this change, but noticed while checking the ServerOptions table, so I left a TODO on it rather than silently fixing it here.

The arthas options are declared camelCase (arthas.telnetPort, arthas.httpPort, arthas.disabledCommands) while conf/rest-server.properties ships them snake_case (arthas.telnet_port, arthas.http_port, arthas.disabled_commands). Nothing normalizes between the two, so those keys never match and config.get() returns the declared default. Only arthas.ip works.

It is invisible today because each shipped value equals its default. It bites on any change, e.g. arthas.disabled_commands=jad,exec is dropped and exec stays enabled.

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The mounted-properties override path can create duplicate scalar keys and make init-store or server startup fail. Evidence: six independent review lanes, exact-head static tracing through docker-entrypoint.sh and HugeConfig.java, and all visible latest-head checks passing.

log "ERROR: HG_SERVER_INIT_STORE_ENABLED must be a boolean, got '${HG_SERVER_INIT_STORE_ENABLED}'"
exit 1
fi
set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}" "${REST_SERVER_CONF}"

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.

⚠️ get_prop() accepts the =, :, and whitespace separators, but set_prop() only replaces key=value. With a mounted init_store.enabled:false and HG_SERVER_INIT_STORE_ENABLED=true, this appends a second key instead of overriding the first. Commons PropertiesConfiguration then exposes multiple values for this scalar Boolean, so HugeConfig.get(INIT_STORE_ENABLED) no longer receives a Boolean and init-store/server startup fails; the same issue affects a colon-form auth.admin_pa overridden by PASSWORD. Please make set_prop() recognize the same separators and normalize all existing definitions to one canonical key, then add colon/whitespace env-override tests that load the result through HugeConfig.

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.

Confirmed and fixed in 3cb4fda. set_prop now matches the same =, : and whitespace separators as get_prop and collapses every existing definition into one canonical key=value line, leaving comments alone. Rewrote it in awk so the key and value are matched and emitted literally, with no regex escaping.

Worth noting it is worse than a stale read: a duplicated key fails the scalar type check while the config is still loading, so new HugeConfig(...) throws Invalid value for key 'init_store.enabled': '[false, true]' and both init-store and server startup fail outright. Pinned that with a test that loads a duplicated key through HugeConfig.

Entrypoint coverage added for a colon-form override, a whitespace-form override, a colon-form auth.admin_pa overridden by PASSWORD, and that commented defaults are not counted as definitions.

get_prop was widened to read the `=`, `:` and whitespace separators a
properties file allows, but set_prop still matched only `key=value`. A mounted
`init_store.enabled:false` overridden by HG_SERVER_INIT_STORE_ENABLED therefore
gained a second definition instead of being replaced, and a key defined twice
collects both values into a list that fails the scalar type check while the
config is still loading. Both init-store and server startup would fail. The
same applied to a colon-form auth.admin_pa overridden by PASSWORD.

set_prop now matches the same separators and collapses every existing
definition into one canonical `key=value` line, leaving comments alone. It is
written in awk, so the key and value are matched and emitted literally and no
regex escaping is involved.

Cover colon-form and whitespace-form overrides asserting a single remaining
definition, a colon-form auth.admin_pa override, and that commented defaults
are not treated as definitions. Add a test loading a duplicated key through
HugeConfig to pin why the collapse is required.
@bitflicker64
bitflicker64 requested a review from imbajin July 27, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. tests Add or improve test cases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InitStore should skip local init when graph.load_from_local_config is explicitly false

2 participants