Skip to content

Fix for postgres instance process closing #12927

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

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft

Conversation

UdayHE
Copy link

@UdayHE UdayHE commented Apr 13, 2025

Fixes #12844

Summary

  • Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.
  • Introduced PostgreProcessCleaner to safely shut down any orphaned PostgreSQL processes using port-based detection and Java PID tracking.
  • Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdowns (excluding SIGKILL).

Mandatory checks

  • I own the copyright of the code submitted and I license it under the MIT license.
  • Change in CHANGELOG.md described in a way that is understandable for the average user (if change is visible to the user)
  • Tests created for changes (if applicable)
  • Manually tested changed features in running JabRef (always required)
  • Screenshots added in PR description (if change is visible to the user)
  • Checked developer's documentation: Is the information available and up to date? If not, I outlined it in this pull request.
  • Checked documentation: Is the information available and up to date? If not, I created an issue at https://github.com/JabRef/user-documentation/issues or, even better, I submitted a pull request to the documentation repository.

macOS Sequoia 15.4.
https://www.loom.com/share/9ed0da7815714797b4f0568c33f6df03?sid=34114b6c-ca32-43ff-9896-bfae39b36c81

Windows
https://github.com/user-attachments/assets/aa7a7da4-661e-464e-a08f-f6befba66363

UdayHE added 7 commits April 13, 2025 10:22
…ination (Fix JabRef#12844)

* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.

* Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection.

* Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.
…ination (Fix JabRef#12844)

* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.

* Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection.

* Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.

* Updated CHANGELOG.md
…ination (Fix JabRef#12844)

* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.

* Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection.

* Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.
…ination (Fix JabRef#12844)

* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.

* Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection.

* Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.

* Updated CHANGELOG.md
…ination (Fix JabRef#12844)

* Added logic to detect and clean up stale embedded PostgreSQL instances left behind by previous JabRef sessions.

* Introduced PostgresProcessCleaner to safely shut down any orphaned PostgreSQL processes using PID-based detection.

* Registered a shutdown hook in Launcher to ensure embedded PostgreSQL is properly terminated during normal or abrupt shutdown.

* Updated CHANGELOG.md
…ination (Fix JabRef#12844)

* style changes in PostgreProcessCleaner, Launcher

private void destroyPreviousJavaProcess(Map<String, Object> meta) throws InterruptedException {
long javaPid = ((Number) meta.get("javaPid")).longValue();
destroyProcessByPID(javaPid, 1000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does one know whether this stores JabRef data? Shouldn't a connection attempt be made?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In writeMetadataToFile method, I am using ProcessHandle.current().pid(), so that the current running process id will be stored in the json file.

@koppor
Copy link
Member

koppor commented Apr 13, 2025

This is AI generated, isn't it?

@subhramit
Copy link
Member

Have you tested this by observing the processes? If so, on which OS?
I tried on Windows. Doesn't work.

@UdayHE
Copy link
Author

UdayHE commented Apr 13, 2025

This is AI generated, isn't it?

nope.

import static org.jabref.model.search.PostgreConstants.BIB_FIELDS_SCHEME;

public class PostgreServer {
private static final Logger LOGGER = LoggerFactory.getLogger(PostgreServer.class);
public static final Path POSTGRES_METADATA_FILE = Path.of("/tmp/jabref-postgres-info.json");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will not work in Windows. Will not work if multiple JabRef instances run.

For the former, AppDirs can be used (can't it?) - for the latter, the whole flow needs to be thought through.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I need to check about AppDirs once & get back on this.

@UdayHE
Copy link
Author

UdayHE commented Apr 13, 2025

Have you tested this by observing the processes? If so, on which OS? I tried on Windows. Doesn't work.

yes, I have tested on macOS Sequoia 15.4.
https://www.loom.com/share/9ed0da7815714797b4f0568c33f6df03?sid=34114b6c-ca32-43ff-9896-bfae39b36c81

I will check on windows and update here.

@subhramit
Copy link
Member

yes, I have tested on macOS Sequoia 15.4.
https://www.loom.com/share/9ed0da7815714797b4f0568c33f6df03?sid=34114b6c-ca32-43ff-9896-bfae39b36c81

Interesting - this seems to be going in a positive direction, if we can fix it for Windows.
Let me know if you want me to test and give feedback after a certain round of changes.

@Siedlerchr Siedlerchr changed the title Fix for Issue 12844 Fix for postgres instance process closing Apr 13, 2025
UdayHE added 3 commits April 13, 2025 18:17
…ination (Fix JabRef#12844)

* style changes in PostgreServer imports
…ination (Fix JabRef#12844)

* Modified PostgreProcessCleaner logic to fix windows os issue.
@UdayHE
Copy link
Author

UdayHE commented Apr 15, 2025

Have you tested this by observing the processes? If so, on which OS? I tried on Windows. Doesn't work.

checked on windows as well, can you please check from your end as well?

screen-capture-windows.mp4

…ination (Fix JabRef#12844)

* Used Path.of() instead of Paths.get() in PostgreProcessCleaner.
@UdayHE
Copy link
Author

UdayHE commented Apr 15, 2025

Embedded Postgres Cleanup Flow
1.Start the Application
When a new JabRef instance starts, it initiates the embedded Postgres server.

2.Write Metadata File
During startup, the application writes a JSON file to the temporary directory (java.io.tmpdir).
File naming convention: jabref-postgres-info-.json (e.g., jabref-postgres-info-1234.json).
This file contains:
javaPid: The PID of the current Java process.
postgresPort: The port used by the embedded Postgres instance.
etc..

3.Check for Metadata Files on Startup
On startup, the application scans the temp directory for any matching metadata files (jabref-postgres-info-*.json).
This supports scenarios where multiple instances may have left behind multiple metadata files.
Read Stored Process Details.
For each metadata file found:
It reads the javaPid and postgresPort from the JSON.

4.Verify Java Process Liveness
For each javaPid, the application checks whether the corresponding Java process is still alive.

5.Handle Stale Java Processes
If any Java process is no longer running:
It indicates that a previous JabRef instance was terminated (e.g., via kill -9 or SIGKILL).

6.Lookup Postgres Processes
For each stale entry:
The application performs a port-based lookup to check if any process is still listening on the postgresPort.

7.Kill Stale Postgres
If a Postgres process is found on that port:
It is considered stale and is safely terminated.

8.Delete Metadata Files
After handling each stale process:
The corresponding metadata file (jabref-postgres-info-.json) is deleted.
Metadata files for live Java processes are retained, to avoid interfering with other active instances.

9.Proceed with Fresh Initialization
The current instance proceeds to launch a new embedded Postgres server.
A new metadata file is created specific to this instance.

10. Multiple Instance Safety
Each JabRef instance maintains its own metadata file, isolated by Java PID.
Stale cleanup only affects processes no longer alive, ensuring that active instances are never disrupted.

@UdayHE UdayHE requested a review from koppor April 15, 2025 09:19
@jabref-machine

This comment was marked as resolved.

Map<String, Object> metadata = new HashMap<>(OBJECT_MAPPER.readValue(Files.readAllBytes(metadataPath), HashMap.class));
long javaPid = ((Number) metadata.get("javaPid")).longValue();
if (isJavaProcessAlive(javaPid)) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the database is still running, is it necessary to clean up the data to ensure it's in the same state as a freshly started database?

}
int postgresPort = ((Number) metadata.get("postgresPort")).intValue();
destroyPostgresProcess(postgresPort);
Files.deleteIfExists(metadataPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question—what happens if you're running multiple JabRef instances? How can you distinguish between them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally JabRef has only a single instance open, it is a setting in the preferences
grafik

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the question was for the other case.

Maybe, we should just start to disallow multiple instances?

.redirectErrorStream(true).start();
} else if (OS.WINDOWS) {
return executeWindowsCommand(port);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor question: Is lsof included by default on macOS and Linux? For example, if someone is running Ubuntu with a standard configuration—without adding any extra packages—the solution should ideally work out of the box.

Additionally, there could be security or access rights issues. For instance, on Windows, a domain admin might block users from executing cmd.exe.

At the very least, the documentation should specify the conditions under which the solution is expected to run successfully.

}

public static Path getMetadataFilePath() {
return Path.of(System.getProperty("java.io.tmpdir"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the org.jabref.logic.util.Directories class should be used here to obtain the directory in a standardized way.

private Process executeWindowsCommand(int port) throws IOException {
String systemRoot = System.getenv("SystemRoot");
if (systemRoot != null && !systemRoot.isBlank()) {
String netStatPath = systemRoot + "\\System32\\netstat.exe";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use Java paths?

if (systemRoot != null && !systemRoot.isBlank()) {
String netStatPath = systemRoot + "\\System32\\netstat.exe";
String findStrPath = systemRoot + "\\System32\\findstr.exe";
String command = netStatPath + " -ano | " + findStrPath + " :" + port;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. is there an API, where you specify this for running a process:

  1. Command name
  2. List of strings that represent arguments

I think this is more stable than just passing a string

if (systemRoot != null && !systemRoot.isBlank()) {
String netStatPath = systemRoot + "\\System32\\netstat.exe";
String findStrPath = systemRoot + "\\System32\\findstr.exe";
String command = netStatPath + " -ano | " + findStrPath + " :" + port;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does | do here? It's like a pipe from Unix? Does it work on Windows, or it has other meaning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findstr should be programmed in Java

Code reads like AI generated...


tasklist ist the proposal by "the internet" - https://www.rgagnon.com/javadetails/java-0593.html

Copy link
Member

@Siedlerchr Siedlerchr Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With java 9 + we can use Process Handles https://stackoverflow.com/a/45068036

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findstr should be programmed in Java

Code reads like AI generated...

tasklist ist the proposal by "the internet" - https://www.rgagnon.com/javadetails/java-0593.html

Not AI, I have referred these for executing commands:
https://coderanch.com/t/688902/java/Executing-netstat-command-process-builder
https://mkyong.com/java/java-processbuilder-examples/

Also I will check the commands we need to program using java, We can make use of command design pattern, so that if any other command needs to be implemented, we can easily extend that as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for ProcessHandles

return null;
}

private long extractPidFromOutput(BufferedReader reader) throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you call some external command to get PID, right?

I wonder, is there a nicer package for Java? Or some alternative in packages that we have.. (If there is no such thing, then I think it's okay to use commands)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried google, did not find something immediatly.

Map<String, Object> meta = new HashMap<>();
meta.put("javaPid", ProcessHandle.current().pid());
meta.put("postgresPort", port);
meta.put("startedBy", "jabref");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some sense, trag-bot is right.

Is it possible to, maybe, make a serializable class?

@koppor koppor marked this pull request as draft May 11, 2025 20:21
@koppor
Copy link
Member

koppor commented May 11, 2025

I did not see any progress since weeks. I will close this PR. @UdayHE If you intend to continue here; feel free to ping us.

@koppor koppor closed this May 11, 2025
@koppor koppor reopened this May 17, 2025
@UdayHE UdayHE closed this May 18, 2025
@UdayHE UdayHE reopened this May 18, 2025
}

private void destroyProcessByPID(long pid) throws InterruptedException {
Optional<ProcessHandle> aliveProcess = ProcessHandle.of(pid).filter(ProcessHandle::isAlive);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method destroyProcessByPID should use ifPresent on Optional instead of checking isPresent and then calling get.

Comment on lines 80 to 81
if (aliveProcess.isPresent()) {
aliveProcess.get().destroy();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code should use ifPresent to handle the Optional instead of checking isPresent and then calling get.

@UdayHE
Copy link
Author

UdayHE commented May 18, 2025

Below is the flow and classes involved.
stop_stale_postgres.txt

Few doubts I have, please find below

  1. Are we going to disallow multiple jabref instances?
  2. As this approach finds the stale postgres process using port-lookup, it uses commands like netstat, lsof to find the process & its PID, is there any other way to kill stale postgres without depending on these commands? like any library which helps to interact with OS related stuffs.
    I found oshi (https://github.com/oshi/oshi), but I need to explore the possibility.

meta.put("javaPid", ProcessHandle.current().pid());
meta.put("postgresPort", port);
meta.put("startedBy", "jabref");
meta.put("startedAt", DateTimeFormatter.ISO_INSTANT.format(Instant.now()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "startedAt" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.


private static Map<String, Object> createMetadata(int port) {
Map<String, Object> meta = new HashMap<>();
meta.put("javaPid", ProcessHandle.current().pid());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "javaPid" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.

private static Map<String, Object> createMetadata(int port) {
Map<String, Object> meta = new HashMap<>();
meta.put("javaPid", ProcessHandle.current().pid());
meta.put("postgresPort", port);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string literal key "postgresPort" should be declared as a private static final constant to improve maintainability and readability, following best practices for handling literals.

@koppor
Copy link
Member

koppor commented May 18, 2025

Below is the flow and classes involved.
stop_stale_postgres.txt

Few doubts I have, please find below

  1. Are we going to disallow multiple jabref instances?

I see two options:

  1. Switch to another method to detect multiple instances. Furthermore: Command passing needs to work with the new method! - One method is to use Postgres' notification system
  2. Keep the concept as is.

The postgres notification system is not too hard; maybe a better way to habdle things.

  1. As this approach finds the stale postgres process using port-lookup, it uses commands like netstat, lsof to find the process & its PID, is there any other way to kill stale postgres without depending on these commands? like any library which helps to interact with OS related stuffs.
    I found oshi (https://github.com/oshi/oshi), but I need to explore the possibility.

oshi sounds promising

UdayHE added 2 commits May 19, 2025 09:48
* Modified getPidUsingPort() to use OSHI.
* Fix for review comments.
@@ -77,6 +81,9 @@ public static void main(String[] args) {

CSLStyleLoader.loadInternalStyles();

// Register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(postgreServer::shutdown));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'new Thread()' is discouraged. Instead, 'org.jabref.logic.util.BackgroundTask' and its 'executeWith' method should be used for better resource management and consistency with the rest of the codebase.

@@ -246,5 +246,6 @@
requires mslinks;
requires org.antlr.antlr4.runtime;
requires org.libreoffice.uno;
requires com.github.oshi;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pull request title should contain a short title of the issue fixed or what the PR addresses, not just 'Fix issue xyz'. This is important for clarity and tracking purposes.

}

public void checkAndCleanupOldInstances() {
try (Stream<Path> files = Files.list(TEMP_DIR)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try block covers too many statements, which can make it harder to identify where an exception occurs. It should cover as few statements as possible.

Comment on lines +33 to +39
try {
Path path = getMetadataFilePath();
OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), createMetadata(port));
LOGGER.info("Postgres metadata file path: {}", path);
} catch (IOException e) {
LOGGER.warn("Failed to write Postgres metadata file.", e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try block covers multiple statements. It is recommended to minimize the scope of try blocks to only the statements that might throw exceptions.

@@ -59,4 +59,8 @@ public static Path getSslDirectory() {
"ssl",
OS.APP_DIR_APP_AUTHOR));
}

public static Path getTmpDirectory() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method getTmpDirectory() is newly added and should not return null. It should use java.util.Optional to handle potential null values, ensuring better null safety and adherence to modern Java practices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PostgreSQL processes remain running after JabRef crashes or is forcefully terminated
7 participants