forked from Qortal/qortal
Track the storage capacity and the total data/temp directory sizes
Nodes will stop proactively storing new data when they reach 90% capacity. A new "maxStorageCapacity" setting has been added to allow the user to optionally limit the allocated space for this node. Limits are approximate only, not exact.
This commit is contained in:
parent
ae0f01d326
commit
8bf7daff65
@ -54,6 +54,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
|||||||
import org.qortal.controller.arbitrary.ArbitraryDataCleanupManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataCleanupManager;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||||
|
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||||
import org.qortal.controller.repository.PruneManager;
|
import org.qortal.controller.repository.PruneManager;
|
||||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
@ -490,6 +491,7 @@ public class Controller extends Thread {
|
|||||||
ArbitraryDataManager.getInstance().start();
|
ArbitraryDataManager.getInstance().start();
|
||||||
ArbitraryDataBuildManager.getInstance().start();
|
ArbitraryDataBuildManager.getInstance().start();
|
||||||
ArbitraryDataCleanupManager.getInstance().start();
|
ArbitraryDataCleanupManager.getInstance().start();
|
||||||
|
ArbitraryDataStorageManager.getInstance().start();
|
||||||
|
|
||||||
// Auto-update service?
|
// Auto-update service?
|
||||||
if (Settings.getInstance().isAutoUpdateEnabled()) {
|
if (Settings.getInstance().isAutoUpdateEnabled()) {
|
||||||
@ -1080,6 +1082,7 @@ public class Controller extends Thread {
|
|||||||
ArbitraryDataManager.getInstance().shutdown();
|
ArbitraryDataManager.getInstance().shutdown();
|
||||||
ArbitraryDataBuildManager.getInstance().shutdown();
|
ArbitraryDataBuildManager.getInstance().shutdown();
|
||||||
ArbitraryDataCleanupManager.getInstance().shutdown();
|
ArbitraryDataCleanupManager.getInstance().shutdown();
|
||||||
|
ArbitraryDataStorageManager.getInstance().shutdown();
|
||||||
|
|
||||||
if (blockMinter != null) {
|
if (blockMinter != null) {
|
||||||
LOGGER.info("Shutting down block minter");
|
LOGGER.info("Shutting down block minter");
|
||||||
|
@ -76,6 +76,11 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait until storage capacity has been calculated
|
||||||
|
if (!storageManager.isStorageCapacityCalculated()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Periodically delete any unnecessary files from the temp directory
|
// Periodically delete any unnecessary files from the temp directory
|
||||||
if (offset == 0 || offset % (limit * 10) == 0) {
|
if (offset == 0 || offset % (limit * 10) == 0) {
|
||||||
this.cleanupTempDirectory(now);
|
this.cleanupTempDirectory(now);
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
package org.qortal.controller.arbitrary;
|
package org.qortal.controller.arbitrary;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.list.ResourceListManager;
|
import org.qortal.list.ResourceListManager;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.utils.FilesystemUtils;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class ArbitraryDataStorageManager {
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public class ArbitraryDataStorageManager extends Thread {
|
||||||
|
|
||||||
public enum StoragePolicy {
|
public enum StoragePolicy {
|
||||||
FOLLOWED_AND_VIEWED,
|
FOLLOWED_AND_VIEWED,
|
||||||
@ -14,7 +24,21 @@ public class ArbitraryDataStorageManager {
|
|||||||
NONE
|
NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataStorageManager.class);
|
||||||
|
|
||||||
private static ArbitraryDataStorageManager instance;
|
private static ArbitraryDataStorageManager instance;
|
||||||
|
private volatile boolean isStopping = false;
|
||||||
|
|
||||||
|
private Long storageCapacity = null;
|
||||||
|
private long totalDirectorySize = 0L;
|
||||||
|
private long lastDirectorySizeCheck = 0;
|
||||||
|
|
||||||
|
private static long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||||
|
|
||||||
|
/** Treat storage as full at 90% usage, to reduce risk of going over the limit.
|
||||||
|
* This is necessary because we don't calculate total storage values before every write.
|
||||||
|
* It also helps avoid a fetch/delete loop, as we will stop fetching before the hard limit. */
|
||||||
|
private static double STORAGE_FULL_THRESHOLD = 0.9; // 90%
|
||||||
|
|
||||||
public ArbitraryDataStorageManager() {
|
public ArbitraryDataStorageManager() {
|
||||||
}
|
}
|
||||||
@ -26,6 +50,42 @@ public class ArbitraryDataStorageManager {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Arbitrary Data Storage Manager");
|
||||||
|
try {
|
||||||
|
while (!isStopping) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the total directory size if we haven't in a while
|
||||||
|
if (this.shouldCalculateDirectorySize(now)) {
|
||||||
|
this.calculateDirectorySize(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(59000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Fall-through to exit thread...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
isStopping = true;
|
||||||
|
this.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data relating to a transaction is allowed to
|
||||||
|
* exist on this node, therefore making it a mirror for this data.
|
||||||
|
*
|
||||||
|
* @param arbitraryTransactionData - the transaction
|
||||||
|
* @return boolean - whether to prefetch or not
|
||||||
|
*/
|
||||||
public boolean canStoreData(ArbitraryTransactionData arbitraryTransactionData) {
|
public boolean canStoreData(ArbitraryTransactionData arbitraryTransactionData) {
|
||||||
String name = arbitraryTransactionData.getName();
|
String name = arbitraryTransactionData.getName();
|
||||||
|
|
||||||
@ -34,6 +94,8 @@ public class ArbitraryDataStorageManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't check for storage limits here, as it can cause the cleanup manager to delete existing data
|
||||||
|
|
||||||
// Check if our storage policy and blacklist allows us to host data for this name
|
// Check if our storage policy and blacklist allows us to host data for this name
|
||||||
switch (Settings.getInstance().getStoragePolicy()) {
|
switch (Settings.getInstance().getStoragePolicy()) {
|
||||||
case FOLLOWED_AND_VIEWED:
|
case FOLLOWED_AND_VIEWED:
|
||||||
@ -53,20 +115,38 @@ public class ArbitraryDataStorageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data relating to a transaction should be downloaded
|
||||||
|
* automatically, making this node a mirror for that data.
|
||||||
|
*
|
||||||
|
* @param arbitraryTransactionData - the transaction
|
||||||
|
* @return boolean - whether to prefetch or not
|
||||||
|
*/
|
||||||
public boolean shouldPreFetchData(ArbitraryTransactionData arbitraryTransactionData) {
|
public boolean shouldPreFetchData(ArbitraryTransactionData arbitraryTransactionData) {
|
||||||
String name = arbitraryTransactionData.getName();
|
String name = arbitraryTransactionData.getName();
|
||||||
if (name == null) {
|
|
||||||
return this.shouldPreFetchDataWithoutName(arbitraryTransactionData);
|
// Don't fetch anything more if we're (nearly) out of space
|
||||||
}
|
// Make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
|
||||||
// Never fetch data from blacklisted names, even if they are followed
|
// avoid a fetch/delete loop
|
||||||
if (this.isNameInBlacklist(name)) {
|
if (!this.isStorageSpaceAvailable(STORAGE_FULL_THRESHOLD)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't store data unless it's an allowed type (public/private)
|
// Don't store data unless it's an allowed type (public/private)
|
||||||
if (!this.isDataTypeAllowed(arbitraryTransactionData)) {
|
if (!this.isDataTypeAllowed(arbitraryTransactionData)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle transactions without names differently
|
||||||
|
if (name == null) {
|
||||||
|
return this.shouldPreFetchDataWithoutName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never fetch data from blacklisted names, even if they are followed
|
||||||
|
if (this.isNameInBlacklist(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (Settings.getInstance().getStoragePolicy()) {
|
switch (Settings.getInstance().getStoragePolicy()) {
|
||||||
case FOLLOWED:
|
case FOLLOWED:
|
||||||
case FOLLOWED_AND_VIEWED:
|
case FOLLOWED_AND_VIEWED:
|
||||||
@ -82,10 +162,17 @@ public class ArbitraryDataStorageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldPreFetchDataWithoutName(ArbitraryTransactionData arbitraryTransactionData) {
|
/**
|
||||||
|
* Don't call this method directly.
|
||||||
|
* Use the wrapper method shouldPreFetchData() instead, as it contains
|
||||||
|
* additional checks.
|
||||||
|
*
|
||||||
|
* @return boolean - whether the storage policy allows for unnamed data
|
||||||
|
*/
|
||||||
|
private boolean shouldPreFetchDataWithoutName() {
|
||||||
switch (Settings.getInstance().getStoragePolicy()) {
|
switch (Settings.getInstance().getStoragePolicy()) {
|
||||||
case ALL:
|
case ALL:
|
||||||
return this.isDataTypeAllowed(arbitraryTransactionData);
|
return true;
|
||||||
|
|
||||||
case NONE:
|
case NONE:
|
||||||
case VIEWED:
|
case VIEWED:
|
||||||
@ -118,4 +205,105 @@ public class ArbitraryDataStorageManager {
|
|||||||
private boolean isFollowingName(String name) {
|
private boolean isFollowingName(String name) {
|
||||||
return ResourceListManager.getInstance().listContains("followed", "names", name, false);
|
return ResourceListManager.getInstance().listContains("followed", "names", name, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Size limits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limit to reduce IO load
|
||||||
|
*/
|
||||||
|
private boolean shouldCalculateDirectorySize(Long now) {
|
||||||
|
if (now == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If storage capacity is null, we need to calculate it
|
||||||
|
if (this.storageCapacity == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If we haven't checked for a while, we need to check it now
|
||||||
|
if (now - lastDirectorySizeCheck > DIRECTORY_SIZE_CHECK_INTERVAL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We shouldn't check this time, as we want to reduce IO load on the SSD/HDD
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateDirectorySize(Long now) {
|
||||||
|
if (now == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalSize = 0;
|
||||||
|
long remainingCapacity = 0;
|
||||||
|
|
||||||
|
// Calculate remaining capacity
|
||||||
|
try {
|
||||||
|
remainingCapacity = this.getRemainingUsableStorageCapacity();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Unable to calculate remaining storage capacity: {}", e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total size of data directory
|
||||||
|
LOGGER.trace("Calculating data directory size...");
|
||||||
|
Path dataDirectoryPath = Paths.get(Settings.getInstance().getDataPath());
|
||||||
|
if (dataDirectoryPath.toFile().exists()) {
|
||||||
|
totalSize += FileUtils.sizeOfDirectory(dataDirectoryPath.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total size of temp directory, if it's not already inside the data directory
|
||||||
|
Path tempDirectoryPath = Paths.get(Settings.getInstance().getTempDataPath());
|
||||||
|
if (tempDirectoryPath.toFile().exists()) {
|
||||||
|
if (!FilesystemUtils.isChild(tempDirectoryPath, dataDirectoryPath)) {
|
||||||
|
LOGGER.trace("Calculating temp directory size...");
|
||||||
|
totalSize += FileUtils.sizeOfDirectory(dataDirectoryPath.toFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalDirectorySize = totalSize;
|
||||||
|
this.lastDirectorySizeCheck = now;
|
||||||
|
|
||||||
|
// It's essential that used space (this.totalDirectorySize) is included in the storage capacity
|
||||||
|
LOGGER.trace("Calculating total storage capacity...");
|
||||||
|
long storageCapacity = remainingCapacity + this.totalDirectorySize;
|
||||||
|
|
||||||
|
// Make sure to limit the storage capacity if the user is overriding it in the settings
|
||||||
|
if (Settings.getInstance().getMaxStorageCapacity() != null) {
|
||||||
|
storageCapacity = Math.min(storageCapacity, Settings.getInstance().getMaxStorageCapacity());
|
||||||
|
}
|
||||||
|
this.storageCapacity = storageCapacity;
|
||||||
|
|
||||||
|
LOGGER.info("Total used: {} bytes, Total capacity: {} bytes", this.totalDirectorySize, this.storageCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getRemainingUsableStorageCapacity() throws IOException {
|
||||||
|
// Create data directory if it doesn't exist so that we can perform calculations on it
|
||||||
|
Path dataDirectoryPath = Paths.get(Settings.getInstance().getDataPath());
|
||||||
|
if (!dataDirectoryPath.toFile().exists()) {
|
||||||
|
Files.createDirectories(dataDirectoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataDirectoryPath.toFile().getUsableSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalDirectorySize() {
|
||||||
|
return this.totalDirectorySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStorageSpaceAvailable(double threshold) {
|
||||||
|
if (!this.isStorageCapacityCalculated()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long maxStorageCapacity = (long)((double)this.storageCapacity / 100.0f * threshold);
|
||||||
|
if (this.totalDirectorySize >= maxStorageCapacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStorageCapacityCalculated() {
|
||||||
|
return (this.storageCapacity != null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,6 +289,8 @@ public class Settings {
|
|||||||
/** Whether to allow private (non-decryptable) data to be stored */
|
/** Whether to allow private (non-decryptable) data to be stored */
|
||||||
private boolean privateDataEnabled = false;
|
private boolean privateDataEnabled = false;
|
||||||
|
|
||||||
|
/** Maximum total size of hosted data, in bytes. Unlimited if null */
|
||||||
|
private Long maxStorageCapacity = null;
|
||||||
|
|
||||||
// Domain mapping
|
// Domain mapping
|
||||||
public static class DomainMap {
|
public static class DomainMap {
|
||||||
@ -840,4 +842,8 @@ public class Settings {
|
|||||||
public boolean isPrivateDataEnabled() {
|
public boolean isPrivateDataEnabled() {
|
||||||
return this.privateDataEnabled;
|
return this.privateDataEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getMaxStorageCapacity() {
|
||||||
|
return this.maxStorageCapacity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,6 +181,10 @@ public class FilesystemUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isChild(Path child, Path parent) {
|
||||||
|
return child.toAbsolutePath().startsWith(parent.toAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
public static long getDirectorySize(Path path) throws IOException {
|
public static long getDirectorySize(Path path) throws IOException {
|
||||||
if (path == null || !Files.exists(path)) {
|
if (path == null || !Files.exists(path)) {
|
||||||
return 0L;
|
return 0L;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user