GH-452 - Introduce EventPublicationRepository.findCompletedPublications().

The default EventPublicationRegistry previously erroneously called find*In*completePublications(), i.e. returned incomplete publications when it was supposed to look up the completed ones. To be able to do the latter we need to extend EventPublicationRepository and introduce a query execution for all completed publications.

As we'd normally only chance API in major versions, but the bugfix needing to go into a bugfix release, the new method is introduced as default method rejecting the execution in case a currently existing repository implementation does not implement it. This allows us to adapt the implementations we ship to support the bugfix, but will require other implementations to ship an adapted version. This means we can ship a release that's not breaking assuming one of the official store implementations is used.
This commit is contained in:
Oliver Drotbohm
2024-01-16 17:44:49 +01:00
parent e3dbd029d0
commit 6afc00c554
10 changed files with 154 additions and 4 deletions

View File

@@ -24,7 +24,6 @@ import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationListener;
import org.springframework.modulith.events.CompletedEventPublications;
import org.springframework.modulith.events.EventPublication;
import org.springframework.transaction.annotation.Propagation;
@@ -132,7 +131,7 @@ public class DefaultEventPublicationRegistry
*/
@Override
public Collection<? extends TargetEventPublication> findAll() {
return findIncompletePublications();
return events.findCompletedPublications();
}
/*

View File

@@ -92,6 +92,17 @@ public interface EventPublicationRepository {
Optional<TargetEventPublication> findIncompletePublicationsByEventAndTargetIdentifier( //
Object event, PublicationTargetIdentifier targetIdentifier);
/**
* Returns all completed event publications currently found in the system.
*
* @return will never be {@literal null}.
* @since 1.1.2
*/
default List<TargetEventPublication> findCompletedPublications() {
throw new UnsupportedOperationException(
"Your store implementation does not support looking up completed publications!");
}
/**
* Deletes all publications with the given identifiers.
*

View File

@@ -30,8 +30,6 @@ import java.util.stream.IntStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.modulith.events.core.EventPublicationRepository;
import org.springframework.modulith.events.core.EventSerializer;
@@ -56,6 +54,13 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
VALUES (?, ?, ?, ?, ?)
""";
private static final String SQL_STATEMENT_FIND_COMPLETED = """
SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
FROM EVENT_PUBLICATION
WHERE COMPLETION_DATE IS NOT NULL
ORDER BY PUBLICATION_DATE ASC
""";
private static final String SQL_STATEMENT_FIND_UNCOMPLETED = """
SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
FROM EVENT_PUBLICATION
@@ -189,6 +194,18 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
return result == null ? Optional.empty() : result.stream().findFirst();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#findCompletedPublications()
*/
@Override
public List<TargetEventPublication> findCompletedPublications() {
var result = operations.query(SQL_STATEMENT_FIND_COMPLETED, this::resultSetToPublications);
return result == null ? Collections.emptyList() : result;
}
@Override
@Transactional(readOnly = true)
@SuppressWarnings("null")

View File

