Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ updates:
- dependency-name: "org.apache.lucene:lucene-queries"
versions:
- ">= 6.0"
- dependency-name: "org.apache.lucene:lucene-backward-codecs"
versions:
- ">= 6.0"
- dependency-name: "org.apache.lucene:lucene-analyzers-phonetic"
versions:
- ">= 6.0"
Expand Down
11 changes: 11 additions & 0 deletions api/src/main/java/org/openmrs/api/context/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.openmrs.util.DatabaseUpdater;
import org.openmrs.util.InputRequiredException;
import org.openmrs.util.LocaleUtility;
import org.openmrs.util.LuceneIndexUpgrader;
import org.openmrs.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
Expand Down Expand Up @@ -947,6 +948,16 @@ public static synchronized void startup(Properties props) throws DatabaseUpdateE
// data directory can be set from the runtime properties
OpenmrsUtil.startup(props);

// Upgrade Lucene indexes if needed before any database operations
// This addresses TRUNK-5731 by ensuring old Lucene410 indexes are upgraded
// before Hibernate Search tries to read them
try {
LuceneIndexUpgrader.upgradeLuceneIndexesIfNeeded(props);
} catch (Exception e) {
log.warn("Failed to upgrade Lucene indexes during startup: {}", e.getMessage());
// Don't fail startup for Lucene index upgrade issues, but log the warning
}

openSession();
clearSession();

Expand Down
139 changes: 139 additions & 0 deletions api/src/main/java/org/openmrs/util/LuceneIndexUpgrader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.util;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utility class to handle Lucene index upgrades during OpenMRS startup. This addresses TRUNK-5731
* by ensuring Lucene indexes are upgraded before any database operations that might try to read
* them.
*
* @since 2.4.0
*/
public class LuceneIndexUpgrader {

private static final Logger log = LoggerFactory.getLogger(LuceneIndexUpgrader.class);

/**
* Upgrades Lucene indexes if they exist and are using old codecs. This method should be called
* before any database operations during startup.
*
* @param properties Runtime properties containing application data directory
* @return true if upgrade was performed, false if no upgrade was needed
*/
public static boolean upgradeLuceneIndexesIfNeeded(Properties properties) {
String appDataDir = properties.getProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY);
if (appDataDir == null || appDataDir.trim().isEmpty()) {
log.debug("No application data directory specified, skipping Lucene index upgrade check");
return false;
}

Path luceneIndexDir = Paths.get(appDataDir, "lucene", "indexes");
if (!Files.exists(luceneIndexDir)) {
log.debug("Lucene index directory does not exist: {}", luceneIndexDir);
return false;
}

log.info("Checking for Lucene index upgrade requirements in: {}", luceneIndexDir);

boolean upgradePerformed = false;
try {
// Check each subdirectory in the lucene indexes directory
Files.list(luceneIndexDir)
.filter(Files::isDirectory)
.forEach(indexPath -> {
try {
if (needsUpgrade(indexPath)) {
log.info("Upgrading Lucene index: {}", indexPath);
upgradeIndex(indexPath);
}
} catch (Exception e) {
log.warn("Failed to upgrade Lucene index at {}: {}", indexPath, e.getMessage());
// Continue with other indexes even if one fails
}
});
} catch (IOException e) {
log.warn("Failed to list Lucene index directories: {}", e.getMessage());
}

return upgradePerformed;
}

/**
* Checks if a Lucene index needs upgrading by attempting to read it. If it fails with a
* codec-related error, it needs upgrading.
*
* @param indexPath Path to the Lucene index
* @return true if the index needs upgrading
*/
private static boolean needsUpgrade(Path indexPath) {
try (Directory directory = FSDirectory.open(indexPath)) {
// Try to open the index with current Lucene version
try (IndexReader reader = DirectoryReader.open(directory)) {
// If we can open it successfully, no upgrade needed
log.debug("Lucene index at {} is already compatible", indexPath);
return false;
}
} catch (Exception e) {
// Check if this is a codec-related error
String errorMessage = e.getMessage();
if (errorMessage != null &&
(errorMessage.contains("Lucene410") ||
errorMessage.contains("does not exist") ||
errorMessage.contains("codec"))) {
log.info("Lucene index at {} needs upgrading: {}", indexPath, errorMessage);
return true;
} else {
log.debug("Lucene index at {} has a different error (not codec-related): {}", indexPath, errorMessage);
return false;
}
}
}

/**
* Upgrades a Lucene index by deleting the old index files. This forces Hibernate Search to
* recreate the index with the current codec.
*
* @param indexPath Path to the Lucene index to upgrade
*/
private static void upgradeIndex(Path indexPath) {
try {
log.info("Deleting old Lucene index files at {} to force recreation with current codec", indexPath);

// Delete all files in the index directory
Files.list(indexPath)
.forEach(file -> {
try {
Files.delete(file);
log.debug("Deleted index file: {}", file);
} catch (IOException e) {
log.warn("Failed to delete index file {}: {}", file, e.getMessage());
}
});

log.info("Successfully upgraded Lucene index at {}", indexPath);
} catch (IOException e) {
log.error("Failed to upgrade Lucene index at {}: {}", indexPath, e.getMessage());
throw new RuntimeException("Failed to upgrade Lucene index", e);
}
}
}
77 changes: 77 additions & 0 deletions api/src/test/java/org/openmrs/util/LuceneIndexUpgraderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.util;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* Tests for {@link LuceneIndexUpgrader}
*
* @since 2.4.0
*/
public class LuceneIndexUpgraderTest {

@TempDir
Path tempDir;

@Test
public void upgradeLuceneIndexesIfNeeded_shouldReturnFalseWhenNoAppDataDir() {
Properties props = new Properties();
// No application data directory set

boolean result = LuceneIndexUpgrader.upgradeLuceneIndexesIfNeeded(props);

assertFalse(result);
}

@Test
public void upgradeLuceneIndexesIfNeeded_shouldReturnFalseWhenLuceneDirDoesNotExist() throws IOException {
Properties props = new Properties();
props.setProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY, tempDir.toString());

boolean result = LuceneIndexUpgrader.upgradeLuceneIndexesIfNeeded(props);

assertFalse(result);
}

@Test
public void upgradeLuceneIndexesIfNeeded_shouldReturnFalseWhenLuceneDirExistsButEmpty() throws IOException {
Properties props = new Properties();
props.setProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY, tempDir.toString());

// Create lucene directory but leave it empty
Path luceneDir = tempDir.resolve("lucene").resolve("indexes");
Files.createDirectories(luceneDir);

boolean result = LuceneIndexUpgrader.upgradeLuceneIndexesIfNeeded(props);

assertFalse(result);
}

@Test
public void upgradeLuceneIndexesIfNeeded_shouldHandleNonExistentAppDataDir() {
Properties props = new Properties();
props.setProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY, "/nonexistent/path");

// Should not throw exception, just return false
boolean result = LuceneIndexUpgrader.upgradeLuceneIndexesIfNeeded(props);

assertFalse(result);
}
}