Possible inconsistency between the JdbcEventStorageEngine and the InMemoryStorageEngine

Hi everyone,

I think there is an inconsistency between the JdbcEventStorageEngine and the InMemoryStorageEngine when it comes to the lastSequenceNumberFor() method. The InMemoryStorageEngine version returns an empty Optional when an aggregate is not found while the JdbcEventStorageEngine returns an Optional with the value of 0 for the same condition.

I believe the issue here may be caused by the following two things:

  1. The query used in the aforementioned method returns a single row with a NULL value when executed when the aggregate is not found.

    SELECT max(sequence_number) FROM domain_event_entry WHERE aggregate_identifier = ‘XYZ’

    This leads to the following issue.

  2. The ResultSet getObject() method returns 0 instead of a NULL value

    resultSet.getObject(1, Long.class);

    This is because, internally MySQL (through the ResultSetImpl implementation), uses the ResultSet getLong() method which returns 0 if the SQL column read yielded a NULL value.

    } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
    return (T) Long.valueOf(getLong(columnIndex));

In order to determine whether the value read from the ResultSet was a NULL or not I usually rely on the ResultSet wasNull() method.

Following is my copy of the lastSequenceNumberFor() which works as expected.

@Override
public Optional lastSequenceNumberFor(final String aggregateIdentifier) {
final String sql = “SELECT max(” + schema().sequenceNumberColumn() + “) FROM " + schema().domainEventTable() + " WHERE " + schema().aggregateIdentifierColumn() + " = ?”;
return Optional.ofNullable(transactionManager.fetchInTransaction(
() -> JdbcUtils.executeQuery(getConnection(), connection -> {
final PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, aggregateIdentifier);
return stmt;
}, resultSet -> {
if (resultSet.next()) {
final Long value = resultSet.getObject(1, Long.class);
if (false == resultSet.wasNull()) {
return value;
}
}
return null;
},
e -> new EventStoreException(String.format(“Failed to read events for aggregate [%s]”, aggregateIdentifier), e))));
}

Basically, I replaced the statement

resultSet -> resultSet.next() ? resultSet.getObject(1, Long.class) : null

with

resultSet -> {
if (resultSet.next()) {
final Long value = resultSet.getObject(1, Long.class);
if (false == resultSet.wasNull()) {
return value;
}
}
return null;
}

as highlighted in the above example.

Any feedback is highly appreciated.
Albert

Hi Albert,

thank you for pointing us towards this issue. You suggestion looks good. We’ll try to reproduce (and fix) it using your suggestions.

Cheers,

Allard

Welcome Allard.

Let me know if I can be of any help.

Hi Albert,

If you would have the time to send in a PR or make an issue for this, that would be much appreciated!

Cheers,
Steven

Will try Steven.

Never did it before, but will inform myself and try to do it by the end of the week.

Hi,

Created the issue (https://github.com/AxonFramework/AxonFramework/issues/636) and made the required changes.

Changes

  1. Created MysqlJdbcEventStorageEngineTest that works with MySQL and use the mysql.temp.database.properties file. The test replicates the problem
  2. Fixed the issue in file JdbcEventStorageEngine

MysqlJdbcEventStorageEngineTest.java (3.93 KB)

JdbcEventStorageEngine.java (42.2 KB)

Managed to make a pull request.

https://github.com/AxonFramework/AxonFramework/pull/638

Hi Albert,

Thank you very much for this effort!

I will have a look at it ASAP.

Cheers,
Steven

Welcome Steven,

It does not look like a big success, as all tests failed.

image.png

Let me know if something is not clear or need fixing and will take care of it.

The problem is that the test requires MySQL, which is not running on the build server.
It should be fairly simple to add a MySQL container to the build script.

image.png

Thanks Allard.

Is this something that you will do or you want me to take care of that?

Please note that I used the existing properties file (mysql.temp.database.properties) which uses the database named axon with the user: build/build.

image.png