diff --git a/build.gradle b/build.gradle index ad23be8119..5d6338e88e 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 0000000000..56cb98fe62 --- /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 0000000000..c49aff7924 --- /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 0000000000..83db5a8a13 --- /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 0000000000..110f86edbf --- /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 0000000000..de12e3ce3e --- /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 0000000000..d246b1ad7f --- /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 0000000000..1ae3b18646 --- /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 0000000000..e31b567231 --- /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 0000000000..35070f7887 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java @@ -0,0 +1,49 @@ +/** + * 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 String executor = "sync"; + + @Builder.Default + private Integer ringBufferSize = 1024; +} 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 0000000000..a6773d6cac --- /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 0000000000..9a2a290b31 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/core/exception/CommandHandlerNotFoundException.java @@ -0,0 +1,35 @@ +/** + * 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 { + + private final String commandClass; + + public CommandHandlerNotFoundException(Command command) { + this.commandClass = command.getClass().getSimpleName(); + } + + @Override + public String getMessage() { + return "Cannot find a matching handler for " + commandClass + " command"; + } +} 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 0000000000..2dcf7325e8 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java @@ -0,0 +1,55 @@ +/** + * 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.concurrent.CompletableFuture; +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.CommandHandler; +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 = "async") +@SuppressWarnings({ "unchecked" }) +public class AsynchronousCommandExecutor implements CommandExecutor { + + private final CommandRouter router; + + @Override + public Supplier execute(Command command) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + CommandHandler handler = (CommandHandler) router.route(command).handle(command); + return handler.handle(command); + }); + + try { + return future::join; + } catch (Exception e) { + // TODO: create proper exception + throw new RuntimeException(e.getMessage()); + } + } +} 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 0000000000..1554ebb8bd --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.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 static java.util.Objects.requireNonNull; + +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.ConditionalOnBean; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnBean(CommandPipeline.class) +public class DefaultCommandPipeline implements CommandPipeline { + + private final List middlewares; + + private final CommandExecutor executor; + + public Supplier send(final Command command) { + requireNonNull(command, "Command must not be null"); + + middlewares.forEach(middleware -> { + middleware.invoke(command); + }); + + 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 0000000000..3d75959c76 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandRouter.java @@ -0,0 +1,46 @@ +/** + * 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 + 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 0000000000..9cd0d6ad1e --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java @@ -0,0 +1,93 @@ +/** + * 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.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.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); + + log.warn("Resolving LMAX Disruptor result..."); + 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 CommandRouter router; + + @Override + public void onEvent(CommandEvent event, long sequence, boolean endOfBatch) throws Exception { + var handler = router.route(event.getCommand()); + + event.getFuture().complete(handler.handle(event.getCommand())); + } + } +} 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 0000000000..f6372db1cd --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java @@ -0,0 +1,44 @@ +/** + * 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.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.CommandHandler; +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 = "sync") +@SuppressWarnings({ "unchecked" }) +public class SynchronousCommandExecutor implements CommandExecutor { + + private final CommandRouter router; + + @Override + public Supplier execute(Command command) { + return () -> ((CommandHandler) router.route(command)).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 0000000000..e4d5955ab5 --- /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 0000000000..8ec25d9ba4 --- /dev/null +++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java @@ -0,0 +1,54 @@ +/** + * 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.dsl.Disruptor; +import com.lmax.disruptor.util.DaemonThreadFactory; +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(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(router)); + + // 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 0000000000..f16efccfc3 --- /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 0000000000..2b7a68f637 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java @@ -0,0 +1,66 @@ +package org.apache.fineract.command; + +import com.lmax.disruptor.dsl.Disruptor; +import com.lmax.disruptor.util.DaemonThreadFactory; +import java.util.List; +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.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(router)); + + // Start the disruptor + disruptor.start(); + + pipeline = new DefaultCommandPipeline( + List.of(command -> log.warn("Command middleware invoked: {} ({})", command.getPayload(), command.getId())), + new DisruptorCommandExecutor(disruptor)); + } + + @TearDown(Level.Iteration) + public void tearDown() {} + + @Benchmark + public void processCommand() { + var command = new DummyCommand(); + command.setPayload(DummyRequest.builder().content("hello").build()); + + Supplier result = pipeline.send(command); + + log.info("Result: {}", 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 0000000000..9e662911c4 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java @@ -0,0 +1,32 @@ +package org.apache.fineract.command; + +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.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) +public class CommandPipelineTest { + + @Autowired + private CommandPipeline pipeline; + + @Test + public void processCommand() { + var command = new DummyCommand(); + command.setId(UUID.randomUUID()); + command.setPayload(DummyRequest.builder().content("hello").build()); + + var result = pipeline.send(command); + + log.warn("RESULT: {}", result.get()); + } +} 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 0000000000..97f0c32c89 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleWebTest.java @@ -0,0 +1,78 @@ +package org.apache.fineract.command; + +import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID; +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.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; +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 +// @ExtendWith({ RestDocumentationExtension.class, SpringExtension.class }) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = TestConfiguration.class) +@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") +public 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() { + restTemplate.getRestTemplate().setInterceptors(interceptors); + var result = restTemplate.postForObject(baseUrl + "/sync", DummyRequest.builder().content("test").build(), DummyResponse.class); + + log.warn("RESULT: {}", result); + + assertNotNull(result, "Response should not be null."); + assertNotNull(result.getContent(), "Response body should not be null."); + assertEquals("TEST", result.getContent(), "Wrong response content."); + } + + @Test + void dummyApiAsync() { + restTemplate.getRestTemplate().setInterceptors(interceptors); + var result = restTemplate.postForObject(baseUrl + "/async", DummyRequest.builder().content("test").build(), DummyResponse.class); + + log.warn("RESULT: {}", result); + + assertNotNull(result, "Response should not be null."); + assertNotNull(result.getContent(), "Response body should not be null."); + assertEquals("TEST", result.getContent(), "Wrong response content."); + } +} 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 0000000000..469f984453 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java @@ -0,0 +1,19 @@ +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 0000000000..4c52402da8 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyCommand.java @@ -0,0 +1,10 @@ +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 0000000000..646de7ba17 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java @@ -0,0 +1,23 @@ +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/DummyMiddleware.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java new file mode 100644 index 0000000000..e85357ef64 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java @@ -0,0 +1,13 @@ +package org.apache.fineract.command.sample.middleware; + +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.command.core.CommandRouter; + +public class DummyMiddleware implements CommandRouter { + + @Override + public CommandHandler route(Command command) { + return null; + } +} 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 0000000000..53710a1a73 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyRequest.java @@ -0,0 +1,22 @@ +package org.apache.fineract.command.sample.model; + +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 DummyRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + 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 0000000000..e272972963 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/model/DummyResponse.java @@ -0,0 +1,22 @@ +package org.apache.fineract.command.sample.model; + +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 DummyResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String content; +} 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 0000000000..bee19eee62 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java @@ -0,0 +1,15 @@ +package org.apache.fineract.command.sample.service; + +import java.util.Locale; +import org.apache.fineract.command.sample.model.DummyRequest; +import org.apache.fineract.command.sample.model.DummyResponse; +import org.springframework.stereotype.Service; + +@Service +public class DefaultDummyService implements DummyService { + + @Override + public DummyResponse process(DummyRequest request) { + return DummyResponse.builder().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 0000000000..d98a528ac2 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DummyService.java @@ -0,0 +1,10 @@ +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 0000000000..52ee16b3d6 --- /dev/null +++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/web/DummyApiController.java @@ -0,0 +1,52 @@ +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 }) +public class DummyApiController { + + private final CommandPipeline pipeline; + + @PostMapping("/sync") + public DummyResponse dummySync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) String requestId, + @RequestBody DummyRequest request) { + var command = new DummyCommand(); + command.setId(UUID.fromString(requestId)); + command.setPayload(request); + + Supplier result = pipeline.send(command); + + return result.get(); + } + + @Async + @PostMapping("/async") + public CompletableFuture dummyAsync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) String requestId, + @RequestBody DummyRequest request) { + var command = new DummyCommand(); + command.setId(UUID.fromString(requestId)); + command.setPayload(request); + + Supplier result = pipeline.send(command); + + return CompletableFuture.supplyAsync(result); + } +} 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 0000000000..a3a66adf04 --- /dev/null +++ b/fineract-command/src/test/resources/application-test.properties @@ -0,0 +1,3 @@ +fineract.command.enabled=true +fineract.command.executor=disruptor +fineract.command.ring-buffer-size=1024 diff --git a/settings.gradle b/settings.gradle index 04999ae3f4..64211673eb 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'