monachitnis commented on issue #10474:
URL: https://github.com/apache/gravitino/issues/10474#issuecomment-4472782955

   
   ## A DB-Native Approach: Three Targeted Fixes + Phased Topology for HA 
cluster
   
   The prior proposals treat TreeLock replacement as the solution. Different 
position: **TreeLock is correct for intra-node concurrency — the HA correctness 
gaps are three specific DB-layer omissions.**
   
   ```
   TODAY (single-node correct; HA broken)
     Node A                         Node B
     REST → TreeLock → MetaService  REST → TreeLock → MetaService
              (in-memory)                    (in-memory)
                     ↓                              ↓
                 Same DB ←──────── no cross-node visibility ────────┘
   
   PROPOSED (DB layer is the correctness authority)
     Node A                              Node B
     REST → TreeLock (unchanged)         REST → TreeLock (unchanged)
                 ↓                                   ↓
            MetaService                         MetaService
                 ↓                                   ↓
            BEGIN TXN ◄── SELECT parent FOR UPDATE ──► BEGIN TXN
                 ↓         (held by DB conn to COMMIT)      ↓
            INSERT child ──────── one wins, one blocks ── INSERT child
                 ↓
         entity_change_log ← written on every mutation
                 ↓                    ↓
             Node A poller        Node B poller   (independent cursors)
                 ↓                    ↓
         GravitinoCache.invalidate()   GravitinoCache.invalidate()
   
     etcd: leader election only — zero overhead per write
       lease → CreateRevision epoch → stored in DB → rejects stale-epoch writes
   ```
   
   ---
   
   ## Confirmed Race Conditions
   
   | | Race | Scenario | Root cause |
   |---|---|---|---|
   | Race 1 | TOCTOU rename | Node A and Node B both check a name doesn't 
exist, both proceed — one rename silently wins or both succeed with duplicate 
names | No cross-node lock on parent namespace during check-then-write |
   | Race 3 | Orphaned child | `DROP SCHEMA` on Node A commits; Node B's 
`CREATE TABLE` under that schema also commits — `table_meta` row exists with a 
soft-deleted `schema_id` | No FK between `table_meta` and `schema_meta`; 
soft-delete + manual ID joins means the INSERT sees no constraint violation |
   | ABA | Lost update | Metalake at V1 → updated to V2 → reverted to 
V1-equivalent content; stale writer at V1 passes the `UPDATE WHERE old_value` 
OCC guard undetected | `POConverters` copies `lastVersion` to `nextVersion` 
without incrementing — version never advances, ABA cycle invisible to OCC |
   
   ---
   
   ## Three Targeted Fixes
   
   | Fix | Race closed | Location |
   |-----|-------------|----------|
   | `SELECT ... FOR UPDATE` on parent row, within `SessionUtils.doWithCommit` 
| Race 1 (TOCTOU rename), Race 3 (orphaned child on concurrent drop+create) | 
All `*MetaService` structural ops |
   | `nextVersion = lastVersion + 1` | ABA: `nextVersion = lastVersion` means 
V1→V2→V1 passes OCC guard undetected | 
`POConverters.updateMetalakePOWithVersion` |
   | Wire `EntityChangeLogPoller` in `GravitinoEnv` | Cross-node cache 
