diff --git a/console/src/main/java/com/arcadedb/console/Console.java b/console/src/main/java/com/arcadedb/console/Console.java index 2eed2b193e..6acac48257 100644 --- a/console/src/main/java/com/arcadedb/console/Console.java +++ b/console/src/main/java/com/arcadedb/console/Console.java @@ -217,7 +217,7 @@ private void executeBegin() { if (localDatabase != null) localDatabase.begin(); else - remoteDatabase.command("SQL", "begin"); + remoteDatabase.begin(); } private void executeCommit() { @@ -225,7 +225,7 @@ private void executeCommit() { if (localDatabase != null) localDatabase.commit(); else - remoteDatabase.command("SQL", "commit"); + remoteDatabase.commit(); } private void executeRollback() { @@ -233,7 +233,7 @@ private void executeRollback() { if (localDatabase != null) localDatabase.rollback(); else - remoteDatabase.command("SQL", "rollback"); + remoteDatabase.rollback(); } private void executeClose() { diff --git a/console/src/test/java/com/arcadedb/console/BaseGraphServerTest.java b/console/src/test/java/com/arcadedb/console/BaseGraphServerTest.java index be967ef75b..45947c2059 100644 --- a/console/src/test/java/com/arcadedb/console/BaseGraphServerTest.java +++ b/console/src/test/java/com/arcadedb/console/BaseGraphServerTest.java @@ -310,7 +310,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) databases[i].drop(); } @@ -322,7 +322,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); } diff --git a/console/src/test/java/com/arcadedb/console/RemoteConsoleIT.java b/console/src/test/java/com/arcadedb/console/RemoteConsoleIT.java index 87f2601fa6..f1b85ea9b3 100644 --- a/console/src/test/java/com/arcadedb/console/RemoteConsoleIT.java +++ b/console/src/test/java/com/arcadedb/console/RemoteConsoleIT.java @@ -15,7 +15,9 @@ */ package com.arcadedb.console; +import com.arcadedb.GlobalConfiguration; import com.arcadedb.remote.RemoteException; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -30,6 +32,11 @@ public class RemoteConsoleIT extends BaseGraphServerTest { private static final String URL_WRONGPASSWD = "remote:localhost/console root wrong"; private static Console console; + public void setTestConfiguration() { + super.setTestConfiguration(); + GlobalConfiguration.SERVER_HTTP_TX_EXPIRE_TIMEOUT.setValue(3); + } + @Test public void testConnect() throws IOException { Assertions.assertTrue(console.parse("connect " + URL, false)); @@ -64,12 +71,7 @@ public void testCreateType() throws IOException { Assertions.assertTrue(console.parse("create document type Person2", false)); final StringBuilder buffer = new StringBuilder(); - console.setOutput(new ConsoleOutput() { - @Override - public void onOutput(final String output) { - buffer.append(output); - } - }); + console.setOutput(output -> buffer.append(output)); Assertions.assertTrue(console.parse("info types", false)); Assertions.assertTrue(buffer.toString().contains("Person2")); Assertions.assertTrue(console.parse("drop type Person2", false)); @@ -82,45 +84,64 @@ public void testInsertAndSelectRecord() throws IOException { Assertions.assertTrue(console.parse("insert into Person2 set name = 'Jay', lastname='Miner'", false)); final StringBuilder buffer = new StringBuilder(); - console.setOutput(new ConsoleOutput() { - @Override - public void onOutput(final String output) { - buffer.append(output); - } - }); + console.setOutput(output -> buffer.append(output)); Assertions.assertTrue(console.parse("select from Person2", false)); Assertions.assertTrue(buffer.toString().contains("Jay")); Assertions.assertTrue(console.parse("drop type Person2", false)); } -// -// @Test -// public void testInsertAndRollback() throws IOException { -// Assertions.assertTrue(console.parse("connect " + URL)); -// Assertions.assertTrue(console.parse("begin")); -// Assertions.assertTrue(console.parse("create document type Person")); -// Assertions.assertTrue(console.parse("insert into Person set name = 'Jay', lastname='Miner'")); -// Assertions.assertTrue(console.parse("rollback")); -// -// final StringBuilder buffer = new StringBuilder(); -// console.setOutput(new ConsoleOutput() { -// @Override -// public void onOutput(final String output) { -// buffer.append(output); -// } -// }); -// Assertions.assertTrue(console.parse("select from Person")); -// Assertions.assertFalse(buffer.toString().contains("Jay")); -// } + + @Test + public void testInsertAndRollback() throws IOException { + Assertions.assertTrue(console.parse("connect " + URL, false)); + Assertions.assertTrue(console.parse("begin", false)); + Assertions.assertTrue(console.parse("create document type Person", false)); + Assertions.assertTrue(console.parse("insert into Person set name = 'Jay', lastname='Miner'", false)); + Assertions.assertTrue(console.parse("rollback", false)); + + final StringBuilder buffer = new StringBuilder(); + console.setOutput(output -> buffer.append(output)); + Assertions.assertTrue(console.parse("select from Person", false)); + Assertions.assertFalse(buffer.toString().contains("Jay")); + } + + @Test + public void testInsertAndCommit() throws IOException { + Assertions.assertTrue(console.parse("connect " + URL, false)); + Assertions.assertTrue(console.parse("begin", false)); + Assertions.assertTrue(console.parse("create document type Person", false)); + Assertions.assertTrue(console.parse("insert into Person set name = 'Jay', lastname='Miner'", false)); + Assertions.assertTrue(console.parse("commit", false)); + + final StringBuilder buffer = new StringBuilder(); + console.setOutput(output -> buffer.append(output)); + Assertions.assertTrue(console.parse("select from Person", false)); + Assertions.assertTrue(buffer.toString().contains("Jay")); + } + + @Test + public void testTransactionExpired() throws IOException, InterruptedException { + Assertions.assertTrue(console.parse("connect " + URL, false)); + Assertions.assertTrue(console.parse("begin", false)); + Assertions.assertTrue(console.parse("create document type Person", false)); + Assertions.assertTrue(console.parse("insert into Person set name = 'Jay', lastname='Miner'", false)); + Thread.sleep(5000); + try { + Assertions.assertTrue(console.parse("commit", false)); + Assertions.fail(); + } catch (Exception e) { + // EXPECTED + } + + final StringBuilder buffer = new StringBuilder(); + console.setOutput(output -> buffer.append(output)); + Assertions.assertTrue(console.parse("select from Person", false)); + Assertions.assertFalse(buffer.toString().contains("Jay")); + } @Test public void testHelp() throws IOException { final StringBuilder buffer = new StringBuilder(); - console.setOutput(new ConsoleOutput() { - @Override - public void onOutput(final String output) { - buffer.append(output); - } - }); + console.setOutput(output -> buffer.append(output)); Assertions.assertTrue(console.parse("?", false)); Assertions.assertTrue(buffer.toString().contains("quit")); } @@ -155,4 +176,9 @@ public void endTest() { if (console != null) console.close(); } + + @AfterAll + public static void afterAll() { + GlobalConfiguration.SERVER_HTTP_TX_EXPIRE_TIMEOUT.setValue(GlobalConfiguration.SERVER_HTTP_TX_EXPIRE_TIMEOUT.getDefValue()); + } } diff --git a/engine/src/main/java/com/arcadedb/GlobalConfiguration.java b/engine/src/main/java/com/arcadedb/GlobalConfiguration.java index 3050d61916..f315b4ae74 100644 --- a/engine/src/main/java/com/arcadedb/GlobalConfiguration.java +++ b/engine/src/main/java/com/arcadedb/GlobalConfiguration.java @@ -221,6 +221,9 @@ public Object call(final Object value) { SERVER_HTTP_AUTOINCREMENT_PORT("arcadedb.server.httpAutoIncrementPort", "True to increment the TCP/IP port number used for incoming HTTP in case the configured is not available", Boolean.class, true), + SERVER_HTTP_TX_EXPIRE_TIMEOUT("arcadedb.server.httpTxExpireTimeout", + "Timeout in seconds for a HTTP transaction to expire. This timeout is computed from the latest command against the transaction", Long.class, 30), + // SERVER SECURITY SERVER_SECURITY_ALGORITHM("arcadedb.server.securityAlgorithm", "Default encryption algorithm used for passwords hashing", String.class, "PBKDF2WithHmacSHA256"), diff --git a/engine/src/main/java/com/arcadedb/database/DatabaseContext.java b/engine/src/main/java/com/arcadedb/database/DatabaseContext.java index 2766a116af..db9c688d32 100644 --- a/engine/src/main/java/com/arcadedb/database/DatabaseContext.java +++ b/engine/src/main/java/com/arcadedb/database/DatabaseContext.java @@ -25,6 +25,10 @@ */ public class DatabaseContext extends ThreadLocal> { public DatabaseContextTL init(final DatabaseInternal database) { + return init(database, null); + } + + public DatabaseContextTL init(final DatabaseInternal database, final TransactionContext firstTransaction) { Map map = get(); final String key = database.getDatabasePath(); @@ -57,7 +61,7 @@ public DatabaseContextTL init(final DatabaseInternal database) { } if (current.transactions.isEmpty()) - current.transactions.add(new TransactionContext(database.getWrappedDatabaseInstance())); + current.transactions.add(firstTransaction != null ? firstTransaction : new TransactionContext(database.getWrappedDatabaseInstance())); return current; } diff --git a/engine/src/main/java/com/arcadedb/database/EmbeddedDatabase.java b/engine/src/main/java/com/arcadedb/database/EmbeddedDatabase.java index b49fe8bf6c..87c5cfc68c 100644 --- a/engine/src/main/java/com/arcadedb/database/EmbeddedDatabase.java +++ b/engine/src/main/java/com/arcadedb/database/EmbeddedDatabase.java @@ -246,30 +246,6 @@ private void openInternal() { } } - private void checkForRecovery() throws IOException { - lockFile = new File(databasePath + "/database.lck"); - - if (lockFile.exists()) { - lockDatabase(); - - // RECOVERY - LogManager.instance().log(this, Level.WARNING, "Database '%s' was not closed properly last time", null, name); - - if (mode == PaginatedFile.MODE.READ_ONLY) - throw new DatabaseMetadataException("Database needs recovery but has been open in read only mode"); - - executeCallbacks(CALLBACK_EVENT.DB_NOT_CLOSED); - - transactionManager.checkIntegrity(); - } else { - if (mode == PaginatedFile.MODE.READ_WRITE) { - lockFile.createNewFile(); - lockDatabase(); - } else - lockFile = null; - } - } - @Override public void drop() { checkDatabaseIsOpen(); @@ -1580,4 +1556,27 @@ private void internalClose(final boolean drop) { DatabaseFactory.removeActiveDatabaseInstance(databasePath); } + private void checkForRecovery() throws IOException { + lockFile = new File(databasePath + "/database.lck"); + + if (lockFile.exists()) { + lockDatabase(); + + // RECOVERY + LogManager.instance().log(this, Level.WARNING, "Database '%s' was not closed properly last time", null, name); + + if (mode == PaginatedFile.MODE.READ_ONLY) + throw new DatabaseMetadataException("Database needs recovery but has been open in read only mode"); + + executeCallbacks(CALLBACK_EVENT.DB_NOT_CLOSED); + + transactionManager.checkIntegrity(); + } else { + if (mode == PaginatedFile.MODE.READ_WRITE) { + lockFile.createNewFile(); + lockDatabase(); + } else + lockFile = null; + } + } } diff --git a/mongodbw/src/test/java/com/arcadedb/mongo/BaseGraphServerTest.java b/mongodbw/src/test/java/com/arcadedb/mongo/BaseGraphServerTest.java index 4e749098bf..8dc22cbe9b 100644 --- a/mongodbw/src/test/java/com/arcadedb/mongo/BaseGraphServerTest.java +++ b/mongodbw/src/test/java/com/arcadedb/mongo/BaseGraphServerTest.java @@ -332,7 +332,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) ((ServerDatabase) databases[i]).getWrappedDatabaseInstance().drop(); } @@ -345,7 +345,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); } diff --git a/network/src/main/java/com/arcadedb/network/http/HttpUtils.java b/network/src/main/java/com/arcadedb/network/http/HttpUtils.java new file mode 100644 index 0000000000..b053c352bd --- /dev/null +++ b/network/src/main/java/com/arcadedb/network/http/HttpUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Arcade Data Ltd + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.arcadedb.network.http; + +/** + * @author Luca Garulli (l.garulli@arcadedata.com) + **/ +public class HttpUtils { + public static final String ARCADEDB_SESSION_ID = "arcadedb-session-id"; +} diff --git a/network/src/main/java/com/arcadedb/remote/RemoteDatabase.java b/network/src/main/java/com/arcadedb/remote/RemoteDatabase.java index b5cdeba40d..c2ee083c6c 100644 --- a/network/src/main/java/com/arcadedb/remote/RemoteDatabase.java +++ b/network/src/main/java/com/arcadedb/remote/RemoteDatabase.java @@ -28,6 +28,7 @@ import com.arcadedb.log.LogManager; import com.arcadedb.network.binary.QuorumNotReachedException; import com.arcadedb.network.binary.ServerIsNotTheLeaderException; +import com.arcadedb.network.http.HttpUtils; import com.arcadedb.query.sql.executor.InternalResultSet; import com.arcadedb.query.sql.executor.ResultInternal; import com.arcadedb.query.sql.executor.ResultSet; @@ -50,7 +51,7 @@ public class RemoteDatabase extends RWLockContext { private final int originalPort; private int apiVersion = 1; private final ContextConfiguration configuration; - private final String name; + private final String databaseName; private final String userName; private final String userPassword; private final List> replicaServerList = new ArrayList<>(); @@ -62,12 +63,17 @@ public class RemoteDatabase extends RWLockContext { private int timeout = 5000; private static final String protocol = "http"; private static final String charset = "UTF-8"; + private String sessionId; - public RemoteDatabase(final String server, final int port, final String name, final String userName, final String userPassword) { - this(server, port, name, userName, userPassword, new ContextConfiguration()); + public enum CONNECTION_STRATEGY { + STICKY, ROUND_ROBIN } - public RemoteDatabase(final String server, final int port, final String name, final String userName, final String userPassword, + public RemoteDatabase(final String server, final int port, final String databaseName, final String userName, final String userPassword) { + this(server, port, databaseName, userName, userPassword, new ContextConfiguration()); + } + + public RemoteDatabase(final String server, final int port, final String databaseName, final String userName, final String userPassword, final ContextConfiguration configuration) { this.originalServer = server; this.originalPort = port; @@ -75,7 +81,7 @@ public RemoteDatabase(final String server, final int port, final String name, fi this.currentServer = originalServer; this.currentPort = originalPort; - this.name = name; + this.databaseName = databaseName; this.userName = userName; this.userPassword = userPassword; @@ -86,7 +92,7 @@ public RemoteDatabase(final String server, final int port, final String name, fi } public String getName() { - return name; + return databaseName; } public void create() { @@ -110,6 +116,50 @@ public void drop() { close(); } + public void begin() { + if (sessionId != null) + throw new TransactionException("Transaction already begun"); + + try { + final HttpURLConnection connection = createConnection(getUrl("begin", databaseName)); + connection.connect(); + if (connection.getResponseCode() != 204) + throw new TransactionException("Error on transaction begin"); + sessionId = connection.getHeaderField(HttpUtils.ARCADEDB_SESSION_ID); + } catch (Exception e) { + throw new TransactionException("Error on transaction begin", e); + } + } + + public void commit() { + if (sessionId == null) + throw new TransactionException("Transaction not begun"); + try { + final HttpURLConnection connection = createConnection(getUrl("commit", databaseName)); + connection.connect(); + if (connection.getResponseCode() != 204) + throw new TransactionException("Error on transaction commit"); + sessionId = null; + } catch (Exception e) { + throw new TransactionException("Error on transaction commit", e); + } + } + + public void rollback() { + if (sessionId == null) + throw new TransactionException("Transaction not begun"); + try { + final HttpURLConnection connection = createConnection(getUrl("rollback", databaseName)); + connection.connect(); + if (connection.getResponseCode() != 204) + throw new TransactionException("Error on transaction rollback"); + + sessionId = null; + } catch (Exception e) { + throw new TransactionException("Error on transaction rollback", e); + } + } + public ResultSet command(final String language, final String command, final Object... args) { Map params = mapArgs(args); @@ -126,21 +176,6 @@ public ResultSet command(final String language, final String command, final Obje }); } - private Map mapArgs(Object[] args) { - Map params = null; - if (args != null && args.length > 0) { - if (args.length == 1 && args[0] instanceof Map) - params = (Map) args[0]; - else { - params = new HashMap<>(); - for (Object o : args) { - params.put("" + params.size(), o); - } - } - } - return params; - } - public ResultSet query(final String language, final String command, final Object... args) { Map params = mapArgs(args); @@ -160,10 +195,6 @@ public Object call(final HttpURLConnection connection, final JSONObject response }); } - public void rollback() { - command("SQL", "rollback"); - } - public CONNECTION_STRATEGY getConnectionStrategy() { return connectionStrategy; } @@ -182,7 +213,7 @@ public void setTimeout(final int timeout) { @Override public String toString() { - return name; + return databaseName; } private Object serverCommand(final String operation, final String language, final String payloadCommand, final Map params, @@ -192,7 +223,7 @@ private Object serverCommand(final String operation, final String language, fina private Object databaseCommand(final String operation, final String language, final String payloadCommand, final Map params, final boolean requiresLeader, final Callback callback) { - return httpCommand(name, operation, language, payloadCommand, params, requiresLeader, true, callback); + return httpCommand(databaseName, operation, language, payloadCommand, params, requiresLeader, true, callback); } private Object httpCommand(final String extendedURL, final String operation, final String language, final String payloadCommand, @@ -211,7 +242,8 @@ private Object httpCommand(final String extendedURL, final String operation, fin url += "/" + extendedURL; try { - final HttpURLConnection connection = connect(url); + final HttpURLConnection connection = createConnection(url); + connection.setDoOutput(true); try { if (payloadCommand != null) { @@ -232,8 +264,6 @@ private Object httpCommand(final String extendedURL, final String operation, fin } } - connection.setConnectTimeout(timeout); - connection.setReadTimeout(timeout); connection.connect(); if (connection.getResponseCode() != 200) { @@ -344,7 +374,19 @@ private Object httpCommand(final String extendedURL, final String operation, fin throw new RemoteException("Error on executing remote operation " + operation, lastException); } - protected HttpURLConnection connect(final String url) throws IOException { + public int getApiVersion() { + return apiVersion; + } + + public void setApiVersion(final int apiVersion) { + this.apiVersion = apiVersion; + } + + public interface Callback { + Object call(HttpURLConnection iArgument, JSONObject response) throws Exception; + } + + protected HttpURLConnection createConnection(final String url) throws IOException { final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestProperty("charset", "utf-8"); connection.setRequestMethod("POST"); @@ -352,7 +394,12 @@ protected HttpURLConnection connect(final String url) throws IOException { final String authorization = userName + ":" + userPassword; connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString(authorization.getBytes(DatabaseFactory.getDefaultCharset()))); - connection.setDoOutput(true); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + + if (sessionId != null) + connection.setRequestProperty(HttpUtils.ARCADEDB_SESSION_ID, sessionId); + return connection; } @@ -412,7 +459,7 @@ private Pair getNextReplicaAddress() { private boolean reloadClusterConfiguration() { final Pair oldLeader = leaderServer; - // ASK TO REPLICA FIRST + // ASK REPLICA FIRST for (int replicaIdx = 0; replicaIdx < replicaServerList.size(); ++replicaIdx) { Pair connectToServer = replicaServerList.get(replicaIdx); @@ -443,19 +490,22 @@ private boolean reloadClusterConfiguration() { return leaderServer != null; } - public int getApiVersion() { - return apiVersion; - } - - public void setApiVersion(final int apiVersion) { - this.apiVersion = apiVersion; - } - - public enum CONNECTION_STRATEGY { - STICKY, ROUND_ROBIN + private Map mapArgs(Object[] args) { + Map params = null; + if (args != null && args.length > 0) { + if (args.length == 1 && args[0] instanceof Map) + params = (Map) args[0]; + else { + params = new HashMap<>(); + for (Object o : args) { + params.put("" + params.size(), o); + } + } + } + return params; } - public interface Callback { - Object call(HttpURLConnection iArgument, JSONObject response) throws Exception; + private String getUrl(final String command, final String databaseName) { + return protocol + "://" + currentServer + ":" + currentPort + "/api/v" + apiVersion + "/" + command + "/" + databaseName; } } diff --git a/postgresw/src/test/java/com/arcadedb/postgres/BaseGraphServerTest.java b/postgresw/src/test/java/com/arcadedb/postgres/BaseGraphServerTest.java index 6c658b005b..289ef17884 100644 --- a/postgresw/src/test/java/com/arcadedb/postgres/BaseGraphServerTest.java +++ b/postgresw/src/test/java/com/arcadedb/postgres/BaseGraphServerTest.java @@ -242,7 +242,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) databases[i].drop(); } @@ -255,7 +255,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); } diff --git a/redisw/src/test/java/com/arcadedb/redis/BaseGraphServerTest.java b/redisw/src/test/java/com/arcadedb/redis/BaseGraphServerTest.java index b03fe4d2a3..7acd8feb47 100644 --- a/redisw/src/test/java/com/arcadedb/redis/BaseGraphServerTest.java +++ b/redisw/src/test/java/com/arcadedb/redis/BaseGraphServerTest.java @@ -304,7 +304,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) ((ServerDatabase) databases[i]).getWrappedDatabaseInstance().drop(); } @@ -317,7 +317,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); } diff --git a/server/src/main/java/com/arcadedb/server/http/HttpServer.java b/server/src/main/java/com/arcadedb/server/http/HttpServer.java index 7e7f847bb8..c5df5c449e 100644 --- a/server/src/main/java/com/arcadedb/server/http/HttpServer.java +++ b/server/src/main/java/com/arcadedb/server/http/HttpServer.java @@ -21,16 +21,19 @@ import com.arcadedb.server.ArcadeDBServer; import com.arcadedb.server.ServerException; import com.arcadedb.server.ServerPlugin; +import com.arcadedb.server.http.handler.GetDatabasesHandler; +import com.arcadedb.server.http.handler.GetDocumentHandler; +import com.arcadedb.server.http.handler.GetDynamicContentHandler; +import com.arcadedb.server.http.handler.GetExistsDatabaseHandler; +import com.arcadedb.server.http.handler.GetQueryHandler; +import com.arcadedb.server.http.handler.PostBeginHandler; import com.arcadedb.server.http.handler.PostCommandHandler; +import com.arcadedb.server.http.handler.PostCommitHandler; import com.arcadedb.server.http.handler.PostCreateDatabaseHandler; import com.arcadedb.server.http.handler.PostCreateDocumentHandler; import com.arcadedb.server.http.handler.PostDropDatabaseHandler; -import com.arcadedb.server.http.handler.GetDynamicContentHandler; -import com.arcadedb.server.http.handler.GetExistsDatabaseHandler; -import com.arcadedb.server.http.handler.GetDatabasesHandler; -import com.arcadedb.server.http.handler.GetDocumentHandler; -import com.arcadedb.server.http.handler.GetQueryHandler; import com.arcadedb.server.http.handler.PostQueryHandler; +import com.arcadedb.server.http.handler.PostRollbackHandler; import com.arcadedb.server.http.handler.PostServersHandler; import io.undertow.Handlers; import io.undertow.Undertow; @@ -43,15 +46,17 @@ import static io.undertow.UndertowOptions.SHUTDOWN_TIMEOUT; public class HttpServer implements ServerPlugin { - private Undertow undertow; - private final JsonSerializer jsonSerializer = new JsonSerializer(); - private final ArcadeDBServer server; - private String listeningAddress; - private String host; - private int port; + private final ArcadeDBServer server; + private final HttpTransactionManager transactionManager; + private final JsonSerializer jsonSerializer = new JsonSerializer(); + private Undertow undertow; + private String listeningAddress; + private String host; + private int port; public HttpServer(final ArcadeDBServer server) { this.server = server; + this.transactionManager = new HttpTransactionManager(server.getConfiguration().getValueAsInteger(GlobalConfiguration.SERVER_HTTP_TX_EXPIRE_TIMEOUT) * 1000); } @Override @@ -84,7 +89,9 @@ public void startService() { final RoutingHandler basicRoutes = Handlers.routing(); routes.addPrefixPath("/api/v1",// basicRoutes// + .post("/begin/{database}", new PostBeginHandler(this))// .post("/command/{database}", new PostCommandHandler(this))// + .post("/commit/{database}", new PostCommitHandler(this))// .post("/create/{database}", new PostCreateDatabaseHandler(this))// .get("/databases", new GetDatabasesHandler(this))// .get("/document/{database}/{rid}", new GetDocumentHandler(this))// @@ -93,6 +100,7 @@ public void startService() { .get("/exists/{database}", new GetExistsDatabaseHandler(this))// .get("/query/{database}/{language}/{command}", new GetQueryHandler(this))// .post("/query/{database}", new PostQueryHandler(this))// + .post("/rollback/{database}", new PostRollbackHandler(this))// .post("/server", new PostServersHandler(this))// ); @@ -128,6 +136,10 @@ public void startService() { } while (httpAutoIncrementPort); } + public HttpTransactionManager getTransactionManager() { + return transactionManager; + } + public ArcadeDBServer getServer() { return server; } diff --git a/server/src/main/java/com/arcadedb/server/http/HttpSession.java b/server/src/main/java/com/arcadedb/server/http/HttpSession.java new file mode 100644 index 0000000000..36906c3437 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/http/HttpSession.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Arcade Data Ltd + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.arcadedb.server.http; + +import com.arcadedb.database.TransactionContext; +import com.arcadedb.exception.TransactionException; +import com.arcadedb.server.security.ServerSecurityUser; + +/** + * Manage a transaction on the HTTP protocol. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class HttpSession { + public final String id; + public final TransactionContext transaction; + public final ServerSecurityUser user; + public volatile Thread currentThreadUsing; + private volatile long lastUpdate = 0L; + + public HttpSession(final ServerSecurityUser user, final String id, final TransactionContext dbTx) { + this.user = user; + this.id = id; + this.transaction = dbTx; + use(user); + } + + public long elapsedFromLastUpdate() { + return System.currentTimeMillis() - lastUpdate; + } + + public HttpSession use(final ServerSecurityUser user) { + if (currentThreadUsing != null) + throw new TransactionException("Cannot use the requested transaction because in use by a different thread"); + + if (!this.user.equals(user)) + throw new SecurityException("Cannot use the requested transaction because in use by a different user"); + + lastUpdate = System.currentTimeMillis(); + currentThreadUsing = Thread.currentThread(); + return this; + } + + public HttpSession endUsage() { + currentThreadUsing = null; + return this; + } +} diff --git a/server/src/main/java/com/arcadedb/server/http/HttpTransactionManager.java b/server/src/main/java/com/arcadedb/server/http/HttpTransactionManager.java new file mode 100644 index 0000000000..862f167786 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/http/HttpTransactionManager.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 Arcade Data Ltd + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.arcadedb.server.http; + +import com.arcadedb.database.TransactionContext; +import com.arcadedb.log.LogManager; +import com.arcadedb.server.security.ServerSecurityUser; +import com.arcadedb.utility.RWLockContext; + +import java.util.*; +import java.util.logging.*; + +/** + * Handles the stateful transactions in HTTP protocol as sessions. A HTTP transaction starts with the `/begin` command and is committed with `/commit` and + * rolled back with `/rollback`. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class HttpTransactionManager extends RWLockContext { + public static final String ARCADEDB_SESSION_ID = "arcadedb-session-id"; + private final Map sessions = new HashMap<>(); + private final long expirationTimeInMs; + private final Timer timer; + + public HttpTransactionManager(final long expirationTimeInMs) { + this.expirationTimeInMs = expirationTimeInMs; + + timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + final int expired = checkSessionsValidity(); + if (expired > 0) + LogManager.instance().log(this, Level.FINE, "Removed %d expired sessions", null, expired); + } + }, expirationTimeInMs, expirationTimeInMs); + } + + public void close() { + timer.cancel(); + sessions.clear(); + } + + public int checkSessionsValidity() { + return executeInWriteLock(() -> { + int expired = 0; + Map.Entry s; + for (Iterator> it = sessions.entrySet().iterator(); it.hasNext(); ) { + s = it.next(); + + if (s.getValue().elapsedFromLastUpdate() > expirationTimeInMs) { + // REMOVE THE SESSION + it.remove(); + expired++; + } + } + return expired; + }); + } + + public HttpSession getSessionById(final ServerSecurityUser user, final String txId) { + return executeInReadLock(() -> { + final HttpSession tx = sessions.get(txId); + if (tx != null) + tx.use(user); + return tx; + }); + } + + public HttpSession createSession(final ServerSecurityUser user, final TransactionContext dbTx) { + return executeInWriteLock(() -> { + final String id = "AS-" + UUID.randomUUID(); + final HttpSession session = new HttpSession(user, id, dbTx); + sessions.put(id, session); + return session; + }); + } + + public HttpSession removeSession(final String iSessionId) { + return executeInWriteLock(() -> sessions.remove(iSessionId)); + } +} diff --git a/server/src/main/java/com/arcadedb/server/http/handler/AbstractHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/AbstractHandler.java index 237998cd68..b34cef26e4 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/AbstractHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/AbstractHandler.java @@ -16,6 +16,7 @@ package com.arcadedb.server.http.handler; import com.arcadedb.Constants; +import com.arcadedb.GlobalConfiguration; import com.arcadedb.database.DatabaseFactory; import com.arcadedb.exception.CommandExecutionException; import com.arcadedb.exception.CommandSQLParsingException; @@ -114,19 +115,19 @@ public void handleRequest(HttpServerExchange exchange) { } } catch (ServerSecurityException e) { - LogManager.instance().log(this, Level.FINE, "Security error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Security error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(403); exchange.getResponseSender().send(error2json("Security error", e.getMessage(), e, null, null)); } catch (ServerIsNotTheLeaderException e) { - LogManager.instance().log(this, Level.FINE, "Error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(400); exchange.getResponseSender().send(error2json("Cannot execute command", e.getMessage(), e, e.getLeaderAddress(), null)); } catch (NeedRetryException e) { - LogManager.instance().log(this, Level.FINE, "Error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(503); exchange.getResponseSender().send(error2json("Cannot execute command", e.getMessage(), e, null, null)); } catch (DuplicatedKeyException e) { - LogManager.instance().log(this, Level.FINE, "Error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(503); exchange.getResponseSender() .send(error2json("Found duplicate key in index", e.getMessage(), e, e.getIndexName() + "|" + e.getKeys() + "|" + e.getCurrentIndexedRID(), null)); @@ -135,7 +136,7 @@ public void handleRequest(HttpServerExchange exchange) { if (e.getCause() != null) realException = e.getCause(); - LogManager.instance().log(this, Level.FINE, "Error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(500); exchange.getResponseSender().send(error2json("Cannot execute command", realException.getMessage(), realException, null, null)); } catch (TransactionException e) { @@ -143,11 +144,11 @@ public void handleRequest(HttpServerExchange exchange) { if (e.getCause() != null) realException = e.getCause(); - LogManager.instance().log(this, Level.FINE, "Error on transaction execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on transaction execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(500); exchange.getResponseSender().send(error2json("Error on transaction commit", realException.getMessage(), realException, null, null)); } catch (Exception e) { - LogManager.instance().log(this, Level.FINE, "Error on command execution (%s)", e, getClass().getSimpleName()); + LogManager.instance().log(this, getErrorLogLevel(), "Error on command execution (%s)", e, getClass().getSimpleName()); exchange.setStatusCode(500); exchange.getResponseSender().send(error2json("Internal error", e.getMessage(), e, null, null)); } finally { @@ -192,4 +193,8 @@ protected String error2json(final String error, final String detail, final Throw protected String encodeError(final String message) { return message.replaceAll("\\\\", " ").replaceAll("\n", " ");//.replaceAll("\"", "'"); } + + private Level getErrorLogLevel() { + return "development".equals(httpServer.getServer().getConfiguration().getValueAsString(GlobalConfiguration.SERVER_MODE)) ? Level.INFO : Level.FINE; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/DatabaseAbstractHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/DatabaseAbstractHandler.java index 7e2293e499..14cb1c5c0d 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/DatabaseAbstractHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/DatabaseAbstractHandler.java @@ -19,22 +19,30 @@ import com.arcadedb.database.DatabaseContext; import com.arcadedb.database.DatabaseInternal; import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.http.HttpSession; +import com.arcadedb.server.http.HttpTransactionManager; import com.arcadedb.server.security.ServerSecurityUser; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; +import io.undertow.util.HttpString; import java.util.*; public abstract class DatabaseAbstractHandler extends AbstractHandler { - public DatabaseAbstractHandler(final HttpServer httpServer) { + private static final HttpString SESSION_ID_HEADER = new HttpString(HttpTransactionManager.ARCADEDB_SESSION_ID); + + protected DatabaseAbstractHandler(final HttpServer httpServer) { super(httpServer); } protected abstract void execute(HttpServerExchange exchange, ServerSecurityUser user, Database database) throws Exception; @Override - public void execute(final HttpServerExchange exchange, ServerSecurityUser user) throws Exception { + public void execute(final HttpServerExchange exchange, final ServerSecurityUser user) throws Exception { final Database database; - if (openDatabase()) { + HttpSession activeSession = null; + boolean atomicTransaction = false; + if (requiresDatabase()) { final Deque databaseName = exchange.getQueryParameters().get("database"); if (databaseName.isEmpty()) { exchange.setStatusCode(400); @@ -44,22 +52,70 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user) database = httpServer.getServer().getDatabase(databaseName.getFirst()); - DatabaseContext.INSTANCE.init((DatabaseInternal) database).setCurrentUser(user.getDatabaseUser(database)); + activeSession = setTransactionInThreadLocal(exchange, database, user, false); + + if (requiresTransaction() && activeSession == null) { + atomicTransaction = true; + database.begin(); + } } else database = null; try { - execute(exchange, user, database); - } finally { - if (database != null) - database.rollbackAllNested(); + + if (activeSession != null) { + // TRANSACTION FOUND, REMOVE THE TRANSACTION TO BE REUSED IN ANOTHER REQUEST + activeSession.endUsage(); + DatabaseContext.INSTANCE.removeContext(database.getDatabasePath()); + } else if (database != null) { + if (atomicTransaction) + // STARTED ATOMIC TRANSACTION, COMMIT + database.commit(); + else + // NO TRANSACTION, ROLLBACK TO MAKE SURE ANY PENDING OPERATION IS REMOVED + database.rollbackAllNested(); + } } } - protected boolean openDatabase() { + protected boolean requiresDatabase() { + return true; + } + + protected boolean requiresTransaction() { return true; } + + protected HttpSession setTransactionInThreadLocal(final HttpServerExchange exchange, final Database database, ServerSecurityUser user, + final boolean mandatory) { + final HeaderValues sessionId = exchange.getRequestHeaders().get(HttpTransactionManager.ARCADEDB_SESSION_ID); + if (sessionId == null || sessionId.isEmpty()) { + if (mandatory) { + exchange.setStatusCode(401); + exchange.getResponseSender().send("{ \"error\" : \"Transaction id not found in request headers\" }"); + } + return null; + } + + final HttpSession session = httpServer.getTransactionManager().getSessionById(user, sessionId.getFirst()); + if (session == null) { + if (mandatory) { + exchange.setStatusCode(401); + exchange.getResponseSender().send("{ \"error\" : \"Transaction not found or expired\" }"); + return null; + } + } + + if (session != null) { + // FORCE THE RESET OF TL + final DatabaseContext.DatabaseContextTL current = DatabaseContext.INSTANCE.init((DatabaseInternal) database, session.transaction); + current.setCurrentUser(user != null ? user.getDatabaseUser(database) : null); + exchange.getResponseHeaders().put(SESSION_ID_HEADER, session.id); + } + + return session; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/GetDocumentHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/GetDocumentHandler.java index 3b668ec8f0..31876f71ef 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/GetDocumentHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/GetDocumentHandler.java @@ -42,15 +42,14 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final String[] ridParts = rid.getFirst().split(":"); - database.begin(); - try { - final Document record = (Document) database.lookupByRID(new RID(database, Integer.parseInt(ridParts[0]), Long.parseLong(ridParts[1])), true); + final Document record = (Document) database.lookupByRID(new RID(database, Integer.parseInt(ridParts[0]), Long.parseLong(ridParts[1])), true); - exchange.setStatusCode(200); - exchange.getResponseSender().send("{ \"result\" : " + httpServer.getJsonSerializer().serializeDocument(record).toString() + "}"); + exchange.setStatusCode(200); + exchange.getResponseSender().send("{ \"result\" : " + httpServer.getJsonSerializer().serializeDocument(record).toString() + "}"); + } - } finally { - database.rollbackAllNested(); - } + @Override + protected boolean requiresTransaction() { + return false; } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/GetExistsDatabaseHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/GetExistsDatabaseHandler.java index 7d4a0f0998..02ae05fa34 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/GetExistsDatabaseHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/GetExistsDatabaseHandler.java @@ -29,7 +29,7 @@ public GetExistsDatabaseHandler(final HttpServer httpServer) { } @Override - protected boolean openDatabase() { + protected boolean requiresDatabase() { return false; } @@ -50,4 +50,10 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, exchange.setStatusCode(200); exchange.getResponseSender().send("{ \"result\" : \"" + existsDatabase + "\"}"); } + + @Override + protected boolean requiresTransaction() { + return false; + } + } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/GetQueryHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/GetQueryHandler.java index db6b65e16d..d9cb9f901e 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/GetQueryHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/GetQueryHandler.java @@ -50,8 +50,6 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final StringBuilder result = new StringBuilder(); final ServerMetrics.MetricTimer timer = httpServer.getServer().getServerMetrics().timer("http.query"); - - database.begin(); try { final String command = URLDecoder.decode(text.getFirst(), exchange.getRequestCharset()); @@ -63,11 +61,15 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, } } finally { - database.rollbackAllNested(); timer.stop(); } exchange.setStatusCode(200); exchange.getResponseSender().send("{ \"result\" : [" + result + "] }"); } + + @Override + protected boolean requiresTransaction() { + return false; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostBeginHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostBeginHandler.java new file mode 100644 index 0000000000..32af305ae5 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostBeginHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 com.arcadedb.server.http.handler; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseContext; +import com.arcadedb.database.DatabaseInternal; +import com.arcadedb.database.TransactionContext; +import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.http.HttpSession; +import com.arcadedb.server.http.HttpTransactionManager; +import com.arcadedb.server.security.ServerSecurityUser; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; +import io.undertow.util.HttpString; + +import java.io.*; + +public class PostBeginHandler extends DatabaseAbstractHandler { + + public PostBeginHandler(final HttpServer httpServer) { + super(httpServer); + } + + @Override + public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final Database database) throws IOException { + final HeaderValues txId = exchange.getRequestHeaders().get(HttpTransactionManager.ARCADEDB_SESSION_ID); + if (txId != null && !txId.isEmpty()) { + final HttpSession tx = httpServer.getTransactionManager().getSessionById(user, txId.getFirst()); + if (tx != null) { + exchange.setStatusCode(401); + exchange.getResponseSender().send("{ \"error\" : \"Transaction already started\" }"); + return; + } + } + + DatabaseContext.INSTANCE.init((DatabaseInternal) database); + + database.begin(); + final TransactionContext tx = ((DatabaseInternal) database).getTransaction(); + + final HttpSession session = httpServer.getTransactionManager().createSession(user, tx).endUsage(); + + DatabaseContext.INSTANCE.removeContext(database.getDatabasePath()); + + exchange.getResponseHeaders().put(new HttpString(HttpTransactionManager.ARCADEDB_SESSION_ID), session.id); + exchange.setStatusCode(204); + exchange.getResponseSender().send(""); + } + + @Override + protected boolean requiresTransaction() { + return false; + } +} diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostCommandHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostCommandHandler.java index c2543b19ab..8a873a4b8a 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/PostCommandHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostCommandHandler.java @@ -150,14 +150,10 @@ public void execute(final HttpServerExchange exchange, final ServerSecurityUser } } - if (database.isTransactionActive()) - database.commit(); - exchange.setStatusCode(200); exchange.getResponseSender().send(response.toString()); } finally { - database.rollbackAllNested(); timer.stop(); } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostCommitHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostCommitHandler.java new file mode 100644 index 0000000000..addc4e5e3a --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostCommitHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 com.arcadedb.server.http.handler; + +import com.arcadedb.database.Database; +import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.http.HttpTransactionManager; +import com.arcadedb.server.security.ServerSecurityUser; +import io.undertow.server.HttpServerExchange; + +import java.io.*; + +public class PostCommitHandler extends DatabaseAbstractHandler { + + public PostCommitHandler(final HttpServer httpServer) { + super(httpServer); + } + + @Override + public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final Database database) throws IOException { + database.commit(); + + exchange.getResponseHeaders().remove(HttpTransactionManager.ARCADEDB_SESSION_ID); + exchange.setStatusCode(204); + exchange.getResponseSender().send(""); + } + + @Override + protected boolean requiresTransaction() { + return false; + } +} diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDatabaseHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDatabaseHandler.java index fc8a4c5539..07739854a2 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDatabaseHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDatabaseHandler.java @@ -28,7 +28,7 @@ public PostCreateDatabaseHandler(final HttpServer httpServer) { } @Override - protected boolean openDatabase() { + protected boolean requiresDatabase() { return false; } @@ -48,4 +48,9 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, exchange.setStatusCode(200); exchange.getResponseSender().send("{ \"result\" : \"ok\"}"); } + + @Override + protected boolean requiresTransaction() { + return false; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDocumentHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDocumentHandler.java index 36ed1c330a..33d58484d8 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDocumentHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostCreateDocumentHandler.java @@ -44,18 +44,11 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, httpServer.getServer().getServerMetrics().meter("http.create-record").mark(); - database.begin(); - try { - final MutableDocument document = database.newDocument(type); - document.fromJSON(json); - document.save(); - database.commit(); - - exchange.setStatusCode(200); - exchange.getResponseSender().send("{ \"result\" : \"" + document.getIdentity() + "\"}"); - - } finally { - database.rollbackAllNested(); - } + final MutableDocument document = database.newDocument(type); + document.fromJSON(json); + document.save(); + + exchange.setStatusCode(200); + exchange.getResponseSender().send("{ \"result\" : \"" + document.getIdentity() + "\"}"); } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostDropDatabaseHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostDropDatabaseHandler.java index cad6ca68d8..32a009ff34 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/PostDropDatabaseHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostDropDatabaseHandler.java @@ -37,4 +37,9 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, exchange.setStatusCode(200); exchange.getResponseSender().send("{ \"result\" : \"ok\"}"); } + + @Override + protected boolean requiresTransaction() { + return false; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostQueryHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostQueryHandler.java index f82cad0078..279413789d 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/PostQueryHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostQueryHandler.java @@ -46,9 +46,7 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final JSONObject json = new JSONObject(payload); final Map requestMap = json.toMap(); - final String language = (String) requestMap.get("language"); - final String command = (String) requestMap.get("command"); if (command == null || command.isEmpty()) { @@ -58,16 +56,11 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, } final Map paramMap = (Map) requestMap.get("params"); - final ServerMetrics.MetricTimer timer = httpServer.getServer().getServerMetrics().timer("http.command"); - database.begin(); try { - final ResultSet qResult = command(database, language, command, paramMap); - final JsonSerializer serializer = httpServer.getJsonSerializer(); - final String result = qResult.stream().map(r -> serializer.serializeResult(r).toString()).collect(Collectors.joining(",")); if (database.isTransactionActive()) @@ -78,7 +71,6 @@ public void execute(final HttpServerExchange exchange, ServerSecurityUser user, } finally { timer.stop(); - database.rollbackAllNested(); } } @@ -105,4 +97,9 @@ private Object mapParams(Map paramMap) { } return Optional.ofNullable(paramMap).orElse(Collections.emptyMap()); } + + @Override + protected boolean requiresTransaction() { + return false; + } } diff --git a/server/src/main/java/com/arcadedb/server/http/handler/PostRollbackHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/PostRollbackHandler.java new file mode 100644 index 0000000000..c3fbb7876f --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/http/handler/PostRollbackHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 com.arcadedb.server.http.handler; + +import com.arcadedb.database.Database; +import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.http.HttpTransactionManager; +import com.arcadedb.server.security.ServerSecurityUser; +import io.undertow.server.HttpServerExchange; + +import java.io.*; + +public class PostRollbackHandler extends DatabaseAbstractHandler { + + public PostRollbackHandler(final HttpServer httpServer) { + super(httpServer); + } + + @Override + public void execute(final HttpServerExchange exchange, ServerSecurityUser user, final Database database) throws IOException { + database.rollback(); + + exchange.getResponseHeaders().remove(HttpTransactionManager.ARCADEDB_SESSION_ID); + exchange.setStatusCode(204); + exchange.getResponseSender().send(""); + } + + @Override + protected boolean requiresTransaction() { + return false; + } +} diff --git a/server/src/main/java/com/arcadedb/server/security/ServerSecurityUser.java b/server/src/main/java/com/arcadedb/server/security/ServerSecurityUser.java index 7d50e2a608..53f252bce3 100644 --- a/server/src/main/java/com/arcadedb/server/security/ServerSecurityUser.java +++ b/server/src/main/java/com/arcadedb/server/security/ServerSecurityUser.java @@ -124,6 +124,21 @@ public Set getAuthorizedDatabases() { return databasesNames; } + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (!(o instanceof ServerSecurityUser)) + return false; + final ServerSecurityUser that = (ServerSecurityUser) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + private ServerSecurityDatabaseUser registerDatabaseUser(final ArcadeDBServer server, final Database database, final String databaseName) { final JSONObject userDatabases = userConfiguration.getJSONObject("databases"); final List groupList = userDatabases.getJSONArray(databaseName).toList(); diff --git a/server/src/main/resources/static/api.html b/server/src/main/resources/static/api.html index 671ee4945f..c4f5f047ff 100644 --- a/server/src/main/resources/static/api.html +++ b/server/src/main/resources/static/api.html @@ -10,11 +10,24 @@

HTTP API

URL
-
+
Description
+
+
+ POST +
+
+
/api/v1/begin/{database}
+
+
+ Begins a transaction on the server managed as a session. The response header contains the session id. Set this id in the following requests to execute + them in the same transaction scope. See also /commit and /rollback. +
+
+
POST @@ -22,11 +35,24 @@

HTTP API

/api/v1/command/{database}
-
+
Executes a non-idempotent command.
+
+
+ POST +
+
+
/api/v1/commit/{database}
+
+
+ Commits a transaction on the server. Set the session id obtained with the /begin command as a header of the request. See also /begin and + /rollback. +
+
+
POST @@ -34,7 +60,7 @@

HTTP API

/api/v1/create/{database}
-
+
Creates a database
@@ -46,7 +72,7 @@

HTTP API

/api/v1/databases
-
+
Returns the list of databases the current authenticated user can access to.
@@ -58,7 +84,7 @@

HTTP API

/api/v1/document/{database}
-
+
Creates a new document
@@ -70,7 +96,7 @@

HTTP API

/api/v1/document/{database}/{rid}
-
+
Returns a document by record id (rid)
@@ -82,7 +108,7 @@

HTTP API

/api/v1/drop/{database}
-
+
Drops a database
@@ -94,10 +120,22 @@

HTTP API

/api/v1/query/{database}
-
+
Executes an idempotent commands, like SELECT ad MATCH.
+
+
+ POST +
+
+
/api/v1/rollback/{database}
+
+
+ Rollbacks a transaction on the server. Set the session id obtained with the /begin command as a header of the request. See also + /begin and /commit. +
+
@@ -106,7 +144,7 @@

HTTP API

/api/v1/server
-
+
Returns the current HA configuration.
diff --git a/server/src/test/java/com/arcadedb/server/BaseGraphServerTest.java b/server/src/test/java/com/arcadedb/server/BaseGraphServerTest.java index 08d2d5a739..ed61d1a943 100644 --- a/server/src/test/java/com/arcadedb/server/BaseGraphServerTest.java +++ b/server/src/test/java/com/arcadedb/server/BaseGraphServerTest.java @@ -402,7 +402,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) ((DatabaseInternal) databases[i]).getWrappedDatabaseInstance().drop(); } @@ -415,7 +415,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); } @@ -445,7 +445,6 @@ protected void testLog(final String msg, final Object... args) { LogManager.instance().log(this, Level.INFO, "TEST: " + msg, null, args); LogManager.instance() .log(this, Level.INFO, "****************************************************************************************************************"); - } protected void testEachServer(Callback callback) throws Exception { @@ -453,5 +452,4 @@ protected void testEachServer(Callback callback) throws Exception { callback.call(i); } } - } diff --git a/server/src/test/java/com/arcadedb/server/HTTPTransactionIT.java b/server/src/test/java/com/arcadedb/server/HTTPTransactionIT.java new file mode 100644 index 0000000000..268a1012b8 --- /dev/null +++ b/server/src/test/java/com/arcadedb/server/HTTPTransactionIT.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 com.arcadedb.server; + +import com.arcadedb.log.LogManager; +import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.logging.*; + +import static com.arcadedb.server.http.HttpTransactionManager.ARCADEDB_SESSION_ID; + +public class HTTPTransactionIT extends BaseGraphServerTest { + + @Test + public void simpleTx() throws Exception { + testEachServer((serverIndex) -> { + // BEGIN + HttpURLConnection connection = (HttpURLConnection) new URL("http://127.0.0.1:248" + serverIndex + "/api/v1/begin/graph").openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", + "Basic " + Base64.getEncoder().encodeToString(("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes())); + connection.connect(); + + String sessionId; + try { + final String response = readResponse(connection); + LogManager.instance().log(this, Level.INFO, "Response: ", null, response); + Assertions.assertEquals(204, connection.getResponseCode()); + sessionId = connection.getHeaderField(ARCADEDB_SESSION_ID).trim(); + + Assertions.assertNotNull(sessionId); + + } finally { + connection.disconnect(); + } + + // CREATE DOCUMENT + connection = (HttpURLConnection) new URL("http://127.0.0.1:248" + serverIndex + "/api/v1/document/graph").openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty(ARCADEDB_SESSION_ID, sessionId); + connection.setRequestProperty("Authorization", + "Basic " + Base64.getEncoder().encodeToString(("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes())); + + final JSONObject payload = new JSONObject("{\"@type\":\"Person\",\"name\":\"Jay\",\"surname\":\"Miner\",\"age\":69}"); + + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + connection.connect(); + + PrintWriter pw = new PrintWriter(new OutputStreamWriter(connection.getOutputStream())); + pw.write(payload.toString()); + pw.close(); + + final String rid; + try { + final String response = readResponse(connection); + + Assertions.assertEquals(200, connection.getResponseCode()); + Assertions.assertEquals("OK", connection.getResponseMessage()); + LogManager.instance().log(this, Level.INFO, "Response: ", null, response); + final JSONObject responseAsJson = new JSONObject(response); + Assertions.assertTrue(responseAsJson.has("result")); + rid = responseAsJson.getString("result"); + Assertions.assertTrue(rid.contains("#")); + } finally { + connection.disconnect(); + } + + // CANNOT RETRIEVE DOCUMENT OUTSIDE A TX + try { + checkDocumentWasCreated(serverIndex, payload, rid, null); + Assertions.fail(); + } catch (Exception e) { + // EXPECTED + } + + // RETRIEVE DOCUMENT + checkDocumentWasCreated(serverIndex, payload, rid, sessionId); + + // COMMIT + connection = (HttpURLConnection) new URL("http://127.0.0.1:248" + serverIndex + "/api/v1/commit/graph").openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty(ARCADEDB_SESSION_ID, sessionId); + connection.setRequestProperty("Authorization", + "Basic " + Base64.getEncoder().encodeToString(("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes())); + connection.connect(); + + try { + final String response = readResponse(connection); + LogManager.instance().log(this, Level.INFO, "Response: ", null, response); + Assertions.assertEquals(204, connection.getResponseCode()); + Assertions.assertNull(connection.getHeaderField(ARCADEDB_SESSION_ID)); + + } finally { + connection.disconnect(); + } + + // RETRIEVE DOCUMENT + checkDocumentWasCreated(serverIndex, payload, rid, sessionId); + }); + } + + private void checkDocumentWasCreated(int serverIndex, JSONObject payload, String rid, String sessionId) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL( + "http://127.0.0.1:248" + serverIndex + "/api/v1/document/graph/" + rid.substring(1)).openConnection(); + + connection.setRequestMethod("GET"); + if (sessionId != null) + connection.setRequestProperty(ARCADEDB_SESSION_ID, sessionId); + connection.setRequestProperty("Authorization", + "Basic " + Base64.getEncoder().encodeToString(("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes())); + connection.connect(); + + try { + final String response = readResponse(connection); + LogManager.instance().log(this, Level.INFO, "Response: ", null, response); + final JSONObject responseAsJson = new JSONObject(response); + Assertions.assertTrue(responseAsJson.has("result")); + final JSONObject object = responseAsJson.getJSONObject("result"); + Assertions.assertEquals(200, connection.getResponseCode()); + Assertions.assertEquals("OK", connection.getResponseMessage()); + Assertions.assertEquals(rid, object.remove("@rid").toString()); + Assertions.assertEquals("d", object.remove("@cat")); + Assertions.assertEquals(payload.toMap(), object.toMap()); + + } finally { + connection.disconnect(); + } + } +} diff --git a/server/src/test/java/performance/BasePerformanceTest.java b/server/src/test/java/performance/BasePerformanceTest.java index 2cdd0c875b..ca5078007e 100644 --- a/server/src/test/java/performance/BasePerformanceTest.java +++ b/server/src/test/java/performance/BasePerformanceTest.java @@ -178,7 +178,7 @@ protected int[] getServerToCheck() { protected void deleteDatabaseFolders() { if (databases != null) for (int i = 0; i < databases.length; ++i) { - if (databases[i] != null) + if (databases[i] != null && databases[i].isOpen()) databases[i].drop(); } @@ -190,7 +190,7 @@ protected void deleteDatabaseFolders() { Assertions.assertTrue(DatabaseFactory.getActiveDatabaseInstances().isEmpty(), "Found active databases: " + DatabaseFactory.getActiveDatabaseInstances()); for (int i = 0; i < getServerCount(); ++i) - FileUtils.deleteRecursively(new File(getDatabasePath(i))); + FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_DATABASE_DIRECTORY.getValueAsString() + i + "/")); FileUtils.deleteRecursively(new File(GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString() + "/replication")); }