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
15 changes: 14 additions & 1 deletion docs/using-the-jdbc-driver/UsingTheJdbcDriver.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ These parameters are applicable to any instance of the AWS Advanced JDBC Wrapper
| `user` | `String` | No | Database username. | `null` |
| `password` | `String` | No | Database password. | `null` |
| `wrapperDialect` | `String` | No | Please see [this page on database dialects](./DatabaseDialects.md), and whether you should include it. | `null` |
| `wrapperLogUnclosedConnections` | `Boolean` | No | Allows the AWS Advanced JDBC Wrapper to capture a stacktrace for each connection that is opened. If the `finalize()` method is reached without the connection being closed, the stacktrace is printed to the log. This helps developers to detect and correct the source of potential connection leaks. | `false` |
| `wrapperLogUnclosedConnections` | `Boolean` | No | Allows the AWS Advanced JDBC Wrapper to capture a stacktrace for each connection that is opened. If the connection is garbage collected without being closed, the stacktrace is printed to the log. This helps developers to detect and correct the source of potential connection leaks. | `false` |
| `loginTimeout` | `Integer` | No | Login timeout in milliseconds. | `null` |
| `connectTimeout` | `Integer` | No | Socket connect timeout in milliseconds. | `null` |
| `socketTimeout` | `Integer` | No | Socket timeout in milliseconds. | `null` |
Expand All @@ -98,6 +98,19 @@ These parameters are applicable to any instance of the AWS Advanced JDBC Wrapper
| `wrapperCaseSensitive`,<br>`wrappercasesensitive` | `Boolean` | No | Allows the driver to change case sensitivity for parameter names in the connection string and in connection properties. Set parameter to `false` to allow case-insensitive parameter names. | `true` |
| `skipWrappingForPackages` | `String` | No | Register Java package names (separated by comma) which will be left unwrapped. This setting modifies all future connections established by the driver, not just a particular connection. | `com.pgvector` |

## System Properties

These Java system properties can be set using `-D` flags when starting the JVM to configure global behavior of the AWS Advanced JDBC Wrapper.

| Property | Value | Description | Default Value |
|---------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `aws.jdbc.cleanup.thread.ttl` | `Long` | Time-to-live in milliseconds for the cleanup thread that monitors unclosed connections. The thread will automatically stop after this period of inactivity to conserve resources. Only relevant when `wrapperLogUnclosedConnections` is enabled. | `30000` (30 seconds) |

**Example:**
```bash
java -Daws.jdbc.cleanup.thread.ttl=60000 -jar myapp.jar
```

