FINERACT-2452: New command processing - audit
Co-Authored-By: Adam Saghy <adam.saghy@dpc.hu> Co-Authored-By: Yu Wati Nyi <nyi22y@mtholyoke.edu>
This commit is contained in:
committed by
Aleksandar Vidakovic
co-authored by
Adam Saghy
Yu Wati Nyi
parent
1d1876edfd
commit
8d064ddf6c
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.persistence.domain.CommandRepository;
|
||||
import org.apache.fineract.command.persistence.mapping.CommandMapper;
|
||||
@@ -27,9 +26,12 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
import org.testcontainers.containers.MySQLContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@@ -40,9 +42,16 @@ abstract class CommandBaseTest {
|
||||
protected static Network network = Network.newNetwork();
|
||||
|
||||
@Container
|
||||
protected static final GenericContainer POSTGRES_CONTAINER = new GenericContainer("postgres:16").withNetwork(network)
|
||||
.withNetworkAliases("postgres").withExposedPorts(5432)
|
||||
.withEnv(Map.of("POSTGRES_DB", "fineract-test", "POSTGRES_USER", "root", "POSTGRES_PASSWORD", "mifos"));
|
||||
private static final PostgreSQLContainer<?> POSTGRES_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16"))
|
||||
.withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
|
||||
|
||||
@Container
|
||||
private static final MariaDBContainer<?> MARIADB_CONTAINER = new MariaDBContainer<>(DockerImageName.parse("mariadb:11.4"))
|
||||
.withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
|
||||
|
||||
@Container
|
||||
private static final MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8")).withNetwork(network)
|
||||
.withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
|
||||
|
||||
@Autowired
|
||||
protected CommandRepository commandRepository;
|
||||
@@ -52,13 +61,43 @@ abstract class CommandBaseTest {
|
||||
|
||||
@DynamicPropertySource
|
||||
protected static void configure(DynamicPropertyRegistry registry) {
|
||||
postgres(registry);
|
||||
// mariadb(registry);
|
||||
// mysql(registry);
|
||||
}
|
||||
|
||||
private static void postgres(DynamicPropertyRegistry registry) {
|
||||
POSTGRES_CONTAINER.start();
|
||||
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
registry.add("spring.datasource.username", () -> "root");
|
||||
registry.add("spring.datasource.password", () -> "mifos");
|
||||
registry.add("spring.datasource.url", () -> "jdbc:postgresql://" + POSTGRES_CONTAINER.getHost() + ":"
|
||||
+ POSTGRES_CONTAINER.getMappedPort(5432) + "/fineract-test");
|
||||
registry.add("spring.datasource.driver-class-name", POSTGRES_CONTAINER::getDriverClassName);
|
||||
registry.add("spring.datasource.username", POSTGRES_CONTAINER::getUsername);
|
||||
registry.add("spring.datasource.password", POSTGRES_CONTAINER::getPassword);
|
||||
registry.add("spring.datasource.url", POSTGRES_CONTAINER::getJdbcUrl);
|
||||
registry.add("spring.datasource.platform", () -> "postgresql");
|
||||
registry.add("spring.liquibase.contexts", () -> "postgresql");
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private static void mariadb(DynamicPropertyRegistry registry) {
|
||||
MARIADB_CONTAINER.start();
|
||||
|
||||
registry.add("spring.datasource.driver-class-name", MARIADB_CONTAINER::getDriverClassName);
|
||||
registry.add("spring.datasource.username", MARIADB_CONTAINER::getUsername);
|
||||
registry.add("spring.datasource.password", MARIADB_CONTAINER::getPassword);
|
||||
registry.add("spring.datasource.url", MARIADB_CONTAINER::getJdbcUrl);
|
||||
registry.add("spring.datasource.platform", () -> "mysql");
|
||||
registry.add("spring.liquibase.contexts", () -> "mysql");
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private static void mysql(DynamicPropertyRegistry registry) {
|
||||
MYSQL_CONTAINER.start();
|
||||
|
||||
registry.add("spring.datasource.driver-class-name", MYSQL_CONTAINER::getDriverClassName);
|
||||
registry.add("spring.datasource.username", MYSQL_CONTAINER::getUsername);
|
||||
registry.add("spring.datasource.password", MYSQL_CONTAINER::getPassword);
|
||||
registry.add("spring.datasource.url", MYSQL_CONTAINER::getJdbcUrl);
|
||||
registry.add("spring.datasource.platform", () -> "mysql");
|
||||
registry.add("spring.liquibase.contexts", () -> "mysql");
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -18,10 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import java.util.UUID;
|
||||
import static org.apache.fineract.command.persistence.domain.CommandEntityState.ERROR;
|
||||
|
||||
import java.time.Instant;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
@@ -35,10 +38,25 @@ class CommandPersistenceTest extends CommandBaseTest {
|
||||
void save() {
|
||||
var content = "hello";
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
var now = Instant.now();
|
||||
|
||||
var commandEntity = commandMapper.map(command);
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
command.setIdempotencyKey("1234567890");
|
||||
command.setIpAddress("127.0.0.1");
|
||||
command.setCreatedAt(now);
|
||||
command.setExecutedAt(now);
|
||||
command.setApprovedAt(now);
|
||||
command.setRejectedAt(now);
|
||||
command.setInitiatedByUsername("abc");
|
||||
command.setExecutedByUsername("abc");
|
||||
command.setApprovedByUsername("abc");
|
||||
command.setRejectedByUsername("abc");
|
||||
|
||||
var response = DummyResponse.builder().tenantId("dummy").content(content).build();
|
||||
|
||||
var commandEntity = commandMapper.map(command, response);
|
||||
commandEntity.setState(ERROR);
|
||||
commandEntity.setError("Some error message");
|
||||
|
||||
var result = commandRepository.save(commandEntity);
|
||||
|
||||
|
||||
+26
-12
@@ -18,27 +18,31 @@
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lmax.disruptor.YieldingWaitStrategy;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import com.lmax.disruptor.dsl.ProducerType;
|
||||
import com.lmax.disruptor.util.DaemonThreadFactory;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.command.core.CommandRouter;
|
||||
import org.apache.fineract.command.core.CommandProperties;
|
||||
import org.apache.fineract.command.implementation.DefaultCommandAuditor;
|
||||
import org.apache.fineract.command.implementation.DefaultCommandPipeline;
|
||||
import org.apache.fineract.command.implementation.DefaultCommandRouter;
|
||||
import org.apache.fineract.command.implementation.DisruptorCommandExecutor;
|
||||
import org.apache.fineract.command.persistence.domain.CommandRepository;
|
||||
import org.apache.fineract.command.persistence.mapping.CommandJsonMapper;
|
||||
import org.apache.fineract.command.persistence.mapping.CommandMapperImpl;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.handler.DummyCommandHandler;
|
||||
import org.apache.fineract.command.sample.middleware.DummyIdempotencyMiddleware;
|
||||
import org.apache.fineract.command.sample.middleware.DummyMiddleware;
|
||||
import org.apache.fineract.command.sample.service.DefaultDummyService;
|
||||
import org.apache.fineract.command.sample.service.DefaultDummyTenantService;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
@@ -58,26 +62,37 @@ import org.openjdk.jmh.annotations.TearDown;
|
||||
@SuppressWarnings({ "raw" })
|
||||
public class CommandPipelineBenchmark {
|
||||
|
||||
private CommandRouter router;
|
||||
private Disruptor<DisruptorCommandExecutor.CommandEvent> disruptor;
|
||||
|
||||
private CommandPipeline pipeline;
|
||||
|
||||
@Setup(Level.Iteration)
|
||||
public void setUp() {
|
||||
this.router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService(new DefaultDummyTenantService()))));
|
||||
// create router
|
||||
var router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService(new DefaultDummyTenantService()))));
|
||||
|
||||
// Create the disruptor
|
||||
// create mapper
|
||||
var mapper = new CommandMapperImpl(new CommandJsonMapper(new ObjectMapper()));
|
||||
|
||||
// mock repository
|
||||
var repository = mock(CommandRepository.class);
|
||||
|
||||
// default properties
|
||||
var properties = new CommandProperties();
|
||||
|
||||
// create auditor
|
||||
var auditor = new DefaultCommandAuditor(mapper, repository, properties, new ObjectMapper());
|
||||
|
||||
// create the disruptor
|
||||
this.disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new, 2048, DaemonThreadFactory.INSTANCE, ProducerType.MULTI,
|
||||
new YieldingWaitStrategy());
|
||||
|
||||
disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(
|
||||
List.of(new DummyMiddleware(), new DummyIdempotencyMiddleware()), router));
|
||||
disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(router));
|
||||
|
||||
// Start the disruptor
|
||||
// start the disruptor
|
||||
disruptor.start();
|
||||
|
||||
pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor));
|
||||
pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor, router, auditor), properties);
|
||||
}
|
||||
|
||||
@TearDown(Level.Iteration)
|
||||
@@ -89,7 +104,6 @@ public class CommandPipelineBenchmark {
|
||||
@Benchmark
|
||||
public void processCommand() {
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content("hello").build());
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
+43
-12
@@ -21,14 +21,20 @@ package org.apache.fineract.command;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandPipeline;
|
||||
import org.apache.fineract.command.persistence.domain.CommandRepository;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.sample.command.DummyErrorCommand;
|
||||
import org.apache.fineract.command.sample.data.DummyErrorRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyErrorResponse;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.exception.DummyException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -42,29 +48,54 @@ class CommandPipelineTest extends CommandBaseTest {
|
||||
@Autowired
|
||||
private CommandPipeline pipeline;
|
||||
|
||||
@Autowired
|
||||
private CommandRepository repository;
|
||||
|
||||
@Test
|
||||
void processCommand() {
|
||||
void process() {
|
||||
var content = "hello";
|
||||
var command = new DummyCommand();
|
||||
command.setId(UUID.randomUUID());
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
|
||||
var result = pipeline.send(command);
|
||||
command.setPayload(DummyRequest.builder().content(content).build());
|
||||
command.setIdempotencyKey("1234567890");
|
||||
command.setIpAddress("127.0.0.1");
|
||||
command.setInitiatedByUsername("abc");
|
||||
|
||||
Supplier<DummyResponse> result = pipeline.send(command);
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
|
||||
Object response = result.get();
|
||||
var response = result.get();
|
||||
|
||||
assertNotNull(response, "Response should not be null.");
|
||||
|
||||
assertInstanceOf(DummyResponse.class, response, "Response is of wrong type.");
|
||||
|
||||
if (response instanceof DummyResponse dummyResponse) {
|
||||
log.warn("Result: {}", dummyResponse);
|
||||
log.info("Result: {}", response);
|
||||
log.info("Command ID: {}", command.getCommandId());
|
||||
|
||||
assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
|
||||
assertNotNull(dummyResponse.getRequestId(), "Request ID should not be null.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
|
||||
}
|
||||
assertNotNull(command.getCommandId(), "Command should have an ID after processing.");
|
||||
assertNotNull(response.getContent(), "Response body should not be null.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), response.getContent(), "Wrong response content.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void error() {
|
||||
var content = "hello error";
|
||||
var command = new DummyErrorCommand();
|
||||
|
||||
command.setPayload(DummyErrorRequest.builder().content(content).build());
|
||||
|
||||
Supplier<DummyErrorResponse> result = pipeline.send(command);
|
||||
|
||||
var exception = assertThrows(DummyException.class, () -> {
|
||||
var response = result.get();
|
||||
});
|
||||
|
||||
assertNotNull(exception, "Expected exception!");
|
||||
|
||||
var commands = repository.findAll();
|
||||
|
||||
log.info("All commands: {}", commands);
|
||||
}
|
||||
}
|
||||
|
||||
+68
-15
@@ -18,7 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.command;
|
||||
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_REQUEST_ID;
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_TENANT_ID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -35,14 +36,19 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandAuditor;
|
||||
import org.apache.fineract.command.core.CommandProperties;
|
||||
import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
@@ -63,13 +69,19 @@ class CommandSampleApiTest extends CommandBaseTest {
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private CommandAuditor auditor;
|
||||
|
||||
@Autowired
|
||||
private CommandProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.baseUrl = "http://localhost:" + port + "/test/dummy";
|
||||
this.interceptors = Collections.singletonList((request, body, execution) -> {
|
||||
this.interceptors = List.of((request, body, execution) -> {
|
||||
var headers = request.getHeaders();
|
||||
headers.add(COMMAND_REQUEST_ID, UUID.randomUUID().toString());
|
||||
headers.add("x-fineract-tenant-id", "dummy");
|
||||
headers.add(COMMAND_HTTP_HEADER_REQUEST_ID, UUID.randomUUID().toString());
|
||||
headers.add(COMMAND_HTTP_HEADER_TENANT_ID, "dummy");
|
||||
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
|
||||
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
|
||||
return execution.execute(request, body);
|
||||
@@ -88,45 +100,86 @@ class CommandSampleApiTest extends CommandBaseTest {
|
||||
|
||||
@Test
|
||||
void dummyApiSync() {
|
||||
var content = "test-sync";
|
||||
final var content = "test-sync";
|
||||
final var idempotencyKey = UUID.randomUUID().toString();
|
||||
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
var result = restTemplate.postForObject(baseUrl + "/sync", DummyRequest.builder().content(content).build(), DummyResponse.class);
|
||||
|
||||
log.warn("Result (sync) : {} ({})", result.getContent(), result.getRequestId());
|
||||
final var headers = new HttpHeaders();
|
||||
headers.add(properties.getIdemPotencyKeyHeaderName(), idempotencyKey);
|
||||
|
||||
final var request = new HttpEntity<>(DummyRequest.builder().content(content).build(), headers);
|
||||
|
||||
final var result = restTemplate.postForObject(baseUrl + "/sync", request, DummyResponse.class);
|
||||
|
||||
log.warn("Result (sync) : {}", result.getContent());
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
assertNotNull(result.getContent(), "Response body should not be null.");
|
||||
assertNotNull(result.getRequestId(), "Request ID should not be null.");
|
||||
assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
|
||||
assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
|
||||
|
||||
var response = auditor.getResponseByIdempotencyKey(idempotencyKey);
|
||||
|
||||
if (response instanceof DummyResponse dummyResponse) {
|
||||
assertNotNull(dummyResponse, "There should be a command entity stored with a dummyResponse.");
|
||||
assertNotNull(dummyResponse, "Response should not be null.");
|
||||
assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
|
||||
assertNotNull(dummyResponse.getTenantId(), "Tenant ID should not be null.");
|
||||
assertEquals("dummy", dummyResponse.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
|
||||
}
|
||||
|
||||
log.warn("Command response: {}", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dummyApiAsync() {
|
||||
var content = "test-async";
|
||||
final var content = "test-async";
|
||||
final var idempotencyKey = UUID.randomUUID().toString();
|
||||
|
||||
restTemplate.getRestTemplate().setInterceptors(interceptors);
|
||||
var result = restTemplate.postForObject(baseUrl + "/async", DummyRequest.builder().content(content).build(), DummyResponse.class);
|
||||
|
||||
log.warn("Result (async): {} ({})", result.getContent(), result.getRequestId());
|
||||
final var headers = new HttpHeaders();
|
||||
headers.add(properties.getIdemPotencyKeyHeaderName(), idempotencyKey);
|
||||
|
||||
final var request = new HttpEntity<>(DummyRequest.builder().content(content).build(), headers);
|
||||
|
||||
final var result = restTemplate.postForObject(baseUrl + "/async", request, DummyResponse.class);
|
||||
|
||||
log.warn("Result (async): {}", result.getContent());
|
||||
|
||||
assertNotNull(result, "Response should not be null.");
|
||||
assertNotNull(result.getContent(), "Response body should not be null.");
|
||||
assertNotNull(result.getRequestId(), "Request ID should not be null.");
|
||||
// TODO: make sure all ThreadLocal variables are available
|
||||
// assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
|
||||
// assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
|
||||
|
||||
var response = auditor.getResponseByIdempotencyKey(idempotencyKey);
|
||||
|
||||
// TODO: always null; need to fix this before we release async mode
|
||||
if (response instanceof DummyResponse dummyResponse) {
|
||||
assertNotNull(dummyResponse, "There should be a command entity stored with a dummyResponse.");
|
||||
assertNotNull(dummyResponse, "Response should not be null.");
|
||||
assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
|
||||
assertNotNull(dummyResponse.getTenantId(), "Tenant ID should not be null.");
|
||||
assertEquals("dummy", dummyResponse.getTenantId(), "Unexpected tenant ID.");
|
||||
assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
|
||||
}
|
||||
|
||||
log.warn("Command response: {}", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO: implement idempotency properly with backwards compatibility
|
||||
void dummyApiIdempotencyAsync() {
|
||||
dummyApiIdempotency("/async");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO: implement idempotency properly with backwards compatibility
|
||||
void dummyApiIdempotencySync() {
|
||||
dummyApiIdempotency("/sync");
|
||||
}
|
||||
@@ -137,8 +190,8 @@ class CommandSampleApiTest extends CommandBaseTest {
|
||||
var request = DummyRequest.builder().content(content).build();
|
||||
List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList((req, body, execution) -> {
|
||||
var headers = req.getHeaders();
|
||||
headers.add(COMMAND_REQUEST_ID, id);
|
||||
headers.add("x-fineract-tenant-id", "dummy");
|
||||
headers.add(COMMAND_HTTP_HEADER_REQUEST_ID, id);
|
||||
headers.add(COMMAND_HTTP_HEADER_TENANT_ID, "dummy");
|
||||
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
|
||||
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
|
||||
return execution.execute(req, body);
|
||||
@@ -154,7 +207,7 @@ class CommandSampleApiTest extends CommandBaseTest {
|
||||
// second request fails, because we are using the same request ID in both cases
|
||||
response = restTemplate.postForEntity(baseUrl + path, request, Map.class);
|
||||
|
||||
log.warn("Body: {} - {}", response.getBody(), response.getStatusCode());
|
||||
log.info("Body: {} - {}", response.getBody(), response.getStatusCode());
|
||||
|
||||
// Assert HTTP status
|
||||
assertThat(response.getStatusCode()).isEqualTo(INTERNAL_SERVER_ERROR);
|
||||
|
||||
@@ -21,19 +21,16 @@ package org.apache.fineract.command;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.CommandProperties;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({ CommandProperties.class, JpaProperties.class })
|
||||
@EnableConfigurationProperties({ CommandProperties.class })
|
||||
@EnableAutoConfiguration
|
||||
@EnableJpaRepositories(basePackages = { "org.apache.fineract.command.persistence" })
|
||||
@EnableAsync
|
||||
@PropertySource("classpath:application-test.properties")
|
||||
@ComponentScan("org.apache.fineract.command.sample")
|
||||
|
||||
+5
-12
@@ -18,13 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.command.sample.api;
|
||||
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
|
||||
import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_TENANT_ID;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -47,19 +44,16 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping(value = "/test/dummy", consumes = APPLICATION_JSON_VALUE, produces = { APPLICATION_JSON_VALUE,
|
||||
APPLICATION_PROBLEM_JSON_VALUE })
|
||||
class DummyApiController {
|
||||
// "application/vnd.fineract+json;charset=UTF-8;version=1.0"
|
||||
|
||||
private final DefaultDummyTenantService tenantService;
|
||||
|
||||
private final CommandPipeline pipeline;
|
||||
|
||||
@PostMapping("/sync")
|
||||
DummyResponse dummySync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
|
||||
@RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @RequestBody DummyRequest request) {
|
||||
DummyResponse dummySync(@RequestHeader(value = COMMAND_HTTP_HEADER_TENANT_ID, required = false) String tenantId,
|
||||
@RequestBody DummyRequest request) {
|
||||
var command = new DummyCommand();
|
||||
command.setId(requestId);
|
||||
command.setPayload(request);
|
||||
command.setCreatedAt(OffsetDateTime.now(ZoneId.of("UTC")));
|
||||
|
||||
tenantService.set(tenantId);
|
||||
|
||||
@@ -70,10 +64,9 @@ class DummyApiController {
|
||||
|
||||
@Async
|
||||
@PostMapping("/async")
|
||||
CompletableFuture<DummyResponse> dummyAsync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
|
||||
@RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @RequestBody DummyRequest request) {
|
||||
CompletableFuture<DummyResponse> dummyAsync(@RequestHeader(value = COMMAND_HTTP_HEADER_TENANT_ID, required = false) String tenantId,
|
||||
@RequestBody DummyRequest request) {
|
||||
var command = new DummyCommand();
|
||||
command.setId(requestId);
|
||||
command.setPayload(request);
|
||||
|
||||
tenantService.set(tenantId);
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.command;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.sample.data.DummyErrorRequest;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DummyErrorCommand extends Command<DummyErrorRequest> {}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyErrorRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "{org.apache.fineract.dummy.request.content.not-empty}")
|
||||
private String content;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class DummyErrorResponse implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String content;
|
||||
private String error;
|
||||
}
|
||||
-3
@@ -21,7 +21,6 @@ package org.apache.fineract.command.sample.data;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -38,8 +37,6 @@ public class DummyRequest implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "{org.apache.fineract.dummy.request.content.not-empty}")
|
||||
private String content;
|
||||
}
|
||||
|
||||
-5
@@ -20,7 +20,6 @@ package org.apache.fineract.command.sample.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -37,11 +36,7 @@ public class DummyResponse implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private UUID requestId;
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String content;
|
||||
|
||||
private String error;
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.exception;
|
||||
|
||||
public class DummyException extends RuntimeException {
|
||||
|
||||
public DummyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+10
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.command.sample.handler;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
@@ -26,6 +27,7 @@ import org.apache.fineract.command.sample.data.DummyRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyResponse;
|
||||
import org.apache.fineract.command.sample.service.DummyService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -34,8 +36,16 @@ public class DummyCommandHandler implements CommandHandler<DummyRequest, DummyRe
|
||||
|
||||
private final DummyService dummyService;
|
||||
|
||||
@Retry(name = "commandDummy", fallbackMethod = "fallback")
|
||||
@Transactional
|
||||
@Override
|
||||
public DummyResponse handle(Command<DummyRequest> command) {
|
||||
return dummyService.process(command.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DummyResponse fallback(Command<DummyRequest> command, Throwable t) {
|
||||
// NOTE: fallback method needs to be in the same class
|
||||
return CommandHandler.super.fallback(command, t);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -16,24 +16,24 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.middleware;
|
||||
package org.apache.fineract.command.sample.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.apache.fineract.command.core.CommandHandler;
|
||||
import org.apache.fineract.command.sample.data.DummyErrorRequest;
|
||||
import org.apache.fineract.command.sample.data.DummyErrorResponse;
|
||||
import org.apache.fineract.command.sample.exception.DummyException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DummyMiddleware implements CommandMiddleware {
|
||||
public class DummyErrorCommandHandler implements CommandHandler<DummyErrorRequest, DummyErrorResponse> {
|
||||
|
||||
@Override
|
||||
public void invoke(Command<?> command) {
|
||||
if (command instanceof DummyCommand c) {
|
||||
c.getPayload().setId(command.getId());
|
||||
}
|
||||
public DummyErrorResponse handle(Command<DummyErrorRequest> command) {
|
||||
throw new DummyException("Simulating a business logic error: " + command.getPayload().getContent());
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.command.sample.middleware;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.command.core.Command;
|
||||
import org.apache.fineract.command.core.CommandMiddleware;
|
||||
import org.apache.fineract.command.core.exception.CommandIllegalArgumentException;
|
||||
import org.apache.fineract.command.sample.command.DummyCommand;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DummyIdempotencyMiddleware implements CommandMiddleware {
|
||||
|
||||
// NOTE: in production you would use of course a database or Redis
|
||||
private static final List<UUID> IDS = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void invoke(Command<?> command) {
|
||||
if (command instanceof DummyCommand c) {
|
||||
if (IDS.contains(c.getId())) {
|
||||
throw new CommandIllegalArgumentException(c, "Duplicate request ID: " + c.getId());
|
||||
}
|
||||
|
||||
IDS.add(c.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -34,7 +34,6 @@ public class DefaultDummyService implements DummyService {
|
||||
|
||||
@Override
|
||||
public DummyResponse process(DummyRequest request) {
|
||||
return DummyResponse.builder().requestId(request.getId()).tenantId(tenantService.get())
|
||||
.content(request.getContent().toUpperCase(Locale.ROOT)).build();
|
||||
return DummyResponse.builder().tenantId(tenantService.get()).content(request.getContent().toUpperCase(Locale.ROOT)).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ fineract.command.enabled=true
|
||||
fineract.command.executor=sync
|
||||
fineract.command.ring-buffer-size=1024
|
||||
fineract.command.producer-type=single
|
||||
fineract.command.auditable=true
|
||||
|
||||
spring.liquibase.enabled=true
|
||||
spring.liquibase.drop-first=true
|
||||
spring.liquibase.default-schema=public
|
||||
spring.liquibase.contexts=postgresql
|
||||
spring.liquibase.change-log=classpath:/db/custom-changelog/0001_command_init.xml
|
||||
spring.liquibase.change-log=classpath:/db/changelog/tenant/module/command/module-changelog-master.xml
|
||||
|
||||
Reference in New Issue
Block a user