From bd8bdbf7d18f83f49ce883087661505262bcabaf Mon Sep 17 00:00:00 2001 From: Aleksandar Vidakovic Date: Thu, 16 Jan 2025 21:44:20 +0100 Subject: [PATCH] FINERACT-2169: New command processing infrastructure --- build.gradle | 2 + fineract-command/build.gradle | 82 ++++++++++ fineract-command/dependencies.gradle | 60 ++++++++ .../apache/fineract/command/core/Command.java | 40 +++++ .../command/core/CommandConstants.java | 27 ++++ .../command/core/CommandExecutor.java | 26 ++++ .../fineract/command/core/CommandHandler.java | 32 ++++ .../command/core/CommandMiddleware.java | 25 +++ .../command/core/CommandPipeline.java | 26 ++++ .../command/core/CommandProperties.java | 53 +++++++ .../fineract/command/core/CommandRouter.java | 25 +++ .../CommandHandlerNotFoundException.java | 28 ++++ .../CommandIllegalArgumentException.java | 28 ++++ .../AsynchronousCommandExecutor.java | 56 +++++++ .../DefaultCommandPipeline.java | 45 ++++++ .../implementation/DefaultCommandRouter.java | 50 ++++++ .../DisruptorCommandExecutor.java | 104 +++++++++++++ .../SynchronousCommandExecutor.java | 50 ++++++ .../starter/CommandAutoConfiguration.java | 26 ++++ .../command/starter/CommandConfiguration.java | 58 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../command/CommandPipelineBenchmark.java | 88 +++++++++++ .../fineract/command/CommandPipelineTest.java | 69 +++++++++ .../command/CommandSampleWebTest.java | 142 ++++++++++++++++++ .../fineract/command/TestConfiguration.java | 37 +++++ .../command/sample/command/DummyCommand.java | 28 ++++ .../sample/handler/DummyCommandHandler.java | 41 +++++ .../DummyIdempotencyMiddleware.java | 50 ++++++ .../sample/middleware/DummyMiddleware.java | 39 +++++ .../command/sample/model/DummyRequest.java | 43 ++++++ .../command/sample/model/DummyResponse.java | 45 ++++++ .../sample/service/DefaultDummyService.java | 37 +++++ .../command/sample/service/DummyService.java | 27 ++++ .../sample/web/DummyApiController.java | 70 +++++++++ .../sample/web/DummyExceptionHandler.java | 19 +++ .../resources/application-test.properties | 26 ++++ settings.gradle | 1 + 37 files changed, 1606 insertions(+) create mode 100644 fineract-command/build.gradle create mode 100644 fineract-command/dependencies.gradle create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/Command.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandExecutor.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandPipeline.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/CommandRouter.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandHandlerNotFoundException.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandIllegalArgumentException.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandRouter.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/starter/CommandAutoConfiguration.java create mode 100644 fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java create mode 100644 fineract-command/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/CommandSampleWebTest.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyCommand.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyRequest.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyResponse.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/service/DummyService.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyApiController.java create mode 100644 fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyExceptionHandler.java create mode 100644 fineract-command/src/test/resources/application-test.properties diff --git a/build.gradle b/build.gradle index ad23be81196..5d6338e88e4 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ buildscript { [ 'fineract-api', 'fineract-core', + 'fineract-command', 'fineract-accounting', 'fineract-provider', 'fineract-branch', @@ -112,6 +113,7 @@ plugins { id "com.github.davidmc24.gradle.plugin.avro-base" version "1.9.1" apply false id 'org.openapi.generator' version '7.8.0' apply false id 'com.gradleup.shadow' version '8.3.5' apply false + id 'me.champeau.jmh' version '0.7.1' apply false } apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.release.gradle" diff --git a/fineract-command/build.gradle b/fineract-command/build.gradle new file mode 100644 index 00000000000..56cb98fe622 --- /dev/null +++ b/fineract-command/build.gradle @@ -0,0 +1,82 @@ +/** + * 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. + */ +description = 'Fineract Command' + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'me.champeau.jmh' + +configurations { + asciidoctorExtensions + providedRuntime // needed for Spring Boot executable WAR + providedCompile + compile() { + exclude module: 'hibernate-entitymanager' + exclude module: 'hibernate-validator' + exclude module: 'activation' + exclude module: 'bcmail-jdk14' + exclude module: 'bcprov-jdk14' + exclude module: 'bctsp-jdk14' + exclude module: 'c3p0' + exclude module: 'stax-api' + exclude module: 'jaxb-api' + exclude module: 'jaxb-impl' + exclude module: 'jboss-logging' + exclude module: 'itext-rtf' + exclude module: 'classworlds' + } + runtime +} + +apply from: 'dependencies.gradle' + +// Configuration for the modernizer plugin +// https://github.com/andygoossens/gradle-modernizer-plugin +modernizer { + ignoreClassNamePatterns = [ + '.*AbstractPersistableCustom', + '.*EntityTables', + '.*domain.*' + ] +} + +// If we are running Gradle within Eclipse to enhance classes with OpenJPA, +// set the classes directory to point to Eclipse's default build directory +if (project.hasProperty('env') && project.getProperty('env') == 'eclipse') { + sourceSets.main.java.outputDir = new File(rootProject.projectDir, "fineract-command/bin/main") +} + +if (!(project.hasProperty('env') && project.getProperty('env') == 'dev')) { + sourceSets { + test { + java { + exclude '**/core/boot/tests/**' + } + } + } +} + +jmh { + // include = ['.*'] // Include all benchmarks (regex-based filter) + warmupIterations = 3 // Number of warm-up iterations + iterations = 5 // Number of measurement iterations + fork = 2 // Number of forks + timeOnIteration = '1s' // Time per iteration + benchmarkMode = ['thrpt'] // Default benchmark mode +} \ No newline at end of file diff --git a/fineract-command/dependencies.gradle b/fineract-command/dependencies.gradle new file mode 100644 index 00000000000..c49aff79242 --- /dev/null +++ b/fineract-command/dependencies.gradle @@ -0,0 +1,60 @@ +/** + * 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. + */ + +dependencies { + implementation( + 'org.springframework.boot:spring-boot-starter', + 'org.springframework.boot:spring-boot-starter-validation', + 'io.github.resilience4j:resilience4j-spring-boot3', + + 'com.google.guava:guava', + + 'org.apache.commons:commons-lang3', + + 'com.github.spotbugs:spotbugs-annotations', + 'org.mapstruct:mapstruct', + 'com.lmax:disruptor:3.4.4', + ) + implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') { + exclude group: 'org.eclipse.persistence', module: 'jakarta.persistence' + } + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.mapstruct:mapstruct-processor' + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + + asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.0.3' + + testImplementation ('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'com.jayway.jsonpath', module: 'json-path' + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + exclude group: 'jakarta.activation' + exclude group: 'javax.activation' + exclude group: 'org.skyscreamer' + } + testImplementation ( + 'org.springframework.boot:spring-boot-starter-web', + 'org.mockito:mockito-inline', + 'org.openjdk.jmh:jmh-core:1.37', + 'org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3', + 'org.springframework.restdocs:spring-restdocs-webtestclient:3.0.3', + 'org.springframework.restdocs:spring-restdocs-restassured:3.0.3', + ) +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java b/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java new file mode 100644 index 00000000000..83db5a8a13e --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java @@ -0,0 +1,40 @@ +/** + * 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.core; + +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; +import lombok.Data; +import lombok.experimental.FieldNameConstants; + +@Data +@FieldNameConstants +public abstract class Command implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id; + private Instant createdAt; + private String tenantId; + private String username; + private T payload; +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java new file mode 100644 index 00000000000..110f86edbf6 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java @@ -0,0 +1,27 @@ +/** + * 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.core; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class CommandConstants { + + public static final String COMMAND_REQUEST_ID = "x-fineract-request-id"; +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandExecutor.java new file mode 100644 index 00000000000..de12e3ce3e3 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandExecutor.java @@ -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.core; + +import java.util.function.Supplier; + +public interface CommandExecutor { + + Supplier execute(Command command); +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java new file mode 100644 index 00000000000..d246b1ad7ff --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java @@ -0,0 +1,32 @@ +/** + * 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.core; + +import com.google.common.reflect.TypeToken; + +public interface CommandHandler { + + RES handle(Command command); + + default boolean matches(Command command) { + TypeToken handlerType = new TypeToken<>(getClass()) {}; + + return handlerType.getRawType().isAssignableFrom(command.getPayload().getClass()); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java new file mode 100644 index 00000000000..1ae3b18646f --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java @@ -0,0 +1,25 @@ +/** + * 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.core; + +@FunctionalInterface +public interface CommandMiddleware { + + void invoke(Command command); +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandPipeline.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandPipeline.java new file mode 100644 index 00000000000..e31b5672310 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandPipeline.java @@ -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.core; + +import java.util.function.Supplier; + +public interface CommandPipeline { + + Supplier send(Command command); +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java new file mode 100644 index 00000000000..d127a0fb0de --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java @@ -0,0 +1,53 @@ +/** + * 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.core; + +import java.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +@ConfigurationProperties(prefix = "fineract.command") +public final class CommandProperties implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Builder.Default + private Boolean enabled = true; + + @Builder.Default + private CommandExecutorType executor = CommandExecutorType.sync; + + @Builder.Default + private Integer ringBufferSize = 1024; + + public enum CommandExecutorType { + sync, async, disruptor + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandRouter.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandRouter.java new file mode 100644 index 00000000000..a6773d6cac3 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandRouter.java @@ -0,0 +1,25 @@ +/** + * 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.core; + +@FunctionalInterface +public interface CommandRouter { + + CommandHandler route(Command command); +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandHandlerNotFoundException.java b/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandHandlerNotFoundException.java new file mode 100644 index 00000000000..28c6bc36ab7 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandHandlerNotFoundException.java @@ -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.core.exception; + +import org.apache.fineract.command.core.Command; + +public class CommandHandlerNotFoundException extends RuntimeException { + + public CommandHandlerNotFoundException(Command command) { + super("Cannot find a matching handler for " + command.getClass().getSimpleName() + " command"); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandIllegalArgumentException.java b/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandIllegalArgumentException.java new file mode 100644 index 00000000000..9bced649153 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandIllegalArgumentException.java @@ -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.core.exception; + +import org.apache.fineract.command.core.Command; + +public class CommandIllegalArgumentException extends RuntimeException { + + public CommandIllegalArgumentException(Command command, String message) { + super("Illegal argument for " + command.getClass().getSimpleName() + " command: " + message); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java new file mode 100644 index 00000000000..f8ee739ad2f --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java @@ -0,0 +1,56 @@ +/** + * 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.implementation; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "async") +public class AsynchronousCommandExecutor implements CommandExecutor { + + private final List middlewares; + + private final CommandRouter router; + + @Override + public Supplier execute(Command command) { + CompletableFuture future = null; + + future = CompletableFuture.supplyAsync(() -> { + for (CommandMiddleware middleware : middlewares) { + middleware.invoke(command); + } + + CommandHandler handler = router.route(command); + + return handler.handle(command); + }); + + return future::join; + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java new file mode 100644 index 00000000000..6cd5196e4c3 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java @@ -0,0 +1,45 @@ +/** + * 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.implementation; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandExecutor; +import org.apache.fineract.command.core.CommandPipeline; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnBean(CommandPipeline.class) +public class DefaultCommandPipeline implements CommandPipeline { + + private final CommandExecutor executor; + + public Supplier send(final Command command) { + requireNonNull(command, "Command must not be null"); + + return executor.execute(command); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandRouter.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandRouter.java new file mode 100644 index 00000000000..fc8e17f89a9 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandRouter.java @@ -0,0 +1,50 @@ +/** + * 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.implementation; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.command.core.CommandRouter; +import org.apache.fineract.command.core.exception.CommandHandlerNotFoundException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnBean(CommandRouter.class) +@SuppressWarnings({ "unchecked", "raw" }) +public class DefaultCommandRouter implements CommandRouter { + + private final List commandHandlers; + + @Override + public CommandHandler route(Command command) { + // TODO: make sure there are no duplicate handlers + if (command == null) { + throw new CommandHandlerNotFoundException(command); + } + + return commandHandlers.stream().filter(handler -> handler.matches(command)).findFirst() + .orElseThrow(() -> new CommandHandlerNotFoundException(command)); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java new file mode 100644 index 00000000000..91ca1213b95 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java @@ -0,0 +1,104 @@ +/** + * 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.implementation; + +import com.lmax.disruptor.EventHandler; +import com.lmax.disruptor.dsl.Disruptor; +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandExecutor; +import org.apache.fineract.command.core.CommandMiddleware; +import org.apache.fineract.command.core.CommandRouter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "disruptor") +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class DisruptorCommandExecutor implements CommandExecutor, Closeable { + + private final Disruptor disruptor; + + @Override + public Supplier execute(Command command) { + CommandEvent processedEvent = next(command); + + return processedEvent.getFuture()::join; + } + + @Override + public void close() throws IOException { + disruptor.shutdown(); + } + + @SuppressWarnings({ "unchecked" }) + private CommandEvent next(Command command) { + var ringBuffer = disruptor.getRingBuffer(); + + var sequenceId = ringBuffer.next(); + + CommandEvent event = ringBuffer.get(sequenceId); + event.setCommand(command); + ringBuffer.publish(sequenceId); + + return event; + } + + @Getter + @Setter + public static class CommandEvent { + + private Command command; + private CompletableFuture future = new CompletableFuture<>(); + } + + @RequiredArgsConstructor + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static class CompleteableCommandEventHandler implements EventHandler { + + private final List middlewares; + + private final CommandRouter router; + + @Override + public void onEvent(CommandEvent event, long sequence, boolean endOfBatch) throws Exception { + try { + for (CommandMiddleware middleware : middlewares) { + middleware.invoke(event.getCommand()); + } + + var handler = router.route(event.getCommand()); + + event.getFuture().complete(handler.handle(event.getCommand())); + } catch (Exception e) { + event.getFuture().completeExceptionally(e); + } + } + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java new file mode 100644 index 00000000000..be68d823a5c --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java @@ -0,0 +1,50 @@ +/** + * 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.implementation; + +import java.util.List; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "sync") +@SuppressWarnings({ "unchecked" }) +public class SynchronousCommandExecutor implements CommandExecutor { + + private final List middlewares; + + private final CommandRouter router; + + @Override + public Supplier execute(Command command) { + for (CommandMiddleware middleware : middlewares) { + middleware.invoke(command); + } + + CommandHandler handler = router.route(command); + + return () -> handler.handle(command); + } +} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandAutoConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandAutoConfiguration.java new file mode 100644 index 00000000000..e4d5955ab53 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandAutoConfiguration.java @@ -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.starter; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +@AutoConfiguration +@Import({ CommandConfiguration.class }) +public class CommandAutoConfiguration {} diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java new file mode 100644 index 00000000000..2d2f4775e67 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java @@ -0,0 +1,58 @@ +/** + * 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.starter; + +import com.lmax.disruptor.IgnoreExceptionHandler; +import com.lmax.disruptor.dsl.Disruptor; +import com.lmax.disruptor.util.DaemonThreadFactory; +import java.util.List; +import org.apache.fineract.command.core.CommandMiddleware; +import org.apache.fineract.command.core.CommandProperties; +import org.apache.fineract.command.core.CommandRouter; +import org.apache.fineract.command.implementation.DisruptorCommandExecutor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(CommandProperties.class) +@ComponentScan("org.apache.fineract.command.core") +@ComponentScan("org.apache.fineract.command.implementation") +class CommandConfiguration { + + @Bean + Disruptor disruptor(List middlewares, CommandRouter router) { + // TODO: make this more configurable + + // Create the disruptor + Disruptor disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new, 1024, + DaemonThreadFactory.INSTANCE); + // this.disruptor = new Disruptor(CommandEventFactory.class, 1024, DaemonThreadFactory.INSTANCE, + // ProducerType.SINGLE, new BlockingWaitStrategy()); + + disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(middlewares, router)); + disruptor.setDefaultExceptionHandler(new IgnoreExceptionHandler()); + + // Start the disruptor + disruptor.start(); + + return disruptor; + } +} diff --git a/fineract-command/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/fineract-command/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..f16efccfc31 --- /dev/null +++ b/fineract-command/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.apache.fineract.command.starter.CommandAutoConfiguration \ No newline at end of file diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java new file mode 100644 index 00000000000..f2026000bb0 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java @@ -0,0 +1,88 @@ +/** + * 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; + +import com.lmax.disruptor.dsl.Disruptor; +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.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.CommandPipeline; +import org.apache.fineract.command.core.CommandRouter; +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.sample.command.DummyCommand; +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.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.apache.fineract.command.sample.service.DefaultDummyService; +import org.openjdk.jmh.annotations.*; + +@Slf4j +@BenchmarkMode(Mode.Throughput) // Measures operations per second +@Warmup(iterations = 2) // JVM warm-up iterations +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // Measurement iterations +@State(Scope.Thread) // Benchmark state for each thread +@OutputTimeUnit(TimeUnit.MILLISECONDS) // Output results in milliseconds +@SuppressWarnings({ "raw" }) +public class CommandPipelineBenchmark { + + private CommandRouter router; + private Disruptor disruptor; + + private CommandPipeline pipeline; + + @Setup(Level.Iteration) + public void setUp() { + this.router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService()))); + + // Create the disruptor + this.disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new, 1024, DaemonThreadFactory.INSTANCE); + // this.disruptor = new Disruptor(CommandEventFactory.class, 1024, DaemonThreadFactory.INSTANCE, + // ProducerType.SINGLE, new BlockingWaitStrategy()); + + disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler( + List.of(new DummyMiddleware(), new DummyIdempotencyMiddleware()), router)); + + // Start the disruptor + disruptor.start(); + + pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor)); + } + + @TearDown(Level.Iteration) + public void tearDown() {} + + @Benchmark + public void processCommand() { + var command = new DummyCommand(); + command.setId(UUID.randomUUID()); + command.setPayload(DummyRequest.builder().content("hello").build()); + + Supplier result = pipeline.send(command); + + // NOTE: force yield + result.get(); + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java new file mode 100644 index 00000000000..c238b773ba0 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java @@ -0,0 +1,69 @@ +/** + * 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; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Locale; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.CommandPipeline; +import org.apache.fineract.command.sample.command.DummyCommand; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@Slf4j +@ExtendWith({ SpringExtension.class }) +@ContextConfiguration(classes = TestConfiguration.class) +class CommandPipelineTest { + + @Autowired + private CommandPipeline pipeline; + + @Test + void processCommand() { + var content = "hello"; + var command = new DummyCommand(); + command.setId(UUID.randomUUID()); + command.setPayload(DummyRequest.builder().content(content).build()); + + var result = pipeline.send(command); + + assertNotNull(result, "Response should not be null."); + + Object 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); + + 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."); + } + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleWebTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleWebTest.java new file mode 100644 index 00000000000..dd663c350a9 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleWebTest.java @@ -0,0 +1,142 @@ +/** + * 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; + +import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_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; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE; + +import java.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.junit.jupiter.api.BeforeEach; +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.client.ClientHttpRequestInterceptor; +import org.springframework.test.context.ContextConfiguration; + +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = TestConfiguration.class) +@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") +class CommandSampleWebTest { + + @LocalServerPort + private int port; + + private String baseUrl; + + private List interceptors; + + @Autowired + private TestRestTemplate restTemplate; + + @BeforeEach + public void setUp() { + this.baseUrl = "http://localhost:" + port + "/test/dummy"; + this.interceptors = Collections.singletonList((request, body, execution) -> { + var headers = request.getHeaders(); + headers.add(COMMAND_REQUEST_ID, UUID.randomUUID().toString()); + headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE); + headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE)); + return execution.execute(request, body); + }); + } + + @Test + void dummyApiSync() { + var content = "test-sync"; + + 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()); + + 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."); + assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content."); + } + + @Test + void dummyApiAsync() { + var content = "test-async"; + + 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()); + + 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."); + assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content."); + } + + @Test + void dummyApiIdempotencyAsync() { + dummyApiIdempotency("/async"); + } + + @Test + void dummyApiIdempotencySync() { + dummyApiIdempotency("/sync"); + } + + void dummyApiIdempotency(String path) { + var ID = UUID.randomUUID().toString(); + var content = "test-idempotency"; + var request = DummyRequest.builder().content(content).build(); + List interceptors = Collections.singletonList((req, body, execution) -> { + var headers = req.getHeaders(); + headers.add(COMMAND_REQUEST_ID, ID); + headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE); + headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE)); + return execution.execute(req, body); + }); + + restTemplate.getRestTemplate().setInterceptors(interceptors); + + // first request passes + var response = restTemplate.postForEntity(baseUrl + path, request, Map.class); + + assertThat(response.getStatusCode()).isEqualTo(OK); + + // 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()); + + // Assert HTTP status + assertThat(response.getStatusCode()).isEqualTo(INTERNAL_SERVER_ERROR); + + log.info("Idempotency all good!"); + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java b/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java new file mode 100644 index 00000000000..1c693b3cc61 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java @@ -0,0 +1,37 @@ +/** + * 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; + +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.CommandProperties; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.scheduling.annotation.EnableAsync; + +@Slf4j +@Configuration +@EnableConfigurationProperties(CommandProperties.class) +@EnableAutoConfiguration +@EnableAsync +@PropertySource("classpath:application-test.properties") +@ComponentScan("org.apache.fineract.command") +public class TestConfiguration {} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyCommand.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyCommand.java new file mode 100644 index 00000000000..25df0bc89ad --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyCommand.java @@ -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.model.DummyRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class DummyCommand extends Command {} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java new file mode 100644 index 00000000000..6437a16ca45 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java @@ -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.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.apache.fineract.command.sample.service.DummyService; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DummyCommandHandler implements CommandHandler { + + private final DummyService dummyService; + + @Override + public DummyResponse handle(Command command) { + return dummyService.process(command.getPayload()); + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java new file mode 100644 index 00000000000..4f9a52e5aaa --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java @@ -0,0 +1,50 @@ +/** + * 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 final List 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()); + } + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java new file mode 100644 index 00000000000..5cfeba66331 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java @@ -0,0 +1,39 @@ +/** + * 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 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.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DummyMiddleware implements CommandMiddleware { + + @Override + public void invoke(Command command) { + if (command instanceof DummyCommand c) { + c.getPayload().setId(command.getId()); + } + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyRequest.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyRequest.java new file mode 100644 index 00000000000..03ea3878266 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyRequest.java @@ -0,0 +1,43 @@ +/** + * 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.model; + +import java.io.Serial; +import java.io.Serializable; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class DummyRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id; + + private String content; +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyResponse.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyResponse.java new file mode 100644 index 00000000000..95dca585863 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyResponse.java @@ -0,0 +1,45 @@ +/** + * 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.model; + +import java.io.Serial; +import java.io.Serializable; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class DummyResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID requestId; + + private String content; + + private String error; +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java new file mode 100644 index 00000000000..af54383a4a4 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java @@ -0,0 +1,37 @@ +/** + * 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.service; + +import java.util.Locale; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class DefaultDummyService implements DummyService { + + @Override + public DummyResponse process(DummyRequest request) { + return DummyResponse.builder().requestId(request.getId()).content(request.getContent().toUpperCase(Locale.ROOT)).build(); + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DummyService.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DummyService.java new file mode 100644 index 00000000000..61a833d3ca0 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DummyService.java @@ -0,0 +1,27 @@ +/** + * 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.service; + +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; + +public interface DummyService { + + DummyResponse process(DummyRequest request); +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyApiController.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyApiController.java new file mode 100644 index 00000000000..697564b85a3 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyApiController.java @@ -0,0 +1,70 @@ +/** + * 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.web; + +import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.CommandPipeline; +import org.apache.fineract.command.sample.command.DummyCommand; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.springframework.scheduling.annotation.Async; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/test/dummy", consumes = APPLICATION_JSON_VALUE, produces = { APPLICATION_JSON_VALUE, + APPLICATION_PROBLEM_JSON_VALUE }) +class DummyApiController { + + private final CommandPipeline pipeline; + + @PostMapping("/sync") + DummyResponse dummySync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId, + @RequestBody DummyRequest request) { + var command = new DummyCommand(); + command.setId(requestId); + command.setPayload(request); + + Supplier result = pipeline.send(command); + + return result.get(); + } + + @Async + @PostMapping("/async") + CompletableFuture dummyAsync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId, + @RequestBody DummyRequest request) { + var command = new DummyCommand(); + command.setId(requestId); + command.setPayload(request); + + Supplier result = pipeline.send(command); + + return CompletableFuture.supplyAsync(result); + } +} diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyExceptionHandler.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyExceptionHandler.java new file mode 100644 index 00000000000..f3b6095cac2 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyExceptionHandler.java @@ -0,0 +1,19 @@ +package org.apache.fineract.command.sample.web; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +public class DummyExceptionHandler { + + @ExceptionHandler(Throwable.class) + public ResponseEntity> handleRuntimeException(Throwable ex) { + return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(Map.of("error", ex.getMessage())); + } +} diff --git a/fineract-command/src/test/resources/application-test.properties b/fineract-command/src/test/resources/application-test.properties new file mode 100644 index 00000000000..8e872984c9d --- /dev/null +++ b/fineract-command/src/test/resources/application-test.properties @@ -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. +# + +#server.error.include-message=always +#server.error.include-binding-errors=always +#server.error.include-stacktrace=always + +fineract.command.enabled=true +fineract.command.executor=disruptor +fineract.command.ring-buffer-size=1024 diff --git a/settings.gradle b/settings.gradle index 04999ae3f45..64211673eb5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,6 +48,7 @@ buildCache { rootProject.name='fineract' include ':fineract-core' +include ':fineract-command' include ':fineract-accounting' include ':fineract-provider' include ':fineract-branch'