staleness (write side in PR #10914; consumer not yet wired) | New 
`EntityChangeLogPoller`, `GravitinoEnv` |
   
   **TreeLock unchanged.** DB row lock handles cross-node serialization; 
TreeLock handles intra-node. Complementary, not redundant.
   
   ---
   
   ## Why `SELECT FOR UPDATE`, Not etcd Per-Write
   
   The DB row lock is held by the DB connection through `COMMIT` — immune to 
the failure modes that motivate external coordinators:
   
   - **k8s CPU throttle**: throttled JVM thread cannot release a DB connection 
lock; lease keepalives can miss their window while the process is alive
   - **GPU→CPU PCIe offload stalls**: AI inference deployments offload host 
memory over PCIe, stalling CPU threads; DB lock unaffected by JVM scheduling
   - **etcd per-write cost**: 4 hierarchy levels × 2–5ms RTT = 8–20ms overhead 
per write, always paid, even uncontested — before any DB work
   
   etcd per-write locking is only correct if a fencing token propagates to 
every DB write (`UPDATE WHERE fencing_token = N`). At that point etcd is 
redundant with `SELECT FOR UPDATE`.
   
   ---
   
   ## Agentic Workload Fit: OCC Preserves Parallelism; Distributed Locks 
Destroy It
   
   Gravitino increasingly serves multi-agent AI systems: LLM agents issuing 
concurrent DDL via tool calls, training pipelines registering model versions in 
parallel via `ModelMetaService`, inference servers reading schema metadata at 
serving time.
   
   **OCC is optimistic — all agents run in parallel, conflicts detected only at 
commit:**
   ```
   Agent 1 ──read──write──commit✓
   Agent 2 ──read──write──commit✓
   Agent 3 ──read──write──commit✓  ← all in-flight simultaneously
   Agent 4 ──read──write──conflict→retry→commit✓
   ```
   Only the conflicting agent serializes, and only for one retry. Uncontested 
agents are never delayed. LLM agent tool-call loops have retry-with-backoff 
built in by design — a 409 Conflict is just another retryable response, 
requiring no special handling.
   
   **Distributed locks are pessimistic — agents queue at lock acquisition 
before any work begins:**
   ```
   Agent 1 ──acquire lock──write──release  ← others blocked here
   Agent 2           ──────────────────────acquire lock──write──release
   Agent 3                                            ──────────────────acquire 
lock──...
   ```
   Even agents operating on completely unrelated entities wait in the same 
queue. A 10-agent model registration storm: ~50ms parallel under OCC vs. 10 × 
(lock RTT + write) ≈ 300ms+ serialized under etcd per-write — before contention 
is even factored in. Lock-free reads are essential for inference-time 
`loadTable` latency; acquiring an etcd READ lock per metadata lookup is a 
non-starter.
   
   The 2026 field consensus (Iceberg REST v1.6+, Delta Lake Unity Catalog, 
Kafka KRaft) converges here: push serialization into the storage layer via OCC; 
reserve external coordinators for coarse-grained topology only. Agentic 
workloads make this constraint harder, not softer.
   
   ---
   
   ## Tradeoffs
   
   | Tradeoff | This proposal's choice | Rejected alternative |
   |---|---|---|
   | Serialization mechanism | DB row lock (no new dep, ~5ms DDL txn) | etcd/ZK 
per-write (8–20ms, external cluster) |
   | Cache consistency | Eventual (poll interval, ~500ms), DB always 
authoritative | Sync cross-node ack (adds coordination channel, latency on 
every write) |
   | HA topology | Active-standby first (Phase C) | Active-active now (Phase 
A's `FOR UPDATE` is already the full story; test gap not justified until write 
throughput measured) |
   | Race 3 closure | `SELECT FOR UPDATE` on parent (no schema change) | FK `ON 
DELETE CASCADE` (correct long-term; requires migration on MySQL/PG/H2 + 
soft-delete semantics change) |
   
   ---
   
   ## Evolution Path
   
   ```
   Phase A  DB correctness, zero new deps
     ├─ SELECT FOR UPDATE on parent rows (all MetaServices)
     └─ Version increment fix in POConverters
     ✓ Race 1, Race 3, ABA closed  ✗ cache staleness  ✗ auto-failover
   
   Phase B  Cross-node cache coherence
     └─ EntityChangeLogPoller wired, gravitino.cache.entityChangeLog.enabled
     ✓ + bounded-staleness invalidation  ✗ auto-failover
   
   Phase C  Active-standby HA  [first external dep]
     └─ etcd leader election, controller_epoch in DB, standby serves reads
     ✓ + automatic failover within lease TTL  new dep: etcd (3-node)
   
   Phase D  Active-active  [no new locking work — Phase A is complete]
     └─ client retry semantics, per-catalog conflict testing, throughput 
benchmarks
     when: single-node write ceiling measured, not speculative
   
   Phase E  Zero-dependency HA
     └─ embedded Raft (Apache Ratis, already in ASF/Ozone ecosystem)
     when: air-gapped / edge deployments require it
   ```
   
   Each phase is independently shippable. No phase invalidates the previous.
   
   ---
   
   ## Why Not the Other Proposals
   
   | Proposal | Gap |
   |---|---|
   | Option A (etcd/ZK per-write) | Conflates topology management with 
per-write locking; 8–20ms overhead per DDL; only correct if fencing token 
propagated to every DB write — at which point etcd is redundant with `FOR 
UPDATE` |
   | Option B (delete TreeLock + OCC) | Correct direction; Race 3 unaddressed 
without parent `FOR UPDATE`; removing TreeLock costs correct intra-node 
serialization for zero gain |
   | Option C (partition + watchdog + trigger) | `tree_version` alone is 
insufficient fencing token — needs `(tree_version, owner_node)` compound; 
`System.exit()` watchdog has high blast radius on transient DB hiccup; `FOR 
UPDATE` provides same storage-layer fencing without trigger, watchdog, or 
rebalancing machinery |
   | Counter-proposal (new lease table) | `FOR UPDATE` on the existing parent 
row is strictly simpler — no schema migration, no lease renewal thread, and 
simultaneously closes Race 3 which the lease table does not |
   | PR #11020 (LockBackend SPI) | Correct abstraction for Phase C/E; deferred 
Phase B (OCC in RelationalEntityStore) is what this proposal delivers directly |
   
   ---
   
   ## Agentic Contribution
   
   This proposal was developed with an agentic coding workflow. A 
**`gravitino-locking-reviewer` Claude Code skill** is included on the fork 
branch:
   
   ```
   .claude/skills/gravitino-locking-reviewer/SKILL.md
   ```
   
   It encodes Gravitino's distributed state discipline as an invocable code 
review checklist covering TreeLock patterns, `SELECT FOR UPDATE` placement, OCC 
version correctness, `EntityChangeLog` invalidation scope, and leader election 
epoch fencing. Contributors working in this area can invoke it to verify 
correctness before review, not just read the docs passively.
   
   ---
   
   ## Validation Pipeline: CI → Playground Staging → Managed Rollout
   
   For revenue-sensitive deployments, correctness fixes must ship with staged 
confidence — not just passing CI.
   
   **Tier 1 — Hermetic CI** (`HaBaseIT` + `MiniGravitino` + TestContainers):
   Two in-process Gravitino nodes, shared PostgreSQL, 6 correctness scenarios. 
Runs on every PR touching `MetaService`, `POConverters`, `EntityChangeLog`, or 
locking code. Fast, no external deps.
   
   **Tier 2 — Playground staging** (full data plane fidelity):
   [gravitino-playground](https://github.com/apache/gravitino-playground) ships 
Hive, MySQL, PostgreSQL, Spark, Trino, Prometheus, and Grafana as a single 
`docker compose up`. A thin overlay converts it into a 2-node HA cluster:
   
   ```bash
   docker compose \
     -f gravitino-playground/docker-compose.yaml \
     -f docs/treelock-ha/docker/docker-compose-ha-overlay.yml up
   ```
   
   The overlay (`docker-compose-ha-overlay.yml`) adds only `gravitino-node-b` 
(same image, shared entity store) and a single-node `etcd`. All catalog 
backends — Hive metastore, Iceberg REST, MySQL, PostgreSQL — are shared between 
both nodes. This is the exact topology that surfaces Race 1 and Race 3. No new 
infra to stand up.
   
   **Interactive beta validation via Jupyter**: Playground's existing 
`gravitino-spark-trino-example.ipynb` is extended with HA race scenario cells — 
concurrent Python REST calls racing DDL across both nodes. 
`gravitino_llamaIndex_demo.ipynb` (already in playground) directly exercises 
the AI agent model-registration storm (Scenario 5). OSS contributors and beta 
customers run these before a release cut.
   
   **Observability without new infra**: Playground's Prometheus + Grafana are 
already wired. Three HA-specific metrics to add as a dashboard:
   
   | Metric | Signal |
   |---|---|
   | OCC retry rate | Spikes after Phase A ships → confirms `FOR UPDATE` is 
serializing correctly |
   | EntityChangeLog poll lag (`maxId - lastConsumedId`) | Confirms Phase B 
consumer is keeping up; alerts if falling behind |
   | Structural op P99 latency | Baseline before / after `FOR UPDATE` — 
quantifies the ~5ms DDL overhead claimed in the tradeoff table |
   
   **Staged rollout for managed deployments**:
   
   ```
   Phase A  Drop-in correctness fix — zero topology change, safe for all 
existing deployments
   Phase B  Opt-in: gravitino.cache.entityChangeLog.enabled=false default
            → dark-launch in staging, flip per tenant when validated
   Phase C  New dep (etcd): validate in playground overlay before enabling in 
production cluster
            Rollback path: disable etcd → falls back to Phase B behavior, 
single active writer
            Feature flag per deployment; no flag removal until Phase C is 
stable across customer base
   ```
   
   Phase A ships with no rollout risk. Phases B and C are gated behind feature 
flags with explicit rollback paths — the pattern appropriate for production 
systems with SLA obligations.
   
   ---
   
   ## Open Questions
   
   1. **DB isolation level**: `FOR UPDATE` on `READ COMMITTED` (PG default) and 
`REPEATABLE READ` (MySQL/InnoDB default) both read current committed rows for 
locked queries — should be documented explicitly per supported DB.
   2. **Poll interval SLA**: 500ms cross-node cache staleness acceptable for 
DDL workloads; inference-time reads may need tuning — make configurable.
   3. **POConverters audit scope**: `updateMetalakePOWithVersion` confirmed. 
Are there other `update*POWithVersion` methods where version intentionally does 
not increment?
   4. **Active-active threshold**: Phase D should be data-driven. What write 
throughput triggers the single-node ceiling?
   5. **Phase E dependency**: Is zero-external-dependency HA a roadmap 
requirement? Apache Ratis is available in-ecosystem if yes.
   
   ---
   
   ## Full Spec on Fork
   
   
[`monachitnis/gravitino:docs/locking-deep-dive-and-ha-design`](https://github.com/monachitnis/gravitino/tree/docs/locking-deep-dive-and-ha-design)
   
   | Document | Contents |
   |---|---|
   | `docs/treelock-ha/2026-05-17-gravitino-ha-locking-proposal-final.md` | 
Full design: confirmed bugs, prior proposal verdicts, 2026 ecosystem lens, 
architecture diagrams, migration phases |
   | `docs/treelock-ha/2026-05-17-gravitino-ha-implementation-spec.md` | 
Implementation contracts: mapper interfaces, version increment audit, 
`EntityChangeLogPoller` SPI, `GravitinoLeaderElection` etcd SPI |
   | `docs/treelock-ha/2026-05-17-gravitino-ha-test-harness.md` | Integration 
test spec: 6 correctness scenarios across Hive/Iceberg/Kafka/JDBC/Paimon, 
`HaBaseIT` base class |
   | `docs/treelock-ha/docker/docker-compose-ha-test.yml` | Standalone HA 
compose: 2 Gravitino nodes, shared PostgreSQL, etcd, all catalog backends |
   | `docs/treelock-ha/docker/docker-compose-ha-overlay.yml` | Playground 
overlay: adds `gravitino-node-b` + `etcd` on top of existing playground stack |
   | `.claude/skills/gravitino-locking-reviewer/SKILL.md` | Claude Code skill: 
invocable distributed state review checklist |
   


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

To unsubscribe, e-mail: [email protected]

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

Reply via email to