From 27d81460850b4ceead9e3f99a52faeeca4ae54f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 12:26:33 +0100 Subject: [PATCH 1/5] feat: add endpoint to publish raw domain events --- .../playground/DomainEventPostController.java | 55 +++++++++++++++++++ docker-compose.ci.yml | 2 +- docker-compose.yml | 18 +----- .../http}/backoffice_frontend.http | 0 etc/http/publish_domain_events.http | 20 +++++++ .../main/tv/codely/shared/domain/Utils.java | 8 +++ 6 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java rename {doc/endpoints => etc/http}/backoffice_frontend.http (100%) create mode 100644 etc/http/publish_domain_events.http diff --git a/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java new file mode 100644 index 00000000..b221dc36 --- /dev/null +++ b/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java @@ -0,0 +1,55 @@ +package tv.codely.apps.mooc.backend.controller.playground; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import tv.codely.shared.domain.Utils; + +import java.io.Serializable; +import java.util.HashMap; + +@RestController +record DomainEventPostController(RabbitTemplate rabbitTemplate) { + @PostMapping(value = "/domain-events") + public ResponseEntity index(@RequestBody Request request) { + System.out.println(request.eventName()); + + var serializedEvent = Utils.jsonEncode(request.eventRaw()); + + Message message = new Message( + serializedEvent.getBytes(), + MessagePropertiesBuilder.newInstance().setContentEncoding("utf-8").setContentType("application/json").build() + ); + + rabbitTemplate.send("domain_events", request.eventName(), message); + + return new ResponseEntity<>(HttpStatus.CREATED); + } +} + +final class Request { + + private String eventName; + private Object eventRaw; + + public void setEventName(String eventName) { + this.eventName = eventName; + } + + public void setEventRaw(Object eventRaw) { + this.eventRaw = eventRaw; + } + + String eventName() { + return eventName; + } + + Object eventRaw() { + return eventRaw; + } +} diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 654ffd12..d2c1c99c 100755 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -23,7 +23,7 @@ services: platform: linux/amd64 restart: unless-stopped ports: - - "5630:5672" + - "5672:5672" - "8090:15672" environment: - RABBITMQ_DEFAULT_USER=codely diff --git a/docker-compose.yml b/docker-compose.yml index 3da6fcb7..2dd4b00c 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: platform: linux/amd64 restart: unless-stopped ports: - - "5630:5672" + - "5672:5672" - "8090:15672" environment: - RABBITMQ_DEFAULT_USER=codely @@ -91,23 +91,7 @@ services: - backoffice_elasticsearch command: ["./gradlew", "bootRun", "--args", "mooc_backend server"] - test_server_java: - container_name: codely-java_ddd_example-test_server - build: - context: . - dockerfile: Dockerfile - restart: unless-stopped - volumes: - - .:/app:delegated - - test_gradle_cache:/app/.gradle - depends_on: - - shared_mysql - - shared_rabbitmq - - backoffice_elasticsearch - tty: true - volumes: backoffice_backend_gradle_cache: backoffice_frontend_gradle_cache: mooc_backend_gradle_cache: - test_gradle_cache: diff --git a/doc/endpoints/backoffice_frontend.http b/etc/http/backoffice_frontend.http similarity index 100% rename from doc/endpoints/backoffice_frontend.http rename to etc/http/backoffice_frontend.http diff --git a/etc/http/publish_domain_events.http b/etc/http/publish_domain_events.http new file mode 100644 index 00000000..d6a19315 --- /dev/null +++ b/etc/http/publish_domain_events.http @@ -0,0 +1,20 @@ +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.created", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.created", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "{{$random.uuid}}", + "name": "Demo course", + "duration": "2 days" + } + }, + "meta": { + } + } +} diff --git a/src/shared/main/tv/codely/shared/domain/Utils.java b/src/shared/main/tv/codely/shared/domain/Utils.java index 53dbc3ea..a3a56a6f 100644 --- a/src/shared/main/tv/codely/shared/domain/Utils.java +++ b/src/shared/main/tv/codely/shared/domain/Utils.java @@ -28,6 +28,14 @@ public static String jsonEncode(HashMap map) { } } + public static String jsonEncode(Object map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return ""; + } + } + public static HashMap jsonDecode(String body) { try { return new ObjectMapper().readValue(body, HashMap.class); From ec05c6cd9bb3f7036d3e7e24550cc44c1fbba310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 15:37:58 +0100 Subject: [PATCH 2/5] feat: add use case to rename a course --- .../playground/DomainEventPostController.java | 4 +- .../rename/BackofficeCourseRenamer.java | 22 ++++ ...RenameBackofficeCourseOnCourseRenamed.java | 22 ++++ .../courses/domain/BackofficeCourse.java | 6 +- .../domain/BackofficeCourseRepository.java | 5 +- ...asticsearchBackofficeCourseRepository.java | 8 +- ...MemoryCacheBackofficeCourseRepository.java | 72 ++++++----- .../MySqlBackofficeCourseRepository.java | 8 +- ...earchBackofficeCourseRepositoryShould.java | 118 +++++++++++++----- .../course/CourseRenamedDomainEvent.java | 78 ++++++++++++ .../ElasticsearchRepository.java | 22 +++- .../hibernate/HibernateRepository.java | 4 + .../InfrastructureTestCase.java | 39 +++--- 13 files changed, 317 insertions(+), 91 deletions(-) create mode 100644 src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java create mode 100644 src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java create mode 100644 src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java diff --git a/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java index b221dc36..dfc96572 100644 --- a/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java +++ b/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java @@ -8,10 +8,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import tv.codely.shared.domain.Utils; -import java.io.Serializable; -import java.util.HashMap; +import tv.codely.shared.domain.Utils; @RestController record DomainEventPostController(RabbitTemplate rabbitTemplate) { diff --git a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java new file mode 100644 index 00000000..35a3034a --- /dev/null +++ b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java @@ -0,0 +1,22 @@ +package tv.codely.backoffice.courses.application.rename; + +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class BackofficeCourseRenamer { + private final BackofficeCourseRepository repository; + + public BackofficeCourseRenamer(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public void rename(String id, String name) { + this.repository.search(id) + .ifPresent(course -> { + course.rename(name); + + this.repository.save(course); + }); + } +} diff --git a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java new file mode 100644 index 00000000..d9a05491 --- /dev/null +++ b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java @@ -0,0 +1,22 @@ +package tv.codely.backoffice.courses.application.rename; + +import org.springframework.context.event.EventListener; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.domain.course.CourseRenamedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class RenameBackofficeCourseOnCourseRenamed { + private final BackofficeCourseRenamer renamer; + + public RenameBackofficeCourseOnCourseRenamed(BackofficeCourseRenamer renamer) { + this.renamer = renamer; + } + + @EventListener + public void on(CourseRenamedDomainEvent event) { + renamer.rename(event.aggregateId(), event.name()); + } +} diff --git a/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java index f97d7488..05f88b1c 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java @@ -7,7 +7,7 @@ public final class BackofficeCourse { private final String id; - private final String name; + private String name; private final String duration; public BackofficeCourse() { @@ -30,6 +30,10 @@ public static BackofficeCourse fromPrimitives(Map plainData) { ); } + public void rename(String newName) { + this.name = newName; + } + public String id() { return id; } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java index 2183d54a..36893521 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java @@ -3,11 +3,14 @@ import tv.codely.shared.domain.criteria.Criteria; import java.util.List; +import java.util.Optional; public interface BackofficeCourseRepository { void save(BackofficeCourse course); - List searchAll(); + Optional search(String id); + + List searchAll(); List matching(Criteria criteria); } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java index cb84311a..1206e8fb 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java @@ -9,6 +9,7 @@ import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchRepository; import java.util.List; +import java.util.Optional; @Primary @Service @@ -22,7 +23,12 @@ public void save(BackofficeCourse course) { persist(course.id(), course.toPrimitives()); } - @Override + @Override + public Optional search(String id) { + return this.searchById(id, BackofficeCourse::fromPrimitives); + } + + @Override public List searchAll() { return searchAllInElastic(BackofficeCourse::fromPrimitives); } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java index c616b49c..b645f5b4 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java @@ -7,46 +7,58 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; public final class InMemoryCacheBackofficeCourseRepository implements BackofficeCourseRepository { - private final BackofficeCourseRepository repository; - private List courses = new ArrayList<>(); - private HashMap> matchingCourses = new HashMap<>(); + private final BackofficeCourseRepository repository; + private List courses = new ArrayList<>(); + private HashMap> matchingCourses = new HashMap<>(); - public InMemoryCacheBackofficeCourseRepository(BackofficeCourseRepository repository) { - this.repository = repository; - } + public InMemoryCacheBackofficeCourseRepository(BackofficeCourseRepository repository) { + this.repository = repository; + } - @Override - public void save(BackofficeCourse course) { - repository.save(course); - } + @Override + public void save(BackofficeCourse course) { + repository.save(course); + } - @Override - public List searchAll() { - return courses.isEmpty() ? searchAndFillCache() : courses; - } + @Override + public List searchAll() { + return courses.isEmpty() ? searchAndFillCache() : courses; + } - @Override - public List matching(Criteria criteria) { - return matchingCourses.containsKey(criteria.serialize()) - ? matchingCourses.get(criteria.serialize()) - : searchMatchingAndFillCache(criteria); - } + public Optional search(String id) { + return courses.stream() + .filter(course -> course.id().equals(id)) + .findFirst() + .or(() -> { + Optional course = repository.search(id); + course.ifPresent(courses::add); + return course; + }); + } - private List searchMatchingAndFillCache(Criteria criteria) { - List courses = repository.matching(criteria); + @Override + public List matching(Criteria criteria) { + return matchingCourses.containsKey(criteria.serialize()) + ? matchingCourses.get(criteria.serialize()) + : searchMatchingAndFillCache(criteria); + } - this.matchingCourses.put(criteria.serialize(), courses); + private List searchMatchingAndFillCache(Criteria criteria) { + List courses = repository.matching(criteria); - return courses; - } + this.matchingCourses.put(criteria.serialize(), courses); - private List searchAndFillCache() { - List courses = repository.searchAll(); + return courses; + } - this.courses = courses; + private List searchAndFillCache() { + List courses = repository.searchAll(); - return courses; - } + this.courses = courses; + + return courses; + } } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java index bcb89765..a23fd771 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java @@ -10,6 +10,7 @@ import tv.codely.shared.infrastructure.hibernate.HibernateRepository; import java.util.List; +import java.util.Optional; @Service @Transactional("backoffice-transaction_manager") @@ -23,7 +24,12 @@ public void save(BackofficeCourse course) { persist(course); } - @Override + @Override + public Optional search(String id) { + return byId(id); + } + + @Override public List searchAll() { return all(); } diff --git a/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java b/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java index 5f632890..f94748b9 100644 --- a/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java +++ b/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java @@ -7,56 +7,108 @@ import tv.codely.backoffice.courses.domain.BackofficeCourse; import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.shared.domain.UuidMother; +import tv.codely.shared.domain.WordMother; import tv.codely.shared.domain.criteria.Criteria; import java.io.IOException; import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; final class ElasticsearchBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { - @Autowired - private ElasticsearchBackofficeCourseRepository repository; + @Autowired + private ElasticsearchBackofficeCourseRepository repository; - @BeforeEach - protected void setUp() throws IOException { - clearElasticsearch(); - } + @BeforeEach + protected void setUp() throws IOException { + clearElasticsearch(); + } - @Test - void save_a_course() { - repository.save(BackofficeCourseMother.random()); - } + @Test + void save_a_course() { + repository.save(BackofficeCourseMother.random()); + } - @Test - void search_all_existing_courses() throws Exception { - BackofficeCourse course = BackofficeCourseMother.random(); - BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + @Test + void search_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); - List expected = Arrays.asList(course, anotherCourse); + repository.save(course); - repository.save(course); - repository.save(anotherCourse); + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } - eventually(() -> assertEquals(expected, repository.searchAll())); - } + @Test + void update_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); - @Test - void search_courses_using_a_criteria() throws Exception { - BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); - BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); - BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); - BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + repository.save(course); + course.rename(WordMother.random()); + repository.save(course); - Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); - List expected = Arrays.asList(matchingCourse, anotherMatchingCourse); + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } - repository.save(matchingCourse); - repository.save(anotherMatchingCourse); - repository.save(intellijCourse); - repository.save(cobolCourse); + @Test + void not_find_a_non_existing_course() { + assertEquals(Optional.empty(), repository.search(UuidMother.random())); + } - eventually(() -> assertEquals(expected, repository.matching(criteria))); - } + @Test + void search_all_existing_courses() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + + List expected = Arrays.asList(course, anotherCourse); + + repository.save(course); + repository.save(anotherCourse); + + eventually(() -> { + List actual = repository.searchAll(); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } + + @Test + void search_courses_using_a_criteria() throws Exception { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); + BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + List expected = Arrays.asList(matchingCourse, anotherMatchingCourse); + + repository.save(matchingCourse); + repository.save(anotherMatchingCourse); + repository.save(intellijCourse); + repository.save(cobolCourse); + + eventually(() -> { + List actual = repository.matching(criteria); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } } diff --git a/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 00000000..b85592b6 --- /dev/null +++ b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java @@ -0,0 +1,78 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseRenamedDomainEvent extends DomainEvent { + private final String name; + + public CourseRenamedDomainEvent() { + super(null); + + this.name = null; + } + + public CourseRenamedDomainEvent(String aggregateId, String name) { + super(aggregateId); + + this.name = name; + } + + public CourseRenamedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + } + + @Override + public String eventName() { + return "course.created"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap<>() {{ + put("name", name); + }}; + } + + @Override + public CourseRenamedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseRenamedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name") + ); + } + + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CourseRenamedDomainEvent that = (CourseRenamedDomainEvent) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java b/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java index 3f4730a6..c87903b0 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java +++ b/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java @@ -1,5 +1,7 @@ package tv.codely.shared.infrastructure.elasticsearch; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RequestOptions; @@ -27,7 +29,25 @@ protected List searchAllInElastic(Function, T> unserializ return searchAllInElastic(unserializer, new SearchSourceBuilder()); } - protected List searchAllInElastic( + protected Optional searchById(String id, Function, T> unserializer) { + GetRequest request = new GetRequest(client.indexFor(moduleName()), "_doc", id); + + try { + GetResponse getResponse = client.highLevelClient().get(request, RequestOptions.DEFAULT); + + if (!getResponse.isExists()) { + return Optional.empty(); + } + + return Optional.of(unserializer.apply(getResponse.getSourceAsMap())); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + + protected List searchAllInElastic( Function, T> unserializer, SearchSourceBuilder sourceBuilder ) { diff --git a/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java b/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java index 38413cc2..b6ae61e2 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java +++ b/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java @@ -29,6 +29,10 @@ protected Optional byId(Identifier id) { return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); } + protected Optional byId(String id) { + return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); + } + protected List byCriteria(Criteria criteria) { CriteriaQuery hibernateCriteria = criteriaConverter.convert(criteria, aggregateClass); diff --git a/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java b/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java index ec55ea6a..af93e673 100644 --- a/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java +++ b/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java @@ -2,28 +2,27 @@ public abstract class InfrastructureTestCase { private final int MAX_ATTEMPTS = 3; - private final int MILLIS_TO_WAIT_BETWEEN_RETRIES = 300; + private final int MILLIS_TO_WAIT_BETWEEN_RETRIES = 700; - protected void eventually(Runnable assertion) throws Exception { - int attempts = 0; - boolean allOk = false; + protected void eventually(Runnable assertion) throws Exception { + int attempts = 0; - while (attempts < MAX_ATTEMPTS && !allOk) { - try { - assertion.run(); + while (true) { + try { + assertion.run(); + return; + } catch (Throwable error) { + attempts++; - allOk = true; - } catch (Throwable error) { - attempts++; + if (attempts >= MAX_ATTEMPTS) { + throw new Exception( + String.format("Could not assert after %d retries. Last error: %s", MAX_ATTEMPTS, error.getMessage()), + error + ); + } - if (attempts > MAX_ATTEMPTS) { - throw new Exception( - String.format("Could not assert after some retries. Last error: %s", error.getMessage()) - ); - } - - Thread.sleep(MILLIS_TO_WAIT_BETWEEN_RETRIES); - } - } - } + Thread.sleep(MILLIS_TO_WAIT_BETWEEN_RETRIES); + } + } + } } From 4f0ff478e858df60d105aad16e0f5565431ee850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 18:24:09 +0100 Subject: [PATCH 3/5] fix: rabbitmq event bus --- Makefile | 2 +- apps/main/resources/application.properties | 1 + .../pages/courses/partials/list_courses.ftl | 4 +- .../backend/BackofficeBackendApplication.java | 7 +- .../apps/backoffice/backend/command/.gitkeep | 0 .../ConsumeRabbitMqDomainEventsCommand.java | 18 +++++ .../config/BackofficeFrontendWebConfig.java | 15 ++++ .../ConsumeRabbitMqDomainEventsCommand.java | 2 +- .../MoocBackendServerConfiguration.java | 8 +- docker-compose.yml | 42 ++++++++++ etc/http/publish_domain_events.http | 23 +++++- .../rename/BackofficeCourseRenamer.java | 12 ++- ...RenameBackofficeCourseOnCourseRenamed.java | 3 +- .../domain/BackofficeCourseNotFound.java | 7 ++ .../BackofficeMySqlEventBusConfiguration.java | 1 + .../rabbitmq/RabbitMqEventBusShould.java | 2 +- .../course/CourseRenamedDomainEvent.java | 2 +- .../DomainEventSubscribersInformation.java | 79 ++++++++++--------- .../RabbitMqDomainEventsConsumer.java | 20 ++--- .../bus/event/rabbitmq/RabbitMqEventBus.java | 42 +++++----- .../RabbitMqEventBusConfiguration.java | 2 +- .../spring/SpringApplicationEventBus.java | 2 - .../infrastructure/cli/ConsoleCommand.java | 28 +++---- 23 files changed, 219 insertions(+), 103 deletions(-) create mode 100644 apps/main/resources/application.properties delete mode 100644 apps/main/tv/codely/apps/backoffice/backend/command/.gitkeep create mode 100644 apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java create mode 100644 src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java diff --git a/Makefile b/Makefile index c312ce16..4855cd12 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: build start: - @docker-compose -f docker-compose.ci.yml up -d + @docker compose -f docker-compose.ci.yml up -d build: @./gradlew build --warning-mode all diff --git a/apps/main/resources/application.properties b/apps/main/resources/application.properties new file mode 100644 index 00000000..e439ebd8 --- /dev/null +++ b/apps/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true diff --git a/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl b/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl index e81af3c3..aa64bee9 100644 --- a/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl +++ b/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl @@ -142,12 +142,12 @@ const urlParts = inputs.map(input => input.name + "=" + input.value); - const url = "http://localhost:8091/courses?" + urlParts.join("&"); + const url = "http://localhost:8040/courses?" + urlParts.join("&"); addCoursesList(url); } diff --git a/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java b/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java index 873122f3..0a9374e3 100644 --- a/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java +++ b/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; +import tv.codely.apps.backoffice.backend.command.ConsumeRabbitMqDomainEventsCommand; import tv.codely.shared.domain.Service; @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) @@ -17,8 +18,10 @@ public class BackofficeBackendApplication { public static HashMap> commands() { - return new HashMap>() { - {} + return new HashMap<>() { + { + put("domain-events:rabbitmq:consume", ConsumeRabbitMqDomainEventsCommand.class); + } }; } } diff --git a/apps/main/tv/codely/apps/backoffice/backend/command/.gitkeep b/apps/main/tv/codely/apps/backoffice/backend/command/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 00000000..018972f4 --- /dev/null +++ b/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.backoffice.backend.command; + +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeRabbitMqDomainEventsCommand extends ConsoleCommand { + + private final RabbitMqDomainEventsConsumer consumer; + + public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume("backoffice"); + } +} diff --git a/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java b/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java index 31405d6f..62749d15 100644 --- a/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java +++ b/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java @@ -1,7 +1,9 @@ package tv.codely.apps.backoffice.frontend.config; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; @@ -10,6 +12,10 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + @Configuration @EnableWebMvc public class BackofficeFrontendWebConfig implements WebMvcConfigurer { @@ -43,4 +49,13 @@ public FreeMarkerConfigurer freeMarkerConfigurer() { return configurer; } + + @Primary + @Bean + public RabbitMqEventBus rabbitMqEventBus( + RabbitMqPublisher publisher, + @Qualifier("backofficeMysqlEventBus") MySqlEventBus failoverPublisher + ) { + return new RabbitMqEventBus(publisher, failoverPublisher); + } } diff --git a/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java index 993dcd5b..ce5d7bdc 100644 --- a/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java +++ b/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -13,6 +13,6 @@ public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) @Override public void execute(String[] args) { - consumer.consume(); + consumer.consume("mooc"); } } diff --git a/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java b/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java index 9974cddc..3041e7bd 100644 --- a/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java +++ b/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java @@ -1,5 +1,7 @@ package tv.codely.apps.mooc.backend.config; +import java.util.Optional; + import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,9 +12,9 @@ @Configuration public class MoocBackendServerConfiguration { - private final RequestMappingHandlerMapping mapping; + private final Optional mapping; - public MoocBackendServerConfiguration(RequestMappingHandlerMapping mapping) { + public MoocBackendServerConfiguration(Optional mapping) { this.mapping = mapping; } @@ -20,7 +22,7 @@ public MoocBackendServerConfiguration(RequestMappingHandlerMapping mapping) { public FilterRegistrationBean apiExceptionMiddleware() { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new ApiExceptionMiddleware(mapping)); + mapping.ifPresent(map -> registrationBean.setFilter(new ApiExceptionMiddleware(map))); return registrationBean; } diff --git a/docker-compose.yml b/docker-compose.yml index 2dd4b00c..216bacd5 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,12 +51,29 @@ services: volumes: - .:/app:delegated - backoffice_backend_gradle_cache:/app/.gradle + - backoffice_backend_build:/app/build depends_on: - shared_mysql - shared_rabbitmq - backoffice_elasticsearch command: ["./gradlew", "bootRun", "--args", "backoffice_backend server"] + backoffice_backend_consumers_java: + container_name: codely-java_ddd_example-backoffice_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - backoffice_consumers_gradle_cache:/app/.gradle + - backoffice_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_backend domain-events:rabbitmq:consume"] + backoffice_frontend_server_java: container_name: codely-java_ddd_example-backoffice_frontend_server build: @@ -68,6 +85,7 @@ services: volumes: - .:/app:delegated - backoffice_frontend_gradle_cache:/app/.gradle + - backoffice_frontend_build:/app/build depends_on: - shared_mysql - shared_rabbitmq @@ -85,13 +103,37 @@ services: volumes: - .:/app:delegated - mooc_backend_gradle_cache:/app/.gradle + - mooc_backend_build:/app/build depends_on: - shared_mysql - shared_rabbitmq - backoffice_elasticsearch command: ["./gradlew", "bootRun", "--args", "mooc_backend server"] + mooc_backend_consumers_java: + container_name: codely-java_ddd_example-mooc_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - mooc_consumers_gradle_cache:/app/.gradle + - mooc_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "mooc_backend domain-events:rabbitmq:consume"] + volumes: backoffice_backend_gradle_cache: + backoffice_backend_build: + backoffice_consumers_gradle_cache: + backoffice_consumers_build: backoffice_frontend_gradle_cache: + backoffice_frontend_build: mooc_backend_gradle_cache: + mooc_backend_build: + mooc_consumers_gradle_cache: + mooc_consumers_build: diff --git a/etc/http/publish_domain_events.http b/etc/http/publish_domain_events.http index d6a19315..9b512e6b 100644 --- a/etc/http/publish_domain_events.http +++ b/etc/http/publish_domain_events.http @@ -9,7 +9,7 @@ Content-Type: application/json "type": "course.created", "occurred_on": "2023-11-14 10:00:00", "attributes": { - "id": "{{$random.uuid}}", + "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", "name": "Demo course", "duration": "2 days" } @@ -18,3 +18,24 @@ Content-Type: application/json } } } + +### +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.renamed", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.renamed", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", + "name": "heeey 22222" + } + }, + "meta": { + } + } +} diff --git a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java index 35a3034a..e50c42f4 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java @@ -1,5 +1,6 @@ package tv.codely.backoffice.courses.application.rename; +import tv.codely.backoffice.courses.domain.BackofficeCourseNotFound; import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; import tv.codely.shared.domain.Service; @@ -13,10 +14,13 @@ public BackofficeCourseRenamer(BackofficeCourseRepository repository) { public void rename(String id, String name) { this.repository.search(id) - .ifPresent(course -> { - course.rename(name); + .ifPresentOrElse(course -> { + course.rename(name); - this.repository.save(course); - }); + this.repository.save(course); + }, + () -> { + throw new BackofficeCourseNotFound(id); + }); } } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java index d9a05491..c92e48d3 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java @@ -3,11 +3,10 @@ import org.springframework.context.event.EventListener; import tv.codely.shared.domain.Service; import tv.codely.shared.domain.bus.event.DomainEventSubscriber; -import tv.codely.shared.domain.course.CourseCreatedDomainEvent; import tv.codely.shared.domain.course.CourseRenamedDomainEvent; @Service -@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +@DomainEventSubscriber({CourseRenamedDomainEvent.class}) public final class RenameBackofficeCourseOnCourseRenamed { private final BackofficeCourseRenamer renamer; diff --git a/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java new file mode 100644 index 00000000..f5596ffa --- /dev/null +++ b/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.courses.domain; + +public class BackofficeCourseNotFound extends RuntimeException { + public BackofficeCourseNotFound(String id) { + super(String.format("The course <%s> doesn't exist", id)); + } +} diff --git a/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java b/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java index 4704f7b3..1113298e 100644 --- a/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java +++ b/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; diff --git a/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java b/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java index e5592563..70c8de26 100644 --- a/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java +++ b/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java @@ -46,7 +46,7 @@ void publish_and_consume_domain_events_from_rabbitmq() throws Exception { eventBus.publish(Collections.singletonList(domainEvent)); - consumer.consume(); + consumer.consume("mooc"); eventually(() -> assertTrue(subscriber.hasBeenExecuted)); } diff --git a/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java index b85592b6..2fb40972 100644 --- a/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java +++ b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java @@ -34,7 +34,7 @@ public CourseRenamedDomainEvent( @Override public String eventName() { - return "course.created"; + return "course.renamed"; } @Override diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java index e7f5b923..88556f11 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java @@ -11,43 +11,44 @@ @Service public final class DomainEventSubscribersInformation { - HashMap, DomainEventSubscriberInformation> information; - - public DomainEventSubscribersInformation(HashMap, DomainEventSubscriberInformation> information) { - this.information = information; - } - - public DomainEventSubscribersInformation() { - this(scanDomainEventSubscribers()); - } - - private static HashMap, DomainEventSubscriberInformation> scanDomainEventSubscribers() { - Reflections reflections = new Reflections("tv.codely"); - Set> subscribers = reflections.getTypesAnnotatedWith(DomainEventSubscriber.class); - - HashMap, DomainEventSubscriberInformation> subscribersInformation = new HashMap<>(); - - for (Class subscriberClass : subscribers) { - DomainEventSubscriber annotation = subscriberClass.getAnnotation(DomainEventSubscriber.class); - - subscribersInformation.put( - subscriberClass, - new DomainEventSubscriberInformation(subscriberClass, Arrays.asList(annotation.value())) - ); - } - - return subscribersInformation; - } - - public Collection all() { - return information.values(); - } - - public String[] rabbitMqFormattedNames() { - return information.values() - .stream() - .map(DomainEventSubscriberInformation::formatRabbitMqQueueName) - .distinct() - .toArray(String[]::new); - } + HashMap, DomainEventSubscriberInformation> information; + + public DomainEventSubscribersInformation(HashMap, DomainEventSubscriberInformation> information) { + this.information = information; + } + + public DomainEventSubscribersInformation() { + this(scanDomainEventSubscribers()); + } + + private static HashMap, DomainEventSubscriberInformation> scanDomainEventSubscribers() { + Reflections reflections = new Reflections("tv.codely"); + Set> subscribers = reflections.getTypesAnnotatedWith(DomainEventSubscriber.class); + + HashMap, DomainEventSubscriberInformation> subscribersInformation = new HashMap<>(); + + for (Class subscriberClass : subscribers) { + DomainEventSubscriber annotation = subscriberClass.getAnnotation(DomainEventSubscriber.class); + + subscribersInformation.put( + subscriberClass, + new DomainEventSubscriberInformation(subscriberClass, Arrays.asList(annotation.value())) + ); + } + + return subscribersInformation; + } + + public Collection all() { + return information.values(); + } + + public String[] rabbitMqFormattedNamesFor(String contextName) { + return information.values() + .stream() + .map(DomainEventSubscriberInformation::formatRabbitMqQueueName) + .distinct() + .filter(queueName -> queueName.contains("." + contextName + ".")) + .toArray(String[]::new); + } } diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java index f613ecc2..1311278a 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -13,7 +13,6 @@ import tv.codely.shared.infrastructure.bus.event.DomainEventJsonDeserializer; import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -21,13 +20,14 @@ @Service public final class RabbitMqDomainEventsConsumer { private final String CONSUMER_NAME = "domain_events_consumer"; - private final int MAX_RETRIES = 2; + private final int MAX_RETRIES = 10; private final DomainEventJsonDeserializer deserializer; private final ApplicationContext context; private final RabbitMqPublisher publisher; private final HashMap domainEventSubscribers = new HashMap<>(); RabbitListenerEndpointRegistry registry; private DomainEventSubscribersInformation information; + private String contextName; public RabbitMqDomainEventsConsumer( RabbitListenerEndpointRegistry registry, @@ -43,12 +43,14 @@ public RabbitMqDomainEventsConsumer( this.publisher = publisher; } - public void consume() { + public void consume(String contextName) { + this.contextName = contextName; + AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) registry.getListenerContainer( CONSUMER_NAME ); - container.addQueueNames(information.rabbitMqFormattedNames()); + container.addQueueNames(information.rabbitMqFormattedNamesFor(contextName)); container.start(); } @@ -68,12 +70,6 @@ public void consumer(Message message) throws Exception { try { subscriberOnMethod.invoke(subscriber, domainEvent); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException error) { - throw new Exception(String.format( - "The subscriber <%s> should implement a method `on` listening the domain event <%s>", - queue, - domainEvent.eventName() - )); } catch (Exception error) { handleConsumptionError(message, queue); } @@ -92,10 +88,14 @@ private void handleConsumptionError(Message message, String queue) { } private void sendToRetry(Message message, String queue) { + System.out.println("SENDING TO RETRY: " + contextName + " - " + queue); + sendMessageTo(RabbitMqExchangeNameFormatter.retry("domain_events"), message, queue); } private void sendToDeadLetter(Message message, String queue) { + System.out.println("SENDING TO DEAD LETTER: " + contextName + " - " + queue); + sendMessageTo(RabbitMqExchangeNameFormatter.deadLetter("domain_events"), message, queue); } diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java index 47d1738f..2bad6fd2 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java @@ -1,6 +1,8 @@ package tv.codely.shared.infrastructure.bus.event.rabbitmq; import org.springframework.amqp.AmqpException; +import org.springframework.context.annotation.Primary; +import tv.codely.shared.domain.Service; import tv.codely.shared.domain.bus.event.DomainEvent; import tv.codely.shared.domain.bus.event.EventBus; import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; @@ -8,27 +10,29 @@ import java.util.Collections; import java.util.List; +@Primary +@Service public class RabbitMqEventBus implements EventBus { - private final RabbitMqPublisher publisher; - private final MySqlEventBus failoverPublisher; - private final String exchangeName; + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + private final String exchangeName; - public RabbitMqEventBus(RabbitMqPublisher publisher, MySqlEventBus failoverPublisher) { - this.publisher = publisher; - this.failoverPublisher = failoverPublisher; - this.exchangeName = "domain_events"; - } + public RabbitMqEventBus(RabbitMqPublisher publisher, MySqlEventBus failoverPublisher) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + this.exchangeName = "domain_events"; + } - @Override - public void publish(List events) { - events.forEach(this::publish); - } + @Override + public void publish(List events) { + events.forEach(this::publish); + } - private void publish(DomainEvent domainEvent) { - try { - this.publisher.publish(domainEvent, exchangeName); - } catch (AmqpException error) { - failoverPublisher.publish(Collections.singletonList(domainEvent)); - } - } + private void publish(DomainEvent domainEvent) { + try { + this.publisher.publish(domainEvent, exchangeName); + } catch (AmqpException error) { + failoverPublisher.publish(Collections.singletonList(domainEvent)); + } + } } diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java index 4fe9688a..f95e1888 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java @@ -128,7 +128,7 @@ private HashMap retryQueueArguments(TopicExchange exchange, Stri return new HashMap() {{ put("x-dead-letter-exchange", exchange.getName()); put("x-dead-letter-routing-key", routingKey); - put("x-message-ttl", 1000); + put("x-message-ttl", 3000); }}; } } diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java index 0b2a16a4..56a6205e 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java @@ -1,14 +1,12 @@ package tv.codely.shared.infrastructure.bus.event.spring; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Primary; import tv.codely.shared.domain.Service; import tv.codely.shared.domain.bus.event.DomainEvent; import tv.codely.shared.domain.bus.event.EventBus; import java.util.List; -@Primary @Service public class SpringApplicationEventBus implements EventBus { private final ApplicationEventPublisher publisher; diff --git a/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java b/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java index 45468089..dd90a889 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java +++ b/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java @@ -4,22 +4,22 @@ @Service public abstract class ConsoleCommand { - private static final String ANSI_RESET = "\u001B[0m"; - private static final String ANSI_RED = "\u001B[31m"; - private static final String ANSI_CYAN = "\u001B[36m"; - private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_GREEN = "\u001B[32m"; - abstract public void execute(String[] args); + abstract public void execute(String[] args); - protected void log(String text) { - System.out.println(String.format("%s%s%s", ANSI_GREEN, text, ANSI_RESET)); - } + protected void log(String text) { + System.out.println(String.format("%s%s%s", ANSI_GREEN, text, ANSI_RESET)); + } - protected void info(String text) { - System.out.println(String.format("%s%s%s", ANSI_CYAN, text, ANSI_RESET)); - } + protected void info(String text) { + System.out.println(String.format("%s%s%s", ANSI_CYAN, text, ANSI_RESET)); + } - protected void error(String text) { - System.out.println(String.format("%s%s%s", ANSI_RED, text, ANSI_RESET)); - } + protected void error(String text) { + System.out.println(String.format("%s%s%s", ANSI_RED, text, ANSI_RESET)); + } } From b2681585c9b76912f8743315fc28348856e8f721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 19:52:01 +0100 Subject: [PATCH 4/5] feat: improve event consumption --- etc/http/publish_domain_events.http | 27 +++++++++++-- .../create/BackofficeCourseCreator.java | 4 +- ...asticsearchBackofficeCourseRepository.java | 40 +++++++++---------- .../RabbitMqDomainEventsConsumer.java | 6 ++- 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/etc/http/publish_domain_events.http b/etc/http/publish_domain_events.http index 9b512e6b..309c2fd7 100644 --- a/etc/http/publish_domain_events.http +++ b/etc/http/publish_domain_events.http @@ -9,7 +9,7 @@ Content-Type: application/json "type": "course.created", "occurred_on": "2023-11-14 10:00:00", "attributes": { - "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", + "id": "c3a11f1d-512e-420b-aeae-e687a3c449aa", "name": "Demo course", "duration": "2 days" } @@ -31,8 +31,29 @@ Content-Type: application/json "type": "course.renamed", "occurred_on": "2023-11-14 10:00:00", "attributes": { - "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", - "name": "heeey 22222" + "id": "7b081a3e-f90e-4efe-a3a5-81e853e89c8b", + "name": "Este es el nombre bueno" + } + }, + "meta": { + } + } +} + +### +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.renamed", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.renamed", + "occurred_on": "2022-11-14 10:00:00", + "attributes": { + "id": "7b081a3e-f90e-4efe-a3a5-81e853e89c8b", + "name": "Este es el nombre malo" } }, "meta": { diff --git a/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java b/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java index f129d16a..496b8cb5 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java @@ -13,6 +13,8 @@ public BackofficeCourseCreator(BackofficeCourseRepository repository) { } public void create(String id, String name, String duration) { - this.repository.save(new BackofficeCourse(id, name, duration)); + if (this.repository.search(id).isEmpty()) { + this.repository.save(new BackofficeCourse(id, name, duration)); + } } } diff --git a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java index 1206e8fb..c97dd85d 100644 --- a/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java +++ b/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java @@ -14,14 +14,14 @@ @Primary @Service public final class ElasticsearchBackofficeCourseRepository extends ElasticsearchRepository implements BackofficeCourseRepository { - public ElasticsearchBackofficeCourseRepository(ElasticsearchClient client) { - super(client); - } + public ElasticsearchBackofficeCourseRepository(ElasticsearchClient client) { + super(client); + } - @Override - public void save(BackofficeCourse course) { - persist(course.id(), course.toPrimitives()); - } + @Override + public void save(BackofficeCourse course) { + persist(course.id(), course.toPrimitives()); + } @Override public Optional search(String id) { @@ -29,17 +29,17 @@ public Optional search(String id) { } @Override - public List searchAll() { - return searchAllInElastic(BackofficeCourse::fromPrimitives); - } - - @Override - public List matching(Criteria criteria) { - return searchByCriteria(criteria, BackofficeCourse::fromPrimitives); - } - - @Override - protected String moduleName() { - return "courses"; - } + public List searchAll() { + return searchAllInElastic(BackofficeCourse::fromPrimitives); + } + + @Override + public List matching(Criteria criteria) { + return searchByCriteria(criteria, BackofficeCourse::fromPrimitives); + } + + @Override + protected String moduleName() { + return "courses"; + } } diff --git a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java index 1311278a..d1620f3f 100644 --- a/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java +++ b/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -70,8 +70,12 @@ public void consumer(Message message) throws Exception { try { subscriberOnMethod.invoke(subscriber, domainEvent); + + System.out.println("ACK: Consumed correctly!"); } catch (Exception error) { - handleConsumptionError(message, queue); + System.out.println("Error consuming"); + + handleConsumptionError(message, queue); } } From 4d337d63f83470d841d5a1db932cde2014e13add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 20:14:38 +0100 Subject: [PATCH 5/5] feat: add script to create infinite courses --- etc/scripts/create_infinite_courses.sh | 33 +++++++++++++++++++ .../increment/CoursesCounterIncrementer.java | 28 ++++++++-------- 2 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 etc/scripts/create_infinite_courses.sh diff --git a/etc/scripts/create_infinite_courses.sh b/etc/scripts/create_infinite_courses.sh new file mode 100644 index 00000000..2467ee9a --- /dev/null +++ b/etc/scripts/create_infinite_courses.sh @@ -0,0 +1,33 @@ +function create_course() { + eventId=$(uuidgen) + courseId=$(uuidgen) + courseName=$(shuf -n3 /usr/share/dict/words | xargs) + courseDuration=$((1 + RANDOM % 1000)) + + curl -X POST --location "http://localhost:8030/domain-events" \ + -H "Content-Type: application/json" \ + -d '{ + "eventName": "course.created", + "eventRaw": { + "data": { + "id": "'"$eventId"'", + "type": "course.created", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "'"$courseId"'", + "name": "'"$courseName"'", + "duration": "'"$courseDuration"' days" + } + }, + "meta": { + } + } + }' + + echo "Created: $courseName" +} + +while true; do + create_course & + sleep 0.001 +done diff --git a/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java b/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java index c4433c07..2f80400d 100644 --- a/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java +++ b/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java @@ -8,22 +8,22 @@ @Service public final class CoursesCounterIncrementer { - private CoursesCounterRepository repository; - private UuidGenerator uuidGenerator; + private CoursesCounterRepository repository; + private UuidGenerator uuidGenerator; - public CoursesCounterIncrementer(CoursesCounterRepository repository, UuidGenerator uuidGenerator) { - this.repository = repository; - this.uuidGenerator = uuidGenerator; - } + public CoursesCounterIncrementer(CoursesCounterRepository repository, UuidGenerator uuidGenerator) { + this.repository = repository; + this.uuidGenerator = uuidGenerator; + } - public void increment(CourseId id) { - CoursesCounter counter = repository.search() - .orElseGet(() -> CoursesCounter.initialize(uuidGenerator.generate())); + public void increment(CourseId id) { + CoursesCounter counter = repository.search() + .orElseGet(() -> CoursesCounter.initialize(uuidGenerator.generate())); - if (!counter.hasIncremented(id)) { - counter.increment(id); + if (!counter.hasIncremented(id)) { + counter.increment(id); - repository.save(counter); - } - } + repository.save(counter); + } + } }