/* * Copyright 2013 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.persistence.internal; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.config.Config; import org.terasology.engine.ComponentSystemManager; import org.terasology.engine.Time; import org.terasology.engine.module.ModuleManager; import org.terasology.entitySystem.Component; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.entitySystem.entity.internal.EngineEntityManager; import org.terasology.entitySystem.entity.internal.EntityChangeSubscriber; import org.terasology.entitySystem.entity.internal.EntityDestroySubscriber; import org.terasology.entitySystem.entity.internal.PojoEntityManager; import org.terasology.entitySystem.metadata.ComponentLibrary; import org.terasology.entitySystem.systems.ComponentSystem; import org.terasology.game.Game; import org.terasology.game.GameManifest; import org.terasology.logic.location.LocationComponent; import org.terasology.math.geom.Vector3f; import org.terasology.math.geom.Vector3i; import org.terasology.module.Module; import org.terasology.module.ModuleEnvironment; import org.terasology.monitoring.PerformanceMonitor; import org.terasology.network.Client; import org.terasology.network.ClientComponent; import org.terasology.network.NetworkSystem; import org.terasology.persistence.typeHandling.TypeSerializationLibrary; import org.terasology.protobuf.EntityData; import org.terasology.registry.CoreRegistry; import org.terasology.utilities.FilesUtil; import org.terasology.utilities.concurrency.ShutdownTask; import org.terasology.utilities.concurrency.Task; import org.terasology.utilities.concurrency.TaskMaster; import org.terasology.world.WorldProvider; import org.terasology.world.biomes.Biome; import org.terasology.world.biomes.BiomeManager; import org.terasology.world.block.BlockManager; import org.terasology.world.block.family.BlockFamily; import org.terasology.world.chunks.Chunk; import org.terasology.world.chunks.ChunkProvider; import org.terasology.world.chunks.ManagedChunk; import org.terasology.world.chunks.internal.ChunkImpl; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** */ public final class ReadWriteStorageManager extends AbstractStorageManager implements EntityDestroySubscriber, EntityChangeSubscriber, DelayedEntityRefFactory { private static final Logger logger = LoggerFactory.getLogger(ReadWriteStorageManager.class); private final TaskMaster<Task> saveThreadManager; private final SaveTransactionHelper saveTransactionHelper; /** * This lock should be hold during read and write operation in the world directory. Currently it is being hold * during reads of chunks or players as they are crruently the only data that needs to be loaded during the game. * <br><br> * This lock ensures that reading threads can properly finish reading even when for example the ZIP file with the * chunks got replaced with a newer version. Chunks that are getting saved get loaded from memory. It can however * still be that a thread tries to load another chunk from the same ZIP file that contains the chunk that needs to * be saved. Thus it can potentially happen that 2 threads want to read/write the same ZIP file with chunks. */ private final ReadWriteLock worldDirectoryLock = new ReentrantReadWriteLock(true); private final Lock worldDirectoryReadLock = worldDirectoryLock.readLock(); private final Lock worldDirectoryWriteLock = worldDirectoryLock.writeLock(); private SaveTransaction saveTransaction; private Config config; /** * Time of the next save in the format that {@link System#currentTimeMillis()} returns. */ private Long nextAutoSave; private boolean saveRequested; private ConcurrentMap<Vector3i, CompressedChunkBuilder> unloadedAndUnsavedChunkMap = Maps.newConcurrentMap(); private ConcurrentMap<Vector3i, CompressedChunkBuilder> unloadedAndSavingChunkMap = Maps.newConcurrentMap(); private ConcurrentMap<String, EntityData.PlayerStore> unloadedAndUnsavedPlayerMap = Maps.newConcurrentMap(); private ConcurrentMap<String, EntityData.PlayerStore> unloadedAndSavingPlayerMap = Maps.newConcurrentMap(); private EngineEntityManager privateEntityManager; private EntitySetDeltaRecorder entitySetDeltaRecorder; /** * A component library that provides a copy() method that replaces {@link EntityRef}s which {@link EntityRef}s * that will use the privateEntityManager. */ private ComponentLibrary entityRefReplacingComponentLibrary; public ReadWriteStorageManager(Path savePath, ModuleEnvironment environment, EngineEntityManager entityManager, BlockManager blockManager, BiomeManager biomeManager) throws IOException { this(savePath, environment, entityManager, blockManager, biomeManager, true); } public ReadWriteStorageManager(Path savePath, ModuleEnvironment environment, EngineEntityManager entityManager, BlockManager blockManager, BiomeManager biomeManager, boolean storeChunksInZips) throws IOException { super(savePath, environment, entityManager, blockManager, biomeManager, storeChunksInZips); entityManager.subscribeForDestruction(this); entityManager.subscribeForChanges(this); // TODO Ensure that the component library and the type serializer library are thread save (e.g. immutable) this.privateEntityManager = createPrivateEntityManager(entityManager.getComponentLibrary()); Files.createDirectories(getStoragePathProvider().getStoragePathDirectory()); this.saveTransactionHelper = new SaveTransactionHelper(getStoragePathProvider()); this.saveThreadManager = TaskMaster.createFIFOTaskMaster("Saving", 1); this.config = CoreRegistry.get(Config.class); this.entityRefReplacingComponentLibrary = privateEntityManager.getComponentLibrary() .createCopyUsingCopyStrategy(EntityRef.class, new DelayedEntityRefCopyStrategy(this)); this.entitySetDeltaRecorder = new EntitySetDeltaRecorder(this.entityRefReplacingComponentLibrary); } private static EngineEntityManager createPrivateEntityManager(ComponentLibrary componentLibrary) { PojoEntityManager pojoEntityManager = new PojoEntityManager(); pojoEntityManager.setComponentLibrary(componentLibrary); pojoEntityManager.setTypeSerializerLibrary(CoreRegistry.get(TypeSerializationLibrary.class)); return pojoEntityManager; } @Override public void finishSavingAndShutdown() { saveThreadManager.shutdown(new ShutdownTask(), true); checkSaveTransactionAndClearUpIfItIsDone(); } private void checkSaveTransactionAndClearUpIfItIsDone() { if (saveTransaction != null) { SaveTransactionResult result = saveTransaction.getResult(); if (result != null) { Throwable t = saveTransaction.getResult().getCatchedThrowable(); if (t != null) { throw new RuntimeException("Saving failed", t); } saveTransaction = null; } unloadedAndSavingChunkMap.clear(); } } private void addGlobalStoreBuilderToSaveTransaction(SaveTransactionBuilder transactionBuilder) { GlobalStoreBuilder globalStoreBuilder = new GlobalStoreBuilder(getEntityManager(), getPrefabSerializer()); transactionBuilder.setGlobalStoreBuilder(globalStoreBuilder); } @Override public void deactivatePlayer(Client client) { EntityRef character = client.getEntity().getComponent(ClientComponent.class).character; PlayerStoreBuilder playerStoreBuilder = createPlayerStore(client, character); EntityData.PlayerStore playerStore = playerStoreBuilder.build(getEntityManager()); deactivateOrDestroyEntityRecursive(character); unloadedAndUnsavedPlayerMap.put(client.getId(), playerStore); } @Override protected EntityData.PlayerStore loadPlayerStoreData(String playerId) { EntityData.PlayerStore disposedUnsavedPlayer = unloadedAndUnsavedPlayerMap.get(playerId); if (disposedUnsavedPlayer != null) { return disposedUnsavedPlayer; } EntityData.PlayerStore disposedSavingPlayer = unloadedAndSavingPlayerMap.get(playerId); if (disposedSavingPlayer != null) { return disposedSavingPlayer; } worldDirectoryReadLock.lock(); try { return super.loadPlayerStoreData(playerId); } finally { worldDirectoryReadLock.unlock(); } } private void addChunksToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder, ChunkProvider chunkProvider) { unloadedAndSavingChunkMap.clear(); /** * New entries might be added concurrently. By using putAll + clear to transfer entries we might loose new * ones added in between putAll and clear. Bz iterating we can make sure that all entires removed * from unloadedAndUnsavedChunkMap get added to unloadedAndSavingChunkMap. */ Iterator<Map.Entry<Vector3i, CompressedChunkBuilder>> unsavedEntryIterator = unloadedAndUnsavedChunkMap.entrySet().iterator(); while (unsavedEntryIterator.hasNext()) { Map.Entry<Vector3i, CompressedChunkBuilder> entry = unsavedEntryIterator.next(); unloadedAndSavingChunkMap.put(entry.getKey(), entry.getValue()); unsavedEntryIterator.remove(); } chunkProvider.getAllChunks().stream().filter(ManagedChunk::isReady).forEach(chunk -> { // If there is a newer undisposed version of the chunk,we don't need to save the disposed version: unloadedAndSavingChunkMap.remove(chunk.getPosition()); ChunkImpl chunkImpl = (ChunkImpl) chunk; // this storage manager can only work with ChunkImpls saveTransactionBuilder.addLoadedChunk(chunk.getPosition(), chunkImpl); }); for (Map.Entry<Vector3i, CompressedChunkBuilder> entry : unloadedAndSavingChunkMap.entrySet()) { saveTransactionBuilder.addUnloadedChunk(entry.getKey(), entry.getValue()); } } @Override public void requestSaving() { this.saveRequested = true; } @Override public void waitForCompletionOfPreviousSaveAndStartSaving() { waitForCompletionOfPreviousSave(); startSaving(); } private void waitForCompletionOfPreviousSave() { if (saveTransaction != null && saveTransaction.getResult() == null) { saveThreadManager.shutdown(new ShutdownTask(), true); saveThreadManager.restart(); } checkSaveTransactionAndClearUpIfItIsDone(); } private SaveTransaction createSaveTransaction() { SaveTransactionBuilder saveTransactionBuilder = new SaveTransactionBuilder(privateEntityManager, entitySetDeltaRecorder, isStoreChunksInZips(), getStoragePathProvider(), worldDirectoryWriteLock); ChunkProvider chunkProvider = CoreRegistry.get(ChunkProvider.class); NetworkSystem networkSystem = CoreRegistry.get(NetworkSystem.class); addChunksToSaveTransaction(saveTransactionBuilder, chunkProvider); addPlayersToSaveTransaction(saveTransactionBuilder, networkSystem); addGlobalStoreBuilderToSaveTransaction(saveTransactionBuilder); addGameManifestToSaveTransaction(saveTransactionBuilder); return saveTransactionBuilder.build(); } private void addPlayersToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder, NetworkSystem networkSystem) { unloadedAndSavingPlayerMap.clear(); /** * New entries might be added concurrently. By using putAll + clear to transfer entries we might loose new * ones added in between putAll and clear. By iterating we can make sure that all entities removed * from unloadedAndUnsavedPlayerMap get added to unloadedAndSavingPlayerMap. */ Iterator<Map.Entry<String, EntityData.PlayerStore>> unsavedEntryIterator = unloadedAndUnsavedPlayerMap.entrySet().iterator(); while (unsavedEntryIterator.hasNext()) { Map.Entry<String, EntityData.PlayerStore> entry = unsavedEntryIterator.next(); unloadedAndSavingPlayerMap.put(entry.getKey(), entry.getValue()); unsavedEntryIterator.remove(); } for (Client client : networkSystem.getPlayers()) { // If there is a newer undisposed version of the player,we don't need to save the disposed version: unloadedAndSavingPlayerMap.remove(client.getId()); EntityRef character = client.getEntity().getComponent(ClientComponent.class).character; saveTransactionBuilder.addLoadedPlayer(client.getId(), createPlayerStore(client, character)); } for (Map.Entry<String, EntityData.PlayerStore> entry : unloadedAndSavingPlayerMap.entrySet()) { saveTransactionBuilder.addUnloadedPlayer(entry.getKey(), entry.getValue()); } } private PlayerStoreBuilder createPlayerStore(Client client, EntityRef character) { LocationComponent location = character.getComponent(LocationComponent.class); Vector3f relevanceLocation; if (location != null) { relevanceLocation = location.getWorldPosition(); } else { relevanceLocation = new Vector3f(); } Long characterId; if (character.exists()) { characterId = character.getId(); } else { characterId = null; } return new PlayerStoreBuilder(characterId, relevanceLocation); } @Override public void deactivateChunk(Chunk chunk) { Collection<EntityRef> entitiesOfChunk = getEntitiesOfChunk(chunk); ChunkImpl chunkImpl = (ChunkImpl) chunk; // storage manager only works with ChunkImpl unloadedAndUnsavedChunkMap.put(chunk.getPosition(), new CompressedChunkBuilder(getEntityManager(), chunkImpl, entitiesOfChunk, true)); entitiesOfChunk.forEach(this::deactivateOrDestroyEntityRecursive); } @Override protected byte[] loadCompressedChunk(Vector3i chunkPos) { CompressedChunkBuilder disposedUnsavedChunk = unloadedAndUnsavedChunkMap.get(chunkPos); if (disposedUnsavedChunk != null) { return disposedUnsavedChunk.buildEncodedChunk(); } CompressedChunkBuilder disposedSavingChunk = unloadedAndSavingChunkMap.get(chunkPos); if (disposedSavingChunk != null) { return disposedSavingChunk.buildEncodedChunk(); } worldDirectoryReadLock.lock(); try { return super.loadCompressedChunk(chunkPos); } finally { worldDirectoryReadLock.unlock(); } } @Override public void onEntityDestroyed(EntityRef entity) { entitySetDeltaRecorder.onEntityDestroyed(entity); } private void addGameManifestToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder) { BlockManager blockManager = CoreRegistry.get(BlockManager.class); BiomeManager biomeManager = CoreRegistry.get(BiomeManager.class); WorldProvider worldProvider = CoreRegistry.get(WorldProvider.class); Time time = CoreRegistry.get(Time.class); Game game = CoreRegistry.get(Game.class); GameManifest gameManifest = new GameManifest(game.getName(), game.getSeed(), time.getGameTimeInMs()); for (Module module : CoreRegistry.get(ModuleManager.class).getEnvironment()) { gameManifest.addModule(module.getId(), module.getVersion()); } List<String> registeredBlockFamilies = Lists.newArrayList(); for (BlockFamily family : blockManager.listRegisteredBlockFamilies()) { registeredBlockFamilies.add(family.getURI().toString()); } gameManifest.setRegisteredBlockFamilies(registeredBlockFamilies); gameManifest.setBlockIdMap(blockManager.getBlockIdMap()); List<Biome> biomes = biomeManager.getBiomes(); Map<String, Short> biomeIdMap = new HashMap<>(biomes.size()); for (Biome biome : biomes) { short shortId = biomeManager.getBiomeShortId(biome); String id = biomeManager.getBiomeId(biome); biomeIdMap.put(id, shortId); } gameManifest.setBiomeIdMap(biomeIdMap); gameManifest.addWorld(worldProvider.getWorldInfo()); saveTransactionBuilder.setGameManifest(gameManifest); } @Override public void update() { if (!isRunModeAllowSaving()) { return; } if (isSaving()) { return; } checkSaveTransactionAndClearUpIfItIsDone(); if (saveRequested || isSavingNecessary()) { startSaving(); } } private boolean isRunModeAllowSaving() { NetworkSystem networkSystem = CoreRegistry.get(NetworkSystem.class); return networkSystem.getMode().isAuthority(); } private void startSaving() { logger.info("Saving - Creating game snapshot"); PerformanceMonitor.startActivity("Auto Saving"); ComponentSystemManager componentSystemManager = CoreRegistry.get(ComponentSystemManager.class); for (ComponentSystem sys : componentSystemManager.iterateAll()) { sys.preSave(); } saveRequested = false; saveTransaction = createSaveTransaction(); saveThreadManager.offer(saveTransaction); for (ComponentSystem sys : componentSystemManager.iterateAll()) { sys.postSave(); } scheduleNextAutoSave(); PerformanceMonitor.endActivity(); entitySetDeltaRecorder = new EntitySetDeltaRecorder(this.entityRefReplacingComponentLibrary); logger.info("Saving - Snapshot created: Writing phase starts"); } private boolean isSavingNecessary() { ChunkProvider chunkProvider = CoreRegistry.get(ChunkProvider.class); int unloadedChunkCount = unloadedAndUnsavedChunkMap.size(); int loadedChunkCount = chunkProvider.getAllChunks().size(); double totalChunkCount = unloadedChunkCount + loadedChunkCount; double percentageUnloaded = 100.0 * unloadedChunkCount / totalChunkCount; if (percentageUnloaded >= config.getSystem().getMaxUnloadedChunksPercentageTillSave()) { return true; } long currentTime = System.currentTimeMillis(); if (nextAutoSave == null) { scheduleNextAutoSave(); return false; } return currentTime >= nextAutoSave; } private void scheduleNextAutoSave() { long msBetweenAutoSave = config.getSystem().getMaxSecondsBetweenSaves() * 1000; nextAutoSave = System.currentTimeMillis() + msBetweenAutoSave; } @Override public boolean isSaving() { return saveTransaction != null && saveTransaction.getResult() == null; } @Override public void checkAndRepairSaveIfNecessary() throws IOException { saveTransactionHelper.cleanupSaveTransactionDirectory(); if (Files.exists(getStoragePathProvider().getUnmergedChangesPath())) { saveTransactionHelper.mergeChanges(); } } @Override public void deleteWorld() { waitForCompletionOfPreviousSave(); unloadedAndUnsavedChunkMap.clear(); unloadedAndSavingChunkMap.clear(); unloadedAndUnsavedPlayerMap.clear(); unloadedAndSavingPlayerMap.clear(); try { FilesUtil.recursiveDelete(getStoragePathProvider().getWorldPath()); } catch (IOException e) { logger.error("Failed to purge chunks", e); } } @Override public void onEntityComponentAdded(EntityRef entity, Class<? extends Component> component) { entitySetDeltaRecorder.onEntityComponentAdded(entity, component); } @Override public void onEntityComponentChange(EntityRef entity, Class<? extends Component> component) { entitySetDeltaRecorder.onEntityComponentChange(entity, component); } @Override public void onEntityComponentRemoved(EntityRef entity, Class<? extends Component> component) { entitySetDeltaRecorder.onEntityComponentRemoved(entity, component); } @Override public void onReactivation(EntityRef entity, Collection<Component> components) { entitySetDeltaRecorder.onReactivation(entity, components); } @Override public void onBeforeDeactivation(EntityRef entity, Collection<Component> components) { entitySetDeltaRecorder.onBeforeDeactivation(entity, components); } @Override public DelayedEntityRef createDelayedEntityRef(long id) { DelayedEntityRef delayedEntityRef = new DelayedEntityRef(id); entitySetDeltaRecorder.registerDelayedEntityRef(delayedEntityRef); return delayedEntityRef; } }