1

This is using MySQL 5.7.28 as a Docker image, in case that's relevant. The client is JDBC 5.1.48, running on GraalVM 11.20 on Mac.

When I run the following query in the information_schema schema, I get 3,105 rows:

PreparedStatement ps = conn.prepareStatement("select * from COLUMNS");
ResultSet rs = ps.executeQuery();
int numRows = 0;
while (rs.next()) {
    numRows++;
}

But if I do this repeatedly in 100 parallel threads, each thread using its own connection, I occasionally get a slightly different result set. About 0.5% of the time, the number of rows is slightly lower, like 3,098. It's inconsistent, but easily reproducible.

Notice that this is not select count(*) -- this is select * and then iterate over all the rows.

This only happens against tables in the information_schema catalog -- for instance the TABLES table.

When doing the same thing against tables in the mysql catalog, or any other catalog actually, this does not happen, so my best guess is that this is due to the fact that the information_schema schema is special, like maybe it's cached in the JDBC driver, or the server, or something like that.

Can anyone shed any light on this?

Here is the complete code, which should run against any MySQL once you change the URL, user name and password:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class ManyQueryThreads {

    private static final List<Thread> threads = new ArrayList<>();

    private static final List<QueryThread> queryThreads = new ArrayList<>();

    private static final int NUM_THREADS = 100;

    private static final int NUM_ITERS = 20;

    /**
     * This gets set by the first thread to complete the query. All subsequent queries are expected
     * to return the same number of rows.
     */
    private static final AtomicInteger expectedNumRows = new AtomicInteger(-1);

    private static class QueryThread implements Runnable {

        public boolean done = false;

        @Override
        public void run() {
            Connection conn;
            try {
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/information_schema?useSSL=false",
                        "root", "Password1");
            }
            catch(Exception ex) {
                throw new RuntimeException(ex);
            }

            for (int i = 0; i < NUM_ITERS; i++) {
                try {
                    PreparedStatement ps = conn.prepareStatement("select * from COLUMNS");
                    ResultSet rs = ps.executeQuery();
                    int numRows = 0;
                    while (rs.next()) {
                        numRows++;
                    }
                    expectedNumRows.compareAndExchange(-1, numRows);
                    if (numRows != expectedNumRows.get()) {
                        System.out.println("Incorrect number of rows, expected " +
                                expectedNumRows.get() + ", but got " + numRows + ", thread " + Thread.currentThread().getId() +
                                ", iteration " + i);
                    }

                    rs.close();
                    ps.close();
                }
                catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            }

            try {
                conn.close();
            }
            catch(Exception ex) {
                throw new RuntimeException(ex);
            }

            done = true;
        }
    }

    public static void main(String[] args) throws Exception {

        System.out.println("Creating " + NUM_THREADS + " threads...");
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < NUM_THREADS; i++) {
            QueryThread qt = new QueryThread();
            queryThreads.add(qt);
            Thread t = new Thread(qt);
            threads.add(t);
        }

        System.out.println("Threads created, starting them for " + NUM_ITERS + " iterations...");
        for (int i = 0; i < NUM_THREADS; i++) {
            threads.get(i).start();
        }

        // Now wait for everyone to be done
        System.out.println("Threads are running, waiting for them to end...");
        deathWatch:
        while (true) {
            for (int i = 0; i < NUM_THREADS; i++) {
                QueryThread qt = queryThreads.get(i);
                if ( ! qt.done) {
                    Thread.sleep(100);
                    continue deathWatch;
                }
            }
            break deathWatch;
        }

        System.out.println("All done, time elapsed: " + (System.currentTimeMillis() - startTime) + "ms.");
    }
}

Typical output:

Creating 100 threads...
Threads created, starting them for 20 iterations...
Threads are running, waiting for them to end...
Incorrect number of rows, expected 3105, but got 3099, thread 49, iteration 4
Incorrect number of rows, expected 3105, but got 3086, thread 17, iteration 6
Incorrect number of rows, expected 3105, but got 3101, thread 64, iteration 8
Incorrect number of rows, expected 3105, but got 2853, thread 57, iteration 12
etc...
  • So rather than showing the code that does work, why not show the code that doesn't work? – Scary Wombat Jun 12 '20 at 00:37
  • *Can anyone shed any light on this?* - you have a bug – Scary Wombat Jun 12 '20 at 00:37
  • >> you have a bug That's of course the simplest explanation, though I don't know why this would work correctly with every table other than the ones in `information_schema`. Can you point out the bug? – Max Tardiveau Jun 12 '20 at 00:40
  • Do you have something to add? – Scary Wombat Jun 12 '20 at 00:41
  • I have added the full code, it's reasonably short and has no dependencies beyond JDBC. – Max Tardiveau Jun 12 '20 at 00:55
  • « client is JDBC 5.1.48 » is not a client, and JDBC spec is currently at 4.3. Perhaps you were trying to indicate your particular JDBC driver? Specify name, version, and ideally a link to the product page. – Basil Bourque Jun 12 '20 at 01:06
  • FYI, `Vector` went out of vogue in the 90s. Use a modern implementation of `List` such as `ArrayList`. And using try-with-resources syntax will simplify your JDBC code. – Basil Bourque Jun 12 '20 at 01:09
  • Thanks, but not really relevant here. Duly noted though. – Max Tardiveau Jun 12 '20 at 01:13
  • I have updated the code to use ArrayList, to avoid distraction. – Max Tardiveau Jun 12 '20 at 01:16
  • Yes, it's the MySQL JDBC driver, version 5.1.48, from https://mvnrepository.com/artifact/mysql/mysql-connector-java/5.1.48 The MySQL server is a Docker image, MySQL 5.7.28 from https://hub.docker.com/_/mysql – Max Tardiveau Jun 12 '20 at 01:17
  • Can't really see any obvious error, but something seems not be thread safe. The `compareandexchange` seems a bit of overkill - do you need to check the return value? – Scary Wombat Jun 12 '20 at 01:28
  • I don't think I need to check the return value. AtomicInteger is overkill, but you only live once. Thanks for looking at this, I appreciate that. I agree that it smells like a race condition, but this only happens for tables in the `information_schema` schema, which is weird. – Max Tardiveau Jun 12 '20 at 01:43
  • Good luck, does this code work correctly if you radically reduce the amount of threads/iterations? – Scary Wombat Jun 12 '20 at 01:51
  • Yes, if I run it with just a few threads, I hardly ever see the problem. At around 20 threads, the problem starts to appear regularly, and strangely it happens less often (but it still does) with a high number of threads (e.g. 500). Weird. – Max Tardiveau Jun 12 '20 at 02:02
  • This does not occur with MySQL 8.0.20, all other things being equal, so that would tend to point to the server. – Max Tardiveau Jun 12 '20 at 21:25
  • I have tried to reproduce this with various other versions of MySQL, and even with the same Docker image but different database, unsuccessfully. Only this one instance exhibits the problem, so I'm going to ascribe it to some database corruption or something like that and move on. – Max Tardiveau Jun 12 '20 at 21:51

0 Answers0