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

   # HA-Safe Metadata Concurrency for Gravitino
   
   ## 1. Overview (the whole idea)
   
   **Problem.** `TreeLock` only orders threads **inside one JVM**. In **HA**, 
many JVMs share one metadata database; each has its **own** lock tree, so two 
servers can still corrupt hierarchy state.
   
   **Approach — dual layer (not two TreeLocks: one DB step, then one in-memory 
lock).**
   
   
   | Layer                  | Role                                              
                                                                                
                                                    |
   | ---------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
   | **Cross-node (new)**   | Use the **same JDBC metadata database** to record 
who may run **structural** work on which part of the tree (lease row + prefix 
rules; optional PostgreSQL advisory lock where safe). |
   | **In-node (existing)** | Keep `**TreeLock` / `LockManager` / 
`TreeLockUtils`** so threads on **this** JVM stay ordered **the same way as 
today**.                                                              |
   
   
   We **do not** default to ZooKeeper / Redis / etcd, and we **do not** drop 
`TreeLock` in favor of DB-only `SERIALIZABLE` everywhere (see §7 for other 
options).
   
   **Structural only.** The new DB claim applies to work that changes **shape 
or membership** of the tree (rename/move under a parent, cascade drops, creates 
that add children, …). Simple property updates and most reads use lighter rules.
   
   **One structural write, in order**
   
   1. **Claim in the shared DB** so other nodes see it. Claim key = canonical 
`**scope_path`** (same *idea* as today’s `TreeLock` leaf path — §5).
   2. **Take exactly one `TreeLock*`*, same `NameIdentifier` + `LockType` the 
dispatcher already picks (not a second policy).
   3. **Run work:** **connector** (Hive / Iceberg / JDBC / …) + 
`**EntityStore`** (Gravitino’s own metadata rows) — see §3.
   4. **Release `TreeLock`, then release the DB claim.**
   
   **Why DB before `TreeLock`?** Avoid holding the in-memory tree lock while 
waiting on JDBC and blocking unrelated local threads.
   
   **Reads.** No distributed lock on every read. Use **one snapshot 
transaction** for multi-step **relational** loads so parent/child rows cannot 
“tear.” Engine + store stays **best-effort** (not one atomic transaction).
   
   **Rollout.** Feature flag **off** = today’s behavior. **On** for HA = **all 
nodes** together; half-on / half-off brings races back.
   
   ---
   
   ## 2. Background: today’s code and what breaks
   
   **Code-anchored** = you can check this in the repo: 
`TreeLockUtils.doWithTreeLock` → `GravitinoEnv.lockManager()` 
(`TreeLockUtils.java`, `GravitinoEnv.java`). `TreeLock#lock` reads ancestors, 
read/write on the leaf (`TreeLock.java`). Dispatchers and managers listed in 
#10474 call this; e.g. rename shifts the leaf in 
`TableOperationDispatcher#alterTable` when `TableChange.RenameTable` is present.
   
   **HA gap.** Two processes each pass `TreeLock` locally but both touch 
`**EntityStore`** → cross-node races.
   
   **Failures we close:** (a) conflicting structural writes, (b) torn 
multi-query relational reads, (c) **zombie commits** after lease steal — fixed 
with **fencing** (§4).
   
   **Design rules (short):** distributed scope must **match** today’s 
`TreeLock` leaf choices; **DB claim before `TreeLock`**; prefer **one fence 
anchor** (lock row or parent), not a fence column on every `*_meta` row; 
document engine-vs-store order and failures; **heartbeats + reclaimer** for 
leases; warn on mixed flag rollout.
   
   ---
   
   ## 3. Flow, diagram
   
   Use a thin facade (e.g. `MetadataConcurrencyCoordinator`) at **structural** 
boundaries.
   
   ```mermaid
   flowchart TB
     subgraph node [Gravitino process]
       API[REST / gRPC handler]
       MCC[MetadataConcurrencyCoordinator]
       TL[TreeLockUtils / LockManager]
       EC[EntityStore + mappers]
       CC[Catalog connector]
     end
     DB[(Relational metadata DB)]
   
     API --> MCC
     MCC -->|"Step 1: DB scope claim"| DB
     MCC -->|"Step 2: one TreeLock, same rules as today"| TL
     MCC --> EC
     MCC --> CC
   ```
   
   
   
   **Steps (same as §1):** (0) Request hits **REST/gRPC** — entry only, not 
special to locking. (1) DB claim. (2) One `TreeLock`. (3) Connector + 
`EntityStore`. (4) Release `TreeLock`, then DB claim.
   
   
   |             | **Connector**                                                
                                           | `**EntityStore`**                  
                    |
   | ----------- | 
-------------------------------------------------------------------------------------------------------
 | ------------------------------------------------------ |
   | **What**    | Catalog plugin: real Hive / Iceberg / JDBC / … 
(`doWithCatalog` → `doWithTableOps` → `TableCatalog` …). | Gravitino JDBC 
metadata (`store.*`, `TableEntity`, …). |
   | **Example** | Rename table in metastore.                                   
                                           | Update stored 
name/namespace/audit.                    |
   
   
   Connector and store use **different connections/protocols** — not one big 
