-
Notifications
You must be signed in to change notification settings - Fork 584
Enhancement implemented #2954 : Recover from fatal invalid cache state #3103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1464115
7f28e7c
a6a61b1
a6b7039
5012e92
a6265de
ecf166c
0f5098a
ceebfe4
ea0eb35
2fbc98c
76287e7
93969c4
976a137
79d042e
cc87d58
c6cfcab
15821b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ | |
| import static org.ehcache.impl.persistence.FileUtils.safeIdentifier; | ||
| import static org.ehcache.impl.persistence.FileUtils.tryRecursiveDelete; | ||
| import static org.ehcache.impl.persistence.FileUtils.validateName; | ||
| import static org.ehcache.impl.persistence.FileUtils.isDirectoryEmpty; | ||
|
|
||
| /** | ||
| * Implements the local persistence service that provides individual sub-spaces for different | ||
|
|
@@ -48,10 +49,12 @@ public class DefaultLocalPersistenceService implements LocalPersistenceService { | |
|
|
||
| private final File rootDirectory; | ||
| private final File lockFile; | ||
| private final File cleanFile; | ||
|
|
||
| private FileLock lock; | ||
| private RandomAccessFile rw; | ||
| private boolean started; | ||
| private boolean clean; | ||
|
|
||
| /** | ||
| * Creates a new service instance using the provided configuration. | ||
|
|
@@ -65,6 +68,7 @@ public DefaultLocalPersistenceService(final DefaultPersistenceConfiguration pers | |
| throw new NullPointerException("DefaultPersistenceConfiguration cannot be null"); | ||
| } | ||
| lockFile = new File(rootDirectory, ".lock"); | ||
| cleanFile = new File(rootDirectory, ".clean"); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -82,7 +86,15 @@ public synchronized void startForMaintenance(ServiceProvider<? super Maintainabl | |
|
|
||
| private void internalStart() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method should always positively set the clean variable since services can be stopped and restarted (although it's not common). |
||
| if (!started) { | ||
| clean = false; | ||
| createLocationIfRequiredAndVerify(rootDirectory); | ||
| try { | ||
| if (isDirectoryEmpty(rootDirectory.toPath())) { | ||
| clean = true; | ||
| } | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| try { | ||
| rw = new RandomAccessFile(lockFile, "rw"); | ||
| } catch (FileNotFoundException e) { | ||
|
|
@@ -104,6 +116,19 @@ private void internalStart() { | |
| if (lock == null) { | ||
| throw new RuntimeException("Persistence directory already locked by another process: " + rootDirectory.getAbsolutePath()); | ||
| } | ||
|
|
||
| if (cleanFile.exists()) { | ||
| try { | ||
| LOGGER.debug("clean file exists, trying to delete the file."); | ||
| Files.delete(cleanFile.toPath()); | ||
| clean = true; | ||
| LOGGER.debug("clean file is deleted."); | ||
| } catch (IOException e) { | ||
| LOGGER.debug("clean file is not deleted {}.", cleanFile.getPath()); | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| started = true; | ||
| LOGGER.debug("RootDirectory Locked"); | ||
| } | ||
|
|
@@ -115,6 +140,19 @@ private void internalStart() { | |
| @Override | ||
| public synchronized void stop() { | ||
| if (started) { | ||
| try { | ||
| if (cleanFile.createNewFile()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably log some kind of warning on a false return here too. |
||
| LOGGER.debug("clean file is created."); | ||
| } else { | ||
| LOGGER.warn("clean file already exists. The file didn't got deleted, may be due to network issue or file permission on directory." + | ||
| "\n Hint: clean file exists on service start-up, indicates service was stopped cleanly last time. It gets created while the service is stopped and it should be deleted while the service is started." + | ||
| "\n Action: Please verify there permission to delete the file and delete the root directory prior to start the service again."); | ||
| } | ||
| } catch (IOException e) { | ||
| LOGGER.warn("clean file is not created. Reason: " + e.getMessage() + | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This log line suffers the same problem. It's long, wordy, but doesn't help the user. |
||
| "\n Hint: clean file exists on service start-up, indicates service was stopped cleanly last time. It gets created while the service is stopped and it should be deleted while the service is started." + | ||
| "\n Action: Do resolve the exception received. Prior to start the service again, please delete the root directory."); | ||
| } | ||
| try { | ||
| lock.release(); | ||
| // Closing RandomAccessFile so that files gets deleted on windows and | ||
|
|
@@ -129,6 +167,7 @@ public synchronized void stop() { | |
| } catch (IOException e) { | ||
| throw new RuntimeException("Couldn't unlock rootDir: " + rootDirectory.getAbsolutePath(), e); | ||
| } | ||
|
|
||
| started = false; | ||
| LOGGER.debug("RootDirectory Unlocked"); | ||
| } | ||
|
|
@@ -198,6 +237,15 @@ public void destroyAll(String owner) { | |
| } | ||
| } | ||
|
|
||
| @Override | ||
| public final synchronized boolean isClean() { | ||
| if (started) { | ||
| return clean; | ||
| } else { | ||
| throw new IllegalStateException("Service is not running"); | ||
| } | ||
| } | ||
|
|
||
| private void destroy(SafeSpace ss, boolean verbose) { | ||
| if (verbose) { | ||
| LOGGER.debug("Destroying file based persistence context for {}", ss.identifier); | ||
|
|
@@ -209,7 +257,6 @@ private void destroy(SafeSpace ss, boolean verbose) { | |
| } | ||
| } | ||
|
|
||
|
|
||
| private SafeSpace createSafeSpaceLogical(String owner, String identifier) { | ||
| File ownerDirectory = new File(rootDirectory, owner); | ||
| File directory = new File(ownerDirectory, safeIdentifier(identifier)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,8 @@ | |
| import static org.hamcrest.Matchers.is; | ||
| import static org.hamcrest.Matchers.not; | ||
| import static org.junit.Assert.assertThrows; | ||
| import static org.junit.Assert.assertTrue; | ||
| import static org.junit.Assert.assertFalse; | ||
| import static org.junit.Assert.fail; | ||
| import static org.junit.Assume.assumeTrue; | ||
|
|
||
|
|
@@ -140,4 +142,50 @@ public void testExclusiveLock() throws IOException { | |
| RuntimeException thrown = assertThrows(RuntimeException.class, () -> service2.start(null)); | ||
| assertThat(thrown, hasProperty("message", is("Persistence directory already locked by this process: " + testFolder.getAbsolutePath()))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testServiceShutdownWithEmptyDirectory() throws IOException { | ||
| File f = folder.newFolder("testServiceShutdownWithEmptyDirectory"); | ||
| final DefaultLocalPersistenceService service = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(f)); | ||
| service.start(null); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test should actually create a file inside the directory to ensure we hit the non-empty directory codepath. |
||
| assertTrue(service.isClean()); | ||
| service.stop(); | ||
| service.start(null); | ||
| assertTrue(service.isClean()); | ||
| service.stop(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testServiceShutdownWithNonEmptyDirectory() throws IOException { | ||
| File f = folder.newFolder("testServiceShutdownWithNonEmptyDirectory"); | ||
| final DefaultLocalPersistenceService service = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(f)); | ||
| new File(f, "dummy.txt").createNewFile(); | ||
| new File(f, ".clean").createNewFile(); | ||
| service.start(null); | ||
| assertTrue(service.isClean()); | ||
| service.stop(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testServiceShutdownUnexpectedly() throws IOException { | ||
| // Service shutdown unexpectedly means directory exists with some data but without .clean file. | ||
| File f = folder.newFolder("testServiceShutdownUnexpectedly"); | ||
| final DefaultLocalPersistenceService service = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(f)); | ||
| new File(f, "dummy.txt").createNewFile(); | ||
| service.start(null); | ||
| assertFalse(service.isClean()); | ||
| service.stop(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testServiceShutdownStatusIfServiceIsNotRunning() throws IOException { | ||
| File f = folder.newFolder("testServiceShutdownStatusIfServiceIsNotRunning"); | ||
| final DefaultLocalPersistenceService service = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(f)); | ||
| try { | ||
| service.isClean(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only the method call being tested should be inside the try-catch block. |
||
| fail("Expected IllegalStateException"); | ||
| } catch(IllegalStateException e) { | ||
| assertThat(e.getMessage(), equalTo("Service is not running")); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Javadoc needs polish, and the
@throwsshould be moved up to here: