Search code examples
javaspringspring-bootspring-jdbchikaricp

Spring JDBCTemplate with Hikari datasource batch update behaving asynchronously


I'm working on a webapp where I display some information that the user can act on. After taking action the list should update and reflect those changes in the webpage. I've got all the individual pieces working, but connecting them together is causing issues.

Here's essentially the retrieval code:

public List<Entry> findEntriesForIdByStatus(Long id, Status status) {

    MapSqlParameterSource paramSource = new MapSqlParameterSource();

    paramSource.addValue("id", id);

    if (null == status) {
        return template.query(FIND_ALL_ENTRIES_QUERY, paramSource, entryResultSetExtractor);
    }

    paramSource.addValue("status", status.getCode());

    List<Entry> entryWithStatus = springJdbcTemplate.query(FIND_ENTRIES_FOR_STATUS_QUERY, 
            paramSource,
            entryResultSetExtractor);

    return equipmentWithStatus;
}

And the update code:

@Transactional("myTransactionManager")
public void expire(CustomObject customObj) {
    Timestamp expirationTime = Timestamp.valueOf(dateTimeFactory.now());

    Long id = customObj.getId();
    List<Entry> entryList = customObj.getEntryList();

    SqlParameterSource[] params = new MapSqlParameterSource[entryList.size()];
    for (int i = 0; i < entryList.size(); i++) {
        Entry entry = entryList.get(i);
        MapSqlParameterSource paramSource = new MapSqlParameterSource("id", id)
                .addValue("fieldA", entry.getFieldA())
                .addValue("fieldB", entry.getFieldB())
                .addValue("expirationTime", expirationTime);
        params[i] = paramSource;
    }

    springJdbcTemplate.batchUpdate(EXPIRE_QUERY, params);
}

The template is is a SpringJDBCNamedParameterTemplate that's being managed by Spring Boot and the datasource is an instance of com.zaxxer.hikari.HikariDataSource.

The retrieval method is called upon page load and returning the correct data. When running the expire method, the correct records are expired, and both methods execute successfully. Initially I was chaining them on the front end using an excludeMap (rxjs/angular 7), but it was being inconsistent. Sometimes the retrieve after expire would return as though the expire hadn't happened, and sometimes it would. However, regardless of outcome, if I refreshed the page, the results would be empty, as expected. I switched it to chaining the calls on the back end to see if it would help at all, and it started returning the results as though the expire hadn't happened all the time, but upon page refresh, it worked as expected.

Here's the chaining on the backend:

public @ResponseBody List<Entry> expireEntries(@RequestBody CustomObj customObj) {
    entryService.expire(customObj, WebUtil.getCurrentUser());
    System.out.println(LocalDateTime.now().toString() + ": about to retrieve");
    List<Entry> entries = entryService.findEntriesByStatus(customObj.getId(), Status.NA);
    System.out.println(LocalDateTime.now().toString() + ": it's done retrieving");
    return equips;
}

The services pretty much just call the DAOs without much in between. The logging output is similar to below:

didn't work

2019-09-19T13:33:50.998: about to delete
2019-09-19T13:33:51.050: It's done deleting
2019-09-19T13:33:51.246: about to retrieve
13:33:51.465 [http-nio-8080-exec-3] INFO EntryDAO - query for parent 27 with status=NA returned 364 and took 219 milliseconds
13:33:51.466 [http-nio-8080-exec-3] INFO EntryService - query for parent 27 and status NA returned 364
2019-09-19T13:33:51.466: it's done retrieving

worked

2019-09-19T13:38:13.752: about to delete
2019-09-19T13:38:13.798: It's done deleting
2019-09-19T13:38:14.112: about to retrieve
13:38:14.120 [http-nio-8080-exec-5] INFO EntryDAO - query for parent 27 with status=NA returned 0 and took 8 milliseconds
13:38:14.120 [http-nio-8080-exec-5] INFO EntryService - query for parent 27 and status NA returned 0
2019-09-19T13:38:14.120: it's done retrieving

These are from when the chaining was on the front end, but after moving to the backend, it's the same, just with "It's done deleting" and "about to retrieve" closer in time, generally within a millisecond or two. Adding a 2 second sleep between the update and the retrieval seems to fix the issue. I tried locking the rows for update and making the methods transactional with Spring's @Transactional annotation, but neither seemed to help.

Near as I can tell, it seems like the batch update is getting triggered, but the Java method returns before the update has actually finished in the database. So when the retrieve query executes, it gets the pre-update data. Is anyone able to confirm that this is the behavior, and if it is, is there a way to fix it without just hacking it with a Thread.sleep?

EDIT: Here's the bean definition for the transaction manager.

@Bean(name = "myDbProperties")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.mydb")
public DataSourceProperties dbProperties() {
    return new DataSourceProperties();
}

@Primary
@Bean(name = "myDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mydb.configuration")
public DataSource dataSource(@Qualifier("myDbProperties") DataSourceProperties dbProperties) {
    return dbProperties.initializeDataSourceBuilder().build();
}

@Bean
@Primary
@Qualifier("myTransactionManager")
public PlatformTransactionManager loadauthTransactionManager(
        @Qualifier("myDataSource") DataSource datasource) {
    return new DataSourceTransactionManager(datasource);
}

Solution

  • So turns out this was a case of overthinking and the solution was actually quite simple. The read query used sysdate between create and delete. Turns out between is inclusive and since the queries were happening within whatever sysdate's minimum time increment results returned because sysdate = delete. Switching it to where systimestamp >= create and systimestamp < delete fixed the issue.