1

I have a client / server application (local) where several clients perform operations concurrently on the server. The server creates a separate thread for each client in which the desired operations (register/login) are then processed. Each thread has its own connection to the database and communicates with the client via its own socket that i get from ServerSocket.accept(). The operations from the client require read and write access to a database (sqlite) on the server. The register op checks whether the entry already exists, if not it gets added. And the login op is just checking passwords. The thread will then tell the client whether the operation was successful or not. When a client is finished, it sends a message to the server to close the connection. The thread on the server interrupts its loop and then closes its socket and its connection to the database.

When there is just one client, there are no problems with the program. If There are multiple clients concurrently, then i get 2 types of SQLExceptions, "database is locked" and "unique constraint failed". The former might happen, because there are concurrent transactions being performed on the database. And this is prevented, because of ACID. The latter might happen, because of concurrency (before inserting a new entry into the DB I check whether the primary key is already used, the primary key is a username). Correct me if im wrong. Right now I am not using thread synchronization or disabling auto commit when doing the operations on the database.

I thought that the transactions on ACID DBs are automatically queued up if new ones are invoked while the current one has not finished yet. But this does not seem to be the case. So am I supposed to actively prevent these exceptions by using thread synchronization (e.g. by using Locks) when executing queries/updates or perhaps just retry until the operations succeed? What are common approaches to handle the exceptions? I know that its problematic that the thread wait for a message to end but this is another topic. This is just a program to learn about working with databases.

Here are some snippets of code that runs on the server's threads. When a thread using this runnable is created I create a new Socket from ServerSocket.accept() and a new Connection object from the driver and pass both to it.

public class ConnectionHandler implements Runnable {

    private final Socket socket;
    private final Connection dbConnection;

    public ConnectionHandler(Socket socket, Connection databaseConnection) {
        this.socket = socket;
        this.dbConnection = databaseConnection;
    }

    @Override
    public void run() {
        try (socket; dbConnection;
             ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
             ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); )
        {
            boolean shouldClose = false;
            while (!shouldClose)
            {
                Actions action = (Actions) ois.readObject();

                boolean success = false;
                if (action == Actions.REGISTER)
                {
                    success = performClientRegistration(ois, dbConnection);
                }
                else if (action == Actions.LOGIN)
                {
                    success = performClientLogin(ois, dbConnection);
                }
                else if(action == Actions.SHUTDOWN)
                {
                    shouldClose = true;
                }

                if (success) {
                    oos.writeObject(Response.SUCCESS);
                } else {
                    oos.writeObject(Response.FAILURE);
                }
            }
        }
        catch (ClassNotFoundException | IOException | SQLException e) {
            e.printStackTrace();
        }
        System.out.println("Socket to " + socket + " closed.");
    }

private boolean performClientRegistration(ObjectInputStream ois, Connection dbConnection) throws IOException, ClassNotFoundException {
        String userName = (String)ois.readObject();
        String pwd = (String)ois.readObject();
        String firstName = (String)ois.readObject();
        String lastName = (String)ois.readObject();

        try
        {
            boolean isClientRegistrationPossible = !DBHelper.existsEntry(dbConnection, userName);
            if(isClientRegistrationPossible) {
                DBHelper.addEntry(dbConnection, userName, pwd, firstName, lastName);
                return true;
            }
        }
        catch (SQLException e) {
            e.printStackTrace();
        }

        return false;
    }
} // END OF CLASS


public static void addEntry(Connection connection, String userName, String pwd, String firstName, String lastName) throws SQLException {
    PreparedStatement statement = connection.prepareStatement("INSERT INTO users " +
        "(userName, pwd, lastName, firstName) VALUES (?, ?, ?, ?)");
    statement.setString(1, userName);
    statement.setString(2, pwd);
    statement.setString(3, lastName);
    statement.setString(4, firstName);
    statement.executeUpdate();
}