transaction. Pick engine-first vs store-first **per operation**; document 
partial failure (#10474 honesty: we fully serialize **relational** cross-node 
races; **end-to-end** with every engine needs connector-level discipline).
   
   ---
   
   ## 4. Fencing
   
   Put a monotonic `**fence`** on the **lease row** (simplest: no DDL on every 
entity table). After lease expiry, a stealer bumps `fence`; late commits from 
the old holder must **fail** if they no longer match. Prefer **one** DB 
transaction for the structural `EntityStore` updates guarded by that fence.
   
   ---
   
   ## 5. DB coordination: key, DDL, behavior
   
   `**scope_path` (PK)** — canonical path string aligned with the `TreeLock` 
leaf for that op (e.g. `/mlake/cat/db` for schema scope, `/mlake/cat` for 
catalog). **Prefix = ancestor** → conflict if any **active** row is equal, 
ancestor, or descendant path. `**holder_id`** — which JVM owns the row 
(renew/release), not the tree key.
   
   **Reference DDL (PostgreSQL-style; tune per dialect)**
   
   ```sql
   CREATE TABLE gravitino_metadata_scope_lock (
     scope_path         VARCHAR(2048) NOT NULL,
     holder_id          VARCHAR(256)  NOT NULL,
     fence              BIGINT        NOT NULL DEFAULT 0,
     lease_expires_at   BIGINT        NOT NULL,
     acquired_at        BIGINT        NOT NULL,
     updated_at         BIGINT        NOT NULL,
     CONSTRAINT pk_metadata_scope_lock PRIMARY KEY (scope_path)
   );
   CREATE INDEX idx_metadata_scope_lock_expires ON 
gravitino_metadata_scope_lock (lease_expires_at);
   CREATE INDEX idx_metadata_scope_lock_path_prefix ON 
gravitino_metadata_scope_lock (scope_path varchar_pattern_ops);
   ```
   
   **Acquire (one txn):** scan for conflicting **unexpired** rows → `INSERT` or 
steal after expiry → bump `fence` on steal. **MySQL/H2:** same idea; watch 
index length on long paths.
   
   **PostgreSQL optional fast path:** `pg_try_advisory_xact_lock` in the 
**same** txn as structural writes — auto-released at commit; use only when 
**one coarse key** matches the real scope, else use the lease table (advisory 
locks don’t encode hierarchy by themselves).
   
   **Stale leases:** holder heartbeats + background **reclaimer** + metrics.
   
   ---
   
   ## 6. Rollout, migration, tests
   
   
   | Config                                        | Meaning                    
                                                  |
   | --------------------------------------------- | 
---------------------------------------------------------------------------- |
   | `gravitino.metadata.distributed-lock.enabled` | Master switch; ship 
default **off**.                                         |
   | `gravitino.metadata.distributed-lock.backend` | `LEASE_TABLE` | 
`PG_ADVISORY` | `AUTO`.                                      |
   | Metrics                                       | e.g. acquire latency, 
contention, fence rejects, steals, snapshot-read txns. |
   
   
   **Rollback:** flag off → **TreeLock-only** (no redeploy). **Ops:** don’t run 
HA mixed on/off long-term.
   
   **Phases:** inventory `TreeLockUtils` call sites → ship DDL + coordinator 
(no-op when flag off) → wrap structural paths + two-node tests → snapshot reads 
→ optional PG advisory.
   
   **Tests:** overlapping claims; steal + fence; DB claim before `TreeLock`; 
torn-read under RR; flag-off compatibility.
   
   ---
   
   ## 7. Other options (why we didn’t default there)
   
   
   | Option                                            | Drawback for default   
                                                                                
                                                                                
                                                                                
                                                                                
                                                                     |
   | ------------------------------------------------- | 
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
   | ZooKeeper / etcd / Redis                          | Operators must run and 
secure **another** HA system beside Gravitino and the metadata DB; its outage 
or partition blocks or confuses **all** lock holders, not just one feature. 
Every incident is now “is it Gravitino, the DB, **or** the coord?” — wider 
blast radius and longer MTTR. You still need **fencing / lease** discipline in 
the app; the coord does not remove careful storage writes by itself.            
 |
   | DB-only `SERIALIZABLE` (or equivalent) everywhere | Most real flows touch 
**both** the external catalog and JDBC; they **cannot** be one database 
transaction, so you still need an explicit cross-node story for the non-DB 
half. Serialization conflicts mean **retries and tail latency** on hot parents 
(schemas, catalogs) where many ops overlap. “Subtree shape” invariants map 
awkwardly to row locks — easy to miss an edge case versus an explicit **path** 
lease.    |
   | Single active writer (leader)                     | Only the leader may 
accept writes; other nodes are **standby for metadata mutations**, so you pay 
HA complexity without **parallel write** throughput. Failover must guarantee 
**no split-brain** and clean handoff of in-flight work — harder to reason about 
than **peer nodes + shared lease rows**. Read scaling and regional routing get 
tangled (“writes to leader, reads where?”) unless you add more policy anyway. |
   
   
   **Why the proposed default instead:** reuse the **metadata DB** you already 
require, keep **`TreeLock`** for local threads, and add **only structural** 
cross-node claims — smaller new moving parts and a clear flag rollback.
   
   **Closing.** Keep **`TreeLock`** for local ordering; add a **small, 
store-backed scope lease + fencing** for structural HA; tighten **relational 
reads** with snapshot transactions; be explicit about **connector vs 
`EntityStore`**.


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