yurongfu opened a new pull request, #8791:
URL: https://github.com/apache/hbase/pull/8791

   JIRA: HBASE-30484
   # Description
   
   A table space quota with the `DISABLE` violation policy disables the table 
when
   its usage exceeds the limit. On the active Master, `QuotaObserverChore`
   periodically recomputes each table's quota snapshot and transitions the table
   between violation and observance.
   
   For example, a table over its limit reaches state similar to:
   
   ```text
   hbase:quota snapshot:       inViolation, policy=DISABLE
   chore in-memory snapshot:   inViolation, policy=DISABLE
   table state:                DISABLED
   ```
   
   When the operator raises the limit (or usage drops) so the table is no longer
   in violation, `QuotaObserverChore.updateTableQuota()` previously performed 
the
   transition in this order:
   
   ```java
   if (!currentSnapshot.equals(targetSnapshot)) {
     this.snapshotNotifier.transitionTable(table, targetSnapshot);
     // Update it in memory
     tableSnapshotStore.setCurrentState(table, targetSnapshot);
     ...
     if (!targetStatus.isInViolation()) {
       // In case of Disable SVP, we need to enable the table as it moves out 
of violation
       if (isDisableSpaceViolationPolicy(currPolicy, targetPolicy)) {
         QuotaUtil.enableTableIfNotEnabled(conn, table);
       }
       ...
   ```
   
   Both the persisted snapshot (`transitionTable()` writes to `hbase:quota`) and
   the in-memory snapshot (`setCurrentState()`) are advanced before the table is
   enabled. `QuotaUtil.enableTableIfNotEnabled()` calls `Admin.enableTable()`,
   which can fail before an enable procedure is submitted — for example, a
   failing `preEnableTable` coprocessor hook or a failed quota-table read in
   `HMaster.enableTable()`. The exception propagates to `chore()`, which logs:
   
   ```text
   Failed to process quota reports and update quota state. Will retry.
   ```
   
   Because both snapshots were already advanced, the failure leaves this state:
   
   ```text
   hbase:quota snapshot:       notInViolation (persisted)
   chore in-memory snapshot:   notInViolation
   table state:                DISABLED
   ```
   
   On the next chore run, `getTargetState()` computes the same non-violating
   snapshot, so `currentSnapshot.equals(targetSnapshot)` is true and
   `updateTableQuota()` takes the no-op branch ("remains in observance of
   quota"). The "Will retry" log message only means the chore runs again; the
   failed table operation is never retained or retried. The table stays
   `DISABLED` indefinitely, and client operations fail:
   
   ```text
   org.apache.hadoop.hbase.TableNotEnabledException: <table> is disabled
   ```
   
   The insufficient-region-reports path in `_chore()` has the same ordering: a
   table in "limbo" moving out of a `DISABLE` violation calls
   `setCurrentState()` before `enableTableIfNotEnabled()`, with the same
   consequence on failure. The disable direction (moving into violation) is
   symmetric: a failed `disableTableIfNotDisabled()` leaves the table enabled
   while both snapshots already record the violation.
   
   # Solution
   
   Keep the write to `hbase:quota` first — `HMaster.enableTable` consults the
   persisted snapshot, and `transitionTable()` is a plain idempotent `Put`, so
   re-running it on retry is safe. Move the in-memory `setCurrentState()` call
   after the table enable/disable operation, to the end of the transition 
branch:
   
   ```java
   if (!currentSnapshot.equals(targetSnapshot)) {
     this.snapshotNotifier.transitionTable(table, targetSnapshot);
     ...
     if (!targetStatus.isInViolation()) {
       if (isDisableSpaceViolationPolicy(currPolicy, targetPolicy)) {
         QuotaUtil.enableTableIfNotEnabled(conn, table);
       }
       ...
     } else {
       ...
     }
     // Update it in memory only after the table state change succeeded, so 
that a failed
     // enable/disable leaves the in-memory state unchanged and the next chore 
run retries it.
     tableSnapshotStore.setCurrentState(table, targetSnapshot);
   }
   ```
   
   If `enableTableIfNotEnabled()` / `disableTableIfNotDisabled()` throws, the
   `IOException` propagates before `setCurrentState()`, so the old in-memory
   snapshot retains the pending transition. The next chore run sees
   `currentSnapshot != targetSnapshot`, re-enters the branch, and retries the
   table operation. The helpers already suppress `TableNotDisabledException`,
   `TableNotEnabledException`, and `TableNotFoundException`, so a retry against 
a
   table that has since reached the requested state is a no-op.
   
   The insufficient-region-reports path is reordered the same way:
   `transitionTable()` → `enableTableIfNotEnabled()` → `setCurrentState()`.
   
   `updateNamespaceQuota()` is unchanged: its `setCurrentState()` is preceded
   only by idempotent snapshot writes, with no table enable/disable on that 
path.
   
   This fix covers retries while the same Master retains its in-memory state.
   Recovery across a Master restart in the window between the persisted snapshot
   write and the enable operation would require a durable record of the pending
   table-state change and is out of scope.
   
   # Tests
   
   Added:
   
   - `TestQuotaObserverChore.testRetryTableEnableOnFailure`
   - `TestQuotaObserverChore.testRetryTableDisableOnFailure`
   
   Both are small mock-based unit tests (no mini-cluster). The enable test
   performs this sequence:
   
   1. Constructs a real `QuotaObserverChore` with a mocked `Connection` 
returning
      a mocked `Admin`, and a mocked `SpaceQuotaSnapshotNotifier`.
   2. Calls `initializeSnapshotStores()` so the real `TableQuotaSnapshotStore` 
is
      wired to the chore's in-memory snapshot map.
   3. Seeds the current snapshot as in violation with the `DISABLE` policy and
      builds a non-violating target snapshot.
   4. Stubs `Admin.enableTable()` to throw an `IOException` on the first call 
and
      succeed on the second.
   5. Calls `updateTableQuota()` and asserts it throws `IOException`, that the
      in-memory snapshot is still the violating one, and that `enableTable()` 
was
      invoked exactly once.
   6. Calls `updateTableQuota()` again with the same current/target snapshots
      (what the next chore run would compute) and asserts no exception, that
      `enableTable()` was invoked a second time, and that the in-memory snapshot
      has advanced to the target.
   
   `testRetryTableDisableOnFailure` is symmetric: it injects one failure from
   `Admin.disableTable()` while moving into a `DISABLE` violation and verifies
   the disable is retried before the snapshot advances.
   
   Without the fix, the assertion in step 5 fails: the in-memory snapshot has
   already advanced to the non-violating target before the injected enable
   failure, which is exactly the state that caused the production chore to skip
   the retry and leave the table permanently disabled.
   
   `TestQuotaObserverChoreRegionReports` (mini-cluster, covering the
   insufficient-region-reports path) continues to pass with the reorder.


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