blob: c3575ac4ce32835fa03d0e37224ef9dfc18beb9c [file] [log] [blame]
/*
* 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.cloud;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.io.FileUtils;
import org.apache.jute.InputArchive;
import org.apache.jute.OutputArchive;
import org.apache.jute.Record;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient.RemoteSolrException;
import org.apache.solr.client.solrj.request.ConfigSetAdminRequest.Create;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkConfigManager;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.ConfigSetProperties;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.server.DataNode;
import org.apache.zookeeper.server.DataTree;
import org.apache.zookeeper.server.DataTree.ProcessTxnResult;
import org.apache.zookeeper.server.Request;
import org.apache.zookeeper.server.ServerCnxn;
import org.apache.zookeeper.server.ZKDatabase;
import org.apache.zookeeper.server.quorum.Leader.Proposal;
import org.apache.zookeeper.txn.TxnDigest;
import org.apache.zookeeper.txn.TxnHeader;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.apache.solr.common.cloud.ZkConfigManager.CONFIGS_ZKNODE;
/**
* Test the ConfigSets API under ZK failure. In particular,
* if create fails, ensure proper cleanup occurs so we aren't
* left with a partially created ConfigSet.
*/
public class TestConfigSetsAPIZkFailure extends SolrTestCaseJ4 {
private MiniSolrCloudCluster solrCluster;
private ZkTestServer zkTestServer;
private static final String BASE_CONFIGSET_NAME = "baseConfigSet1";
private static final String CONFIGSET_NAME = "configSet1";
@Override
@Before
public void setUp() throws Exception {
super.setUp();
final Path testDir = createTempDir();
final Path zkDir = testDir.resolve("zookeeper/server1/data");
zkTestServer = new ZkTestServer(zkDir);
zkTestServer.run();
zkTestServer.setZKDatabase(new FailureDuringCopyZKDatabase(zkTestServer.getZKDatabase(), zkTestServer));
solrCluster = new MiniSolrCloudCluster(1, testDir,
MiniSolrCloudCluster.DEFAULT_CLOUD_SOLR_XML, buildJettyConfig("/solr"), zkTestServer);
}
@Override
@After
public void tearDown() throws Exception {
if (null != solrCluster) {
solrCluster.shutdown();
solrCluster = null;
}
if (null != zkTestServer) {
zkTestServer.shutdown();
zkTestServer = null;
}
super.tearDown();
}
@Test
public void testCreateZkFailure() throws Exception {
final String baseUrl = solrCluster.getJettySolrRunners().get(0).getBaseUrl().toString();
final SolrClient solrClient = getHttpSolrClient(baseUrl);
final Map<String, String> oldProps = ImmutableMap.of("immutable", "true");
setupBaseConfigSet(BASE_CONFIGSET_NAME, oldProps);
SolrZkClient zkClient = new SolrZkClient(solrCluster.getZkServer().getZkAddress(),
AbstractZkTestCase.TIMEOUT, AbstractZkTestCase.TIMEOUT, null);
try {
ZkConfigManager configManager = new ZkConfigManager(zkClient);
assertFalse(configManager.configExists(CONFIGSET_NAME));
Create create = new Create();
create.setBaseConfigSetName(BASE_CONFIGSET_NAME).setConfigSetName(CONFIGSET_NAME);
RemoteSolrException se = expectThrows(RemoteSolrException.class, () -> create.process(solrClient));
// partial creation should have been cleaned up
assertFalse(configManager.configExists(CONFIGSET_NAME));
assertEquals(SolrException.ErrorCode.SERVER_ERROR.code, se.code());
} finally {
zkClient.close();
}
solrClient.close();
}
private void setupBaseConfigSet(String baseConfigSetName, Map<String, String> oldProps) throws Exception {
final File configDir = getFile("solr").toPath().resolve("configsets/configset-2/conf").toFile();
final File tmpConfigDir = createTempDir().toFile();
tmpConfigDir.deleteOnExit();
FileUtils.copyDirectory(configDir, tmpConfigDir);
if (oldProps != null) {
FileUtils.write(new File(tmpConfigDir, ConfigSetProperties.DEFAULT_FILENAME),
getConfigSetProps(oldProps), StandardCharsets.UTF_8);
}
solrCluster.uploadConfigSet(tmpConfigDir.toPath(), baseConfigSetName);
solrCluster.getZkClient().setData("/configs/" + baseConfigSetName, "{\"trusted\": false}".getBytes(StandardCharsets.UTF_8), true);
}
private StringBuilder getConfigSetProps(Map<String, String> map) {
return new StringBuilder(new String(Utils.toJSON(map), StandardCharsets.UTF_8));
}
private static class FailureDuringCopyZKDatabase extends ForwardingZKDatabase {
private final ZkTestServer zkTestServer;
public FailureDuringCopyZKDatabase(ZKDatabase zkdb, ZkTestServer zkTestServer) {
super(zkdb);
this.zkTestServer = zkTestServer;
}
@Override
public byte[] getData(String path, Stat stat, Watcher watcher) throws KeeperException.NoNodeException {
// we know we are doing a copy when we are getting data from the base config set and
// the new config set (partially) exists
String zkAddress = zkTestServer.getZkAddress();
String chroot = zkAddress.substring(zkAddress.lastIndexOf("/"));
if (path.startsWith(chroot + CONFIGS_ZKNODE + "/" + BASE_CONFIGSET_NAME)
&& !path.contains(ConfigSetProperties.DEFAULT_FILENAME)) {
List<String> children = null;
try {
children = getChildren(chroot + CONFIGS_ZKNODE + "/" + CONFIGSET_NAME, null, null);
} catch (KeeperException.NoNodeException e) {}
if (children != null && children.size() > 0) {
throw new RuntimeException("sample zookeeper error");
}
}
return super.getData(path, stat, watcher);
}
}
private static class ForwardingZKDatabase extends ZKDatabase {
private ZKDatabase zkdb;
public ForwardingZKDatabase(ZKDatabase zkdb) {
super(null);
this.zkdb = zkdb;
}
@Override
public boolean isInitialized() {
return zkdb.isInitialized();
}
@Override
public void clear() {
zkdb.clear();
}
@Override
public DataTree getDataTree() {
return zkdb.getDataTree();
}
@Override
public long getmaxCommittedLog() {
return zkdb.getmaxCommittedLog();
}
@Override
public long getminCommittedLog() {
return zkdb.getminCommittedLog();
}
@Override
public ReentrantReadWriteLock getLogLock() {
return zkdb.getLogLock();
}
@Override
public synchronized Collection<Proposal> getCommittedLog() {
return zkdb.getCommittedLog();
}
@Override
public long getDataTreeLastProcessedZxid() {
return zkdb.getDataTreeLastProcessedZxid();
}
@Override
public Collection<Long> getSessions() {
return zkdb.getSessions();
}
@Override
public ConcurrentHashMap<Long, Integer> getSessionWithTimeOuts() {
return zkdb.getSessionWithTimeOuts();
}
@Override
public long loadDataBase() throws IOException {
return zkdb.loadDataBase();
}
@Override
public void addCommittedProposal(Request request) {
zkdb.addCommittedProposal(request);
}
@Override
public void removeCnxn(ServerCnxn cnxn) {
zkdb.removeCnxn(cnxn);
}
@Override
public void killSession(long sessionId, long zxid) {
zkdb.killSession(sessionId, zxid);
}
@Override
public void dumpEphemerals(PrintWriter pwriter) {
zkdb.dumpEphemerals(pwriter);
}
@Override
public int getNodeCount() {
return zkdb.getNodeCount();
}
@Override
public Set<String> getEphemerals(long sessionId) {
return zkdb.getEphemerals(sessionId);
}
@Override
public void setlastProcessedZxid(long zxid) {
zkdb.setlastProcessedZxid(zxid);
}
@Override
public ProcessTxnResult processTxn(TxnHeader hdr, Record txn, TxnDigest digest) {
return zkdb.processTxn(hdr, txn, digest);
}
@Override
public Stat statNode(String path, ServerCnxn serverCnxn) throws KeeperException.NoNodeException {
return zkdb.statNode(path, serverCnxn);
}
@Override
public DataNode getNode(String path) {
return zkdb.getNode(path);
}
@Override
public List<ACL> aclForNode(DataNode n) {
return zkdb.aclForNode(n);
}
@Override
public byte[] getData(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
return zkdb.getData(path, stat, watcher);
}
@Override
public void setWatches(long relativeZxid, List<String> dataWatches,
List<String> existWatches, List<String> childWatches,
List<String> persistentWatches,
List<String> persistentRecursiveWatches,
Watcher watcher) {
zkdb.setWatches(relativeZxid, dataWatches, existWatches, childWatches,
persistentWatches, persistentRecursiveWatches, watcher);
}
@Override
public List<ACL> getACL(String path, Stat stat) throws NoNodeException {
return zkdb.getACL(path, stat);
}
@Override
public List<String> getChildren(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
return zkdb.getChildren(path, stat, watcher);
}
@Override
public boolean isSpecialPath(String path) {
return zkdb.isSpecialPath(path);
}
@Override
public int getAclSize() {
return zkdb.getAclSize();
}
@Override
public boolean truncateLog(long zxid) throws IOException {
return zkdb.truncateLog(zxid);
}
@Override
public void deserializeSnapshot(InputArchive ia) throws IOException {
zkdb.deserializeSnapshot(ia);
}
@Override
public void serializeSnapshot(OutputArchive oa) throws IOException,
InterruptedException {
zkdb.serializeSnapshot(oa);
}
@Override
public boolean append(Request si) throws IOException {
return zkdb.append(si);
}
@Override
public void rollLog() throws IOException {
zkdb.rollLog();
}
@Override
public void commit() throws IOException {
zkdb.commit();
}
@Override
public void close() throws IOException {
zkdb.close();
}
@Override
public int getTxnCount() {
return zkdb.getTxnCount();
}
@Override
public long getTxnSize() {
return zkdb.getTxnSize();
}
}
}