## Plugins
The AWS Advanced JDBC Wrapper uses plugins to execute JDBC methods. You can think of a plugin as an extensible code module that adds extra logic around any JDBC method calls. The AWS Advanced JDBC Wrapper has a number of [built-in plugins](#list-of-available-plugins) available for use.

Expand Down
212 changes: 212 additions & 0 deletions wrapper/src/main/java/software/amazon/jdbc/util/LazyCleaner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed 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.
*/
/* changes were made to move it into the software.amazon.jdbc.util package
*
* Copyright 2022 Juan Lopes
*
* Licensed 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 software.amazon.jdbc.util;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.time.Duration;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;

public class LazyCleaner {
private static final Logger LOGGER = Logger.getLogger(LazyCleaner.class.getName());
private static final LazyCleaner instance =
new LazyCleaner(
Duration.ofMillis(Long.getLong("aws.jdbc.cleanup.thread.ttl", 30000)),
"AWS-JDBC-Cleaner"
);

public interface Cleanable<T extends Throwable> {
void clean() throws T;
}

public interface CleaningAction<T extends Throwable> {
void onClean(boolean leak) throws T;
}

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
private final long threadTtl;
private final ThreadFactory threadFactory;
private boolean threadRunning;
private int watchedCount;
private @Nullable Node<?> first;

public static LazyCleaner getInstance() {
return instance;
}

public LazyCleaner(Duration threadTtl, final String threadName) {
this(threadTtl, runnable -> {
Thread thread = new Thread(runnable, threadName);
thread.setDaemon(true);
return thread;
});
}

private LazyCleaner(Duration threadTtl, ThreadFactory threadFactory) {
this.threadTtl = threadTtl.toMillis();
this.threadFactory = threadFactory;
}

public <T extends Throwable> Cleanable<T> register(Object obj, CleaningAction<T> action) {
return add(new Node<T>(obj, action));
}

public synchronized int getWatchedCount() {
return watchedCount;
}

public synchronized boolean isThreadRunning() {
return threadRunning;
}

private synchronized boolean checkEmpty() {
if (first == null) {
threadRunning = false;
return true;
}
return false;
}

private synchronized <T extends Throwable> Node<T> add(Node<T> node) {
if (first != null) {
node.next = first;
first.prev = node;
}
first = node;
watchedCount++;

if (!threadRunning) {
threadRunning = startThread();
}
return node;
}

private boolean startThread() {
Thread thread = threadFactory.newThread(new Runnable() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the thread be a daemon thread?

Copy link
Contributor Author

@davecramer davecramer Dec 2, 2025

Choose a reason for hiding this comment

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

Actually we do not use a daemon thread in pgjdbc

@Override
public void run() {
while (true) {
try {
Thread.currentThread().setContextClassLoader(null);
Thread.currentThread().setUncaughtExceptionHandler(null);
Node<?> ref = (Node<?>) queue.remove(threadTtl);
if (ref == null) {
if (checkEmpty()) {
break;
}
continue;
}
try {
ref.onClean(true);
} catch (Throwable e) {
if (e instanceof InterruptedException) {
LOGGER.log(Level.WARNING, "Unexpected interrupt while executing onClean", e);
throw e;
}
LOGGER.log(Level.WARNING, "Unexpected exception while executing onClean", e);
}
} catch (InterruptedException e) {
if (LazyCleaner.this.checkEmpty()) {
LOGGER.log(
Level.FINE,
"Cleanup queue is empty, and got interrupt, will terminate the cleanup thread"
);
break;
}
LOGGER.log(Level.FINE, "Ignoring interrupt since the cleanup queue is non-empty");
} catch (Throwable e) {
LOGGER.log(Level.WARNING, "Unexpected exception in cleaner thread main loop", e);
}
}
}
});
if (thread != null) {
thread.start();
return true;
}
LOGGER.log(Level.WARNING, "Unable to create cleanup thread");
return false;
}

private synchronized boolean remove(Node<?> node) {
if (node.next == node) {
return false;
}

if (first == node) {
first = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
}
if (node.prev != null) {
node.prev.next = node.next;
}

node.next = node;
node.prev = node;

watchedCount--;
return true;
}

private class Node<T extends Throwable> extends PhantomReference<Object> implements Cleanable<T>,
CleaningAction<T> {
private final @Nullable CleaningAction<T> action;
private @Nullable Node<?> prev;
private @Nullable Node<?> next;

Node(Object referent, CleaningAction<T> action) {
super(referent, queue);
this.action = action;
}

@Override
public void clean() throws T {
onClean(false);
}

@Override
public void onClean(boolean leak) throws T {
if (!remove(this)) {
return;
}
if (action != null) {
action.onClean(leak);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import software.amazon.jdbc.cleanup.CanReleaseResources;
import software.amazon.jdbc.profile.ConfigurationProfile;
import software.amazon.jdbc.util.FullServicesContainer;
import software.amazon.jdbc.util.LazyCleaner;
import software.amazon.jdbc.util.Messages;
import software.amazon.jdbc.util.SqlState;
import software.amazon.jdbc.util.WrapperUtils;
Expand All @@ -63,6 +64,7 @@ public class ConnectionWrapper implements Connection, CanReleaseResources {
protected final String originalUrl;
protected @Nullable ConfigurationProfile configurationProfile;
protected @Nullable Throwable openConnectionStacktrace;
private LazyCleaner.@NonNull Cleanable<?> cleanable;

public ConnectionWrapper(
@NonNull final FullServicesContainer servicesContainer,
Expand All @@ -71,8 +73,10 @@ public ConnectionWrapper(
@NonNull final String targetDriverProtocol,
@Nullable final ConfigurationProfile configurationProfile)
throws SQLException {

this.pluginManager = servicesContainer.getConnectionPluginManager();
this.pluginService = servicesContainer.getPluginService();
this.cleanable = LazyCleaner.getInstance().register(this, new CleanupAction(openConnectionStacktrace, pluginManager, pluginService));
this.hostListProviderService = servicesContainer.getHostListProviderService();
this.pluginManagerService = servicesContainer.getPluginManagerService();
this.originalUrl = url;
Expand Down Expand Up @@ -125,9 +129,10 @@ protected void init(final Properties props) throws SQLException {
}

public void releaseResources() {
this.pluginManager.releaseResources();
if (this.pluginService instanceof CanReleaseResources) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we clean up pluginManager similar way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we would have to pass pluginManager into LazyCleaner, but sure

((CanReleaseResources) this.pluginService).releaseResources();
try {
this.cleanable.clean();
} catch (Throwable e) {
// ignore
}
}

Expand Down Expand Up @@ -187,6 +192,11 @@ public void close() throws SQLException {
this.pluginService.getSessionStateService().complete();
this.pluginService.getSessionStateService().reset();
}
try {
this.cleanable.clean();
} catch (Throwable e) {
// Ignore cleanup exceptions
}
this.openConnectionStacktrace = null;
this.pluginManagerService.setInTransaction(false);
});
Expand All @@ -200,10 +210,14 @@ public void close() throws SQLException {
this.pluginService.getSessionStateService().complete();
this.pluginService.getSessionStateService().reset();
}
try {
this.cleanable.clean();
} catch (Throwable e) {
// Ignore cleanup exceptions
}
this.openConnectionStacktrace = null;
this.pluginManagerService.setInTransaction(false);
}
this.releaseResources();
}

@Override
Expand Down Expand Up @@ -1138,27 +1152,38 @@ public String toString() {
return super.toString() + " - " + this.pluginService.getCurrentConnection();
}

@SuppressWarnings("checkstyle:NoFinalizer")
protected void finalize() throws Throwable {
public Connection getCurrentConnection() {
return this.pluginService.getCurrentConnection();
}

try {
if (this.openConnectionStacktrace != null) {
private static class CleanupAction implements LazyCleaner.CleaningAction<RuntimeException> {
private final Throwable openConnectionStacktrace;
private final PluginService pluginService;
private final ConnectionPluginManager pluginManager;

CleanupAction(Throwable openConnectionStacktrace, ConnectionPluginManager pluginManager, PluginService pluginService) {
this.openConnectionStacktrace = openConnectionStacktrace;
this.pluginService = pluginService;
this.pluginManager = pluginManager;
}

@Override
public void onClean(boolean leak) {
if (leak) {
LOGGER.log(
Level.WARNING,
this.openConnectionStacktrace,
() -> Messages.get(
"ConnectionWrapper.finalizingUnclosedConnection"));
this.openConnectionStacktrace = null;
openConnectionStacktrace,
() -> Messages.get("ConnectionWrapper.finalizingUnclosedConnection"));
}

this.releaseResources();

} finally {
super.finalize();
try {
if (pluginService instanceof CanReleaseResources) {
((CanReleaseResources) pluginService).releaseResources();
}
pluginManager.releaseResources();
} catch (Exception e) {
// Ignore exceptions during cleanup
}
}
}

public Connection getCurrentConnection() {
return this.pluginService.getCurrentConnection();
}
}
Loading
Loading