cui2022 commented on issue #3150:
URL: https://github.com/apache/hugegraph/issues/3150#issuecomment-5252074460

   <html>
   <body>
   <!--StartFragment--><html><head></head><body><p>英文翻译如下(适合提交给 HugeGraph 开发者 / 
GitHub Issue / 邮件讨论):</p><hr><p>I provided the source code of 
<strong>pd-service</strong> to Claude Code and explained my issue. Claude Code 
analyzed the code and believes that there may be a bug in the replica migration 
logic when a Store node enters the <strong>Tombstone</strong> state. The issue 
may cause the old Store information in the PD metadata to not be updated 
correctly.</p><p>I am not sure whether this analysis can provide any useful 
information, but I hope it can help with the investigation.</p><p>Below is the 
analysis result from Claude Code:</p><hr><h1>Complete Analysis: Replica 
Migration Failure During Offline → Tombstone Transition</h1><h2>Initial 
Conditions</h2><p>Assume the cluster state is:</p><pre><code>partition 
shardCount = 3
   
   Active Stores:
   Store1 (Up)
   Store2 (Up)
   
   Store3:
   Previously Offline, about to be changed to Tombstone
   
   ShardGroup 0:
   [
     Store1 (Leader),
     Store2 (Follower),
     Store3 (Follower)
   ]
   </code></pre><p>Note:</p><p>When Store3 was changed to 
<strong>Offline</strong>, <code inline="">updateStore()</code> only called 
<code inline="">removeActiveStore()</code> (line 306), but did not call <code 
inline="">storeTurnoff()</code>. Therefore, the ShardGroup stored in RocksDB 
still contains Store3's shard information.</p><hr><h2>Phase 1: REST API 
Entry</h2><p>User sends:</p><pre><code>POST /v1/store/3
   {
     "storeState": "Tombstone"
   }
   </code></pre><p><code inline="">StoreAPI.java:90-97</code></p><pre><code 
class="language-java">Metapb.Store lastStore = pdRestService.getStore(storeId);
   // Store3, current state = Offline
   
   Metapb.Store newStore = ... setState(Tombstone);
   
   pdRestService.updateStore(newStore);
   </code></pre><p>Then the request enters the core update 
logic.</p><hr><h2>Phase 2: updateStore()</h2><p><code 
inline="">updateStore()</code> changes:</p><pre><code>lastStore.state = Offline
   store.state     = Tombstone
   </code></pre><h3>State validation</h3><p>Because:</p><pre><code 
class="language-java">store.getState() == Tombstone
   </code></pre><p>is true.</p><p>However:</p><pre><code 
class="language-java">lastStore.getState() == Up
   </code></pre><p>is false because the previous state was already 
Offline.</p><p>Therefore, the <code inline="">minStoreCount</code> validation 
is skipped and the update succeeds.</p><hr><h2>Phase 3: Tombstone 
Processing</h2><p>The Store metadata is updated:</p><pre><code 
class="language-java">storeInfoMeta.updateStore(store);
   </code></pre><p>The state transition is detected:</p><pre><code>Offline 
-&gt; Tombstone
   </code></pre><p>Then:</p><pre><code 
class="language-java">storeInfoMeta.removeActiveStore(store);
   
   storeTurnoff(store);
   </code></pre><p>is executed.</p><hr><h2>Phase 4: First Replica Migration 
Attempt</h2><p>Inside:</p><pre><code>storeTurnoff(Store3)
   </code></pre><p>The code finds:</p><pre><code>ShardGroup:
   [
    Store1,
    Store2,
    Store3
   ]
   </code></pre><p>Then it removes Store3:</p><pre><code>[
    Store1,
    Store2
   ]
   </code></pre><p>and calls:</p><pre><code 
class="language-java">reallocShards(filteredShardGroup)
   </code></pre><hr><h2>Potential Bug 1: reallocShards() Early 
Return</h2><p>Inside:</p><pre><code>reallocShards()
   </code></pre><p>Active stores:</p><pre><code>[
    Store1,
    Store2
   ]
   </code></pre><p>Configured shard count:</p><pre><code>3
   </code></pre><p>Then:</p><pre><code class="language-java">shardCount = 
Math.min(3, activeStores.size());
   </code></pre><p>becomes:</p><pre><code>shardCount = 2
   </code></pre><p>The input shard list is already:</p><pre><code>[
    Store1,
    Store2
   ]
   </code></pre><p>Therefore:</p><pre><code class="language-java">shardCount == 