@@ -294,6 +294,21 @@ class JdbcEventPublicationRepositoryIntegrationTests {
.element(0).extracting(TargetEventPublication::getIdentifier).isEqualTo(first.getIdentifier());
}
@Test // GH-452
void findsCompletedPublications() {
var event = new TestEvent("first");
var publication = createPublication(event);
repository.markCompleted(publication, Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1)
.element(0)
.extracting(TargetEventPublication::getEvent)
.isEqualTo(event);
}
private TargetEventPublication createPublication(Object event) {
var token = event.toString();

View File

@@ -50,6 +50,15 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
and p.completionDate is null
""";
private static String COMPLETE = """
select p
from JpaEventPublication p
where
p.completionDate is not null
order by
p.publicationDate asc
""";
private static String INCOMPLETE = """
select p
from JpaEventPublication p
@@ -186,6 +195,20 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
.map(this::entityToDomain);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#findCompletedPublications()
*/
@Override
public List<TargetEventPublication> findCompletedPublications() {
return entityManager.createQuery(COMPLETE, JpaEventPublication.class)
.getResultList()
.stream()
.map(this::entityToDomain)
.toList();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#deletePublications(java.util.List)

View File

@@ -270,6 +270,21 @@ class JpaEventPublicationRepositoryIntegrationTests {
.element(0).extracting(TargetEventPublication::getIdentifier).isEqualTo(first.getIdentifier());
}
@Test // GH-452
void findsCompletedPublications() {
var event = new TestEvent("first");
var publication = createPublication(event);
repository.markCompleted(publication, Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1)
.element(0)
.extracting(TargetEventPublication::getEvent)
.isEqualTo(event);
}
private TargetEventPublication createPublication(Object event) {
var token = event.toString();

View File

@@ -127,6 +127,15 @@ class MongoDbEventPublicationRepository implements EventPublicationRepository {
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#findCompletedPublications()
*/
@Override
public List<TargetEventPublication> findCompletedPublications() {
return readMapped(defaultQuery(where(COMPLETION_DATE).ne(null)));
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#deletePublications(java.util.List)

View File

@@ -129,6 +129,21 @@ class MongoDbEventPublicationRepositoryTest {
.element(0).extracting(TargetEventPublication::getIdentifier).isEqualTo(first.getIdentifier());
}
@Test // GH-452
void findsCompletedPublications() {
var event = new TestEvent("first");
var publication = createPublication(event);
repository.markCompleted(publication, Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1)
.element(0)
.extracting(TargetEventPublication::getEvent)
.isEqualTo(event);
}
private TargetEventPublication createPublication(Object event) {
return createPublication(event, TARGET_IDENTIFIER);
}

View File

@@ -17,6 +17,7 @@ package org.springframework.modulith.events.neo4j;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -45,6 +46,7 @@ import org.springframework.util.DigestUtils;
* A {@link Neo4jClient} based implementation of {@link EventPublicationRepository}.
*
* @author Gerrit Meier
* @author Oliver Drotbohm
* @since 1.1
*/
@Transactional
@@ -114,6 +116,12 @@ class Neo4jEventPublicationRepository implements EventPublicationRepository {
.orderBy(EVENT_PUBLICATION_NODE.property(PUBLICATION_DATE))
.build();
private static final ResultStatement ALL_COMPLETED_STATEMENT = Cypher.match(EVENT_PUBLICATION_NODE)
.where(EVENT_PUBLICATION_NODE.property(COMPLETION_DATE).isNotNull())
.returning(EVENT_PUBLICATION_NODE)
.orderBy(EVENT_PUBLICATION_NODE.property(PUBLICATION_DATE))
.build();
private final Neo4jClient neo4jClient;
private final Renderer renderer;
private final EventSerializer eventSerializer;
@@ -225,6 +233,19 @@ class Neo4jEventPublicationRepository implements EventPublicationRepository {
.one();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#findCompletedPublications()
*/
@Override
public List<TargetEventPublication> findCompletedPublications() {
return new ArrayList<>(neo4jClient.query(renderer.render(ALL_COMPLETED_STATEMENT))
.fetchAs(TargetEventPublication.class)
.mappedBy(this::mapRecordToPublication)
.all());
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.core.EventPublicationRepository#deletePublications(java.util.List)

View File

@@ -220,6 +220,31 @@ class Neo4jEventPublicationRepositoryTest {
}
}
@Test // GH-452
void findsCompletedPublications() {
var event = new TestEvent("first");
var publication = createPublication(event);
repository.markCompleted(publication, Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1)
.element(0)
.extracting(TargetEventPublication::getEvent)
.isEqualTo(event);
}
private TargetEventPublication createPublication(Object event) {
var token = event.toString();
doReturn(token).when(eventSerializer).serialize(event);
doReturn(event).when(eventSerializer).deserialize(token, event.getClass());
return repository.create(TargetEventPublication.of(event, TARGET_IDENTIFIER));
}
@Value
static class TestEvent {
String eventId;