numinnex commented on code in PR #4506:
URL: https://github.com/apache/iggy/pull/4506#discussion_r4270237258


##########
core/shard/src/lib.rs:
##########
@@ -6559,11 +6718,17 @@ where
         }
         if consensus.is_normal()
             && !consensus.is_transferring()
-            && consensus.commit_min() < consensus.commit_max()
+            && (consensus.commit_min() < consensus.commit_max() || 
consensus.view_log_unproven())
             && self.metadata_repair.borrow().is_none()
         {
-            let to_op = consensus.commit_max();
-            let from_op = consensus.commit_min() + 1;
+            let to_op = if consensus.view_log_unproven() {
+                consensus.sequencer().current_sequence()

Review Comment:
   **critical**: server panic. While unproven, `to_op` is the sequencer and 
`from_op` is `view_log_repair_op` (the adopted head). A backup's `on_replicate` 
(metadata.rs:1705, `set_sequence(header.op)`) rewinds the sequencer below that 
head when a retransmit fills a journal gap. That builds `RequestPrepares 
[38,37]`, and the `expect` at consensus_message.rs:463 ("repair range must be 
non-empty and 1-based") aborts the process.
   Repro: metadata swarm seed 7002, and `--plane lifecycle --shell --clients 3 
--faults swarm --crash-prob 0.003 --restart-prob 0.02 --crash-primary --ticks 
40000 --heal-before-quiesce` seed 20037. 32/200 metadata seeds at head, 0 on 
the previous head and base; a clamp mutant gives 0.
   Fix: `to_op = max(seq, pending.op_head, from_op)`, and never send an empty 
range. The partition twin (:9288) is lower risk because partition backups only 
move the sequencer forward.



##########
core/server/src/http/forward.rs:
##########
@@ -332,92 +534,69 @@ fn record_relayed_floor(state: &HttpInner, user_id: u32, 
response: &Response) {
     state.metadata_watermarks.record(user_id, applied);
 }
 
-async fn forward_partition_or_pass(state: HttpState, request: Request, next: 
Next) -> Response {
-    if !state.forward.active || 
request.headers().contains_key(FORWARDED_HEADER) {
-        return next.run(request).await;
-    }
-    // A read that does not move consumer progress is answered by whichever
-    // replica the caller reached, which is the point of reading from one. Only
-    // an automatic commit needs a replica that can originate the offset
-    // operation, so only that poll pays for the buffering and the credential
-    // resolution below.
-    if request.method() == Method::GET && 
!wants_auto_commit(request.uri().query()) {
-        return next.run(request).await;
-    }
+async fn attach_partition_request(state: HttpState, request: Request, next: 
Next) -> Response {
+    let received = Instant::now();
+    let hop = match partition_hop(request.headers()) {
+        Ok(hop) => hop,
+        Err(error) => return CustomError::from(error).into_response(),
+    };
+    // Verify-only, like the metadata relay: an unauthenticated caller gets no
+    // buffered body and no relay. The handler authenticates again.
     let bearer = match bearer_token(request.headers()) {
         Ok(bearer) => bearer,
         Err(error) => return CustomError::from(error).into_response(),
     };
     if let Err(rejection) = resolve_credential(&state, bearer).await {
         return rejection.into_response();
     }
-
-    let (parts, request_body) = request.into_parts();
+    let _guard = if matches!(hop, PartitionHop::Target(_)) {
+        let Some(guard) = ForwardGuard::admit(&state.forward.in_flight) else {

Review Comment:
   warning: the Target guard is admitted before the untimed `to_bytes` body 
read, and Target comes from client-supplied headers. 128 authenticated 
slow-body connections pin `MAX_IN_FLIGHT_FORWARDS`, and this node's own 
outbound relays then get `forward_busy` 503, which is passed to clients 
unretried. No test pins the bound.
   Admit after the body is read (scope the guard to the hold), or give Target 
holds their own cap.



##########
core/shard/src/lib.rs:
##########
@@ -5469,6 +5639,8 @@ where
             return;
         }
         partition.apply_repaired_prepare(msg).await;
+        let pending = partition.consensus().pending_view_log();
+        reconcile_partition_view_divergence(self.id, partition, 
pending.as_ref()).await;

Review Comment:
   warning: every repaired frame, held duplicates included, clones 
`pending_view_log` and reruns the full reconcile (O(op_head - gap) walk plus a 
linear canonical find), so a gap of G costs O(G^2) CPU on the shard pump. 
Reconcile once per RepairDone, or only when the frame filled the missing op.



##########
core/consensus/src/impls.rs:
##########
@@ -3666,6 +3704,9 @@ impl<B: MessageBus, P: Pipeline<Entry = PipelineEntry>> 
VsrConsensus<B, P> {
         // it: the new primary's numbers are at least as fresh as any probe
         // answer.
         self.finish_view_probe();
+        let needs_reconciliation = self.view_log_unproven.get()

Review Comment:
   nit: the same-view fix (skip the early return while unproven) and the 
metadata StartView suffix refresh (shard/lib.rs:5092) are unpinned: mutants 
survive consensus (221), shard (183) and sim (242) tests. The new metadata 
unproven-repair arming (shard/lib.rs:6724) is also untested, which is how the 
empty-range panic got in. Add a metadata twin of partition_io.rs:4174 and a 
same-view Commit-while-unproven test.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -11540,8 +11847,12 @@ where
                 self.consensus().set_commit_floor(floor);

Review Comment:
   warning (suspect, conf L): `complete_repair` can raise the commit floor 
while the view log is unproven. That can jump commit_min over durable 
dead-branch entries, which then count as proven (impls.rs:4023-4027) and are 
served by repair. Refuse the floor jump while unproven, or re-prove first.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -8542,11 +8764,45 @@ where
     }
 
     async fn truncate_resident_from(&mut self, from_op: u64) -> Result<usize, 
IggyError> {
+        self.remove_resident_range(from_op..=u64::MAX).await
+    }
+
+    /// Replace a proven mismatch while retaining the canonical tail across 
gaps.
+    ///
+    /// # Errors
+    /// Returns an error if the WAL or resident indexes cannot be rewritten.
+    pub async fn remove_divergent_prepare(&mut self, op: u64) -> Result<(), 
IggyError> {

Review Comment:
   nit: removing a divergent prepare clears `certified_log_view` durably 
(partition_journal.rs:565), so a crash before the refill reboots as 
materialization-missing and needs a full state transfer 
(iggy_partition.rs:1724-1734). Correct, but expensive; worth a comment or 
keeping the certificate for the unchanged prefix.



##########
core/simulator/src/bin/workload-fuzz.rs:
##########
@@ -625,6 +653,23 @@ fn run_quiesce_phase(
             args.min_metadata_ops_compared, convergence.ops_compared,
         ));
     }
+    if sim.is_shell()
+        && workload.options.weights.draws_fixture_groups()
+        && convergence.owners_compared < args.min_owners_compared
+    {
+        vacuous(format_args!(
+            "--min-owners-compared {}: the run joined fixture groups, but the 
owner oracle \
+             compared only {} settled owner(s) (seed={seed:#x})",
+            args.min_owners_compared, convergence.owners_compared,
+        ));
+    }
+    if sim.is_shell() && args.plane == Plane::Lifecycle && 
workload.owner_fence_refusals() == 0 {

Review Comment:
   nit: the vacuity gate exits 3 on many lifecycle `--shell` swarm seeds (7/24 
and 18/30 in two runs; e.g. seeds 1, 2, 5, 8, 18, 21, 23), all with 
`stale_owner_stores=0`. The workload never forces a stale-owner store, so a CI 
cell on a random seed would flake. Deterministically schedule one stale store 
per run.



##########
core/server/README.md:
##########
@@ -39,6 +39,22 @@ cargo run --bin iggy-server --release -- --replica-id 0
 
 Command-line arguments include `--replica-id`, `--fresh` (`-f`), 
`--with-default-root-credentials`, and `--list-config-env-vars`. Other settings 
come from configuration.
 
+## Upgrading
+
+Deploy matching server and SDK releases together. Rolling upgrades and mixed
+server versions are unsupported. Partition control frames from older servers
+carry incarnation zero and are rejected, so a mixed-version cluster cannot
+complete partition view changes. The `IGGY-OWNERSHIP-HISTORY-5` storage gate 
also
+rejects older data directories without modifying them. Existing data requires a
+separately validated offline migration or restore procedure before this release
+can use it.
+
+Cluster metadata includes `replica_id`. Java's `ClusterNode` record adds a 
leading
+`replicaId` component, changing constructor calls and record patterns. C# 
callers
+must initialize the required `ReplicaId` property. The CLI's cluster metadata 
list
+appends a replica ID column. Go, Java, Node.js and C# clients use command 
105's new

Review Comment:
   nit: the upgrade list misses the Swift `ClusterNode(replicaID:)` initializer 
(Resources.swift:586), the C++ FFI `ClusterNode.replica_id` 
(foreign/cpp/src/lib.rs:338) and the new pub field on Rust `iggy_common` 
`ClusterNode`/`ClusterNodeResponse`. The PR body (squash message) still lacks 
an SDK paragraph and still says "Simulator checks cover replacement histories".



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -8125,6 +8360,7 @@ where
         if self.fatal.is_some()
             || self.materialization_missing
             || self.commit_waits_for_checkpoint()
+            || self.consensus.view_log_unproven()

Review Comment:
   **critical**: this gate covers only `commit_journal`. The `resume_io` walk 
at :5909-5914 (`select_persistable_commits` / 
`collect_committable_from_journal` -> `select_commit` (:10075) -> 
`finish_pending_commit` never checks `view_log_unproven` / 
`view_log_entry_proven`. The walk is capped at `persistence.head()`, which 
still covers dead-branch ops made durable in the old view, so a fenced backup 
applies them.
   Seed 7009 (7016 flags): r1 restarts, proof `Missing{60}`, then commits its 
discarded v7 op 59 while unproven (backtrace through `finish_pending_commit`). 
Seed 7056 does the same with op 75.
   Committed divergence is worse than before this fix: partition 7000-7299 
17/300 (previous head 6/300), mixed 1000-1149 9/150 (previously 0). With 
`select_commit` refusing while unproven, both drop to 0.
   Fix: gate per op on `view_log_entry_proven` in `select_commit` and 
`collect_committable_from_journal`.



##########
core/shard/src/lib.rs:
##########
@@ -8991,32 +9262,32 @@ where
         let commit_to_op = consensus.commit_max();
         let commit_lag = consensus.commit_min() < commit_to_op;
         let head = consensus.sequencer().current_sequence();
-        if !commit_lag && head <= commit_to_op {
+        if !commit_lag && head <= commit_to_op && 
!consensus.view_log_unproven() {
             return false;
         }
         let missing_suffix = partition_missing_suffix_through(partition);
         // Fetch the adopted suffix even while committed operations lag. Later
         // live prepares can advance the head while an adopted body is missing.
-        let Some(fetch_to_op) =
+        let Some(fetch_to_op) = consensus.view_log_repair_op().map(|_| 
head).or_else(|| {
             partition_repair_fetch_to_op(consensus.commit_min(), commit_to_op, 
missing_suffix)
-        else {
+        }) else {
             return false;
         };
         // A resident window may only be waiting for persistence or a bounded 
commit walk.
-        if partition
-            .log
-            .journal()
-            .inner
-            .repaired_window_shape(consensus.commit_min(), fetch_to_op)
-            .complete
+        if !consensus.view_log_unproven()
+            && partition
+                .log
+                .journal()
+                .inner
+                .repaired_window_shape(consensus.commit_min(), fetch_to_op)
+                .complete
         {
             return false;
         }
         let nonce = iggy_common::random_id::get_uuid();
-        let from_op = consensus.commit_min() + 1;
-        let cluster = consensus.cluster();
-        let self_id = consensus.replica();
-        let namespace = consensus.group();
+        let from_op = consensus
+            .view_log_repair_op()
+            .unwrap_or_else(|| consensus.commit_min() + 1);

Review Comment:
   **critical** (liveness): while fenced, repair starts at 
`view_log_repair_op`, the HIGHEST missing op, because the proof walks top-down, 
and requests up to head. Each round trip fills one hole and re-serves up to 128 
bodies already held. A unit probe filled a 2046-op gap in 2076 rounds (258k 
bodies, 2.2 s of pump CPU, about quadratic); a ring-sized gap is about 65k 
round trips before falling back to state transfer. Base repairs upward from 
commit_min+1 in 128-op chunks.
   Sim: 7016 flags, seeds 20000-20399, "did not drain" 17/400 at head vs 0/400 
on base. On seed 20007, with r0 down, op 300 was served 125 times but never 
ingested, so the two live replicas stayed wedged for 50k ticks: the fenced 
backup withholds PrepareOk and there is no quorum.
   Fix: request the chunk that ends at the hole (`from = max(commit_min+1, 
missing-127)`, `to = missing`), so the parent chain proves 128 ops per round 
trip. The same applies to the resume path at :5966.



##########
core/metadata/src/impls/metadata.rs:
##########
@@ -4231,6 +4224,9 @@ where
     #[allow(clippy::future_not_send)]
     pub async fn commit_journal(&self) {
         let consensus = self.consensus.as_ref().unwrap();
+        if consensus.view_log_unproven() {

Review Comment:
   warning (suspect): commit is fenced here, but the metadata superblock 
(:3719, via shard tick) persists the new `log_view` even while 
`view_log_unproven`, but the fence itself is not durable (impls.rs init false). 
After a crash, entries that were unproven at or below the fence are reported 
under the new log_view. The merge refuses on a same-log_view conflict, so this 
looks like a stall rather than loss. The partition plane is safe 
(certify_log_view requires the WAL head checksum). Consider deferring the 
persist until proven.



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