shards.size()
   </code></pre><p>and execution reaches:</p><pre><code 
class="language-java">else {
       return shards;
   }
   </code></pre><p>The function returns immediately.</p><p>As a 
result:</p><ul><li><p><code inline="">storeInfoMeta.updateShardGroup()</code> 
is <strong>not called</strong></p></li><li><p><code 
inline="">fireChangeShard()</code> is <strong>not 
called</strong></p></li><li><p>RocksDB still contains:</p></li></ul><pre><code>[
    Store1,
    Store2,
    Store3
   ]
   </code></pre><p>The migration attempt effectively does 
nothing.</p><hr><h2>Phase 5: Store Status Listener 
Triggered</h2><p>After:</p><pre><code>Offline -&gt; Tombstone
   </code></pre><p>the listener is triggered:</p><pre><code 
class="language-java">PartitionService.storeOffline(store);
   </code></pre><p>This starts another migration attempt.</p><hr><h2>Phase 6: 
Second Migration Attempt</h2><p><code inline="">storeOffline()</code> reads the 
ShardGroup from RocksDB:</p><pre><code>[
    Store1,
    Store2,
    Store3
   ]
   </code></pre><p>Because the previous migration did not update 
metadata.</p><p>Then it calls:</p><pre><code 
class="language-java">reallocShards(shardGroup);
   </code></pre><hr><h2>Potential Bug 2: Wrong Replica 
Removal</h2><p>Now:</p><pre><code>activeStores = [
    Store1,
    Store2
   ]
   
   shards = [
    Store1 Leader,
    Store2 Follower,
    Store3 Follower
   ]
   </code></pre><p>Again:</p><pre><code class="language-java">shardCount = 
Math.min(3,2)
   </code></pre><p>so:</p><pre><code>shardCount = 2
   </code></pre><p>The system needs to remove one replica.</p><p>The current 
logic:</p><pre><code class="language-java">if (iterator.next().getRole() != 
Leader) {
       iterator.remove();
   }
   </code></pre><p>only checks whether the shard is a Leader.</p><p>It does not 
check:</p><ul><li><p>whether the Store is Tombstone</p></li><li><p>whether the 
Store is Offline</p></li><li><p>whether the Store is 
healthy</p></li></ul><p>Because the iteration order is:</p><pre><code>Store1 
Leader
   Store2 Follower
   Store3 Follower
   </code></pre><p>The result becomes:</p><pre><code>Store1 Leader  (kept)
   
   Store2 Follower (removed)
   
   Store3 Tombstone (kept)
   </code></pre><p>Therefore:</p><ul><li><p>A healthy Store2 is 
removed.</p></li><li><p>The failed Tombstone Store3 remains in the replica 
list.</p></li></ul><p>The metadata is then updated incorrectly:</p><pre><code>[
    Store1,
    Store3
   ]
   </code></pre><p>and a conf change is 
sent:</p><pre><code>fireChangeShard([Store1, Store3])
   </code></pre><p>which still includes the unavailable Store3.</p><hr><h2>Root 
Cause Summary</h2><p>There appear to be two potential bugs:</p>
   Bug | Location | Problem
   -- | -- | --
   Bug 1 | reallocShards() | When the filtered shard list size equals the 
calculated shard count, the function returns directly without updating PD 
metadata. The removed Tombstone store remains in RocksDB.
   Bug 2 | reallocShards() | Replica removal only avoids removing the Leader. 
It does not consider Store state, so a healthy follower can be removed while a 
Tombstone store remains.
   
   <hr><h2>Trigger Condition</h2><p>The issue happens 
when:</p><pre><code>activeStoreCount &lt;= configuredShardCount
   </code></pre><p>Because:</p><pre><code 
class="language-java">Math.min(configuredShardCount, activeStoreCount)
   </code></pre><p>reduces the target replica count.</p><p>When the filtered 
shard list size equals this reduced value, the early return path is 
triggered.</p><hr><h2>My Questions</h2><ol><li><p>Is this analysis consistent 
with the expected behavior of HugeGraph-Store PD?</p></li><li><p>Is there any 
known issue regarding Offline → Tombstone transition and replica 
migration?</p></li><li><p>Is there a recommended workaround or recovery 
procedure for this situation?</p></li><li><p>Is there any API or administrative 
operation that can manually trigger replica migration or force PD to rebuild 
the ShardGroup metadata?</p></li></ol><p>Thank you very much for your 
help.</p><hr>
   </body>
   </html>


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to