001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.impl.saga; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.concurrent.CompletableFuture; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.CopyOnWriteArrayList; 027import java.util.concurrent.TimeUnit; 028import java.util.concurrent.atomic.AtomicReference; 029import java.util.function.Function; 030 031import org.apache.camel.CamelContext; 032import org.apache.camel.Endpoint; 033import org.apache.camel.Exchange; 034import org.apache.camel.Expression; 035import org.apache.camel.RuntimeCamelException; 036import org.apache.camel.saga.CamelSagaCoordinator; 037import org.apache.camel.saga.CamelSagaStep; 038import org.apache.camel.util.ObjectHelper; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * A in-memory implementation of a saga coordinator. 044 */ 045public class InMemorySagaCoordinator implements CamelSagaCoordinator { 046 047 private enum Status { 048 RUNNING, 049 COMPENSATING, 050 COMPENSATED, 051 COMPLETING, 052 COMPLETED 053 } 054 055 private static final Logger LOG = LoggerFactory.getLogger(InMemorySagaCoordinator.class); 056 057 private CamelContext camelContext; 058 private InMemorySagaService sagaService; 059 private String sagaId; 060 private List<CamelSagaStep> steps; 061 private Map<CamelSagaStep, Map<String, Object>> optionValues; 062 private AtomicReference<Status> currentStatus; 063 064 public InMemorySagaCoordinator(CamelContext camelContext, InMemorySagaService sagaService, String sagaId) { 065 this.camelContext = ObjectHelper.notNull(camelContext, "camelContext"); 066 this.sagaService = ObjectHelper.notNull(sagaService, "sagaService"); 067 this.sagaId = ObjectHelper.notNull(sagaId, "sagaId"); 068 this.steps = new CopyOnWriteArrayList<>(); 069 this.optionValues = new ConcurrentHashMap<>(); 070 this.currentStatus = new AtomicReference<>(Status.RUNNING); 071 } 072 073 @Override 074 public String getId() { 075 return sagaId; 076 } 077 078 @Override 079 public CompletableFuture<Void> beginStep(Exchange exchange, CamelSagaStep step) { 080 this.steps.add(step); 081 082 if (!step.getOptions().isEmpty()) { 083 optionValues.putIfAbsent(step, new ConcurrentHashMap<>()); 084 Map<String, Object> values = optionValues.get(step); 085 for (String option : step.getOptions().keySet()) { 086 Expression expression = step.getOptions().get(option); 087 try { 088 values.put(option, expression.evaluate(exchange, Object.class)); 089 } catch (Exception ex) { 090 return CompletableFuture.supplyAsync(() -> { 091 throw new RuntimeCamelException("Cannot evaluate saga option '" + option + "'", ex); 092 }); 093 } 094 } 095 } 096 097 if (step.getTimeoutInMilliseconds().isPresent()) { 098 sagaService.getExecutorService().schedule(() -> { 099 boolean doAction = currentStatus.compareAndSet(Status.RUNNING, Status.COMPENSATING); 100 if (doAction) { 101 doCompensate(); 102 } 103 }, step.getTimeoutInMilliseconds().get(), TimeUnit.MILLISECONDS); 104 } 105 106 return CompletableFuture.completedFuture(null); 107 } 108 109 @Override 110 public CompletableFuture<Void> compensate() { 111 boolean doAction = currentStatus.compareAndSet(Status.RUNNING, Status.COMPENSATING); 112 113 if (doAction) { 114 doCompensate(); 115 } else { 116 Status status = currentStatus.get(); 117 if (status != Status.COMPENSATING && status != Status.COMPENSATED) { 118 CompletableFuture<Void> res = new CompletableFuture<>(); 119 res.completeExceptionally(new IllegalStateException("Cannot compensate: status is " + status)); 120 return res; 121 } 122 } 123 124 return CompletableFuture.completedFuture(null); 125 } 126 127 128 @Override 129 public CompletableFuture<Void> complete() { 130 boolean doAction = currentStatus.compareAndSet(Status.RUNNING, Status.COMPLETING); 131 132 if (doAction) { 133 doComplete(); 134 } else { 135 Status status = currentStatus.get(); 136 if (status != Status.COMPLETING && status != Status.COMPLETED) { 137 CompletableFuture<Void> res = new CompletableFuture<>(); 138 res.completeExceptionally(new IllegalStateException("Cannot complete: status is " + status)); 139 return res; 140 } 141 } 142 143 return CompletableFuture.completedFuture(null); 144 } 145 146 public CompletableFuture<Boolean> doCompensate() { 147 return doFinalize(CamelSagaStep::getCompensation, "compensation") 148 .thenApply(res -> { 149 currentStatus.set(Status.COMPENSATED); 150 return res; 151 }); 152 } 153 154 public CompletableFuture<Boolean> doComplete() { 155 return doFinalize(CamelSagaStep::getCompletion, "completion") 156 .thenApply(res -> { 157 currentStatus.set(Status.COMPLETED); 158 return res; 159 }); 160 } 161 162 public CompletableFuture<Boolean> doFinalize(Function<CamelSagaStep, Optional<Endpoint>> endpointExtractor, String description) { 163 CompletableFuture<Boolean> result = CompletableFuture.completedFuture(true); 164 for (CamelSagaStep step : reversed(steps)) { 165 Optional<Endpoint> endpoint = endpointExtractor.apply(step); 166 if (endpoint.isPresent()) { 167 result = result.thenCompose(prevResult -> 168 doFinalize(endpoint.get(), step, 0, description).thenApply(res -> prevResult && res)); 169 } 170 } 171 return result.whenComplete((done, ex) -> { 172 if (ex != null) { 173 LOG.error("Cannot finalize " + description + " the saga", ex); 174 } else if (!done) { 175 LOG.warn("Unable to finalize " + description + " for all required steps of the saga " + sagaId); 176 } 177 }); 178 } 179 180 private CompletableFuture<Boolean> doFinalize(Endpoint endpoint, CamelSagaStep step, int doneAttempts, String description) { 181 Exchange exchange = createExchange(endpoint, step); 182 183 return CompletableFuture.supplyAsync(() -> { 184 Exchange res = camelContext.createFluentProducerTemplate().to(endpoint).withExchange(exchange).send(); 185 Exception ex = res.getException(); 186 if (ex != null) { 187 throw new RuntimeCamelException(res.getException()); 188 } 189 return true; 190 }, sagaService.getExecutorService()).exceptionally(ex -> { 191 LOG.warn("Exception thrown during " + description + " at " + endpoint.getEndpointUri() 192 + ". Attempt " + (doneAttempts + 1) + " of " + sagaService.getMaxRetryAttempts(), ex); 193 return false; 194 }).thenCompose(executed -> { 195 int currentAttempt = doneAttempts + 1; 196 if (executed) { 197 return CompletableFuture.completedFuture(true); 198 } else if (currentAttempt >= sagaService.getMaxRetryAttempts()) { 199 return CompletableFuture.completedFuture(false); 200 } else { 201 CompletableFuture<Boolean> future = new CompletableFuture<>(); 202 sagaService.getExecutorService().schedule(() -> { 203 doFinalize(endpoint, step, currentAttempt, description).whenComplete((res, ex) -> { 204 if (ex != null) { 205 future.completeExceptionally(ex); 206 } else { 207 future.complete(res); 208 } 209 }); 210 }, sagaService.getRetryDelayInMilliseconds(), TimeUnit.MILLISECONDS); 211 return future; 212 } 213 }); 214 } 215 216 private Exchange createExchange(Endpoint endpoint, CamelSagaStep step) { 217 Exchange exchange = endpoint.createExchange(); 218 exchange.getIn().setHeader(Exchange.SAGA_LONG_RUNNING_ACTION, getId()); 219 220 Map<String, Object> values = optionValues.get(step); 221 if (values != null) { 222 for (Map.Entry<String, Object> entry : values.entrySet()) { 223 exchange.getIn().setHeader(entry.getKey(), entry.getValue()); 224 } 225 } 226 return exchange; 227 } 228 229 private <T> List<T> reversed(List<T> list) { 230 List<T> reversed = new ArrayList<>(list); 231 Collections.reverse(reversed); 232 return reversed; 233 } 234}