public static boolean existsEntry(Connection connection, String userName) throws SQLException {
    PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM users WHERE userName = ?");
    statement.setString(1, userName);
    ResultSet result = statement.executeQuery();

    if(result.next()) {
        return true;
    }

    return false;
}
Neran
  • 55
  • 7
  • Asking what the common approach for something may be seen as subjective. – Blue Robin Feb 17 '23 at 18:07
  • @BlueRobin well I am just looking for a solution, but probably this title sounds better now? – Neran Feb 17 '23 at 18:10
  • Just to be cautious, as some may not read the post fully. Thanks! – Blue Robin Feb 17 '23 at 18:11
  • 1
    About "database is locked", how many concurrent transactions do you have? SQLite is not designed for heavy use. Have you seen [this thread](https://stackoverflow.com/questions/3172929/operationalerror-database-is-locked) and [this documentation](https://www.sqlite.org/lockingv3.html])? – Diego Ferruchelli Feb 17 '23 at 18:52
  • @DiegoFerruchelli in my test program there are 10 client threads – Neran Feb 17 '23 at 18:56
  • That's not too many, but we should see what are they doing on the DB. Anyway, the "unique constraint failed" says you're trying to insert an already existing ID, which is related to the logic of your code. Could you post something more? – Diego Ferruchelli Feb 17 '23 at 19:01
  • @DiegoFerruchelli added some snippets that are executed by the server threads – Neran Feb 17 '23 at 19:07
  • The code seems ok. There may be a problem with the `ObjectInputStream`. Are you writing the file (or whatever it is) from several threads at the same time? Does it look ok when you open it from the operating system? Try dumping the content of the variables (`userName`, etc) before looking for each record in the DB. If it's not much trouble, also show some ID for the running thread. – Diego Ferruchelli Feb 17 '23 at 21:08
  • @DiegoFerruchelli The server has a run method. In the run method it accepts connections from clients with accept method using its server socket obeject. A new thread is then created on the server with this socket object + a new connection object to the database. The run method of those threads then process the client requests (the snippets i posted here). I get the OIS also from that socket. – Neran Feb 17 '23 at 21:44
  • SQLite is a **single client** database and will have trouble with concurrency. If you need concurrency then change to another database that supports this natively. There are easy to install, free options out there, such as PostgreSQL, MariaDB, MySQL. If you don't want to install a remote datbase, you can use H2 "persistence mode". – The Impaler Feb 18 '23 at 13:18
  • Also, in this day and age, you should be using connection pool. SpringBoot implements them by default. – The Impaler Feb 18 '23 at 13:20

1 Answers1

2

No you should not thread synchronization for this. There are many reasons this will newer work.

Do you explicit start and commit each transactions with begin() and commit()?

But it sounds to me like you have transactions running for to long. You should newer have a transaction open, while communication with the client over the network. So what you should do is:

Get all the data you need from the client. Call Begin() on the database. Do all needed sql on the database. Then commit() and then send the reply back to the client so the transaction is open as little time as possible.

Updated: This also depend on the transaction isolation level. There is description of these is sqllite here:

Btw: You can set sqllite to work in exclusive mode, which might be a good test for you. If you do that and transactions begin to hang, then your problem is that you have open transactions which you don't close.

see: https://dba.stackexchange.com/questions/258003/what-standard-sql-isolation-levels-do-the-sqlite-isolation-levels-correspond-to

MTilsted
  • 5,425
  • 9
  • 44
  • 76
  • I do have 3 static methods (addEntry, existsEntry, getPassword). No im not using begin and commit, im using a prepared statement for the db interactions and then executing it. – Neran Feb 17 '23 at 18:39
  • Have you disabled auto commit? – MTilsted Feb 17 '23 at 18:45
  • I added some code snippets, hope this clarifies how the threads operate – Neran Feb 17 '23 at 19:09
  • I have tried disabling and then reenabling auto commit after the transaction. The exceptions are still thrown. I think this is because I use different connection objects to the database for each thread. If I use only one connection WITH MY CODE, I only get the "unique constraint" error. If I additionally disable autocommit, I get in addition more errors like "sql error or missing db (cannot commit - no transaction is active)" or "cannot start a transaction within a transaction". I think that I have to use Locks here. – Neran Feb 18 '23 at 13:09