/** * 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.pinterest.terrapin.thrift; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.pinterest.terrapin.Constants; import com.pinterest.terrapin.TerrapinUtil; import com.pinterest.terrapin.client.FileSetViewManager; import com.pinterest.terrapin.client.ReplicatedTerrapinClient; import com.pinterest.terrapin.client.TerrapinClient; import com.pinterest.terrapin.thrift.generated.RequestOptions; import com.pinterest.terrapin.thrift.generated.TerrapinGetErrorCode; import com.pinterest.terrapin.thrift.generated.TerrapinGetException; import com.pinterest.terrapin.thrift.generated.TerrapinGetRequest; import com.pinterest.terrapin.thrift.generated.TerrapinMultiGetRequest; import com.pinterest.terrapin.thrift.generated.TerrapinResponse; import com.pinterest.terrapin.thrift.generated.TerrapinService; import com.pinterest.terrapin.thrift.generated.TerrapinSingleResponse; import com.twitter.common.zookeeper.ZooKeeperClient; import com.twitter.ostrich.stats.Stats; import com.twitter.util.ExceptionalFunction; import com.twitter.util.Function0; import com.twitter.util.Function; import com.twitter.util.Future; import org.apache.commons.configuration.PropertiesConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.runtime.BoxedUnit; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; /** * Implements a thrift server on top of the terrapin client. */ public class TerrapinServiceImpl implements TerrapinService.ServiceIface { private static final Logger LOG = LoggerFactory.getLogger(TerrapinServiceImpl.class); // Since each TerrapinClient is per cluster, the thrift server will instantiate // TerrapinClients at startup. The list will remain static throughout the life // time of the thrift server. private final Map<String, TerrapinClient> clusterClientMap; /** * @param configuration Config settings containing settings such * @throws Exception */ public TerrapinServiceImpl(PropertiesConfiguration configuration, List<String> clusterList) throws Exception { String zkQuorum = TerrapinUtil.getZKQuorumFromConf(configuration); Preconditions.checkNotNull(zkQuorum, "Zookeeper quorum should not be empty/null."); ZooKeeperClient zkClient = TerrapinUtil.getZooKeeperClient(zkQuorum, 30); this.clusterClientMap = Maps.newHashMapWithExpectedSize(clusterList.size()); for (String clusterName : clusterList) { try { FileSetViewManager fsViewManager = new FileSetViewManager(zkClient, clusterName); LOG.info("Connecting to cluster " + clusterName + " on " + zkQuorum); TerrapinClient terrapinClient = new TerrapinClient( fsViewManager, clusterName, configuration.getInt(Constants.TERRAPIN_SERVER_TARGET_PORT, Constants.DEFAULT_THRIFT_PORT), configuration.getInt(Constants.CLIENT_CONNECT_TIMEOUT_MILLIS, 300), configuration.getInt(Constants.CLIENT_RPC_TIMEOUT_MILLIS, 500)); clusterClientMap.put(clusterName, terrapinClient); LOG.info("Done."); } catch (Exception e) { LOG.warn("Could not connect to cluster " + clusterName, e); throw e; } } } TerrapinServiceImpl(Map<String, TerrapinClient> clusterClientMap) { this.clusterClientMap = clusterClientMap; } private <T> Future<T> getExceptionFuture(Throwable t) { if (t instanceof TerrapinGetException) { return Future.exception(t); } return Future.exception(new TerrapinGetException("Failed due to " + t.toString(), TerrapinGetErrorCode.OTHER)); } private ReplicatedTerrapinClient getReplicatedTerrapinClient(List<String> clusterList) { String firstCluster = clusterList.get(0); String secondCluster = null; if (clusterList.size() > 1) { secondCluster = clusterList.get(1); } TerrapinClient firstClient = clusterClientMap.get(firstCluster); TerrapinClient secondClient = null; if (secondCluster != null) { secondClient = clusterClientMap.get(secondCluster); } if(firstClient == null && secondClient == null) { return null; } return new ReplicatedTerrapinClient(firstClient, secondClient); } @Override public Future<TerrapinSingleResponse> get(final TerrapinGetRequest request) { final long startTimeMillis = System.currentTimeMillis(); if (request.getClusterList().isEmpty()) { return Future.exception(new TerrapinGetException("Cluster list is empty", TerrapinGetErrorCode.INVALID_REQUEST)); } ReplicatedTerrapinClient terrapinClient = getReplicatedTerrapinClient(request.getClusterList()); if (terrapinClient == null) { return Future.exception(new TerrapinGetException( "Clusters [" + Joiner.on(", ").join(request.getClusterList()) + "] not found.", TerrapinGetErrorCode.CLUSTER_NOT_FOUND)); } RequestOptions options; if (request.isSetOptions()) { options = request.getOptions(); } else { options = new RequestOptions(); } try { return terrapinClient.getMany(request.getFileSet(), Sets.newHashSet(ByteBuffer.wrap(request.getKey())), options).map( new ExceptionalFunction<TerrapinResponse, TerrapinSingleResponse>() { @Override public TerrapinSingleResponse applyE(TerrapinResponse response) throws TerrapinGetException { ByteBuffer keyBuf = ByteBuffer.wrap(request.getKey()); if (response.getResponseMap().containsKey(keyBuf)) { TerrapinSingleResponse returnResponse = response.getResponseMap().get(keyBuf); if (returnResponse.isSetErrorCode()) { throw new TerrapinGetException("Read failed.", returnResponse.getErrorCode()); } else { Stats.addMetric(request.getFileSet() + "-value-size", returnResponse.getValue().length); Stats.addMetric("value-size", returnResponse.getValue().length); return returnResponse; } } else { return new TerrapinSingleResponse(); } } }).rescue(new Function<Throwable, Future<TerrapinSingleResponse>>() { @Override public Future<TerrapinSingleResponse> apply(Throwable t) { return getExceptionFuture(t); } }).ensure(new Function0<BoxedUnit>() { @Override public BoxedUnit apply() { int timeMillis = (int)(System.currentTimeMillis() - startTimeMillis); Stats.addMetric(request.getFileSet() + "-lookup-latency-ms", timeMillis); Stats.addMetric("lookup-latency-ms", timeMillis); return BoxedUnit.UNIT; } }); } catch (Exception e) { return getExceptionFuture(e); } } @Override public Future<TerrapinResponse> multiGet(final TerrapinMultiGetRequest request) { final long startTimeMillis = System.currentTimeMillis(); if (request.getClusterList().isEmpty()) { return Future.exception(new TerrapinGetException("Cluster list is empty", TerrapinGetErrorCode.INVALID_REQUEST)); } ReplicatedTerrapinClient terrapinClient = getReplicatedTerrapinClient(request.getClusterList()); if (terrapinClient == null) { return Future.exception(new TerrapinGetException( "Clusters [" + Joiner.on(", ").join(request.getClusterList()) + "] not found.", TerrapinGetErrorCode.CLUSTER_NOT_FOUND)); } RequestOptions options; if (request.isSetOptions()) { options = request.getOptions(); } else { options = new RequestOptions(); } try { return terrapinClient.getMany(request.getFileSet(), Sets.newHashSet(request.getKeyList()), options) .onSuccess(new Function<TerrapinResponse, BoxedUnit>() { @Override public BoxedUnit apply(TerrapinResponse terrapinResponse) { int responseSize = 0; for (Map.Entry<ByteBuffer, TerrapinSingleResponse> response : terrapinResponse.getResponseMap().entrySet()) { if (!response.getValue().isSetErrorCode()) { responseSize += response.getValue().getValue().length; } } Stats.addMetric(request.getFileSet() + "-multi-value-size", responseSize); Stats.addMetric("multi-value-size", responseSize); return BoxedUnit.UNIT; } }) .rescue(new Function<Throwable, Future<TerrapinResponse>>() { @Override public Future<TerrapinResponse> apply(Throwable t) { return getExceptionFuture(t); } }) .ensure(new Function0<BoxedUnit>() { @Override public BoxedUnit apply() { int timeMillis = (int) (System.currentTimeMillis() - startTimeMillis); Stats.addMetric(request.getFileSet() + "-multi-lookup-latency-ms", timeMillis); Stats.addMetric("multi-lookup-latency-ms", timeMillis); return BoxedUnit.UNIT; } }); } catch (Exception e) { return getExceptionFuture(e); } } }