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"
|
"signatureCapture"
|
||||||
);
|
);
|
||||||
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
|
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
|
||||||
synchronized (this) {
|
return promoteCaptured(requiredCapture, true);
|
||||||
return promotePendingLocked(requiredCapture, true, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,9 +476,7 @@ public final class GenerationHistory {
|
|||||||
|
|
||||||
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
|
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
|
||||||
requireOpen();
|
requireOpen();
|
||||||
synchronized (GenerationHistory.this) {
|
return promoteCaptured(Objects.requireNonNull(signatureCapture, "signature capture"), false);
|
||||||
return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture"), false, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireOpen() {
|
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(
|
private GenerationActivation promotePendingLocked(
|
||||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery, boolean validateSemantics
|
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 {
|
) throws IOException {
|
||||||
sync();
|
sync();
|
||||||
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
|
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
|
||||||
validateReferencedState(Optional.empty(), validateSemantics);
|
validateReferencedState(Optional.empty(), validateSemantics);
|
||||||
Optional<GenerationActivation> pending = store.pendingActivation();
|
Optional<GenerationActivation> pending = store.pendingActivation();
|
||||||
if (pending.isEmpty()) {
|
if (pending.isEmpty()) {
|
||||||
return store.activeActivation();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
long outgoingActivationId = store.activeActivation().activationId();
|
long outgoingActivationId = store.activeActivation().activationId();
|
||||||
ownership.assignUnassigned(inventory, outgoingActivationId);
|
ownership.assignUnassigned(inventory, outgoingActivationId);
|
||||||
ownership.persist();
|
ownership.persist();
|
||||||
requireExplicitOwnership(inventory);
|
requireExplicitOwnership(inventory);
|
||||||
GenerationBoundary boundary = boundaries.publishOwnership(
|
long pendingActivationId = pending.get().activationId();
|
||||||
pending.get().activationId(),
|
return Optional.of(new PreparedPromotion(pendingActivationId, boundaries.publishOwnership(pendingActivationId, ownership)));
|
||||||
ownership
|
}
|
||||||
);
|
|
||||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot;
|
private TerrainBoundarySignatureStore.Snapshot publishTerrainSignatures(
|
||||||
|
PreparedPromotion prepared, BoundarySignatureCapture signatureCapture
|
||||||
|
) throws IOException {
|
||||||
try (TerrainBoundarySignatureStore.SignatureSampler sampler = Objects.requireNonNull(
|
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(
|
GenerationActivation completed = store.completePendingTransition(
|
||||||
pending.get().activationId(),
|
prepared.activationId(),
|
||||||
boundary.identity(),
|
prepared.boundary().identity(),
|
||||||
terrainSnapshot.identity()
|
terrainSnapshot.identity()
|
||||||
);
|
);
|
||||||
GenerationActivation activated = store.activatePending(completed.activationId());
|
GenerationActivation activated = store.activatePending(completed.activationId());
|
||||||
@@ -1396,6 +1425,9 @@ public final class GenerationHistory {
|
|||||||
public interface BoundarySignatureCapture {
|
public interface BoundarySignatureCapture {
|
||||||
TerrainBoundarySignatureStore.SignatureSampler capture(GenerationBoundary boundary) throws IOException;
|
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,
|
public record FreshCreation(Path dimensionRoot, Path packSource, String packFingerprint, long worldSeed,
|
||||||
GenerationEpoch.DimensionContract dimensionContract, GenerationRegistryContract registryContract) {
|
GenerationEpoch.DimensionContract dimensionContract, GenerationRegistryContract registryContract) {
|
||||||
public FreshCreation {
|
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
|
@Test
|
||||||
public void failedMetadataReadReleasesItsStageAdmission() throws Exception {
|
public void failedMetadataReadReleasesItsStageAdmission() throws Exception {
|
||||||
GenerationHistory history = history();
|
GenerationHistory history = history();
|
||||||
|
|||||||
Reference in New Issue
Block a user