Skip to content

Add support for Connection.abort #233

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

Closed
wants to merge 1 commit into from
Closed
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
27 changes: 25 additions & 2 deletions src/main/java/org/tarantool/jdbc/SQLConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLNonTransientConnectionException;
import java.sql.SQLNonTransientException;
import java.sql.SQLPermission;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
Expand All @@ -42,6 +43,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;

/**
Expand All @@ -51,6 +53,9 @@
*/
public class SQLConnection implements TarantoolConnection {

private static final SQLPermission CALL_ABORT_PERMISSION = new SQLPermission("callAbort");
private static final SQLPermission SET_NETWORK_TIMEOUT_PERMISSION = new SQLPermission("setNetworkTimeout");

private static final int UNSET_HOLDABILITY = 0;
private static final String PING_QUERY = "SELECT 1";

Expand All @@ -60,6 +65,8 @@ public class SQLConnection implements TarantoolConnection {
private DatabaseMetaData cachedMetadata;
private int resultSetHoldability = UNSET_HOLDABILITY;

private final AtomicBoolean isClosed = new AtomicBoolean(false);

public SQLConnection(String url, Properties properties) throws SQLException {
this.url = url;
this.properties = properties;
Expand Down Expand Up @@ -205,6 +212,12 @@ public boolean getAutoCommit() throws SQLException {

@Override
public void close() throws SQLException {
if (isClosed.compareAndSet(false, true)) {
closeInternal();
}
}

private void closeInternal() {
client.close();
}

Expand Down Expand Up @@ -234,7 +247,7 @@ public void rollback(Savepoint savepoint) throws SQLException {

@Override
public boolean isClosed() throws SQLException {
return client.isClosed();
return isClosed.get() || client.isClosed();
}

@Override
Expand Down Expand Up @@ -417,6 +430,7 @@ public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLExc
if (milliseconds < 0) {
throw new SQLException("Network timeout cannot be negative.");
}
SET_NETWORK_TIMEOUT_PERMISSION.checkGuard(this);
client.setOperationTimeout(milliseconds);
}

Expand Down Expand Up @@ -515,7 +529,16 @@ public void abort(Executor executor) throws SQLException {
if (isClosed()) {
return;
}
throw new SQLFeatureNotSupportedException();
if (executor == null) {
throw new SQLNonTransientException(
"Executor cannot be null",
SQLStates.INVALID_PARAMETER_VALUE.getSqlState()
);
}
CALL_ABORT_PERMISSION.checkGuard(this);
if (isClosed.compareAndSet(false, true)) {
executor.execute(this::closeInternal);
}
}

@Override
Expand Down
85 changes: 85 additions & 0 deletions src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.tarantool.TestAssumptions.assumeMinimalServerVersion;

import org.tarantool.ServerVersion;
import org.tarantool.TarantoolTestHelper;
import org.tarantool.util.SQLStates;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
Expand All @@ -26,8 +29,14 @@
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLNonTransientException;
import java.sql.Statement;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class JdbcConnectionIT {

Expand Down Expand Up @@ -456,5 +465,81 @@ void testSetClientInfoProperties() {
assertEquals(ClientInfoStatus.REASON_UNKNOWN_PROPERTY, failedProperties.get(targetProperty));
}

@Test
void testConnectionAbort() throws SQLException {
assertFalse(conn.isClosed());
try (Statement statement = conn.createStatement()) {
conn.abort(Executors.newSingleThreadExecutor());
assertTrue(conn.isClosed());
SQLNonTransientException exception = assertThrows(
SQLNonTransientException.class,
() -> statement.executeQuery("SELECT 1")
);
assertEquals(exception.getMessage(), "Statement is closed.");
}
}

@Test
void testOperationInProgressAbort() throws SQLException, ExecutionException, InterruptedException {
testHelper.executeLua("box.internal.sql_create_function('TNT_SLEEP', 'INT'," +
Copy link
Member

Choose a reason for hiding this comment

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

" function(s) require('fiber').sleep(s); return s; end)");
final ExecutorService executor = Executors.newFixedThreadPool(2);
final int sleepSeconds = 10;

long startTime = System.currentTimeMillis();

Future<SQLException> workerFuture = executor.submit(() -> {
try {
Statement statement = conn.createStatement();
statement.execute("SELECT tnt_sleep(" + sleepSeconds + ")");
} catch (SQLException cause) {
return cause;
}
return null;
});

Future<SQLException> abortFuture = executor.submit(() -> {
ExecutorService abortExecutor = Executors.newSingleThreadExecutor();
try {
conn.abort(abortExecutor);
} catch (SQLException cause) {
return cause;
}
abortExecutor.shutdown();
try {
abortExecutor.awaitTermination(sleepSeconds, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
}
return null;
});

SQLException workerException = workerFuture.get();
long endTime = System.currentTimeMillis();
assertNotNull(workerException, "Statement execution should have been aborted, thus throwing an exception");

SQLException abortException = abortFuture.get();
assertNull(abortException, () -> abortException.getMessage());

// It is expected to abort the statement as soon as possible.
// If the execution takes time more than 95% of the estimation the aborting fails.
assertTrue((endTime - startTime) < (sleepSeconds * 95 * 10));
assertTrue(conn.isClosed());
}

@Test
void testAlreadyClosedConnectionAbort() throws SQLException {
conn.close();
try {
conn.abort(Executors.newSingleThreadExecutor());
} catch (SQLException cause) {
fail("Unexpected error", cause);
}
}

@Test
void testNullParameterConnectionAbort() {
SQLException exception = assertThrows(SQLException.class, () -> conn.abort(null));
assertEquals(SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), exception.getSQLState());
}
}

150 changes: 150 additions & 0 deletions src/test/java/org/tarantool/jdbc/JdbcSecurityIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.tarantool.jdbc;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.tarantool.TestAssumptions.assumeMinimalServerVersion;

import org.tarantool.ServerVersion;
import org.tarantool.TarantoolTestHelper;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.security.Permission;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.concurrent.Executors;

public class JdbcSecurityIT {

private static TarantoolTestHelper testHelper;

private Connection connection;
private SecurityManager originalSecurityManager;

@BeforeAll
public static void setupEnv() {
testHelper = new TarantoolTestHelper("jdbc-security-it");
testHelper.createInstance();
testHelper.startInstance();
}

@AfterAll
public static void teardownEnv() {
testHelper.stopInstance();
}

@BeforeEach
public void setUpTest() throws SQLException {
assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1);
connection = DriverManager.getConnection(SqlTestUtils.makeDefaultJdbcUrl());
originalSecurityManager = System.getSecurityManager();
}

@AfterEach
public void tearDownTest() throws SQLException {
assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1);
if (connection != null) {
connection.close();
}
System.setSecurityManager(originalSecurityManager);
}

@Test
void testDeniedConnectionAbort() {
EnumSet<JdbcPermission> exclusions = EnumSet.of(JdbcPermission.CALL_ABORT);
System.setSecurityManager(new JdbcSecurityManager(true, exclusions));

SecurityException securityException = assertThrows(
SecurityException.class,
() -> connection.abort(Executors.newSingleThreadExecutor())
);
assertEquals(securityException.getMessage(), "Permission callAbort is not allowed");
}

@Test
void testDeniedSetConnectionTimeout() {
EnumSet<JdbcPermission> exclusions = EnumSet.of(JdbcPermission.SET_NETWORK_TIMEOUT);
System.setSecurityManager(new JdbcSecurityManager(true, exclusions));

SecurityException securityException = assertThrows(
SecurityException.class,
() -> connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 1000)
);
assertEquals(securityException.getMessage(), "Permission setNetworkTimeout is not allowed");
}

/**
* Lists permissions supported by JDBC API.
*
* <ul>
* <li>setLog</li>
* <li>callAbort</li>
* <li>setSyncFactory<</li>
* <li>setNetworkTimeout</li>
* <li>deregisterDriver</li>
* </ul>
*
* @see java.sql.SQLPermission
*/
private enum JdbcPermission {
SET_LOG("setLog"),
CALL_ABORT("callAbort"),
SET_SYNC_FACTORY("setSyncFactory"),
SET_NETWORK_TIMEOUT("setNetworkTimeout"),
DEREGISTER_DRIVER("deregisterDriver");

private final String permissionName;

JdbcPermission(String permissionName) {
this.permissionName = permissionName;
}

public String getPermissionName() {
return permissionName;
}

public static JdbcPermission fromName(String name) {
for (JdbcPermission values : JdbcPermission.values()) {
if (values.permissionName.equals(name)) {
return values;
}
}
return null;
}
}

private static class JdbcSecurityManager extends SecurityManager {
private final boolean allowAll;
private final EnumSet<JdbcPermission> exclusions;

/**
* Configures a new {@link SecurityManager} that follows the custom rules.
*
* @param allowAll whether permissions are allowed by default or not
* @param exclusions optional set of exclusions
*/
private JdbcSecurityManager(boolean allowAll, EnumSet<JdbcPermission> exclusions) {
this.exclusions = exclusions;
this.allowAll = allowAll;
}

@Override
public void checkPermission(Permission permission) {
JdbcPermission jdbcPermission = JdbcPermission.fromName(permission.getName());
if (jdbcPermission == null) {
return;
}
boolean allowed = allowAll ^ exclusions.contains(jdbcPermission);
if (!allowed) {
throw new SecurityException("Permission " + jdbcPermission.getPermissionName() + " is not allowed");
}
}
}
}