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}