Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion code/chapter08/payment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<plugin>
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-maven-plugin</artifactId>
<version>3.11.2</version>
<version>3.12.0</version>
<configuration>
<serverName>mpServer</serverName>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.microprofile.tutorial.store.payment.resource;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import io.microprofile.tutorial.store.payment.entity.PaymentDetails;
import io.microprofile.tutorial.store.payment.exception.CriticalPaymentException;
import io.microprofile.tutorial.store.payment.exception.PaymentProcessingException;
import io.microprofile.tutorial.store.payment.service.PaymentService;
import jakarta.enterprise.context.RequestScoped;
Expand All @@ -25,10 +25,6 @@
@RequestScoped
@Path("/")
public class PaymentResource {

@Inject
@ConfigProperty(name = "payment.gateway.endpoint")
private String endpoint;

@Inject
private PaymentService paymentService;
Expand All @@ -38,43 +34,55 @@ public class PaymentResource {
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Process payment", description = "Process payment using the payment gateway API with fault tolerance")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Payment processed successfully"),
@APIResponse(responseCode = "400", description = "Invalid input data"),
@APIResponse(responseCode = "500", description = "Internal server error")
@APIResponse(responseCode = "200", description = "Payment processed successfully",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = String.class))),
@APIResponse(responseCode = "400", description = "Invalid input data",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = String.class))),
@APIResponse(responseCode = "500", description = "Internal server error",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = String.class)))
})
public Response processPayment(@QueryParam("amount") Double amount)
throws PaymentProcessingException, CriticalPaymentException {

// Input validation
public CompletionStage<Response> processPayment(@QueryParam("amount") Double amount) {

if (amount == null || amount <= 0) {
throw new CriticalPaymentException("Invalid payment amount: " + amount);
return CompletableFuture.completedFuture(
Response.status(Response.Status.BAD_REQUEST)
.entity("{\"status\":\"error\", \"message\":\"Invalid payment amount: " + amount + "\"}")
.type(MediaType.APPLICATION_JSON)
.build()
);
}

try {
// Create PaymentDetails using constructor
PaymentDetails paymentDetails = new PaymentDetails(
"****-****-****-1111", // cardNumber - placeholder for demo
"Demo User", // cardHolderName
"12/25", // expiryDate
"***", // securityCode
BigDecimal.valueOf(amount) // amount
);
PaymentDetails paymentDetails = new PaymentDetails(
"****-****-****-1111",
"Demo User",
"12/25",
"***",
BigDecimal.valueOf(amount)
);

// Use PaymentService with full fault tolerance features
CompletionStage<String> result = paymentService.processPayment(paymentDetails);

// Wait for async result (in production, consider different patterns)
String paymentResult = result.toCompletableFuture().get();

return Response.ok(paymentResult, MediaType.APPLICATION_JSON).build();

System.out.println("[" + Thread.currentThread().getName() + "] HTTP request received, handing off to managed executor");

try {
return paymentService.processPayment(paymentDetails)
.thenApply(result -> {
System.out.println("[" + Thread.currentThread().getName() + "] Sending HTTP response for amount: " + amount);
return Response.ok(result, MediaType.APPLICATION_JSON).build();
})
.exceptionally(e -> {
Throwable cause = e.getCause() != null ? e.getCause() : e;
System.out.println("[" + Thread.currentThread().getName() + "] Error for amount " + amount + ": " + cause.getMessage());
return Response.ok(cause.getMessage(), MediaType.APPLICATION_JSON).build();
});
} catch (PaymentProcessingException e) {
// Re-throw to let fault tolerance handle it
throw e;
} catch (Exception e) {
// Handle other exceptions
throw new PaymentProcessingException("Payment processing failed: " + e.getMessage());
return CompletableFuture.completedFuture(
Response.serverError()
.entity("{\"status\":\"error\", \"message\":\"" + e.getMessage() + "\"}")
.type(MediaType.APPLICATION_JSON)
.build()
);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.microprofile.tutorial.store.payment.service;

import org.eclipse.microprofile.faulttolerance.ExecutionContext;
import org.eclipse.microprofile.faulttolerance.FallbackHandler;
import org.eclipse.microprofile.faulttolerance.exceptions.BulkheadException;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

public class PaymentFallbackHandler implements FallbackHandler<CompletionStage<String>> {

@Override
public CompletionStage<String> handle(ExecutionContext context) {
if (context.getFailure() instanceof BulkheadException) {
System.out.println("Bulkhead rejected request for method: " + context.getMethod().getName());
return CompletableFuture.completedFuture(
"{\"status\":\"rejected\", \"message\":\"Bulkhead full: too many concurrent requests.\"}");
}
System.out.println("Fallback invoked: " + context.getFailure().getMessage());
return CompletableFuture.completedFuture(
"{\"status\":\"failed\", \"message\":\"Payment service is currently unavailable.\"}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ public class PaymentService {
jitter = 500,
retryOn = PaymentProcessingException.class,
abortOn = CriticalPaymentException.class)
@Fallback(fallbackMethod = "fallbackProcessPayment")
@Bulkhead(value=5)
@Fallback(PaymentFallbackHandler.class)
@Bulkhead(value = 5, waitingTaskQueue = 2)
@CircuitBreaker(
failureRatio = 0.5,
requestVolumeThreshold = 4,
failureRatio = 0.75,
requestVolumeThreshold = 10,
delay = 3000
)
public CompletionStage<String> processPayment(PaymentDetails paymentDetails) throws PaymentProcessingException {
Expand Down
2 changes: 1 addition & 1 deletion code/chapter08/payment/src/main/liberty/config/server.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<server description="MicroProfile Tutorial Liberty Server">
<featureManager>
<platform>jakartaEE-10.0</platform>
<platform>microProfile-6.1</platform>
<platform>microProfile-7.1</platform>
<feature>restfulWS</feature>
<feature>jsonp</feature>
<feature>jsonb</feature>
Expand Down
45 changes: 24 additions & 21 deletions code/chapter08/payment/test-payment-bulkhead.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ echo -e "${CYAN}Base URL: $BASE_URL${NC}"
echo ""

# Set up log monitoring
LOG_FILE="/workspaces/liberty-rest-app/payment/target/liberty/wlp/usr/servers/mpServer/logs/messages.log"
LOG_FILE="/workspaces/microprofile-tutorial/code/chapter08/payment/target/liberty/wlp/usr/servers/mpServer/logs/messages.log"

# Get initial log position for later analysis
if [ -f "$LOG_FILE" ]; then
Expand All @@ -71,17 +71,18 @@ echo ""
# ====================================
echo -e "${BLUE}=== PaymentService Bulkhead Configuration ===${NC}"
echo -e "${YELLOW}Your PaymentService has these bulkhead settings:${NC}"
echo -e "${CYAN}• Maximum Concurrent Requests: 5${NC}"
echo -e "${CYAN}• Maximum Concurrent Threads: 5${NC}"
echo -e "${CYAN}• Waiting Task Queue: 2${NC}"
echo -e "${CYAN}• Effective Capacity: 7 (5 running + 2 queued)${NC}"
echo -e "${CYAN}• Asynchronous Processing: Yes${NC}"
echo -e "${CYAN}• Retry on failure: Yes (3 retries)${NC}"
echo -e "${CYAN}• Processing delay: ~1.5 seconds per attempt${NC}"
echo ""

echo -e "${YELLOW}🔍 WHAT TO EXPECT WITH BULKHEAD:${NC}"
echo -e "${CYAN}• Only 5 concurrent requests will be processed at a time${NC}"
echo -e "${CYAN}• Additional requests beyond the limit will be rejected${NC}"
echo -e "${CYAN}• Rejected requests will receive a 'Bulkhead full' message${NC}"
echo -e "${CYAN}• Successfully queued requests complete in ~1.5-10 seconds${NC}"
echo -e "${YELLOW}WHAT TO EXPECT WITH BULKHEAD (10 concurrent requests):${NC}"
echo -e "${CYAN}• 5 requests processed immediately, 2 queued — 7 total accepted${NC}"
echo -e "${CYAN}• 3 requests rejected instantly with 'Bulkhead full' status${NC}"
echo -e "${CYAN}• Accepted requests complete in ~1.5 seconds${NC}"
echo ""

echo -e "${YELLOW}Make sure the Payment Service is running on port 9080${NC}"
Expand Down Expand Up @@ -114,10 +115,10 @@ send_request() {
if echo "$response" | grep -q "success"; then
echo -e "${GREEN}[Request $id] SUCCESS: Payment processed in ${duration}s${NC}"
echo -e "${GREEN}[Request $id] Response: $response${NC}"
elif echo "$response" | grep -q "Bulkhead"; then
elif echo "$response" | grep -q "rejected"; then
echo -e "${YELLOW}[Request $id] REJECTED: Bulkhead full (took ${duration}s)${NC}"
echo -e "${YELLOW}[Request $id] Response: $response${NC}"
elif echo "$response" | grep -q "fallback"; then
elif echo "$response" | grep -q "failed"; then
echo -e "${PURPLE}[Request $id] FALLBACK: Service used fallback (took ${duration}s)${NC}"
echo -e "${PURPLE}[Request $id] Response: $response${NC}"
else
Expand Down Expand Up @@ -169,7 +170,14 @@ for pid in "${pids[@]}"; do
wait $pid
done

# Collect results
# Display results in order (output was suppressed while running in background)
echo ""
echo -e "${BLUE}=== Concurrent Request Results ===${NC}"
for i in {1..10}; do
cat /tmp/bulkhead_result_$i.txt
done

# Count results by grepping the captured output files
echo ""
echo -e "${BLUE}=== Results Summary ===${NC}"
success_count=0
Expand All @@ -178,26 +186,21 @@ fallback_count=0
error_count=0

for i in {1..10}; do
result=$(cat /tmp/bulkhead_result_$i.txt)
rm /tmp/bulkhead_result_$i.txt
results+=($result)

# Count results by parsing the output log
log_entry=$(grep "\[Request $i\]" /tmp/bulkhead.log 2>/dev/null || echo "")
if echo "$log_entry" | grep -q "SUCCESS"; then
if grep -q "SUCCESS" /tmp/bulkhead_result_$i.txt 2>/dev/null; then
success_count=$((success_count + 1))
elif echo "$log_entry" | grep -q "REJECTED"; then
elif grep -q "REJECTED" /tmp/bulkhead_result_$i.txt 2>/dev/null; then
rejected_count=$((rejected_count + 1))
elif echo "$log_entry" | grep -q "FALLBACK"; then
elif grep -q "FALLBACK" /tmp/bulkhead_result_$i.txt 2>/dev/null; then
fallback_count=$((fallback_count + 1))
else
error_count=$((error_count + 1))
fi
rm /tmp/bulkhead_result_$i.txt
done

echo -e "${CYAN}Successful requests: $success_count${NC}"
echo -e "${CYAN}Rejected requests: $rejected_count${NC}"
echo -e "${CYAN}Fallback requests: $fallback_count${NC}"
echo -e "${CYAN}Rejected requests (bulkhead full): $rejected_count${NC}"
echo -e "${CYAN}Fallback requests (retries exhausted): $fallback_count${NC}"
echo -e "${CYAN}Error requests: $error_count${NC}"

# ====================================
Expand Down
Loading