feat: Idempotent Consumer Microservice Pattern #2683 (#3082)

* added idempotent consumer pattern

* updated doc

* fixed bug in RequestService

* add test converage

* Add test converage to state machine

* renamed module and added code example
This commit is contained in:
David M.
2024-11-11 16:59:35 +01:00
committed by GitHub
parent 25bb77eee7
commit 2a2c5605bc
13 changed files with 930 additions and 0 deletions
@@ -0,0 +1,74 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
* The main entry point for the idempotent-consumer application.
* This application demonstrates the use of the Idempotent Consumer
* pattern which ensures that a message is processed exactly once
* in scenarios where the same message can be delivered multiple times.
*
* @see <a href="https://en.wikipedia.org/wiki/Idempotence">Idempotence (Wikipedia)</a>
* @see <a href="https://camel.apache.org/components/latest/eips/idempotentConsumer-eip.html">Idempotent Consumer Pattern (Apache Camel)</a>
*/
@SpringBootApplication
@Slf4j
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
/**
* The starting point of the CommandLineRunner
* where the main program is run.
*
* @param requestService idempotent request service
* @param requestRepository request jpa repository
*/
@Bean
public CommandLineRunner run(RequestService requestService, RequestRepository requestRepository) {
return args -> {
Request req = requestService.create(UUID.randomUUID());
requestService.create(req.getUuid());
requestService.create(req.getUuid());
LOGGER.info("Nb of requests : {}", requestRepository.count()); // 1, processRequest is idempotent
req = requestService.start(req.getUuid());
try {
req = requestService.start(req.getUuid());
} catch (InvalidNextStateException ex) {
LOGGER.error("Cannot start request twice!");
}
req = requestService.complete(req.getUuid());
LOGGER.info("Request: {}", req);
};
}
}
@@ -0,0 +1,36 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
/**
* This exception is thrown when an invalid transition is attempted in the Statemachine
* for the request status. This can occur when attempting to move to a state that is not valid
* from the current state.
*/
public class InvalidNextStateException extends RuntimeException {
public InvalidNextStateException(String s) {
super(s);
}
}
@@ -0,0 +1,59 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.util.UUID;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* The {@code Request} class represents a request with a unique UUID and a status.
* The status of a request can be one of four values: PENDING, STARTED, COMPLETED, or INERROR.
*/
@Entity
@NoArgsConstructor
@Data
public class Request {
enum Status {
PENDING,
STARTED,
COMPLETED
}
@Id
private UUID uuid;
private Status status;
public Request(UUID uuid) {
this(uuid, Status.PENDING);
}
public Request(UUID uuid, Status status) {
this.uuid = uuid;
this.status = status;
}
}
@@ -0,0 +1,39 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import java.util.UUID;
/**
* This class extends the RuntimeException class to handle scenarios where a Request is not found.
* It is intended to be used where you would like to have a custom exception that signals that a requested object or action
* was not found in the system, based on the UUID of the request.
*
*/
public class RequestNotFoundException extends RuntimeException {
RequestNotFoundException(UUID uuid) {
super(String.format("Request %s not found", uuid));
}
}
@@ -0,0 +1,40 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* This is a repository interface for the "Request" entity. It extends the JpaRepository interface from Spring Data JPA.
* JpaRepository comes with many operations out of the box, including standard CRUD operations.
* With JpaRepository, we are also able to leverage the power of Spring Data's query methods.
* The UUID parameter in JpaRepository refers to the type of the ID in the "Request" entity.
*
*/
@Repository
public interface RequestRepository extends JpaRepository<Request, UUID> {
}
@@ -0,0 +1,91 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import java.util.Optional;
import java.util.UUID;
import org.springframework.stereotype.Service;
/**
* This service is responsible for handling request operations including
* creation, start, and completion of requests.
*/
@Service
public class RequestService {
RequestRepository requestRepository;
RequestStateMachine requestStateMachine;
public RequestService(RequestRepository requestRepository,
RequestStateMachine requestStateMachine) {
this.requestRepository = requestRepository;
this.requestStateMachine = requestStateMachine;
}
/**
* Creates a new Request or returns an existing one by it's UUID.
* This operation is idempotent: performing it once or several times
* successively leads to an equivalent result.
*
* @param uuid The unique identifier for the Request.
* @return Return existing Request or save and return a new Request.
*/
public Request create(UUID uuid) {
Optional<Request> optReq = requestRepository.findById(uuid);
if (!optReq.isEmpty()) {
return optReq.get();
}
return requestRepository.save(new Request(uuid));
}
/**
* Starts the Request assigned with the given UUID.
*
* @param uuid The unique identifier for the Request.
* @return The started Request.
* @throws RequestNotFoundException if a Request with the given UUID is not found.
*/
public Request start(UUID uuid) {
Optional<Request> optReq = requestRepository.findById(uuid);
if (optReq.isEmpty()) {
throw new RequestNotFoundException(uuid);
}
return requestRepository.save(requestStateMachine.next(optReq.get(), Request.Status.STARTED));
}
/**
* Complete the Request assigned with the given UUID.
*
* @param uuid The unique identifier for the Request.
* @return The completed Request.
* @throws RequestNotFoundException if a Request with the given UUID is not found.
*/
public Request complete(UUID uuid) {
Optional<Request> optReq = requestRepository.findById(uuid);
if (optReq.isEmpty()) {
throw new RequestNotFoundException(uuid);
}
return requestRepository.save(requestStateMachine.next(optReq.get(), Request.Status.COMPLETED));
}
}
@@ -0,0 +1,63 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.idempotentconsumer;
import org.springframework.stereotype.Component;
/**
* This class represents a state machine for managing request transitions.
* It supports transitions to the statuses: PENDING, STARTED, and COMPLETED.
*/
@Component
public class RequestStateMachine {
/**
* Provides the next possible state of the request based on the current and next status.
*
* @param req The actual request object. This object MUST NOT be null and SHOULD have a valid status.
* @param nextStatus Represents the next status that the request could transition to. MUST NOT be null.
* @return A new Request object with updated status if the transition is valid.
* @throws InvalidNextStateException If an invalid state transition is attempted.
*/
public Request next(Request req, Request.Status nextStatus) {
String transitionStr = String.format("Transition: %s -> %s", req.getStatus(), nextStatus);
switch (nextStatus) {
case PENDING -> throw new InvalidNextStateException(transitionStr);
case STARTED -> {
if (Request.Status.PENDING.equals(req.getStatus())) {
return new Request(req.getUuid(), Request.Status.STARTED);
}
throw new InvalidNextStateException(transitionStr);
}
case COMPLETED -> {
if (Request.Status.STARTED.equals(req.getStatus())) {
return new Request(req.getUuid(), Request.Status.COMPLETED);
}
throw new InvalidNextStateException(transitionStr);
}
default -> throw new InvalidNextStateException(transitionStr);
}
}
}