/* * 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 org.apache.solr.update; import static org.apache.solr.update.processor.DistributedUpdateProcessor.DistribPhase.FROMLEADER; import static org.apache.solr.update.processor.DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM; import java.io.IOException; import java.net.ConnectException; import java.net.SocketException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.http.NoHttpResponseException; import org.apache.http.client.HttpClient; import org.apache.http.conn.ConnectTimeoutException; import org.apache.lucene.util.BytesRef; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.cloud.ZkController; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.SolrCore; import org.apache.solr.handler.component.HttpShardHandlerFactory; import org.apache.solr.handler.component.ShardHandler; import org.apache.solr.handler.component.ShardHandlerFactory; import org.apache.solr.handler.component.ShardRequest; import org.apache.solr.handler.component.ShardResponse; import org.apache.solr.request.LocalSolrQueryRequest; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.update.processor.UpdateRequestProcessor; import org.apache.solr.update.processor.UpdateRequestProcessorChain; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** @lucene.experimental */ public class PeerSync { public static Logger log = LoggerFactory.getLogger(PeerSync.class); public boolean debug = log.isDebugEnabled(); private List<String> replicas; private int nUpdates; private int maxUpdates; // maximum number of updates to request before failing private UpdateHandler uhandler; private UpdateLog ulog; private HttpShardHandlerFactory shardHandlerFactory; private ShardHandler shardHandler; private UpdateLog.RecentUpdates recentUpdates; private List<Long> startingVersions; private List<Long> ourUpdates; private Set<Long> ourUpdateSet; private Set<Long> requestedUpdateSet; private long ourLowThreshold; // 20th percentile private long ourHighThreshold; // 80th percentile private final boolean cantReachIsSuccess; private final boolean getNoVersionsIsSuccess; private final HttpClient client; private final boolean onlyIfActive; // comparator that sorts by absolute value, putting highest first private static Comparator<Long> absComparator = new Comparator<Long>() { @Override public int compare(Long o1, Long o2) { long l1 = Math.abs(o1); long l2 = Math.abs(o2); if (l1 >l2) return -1; if (l1 < l2) return 1; return 0; } }; // comparator that sorts update records by absolute value of version, putting lowest first private static Comparator<Object> updateRecordComparator = new Comparator<Object>() { @Override public int compare(Object o1, Object o2) { if (!(o1 instanceof List)) return 1; if (!(o2 instanceof List)) return -1; List lst1 = (List)o1; List lst2 = (List)o2; long l1 = Math.abs((Long)lst1.get(1)); long l2 = Math.abs((Long)lst2.get(1)); if (l1 >l2) return 1; if (l1 < l2) return -1; return 0; } }; private static class SyncShardRequest extends ShardRequest { List<Long> reportedVersions; List<Long> requestedUpdates; Exception updateException; } public PeerSync(SolrCore core, List<String> replicas, int nUpdates) { this(core, replicas, nUpdates, false, true); } public PeerSync(SolrCore core, List<String> replicas, int nUpdates, boolean cantReachIsSuccess, boolean getNoVersionsIsSuccess) { this(core, replicas, nUpdates, cantReachIsSuccess, getNoVersionsIsSuccess, false); } public PeerSync(SolrCore core, List<String> replicas, int nUpdates, boolean cantReachIsSuccess, boolean getNoVersionsIsSuccess, boolean onlyIfActive) { this.replicas = replicas; this.nUpdates = nUpdates; this.maxUpdates = nUpdates; this.cantReachIsSuccess = cantReachIsSuccess; this.getNoVersionsIsSuccess = getNoVersionsIsSuccess; this.client = core.getCoreDescriptor().getCoreContainer().getUpdateShardHandler().getHttpClient(); this.onlyIfActive = onlyIfActive; uhandler = core.getUpdateHandler(); ulog = uhandler.getUpdateLog(); // TODO: shutdown shardHandlerFactory = (HttpShardHandlerFactory) core.getCoreDescriptor().getCoreContainer().getShardHandlerFactory(); shardHandler = shardHandlerFactory.getShardHandler(client); } /** optional list of updates we had before possibly receiving new updates */ public void setStartingVersions(List<Long> startingVersions) { this.startingVersions = startingVersions; } public long percentile(List<Long> arr, float frac) { int elem = (int) (arr.size() * frac); return Math.abs(arr.get(elem)); } // start of peersync related debug messages. includes the core name for correlation. private String msg() { ZkController zkController = uhandler.core.getCoreDescriptor().getCoreContainer().getZkController(); String myURL = ""; if (zkController != null) { myURL = zkController.getBaseUrl(); } // TODO: core name turns up blank in many tests - find URL if cloud enabled? return "PeerSync: core="+uhandler.core.getName()+ " url="+myURL +" "; } /** Returns true if peer sync was successful, meaning that this core may not be considered to have the latest updates * when considering the last N updates between it and it's peers. * A commit is not performed. */ public boolean sync() { if (ulog == null) { return false; } log.info(msg() + "START replicas=" + replicas + " nUpdates=" + nUpdates); // TODO: does it ever make sense to allow sync when buffering or applying buffered? Someone might request that we do it... if (!(ulog.getState() == UpdateLog.State.ACTIVE || ulog.getState()==UpdateLog.State.REPLAYING)) { log.error(msg() + "ERROR, update log not in ACTIVE or REPLAY state. " + ulog); // return false; } if (debug) { if (startingVersions != null) { log.debug(msg() + "startingVersions=" + startingVersions.size() + " " + startingVersions); } } // Fire off the requests before getting our own recent updates (for better concurrency) // This also allows us to avoid getting updates we don't need... if we got our updates and then got their updates, they would // have newer stuff that we also had (assuming updates are going on and are being forwarded). for (String replica : replicas) { requestVersions(replica); } recentUpdates = ulog.getRecentUpdates(); try { ourUpdates = recentUpdates.getVersions(nUpdates); } finally { recentUpdates.close(); } Collections.sort(ourUpdates, absComparator); if (startingVersions != null) { if (startingVersions.size() == 0) { log.warn("no frame of reference to tell if we've missed updates"); return false; } Collections.sort(startingVersions, absComparator); ourLowThreshold = percentile(startingVersions, 0.8f); ourHighThreshold = percentile(startingVersions, 0.2f); // now make sure that the starting updates overlap our updates // there shouldn't be reorders, so any overlap will do. long smallestNewUpdate = Math.abs(ourUpdates.get(ourUpdates.size()-1)); if (Math.abs(startingVersions.get(0)) < smallestNewUpdate) { log.warn(msg() + "too many updates received since start - startingUpdates no longer overlaps with our currentUpdates"); return false; } // let's merge the lists List<Long> newList = new ArrayList<>(ourUpdates); for (Long ver : startingVersions) { if (Math.abs(ver) < smallestNewUpdate) { newList.add(ver); } } ourUpdates = newList; } else { if (ourUpdates.size() > 0) { ourLowThreshold = percentile(ourUpdates, 0.8f); ourHighThreshold = percentile(ourUpdates, 0.2f); } else { // we have no versions and hence no frame of reference to tell if we can use a peers // updates to bring us into sync log.info(msg() + "DONE. We have no versions. sync failed."); return false; } } ourUpdateSet = new HashSet<>(ourUpdates); requestedUpdateSet = new HashSet<>(ourUpdates); for(;;) { ShardResponse srsp = shardHandler.takeCompletedOrError(); if (srsp == null) break; boolean success = handleResponse(srsp); if (!success) { log.info(msg() + "DONE. sync failed"); shardHandler.cancelAll(); return false; } } log.info(msg() + "DONE. sync succeeded"); return true; } private void requestVersions(String replica) { SyncShardRequest sreq = new SyncShardRequest(); sreq.purpose = 1; sreq.shards = new String[]{replica}; sreq.actualShards = sreq.shards; sreq.params = new ModifiableSolrParams(); sreq.params.set("qt","/get"); sreq.params.set("distrib",false); sreq.params.set("getVersions",nUpdates); shardHandler.submit(sreq, replica, sreq.params); } private boolean handleResponse(ShardResponse srsp) { ShardRequest sreq = srsp.getShardRequest(); if (srsp.getException() != null) { // TODO: look at this more thoroughly - we don't want // to fail on connection exceptions, but it may make sense // to determine this based on the number of fails // // If the replica went down between asking for versions and asking for specific updates, that // shouldn't be treated as success since we counted on getting those updates back (and avoided // redundantly asking other replicas for them). if (cantReachIsSuccess && sreq.purpose == 1 && srsp.getException() instanceof SolrServerException) { Throwable solrException = ((SolrServerException) srsp.getException()) .getRootCause(); boolean connectTimeoutExceptionInChain = connectTimeoutExceptionInChain(srsp.getException()); if (connectTimeoutExceptionInChain || solrException instanceof ConnectException || solrException instanceof ConnectTimeoutException || solrException instanceof NoHttpResponseException || solrException instanceof SocketException) { log.warn(msg() + " couldn't connect to " + srsp.getShardAddress() + ", counting as success"); return true; } } if (cantReachIsSuccess && sreq.purpose == 1 && srsp.getException() instanceof SolrException && ((SolrException) srsp.getException()).code() == 503) { log.warn(msg() + " got a 503 from " + srsp.getShardAddress() + ", counting as success"); return true; } if (cantReachIsSuccess && sreq.purpose == 1 && srsp.getException() instanceof SolrException && ((SolrException) srsp.getException()).code() == 404) { log.warn(msg() + " got a 404 from " + srsp.getShardAddress() + ", counting as success. " + "Perhaps /get is not registered?"); return true; } // TODO: we should return the above information so that when we can request a recovery through zookeeper, we do // that for these nodes // TODO: at least log??? // srsp.getException().printStackTrace(System.out); log.warn(msg() + " exception talking to " + srsp.getShardAddress() + ", failed", srsp.getException()); return false; } if (sreq.purpose == 1) { return handleVersions(srsp); } else { return handleUpdates(srsp); } } // sometimes the root exception is a SocketTimeoutException, but ConnectTimeoutException // is in the chain private boolean connectTimeoutExceptionInChain(Throwable exception) { Throwable t = exception; while (true) { if (t instanceof ConnectTimeoutException) { return true; } Throwable cause = t.getCause(); if (cause != null) { t = cause; } else { return false; } } } private boolean handleVersions(ShardResponse srsp) { // we retrieved the last N updates from the replica List<Long> otherVersions = (List<Long>)srsp.getSolrResponse().getResponse().get("versions"); // TODO: how to handle short lists? SyncShardRequest sreq = (SyncShardRequest) srsp.getShardRequest(); sreq.reportedVersions = otherVersions; log.info(msg() + " Received " + otherVersions.size() + " versions from " + sreq.shards[0] ); if (otherVersions.size() == 0) { return getNoVersionsIsSuccess; } boolean completeList = otherVersions.size() < nUpdates; // do we have their complete list of updates? Collections.sort(otherVersions, absComparator); if (debug) { log.debug(msg() + " sorted versions from " + sreq.shards[0] + " = " + otherVersions); } long otherHigh = percentile(otherVersions, .2f); long otherLow = percentile(otherVersions, .8f); if (ourHighThreshold < otherLow) { // Small overlap between version windows and ours is older // This means that we might miss updates if we attempted to use this method. // Since there exists just one replica that is so much newer, we must // fail the sync. log.info(msg() + " Our versions are too old. ourHighThreshold="+ourHighThreshold + " otherLowThreshold="+otherLow); return false; } if (ourLowThreshold > otherHigh) { // Small overlap between windows and ours is newer. // Using this list to sync would result in requesting/replaying results we don't need // and possibly bringing deleted docs back to life. log.info(msg() + " Our versions are newer. ourLowThreshold="+ourLowThreshold + " otherHigh="+otherHigh); return true; } List<Long> toRequest = new ArrayList<>(); for (Long otherVersion : otherVersions) { // stop when the entries get old enough that reorders may lead us to see updates we don't need if (!completeList && Math.abs(otherVersion) < ourLowThreshold) break; if (ourUpdateSet.contains(otherVersion) || requestedUpdateSet.contains(otherVersion)) { // we either have this update, or already requested it // TODO: what if the shard we previously requested this from returns failure (because it goes // down) continue; } toRequest.add(otherVersion); requestedUpdateSet.add(otherVersion); } sreq.requestedUpdates = toRequest; if (toRequest.isEmpty()) { log.info(msg() + " Our versions are newer. ourLowThreshold="+ourLowThreshold + " otherHigh="+otherHigh); // we had (or already requested) all the updates referenced by the replica return true; } if (toRequest.size() > maxUpdates) { log.info(msg() + " Failing due to needing too many updates:" + maxUpdates); return false; } return requestUpdates(srsp, toRequest); } private boolean requestUpdates(ShardResponse srsp, List<Long> toRequest) { String replica = srsp.getShardRequest().shards[0]; log.info(msg() + "Requesting updates from " + replica + "n=" + toRequest.size() + " versions=" + toRequest); // reuse our original request object ShardRequest sreq = srsp.getShardRequest(); sreq.purpose = 0; sreq.params = new ModifiableSolrParams(); sreq.params.set("qt", "/get"); sreq.params.set("distrib", false); sreq.params.set("getUpdates", StrUtils.join(toRequest, ',')); sreq.params.set("onlyIfActive", onlyIfActive); sreq.responses.clear(); // needs to be zeroed for correct correlation to occur shardHandler.submit(sreq, sreq.shards[0], sreq.params); return true; } private boolean handleUpdates(ShardResponse srsp) { // we retrieved the last N updates from the replica List<Object> updates = (List<Object>)srsp.getSolrResponse().getResponse().get("updates"); SyncShardRequest sreq = (SyncShardRequest) srsp.getShardRequest(); if (updates.size() < sreq.requestedUpdates.size()) { log.error(msg() + " Requested " + sreq.requestedUpdates.size() + " updates from " + sreq.shards[0] + " but retrieved " + updates.size()); return false; } ModifiableSolrParams params = new ModifiableSolrParams(); params.set(DISTRIB_UPDATE_PARAM, FROMLEADER.toString()); params.set("peersync",true); // debugging SolrQueryRequest req = new LocalSolrQueryRequest(uhandler.core, params); SolrQueryResponse rsp = new SolrQueryResponse(); UpdateRequestProcessorChain processorChain = req.getCore().getUpdateProcessingChain(null); UpdateRequestProcessor proc = processorChain.createProcessor(req, rsp); Collections.sort(updates, updateRecordComparator); Object o = null; long lastVersion = 0; try { // Apply oldest updates first for (Object obj : updates) { // should currently be a List<Oper,Ver,Doc/Id> o = obj; List<Object> entry = (List<Object>)o; if (debug) { log.debug(msg() + "raw update record " + o); } int oper = (Integer)entry.get(0) & UpdateLog.OPERATION_MASK; long version = (Long) entry.get(1); if (version == lastVersion && version != 0) continue; lastVersion = version; switch (oper) { case UpdateLog.ADD: { // byte[] idBytes = (byte[]) entry.get(2); SolrInputDocument sdoc = (SolrInputDocument)entry.get(entry.size()-1); AddUpdateCommand cmd = new AddUpdateCommand(req); // cmd.setIndexedId(new BytesRef(idBytes)); cmd.solrDoc = sdoc; cmd.setVersion(version); cmd.setFlags(UpdateCommand.PEER_SYNC | UpdateCommand.IGNORE_AUTOCOMMIT); if (debug) { log.debug(msg() + "add " + cmd); } proc.processAdd(cmd); break; } case UpdateLog.DELETE: { byte[] idBytes = (byte[]) entry.get(2); DeleteUpdateCommand cmd = new DeleteUpdateCommand(req); cmd.setIndexedId(new BytesRef(idBytes)); cmd.setVersion(version); cmd.setFlags(UpdateCommand.PEER_SYNC | UpdateCommand.IGNORE_AUTOCOMMIT); if (debug) { log.debug(msg() + "delete " + cmd); } proc.processDelete(cmd); break; } case UpdateLog.DELETE_BY_QUERY: { String query = (String)entry.get(2); DeleteUpdateCommand cmd = new DeleteUpdateCommand(req); cmd.query = query; cmd.setVersion(version); cmd.setFlags(UpdateCommand.PEER_SYNC | UpdateCommand.IGNORE_AUTOCOMMIT); if (debug) { log.debug(msg() + "deleteByQuery " + cmd); } proc.processDelete(cmd); break; } default: throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown Operation! " + oper); } } } catch (IOException e) { // TODO: should this be handled separately as a problem with us? // I guess it probably already will by causing replication to be kicked off. sreq.updateException = e; log.error(msg() + "Error applying updates from " + sreq.shards + " ,update=" + o, e); return false; } catch (Exception e) { sreq.updateException = e; log.error(msg() + "Error applying updates from " + sreq.shards + " ,update=" + o, e); return false; } finally { try { proc.finish(); } catch (Exception e) { sreq.updateException = e; log.error(msg() + "Error applying updates from " + sreq.shards + " ,finish()", e); return false; } } return true; } /** Requests and applies recent updates from peers */ public static void sync(SolrCore core, List<String> replicas, int nUpdates) { ShardHandlerFactory shardHandlerFactory = core.getCoreDescriptor().getCoreContainer().getShardHandlerFactory(); ShardHandler shardHandler = shardHandlerFactory.getShardHandler(); for (String replica : replicas) { ShardRequest sreq = new ShardRequest(); sreq.shards = new String[]{replica}; sreq.params = new ModifiableSolrParams(); sreq.params.set("qt","/get"); sreq.params.set("distrib", false); sreq.params.set("getVersions",nUpdates); shardHandler.submit(sreq, replica, sreq.params); } for (String replica : replicas) { ShardResponse srsp = shardHandler.takeCompletedOrError(); } } }