Configuring Mixed Index Fallback Chains
This guide walks through wiring a JanusGraph mixed-index fallback chain — a primary search backend, a secondary backend, and a bounded composite-index path — so that a degraded Elasticsearch/OpenSearch cluster degrades query latency instead of hard-failing traversals and stalling downstream pipelines. It is the hands-on procedure behind the routing theory in the parent Mixed Index Routing guide: production deployments routinely hit transient index degradation from network partitions, JVM garbage-collection pauses, or shard rebalancing, and a query that raises IllegalArgumentException or times out during that window violates SLOs and triggers backpressure. Configured correctly, the chain shifts traffic to a healthy backend or a composite-index fallback and reverts automatically once the primary recovers.
The state machine below captures the circuit-breaker lifecycle the fallback router moves through.
JanusGraph does not natively auto-chain mixed indexes — it dispatches a routed predicate to a single IndexProvider and fails the query if that provider is unreachable. The chain therefore lives at your query proxy or pipeline layer, not inside the graph engine.
Prerequisites
Confirm every item before you start — a fallback chain built on an unverified composite index fails open into full scans, which is worse than the outage it was meant to survive.
- JanusGraph 0.6+ running against a Cassandra, ScyllaDB, or HBase storage backend, with
gremlin-python3.5+ on the client. - Two reachable search backends — a primary and a secondary Elasticsearch 7.x/8.x or OpenSearch 1.x/2.x cluster. If you run mixed engines, reconcile analyzer and mapping differences first per Elasticsearch Integration and OpenSearch Sync Patterns, because a predicate that routes cleanly on one engine can match nothing on the other.
- A composite index that fully covers the fallback predicates. Every property key the fallback path queries must be bound to an
ENABLEDcomposite index. Verify eligibility against Property Indexing Rules — only equality and range predicates on covered keys survive the degraded path. - Management-API access (
graph.openManagement()) and permission to editjanusgraph.propertiesand restart the server pool. - A settled consistency target. Decide where the fallback path sits on the eventual versus strong consistency spectrum before you cut traffic over — serving from a lagging secondary is a deliberate consistency trade, not an accident.
Step-by-Step Procedure
Step 1 — Declare primary and fallback indexes
Define both mixed indexes explicitly in janusgraph.properties, with strict timeout boundaries so a slow backend cannot exhaust the client thread pool during a state transition.
# Primary Mixed Index Configuration
index.primary.backend=elasticsearch
index.primary.hostname=es-primary-01,es-primary-02,es-primary-03
index.primary.elasticsearch.client-only=true
index.primary.elasticsearch.http.connection-timeout=2000
index.primary.elasticsearch.http.socket-timeout=5000
# Fallback Mixed Index Configuration
index.fallback.backend=elasticsearch
index.fallback.hostname=os-fallback-01,os-fallback-02
index.fallback.elasticsearch.client-only=true
index.fallback.elasticsearch.http.connection-timeout=1500
index.fallback.elasticsearch.http.socket-timeout=3000
# Query Safety Boundaries
# query.force-index=true forces all traversals through an index; set false
# only after confirming composite index coverage for the fallback path.
query.force-index=true
query.fast-property=true
storage.read-time=10000
storage.write-time=15000
Keep query.force-index=true as the steady-state default. It guarantees that no traversal silently degrades into a full scan while both search backends are healthy; the router toggles it off deliberately only when it drops to the composite path.
Step 2 — Build the circuit-breaker router
Implement the chain in your ingestion or query pipeline as a stateful circuit breaker. This router polls index health on a cooldown, toggles query.force-index at runtime, and executes each attempt against a freshly built traversal.
import time
import requests
from typing import List, Any, Callable
from gremlin_python.process.graph_traversal import __
from gremlin_python.process.traversal import Traversal
class JanusFallbackRouter:
def __init__(self, primary_es_url: str, graph: Any):
self.primary_es_url = primary_es_url
self.graph = graph
self.state = "primary"
self.last_health_check = 0.0
self.cooldown = 5.0 # seconds
def check_index_health(self) -> bool:
if time.time() - self.last_health_check < self.cooldown:
return self.state == "primary"
try:
# Validate Elasticsearch/OpenSearch cluster health
resp = requests.get(f"{self.primary_es_url}/_cluster/health", timeout=2)
healthy = resp.json().get("status") in ("green", "yellow")
self.last_health_check = time.time()
return healthy
except requests.RequestException:
self.last_health_check = time.time()
return False
def toggle_force_index(self, force: bool) -> None:
# Runtime configuration update via JanusGraph ManagementSystem.
# Note: not all runtime properties can be changed this way; verify
# against JanusGraph's hot-reload documentation before use.
mgmt = self.graph.openManagement()
mgmt.set("query.force-index", str(force).lower())
mgmt.commit()
def execute_query(self, build_traversal: Callable[[], Traversal], timeout_ms: int = 5000) -> List[Any]:
# Accept a factory: a traversal cannot be re-iterated once executed, so
# each attempt must build a fresh one. Per-query timeout is set with the
# 'evaluationTimeout' option.
if self.state == "primary" and not self.check_index_health():
self.state = "fallback"
self.toggle_force_index(False)
if self.state == "fallback":
try:
# Execute against composite/bounded path
return build_traversal().with_("evaluationTimeout", timeout_ms).toList()
except Exception as e:
raise RuntimeError("Fallback traversal failed. Verify composite index coverage.") from e
try:
return build_traversal().with_("evaluationTimeout", timeout_ms).toList()
except Exception:
self.state = "fallback"
self.toggle_force_index(False)
return build_traversal().with_("evaluationTimeout", timeout_ms).toList()
Step 3 — Define the routing hierarchy
Wire the router to walk the chain in strict order so every degradation has a bounded next hop:
- Primary mixed index — the high-throughput search cluster handling all standard predicate queries.
- Secondary mixed index — a geographically isolated or lower-tier cluster that absorbs overflow when primary latency crosses the SLO threshold.
- Composite index + bounded storage scan — the last resort, restricted to exact-match or range predicates the composite index covers, entered only after
query.force-indexis toggled tofalse.
The router must evaluate index health, replication lag, and query complexity before dispatch. Full-text, geoWithin, and open-ended range predicates cannot execute on the composite path — the fallback layer must reject or reshape them rather than let them fall through to an unbounded scan.
Step 4 — Apply and reload
Push the property changes and confirm both index registrations are live:
# Register/confirm index definitions from the Gremlin console
gremlin> mgmt = graph.openManagement()
gremlin> mgmt.printIndexes()
gremlin> mgmt.commit()
Verification Commands
Validate the chain in a staging environment before promoting it — never trust a fallback path you have not tripped on purpose.
-
Baseline health. Confirm the primary reports a healthy status and both indexes are registered.
curl -s "http://es-primary-01:9200/_cluster/health?pretty" | grep '"status"' # expect "green" (or "yellow" on a single-replica staging cluster)In the Gremlin console,
mgmt.printIndexes()must list both the primary mixed index and the composite fallback index at statusENABLED. -
Simulate primary degradation. Throttle or blackhole traffic to the primary hosts with
tc qdisc(Linux Traffic Control), or inject a 503 at the API gateway. -
Observe the routing shift. Watch the pipeline logs for the
primary → fallbacktransition and confirmquery.force-indexflips tofalsewithin one cooldown window (2 seconds by default). -
Confirm composite coverage. Run a bounded, indexed predicate and assert it returns without a full-scan warning:
results = router.execute_query( lambda: g.V().has("user_id", "u-4417"), timeout_ms=3000 ) assert results, "fallback returned empty — composite index does not cover user_id" -
Recovery verification. Restore primary connectivity. After the cooldown expires the router must revert to
state == "primary"and togglequery.force-indexback totrue. Confirm the next query dispatches to the primary index (query latency drops back to the sub-millisecond baseline).
Fallback Procedures
Each step has a distinct failure mode. Recover with the matching rollback rather than retrying blindly.
-
Step 1 fails — server rejects the properties or a backend is unreachable at boot. Revert to a single-index
janusgraph.properties, restart the pool, and restore service on the primary alone. Re-add the fallback block only after the secondary cluster answers/_cluster/health. Rollback:git checkout janusgraph.properties && systemctl restart janusgraph. -
Step 2/3 fails — the router flips to fallback but composite queries raise
IllegalArgumentException. This meansquery.force-indexwas toggled while the fallback predicates are not composite-covered. Immediately force the router back toprimarystate, setquery.force-index=falseglobally as a stopgap to accept bounded scans, and cap blast radius withquery.page-sizeto prevent OOM. Then bind the missing composite index before re-arming the chain. -
Step 4 fails —
printIndexes()shows the fallback index stuck belowENABLED. Do not route traffic to it. Complete the index lifecycle (REGISTER_INDEX→ENABLE_INDEX, reindex if the key predated the index) per the JanusGraph index-lifecycle documentation and re-run verification. -
Fallback returns empty result sets despite valid data. The index trails the storage commit or a mapping drifted. Align the backend commit/refresh interval with the fallback timeout window, confirm the composite index includes every predicate property, and reconcile drift via the sync procedures in OpenSearch Sync Patterns.
-
Router flaps between primary and fallback. Health checks are firing inside GC pauses. Widen
cooldownand tune the poll interval against measured GC pause durations; see the Elasticsearch Cluster Health API documentation for the fields to threshold on. Full-stop rollback for any unrecoverable state: restorequery.force-index=true, pin the router toprimary, and take the degraded backend out of rotation until it is repaired.
Frequently Asked Questions
Why not let JanusGraph fail over on its own?
It has no chaining primitive — a routed predicate targets one IndexProvider, and an unreachable provider raises an exception. The chain must live in the proxy or pipeline layer.
Is it safe to leave query.force-index=false permanently?
No. With it off, any unindexed predicate silently becomes a full scan. Keep it true in steady state and let the router toggle it only for the duration of a degradation.
What predicates can the composite fallback actually serve?
Only equality and bounded range predicates on keys with an ENABLED composite index. Full-text, geo, and open-ended range predicates have no composite equivalent and must be rejected on the fallback path.