mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Studio hotload deadlock fix
- Live boundary capture runs without the generation history lock - Promotion prepares the boundary and activates under the lock in separate steps - Tab completion and find lookups stay responsive while a Studio save captures terrain - Admission test covers history reads during the boundary checkpoint
This commit is contained in:
@@ -457,9 +457,7 @@ public final class GenerationHistory {
|
||||
"signatureCapture"
|
||||
);
|
||||
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
|
||||
synchronized (this) {
|
||||
return promotePendingLocked(requiredCapture, true, true);
|
||||
}
|
||||
return promoteCaptured(requiredCapture, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,9 +476,7 @@ public final class GenerationHistory {
|
||||
|
||||
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
|
||||
requireOpen();
|
||||
synchronized (GenerationHistory.this) {
|
||||
return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture"), false, true);
|
||||
}
|
||||
return promoteCaptured(Objects.requireNonNull(signatureCapture, "signature capture"), false);
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
@@ -497,34 +493,67 @@ public final class GenerationHistory {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation promoteCaptured(
|
||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery
|
||||
) throws IOException {
|
||||
Optional<PreparedPromotion> prepared;
|
||||
synchronized (this) {
|
||||
prepared = preparePromotionLocked(startupRecovery, true);
|
||||
}
|
||||
if (prepared.isEmpty()) {
|
||||
return activeActivation();
|
||||
}
|
||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot = publishTerrainSignatures(prepared.get(), signatureCapture);
|
||||
synchronized (this) {
|
||||
return completePromotionLocked(prepared.get(), terrainSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation promotePendingLocked(
|
||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery, boolean validateSemantics
|
||||
) throws IOException {
|
||||
Optional<PreparedPromotion> prepared = preparePromotionLocked(startupRecovery, validateSemantics);
|
||||
if (prepared.isEmpty()) {
|
||||
return store.activeActivation();
|
||||
}
|
||||
return completePromotionLocked(prepared.get(), publishTerrainSignatures(prepared.get(), signatureCapture));
|
||||
}
|
||||
|
||||
private Optional<PreparedPromotion> preparePromotionLocked(
|
||||
boolean startupRecovery, boolean validateSemantics
|
||||
) throws IOException {
|
||||
sync();
|
||||
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
|
||||
validateReferencedState(Optional.empty(), validateSemantics);
|
||||
Optional<GenerationActivation> pending = store.pendingActivation();
|
||||
if (pending.isEmpty()) {
|
||||
return store.activeActivation();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
long outgoingActivationId = store.activeActivation().activationId();
|
||||
ownership.assignUnassigned(inventory, outgoingActivationId);
|
||||
ownership.persist();
|
||||
requireExplicitOwnership(inventory);
|
||||
GenerationBoundary boundary = boundaries.publishOwnership(
|
||||
pending.get().activationId(),
|
||||
ownership
|
||||
);
|
||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot;
|
||||
long pendingActivationId = pending.get().activationId();
|
||||
return Optional.of(new PreparedPromotion(pendingActivationId, boundaries.publishOwnership(pendingActivationId, ownership)));
|
||||
}
|
||||
|
||||
private TerrainBoundarySignatureStore.Snapshot publishTerrainSignatures(
|
||||
PreparedPromotion prepared, BoundarySignatureCapture signatureCapture
|
||||
) throws IOException {
|
||||
try (TerrainBoundarySignatureStore.SignatureSampler sampler = Objects.requireNonNull(
|
||||
signatureCapture.capture(boundary), "boundary signature sampler"
|
||||
signatureCapture.capture(prepared.boundary()), "boundary signature sampler"
|
||||
)) {
|
||||
terrainSnapshot = terrainSignatures.publish(pending.get().activationId(), boundary, sampler);
|
||||
return terrainSignatures.publish(prepared.activationId(), prepared.boundary(), sampler);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation completePromotionLocked(
|
||||
PreparedPromotion prepared, TerrainBoundarySignatureStore.Snapshot terrainSnapshot
|
||||
) throws IOException {
|
||||
GenerationActivation completed = store.completePendingTransition(
|
||||
pending.get().activationId(),
|
||||
boundary.identity(),
|
||||
prepared.activationId(),
|
||||
prepared.boundary().identity(),
|
||||
terrainSnapshot.identity()
|
||||
);
|
||||
GenerationActivation activated = store.activatePending(completed.activationId());
|
||||
@@ -1396,6 +1425,9 @@ public final class GenerationHistory {
|
||||
public interface BoundarySignatureCapture {
|
||||
TerrainBoundarySignatureStore.SignatureSampler capture(GenerationBoundary boundary) throws IOException;
|
||||
}
|
||||
|
||||
private record PreparedPromotion(long activationId, GenerationBoundary boundary) {
|
||||
}
|
||||
public record FreshCreation(Path dimensionRoot, Path packSource, String packFingerprint, long worldSeed,
|
||||
GenerationEpoch.DimensionContract dimensionContract, GenerationRegistryContract registryContract) {
|
||||
public FreshCreation {
|
||||
|
||||
@@ -143,6 +143,37 @@ public class GenerationHistoryAdmissionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveBoundaryCaptureLeavesHistoryReadableFromOtherThreads() throws Exception {
|
||||
GenerationHistory history = history();
|
||||
Path replacement = pack("replacement", "beta");
|
||||
GenerationActivation pending = history.stageUpdate(replacement, fingerprint(replacement),
|
||||
history.activeEpoch().dimensionContract(), GenerationRegistryContract.empty(), 32);
|
||||
ExecutorService tickThread = Executors.newSingleThreadExecutor();
|
||||
try (GenerationHistory.LiveCutover cutover = history.beginLiveCutover()) {
|
||||
GenerationActivation promoted = cutover.promote(boundary -> new TerrainBoundarySignatureStore.SignatureSampler() {
|
||||
@Override
|
||||
public TerrainBoundarySignature sample(int blockX, int blockZ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
assertEquals(pending.activationId(), tickThread.submit(history::pendingActivation)
|
||||
.get(1, TimeUnit.SECONDS).orElseThrow().activationId());
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException failure) {
|
||||
throw new IOException("History was unreadable while the boundary checkpoint waited", failure);
|
||||
}
|
||||
}
|
||||
});
|
||||
assertEquals(pending.activationId(), promoted.activationId());
|
||||
} finally {
|
||||
drain(tickThread);
|
||||
}
|
||||
assertEquals(pending.activationId(), history.activeActivation().activationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedMetadataReadReleasesItsStageAdmission() throws Exception {
|
||||
GenerationHistory history = history();
|
||||
|
||||
Reference in New Issue
Block a user