/* * Copyright (c) 2015 Spotify AB. * * 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 com.spotify.heroic.cluster; import com.spotify.heroic.metric.QueryTrace; import com.spotify.heroic.metric.RuntimeNodeException; import com.spotify.heroic.statistics.QueryReporter; import eu.toolchain.async.AsyncFramework; import eu.toolchain.async.AsyncFuture; import eu.toolchain.async.RetryException; import eu.toolchain.async.RetryPolicy; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import lombok.Data; import lombok.extern.slf4j.Slf4j; @Slf4j @Data public class ClusterShard { private static final QueryTrace.Identifier RETRY_BACKOFF = new QueryTrace.Identifier("retry-backoff"); private final AsyncFramework async; private final Map<String, String> shard; private final QueryReporter reporter; private final ClusterManager cluster; public <T> AsyncFuture<T> apply( Function<ClusterNode.Group, AsyncFuture<T>> function, BiFunction<T, List<QueryTrace>, T> handleRetryTraceFn ) { final List<ClusterNode> nodesTried = new ArrayList<>(); if (!cluster.hasNextButNotWithId(shard, nodesTried::contains)) { return async.failed(new RuntimeException("No groups available")); } final RetryPolicy parent = RetryPolicy.timed(30000, RetryPolicy.exponential(100, 5000)); /* a policy that is valid as long as there are more nodes available to try */ final RetryPolicy iteratorPolicy = clockSource -> { final RetryPolicy.Instance p = parent.apply(clockSource); return () -> { if (cluster.hasNextButNotWithId(shard, nodesTried::contains)) { return p.next(); } return new RetryPolicy.Decision(false, 0); }; }; return async .retryUntilResolved(() -> { Optional<ClusterManager.NodeResult<AsyncFuture<T>>> ret = cluster.withNodeInShardButNotWithId(shard, nodesTried::contains, function); if (!ret.isPresent()) { throw new RuntimeException("No groups available"); } ClusterManager.NodeResult<AsyncFuture<T>> result = ret.get(); nodesTried.add(result.getNode()); return result.getReturnValue().catchFailed(throwable -> { reporter.reportClusterNodeRpcError(); /* Actually never return;s, instead throws a new exception with added info. * The point is to get Node identifying information into the exception */ throw new RuntimeNodeException(result.getNode().toString(), throwable.getMessage(), throwable); }).catchCancelled(ignore -> { reporter.reportClusterNodeRpcCancellation(); /* In case of the future being cancelled, we should note it as a node exception * and try with the next node in the shard. * It seems like we can get cancellations when there are network issues. */ throw new RuntimeNodeException(result.getNode().toString(), "Operation cancelled"); }); }, iteratorPolicy) .directTransform(retryResult -> handleRetryTraceFn.apply(retryResult.getResult(), queryTracesFromRetries(retryResult.getErrors(), retryResult.getBackoffTimings()))); } public List<String> getNodesAsStringList() { final List<String> nodes = cluster .getNodesForShard(shard) .stream() .map(Object::toString) .collect(Collectors.toList()); return nodes; } private List<QueryTrace> queryTracesFromRetries( final List<RetryException> retryExceptions, final List<Long> backoffTimings ) { final List<QueryTrace> traces = new ArrayList<>(); long lastTS = 0; final Iterator<Long> backoffIterator = backoffTimings.iterator(); for (final RetryException re : retryExceptions) { /* For each RetryException, add a QueryTrace in the current shard with information about * the cause and elapsed time */ final long microTS = TimeUnit.MICROSECONDS.convert(re.getOffsetMillis(), TimeUnit.MILLISECONDS); long elapsed = microTS - lastTS; traces.add(QueryTrace.of(new QueryTrace.Identifier(getMessageFrom(re)), elapsed)); lastTS = microTS; /* After each retry, a backoff pause is inserted before trying the next node. * Here we add a QueryTrace to represent this, including the duration of the pause */ if (!backoffIterator.hasNext()) { continue; } final long backoffTS = TimeUnit.MICROSECONDS.convert(backoffIterator.next(), TimeUnit.MILLISECONDS); elapsed = backoffTS - lastTS; traces.add(QueryTrace.of(RETRY_BACKOFF, elapsed)); lastTS = backoffTS; } return traces; } private String getMessageFrom(final Throwable throwable) { final Throwable cause = throwable.getCause(); if (cause instanceof RuntimeNodeException) { final RuntimeNodeException rne = (RuntimeNodeException) cause; return rne.getUri() + " error=" + cause.getMessage(); } return "error=" + cause.getMessage(); } }