package com.xfestudio.xfeservermanager.core.rollback;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/** Immutable dry-run; ordering is newest event first. */
public record RollbackPlan(
UUID planId,
long watermark,
Instant createdAt,
RollbackDirection direction,
boolean force,
boolean loadUnloadedChunks,
boolean ownerAuthorized,
List<RollbackPlanItem> items,
List<JournalGap> gaps
) {
public RollbackPlan {
Objects.requireNonNull(planId, "planId");
if (watermark < 0) throw new IllegalArgumentException("watermark must be non-negative");
Objects.requireNonNull(createdAt, "createdAt");
Objects.requireNonNull(direction, "direction");
if ((force || loadUnloadedChunks) && !ownerAuthorized) {
throw new SecurityException("force and temporary chunk loading are owner-only");
}
items = List.copyOf(items);
gaps = List.copyOf(gaps);
var sequences = new java.util.HashSet<Long>();
var eventIds = new java.util.HashSet<UUID>();
for (RollbackPlanItem item : items) {
if (item.entry().sequence() > watermark) throw new IllegalArgumentException("item exceeds watermark");
if (!sequences.add(item.entry().sequence())) throw new IllegalArgumentException("duplicate plan sequence");
if (!eventIds.add(item.entry().change().eventId())) throw new IllegalArgumentException("duplicate plan event id");
}
for (int index = 1; index < items.size(); index++) {
long previous = items.get(index - 1).entry().sequence();
long current = items.get(index).entry().sequence();
if ((direction == RollbackDirection.ROLLBACK && previous <= current)
|| (direction == RollbackDirection.REDO && previous >= current)) {
throw new IllegalArgumentException(direction == RollbackDirection.ROLLBACK
? "rollback items must be in reverse sequence order"
: "redo items must be in forward sequence order");
}
}
}
public boolean completeRecoveryPossible() {
return gaps.isEmpty() && items.stream().allMatch(item -> item.executable(force));
}
public long executableCount() {
return items.stream().filter(item -> item.executable(force)).count();
}
}
package com.xfestudio.xfeservermanager.core.rollback;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/** Immutable dry-run; ordering is newest event first. */
public record RollbackPlan(
UUID planId,
long watermark,
Instant createdAt,
RollbackDirection direction,
boolean force,
boolean loadUnloadedChunks,
boolean ownerAuthorized,
List<RollbackPlanItem> items,
List<JournalGap> gaps
) {
public RollbackPlan {
Objects.requireNonNull(planId, "planId");
if (watermark < 0) throw new IllegalArgumentException("watermark must be non-negative");
Objects.requireNonNull(createdAt, "createdAt");
Objects.requireNonNull(direction, "direction");
if ((force || loadUnloadedChunks) && !ownerAuthorized) {
throw new SecurityException("force and temporary chunk loading are owner-only");
}
items = List.copyOf(items);
gaps = List.copyOf(gaps);
var sequences = new java.util.HashSet<Long>();
var eventIds = new java.util.HashSet<UUID>();
for (RollbackPlanItem item : items) {
if (item.entry().sequence() > watermark) throw new IllegalArgumentException("item exceeds watermark");
if (!sequences.add(item.entry().sequence())) throw new IllegalArgumentException("duplicate plan sequence");
if (!eventIds.add(item.entry().change().eventId())) throw new IllegalArgumentException("duplicate plan event id");
}
for (int index = 1; index < items.size(); index++) {
long previous = items.get(index - 1).entry().sequence();
long current = items.get(index).entry().sequence();
if ((direction == RollbackDirection.ROLLBACK && previous <= current)
|| (direction == RollbackDirection.REDO && previous >= current)) {
throw new IllegalArgumentException(direction == RollbackDirection.ROLLBACK
? "rollback items must be in reverse sequence order"
: "redo items must be in forward sequence order");
}
}
}
public boolean completeRecoveryPossible() {
return gaps.isEmpty() && items.stream().allMatch(item -> item.executable(force));
}
public long executableCount() {
return items.stream().filter(item -> item.executable(force)).count();
}
}