WICKET-4478 DiskDataStore to use multi-level directory structure to avoid slowness when thousands of sessions are active.
Project: http://git-wip-us.apache.org/repos/asf/wicket/repo Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/33f57f08 Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/33f57f08 Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/33f57f08 Branch: refs/heads/wicket-1.5.x Commit: 33f57f08944867a619a6b6526e711cf43954e2e8 Parents: d9c48eb Author: Martin Tzvetanov Grigorov <[email protected]> Authored: Thu Mar 29 12:11:14 2012 +0200 Committer: Martin Tzvetanov Grigorov <[email protected]> Committed: Thu Mar 29 12:23:13 2012 +0200 ---------------------------------------------------------------------- .../org/apache/wicket/pageStore/DiskDataStore.java | 55 ++ .../page/persistent/disk/DiskDataStoreTest.java | 383 -------------- .../apache/wicket/pageStore/DiskDataStoreTest.java | 404 +++++++++++++++ 3 files changed, 459 insertions(+), 383 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/wicket/blob/33f57f08/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java index e4bcb01..d5d9b01 100644 --- a/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java +++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java @@ -449,9 +449,33 @@ public class DiskDataStore implements IDataStore if (sessionFolder.exists()) { Files.removeFolder(sessionFolder); + cleanup(sessionFolder); } unbound = true; } + + /** + * deletes the sessionFolder's parent and grandparent, if (and only if) they are empty. + * + * @see #createPathFrom(String sessionId) + * @param sessionFolder + * must not be null + */ + private void cleanup(final File sessionFolder) + { + File high = sessionFolder.getParentFile(); + if (high.list().length == 0) + { + if (Files.removeFolder(high)) + { + File low = high.getParentFile(); + if (low.list().length == 0) + { + Files.removeFolder(low); + } + } + } + } } /** @@ -494,6 +518,8 @@ public class DiskDataStore implements IDataStore sessionId = sessionId.replace('/', '_'); sessionId = sessionId.replace(':', '_'); + sessionId = createPathFrom(sessionId); + File sessionFolder = new File(storeFolder, sessionId); if (create && sessionFolder.exists() == false) { @@ -502,4 +528,33 @@ public class DiskDataStore implements IDataStore return sessionFolder; } + /** + * creates a three-level path from the sessionId in the format 0000/0000/<sessionId>. The two + * prefixing directories are created from the sesionId's hascode and thus, should be well + * distributed. + * + * This is used to avoid problems with Filesystems allowing no more than 32k entries in a + * directory. + * + * Note that the prefix paths are created from Integers and not guaranteed to be four chars + * long. + * + * @param sessionId + * must not be null + * @return path in the form 0000/0000/sessionId + */ + private String createPathFrom(final String sessionId) + { + int hash = Math.abs(sessionId.hashCode()); + String low = String.valueOf(hash % 9973); + String high = String.valueOf((hash / 9973) % 9973); + StringBuilder bs = new StringBuilder(sessionId.length() + 10); + bs.append(low); + bs.append(File.separator); + bs.append(high); + bs.append(File.separator); + bs.append(sessionId); + + return bs.toString(); + } } http://git-wip-us.apache.org/repos/asf/wicket/blob/33f57f08/wicket-core/src/test/java/org/apache/wicket/page/persistent/disk/DiskDataStoreTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/page/persistent/disk/DiskDataStoreTest.java b/wicket-core/src/test/java/org/apache/wicket/page/persistent/disk/DiskDataStoreTest.java deleted file mode 100644 index ed6f1e3..0000000 --- a/wicket-core/src/test/java/org/apache/wicket/page/persistent/disk/DiskDataStoreTest.java +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.wicket.page.persistent.disk; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.wicket.pageStore.AsynchronousDataStore; -import org.apache.wicket.pageStore.DiskDataStore; -import org.apache.wicket.pageStore.IDataStore; -import org.apache.wicket.settings.IStoreSettings; -import org.apache.wicket.settings.def.StoreSettings; -import org.apache.wicket.util.lang.Bytes; -import org.junit.Assert; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - */ -public class DiskDataStoreTest extends Assert -{ - /** Log for reporting. */ - private static final Logger log = LoggerFactory.getLogger(DiskDataStoreTest.class); - - /** - * Construct. - */ - public DiskDataStoreTest() - { - } - - private static final Random random = new Random(); - private static final int FILE_SIZE_MIN = 1024 * 200; - private static final int FILE_SIZE_MAX = 1024 * 300; - private static final Bytes MAX_SIZE_PER_SESSION = Bytes.megabytes(10); - private static final int FILE_CHANNEL_POOL_CAPACITY = 100; - private static final int SESSION_COUNT = 50; - private static final int FILES_COUNT = 1000; - private static final int SLEEP_MAX = 10; - private static final int THREAD_COUNT = 20; - private static final int READ_MODULO = 100; - - private static class File - { - private final String sessionId; - private final int id; - - private byte first; - private byte last; - private int length; - - public File(String sessionId, int id) - { - this.sessionId = sessionId; - this.id = id; - } - - public String getSessionId() - { - return sessionId; - } - - public int getId() - { - return id; - } - - public byte[] generateData() - { - length = FILE_SIZE_MIN + random.nextInt(FILE_SIZE_MAX - FILE_SIZE_MIN); - byte data[] = new byte[length]; - random.nextBytes(data); - first = data[0]; - last = data[data.length - 1]; - return data; - } - - public boolean checkData(byte data[]) - { - if (data == null) - { - log.error("data[] should never be null"); - return false; - } - if (data.length != length) - { - log.error("data.length != length"); - return false; - } - if (first != data[0]) - { - log.error("first != data[0]"); - return false; - } - if (last != data[data.length - 1]) - { - log.error("last != data[data.length - 1]"); - return false; - } - return true; - } - } - - private final Map<String, AtomicInteger> sessionCounter = new ConcurrentHashMap<String, AtomicInteger>(); - private final ConcurrentLinkedQueue<File> filesToSave = new ConcurrentLinkedQueue<File>(); - private final ConcurrentLinkedQueue<File> filesToRead1 = new ConcurrentLinkedQueue<File>(); - private final ConcurrentLinkedQueue<File> filesToRead2 = new ConcurrentLinkedQueue<File>(); - - private final AtomicInteger read1Count = new AtomicInteger(0); - private final AtomicInteger read2Count = new AtomicInteger(0); - private final AtomicInteger saveCount = new AtomicInteger(0); - - private final AtomicBoolean saveDone = new AtomicBoolean(false); - private final AtomicBoolean read1Done = new AtomicBoolean(false); - private final AtomicBoolean read2Done = new AtomicBoolean(false); - - private final AtomicInteger failures = new AtomicInteger(); - - private final AtomicInteger bytesWritten = new AtomicInteger(0); - private final AtomicInteger bytesRead = new AtomicInteger(0); - - private final AtomicInteger saveTime = new AtomicInteger(0); - - private RuntimeException exceptionThrownByThread; - - private String randomSessionId() - { - List<String> s = new ArrayList<String>(sessionCounter.keySet()); - return s.get(random.nextInt(s.size())); - } - - private int nextSessionId(String sessionId) - { - AtomicInteger i = sessionCounter.get(sessionId); - return i.incrementAndGet(); - } - - private void generateFiles() - { - for (int i = 0; i < SESSION_COUNT; ++i) - { - sessionCounter.put(UUID.randomUUID().toString(), new AtomicInteger(0)); - } - for (int i = 0; i < FILES_COUNT; ++i) - { - String session = randomSessionId(); - File file = new File(session, nextSessionId(session)); - long now = System.nanoTime(); - filesToSave.add(file); - long duration = System.nanoTime() - now; - saveTime.addAndGet((int)duration); - } - } - - private IDataStore dataStore; - - /** - * Stores RuntimeException into a field. - */ - private abstract class ExceptionCapturingRunnable implements Runnable - { - public final void run() - { - try - { - doRun(); - } - catch (RuntimeException e) - { - exceptionThrownByThread = e; - } - } - - /** - * Called by {@link #run()}. Thrown RuntimeExceptions are stores into a field for later - * check. - */ - protected abstract void doRun(); - } - - // Store/Save data in DataStore - private class SaveRunnable extends ExceptionCapturingRunnable - { - @Override - protected void doRun() - { - File file; - - while ((file = filesToSave.poll()) != null || saveCount.get() < FILES_COUNT) - { - if (file != null) - { - byte data[] = file.generateData(); - dataStore.storeData(file.getSessionId(), file.getId(), data); - - if (saveCount.get() % READ_MODULO == 0) - { - filesToRead1.add(file); - } - saveCount.incrementAndGet(); - bytesWritten.addAndGet(data.length); - } - - try - { - Thread.sleep(random.nextInt(SLEEP_MAX)); - } - catch (InterruptedException e) - { - log.error(e.getMessage(), e); - } - } - - saveDone.set(true); - } - }; - - // Read data from DataStore - private class Read1Runnable extends ExceptionCapturingRunnable - { - @Override - protected void doRun() - { - File file; - while ((file = filesToRead1.poll()) != null || !saveDone.get()) - { - if (file != null) - { - byte bytes[] = dataStore.getData(file.getSessionId(), file.getId()); - if (file.checkData(bytes) == false) - { - failures.incrementAndGet(); - log.error("Detected error number: " + failures.get()); - } - filesToRead2.add(file); - read1Count.incrementAndGet(); - bytesRead.addAndGet(bytes.length); - } - - try - { - Thread.sleep(random.nextInt(SLEEP_MAX)); - } - catch (InterruptedException e) - { - log.error(e.getMessage(), e); - } - } - - read1Done.set(true); - } - }; - - private class Read2Runnable extends ExceptionCapturingRunnable - { - @Override - protected void doRun() - { - File file; - while ((file = filesToRead2.poll()) != null || !read1Done.get()) - { - if (file != null) - { - byte bytes[] = dataStore.getData(file.getSessionId(), file.getId()); - if (file.checkData(bytes) == false) - { - failures.incrementAndGet(); - log.error("Detected error number: " + failures.get()); - } - read2Count.incrementAndGet(); - bytesRead.addAndGet(bytes.length); - } - - try - { - Thread.sleep(random.nextInt(SLEEP_MAX)); - } - catch (InterruptedException e) - { - log.error(e.getMessage(), e); - } - } - - read2Done.set(true); - } - } - - private void doTestDataStore() - { - log.info("Starting..."); - long start = System.currentTimeMillis(); - - for (int i = 0; i < THREAD_COUNT; ++i) - { - new Thread(new Read1Runnable()).start(); - } - - for (int i = 0; i < THREAD_COUNT; ++i) - { - new Thread(new Read2Runnable()).start(); - } - - for (int i = 0; i < THREAD_COUNT; ++i) - { - new Thread(new SaveRunnable()).start(); - } - - while (!(read1Done.get() && read2Done.get() && saveDone.get())) - { - try - { - Thread.sleep(50); - } - catch (InterruptedException e) - { - log.error(e.getMessage(), e); - } - } - - if (exceptionThrownByThread != null) - { - throw new RuntimeException("One of the worker threads failed.", exceptionThrownByThread); - } - - long duration = System.currentTimeMillis() - start; - - log.info("Took: " + duration + " ms"); - log.info("Save: " + saveCount.intValue() + " files, " + bytesWritten.get() + " bytes"); - log.info("Read: " + (read1Count.get() + read2Count.get()) + " files, " + bytesRead.get() + - " bytes"); - - log.info("Average save time (ns): " + (double)saveTime.get() / (double)saveCount.get()); - - assertEquals(0, failures.get()); - - for (String s : sessionCounter.keySet()) - { - dataStore.removeData(s); - } - } - - /** - * store() - */ - @Test - public void store() - { - generateFiles(); - - IStoreSettings storeSettings = new StoreSettings(null); - java.io.File fileStoreFolder = storeSettings.getFileStoreFolder(); - - dataStore = new DiskDataStore("app1", fileStoreFolder, MAX_SIZE_PER_SESSION); - int asynchronousQueueCapacity = storeSettings.getAsynchronousQueueCapacity(); - dataStore = new AsynchronousDataStore(dataStore, asynchronousQueueCapacity); - - doTestDataStore(); - - dataStore.destroy(); - } -} http://git-wip-us.apache.org/repos/asf/wicket/blob/33f57f08/wicket-core/src/test/java/org/apache/wicket/pageStore/DiskDataStoreTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/pageStore/DiskDataStoreTest.java b/wicket-core/src/test/java/org/apache/wicket/pageStore/DiskDataStoreTest.java new file mode 100644 index 0000000..563ecc2 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/pageStore/DiskDataStoreTest.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.pageStore; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.wicket.settings.IStoreSettings; +import org.apache.wicket.settings.def.StoreSettings; +import org.apache.wicket.util.lang.Bytes; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class DiskDataStoreTest extends Assert +{ + /** Log for reporting. */ + private static final Logger log = LoggerFactory.getLogger(DiskDataStoreTest.class); + + /** + * Construct. + */ + public DiskDataStoreTest() + { + } + + private static final Random random = new Random(); + private static final int FILE_SIZE_MIN = 1024 * 200; + private static final int FILE_SIZE_MAX = 1024 * 300; + private static final Bytes MAX_SIZE_PER_SESSION = Bytes.megabytes(10); + private static final int FILE_CHANNEL_POOL_CAPACITY = 100; + private static final int SESSION_COUNT = 50; + private static final int FILES_COUNT = 1000; + private static final int SLEEP_MAX = 10; + private static final int THREAD_COUNT = 20; + private static final int READ_MODULO = 100; + + private static class File + { + private final String sessionId; + private final int id; + + private byte first; + private byte last; + private int length; + + public File(String sessionId, int id) + { + this.sessionId = sessionId; + this.id = id; + } + + public String getSessionId() + { + return sessionId; + } + + public int getId() + { + return id; + } + + public byte[] generateData() + { + length = FILE_SIZE_MIN + random.nextInt(FILE_SIZE_MAX - FILE_SIZE_MIN); + byte data[] = new byte[length]; + random.nextBytes(data); + first = data[0]; + last = data[data.length - 1]; + return data; + } + + public boolean checkData(byte data[]) + { + if (data == null) + { + log.error("data[] should never be null"); + return false; + } + if (data.length != length) + { + log.error("data.length != length"); + return false; + } + if (first != data[0]) + { + log.error("first != data[0]"); + return false; + } + if (last != data[data.length - 1]) + { + log.error("last != data[data.length - 1]"); + return false; + } + return true; + } + } + + private final Map<String, AtomicInteger> sessionCounter = new ConcurrentHashMap<String, AtomicInteger>(); + private final ConcurrentLinkedQueue<File> filesToSave = new ConcurrentLinkedQueue<File>(); + private final ConcurrentLinkedQueue<File> filesToRead1 = new ConcurrentLinkedQueue<File>(); + private final ConcurrentLinkedQueue<File> filesToRead2 = new ConcurrentLinkedQueue<File>(); + + private final AtomicInteger read1Count = new AtomicInteger(0); + private final AtomicInteger read2Count = new AtomicInteger(0); + private final AtomicInteger saveCount = new AtomicInteger(0); + + private final AtomicBoolean saveDone = new AtomicBoolean(false); + private final AtomicBoolean read1Done = new AtomicBoolean(false); + private final AtomicBoolean read2Done = new AtomicBoolean(false); + + private final AtomicInteger failures = new AtomicInteger(); + + private final AtomicInteger bytesWritten = new AtomicInteger(0); + private final AtomicInteger bytesRead = new AtomicInteger(0); + + private final AtomicInteger saveTime = new AtomicInteger(0); + + private RuntimeException exceptionThrownByThread; + + private String randomSessionId() + { + List<String> s = new ArrayList<String>(sessionCounter.keySet()); + return s.get(random.nextInt(s.size())); + } + + private int nextSessionId(String sessionId) + { + AtomicInteger i = sessionCounter.get(sessionId); + return i.incrementAndGet(); + } + + private void generateFiles() + { + for (int i = 0; i < SESSION_COUNT; ++i) + { + sessionCounter.put(UUID.randomUUID().toString(), new AtomicInteger(0)); + } + for (int i = 0; i < FILES_COUNT; ++i) + { + String session = randomSessionId(); + File file = new File(session, nextSessionId(session)); + long now = System.nanoTime(); + filesToSave.add(file); + long duration = System.nanoTime() - now; + saveTime.addAndGet((int)duration); + } + } + + private IDataStore dataStore; + + /** + * Stores RuntimeException into a field. + */ + private abstract class ExceptionCapturingRunnable implements Runnable + { + public final void run() + { + try + { + doRun(); + } + catch (RuntimeException e) + { + exceptionThrownByThread = e; + } + } + + /** + * Called by {@link #run()}. Thrown RuntimeExceptions are stores into a field for later + * check. + */ + protected abstract void doRun(); + } + + // Store/Save data in DataStore + private class SaveRunnable extends ExceptionCapturingRunnable + { + @Override + protected void doRun() + { + File file; + + while ((file = filesToSave.poll()) != null || saveCount.get() < FILES_COUNT) + { + if (file != null) + { + byte data[] = file.generateData(); + dataStore.storeData(file.getSessionId(), file.getId(), data); + + if (saveCount.get() % READ_MODULO == 0) + { + filesToRead1.add(file); + } + saveCount.incrementAndGet(); + bytesWritten.addAndGet(data.length); + } + + try + { + Thread.sleep(random.nextInt(SLEEP_MAX)); + } + catch (InterruptedException e) + { + log.error(e.getMessage(), e); + } + } + + saveDone.set(true); + } + }; + + // Read data from DataStore + private class Read1Runnable extends ExceptionCapturingRunnable + { + @Override + protected void doRun() + { + File file; + while ((file = filesToRead1.poll()) != null || !saveDone.get()) + { + if (file != null) + { + byte bytes[] = dataStore.getData(file.getSessionId(), file.getId()); + if (file.checkData(bytes) == false) + { + failures.incrementAndGet(); + log.error("Detected error number: " + failures.get()); + } + filesToRead2.add(file); + read1Count.incrementAndGet(); + bytesRead.addAndGet(bytes.length); + } + + try + { + Thread.sleep(random.nextInt(SLEEP_MAX)); + } + catch (InterruptedException e) + { + log.error(e.getMessage(), e); + } + } + + read1Done.set(true); + } + }; + + private class Read2Runnable extends ExceptionCapturingRunnable + { + @Override + protected void doRun() + { + File file; + while ((file = filesToRead2.poll()) != null || !read1Done.get()) + { + if (file != null) + { + byte bytes[] = dataStore.getData(file.getSessionId(), file.getId()); + if (file.checkData(bytes) == false) + { + failures.incrementAndGet(); + log.error("Detected error number: " + failures.get()); + } + read2Count.incrementAndGet(); + bytesRead.addAndGet(bytes.length); + } + + try + { + Thread.sleep(random.nextInt(SLEEP_MAX)); + } + catch (InterruptedException e) + { + log.error(e.getMessage(), e); + } + } + + read2Done.set(true); + } + } + + private void doTestDataStore() + { + log.info("Starting..."); + long start = System.currentTimeMillis(); + + for (int i = 0; i < THREAD_COUNT; ++i) + { + new Thread(new Read1Runnable()).start(); + } + + for (int i = 0; i < THREAD_COUNT; ++i) + { + new Thread(new Read2Runnable()).start(); + } + + for (int i = 0; i < THREAD_COUNT; ++i) + { + new Thread(new SaveRunnable()).start(); + } + + while (!(read1Done.get() && read2Done.get() && saveDone.get())) + { + try + { + Thread.sleep(50); + } + catch (InterruptedException e) + { + log.error(e.getMessage(), e); + } + } + + if (exceptionThrownByThread != null) + { + throw new RuntimeException("One of the worker threads failed.", exceptionThrownByThread); + } + + long duration = System.currentTimeMillis() - start; + + log.info("Took: " + duration + " ms"); + log.info("Save: " + saveCount.intValue() + " files, " + bytesWritten.get() + " bytes"); + log.info("Read: " + (read1Count.get() + read2Count.get()) + " files, " + bytesRead.get() + + " bytes"); + + log.info("Average save time (ns): " + (double)saveTime.get() / (double)saveCount.get()); + + assertEquals(0, failures.get()); + + for (String s : sessionCounter.keySet()) + { + dataStore.removeData(s); + } + } + + /** + * store() + */ + @Test + public void store() + { + generateFiles(); + + IStoreSettings storeSettings = new StoreSettings(null); + java.io.File fileStoreFolder = storeSettings.getFileStoreFolder(); + + dataStore = new DiskDataStore("app1", fileStoreFolder, MAX_SIZE_PER_SESSION); + int asynchronousQueueCapacity = storeSettings.getAsynchronousQueueCapacity(); + dataStore = new AsynchronousDataStore(dataStore, asynchronousQueueCapacity); + + doTestDataStore(); + + dataStore.destroy(); + } + + /** + * https://issues.apache.org/jira/browse/WICKET-4478 + * + * Tests that the folder where a session data is put is partitioned, i.e. + * it is put in folders which names are automatically calculated on the fly. + */ + @Test + public void sessionFolderName() + { + IStoreSettings storeSettings = new StoreSettings(null); + java.io.File fileStoreFolder = storeSettings.getFileStoreFolder(); + DiskDataStore store = new DiskDataStore("sessionFolderName", fileStoreFolder, MAX_SIZE_PER_SESSION); + + String sessionId = "abcdefg"; + java.io.File sessionFolder = store.getSessionFolder(sessionId, true); + assertEquals("/tmp/sessionFolderName-filestore/7141/1279/abcdefg", sessionFolder.getAbsolutePath()); + + DiskDataStore.SessionEntry sessionEntry = new DiskDataStore.SessionEntry(store, sessionId); + sessionEntry.unbind(); + // assert that the 'sessionId' folder and the parents two levels up are removed + assertFalse(sessionFolder.getParentFile().getParentFile().exists()); + + } +}
