H2 1.4.199 database getGeneratedKeys() returns key generated by another insert transaction OR Future objects are mixed up

Viewed 35

I am using Java 1.8 and H2 1.4.199.

I have a method insertRecord(DATA_OBJECT) which inserts single row into table, and then returns the generated ID for this row. It instantiates a Task object and submit it onto SingleThreadExecutorService one-by-one, and the generated ID is retrieved from Future object. Most of the time it works just fine. However, sometimes a situation like this happens:

  • Row 10 insert is submitted to ExecutorService
  • Row 11 insert is meant to be waiting in front of "synchronized" block to be submitted to ExecutorService
  • Row 10 is being inserted by code, and Future returns generated ID = 11
  • Row 11 is being inserted by code, and Future returns generated ID = 11

I have no working and reproducible example, because it is a very rare situation, but it happens.

No exceptions thrown and I have no idea how this could happen.

Below is the example of code:

private static final ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

private static String insertRecord(DATA_OBJECT dataObject) {
    Future<String> future;
    synchronized (singleExecutor) {
        future = singleExecutor.submit(new Task(dataObject));
    }
    try {
        return future.get();
    } catch (InterruptedException ex) {
        Logger.getLogger(CLASS_NAME.class.getName()).log(Level.SEVERE, null, ex);
    } catch (ExecutionException ex) {
        Logger.getLogger(CLASS_NAME.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private class Task implements Callable<String> {

    private DATA_OBJECT dataObject;

    public Task(DATA_OBJECT dataObject) {
        this.dataObject = dataObject;
    }

    @Override
    public String call() {
        try {
            return execute(dataObject);
        } catch (SQLException ex) {
            Logger.getLogger(CLASS_NAME.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

}

private static String execute(DATA_OBJECT dataObject) {
    Connection conn = Database.getTransactedConnection();
    String lastId = null;
    boolean success = false;
    try (
            PreparedStatement statement = 
                    conn.prepareStatement(
                            "insert into TABLE (COLUMN_NAMES) values (VALUES)", 
                            Statement.RETURN_GENERATED_KEYS
                    )
    ) {
            statement.setString(1, dataObject.STRING_1);
            statement.setString(N, dataObject.STRING_N);
            success = statement.executeUpdate() == 1;
            if (success) {
                try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        lastID = generatedKeys.getString(1);
                    }
                }
            }
    } finally {
        if (success)
            conn.commit();
        else 
            conn.rollback();
        Database.releaseConnection(conn);
    }
    return lastId;
}

I am trying to find a way to avoid 100% of these events because the consequences are devastating. How can I do this?

What could be the problem leading to the return of the identifier from the future?

0 Answers
Related