blob: f79f619f1431a8423cb1d46c7f129762b414522c [file] [view]
# Server runtime and query contracts
This guide covers Server configuration, wire formats, query semantics and transaction
ownership. Use it when upgrading applications or changing the
Server runtime. Deployment instructions remain in the [build guide](BUILDING.md)
and [Server module guidance](../hugegraph-server/AGENTS.md).
## Runtime and client configuration
The runtime uses TinkerPop 3.8.1 and Groovy 4.0.25. Build and run every module with Java
17. Applications migrating from TinkerPop 3.5.1 must review the full version interval,
including the configuration changes below.
### Required JVM module options
Server, store initialization, store dumping and Gremlin Console always load the
distribution's `bin/jvm-module.options`. Keep this file with the shipped scripts;
each launcher rejects a missing or unreadable file. Required module permissions
are independent of caller options, so custom `JAVA_OPTIONS` or the Server launcher's
`-j` option do not replace them. Gremlin Console retains its existing
`JAVA_OPTIONS` defaults; store dumping retains its existing command-line interface.
The shared file opens `java.nio` for HBase's reflective `java.nio.Bits.unaligned()`
check during `HBasePlatformDependent` initialization, including on x86 Java 17.
It also opens `java.lang` for SOFA RPC's `ProviderInvoker` initialization, which
reflects on `Throwable.cause`.
PD and Store use their own `bin/jvm-module.options` for the module permission needed
by Hessian serialization on Java 17. Their tests and cluster launchers use the same
component files. CI checks positive-test and startup logs for Java module-access
errors and unsupported JVM options, including errors printed without failing the
operation.
### Launcher GC options
Use `bin/start-hugegraph.sh -g g1` (or `-g G1`) to select G1 explicitly.
`-g zgc` and `-g ZGC` select ZGC; other values are rejected. Without `-g`, the
launcher preserves the JVM's collector choice or the collector set by the caller in
`JAVA_OPTIONS`. When `JAVA_OPTIONS` is empty, `-j "JVM_OPTIONS"` supplies the caller
options instead.
For the default or G1 path, the launcher supplies `-XX:+ParallelRefProcEnabled`,
`-XX:InitiatingHeapOccupancyPercent=50` and `-XX:G1RSetUpdatingPauseTimePercent=5`
before the caller's options, so caller values override these tuning defaults. For
example, `bin/start-hugegraph.sh -g g1 -j "-XX:InitiatingHeapOccupancyPercent=45"`
uses G1 with an occupancy threshold of 45 when `JAVA_OPTIONS` is empty.
Do not combine explicit `-g` selection with a different collector in `JAVA_OPTIONS`
or `-j`; the JVM rejects conflicting collector selections.
### Launcher memory budget
The default launcher heap uses half the smaller of host available memory and the
remaining budget under visible cgroup v1/v2 hard limits, with a 512 MiB minimum and a
32 GiB maximum. It refuses startup when the effective budget is below 512 MiB.
If a known hard limit has missing, unreadable or invalid usage, the budget is zero;
the launcher refuses startup instead of using host memory. For cgroup v1, numeric
unlimited values are recognized only when `uname -m` reports `x86_64`, `aarch64` or
`riscv64` and `getconf PAGESIZE` returns a positive integer. The threshold is the
signed 64-bit maximum rounded down to that page size. Recognized unlimited levels
are skipped so readable finite ancestors still apply; without a finite limit,
host sizing remains available. Unknown or masked machine names, or an unavailable
or invalid page size, keep numeric limits on the finite-limit path.
Inactive file cache can contribute to this budget on a warm restart. For a finite
cgroup v2 limit, the launcher checks the immediate cgroup children of that reclaim
target. If any child's `memory.min` is nonzero, missing, invalid or unreadable, it
excludes cache credit at that limit: child cache may be protected from ancestor reclaim.
If the target directory cannot be enumerated, or a child cannot be searched, cache
credit is also excluded because child protection cannot be determined. Ordinary
searchable directories without cgroup controller files are skipped.
Unprotected children retain the existing warm-cache budget, including inherited and
nested hard limits. A cgroup's own `memory.min` does not block cache credit for its
own limit; another ancestor can still constrain it through child protection.
The cgroup v1 cache calculation is unchanged.
This is a startup estimate, not a guarantee against runtime OOM. Native allocations
and concurrent processes also consume memory. Explicit heap options retain their
existing precedence; see [container heap sizing](../docker/README.md#server-heap-sizing).
### Arthas diagnostics
Server and Store bundle Arthas 4.1.5, managed by the root POM. PD does not embed
an Arthas agent. Diagnostic ports bind to `127.0.0.1` by default. Administrators
can explicitly configure another valid address, including wildcard `0.0.0.0` or
`::`; `localhost` is normalized to `127.0.0.1`. Wildcard listeners can accept both
IPv4 and IPv6 depending on the OS/JVM configuration; restrict both in firewall
rules. Server's `PUT /arthas`
requires authentication to be enabled and the caller to be an authenticated administrator.
Without authentication, the Server endpoint returns HTTP 403, including requests
from localhost or through a reverse proxy. Store has no equivalent administrator
authentication, so `GET /v1/arthasstart` always returns HTTP 403.
For local diagnosis of either running service, use its distribution's bundled
helper as the same OS user that owns the JVM:
```bash
bin/arthas.sh
```
The distribution prepares the agent in `lib/arthas` at build time. The helper
requires JDK 17 and uses `bin/pid`, without downloads, runtime extraction, a user
cache, service restarts or configuration edits. A new local agent listens on
`127.0.0.1:3658`, with HTTP, MCP and reverse tunnels disabled. Use `--pid` or
`--telnet-port` for another running instance, and `--command version` for a one-time
command. Interactive mode requires a terminal; for pipes, scripts or `docker exec`
without `-it`, use `--command` instead. Run `bin/arthas.sh --help` for usage.
When the helper exits, including after `--command`, `quit`, or INT/TERM/HUP, it
stops the agent and all its diagnostic sessions; the application JVM keeps running.
SIGKILL or a host failure cannot run this cleanup: stop any surviving agent manually.
For remote diagnosis, log in to the service host over SSH and run the same command,
or explicitly configure a reachable listener with appropriate authentication and
network access controls as described below.
Official Server (including HStore) and Store images include JDK 17, so the same
helper works inside the container, for example
`docker exec -it <store-container> /hugegraph-store/bin/arthas.sh`.
A JRE-only installation needs JDK 17 for this helper.
If the Server API has already activated Arthas, connect to its existing telnet
port instead: `bin/arthas.sh --telnet-port 8562` with the default configuration.
Use `--target-ip` to select another bind address, for example
`bin/arthas.sh --target-ip 0.0.0.0` or
`bin/arthas.sh --target-ip ::1 --telnet-port 8562 --command version`.
The default is `127.0.0.1`; `localhost` maps to `127.0.0.1`. Wildcard addresses
`0.0.0.0` and `::` may accept both IPv4 and IPv6; the helper connects and cleans
up through `127.0.0.1` for either wildcard. Remote clients connect
to the service host's actual reachable address, not the wildcard address.
An already running agent retains its listener and authentication settings;
helper options do not reconfigure it. To switch to the helper's
HTTP-disabled defaults, run `bin/arthas.sh --telnet-port 8562 --command stop`, then
`bin/arthas.sh`; this stops only Arthas, and leaves the service JVM running.
Arthas can execute code inside the JVM. OS attach permissions control initial
activation; HugeGraph administrator authentication protects only the activation
API, not subsequent diagnostic connections. We recommend enabling Arthas native
authentication, especially when binding beyond loopback. However, native defaults
allow connections from `127.0.0.1` to skip authentication even with credentials;
other OS users or containers in the same Pod can still connect this way. To disable
this bypass, set `-Darthas.localConnectionNonAuth=false` on the service JVM and
restart it. Restrict host/Pod and port-forward access to trusted operators, and
limit firewall rules, port mappings and SSH forwarding accordingly. Authentication
does not encrypt telnet
or ordinary HTTP traffic; use a protected network or encrypted transport for
remote access. Avoid concurrent helper sessions because exiting one stops the
shared agent. A disabled-command list is optional and is not access control.
Pass native Arthas credentials when starting or reusing an agent:
```bash
bin/arthas.sh --target-ip 0.0.0.0 \
--username diagnostics --password '<arthas-password>'
```
Use single-line credentials; printable ASCII avoids the bundled Arthas 4.1.5
telnet client's Unicode limitation. Password-only use defaults to username `arthas`.
Native JVM credentials also apply to helper-started agents: if the target JVM has
`-Darthas.username` or `-Darthas.password`, supply the effective credentials with
`--username` / `--password` for authenticated commands and cleanup, especially
when `-Darthas.localConnectionNonAuth=false` is set. The helper does not clear
native credentials when the corresponding option is omitted.
These credentials are separate from HugeGraph accounts. Batch commands and exit
cleanup use the supplied credentials automatically. For interactive diagnosis,
if prompted, enter `auth --username diagnostics <arthas-password>` in the Arthas
console. Existing agents require their existing credentials; passing new values
does not change them. Password arguments may appear in shell history and process
listings, so handle them on a trusted host. With `--username` alone, native Arthas
generates a password only when none is already configured in the target JVM's
native settings; otherwise it uses that inherited password. Supply `--password` for
authenticated batch commands and automatic cleanup. If cleanup fails, the helper
returns nonzero and the agent may remain running; use the configured or generated credentials with
native `auth`, then `stop`, to close it without stopping the service JVM.
[Arthas native authentication](https://arthas.aliyun.com/en/doc/auth.html) remains
optional and follows upstream defaults: `arthas.localConnectionNonAuth=true`
allows connections from `127.0.0.1` to skip authentication. The helper uses a private
configuration, so editing `lib/arthas/arthas.properties` does not configure it.
If required, the target JVM property `-Darthas.localConnectionNonAuth=false` is
honored after restarting the service JVM. Arthas 4.1.5 generates a password when
binding `0.0.0.0` or `::` without an effective native password and prints it in the target
JVM output and Arthas log; explicitly supplying credentials is more convenient
for remote diagnosis and authenticated cleanup.
Authenticated Server administrators can continue using `PUT /arthas` without an
additional enable switch or restart. Its HTTP/telnet interfaces use the configured
bind address; HugeGraph disables the optional MCP endpoint. The activation response
still contains the same four Arthas configuration keys. For API activation with
native credentials, add `-Darthas.username=diagnostics` and
`-Darthas.password=<arthas-password>` to the Server JVM startup options and restart
Server before activation. These are native JVM properties, not new REST
configuration keys, and also apply to agents started by the local helper as described
above. They may expose secrets in process listings. Without native
credential settings, a specific non-loopback bind address does not require login; wildcard
binds use the generated password described above. API activation alone
leaves the agent running until an operator sends `stop`; connecting through the
helper applies the helper's exit cleanup to that existing agent too.
Before reusing an API-started agent, leave `session` and `stop` out of
`arthas.disabled_commands` (or its legacy alias). `session` verifies the target
PID; `stop` closes Arthas without stopping the service JVM. Existing configurations
remain accepted, but failed PID verification or listener cleanup makes the helper
print an error and exit nonzero; the agent may remain running. To recover from an
agent with `stop` disabled, remove that restriction and restart the service before
activating Arthas again. Newly attached local agents keep both commands available.
No commands are disabled by default. Administrators can optionally set
`arthas.disabled_commands=jad,mc` to restrict selected commands for API activation;
new local agents do not inherit this list.
The existing `arthas.ip`, `arthas.httpPort`, `arthas.telnetPort` and
`arthas.disabledCommands` options configure the Server API-activated agent.
The distribution uses `arthas.telnet_port`, `arthas.http_port` and
`arthas.disabled_commands`. These snake_case keys take precedence over the legacy
camelCase aliases when both are present, including explicit default values.
Conflicting values produce a warning naming both keys and the effective key when
the agent is activated. Remove the unused alias to avoid ambiguity. Store's legacy
configuration fields are retained; the local helper does not read them.
For a synchronous one-time command, POST `{"action":"exec","command":"version"}`
to the agent's `/api` endpoint without a `sessionId`. Check `state=SUCCEEDED`, the
`body.results` version and command status, and `body.jobStatus=TERMINATED`; HTTP 200
alone also occurs for failed commands. Arthas 4.1.5 includes the upstream
[terminal-process interrupt fix](https://github.com/alibaba/arthas/pull/3079) and
[foreground-job cleanup fix](https://github.com/alibaba/arthas/pull/3094), so cleanup
can remove a session whose foreground command has already terminated.
It also includes the [MCP-disabled HTTP handler fix](https://github.com/alibaba/arthas/pull/3127).
Clients that explicitly call `init_session` must call `close_session` when finished.
After closing, `session_info` for that ID reports `state=FAILED` with `session not found`.
[`ArthasApiTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java)
covers one-time execution, explicit session execution and removal.
### Serializer configuration
Use the Gremlin configuration files shipped with the upgraded Server. Serializer classes
move from `org.apache.tinkerpop.gremlin.driver.ser` to
`org.apache.tinkerpop.gremlin.util.ser`. The supplied remote configurations include the
corresponding HugeGraph registries.
The Java Driver sample `gremlin-driver-settings.yaml` and Gremlin Console's
`remote.yaml` now default to GraphBinary V1. The serializer entry changes as follows:
```diff
- className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0
+ className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1
```
Keep the HugeGraph builder and registry in both client configurations:
```yaml
serializer:
className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1
config:
serializeResultToString: false
builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder
ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
```
GraphBinary preserves graph elements and UUID IDs for Java Driver accessors such as
`Result.getVertex()` and `Result.get(UUID.class)`. Untyped GraphSON V1 decodes these
results as maps and strings.
`remote-objects.yaml` serves the Cypher HTTP JSON path and continues to use
`org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1` with
`HugeGraphIoRegistry`. It retains its duplicate registry entry as a TinkerPop driver
compatibility workaround.
For the server, keep the GraphBinary builder and registry. The serializer order is
GraphBinary, untyped GraphSON V1/V2/V3, then typed GraphSON V2/V3. The GraphSON entries
keep the HugeGraph registry. The first entries look like this:
```yaml
serializers:
- className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1
config:
serializeResultToString: false
builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder
ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
- className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1
config:
serializeResultToString: false
ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry,
org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry]
```
The full ordering is in `gremlin-server.yaml`. `application/json` selects untyped V1 in
the supplied server configuration variants.
Typed GraphSON is available for the supported simple values and IDs, not as a
replacement for untyped serialization of every HugeGraph schema or graph object. Typed
V1 is not registered: its legacy `@class` deserialization can construct Java objects
before request authentication. V2/V3 use `@type`/`@value`.
GraphBinary uses `HugeGraphTypeSerializerRegistryBuilder` to write HugeGraph IDs as
their underlying values. Schema results use standard maps and Blob values use standard
binary values (`ByteBuffer` in the Java Driver), including when nested in returned
vertices, edges or collections. `Optional` results carry their value or `null` when
empty, including results from `graph.variables().get()`. `File` results retain the map
shape `{"file": "name"}` using the file name, including inside nested results. Untyped
GraphSON V1 keeps the legacy Tree array of `key`/`value` entries and accepts scalar as
well as element keys through the additional `HugeGraphSONV1MessageIoRegistry` in that
serializer's configuration. Keep this registry limited to untyped V1 messages. Standard
graph-file IO and typed GraphSON serializers keep `HugeGraphIoRegistry` alone and use
the TinkerPop Tree format for their negotiated version.
HugeGraph enums such as `DataType.TEXT` and `Directions.OUT` return their names over
GraphBinary, including in maps and lists. `graph.schema()` returns a map with
`propertykeys`, `vertexlabels`, `edgelabels` and `indexlabels` lists, using the same
schema conversion. TinkerPop enums retain their native wire types. Backend `Shard`
values, including `graph.metadata(HugeType.VERTEX, 'splits', size)`, return standard
maps with `start`, `end` and an exact 64-bit `length`, including inside lists and maps.
The default Driver does not need a private Shard serializer.
Local edge ID filters accept both serialized strings and native edge IDs in mixed
collections: `hasId(not(without(ids)))` and `hasId(within(ids))` agree on membership and
count. Numeric and string vertex IDs keep their distinct types.
Groovy interpolated strings (`GString`, including subclasses) return as standard strings
over GraphBinary, including inside lists and maps. For example, `def value = 42;
"value:${value}"` returns `"value:42"`. The untyped GraphSON path retains the
legacy GString bean map with its `values` and `strings` fields.
Cypher extension predicates use the TinkerPop predicate constructor through the
translator extension point. Computed-expression regex filters retain whole-string
matching (`String.matches`), so `"marko"` matches `"mar.*"` and does not match `"ark"`.
The public `cypherRegex`, `cypherIsString`, `cypherIsNode` and `cypherIsRelationship`
factories imported by the bundled Gremlin configuration use that same adapter, so the
Gremlin translation returned by `EXPLAIN` can be replayed.
Cypher results normalize HugeGraph IDs, including values nested in collections, maps and
paths. Cyclic results and nesting deeper than 32 levels are rejected.
The default script engine is Gremlin Groovy. Validate application scripts against the
full TinkerPop 3.5.1 to 3.8.1 upgrade interval before deploying.
## YAML configuration safety
Jackson JSON and YAML modules share the root BOM version. Fabric8 kubeconfig extensions
can contain integer and decimal values; the K8s configuration regression verifies auto-
configuration without contacting a cluster.
The TinkerPop 3.8.1 runtime uses SnakeYAML 2.2, Spring Boot 2.5.15 and Spring Framework
5.3.27. The Boot patch keeps PD and Store's YAML startup loader compatible with
SnakeYAML 2.x. These versions belong to the upgraded runtime; do not apply the SnakeYAML
override to a TinkerPop 3.5.1 deployment. The runtime also manages Jackson YAML 2.15.2
so Fabric8 can read kubeconfig with SnakeYAML 2.2. Store's Log4j SLF4J binding and CLI
logging use 2.18.0; Store Node retains its Boot-managed Log4j core version.
Quota templates and PD-delivered Store configuration accept standard YAML maps, lists
and scalars. Custom Java object tags such as `!!com.example.Type` are rejected before
object construction. Keep these files as configuration data; Java class names used as
ordinary string values remain supported.
## List property compatibility
The public TinkerPop `Graph.Features` contract changes:
`features().vertex().properties().supportsUniformListValues()` and
`features().edge().properties().supportsUniformListValues()` now return `false` instead
of `true`. Clients and compatibility tests that inspect these flags may stop enabling
uniform-list operations. The corresponding graph-variable flag remains `true`.
HugeGraph still supports vertex and edge properties whose schema declares `LIST`
cardinality with `valueList()` and a supported element type, for example
`schema.propertyKey("tags").asText().valueList().create()`. Values must match the
schema's element type. This schema capability does not provide arbitrary Java `List`
values for `SINGLE` properties. The feature-reporting change itself does not remove
stored lists or require rewriting existing list data.
When upgrading, review client feature-detection branches. Applications using HugeGraph
list properties should retain their explicit element type and `LIST` schema, and verify
list writes and reads with that schema. Generic TinkerPop clients should honor the
reported feature flags; do not treat a skipped uniform-list compatibility test as
validation of HugeGraph's schema list path.
## Query semantics
### First query after startup
Server initializes the condition-query serializer on its trusted startup thread before
REST and Gremlin accept requests. A first label-filtered Gremlin query must succeed
without a preceding REST graph query, and subsequent REST queries must remain usable.
Gremlin scripts still cannot read restricted system properties such as `java.version`
or disable the security manager.
### Property-query index coverage
For type-compatible values, a global property-index query such as
`g.V().has("name", "alice")` or `g.E().has("weight", 0.5)` requires compatible
indexes on every visible candidate label. A candidate declares all queried non-OLAP
properties; even a label with no records requires coverage. Base edge labels have no
direct records; their indexes can cover sub-labels. An explicit sub-label query
also uses a compatible query-visible parent index when no compatible direct index
exists, while still filtering out sibling sub-labels. Shared OLAP indexes follow the graph read
mode.
| Candidate state | Query behavior |
| --- | --- |
| Every visible candidate has compatible indexes | The query combines the indexed results. Composite and joint index plans may serve different labels in the same query. |
| A visible candidate lacks a compatible index and the values pass the property-type check | The query fails with `NoIndexException`, including when other candidates are hidden or deleting. |
| Every candidate is excluded by visibility | The query returns an empty result, including after those labels' indexes have been removed. |
When query-visible candidates remain, index matching precedes the property-type
check. If at least one index matches but a value fails that check, the query returns
an empty result before complete coverage is checked. If no index matches, it still
raises `NoIndexException`, even for a type-mismatched value.
Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED`
status. Internal requests that enable `showHidden` or `showDeleting` include the
corresponding labels and still require their indexes. Visibility follows the outer
request when a query is split into subqueries. Each branch checks index state and
complete coverage before opening its backend iterators. If branch construction
fails, the query closes cursors from both the unfinished branch and earlier
branches, preserving the original error. Joint index planning closes unselected
cursors and transfers the selected cursor to the result. This does not provide a
snapshot against concurrent schema changes.
To fix incomplete coverage, specify an indexed label, for example
`g.V().hasLabel("person").has("name", "alice")`, or add indexes compatible with the
query predicates to every candidate label. Explicit-ID lookups and adjacency
filtering do not introduce a global property-index coverage requirement.
### Predicate and value compatibility
Version-specific handling retains negated predicates and filtering barriers where
pushdown would change their meaning. For UUID IDs, local negated filters such as
`hasId(P.not(P.neq(uuid)))` retain the same matches as ID equality. This also applies
when an ID filter stays local beside a property filter that cannot be pushed down. The
complete `HasStep` remains local; string and numeric ID comparisons keep their existing
semantics. `P.typeOf()` runs as a local filter for system and schema properties,
including inside negated or connective predicates. Its type operands are preserved
without schema value conversion. Missing properties do not match, including when the
type predicate is negated. Type filters do not use backend indexes on their own. The new
TinkerPop step APIs are adapted without discarding property metadata.
Named `GValue` predicates retain their bindings when reused or cloned, and
`updateVariable()` applies when the predicate builds subsequent traversals. Explicit
named `limit()`/`range()` bounds have the same count behavior as literal bounds. Local
String ID comparisons retain their leaf semantics inside `not`, `and` and `or`
predicates. Mixed ID collections retain the typed matches of strings, numbers and UUIDs
when a filter stays local; all-string collections keep standard string-ID comparison
behavior. Fractional numeric IDs are not truncated to integer IDs.
Count filtering is only truncated when every predicate branch has a supported bound that
preserves the filter result. Unsupported negated collection or custom predicate branches
keep the complete count, including inside `and` or `or` conditions.
DATE schema properties accept TinkerPop `OffsetDateTime` values for writes and indexed
equality or range queries. Values normalize by instant to the existing `java.util.Date`
millisecond representation; offsets do not change the stored instant, and sub-
millisecond precision is truncated. Existing DATE data needs no migration.
### Counts and transaction visibility
Counts made inside a transaction include that transaction's uncommitted vertex and edge
changes. Only `count()` supports this fallback; other aggregates with uncommitted
changes are rejected. Queries that combine uncommitted changes with pagination, a limit,
or an offset remain unsupported.
Queries and counts select the current transaction version before applying filters. When
an element is removed and re-added with the same ID, the new element shadows the old
updated record, even if only the old record matches the query's label.
A count query that enters an index query path remains unsupported when that index
transaction has uncommitted changes. It reports `Can't do index query when there are
changes in transaction`. This includes indexed-property conditions and label-only
queries on backends that need a label index. Graph-wide and explicit-ID counts can still
use transaction merging without entering that index path.
Unsupported text predicates remain traversal filters. For ordinary GraphStep and
VertexStep extraction, if any condition in a `HasStep` cannot be converted, the whole
step remains local, including its sibling label, ID and property conditions. Count
optimization preserves that filter step. Existing special handling around `match()` and
connective label filters remains part of traversal optimization.
A self-loop contributes two occurrences to a vertex's `bothE()` traversal and one to
each of `outE()`, `inE()` and the graph-wide `E()` traversal. Counts retain these
multiplicities before and after transaction commit, including edge updates.
Count optimization preserves steps that can filter candidates. Resetting an optimized
count traversal permits it to execute again. Query-step equality does not depend on the
result iterator from a previous execution.
A scan in the middle of a traversal is counted once per incoming traverser, including
its bulk. It is not replaced by a single backend count. For example, with three
vertices, `g.V().V().count()` returns `9L`, while `g.V().count()` returns `3L` and
remains eligible for count optimization.
### Read-only application checks
Run these against an existing graph, replacing the label, keys and values with ones
present in that graph. The queries do not write graph or schema data. Use property keys
with the query indexes required by your backend. The `age` example assumes an integer
property; the path example needs an existing outgoing edge.
```groovy
g.V().hasLabel('person').has('name', 'marko').count()
g.V().hasLabel('person').has('age', P.gt(0)).order().by('age').range(0, 2).values('age').toList()
g.V().has('name', 'marko').limit(1).out().limit(1).path().toList()
g.V().has('name', 'marko').limit(1).id().toList()
```
The first result is a numeric count. The second is a list of at most two numeric values
in ascending order. The third is a list of paths; untyped GraphSON represents each path
with `labels` and `objects`. The fourth is a list of graph IDs encoded as scalar values
by GraphSON or GraphBinary; the scalar type follows the graph's ID type.
For Cypher, send this read-only query to `POST
/graphspaces/{graphspace}/graphs/{graph}/cypher`, substituting existing graph data:
```cypher
MATCH (n:person) WHERE n.name = 'marko' RETURN id(n) AS nodeId
```
The response has rows under `result.data`; each row is a map and `nodeId` is a JSON
scalar, not a HugeGraph internal ID class. The Cypher API test covers this shape, nested
IDs and paths.
## HTTP request errors
The supplied WebSocket/HTTP channelizer releases each HTTP request buffer once,
including when a traversal alias is unknown. Such a request returns one HTTP 400, and a
later valid request on the same keep-alive connection receives its own response. This
avoids TinkerPop 3.8.1's duplicate release and second HTTP 500, which could be mistaken
for the next request's response by a connection pool. The correction applies when the
configured channelizer is TinkerPop's standard `WsAndHttpChannelizer`; HugeGraph selects
its compatibility implementation at startup and retains TinkerPop's WebSocket handling.
Custom channelizer classes keep their configured behavior. Unknown aliases still require
the caller to use a graph or traversal-source name exposed by the server.
## Cypher HTTP contracts
### Request forms
The endpoint is `/graphspaces/{graphspace}/graphs/{graph}/cypher`.
| Method and content type | Request | Behavior |
| --- | --- | --- |
| GET | `?cypher=<URL-encoded statement>` | Query-string form works |
| POST `application/json` | Raw Cypher text | Legacy raw-body form works |
| POST `text/plain` | Any body | Rejected with HTTP 415 |
| POST `application/json` | JSON object below | Separate bindings work |
```json
{
"cypher": "MATCH (n:person) WHERE n.name = $name RETURN n.name",
"parameters": {"name": "marko"}
}
```
POST bodies containing JSON arrays, strings, numbers, booleans, or null are rejected
with HTTP 400. Malformed JSON objects and trailing tokens are also request errors and do
not fall back to raw Cypher.
For the JSON-object form, `cypher` must be a nonblank string and `parameters`, when
present, must be an object. Omitting `parameters` means an empty map. Missing bindings
produce an execution error; an explicit null value is accepted. Bindings may be named
`id` and `label`. There is a fixed limit of 16 top-level parameters. This Cypher
processor does not support a `maxParameters` configuration setting. Values containing
quotes and newlines remain bound data rather than changing query text.
A Map counts as one top-level parameter. Group related values into one Map and reference
its fields explicitly; the vertex label and required properties must match the graph's
existing schema:
```json
{
"cypher": "CREATE (n:person {name:$props.name, age:$props.age, city:$props.city}) RETURN n.name",
"parameters": {"props": {"name": "new-person", "age": 20, "city": "Beijing"}}
}
```
Do not use the shorthand `CREATE (n:person $props)`: the pinned translator does not
assign the bound Map's properties in that path. The parameter limit counts top-level
bindings, not the entries inside a bound Map or a returned Map. A Map containing 17
entries can be bound through one parameter with explicit property references; 17 top-level
bindings are rejected.
The translator's reserved null-marker string `" cypher.null"` (two leading spaces) is
rejected in binding values, including nested maps and lists, to avoid silently
converting a user string to null. Actual JSON null remains accepted. The same marker in
query literals or stored properties is an existing translator limitation and is outside
this compatibility target. Check the HTTP status and body `status.code`: execution
failures retain HTTP 200 with `status.code` 400 and null result data.
Traversal iteration failures, including Java errors, attempt rollback on the execution
thread and return a terminal failure response. A rollback exception is attached to the
original failure.
### Compatibility coverage
The API fixture uses a strong schema with a `SECONDARY` index on `city` and a `RANGE`
index on `age`. The regression suite covers:
| Area | Covered behavior |
|---|---|
| Reads | Label scans, equality and range predicates, computed regex whole-string matching, boolean combinations, directed one- and two-hop patterns, empty results. |
| Results | Aliases, scalar and node values, nested maps/lists, relationship IDs, path shape, null and missing-property semantics. |
| Aggregation and pagination | `DISTINCT`, `count`/`sum`/`min`/`max`/`avg`, `ORDER BY`, `SKIP`, and `LIMIT`. |
| Parameters | String, number, boolean, null, empty and missing bindings; quote/newline preservation. |
| Writes | Vertex and edge `CREATE`, property `SET`, edge and vertex `DELETE`, with native REST readback. |
| Failures and routing | Invalid syntax, request shape, binding keys, schema values, authentication, and graph routing. |
| Failed writes | A rejected multi-vertex create leaves no residue in later Cypher and native reads. |
Write regressions check persisted state through native REST reads. Applications are not
required to issue native reads after each successful Cypher write.
### Scope and regression sources
Advanced constructs including `MERGE`, `OPTIONAL MATCH`, `WITH`, `UNWIND`, `UNION`, and
variable-length paths are not established by the compatibility coverage described here.
HStore-specific behavior, Bolt, cross-request transactions, external client versions and
performance also require separate validation. This guide does not claim full openCypher
or Neo4j compatibility.
The pinned translator is `org.opencypher.gremlin:translation:1.0.4`. Its translation
behavior defines the limitations documented above.
Long-term regression sources:
- [`CypherApiTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java)
covers request forms, schema-aware reads and writes, result shapes,
authentication and graph routing, and failed-write isolation.
- [`CypherClientTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java)
covers result normalization and HTTP client behavior.
- [`CypherOpProcessorTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessorTest.java)
covers binding validation and execution failure handling.
## Transaction and graph ownership
Server releases the current thread's graph transactions when a REST request finishes and
when an authenticated context task exits. Cleanup visits every registered graph even
when one graph fails to close, and reports failures. Cleanup explicitly rolls back
unfinished writes, including when `onClose(COMMIT)` was configured, then releases
backend transactions and resets thread-local transaction behavior and listeners.
Applications must commit successful writes before leaving the request/task boundary.
Closing an OLTP traverser preserves the caller's transaction so that the caller can
still commit or roll it back.
The cleanup also applies when schema caches have become cold. Deleting an auth project
or user must remove its associated access or belong edges while preserving unrelated
relationships. The special OLAP vertex path remains separate; negative schema IDs alone
do not identify edges that can be skipped.
Shared schema and element caches retain their invalidation listeners until the graph
closes. Request cleanup releases backend leases while preserving those graph caches and
their schema identity.
Task database workers release their owned task and graph transactions on the worker
thread. A failure while closing one owner must not skip cleanup of other owners or
server-info resources. Cleanup retains the first exception and attaches later cleanup
failures as suppressed exceptions.
For the local Standard scheduler, a deleted task disappears from public lookup after its persisted record is
removed, while its actual execution still counts toward drain until native cleanup finishes; late completion saves from
that owner do not recreate the removed record.
Synchronous task cancellation preserves a transaction already open on its caller,
including uncommitted graph writes. Cancellation still runs the normal completion and
status callbacks, but their default cleanup does not close that existing transaction.
The scheduler closes transactions opened by a cancellation callback when it had no
transaction before starting, and a running task retains its worker owner until that
worker's own cleanup finishes. The caller remains responsible for committing or rolling
back its original transaction.
Graph close attempts auth, scheduler and caller-transaction cleanup separately. If a
scheduler remains registered or the graph reports active transactions, the graph remains
open for a later close attempt. Pending local tasks retain their task database transaction
so they can persist completion before the next close attempt. An incomplete scheduler
drain does not queue transaction cleanup behind that scheduler's blocked worker. Stopping
distributed task dispatch is distinct from completing drain: a timeout does not make
the next close return success.
During Server shutdown, the task manager waits for task workers to finish, then retries
closing graphs retained in its existing scheduler registry before stopping the task
database worker. This gives pending tasks time to persist completion and releases the
task database owner's transactions on that owner's thread. Each retained graph gets one
close attempt per shutdown call. A close failure remains visible and keeps the database
executor available for a later explicit retry; it does not start background recovery.
A terminated cron executor already satisfies the scheduler's dispatch barrier. Every
executor wait, including an explicit retry after a timeout, checks actual termination;
shutdown requested alone is insufficient. Store-close notifications finish before the
shared event executor is stopped.
Once these logical checks pass, graph close releases shared cache listeners, invokes
`provider.close()` and destroys graph locks even if an earlier cleanup step reported an error. The error
remains visible to the caller. Logical graph closure does not prove that a failed
backend native-resource release succeeded; callers must inspect the reported failure
rather than infer physical release from graph state.
Regression sources:
- [`TransactionLifecycleTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/TransactionLifecycleTest.java)
exercises request and authenticated-task ownership and rollback.
- [`StandardTaskSchedulerTxTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/StandardTaskSchedulerTxTest.java)
exercises task worker transaction cleanup.
- [`HugeFactoryTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/HugeFactoryTest.java)
exercises graph close failure continuation, distributed drain retry and Server stop
with a task that outlives the early graph-close attempts.
## Compatibility evidence and deployment checks
Checked-in regression tests define a coverage boundary, not a supported matrix for every
external client, authentication method or backend deployment.
| Path | Regression coverage | Deployment checks still required |
|---|---|---|
| GraphSON | Serializer configuration, MIME selection and wire fixtures. | External client versions, network and authentication combinations. |
| GraphBinary | Shipped Driver configuration, typed results and in-process ID, predicate and element round trips. | External client, network and authentication combinations. |
| Cypher HTTP | Local API harness, result IDs and paths, Basic-auth test client. | Bearer authentication and external REST clients. |
[`GremlinConfigCompatibilityTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java)
contains serializer and configuration regressions. Use the Cypher sources linked above
for API behavior. Keep runtime dependency versions aligned with the root POM and shipped
configuration rather than copying old configuration fragments into a different runtime.
Structure and Process suites exercise TinkerPop behavior on Memory, RocksDB and HStore.
HStore has separate standard-process and feature-test selections.
[`HugeGraphProviderLifecycleTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java)
checks backend cleanup and graph reopening. The test selections and setup are
maintained in the [test POM](../hugegraph-server/hugegraph-test/pom.xml),
[Server CI](../.github/workflows/server-ci.yml) and
[PD/Store CI](../.github/workflows/pd-store-ci.yml).
Inspect executed tests for the backend and protocols used by the deployment. Selected
Structure or Process suites must execute tests; an all-skipped report is not
compatibility evidence. Run the read-only application examples against representative
existing data and validate the serializers, client versions and authentication
combinations actually used by the application.
## Cache RPC graph lifecycle
When cache RPC is enabled and a backend declares shared storage, Server registers both
graph and schema cache notifiers for graphs loaded during startup and graphs created or
loaded later. A new graph's RPC registration must succeed before its local publication.
Graph services use the graph space and graph name together as their service identity.
PD deletion and startup coordinate RPC removal for the same graph instance. Startup does not
register an owner whose RPC removal is pending, even after its placement-event token is consumed,
and checks logical closure under the actual owner's RPC lifecycle monitor before registration.
Any retained provider configuration must be unregistered before startup can export services;
a cleanup failure fails startup instead of re-exporting that owner. Graph clearing, task drains
and full graph close remain outside this RPC publication gate.
Internal graph loading registers RPC services in Server's administrative scope and
restores the caller's identity afterward. Direct registration and cleanup remain
administrative operations.
Successful graph close releases that graph's notifier listeners, providers and consumer
proxies. A failed task drain during ordinary close or non-clearing local removal retains
the owner and its RPC resources. In PD-enabled deployments, RPC cleanup failures during
graph DELETE retain the local owner and bindings for retry; partial cleanup does not imply
a healthy service. This DELETE path first closes task admission and drains local executions,
then cleans up RPC before metadata deletion and backend clearing. A failed drain or RPC
cleanup can be retried with the same DELETE. Logical graph closure fences access before
all resource cleanup completes. Any close failure retains the local owner and bindings;
retrying the same DELETE retries cleanup without repeating backend clearing. A completed clear
is remembered for that exact graph instance even if an external idle transaction delays close;
retry after that transaction exits. A replacement graph with the same name is cleared independently.
A graph closed by a non-clearing removal has no successful-clear certificate. A later clearing DELETE
refuses that closed owner and keeps its binding, rather than reporting success while old backend data remains.
Raft provider, node and an owned listener are closed independently. The first fatal Java Error takes
priority over ordinary close failures; otherwise the first failure is retained. Other failures are
reported as suppressed, with a repeated reference to the primary failure ignored.
A graph using Server's shared Raft listener closes
only its own node. Server releases that listener and its address registration on shutdown;
deleting one graph keeps the listener available to other local graphs. An interrupted node
join retains the graph owner and interrupt status so a later DELETE can finish cleanup.
The context's backend, snapshot and optional read-index executors stop after node join and
drain queued work before close completes. Their wait uses `task.wait_timeout`; timeout or
interruption retains the graph for retry without cancelling queued operations.
RPC cleanup can start before graph close finishes. Clearing PD DELETE unregisters RPC before
metadata removal and backend clear. Factory, binding and quota removal require a successful graph close;
a failed close retains their owner even if its RPC services have already been unregistered.
One failing graph does not prevent other graphs in a PD removal batch from being processed.
Fatal Java Errors retain their original identity and
propagate out of the removal batch. Failures after backend clearing do not undo deletion;
retries do not restore deleted data or placement metadata.
For local deletion without PD, backend clearing precedes RPC cleanup during graph close.
A close failure retains the same local graph, factory identity and configuration for DELETE retry,
even when access has already been fenced by logical closure. Only a successful clear by that
instance's local drop permits retry to skip clearing and finish its retained resource cleanup.
An ordinary close does not provide that certificate. A failure before clearing keeps the open
owner's existing RPC services. A later cleanup failure does not preserve or restore deleted data.
Failed creation or local loading closes the candidate in Server's administrative scope.
If rollback close fails, Server retains the local owner and factory identity for cleanup retry.
Retained failed candidates are excluded from RPC startup even while they are still open.
An old graph's cleanup cannot remove or re-export a replacement graph's services.
A PD graph-add notification blocked by an existing owner is coalesced until that owner is
released, including when independent watches deliver the new ADD before the old REMOVE.
Duplicate ADD notifications alone keep the healthy owner. Server consumes the PD acknowledgement
of its own local graph creation without retaining a replacement ADD unless that exact owner already
has failed peer cleanup; the existing cleanup retry takes priority. A healthy creation acknowledgement
cannot republish the graph during a later reload. After removal, Server rereads the latest configuration;
a missing configuration creates nothing. Reload consumes pending ADDs only after each old owner is
successfully removed, then loads current metadata together instead of replaying ADDs during removal.
After a peer removal fails, a later same-name PD add retries non-clearing cleanup of that exact
owner before loading its replacement. Another cleanup failure keeps the owner for the next add
or normal reload; no background retry is scheduled. A replacement load failure is logged and
remains pending for a later add or normal reload. The completed old-owner DELETE still succeeds.
Events queued for a closed notifier are ignored; an RPC request already sent cannot be recalled.
Cache RPC uses the configured peer addresses; local provider export does not establish
that every remote consumer connection is ready. Notifications are asynchronous, and
connection or delivery failures are logged. This lifecycle handling does not provide
acknowledged delivery or a distributed cache consistency protocol. Validate actual DML
and cache behavior against the deployment's shared backend and topology.
## Deleting a graph with local tasks
Stop application writes before deletion and keep them stopped during retries. The local
scheduler closes task admission and waits for actual executions, cancellation callbacks
and their transaction cleanup before data or metadata is removed. If draining times out,
the graph stays registered with its data and configuration, but task admission remains
closed: new asynchronous tasks are rejected even after the existing owners exit. Keep
application writes stopped and retry deletion after those owners exit. If no destructive
cleanup has begun, restart the Server to resume task admission instead of completing
the deletion. Closing the scheduler alone leaves unstarted persistent work in storage.
This is a local shutdown guarantee. For a graph shared by several Servers, stop writes
and tasks on every Server before deletion; notification delivery is not a cluster-wide
barrier. Do not recreate the same graph name until deletion finishes. Use a restart for
configuration changes rather than hot reload, whose recovery is handled separately.
Deletion is not transactional: a failure after destructive cleanup needs operator
inspection, and retry does not restore removed data. An owner-cleanup failure must not
be treated as a successful drain; inspect it instead of force-removing the scheduler.
## Local loading ownership
Server keeps ordinary import ownership in memory on each graph instance. The first
owner changes NONE to LOADING; releasing one owner leaves LOADING while others are
active, and the last release restores NONE. Manual LOADING, RESTORING and MERGING
cannot be adopted. Original enum setters reject every request while owners are active,
including bare LOADING; their original behavior remains available without active owners.
`GET /graphspaces/{space}/graphs/{graph}/mode/owners` returns
`{"protocol":1,"instance":"<UUID>","epoch":0,"revision":0}`. This response is illustrative;
recovery must use the actual values returned by the latest GET. `PUT` on that path accepts
`{"action":"acquire","instance":"<UUID>","epoch":0,"token":"<import UUID>"}`;
`release` uses the same tuple. Receipts echo protocol/instance/epoch/token and return
`state` (`active` or `released`) and the effective `mode`. Both operations use existing
graph-owner/STATUS WRITE permissions; the authenticated principal owns the token.
Each live graph admits at most 1024 concurrent owners. At that limit, a new owner
receives HTTP 409 without changing state; existing-owner retries, release and
administrator recovery remain available. Releasing an owner frees a slot.
The request principal is also bound for graphs opened without the authentication proxy.
Each new acquire, actual release and successful recovery writes an INFO audit entry
with the graph name, graph instance, previous/resulting mode, epoch and owner counts. Idempotent
retries do not add entries. These audit entries omit tokens and authenticated principal values.
Keep one tuple and identity for the whole import attempt, including uncertain acquire
outcomes. Release after that import's workers stop. Acquire/release retries are
idempotent. An unknown release cancels a late acquire; a released handle cannot be
reused. Terminal records are bounded to 1024 per epoch. Epoch advancement retains
active old handles and permanently retires all other old handles. An initial
acquire prepared before epoch advancement can therefore be rejected even if it
never became active. The client must not automatically refresh that retired handle:
a new token could undo a prior cancellation or administrator recovery. After a
terminal acquire rejection, stop and drain that import attempt before creating a
fresh Loader context to retry.
This protection coordinates Loader processes connected directly to the same stable
Server and graph instance. It does not fence ordinary data requests across load
balancers, node failover, graph replacement or Server restart. There is no automatic
expiry: after kill -9 or permanent disconnection, stop the import and its in-flight
writes before retrying release with the same handle and identity. If that handle is
lost, an administrator can recover without that token:
1. Stop every related import and drain its in-flight writes. Keep ordinary writers
stopped too; the owner protocol does not fence graph data requests.
2. Read the current instance, epoch and revision from the owners endpoint on the same Server.
3. Send `PUT` to that endpoint with
`{"action":"recover","instance":"<UUID>","epoch":0,"revision":0,"confirm":"I'm sure all imports have stopped"}`.
Recovery requires the authenticated global administrator role even when graph-space
permission checks are disabled; a proxied graph also enforces administrator permission.
With Server authentication disabled, the existing trusted anonymous administrator semantics apply.
Recovery also requires the exact current instance/epoch/revision and explicit confirmation. It clears owners, advances the epoch and returns NONE;
late acquire/release requests from the retired epoch cannot register an owner again.
The receipt contains the new epoch and `state: recovered`, without tokens. A stale
recovery request returns a conflict and cannot clear an import started in a newer
epoch. Owner registration/release and every successful enum mode write advance the
revision, including a write of the current mode. Either operation after inspection
therefore rejects the stale recovery request even within the same epoch. After an uncertain result, inspect current state rather than blindly
retrying with a newly fetched epoch/revision. This operation does not stop or drain imports itself.
The same recovery procedure can reset manual RESTORING or MERGING after that work
has stopped and drained; restore completion keeps its historical RESTORING mode.
Before ordinary import on an owner-capable Server, explicitly recover or set NONE
only after inspecting the completed work and confirming there are no active imports.
Expected loading-owner and manual-mode conflicts return HTTP 409; invalid requests
return 400 and unauthorized recovery is rejected by the authentication layer.
Loaders retain historical ordinary-import and restore behavior with Servers that do
not provide this API. Concurrent owner protection applies only when this API is
supported; RESTORING keeps its existing enum mode path.
## Graph-space storage reporting and removal
PD refreshes graph-space usage from Store heartbeat statistics, counting the leader
copy of each partition. Usage is approximate and advances on the quota check cycle;
it is not a synchronous count of every write. Changing a storage limit preserves
the latest usage, and a space with no remaining graphs reports zero on the next cycle.
PD stores limits in GB and usage in KB. Usage refreshes update only usage and preserve
concurrent limit changes across the REST, gRPC and quota-check service instances.
Server graph-space responses retain the integer GB `storage_used` field and add
`storage_used_kb` for consumers that need to display usage below one GB. The
`storage_percent` ratio is calculated from that precise usage, without rounding it
to whole percentage points. Older clients can continue reading `storage_used`.
Older Servers, including 1.7, preserve the additional `storage_used_kb` field as a
string when rewriting a space. This Server accepts both its integer and string
forms, allowing metadata to survive a rolling upgrade or rollback and re-upgrade.
Missing, negative or invalid precision values fall back to the original GB usage.
Older PD quota cycles stored some `storage_limit` values in KB. These untagged
records are not automatically converted: the same number could be a legitimate GB
quota. After **all PD replicas** are upgraded, an administrator can normalize each
affected space with one ordinary Server `PUT /graphspaces/{name}` update. First read
the current **Server** space configuration, then submit the unchanged quotas,
including its `storage_limit` in GB and existing compute limits, using the update
format below. Do not copy or divide the raw PD limit to infer the intended quota.
This rewrites the PD limit in GB while preserving usage; it is an explicit
configuration write, not a required automatic migration. Until then PD
`GET /graph-spaces` can report mixed limit units. Storage enforcement reads the
Server configuration and is unaffected by the stale PD reporting field.
Each Server refreshes its cached graph-space configuration on metadata ADD and
UPDATE notifications. This includes quota edits, whose REST path can emit ADD for
an existing space. Notifications carry the space name; the receiver reads current
metadata so delayed or repeated events do not restore earlier limits. Refresh follows
asynchronous metadata delivery, not a synchronous consistency guarantee. Upgrade all
Server replicas to receive these refreshes when requests are routed among them.
After a graph space is deleted, each Server evicts its cached space and service
entries on the metadata removal event. Peers do not repeat destructive backend
cleanup or delete shared metadata when handling that notification. A delayed event
for a name already recreated in metadata refreshes the space from metadata and evicts
its old service cache, so callers see the recreated configuration. Refreshes and
cache misses retry when a local space change completes during the read, including
graph and service counter updates. Older reads cannot replace a newer local quota
or counter, or return a discarded snapshot after local creation or deletion.
New graph spaces, including a newly bootstrapped `DEFAULT` space, use finite
resource defaults: 128 CPU cores, 128 GB memory, 128 compute CPU cores, 128 GB
compute memory, 10240 GB storage, 100 graphs and 100 roles. Existing graph-space
configurations are preserved on restart; this change does not migrate their quotas.
For `POST /graphspaces`, omitted compute limits are stored as `0`, meaning continuously
inherit the space's CPU and memory limits. Zero is neither unlimited nor a zero-sized
allocation. A request for 4 cores / 8 GB without compute fields therefore has effective
compute limits of 4 cores / 8 GB; after updating CPU/memory to 8 cores / 16 GB, compute
also becomes 8 cores / 16 GB. Omitting all resource fields gives effective limits of
128 cores / 128 GB for both. Explicit positive compute values remain independent.
`PUT /graphspaces/{name}` with `action=update` preserves omitted compute fields;
explicit `compute_cpu_limit: 0` or `compute_memory_limit: 0` restores inheritance for
that field. Responses retain the raw zero marker so editing clients can preserve it.
The bootstrapped `DEFAULT` space keeps explicit compute limits of 128 cores / 128 GB;
existing positive compute limits are not converted to inheritance.
Upgrade the Java client together with the Server before sending partial space
updates (paired [Toolchain change](https://github.com/apache/hugegraph-toolchain/pull/834)).
Older clients send zero for omitted compute fields; the new Server interprets an
explicit zero as restoring inheritance and cannot distinguish that from an omitted
SDK property. Until clients are upgraded, use read-modify-write and explicitly send
the current compute limits to preserve independent positive values.
When OLTP and OLAP share a Kubernetes namespace, their quotas are added together;
separate namespaces receive the respective resource limits.
The graph-count limit is enforced: after 100 graphs, creating another graph fails with
`reach graph limit`. Storage enforcement reads Server configuration under
`HUGEGRAPH/<pd.server-cluster>/GRAPHSPACE/CONF/`. Set `pd.server-cluster` in every PD
node's `conf/application.yml` to the Server `cluster` in `rest-server.properties`.
The PD setting defaults to `hg` for compatibility; the Server default is `hg-test`,
so deployments using that default must set `pd.server-cluster: hg-test`. This is a
metadata namespace, not the numeric `pd.cluster_id`. Changing it does not migrate
existing metadata or modify the Server configuration. When usage exceeds the
configured storage limit, PD marks the space's graphs
`ReadOnly`; the next check restores `ReadWrite` once usage is within the limit.
CPU and memory limits configure Kubernetes namespace quotas when `server.use_k8s=true`;
they do not impose process limits in other deployments.
An administrator can raise limits with `PUT /graphspaces/DEFAULT`, for example:
```json
{"action":"update","update":{"name":"DEFAULT","max_graph_number":200,"max_role_number":100,
"cpu_limit":128,"memory_limit":128,"storage_limit":20480}}
```
Read the current space first and preserve the fields that are not being changed. The
example raises graph count to 200 and storage to 20480 GB. This changes configuration;
it does not remove existing graphs or migrate other spaces.