From bb82f5446063c065a59a68b3b06869a64f2d8ce8 Mon Sep 17 00:00:00 2001 From: Graham Crockford Date: Mon, 25 Dec 2023 23:02:44 +0000 Subject: [PATCH 1/4] Better example code for Spring Boot --- transactionoutbox-spring/README.md | 3 + transactionoutbox-spring/pom.xml | 12 +++ .../SpringTransactionOutboxConfiguration.java | 4 + .../spring/acceptance/Event.java | 19 ---- .../spring/acceptance/EventPublisher.java | 15 --- .../spring/acceptance/EventRepository.java | 7 -- .../EventuallyConsistentController.java | 45 --------- .../EventuallyConsistentControllerTest.java | 45 --------- .../acceptance/ExternalsConfiguration.java | 9 -- ...ransactionOutboxSpringDemoApplication.java | 30 ------ .../{acceptance => example}/Customer.java | 2 +- .../CustomerRepository.java | 2 +- .../EventuallyConsistentController.java | 36 +++++++ .../EventuallyConsistentControllerTest.java | 95 +++++++++++++++++++ .../spring/example/ExternalQueueService.java | 29 ++++++ .../example/ExternalsConfiguration.java | 10 ++ .../TransactionOutboxBackgroundProcessor.java | 31 ++++++ .../example/TransactionOutboxProperties.java | 16 ++++ ...ransactionOutboxSpringDemoApplication.java | 53 +++++++++++ .../spring/example/Utils.java | 35 +++++++ .../src/test/resources/application.properties | 6 +- 21 files changed, 331 insertions(+), 173 deletions(-) delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Event.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventPublisher.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventRepository.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentController.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentControllerTest.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/ExternalsConfiguration.java delete mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/TransactionOutboxSpringDemoApplication.java rename transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/{acceptance => example}/Customer.java (85%) rename transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/{acceptance => example}/CustomerRepository.java (76%) create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalsConfiguration.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxBackgroundProcessor.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxProperties.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxSpringDemoApplication.java create mode 100644 transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Utils.java diff --git a/transactionoutbox-spring/README.md b/transactionoutbox-spring/README.md index 69af360d..90178048 100644 --- a/transactionoutbox-spring/README.md +++ b/transactionoutbox-spring/README.md @@ -60,6 +60,9 @@ You will need to authenticate with Github to use Github Package Repository. Crea The above example uses environment variables, allowing you to keep the credentials out of source control, but you can hard-code them if you know what you're doing. +## Example +An example application can be found here: https://github.com/gruelbox/transaction-outbox/tree/better-spring-example/transactionoutbox-spring/src/test. + ## Configuration Create your `TransactionOutbox` as a bean: diff --git a/transactionoutbox-spring/pom.xml b/transactionoutbox-spring/pom.xml index 9940b527..361575e4 100644 --- a/transactionoutbox-spring/pom.xml +++ b/transactionoutbox-spring/pom.xml @@ -54,6 +54,12 @@ lombok + + com.gruelbox + transactionoutbox-jackson + test + ${project.version} + ch.qos.logback logback-classic @@ -100,5 +106,11 @@ org.junit.jupiter junit-jupiter-params + + org.awaitility + awaitility + 4.2.0 + test + diff --git a/transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java b/transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java index a659872d..a24721a0 100644 --- a/transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java +++ b/transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java @@ -3,6 +3,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +/** + * @deprecated Just {@code @Import} the components you need. + */ @Configuration +@Deprecated(forRemoval = true) @Import({SpringTransactionManager.class, SpringInstantiator.class}) public class SpringTransactionOutboxConfiguration {} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Event.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Event.java deleted file mode 100644 index 6016d2da..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Event.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Data -@NoArgsConstructor -@AllArgsConstructor -class Event { - @Id private Long id; - @Column private String description; - @Column private LocalDateTime created; -} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventPublisher.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventPublisher.java deleted file mode 100644 index 9f581e2a..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventPublisher.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import java.time.LocalDateTime; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -class EventPublisher { - - @Autowired private EventRepository eventRepository; - - public void publish(long id, String description, LocalDateTime time) { - eventRepository.save(new Event(id, description, time)); - } -} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventRepository.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventRepository.java deleted file mode 100644 index b8a390f8..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; - -@Repository -interface EventRepository extends CrudRepository {} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentController.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentController.java deleted file mode 100644 index 094427d3..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import com.gruelbox.transactionoutbox.TransactionOutbox; -import java.time.LocalDateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SuppressWarnings("unused") -@RestController -class EventuallyConsistentController { - - private static final Logger LOGGER = - LoggerFactory.getLogger(EventuallyConsistentController.class); - - @Autowired private CustomerRepository customerRepository; - @Autowired private TransactionOutbox outbox; - @Autowired private EventRepository eventRepository; - @Autowired private EventPublisher eventPublisher; - - @SuppressWarnings("SameReturnValue") - @RequestMapping("/createCustomer") - @Transactional - public String createCustomer() { - LOGGER.info("Creating customers"); - outbox - .schedule(eventPublisher.getClass()) // Just a trick to get autowiring to work. - .publish(1L, "Created customers", LocalDateTime.now()); - customerRepository.save(new Customer(1L, "Martin", "Carthy")); - customerRepository.save(new Customer(2L, "Dave", "Pegg")); - LOGGER.info("Customers created"); - return "Done"; - } - - @RequestMapping("/gotEventAndCustomers") - public String gotEvent() { - var event = eventRepository.findById(1L); - var customer1 = customerRepository.findById(1L); - var customer2 = customerRepository.findById(2L); - return event.isPresent() && customer1.isPresent() && customer2.isPresent() ? "Yes" : "No"; - } -} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentControllerTest.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentControllerTest.java deleted file mode 100644 index c6ba8659..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/EventuallyConsistentControllerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -import java.net.URL; -import javax.inject.Inject; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.ResponseEntity; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class EventuallyConsistentControllerTest { - - @LocalServerPort private int port; - - private URL base; - - @Inject private TestRestTemplate template; - - @BeforeEach - void setUp() throws Exception { - this.base = new URL("http://localhost:" + port + "/"); - } - - @Test - void testCheck() throws Exception { - ResponseEntity response = - template.getForEntity(base.toString() + "/createCustomer", String.class); - assertThat("Done").isEqualTo(response.getBody()); - - for (int i = 0; i < 10; i++) { - ResponseEntity exists = - template.getForEntity(base.toString() + "/gotEventAndCustomers", String.class); - if ("Yes".equals(exists.getBody())) { - return; - } - Thread.sleep(1000); - } - fail("Could not confirm eventually consistent part of transaction completed"); - } -} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/ExternalsConfiguration.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/ExternalsConfiguration.java deleted file mode 100644 index fa0cb1c5..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/ExternalsConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import com.gruelbox.transactionoutbox.spring.SpringTransactionOutboxConfiguration; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import({SpringTransactionOutboxConfiguration.class}) -class ExternalsConfiguration {} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/TransactionOutboxSpringDemoApplication.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/TransactionOutboxSpringDemoApplication.java deleted file mode 100644 index 7eafcd82..00000000 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/TransactionOutboxSpringDemoApplication.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; - -import com.gruelbox.transactionoutbox.Dialect; -import com.gruelbox.transactionoutbox.Persistor; -import com.gruelbox.transactionoutbox.TransactionOutbox; -import com.gruelbox.transactionoutbox.spring.SpringInstantiator; -import com.gruelbox.transactionoutbox.spring.SpringTransactionManager; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; - -@SpringBootApplication -public class TransactionOutboxSpringDemoApplication { - - public static void main(String[] args) { - SpringApplication.run(TransactionOutboxSpringDemoApplication.class, args); - } - - @Bean - @Lazy - public TransactionOutbox transactionOutbox( - SpringInstantiator instantiator, SpringTransactionManager transactionManager) { - return TransactionOutbox.builder() - .instantiator(instantiator) - .transactionManager(transactionManager) - .persistor(Persistor.forDialect(Dialect.H2)) - .build(); - } -} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Customer.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Customer.java similarity index 85% rename from transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Customer.java rename to transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Customer.java index 9858179e..0d084f21 100644 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/Customer.java +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Customer.java @@ -1,4 +1,4 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; +package com.gruelbox.transactionoutbox.spring.example; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/CustomerRepository.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/CustomerRepository.java similarity index 76% rename from transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/CustomerRepository.java rename to transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/CustomerRepository.java index 304ad8ad..5a134b8d 100644 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/acceptance/CustomerRepository.java +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/CustomerRepository.java @@ -1,4 +1,4 @@ -package com.gruelbox.transactionoutbox.spring.acceptance; +package com.gruelbox.transactionoutbox.spring.example; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java new file mode 100644 index 00000000..fc466beb --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java @@ -0,0 +1,36 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import com.gruelbox.transactionoutbox.TransactionOutbox; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@SuppressWarnings("unused") +@RestController +class EventuallyConsistentController { + + @Autowired private CustomerRepository customerRepository; + @Autowired private TransactionOutbox outbox; + + @SuppressWarnings("SameReturnValue") + @PostMapping(path = "/customer") + @Transactional + public void createCustomer(@RequestBody Customer customer, @RequestParam(name = "ordered", required = false) Boolean ordered) { + customerRepository.save(customer); + if (ordered != null && ordered) { + outbox.with().ordered("justonetopic").schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer); + } else { + outbox.schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer); + } + } + + @GetMapping("/customer/{id}") + public Customer getCustomer(@PathVariable long id) { + return customerRepository + .findById(id) + .orElseThrow(() -> new ResponseStatusException(NOT_FOUND)); + } +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java new file mode 100644 index 00000000..c4e48a0d --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java @@ -0,0 +1,95 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URL; +import javax.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class EventuallyConsistentControllerTest { + + @SuppressWarnings("unused") + @LocalServerPort + private int port; + + private URL base; + + @SuppressWarnings("unused") + @Inject + private TestRestTemplate template; + + @SuppressWarnings("unused") + @Inject private ExternalQueueService externalQueueService; + + @BeforeEach + void setUp() throws Exception { + this.base = new URL("http://localhost:" + port + "/"); + externalQueueService.clear(); + } + + @Test + void testCheckNormal() { + + var joe = new Customer(1L, "Joe", "Strummer"); + var dave = new Customer(2L, "Dave", "Grohl"); + var neil = new Customer(3L, "Neil", "Diamond"); + var tupac = new Customer(4L, "Tupac", "Shakur"); + var jeff = new Customer(5L, "Jeff", "Mills"); + + var url = base.toString() + "/customer"; + assertTrue(template.postForEntity(url, joe, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); + + await() + .atMost(10, SECONDS) + .pollDelay(1, SECONDS) + .untilAsserted( + () -> + assertThat(externalQueueService.getSent()) + .containsExactlyInAnyOrder(joe, dave, neil, tupac, jeff)); + } + + @Test + void testCheckOrdered() { + + var joe = new Customer(1L, "Joe", "Strummer"); + var dave = new Customer(2L, "Dave", "Grohl"); + var neil = new Customer(3L, "Neil", "Diamond"); + var tupac = new Customer(4L, "Tupac", "Shakur"); + var jeff = new Customer(5L, "Jeff", "Mills"); + + var url = base.toString() + "/customer?ordered=true"; + assertTrue(template.postForEntity(url, joe, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue( + template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); + + await() + .atMost(10, SECONDS) + .pollDelay(1, SECONDS) + .untilAsserted( + () -> + assertThat(externalQueueService.getSent()) + .containsExactly(joe, dave, neil, tupac, jeff)); + } +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java new file mode 100644 index 00000000..223cf55c --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java @@ -0,0 +1,29 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import lombok.Getter; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +@Getter +@Service +class ExternalQueueService { + + private final Set attempted = new HashSet<>(); + private final List sent = new CopyOnWriteArrayList<>(); + + void sendCustomerCreatedEvent(Customer customer) { + if (attempted.add(customer.getId())) { + throw new RuntimeException("Temporary failure, try again"); + } + sent.add(customer); + } + + public void clear() { + attempted.clear(); + sent.clear(); + } +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalsConfiguration.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalsConfiguration.java new file mode 100644 index 00000000..ffc73397 --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalsConfiguration.java @@ -0,0 +1,10 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import com.gruelbox.transactionoutbox.spring.SpringInstantiator; +import com.gruelbox.transactionoutbox.spring.SpringTransactionManager; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({SpringInstantiator.class, SpringTransactionManager.class}) +class ExternalsConfiguration {} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxBackgroundProcessor.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxBackgroundProcessor.java new file mode 100644 index 00000000..912551e4 --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxBackgroundProcessor.java @@ -0,0 +1,31 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import com.gruelbox.transactionoutbox.TransactionOutbox; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * Simple implementation of a background processor for {@link TransactionOutbox}. You don't need to + * use this if you need different semantics, but this is a good start for most purposes. + */ +@Component +@Slf4j +@RequiredArgsConstructor(onConstructor_ = {@Autowired}) +class TransactionOutboxBackgroundProcessor { + + private final TransactionOutbox outbox; + + @Scheduled(fixedRateString = "${outbox.repeatEvery}") + void poll() { + try { + do { + log.info("Flushing"); + } while (outbox.flush()); + } catch (Exception e) { + log.error("Error flushing transaction outbox. Pausing", e); + } + } +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxProperties.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxProperties.java new file mode 100644 index 00000000..64f4c55c --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxProperties.java @@ -0,0 +1,16 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import java.time.Duration; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("outbox") +@Data +class TransactionOutboxProperties { + private Duration repeatEvery; + private boolean useJackson; + private Duration attemptFrequency; + private int blockAfterAttempts; +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxSpringDemoApplication.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxSpringDemoApplication.java new file mode 100644 index 00000000..3f2eaa49 --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/TransactionOutboxSpringDemoApplication.java @@ -0,0 +1,53 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gruelbox.transactionoutbox.DefaultPersistor; +import com.gruelbox.transactionoutbox.Dialect; +import com.gruelbox.transactionoutbox.Persistor; +import com.gruelbox.transactionoutbox.TransactionOutbox; +import com.gruelbox.transactionoutbox.jackson.JacksonInvocationSerializer; +import com.gruelbox.transactionoutbox.spring.SpringInstantiator; +import com.gruelbox.transactionoutbox.spring.SpringTransactionManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class TransactionOutboxSpringDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionOutboxSpringDemoApplication.class, args); + } + + @Bean + @Lazy + Persistor persistor(TransactionOutboxProperties properties, ObjectMapper objectMapper) { + if (properties.isUseJackson()) { + return DefaultPersistor.builder() + .serializer(JacksonInvocationSerializer.builder().mapper(objectMapper).build()) + .dialect(Dialect.H2) + .build(); + } else { + return Persistor.forDialect(Dialect.H2); + } + } + + @Bean + @Lazy + TransactionOutbox transactionOutbox( + SpringInstantiator instantiator, + SpringTransactionManager transactionManager, + TransactionOutboxProperties properties, + Persistor persistor) { + return TransactionOutbox.builder() + .instantiator(instantiator) + .transactionManager(transactionManager) + .persistor(persistor) + .attemptFrequency(properties.getAttemptFrequency()) + .blockAfterAttempts(properties.getBlockAfterAttempts()) + .build(); + } +} diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Utils.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Utils.java new file mode 100644 index 00000000..739fa365 --- /dev/null +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/Utils.java @@ -0,0 +1,35 @@ +package com.gruelbox.transactionoutbox.spring.example; + +import com.gruelbox.transactionoutbox.ThrowingRunnable; +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Utils { + + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + + @SuppressWarnings({"SameParameterValue", "WeakerAccess", "UnusedReturnValue"}) + static boolean safelyRun(String gerund, ThrowingRunnable runnable) { + try { + runnable.run(); + return true; + } catch (Exception e) { + LOGGER.error("Error when {}", gerund, e); + return false; + } + } + + @SuppressWarnings("unused") + static void safelyClose(AutoCloseable... closeables) { + safelyClose(Arrays.asList(closeables)); + } + + private static void safelyClose(Iterable closeables) { + closeables.forEach( + d -> { + if (d == null) return; + safelyRun("closing resource", d::close); + }); + } +} diff --git a/transactionoutbox-spring/src/test/resources/application.properties b/transactionoutbox-spring/src/test/resources/application.properties index bafddced..f9162739 100644 --- a/transactionoutbox-spring/src/test/resources/application.properties +++ b/transactionoutbox-spring/src/test/resources/application.properties @@ -1 +1,5 @@ -server.port=8081 \ No newline at end of file +server.port=8081 +outbox.repeatEvery=PT1S +outbox.attemptFrequency=PT0.5S +outbox.blockAfterAttempts=100 +outbox.useJackson=true \ No newline at end of file From 426a4a6fe6a9f51eec52f9aa84af584b2036d87a Mon Sep 17 00:00:00 2001 From: Graham Crockford Date: Sat, 23 Mar 2024 17:27:17 +0000 Subject: [PATCH 2/4] Code format --- .../EventuallyConsistentController.java | 14 +++++++--- .../EventuallyConsistentControllerTest.java | 27 +++++++------------ .../spring/example/ExternalQueueService.java | 5 ++-- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java index fc466beb..9e8bf750 100644 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentController.java @@ -1,13 +1,13 @@ package com.gruelbox.transactionoutbox.spring.example; +import static org.springframework.http.HttpStatus.NOT_FOUND; + import com.gruelbox.transactionoutbox.TransactionOutbox; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import static org.springframework.http.HttpStatus.NOT_FOUND; - @SuppressWarnings("unused") @RestController class EventuallyConsistentController { @@ -18,10 +18,16 @@ class EventuallyConsistentController { @SuppressWarnings("SameReturnValue") @PostMapping(path = "/customer") @Transactional - public void createCustomer(@RequestBody Customer customer, @RequestParam(name = "ordered", required = false) Boolean ordered) { + public void createCustomer( + @RequestBody Customer customer, + @RequestParam(name = "ordered", required = false) Boolean ordered) { customerRepository.save(customer); if (ordered != null && ordered) { - outbox.with().ordered("justonetopic").schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer); + outbox + .with() + .ordered("justonetopic") + .schedule(ExternalQueueService.class) + .sendCustomerCreatedEvent(customer); } else { outbox.schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer); } diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java index c4e48a0d..8c29331a 100644 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/EventuallyConsistentControllerTest.java @@ -27,7 +27,8 @@ class EventuallyConsistentControllerTest { private TestRestTemplate template; @SuppressWarnings("unused") - @Inject private ExternalQueueService externalQueueService; + @Inject + private ExternalQueueService externalQueueService; @BeforeEach void setUp() throws Exception { @@ -46,14 +47,10 @@ void testCheckNormal() { var url = base.toString() + "/customer"; assertTrue(template.postForEntity(url, joe, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); await() .atMost(10, SECONDS) @@ -75,14 +72,10 @@ void testCheckOrdered() { var url = base.toString() + "/customer?ordered=true"; assertTrue(template.postForEntity(url, joe, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); - assertTrue( - template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, dave, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, neil, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, tupac, Void.class).getStatusCode().is2xxSuccessful()); + assertTrue(template.postForEntity(url, jeff, Void.class).getStatusCode().is2xxSuccessful()); await() .atMost(10, SECONDS) diff --git a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java index 223cf55c..9dab1556 100644 --- a/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java +++ b/transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/ExternalQueueService.java @@ -1,12 +1,11 @@ package com.gruelbox.transactionoutbox.spring.example; -import lombok.Getter; -import org.springframework.stereotype.Service; - import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import lombok.Getter; +import org.springframework.stereotype.Service; @Getter @Service From 3b44e15a580be719eca0d59d50dc5e5052c9849f Mon Sep 17 00:00:00 2001 From: Graham Crockford Date: Sat, 23 Mar 2024 20:22:04 +0000 Subject: [PATCH 3/4] Rework build system to separate databases into parallel jobs --- .github/workflows/cd_build.yml | 5 +- .github/workflows/pull_request.yml | 41 ++- .github/workflows/pull_request_delombok.yml | 55 ---- .github/workflows/test-update-versions.yml | 39 --- pom.xml | 62 ++++ .../acceptance/TestOracle18.java | 3 +- .../acceptance/TestOracle21.java | 3 +- ...stractTestDefaultInvocationSerializer.java | 256 +++++++++++++++++ .../TestDefaultInvocationSerializer.java | 272 +----------------- 9 files changed, 375 insertions(+), 361 deletions(-) delete mode 100644 .github/workflows/pull_request_delombok.yml delete mode 100644 .github/workflows/test-update-versions.yml create mode 100644 transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java diff --git a/.github/workflows/cd_build.yml b/.github/workflows/cd_build.yml index 6d9a8018..a6269be0 100644 --- a/.github/workflows/cd_build.yml +++ b/.github/workflows/cd_build.yml @@ -8,10 +8,8 @@ jobs: build: if: "!contains(github.event.head_commit.message, 'skip ci')" runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - - name: Set up JDK 21 uses: actions/setup-java@v2 with: @@ -21,7 +19,6 @@ jobs: server-id: github # Value of the distributionManagement/repository/id field of the pom.xml settings-path: ${{ github.workspace }} # location for the settings.xml file cache: 'maven' - - name: Build, publish to GPR and tag run: | if [ "$GITHUB_REPOSITORY" == "gruelbox/transaction-outbox" ]; then @@ -32,7 +29,7 @@ jobs: git tag $revision git push origin $revision else - mvn -Pconcise,delombok -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + mvn -Pconcise,delombok,only-nodb-tests -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn fi env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 424c8d1d..6ccfca40 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -5,6 +5,44 @@ on: branches: [ master ] jobs: + format-and-delombok: + if: "!contains(github.event.head_commit.message, 'skip ci')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-package: jdk + java-version: 21 + cache: 'maven' + - name: Build with Maven + run: mvn -Pconcise,delombok -B fmt:check package -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + - uses: actions/upload-artifact@v4 + with: + name: javadocs-core + path: transactionoutbox-core/target/apidocs + - uses: actions/upload-artifact@v4 + with: + name: javadocs-guice + path: transactionoutbox-guice/target/apidocs + - uses: actions/upload-artifact@v4 + with: + name: javadocs-jackson + path: transactionoutbox-jackson/target/apidocs + - uses: actions/upload-artifact@v4 + with: + name: javadocs-jooq + path: transactionoutbox-jooq/target/apidocs + - uses: actions/upload-artifact@v4 + with: + name: javadocs-quarkus + path: transactionoutbox-quarkus/target/apidocs + - uses: actions/upload-artifact@v4 + with: + name: javadocs-spring + path: transactionoutbox-spring/target/ build: if: "!contains(github.event.head_commit.message, 'skip ci')" runs-on: ${{ matrix.os }} @@ -12,6 +50,7 @@ jobs: matrix: os: [ ubuntu-latest ] jdk: [ 11,17,21 ] + db: [ nodb,mysql,postgres,oracle ] fail-fast: false steps: - uses: actions/checkout@v4 @@ -23,4 +62,4 @@ jobs: java-version: ${{ matrix.jdk }} cache: 'maven' - name: Build with Maven - run: mvn -Pconcise -B fmt:check package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + run: mvn -Pconcise,only-${{ matrix.db }}-tests -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ No newline at end of file diff --git a/.github/workflows/pull_request_delombok.yml b/.github/workflows/pull_request_delombok.yml deleted file mode 100644 index 906df747..00000000 --- a/.github/workflows/pull_request_delombok.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Pull request (delombok) - -on: - pull_request: - branches: [ master ] - -jobs: - build: - if: "!contains(github.event.head_commit.message, 'skip ci')" - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-package: jdk - java-version: 21 - cache: 'maven' - - - name: Build with Maven and delombok - run: mvn -Pconcise,delombok -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-core - path: transactionoutbox-core/target/apidocs - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-guice - path: transactionoutbox-guice/target/apidocs - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-jackson - path: transactionoutbox-jackson/target/apidocs - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-jooq - path: transactionoutbox-jooq/target/apidocs - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-quarkus - path: transactionoutbox-quarkus/target/apidocs - - - uses: actions/upload-artifact@v4 - with: - name: javadocs-spring - path: transactionoutbox-spring/target/apidocs diff --git a/.github/workflows/test-update-versions.yml b/.github/workflows/test-update-versions.yml deleted file mode 100644 index e6204697..00000000 --- a/.github/workflows/test-update-versions.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Test update versions - -on: workflow_dispatch - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Update READMEs - run: | - set -e - revision=9.8.7-TEST - echo "Updating READMEs" - sed -i "s_\(\)[^<]*_\1${revision}_g" README.md - sed -i "s_\(\)[^<]*_\1${revision}_g" transactionoutbox-guice/README.md - sed -i "s_\(\)[^<]*_\1${revision}_g" transactionoutbox-jackson/README.md - sed -i "s_\(\)[^<]*_\1${revision}_g" transactionoutbox-jooq/README.md - sed -i "s_\(\)[^<]*_\1${revision}_g" transactionoutbox-spring/README.md - sed -i "s_\(\)[^<]*_\1${revision}_g" transactionoutbox-quarkus/README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-core:\)[^']*_\1${revision}_g" README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-guice:\)[^']*_\1${revision}_g" transactionoutbox-guice/README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jackson:\)[^']*_\1${revision}_g" transactionoutbox-jackson/README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jooq:\)[^']*_\1${revision}_g" transactionoutbox-jooq/README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-spring:\)[^']*_\1${revision}_g" transactionoutbox-spring/README.md - sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-quarkus:\)[^']*_\1${revision}_g" transactionoutbox-quarkus/README.md - - - name: Create version update pull request - uses: gruelbox/create-pull-request@master - with: - commit-message: "Update versions in READMEs [skip ci]" - title: Update versions in READMEs - body: Updates the versions in the README files following the release - branch: update-readme-version - base: master - author: GitHub - diff --git a/pom.xml b/pom.xml index 58888557..94291631 100644 --- a/pom.xml +++ b/pom.xml @@ -422,6 +422,68 @@ + + only-nodb-tests + + + + maven-surefire-plugin + + + **/*Oracle*.java + **/*Postgres*.java + **/*MySql*.java + + + + + + + + only-oracle-tests + + + + maven-surefire-plugin + + + **/*Oracle*.java + + + + + + + + only-postgres-tests + + + + maven-surefire-plugin + + + **/*Postgres*.java + + + + + + + + only-mysql-tests + + + + maven-surefire-plugin + + + **/*MySql*.java + + + + + + diff --git a/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java b/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java index 295c4d2b..f6f07f74 100644 --- a/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java +++ b/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java @@ -16,8 +16,7 @@ class TestOracle18 extends AbstractAcceptanceTest { @SuppressWarnings("rawtypes") private static final JdbcDatabaseContainer container = new OracleContainer("gvenzl/oracle-xe:18-slim-faststart") - .withStartupTimeout(Duration.ofHours(1)) - .withReuse(true); + .withStartupTimeout(Duration.ofHours(1)); @Override protected ConnectionDetails connectionDetails() { diff --git a/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java b/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java index 816c2ba1..272f0bdf 100644 --- a/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java +++ b/transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java @@ -16,8 +16,7 @@ class TestOracle21 extends AbstractAcceptanceTest { @SuppressWarnings("rawtypes") private static final JdbcDatabaseContainer container = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") - .withStartupTimeout(Duration.ofHours(1)) - .withReuse(true); + .withStartupTimeout(Duration.ofHours(1)); @Override protected ConnectionDetails connectionDetails() { diff --git a/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java new file mode 100644 index 00000000..5dc13fa0 --- /dev/null +++ b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java @@ -0,0 +1,256 @@ +package com.gruelbox.transactionoutbox; + +import java.io.StringReader; +import java.io.StringWriter; +import java.time.*; +import java.time.temporal.ChronoUnit; +import java.util.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("RedundantCast") +@Slf4j +abstract class AbstractTestDefaultInvocationSerializer { + + private static final String CLASS_NAME = "foo"; + private static final String METHOD_NAME = "bar"; + + private final DefaultInvocationSerializer serializer; + + protected AbstractTestDefaultInvocationSerializer(Integer version) { + this.serializer = + DefaultInvocationSerializer.builder() + .serializableTypes(Set.of(ExampleCustomEnum.class, ExampleCustomClass.class)) + .version(version) + .build(); + } + + @Test + void testNoArgs() { + check(new Invocation(String.class.getName(), "toString", new Class[0], new Object[0])); + } + + @Test + void testArrays() { + check( + new Invocation( + CLASS_NAME, + METHOD_NAME, + new Class[] {int[].class}, + new Object[] {new int[] {1, 2, 3}})); + check( + new Invocation( + CLASS_NAME, + METHOD_NAME, + new Class[] {Integer[].class}, + new Object[] {new Integer[] {1, 2, 3}})); + check( + new Invocation( + CLASS_NAME, + METHOD_NAME, + new Class[] {String[].class}, + new Object[] {new String[] {"1", "2", "3"}})); + } + + @Test + void testPrimitives() { + Class[] primitives = { + byte.class, + short.class, + int.class, + long.class, + float.class, + double.class, + boolean.class, + char.class + }; + Object[] values = {(byte) 1, (short) 2, 3, 4L, 1.23F, 1.23D, true, '-'}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testBoxedPrimitives() { + Class[] primitives = { + Byte.class, + Short.class, + Integer.class, + Long.class, + Float.class, + Double.class, + Boolean.class, + Character.class, + String.class + }; + Object[] values = { + (Byte) (byte) 1, + (Short) (short) 2, + (Integer) 3, + (Long) 4L, + (Float) 1.23F, + (Double) 1.23D, + (Boolean) true, + (Character) '-', + "Foo" + }; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testJavaDateEnums() { + Class[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class}; + Object[] values = {DayOfWeek.FRIDAY, Month.APRIL, ChronoUnit.DAYS}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testJavaDateEnumsNulls() { + Class[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class}; + Object[] values = {null, null, null}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testJavaUtilDate() { + Class[] primitives = {Date.class, Date.class}; + Object[] values = {new Date(), null}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testJavaTimeClasses() { + Class[] primitives = { + Duration.class, + Instant.class, + LocalDate.class, + LocalDateTime.class, + MonthDay.class, + Period.class, + Year.class, + YearMonth.class, + ZonedDateTime.class + }; + Object[] values = { + Duration.ofDays(1), + Instant.now(), + LocalDate.now(), + LocalDateTime.now(), + MonthDay.of(1, 1), + Period.ofMonths(1), + Year.now(), + YearMonth.now(), + ZonedDateTime.now() + }; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testJavaTimeClassesNulls() { + Class[] primitives = { + Duration.class, + Instant.class, + LocalDate.class, + LocalDateTime.class, + MonthDay.class, + Period.class, + Year.class, + YearMonth.class, + ZonedDateTime.class + }; + Object[] values = new Object[9]; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testCustomEnum() { + Class[] primitives = {ExampleCustomEnum.class, ExampleCustomEnum.class}; + Object[] values = {ExampleCustomEnum.ONE, ExampleCustomEnum.TWO}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testCustomEnumNulls() { + Class[] primitives = {ExampleCustomEnum.class}; + Object[] values = {null}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testCustomComplexClass() { + Class[] primitives = {ExampleCustomClass.class, ExampleCustomClass.class}; + Object[] values = { + new ExampleCustomClass("Foo", "Bar"), new ExampleCustomClass("Bish", "Bash") + }; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testMDC() { + Class[] primitives = {Integer.class}; + Object[] values = {1}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values, Map.of("A", "1", "B", "2"))); + } + + @Test + void testUUID() { + Class[] primitives = {UUID.class}; + Object[] values = {UUID.randomUUID()}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + @Test + void testUUIDNull() { + Class[] primitives = {UUID.class}; + Object[] values = {null}; + check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); + } + + void check(Invocation invocation) { + Invocation deserialized = serdeser(invocation); + Assertions.assertEquals(deserialized, serdeser(invocation)); + Assertions.assertEquals(invocation, deserialized); + } + + Invocation serdeser(Invocation invocation) { + var writer = new StringWriter(); + serializer.serializeInvocation(invocation, writer); + log.info("Serialised as: {}", writer); + return serializer.deserializeInvocation(new StringReader(writer.toString())); + } + + enum ExampleCustomEnum { + ONE, + TWO + } + + @Getter + static class ExampleCustomClass { + + private final String arg1; + private final String arg2; + + ExampleCustomClass(String arg1, String arg2) { + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + public String toString() { + return "ExampleCustomClass{" + "arg1='" + arg1 + '\'' + ", arg2='" + arg2 + '\'' + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExampleCustomClass that = (ExampleCustomClass) o; + return Objects.equals(arg1, that.arg1) && Objects.equals(arg2, that.arg2); + } + + @Override + public int hashCode() { + return Objects.hash(arg1, arg2); + } + } +} diff --git a/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java index 4a1c9426..acdc76f7 100644 --- a/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java +++ b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java @@ -1,273 +1,29 @@ package com.gruelbox.transactionoutbox; -import java.io.StringReader; -import java.io.StringWriter; -import java.time.*; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.stream.Stream; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DynamicNode; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; -import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.api.Nested; -@SuppressWarnings("RedundantCast") @Slf4j class TestDefaultInvocationSerializer { - private static final String CLASS_NAME = "foo"; - private static final String METHOD_NAME = "bar"; - - @TestFactory - Stream versions() { - return TestingUtils.parameterizedClassTester( - "serializedVersion={0}", - Inner.class, - Stream.of(Arguments.of(1), Arguments.of(2), Arguments.of(new Object[] {null}))); - } - - @SuppressWarnings("JUnitMalformedDeclaration") - static class Inner { - - private final DefaultInvocationSerializer serializer; - - Inner(Integer version) { - this.serializer = - DefaultInvocationSerializer.builder() - .serializableTypes(Set.of(ExampleCustomEnum.class, ExampleCustomClass.class)) - .version(version) - .build(); - } - - @Test - void testNoArgs() { - check(new Invocation(String.class.getName(), "toString", new Class[0], new Object[0])); - } - - @Test - void testArrays() { - check( - new Invocation( - CLASS_NAME, - METHOD_NAME, - new Class[] {int[].class}, - new Object[] {new int[] {1, 2, 3}})); - check( - new Invocation( - CLASS_NAME, - METHOD_NAME, - new Class[] {Integer[].class}, - new Object[] {new Integer[] {1, 2, 3}})); - check( - new Invocation( - CLASS_NAME, - METHOD_NAME, - new Class[] {String[].class}, - new Object[] {new String[] {"1", "2", "3"}})); - } - - @Test - void testPrimitives() { - Class[] primitives = { - byte.class, - short.class, - int.class, - long.class, - float.class, - double.class, - boolean.class, - char.class - }; - Object[] values = {(byte) 1, (short) 2, 3, 4L, 1.23F, 1.23D, true, '-'}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testBoxedPrimitives() { - Class[] primitives = { - Byte.class, - Short.class, - Integer.class, - Long.class, - Float.class, - Double.class, - Boolean.class, - Character.class, - String.class - }; - Object[] values = { - (Byte) (byte) 1, - (Short) (short) 2, - (Integer) 3, - (Long) 4L, - (Float) 1.23F, - (Double) 1.23D, - (Boolean) true, - (Character) '-', - "Foo" - }; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testJavaDateEnums() { - Class[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class}; - Object[] values = {DayOfWeek.FRIDAY, Month.APRIL, ChronoUnit.DAYS}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testJavaDateEnumsNulls() { - Class[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class}; - Object[] values = {null, null, null}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testJavaUtilDate() { - Class[] primitives = {Date.class, Date.class}; - Object[] values = {new Date(), null}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testJavaTimeClasses() { - Class[] primitives = { - Duration.class, - Instant.class, - LocalDate.class, - LocalDateTime.class, - MonthDay.class, - Period.class, - Year.class, - YearMonth.class, - ZonedDateTime.class - }; - Object[] values = { - Duration.ofDays(1), - Instant.now(), - LocalDate.now(), - LocalDateTime.now(), - MonthDay.of(1, 1), - Period.ofMonths(1), - Year.now(), - YearMonth.now(), - ZonedDateTime.now() - }; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testJavaTimeClassesNulls() { - Class[] primitives = { - Duration.class, - Instant.class, - LocalDate.class, - LocalDateTime.class, - MonthDay.class, - Period.class, - Year.class, - YearMonth.class, - ZonedDateTime.class - }; - Object[] values = new Object[9]; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testCustomEnum() { - Class[] primitives = {ExampleCustomEnum.class, ExampleCustomEnum.class}; - Object[] values = {ExampleCustomEnum.ONE, ExampleCustomEnum.TWO}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testCustomEnumNulls() { - Class[] primitives = {ExampleCustomEnum.class}; - Object[] values = {null}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testCustomComplexClass() { - Class[] primitives = {ExampleCustomClass.class, ExampleCustomClass.class}; - Object[] values = { - new ExampleCustomClass("Foo", "Bar"), new ExampleCustomClass("Bish", "Bash") - }; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testMDC() { - Class[] primitives = {Integer.class}; - Object[] values = {1}; - check( - new Invocation(CLASS_NAME, METHOD_NAME, primitives, values, Map.of("A", "1", "B", "2"))); - } - - @Test - void testUUID() { - Class[] primitives = {UUID.class}; - Object[] values = {UUID.randomUUID()}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - @Test - void testUUIDNull() { - Class[] primitives = {UUID.class}; - Object[] values = {null}; - check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values)); - } - - void check(Invocation invocation) { - Invocation deserialized = serdeser(invocation); - Assertions.assertEquals(deserialized, serdeser(invocation)); - Assertions.assertEquals(invocation, deserialized); - } - - Invocation serdeser(Invocation invocation) { - var writer = new StringWriter(); - serializer.serializeInvocation(invocation, writer); - log.info("Serialised as: {}", writer); - return serializer.deserializeInvocation(new StringReader(writer.toString())); + @Nested + class Version1 extends AbstractTestDefaultInvocationSerializer { + public Version1() { + super(1); } } - enum ExampleCustomEnum { - ONE, - TWO - } - - @Getter - static class ExampleCustomClass { - - private final String arg1; - private final String arg2; - - ExampleCustomClass(String arg1, String arg2) { - this.arg1 = arg1; - this.arg2 = arg2; - } - - @Override - public String toString() { - return "ExampleCustomClass{" + "arg1='" + arg1 + '\'' + ", arg2='" + arg2 + '\'' + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ExampleCustomClass that = (ExampleCustomClass) o; - return Objects.equals(arg1, that.arg1) && Objects.equals(arg2, that.arg2); + @Nested + class Version2 extends AbstractTestDefaultInvocationSerializer { + public Version2() { + super(2); } + } - @Override - public int hashCode() { - return Objects.hash(arg1, arg2); + @Nested + class NullVersion extends AbstractTestDefaultInvocationSerializer { + public NullVersion() { + super(null); } } } From f88b5f17a61cadfcdefc5a6df86f6e5d6b6c11f9 Mon Sep 17 00:00:00 2001 From: Graham Crockford Date: Sat, 23 Mar 2024 20:44:24 +0000 Subject: [PATCH 4/4] Split oracle test runs into separate jobs --- .github/workflows/pull_request.yml | 2 +- pom.xml | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6ccfca40..5202a06e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -50,7 +50,7 @@ jobs: matrix: os: [ ubuntu-latest ] jdk: [ 11,17,21 ] - db: [ nodb,mysql,postgres,oracle ] + db: [ nodb,mysql,postgres,oracle18,oracle21 ] fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/pom.xml b/pom.xml index 94291631..23044163 100644 --- a/pom.xml +++ b/pom.xml @@ -440,14 +440,29 @@ - only-oracle-tests + only-oracle18-tests maven-surefire-plugin - **/*Oracle*.java + **/*Oracle18*.java + + + + + + + + only-oracle21-tests + + + + maven-surefire-plugin + + + **/*Oracle21*.java