diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtils.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtils.java index 58ad240b6800d2aea2ac0f9f6e1a3b3d3c378e81..a1ebf600348ca7d454bf390c4ad57e8052aa6f7d 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtils.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtils.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,6 +35,7 @@ import org.apache.commons.io.FileUtils; import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel; import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked; import ch.systemsx.cisd.base.utilities.OSUtilities; +import ch.systemsx.cisd.common.collection.CollectionUtils; import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; import ch.systemsx.cisd.common.exceptions.UserFailureException; @@ -53,6 +55,7 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService; import ch.systemsx.cisd.openbis.dss.generic.shared.IShareIdManager; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IDatasetLocation; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; import ch.systemsx.cisd.openbis.generic.shared.dto.SimpleDataSetInformationDTO; /** @@ -84,6 +87,15 @@ public class SegmentedStoreUtils } }; + private static final Comparator<SimpleDataSetInformationDTO> MODIFICATION_TIMESTAMP_COMPARATOR = new Comparator<SimpleDataSetInformationDTO>() + { + @Override + public int compare(SimpleDataSetInformationDTO d1, SimpleDataSetInformationDTO d2) + { + return d1.getModificationTimestamp().compareTo(d2.getModificationTimestamp()); + } + }; + private static final FileFilter FILTER_ON_SHARES = new FileFilter() { @Override @@ -204,6 +216,107 @@ public class SegmentedStoreUtils (System.currentTimeMillis() - start) / 1000.0)); return shares; } + + /** + * Frees space in specified share for unarchived data sets. This method assumes that the size of + * all specified data sets are known by the {@link DatasetDescription} objects. Data sets with oldest + * modification date are removed first. The archiving status of these data sets are set back to ARCHIVED. + * + * @param dataSets The data sets which should be kept (if already in the specified share). In addition + * they specify the amount spaced to be freed. + */ + public static void freeSpace(Share unarchivingScratchShare, IEncapsulatedOpenBISService service, + List<DatasetDescription> dataSets, IDataSetDirectoryProvider dataSetDirectoryProvider, + IShareIdManager shareIdManager, ISimpleLogger logger) + { + long requestedSpace = calculateTotalSizeOfDataSetsToKeep(dataSets); + long actualFreeSpace = unarchivingScratchShare.calculateFreeSpace(); + if (actualFreeSpace > requestedSpace) + { + return; + } + List<SimpleDataSetInformationDTO> dataSetsInShare = listRemovableDataSets(unarchivingScratchShare, dataSets); + Collections.sort(dataSetsInShare, MODIFICATION_TIMESTAMP_COMPARATOR); + List<SimpleDataSetInformationDTO> dataSetsToRemoveFromShare = + listDataSetsToRemoveFromShare(dataSetsInShare, requestedSpace, actualFreeSpace, + unarchivingScratchShare, logger); + service.archiveDataSets(extractCodes(dataSetsToRemoveFromShare), false); + for (SimpleDataSetInformationDTO dataSet : dataSetsToRemoveFromShare) + { + deleteDataSet(dataSet, dataSetDirectoryProvider, shareIdManager, logger); + } + logger.log(LogLevel.INFO, "The following data sets have been successfully removed from share '" + + unarchivingScratchShare.getShareId() + "' and their archiving status has been successfully " + + "set back to ARCHIVED: " + CollectionUtils.abbreviate(extractCodes(dataSetsToRemoveFromShare), 10)); + } + + private static long calculateTotalSizeOfDataSetsToKeep(List<DatasetDescription> dataSets) + { + long size = 0; + for (DatasetDescription dataSet : dataSets) + { + Long dataSetSize = dataSet.getDataSetSize(); + if (dataSetSize == null) + { + throw new IllegalArgumentException("Unknown size of data set '" + dataSet.getDataSetCode() + "'."); + } + size += dataSetSize; + } + return size; + } + + private static List<SimpleDataSetInformationDTO> listRemovableDataSets(Share share, + List<DatasetDescription> dataSets) + { + Set<String> dataSetsToBeKept = new HashSet<String>(); + for (DatasetDescription dataSet : dataSets) + { + dataSetsToBeKept.add(dataSet.getDataSetCode()); + } + List<SimpleDataSetInformationDTO> dataSetsInShare = new ArrayList<SimpleDataSetInformationDTO>(); + for (SimpleDataSetInformationDTO dataSetInShare : share.getDataSetsOrderedBySize()) + { + if (dataSetsToBeKept.contains(dataSetInShare.getDataStoreCode()) == false) + { + dataSetsInShare.add(dataSetInShare); + } + } + return dataSetsInShare; + } + + private static List<String> extractCodes(List<SimpleDataSetInformationDTO> dataSets) + { + List<String> codes = new ArrayList<String>(); + for (SimpleDataSetInformationDTO dataSet : dataSets) + { + codes.add(dataSet.getDataSetCode()); + } + return codes; + } + + private static List<SimpleDataSetInformationDTO> listDataSetsToRemoveFromShare( + List<SimpleDataSetInformationDTO> dataSetsInShare, + long requestedSpace, long actualFreeSpace, Share share, ISimpleLogger logger) + { + long freeSpace = actualFreeSpace; + List<SimpleDataSetInformationDTO> dataSetsToRemoveFromShare = new ArrayList<SimpleDataSetInformationDTO>(); + for (int i = 0, n = dataSetsInShare.size(); i < n && requestedSpace >= freeSpace; i++) + { + SimpleDataSetInformationDTO dataSetInShare = dataSetsInShare.get(i); + freeSpace += dataSetInShare.getDataSetSize(); + dataSetsToRemoveFromShare.add(dataSetInShare); + } + logger.log(LogLevel.INFO, "Remove the following data sets from share '" + share.getShareId() + + "' and set their archiving status back to ARCHIVED: " + + CollectionUtils.abbreviate(extractCodes(dataSetsToRemoveFromShare), 10)); + if (requestedSpace >= freeSpace) + { + throw new EnvironmentFailureException("After removing all removable data sets from share '" + + share.getShareId() + "' there is still only " + FileUtilities.byteCountToDisplaySize(freeSpace) + + " free space less than the requested " + FileUtilities.byteCountToDisplaySize(requestedSpace) + "."); + } + return dataSetsToRemoveFromShare; + } static List<Share> getSharesWithDataSets(File storeRoot, String dataStoreCode, boolean filterOutToBeIgnoredForShuffling, IFreeSpaceProvider freeSpaceProvider, diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtilsTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtilsTest.java index d6ec4854e5dbb53eba511a13b63e93ef706f1fde..d29d9411febd10f480a76b06b55f5c0e0922c71a 100644 --- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtilsTest.java +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/SegmentedStoreUtilsTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; import org.apache.commons.io.FileUtils; @@ -48,6 +49,7 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.ProxyShareIdManager; import ch.systemsx.cisd.openbis.generic.shared.Constants; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.PhysicalDataSet; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IDatasetLocation; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatasetDescription; import ch.systemsx.cisd.openbis.generic.shared.dto.SimpleDataSetInformationDTO; /** @@ -84,6 +86,10 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase private File store; + private int modificationTimestamp; + + private File shareFolder; + @BeforeMethod public void beforeMethod() { @@ -106,6 +112,7 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase log = new MockLogger(); store = new File(workingDirectory, "store"); store.mkdirs(); + shareFolder = new File(store, "1"); new File(store, "blabla").mkdirs(); new File(store, "error").mkdirs(); } @@ -123,6 +130,85 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase } } + @Test + public void testFreeSpace() + { + SimpleDataSetInformationDTO ds1 = dataSet(1, 10 * FileUtils.ONE_KB); + SimpleDataSetInformationDTO ds2 = dataSet(2, 10 * FileUtils.ONE_KB); + SimpleDataSetInformationDTO ds3 = dataSet(3, 12 * FileUtils.ONE_KB); + SimpleDataSetInformationDTO ds4 = dataSet(4, 11 * FileUtils.ONE_KB); + SimpleDataSetInformationDTO ds5 = dataSet(5, 14 * FileUtils.ONE_KB); + Share share = new Share(shareFolder, 0, freeSpaceProvider); + share.addDataSet(ds5); + share.addDataSet(ds3); + share.addDataSet(ds1); + RecordingMatcher<HostAwareFile> recordingFileMatcher = prepareFreeSpace(12L); + prepareSetArchingStatus(ds1); + File file = prepareDeleteFromShare(ds1); + assertEquals(true, file.exists()); + + SegmentedStoreUtils.freeSpace(share, service, asDatasetDescriptions(ds2, ds4), dataSetDirectoryProvider, + shareIdManager, log); + + assertEquals(false, file.exists()); + assertEquals(shareFolder.getPath(), recordingFileMatcher.recordedObject().getPath()); + assertEquals("INFO: Remove the following data sets from share '1' and set their archiving status " + + "back to ARCHIVED: [ds-1]\n" + + "INFO: Await for data set ds-1 to be unlocked.\n" + + "INFO: Start deleting data set ds-1 at " + shareFolder + "/abc/ds-1\n" + + "INFO: Data set ds-1 at " + shareFolder + "/abc/ds-1 has been successfully deleted.\n" + + "INFO: The following data sets have been successfully removed from share '1' " + + "and their archiving status has been successfully set back to ARCHIVED: [ds-1]\n", log.toString()); + } + + private File prepareDeleteFromShare(final SimpleDataSetInformationDTO dataSet) + { + final File file = new File(shareFolder, dataSet.getDataSetLocation()); + context.checking(new Expectations() + { + { + one(shareIdManager).await(dataSet.getDataSetCode()); + one(dataSetDirectoryProvider).getDataSetDirectory(dataSet); + will(returnValue(file)); + } + }); + return file; + } + + private void prepareSetArchingStatus(final SimpleDataSetInformationDTO... dataSets) + { + context.checking(new Expectations() + { + { + List<String> dataSetCodes = new ArrayList<String>(); + for (SimpleDataSetInformationDTO dataSet : dataSets) + { + dataSetCodes.add(dataSet.getDataSetCode()); + } + one(service).archiveDataSets(dataSetCodes, false); + } + }); + } + + private RecordingMatcher<HostAwareFile> prepareFreeSpace(final long freeSpace) + { + final RecordingMatcher<HostAwareFile> recorder = new RecordingMatcher<HostAwareFile>(); + context.checking(new Expectations() + { + { + try + { + one(freeSpaceProvider).freeSpaceKb(with(recorder)); + will(returnValue(freeSpace)); + } catch (IOException ex) + { + ex.printStackTrace(); + } + } + }); + return recorder; + } + @Test public void testGetDataSetsPerShare() { @@ -482,13 +568,13 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase share1.mkdirs(); FileUtilities.writeToFile(new File(share1, "share.properties"), ShareFactory.IGNORED_FOR_SHUFFLING_PROP + " = true"); - + String share = SegmentedStoreUtils.findIncomingShare(incomingFolder, store, log); - + assertEquals("1", share); assertEquals("", log.toString()); } - + @Test public void testGetSharesFilterOutIgnoredForShuffling() { @@ -498,15 +584,15 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase ShareFactory.IGNORED_FOR_SHUFFLING_PROP + " = true"); File share2 = new File(store, "2"); share2.mkdirs(); - + List<Share> shares = SegmentedStoreUtils.getSharesWithDataSets(store, DATA_STORE_CODE, true, freeSpaceProvider, service, log, timeProvider); - + assertEquals("2", shares.get(0).getShareId()); assertEquals(1, shares.size()); } - + @Test public void testGetAllShares() { @@ -516,11 +602,11 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase ShareFactory.IGNORED_FOR_SHUFFLING_PROP + " = true"); File share2 = new File(store, "2"); share2.mkdirs(); - + List<Share> shares = SegmentedStoreUtils.getSharesWithDataSets(store, DATA_STORE_CODE, false, freeSpaceProvider, service, log, timeProvider); - + assertEquals("1", shares.get(0).getShareId()); assertEquals("2", shares.get(1).getShareId()); assertEquals(2, shares.size()); @@ -551,6 +637,27 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase assertEquals(Arrays.asList(names).toString(), actualNames.toString()); } + private List<DatasetDescription> asDatasetDescriptions(SimpleDataSetInformationDTO... dataSets) + { + List<DatasetDescription> result = new ArrayList<DatasetDescription>(); + for (SimpleDataSetInformationDTO dataSet : dataSets) + { + DatasetDescription datasetDescription = new DatasetDescription(); + datasetDescription.setDataSetCode(dataSet.getDataSetCode()); + datasetDescription.setDataSetSize(dataSet.getDataSetSize()); + result.add(datasetDescription); + } + return result; + } + + private SimpleDataSetInformationDTO dataSet(int id, long size) + { + File dsFile = new File(shareFolder, "abc/ds-" + id); + dsFile.mkdirs(); + FileUtilities.writeToFile(new File(dsFile, "read.me"), id + " nice works!"); + return dataSet(dsFile, DATA_STORE_CODE, size); + } + private SimpleDataSetInformationDTO dataSet(File dataSetFile, String dataStoreCode, Long size) { SimpleDataSetInformationDTO dataSet = new SimpleDataSetInformationDTO(); @@ -561,6 +668,7 @@ public class SegmentedStoreUtilsTest extends AbstractFileSystemTestCase dataSet.setDataSetShareId(path.substring(0, indexOfFirstSeparator)); dataSet.setDataSetLocation(path.substring(indexOfFirstSeparator + 1)); dataSet.setDataSetSize(size); + dataSet.setModificationTimestamp(new Date(modificationTimestamp += 10000)); return dataSet; } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SimpleDataSetInformationDTO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SimpleDataSetInformationDTO.java index f100956f7aae734f4bae7310410f577aeb95743d..9a1aad195b4734b379989bba9c6534adf981a236 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SimpleDataSetInformationDTO.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SimpleDataSetInformationDTO.java @@ -42,6 +42,8 @@ public class SimpleDataSetInformationDTO implements Serializable, IDatasetLocati private String dataSetLocation; private Date registrationTimestamp; + + private Date modificationTimestamp; private Long dataSetSize; @@ -136,6 +138,16 @@ public class SimpleDataSetInformationDTO implements Serializable, IDatasetLocati this.registrationTimestamp = registrationTimestamp; } + public Date getModificationTimestamp() + { + return modificationTimestamp; + } + + public void setModificationTimestamp(Date modificationTimestamp) + { + this.modificationTimestamp = modificationTimestamp; + } + public int getSpeedHint() { return speedHint; diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SimpleDataSetHelper.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SimpleDataSetHelper.java index 984218728e32c0199bcc493e0dc244f0eff77933..2e603cceee449fde165d59b89a7ab19e40161e69 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SimpleDataSetHelper.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/SimpleDataSetHelper.java @@ -60,6 +60,7 @@ public class SimpleDataSetHelper result.setDataStoreCode(data.getDataStore().getCode()); result.setDataSetCode(data.getCode()); result.setRegistrationTimestamp(data.getRegistrationDate()); + result.setModificationTimestamp(data.getModificationDate()); result.setSpeedHint(data.getSpeedHint()); result.setDataSetShareId(data.getShareId()); result.setDataSetLocation(data.getLocation());