Skip to content
This repository has been archived by the owner on Jul 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #95 from lzoubek/bugs/1074632
Browse files Browse the repository at this point in the history
Bug 1074632 - RFE: Manage storage node snapshots
  • Loading branch information
jsanda committed Jul 23, 2014
2 parents 181e5e7 + 2ac7010 commit fe30df2
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 10 deletions.
5 changes: 5 additions & 0 deletions modules/plugins/cassandra/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
import org.rhq.core.util.StringUtil;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.plugins.cassandra.util.KeyspaceService;
import org.rhq.plugins.cassandra.util.TakeSnapshotOperation;
import org.rhq.plugins.jmx.JMXServerComponent;

/**
Expand Down Expand Up @@ -157,6 +159,8 @@ public OperationResult invokeOperation(String name, Configuration parameters) th
return restartNode();
} else if (name.equals("updateSeedsList")) {
return updateSeedsList(parameters);
} else if (name.equals("takeSnapshot")) {
return new TakeSnapshotOperation(new KeyspaceService(getEmsConnection()), parameters).invoke();
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@

package org.rhq.plugins.cassandra.util;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.mc4j.ems.connection.bean.operation.EmsOperation;

/**
Expand Down Expand Up @@ -97,10 +102,48 @@ public void compact(String keyspace, String... columnFamilies) {
}

public void takeSnapshot(String keyspace, String snapshotName) {
takeSnapshot(new String[]{keyspace}, snapshotName);
}

public void takeSnapshot(String[] keySpaces, String snapshotName) {
EmsBean emsBean = loadBean(STORAGE_SERVICE_BEAN);
EmsOperation operation = emsBean.getOperation(SNAPSHOT_OPERATION, String.class, String[].class);
operation.invoke(snapshotName, keySpaces);
}

public String[] getKeySpaceDataFileLocations() {
EmsBean emsBean = loadBean(KeyspaceService.STORAGE_SERVICE_BEAN);
EmsAttribute attribute = emsBean.getAttribute("AllDataFileLocations");
return (String[]) attribute.getValue();
}

@SuppressWarnings("unchecked")
public List<Object> getKeyspaces() {
List<Object> value = null;

operation.invoke(snapshotName, new String[] {keyspace});
EmsBean emsBean = loadBean(STORAGE_SERVICE_BEAN);
if (emsBean != null) {
EmsAttribute attribute = emsBean.getAttribute("Keyspaces");
if (attribute != null) {
value = (List<Object>) attribute.refresh();
}
}
if (value == null) {
value = new ArrayList<Object>();
}

return value;
}

public List<String> getColumnFamilyDirs() {
List<EmsBean> beans = emsConnection.queryBeans("org.apache.cassandra.db:type=ColumnFamilies,*");
List<String> dirs = new ArrayList<String>(beans.size());
for (EmsBean bean : beans) {
String keySpace = bean.getBeanName().getKeyProperty("keyspace");
String colFamily = bean.getBeanName().getKeyProperty("columnfamily");
dirs.add(keySpace + File.separator + colFamily);
}
return dirs;
}

private EmsBean loadBean(String objectName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
*
* * RHQ Management Platform
* * Copyright (C) 2005-2014 Red Hat, Inc.
* * All rights reserved.
* *
* * This program is free software; you can redistribute it and/or modify
* * it under the terms of the GNU General Public License, version 2, as
* * published by the Free Software Foundation, and/or the GNU Lesser
* * General Public License, version 2.1, also as published by the Free
* * Software Foundation.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License and the GNU Lesser General Public License
* * for more details.
* *
* * You should have received a copy of the GNU General Public License
* * and the GNU Lesser General Public License along with this program;
* * if not, write to the Free Software Foundation, Inc.,
* * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package org.rhq.plugins.cassandra.util;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.pluginapi.operation.OperationResult;

public class TakeSnapshotOperation {

private final String R_STRATEGY_KEEP_ALL = "Keep All";
private final String R_STRATEGY_KEEP_LASTN = "Keep Last N";
private final String R_STRATEGY_DEL_OLDERN = "Delete Older Than N days";

private final String D_STRATEGY_DEL = "Delete";
private final String D_STRATEGY_MOVE = "Move";

private final KeyspaceService service;
private final Configuration parameters;

private final Log log = LogFactory.getLog(TakeSnapshotOperation.class);

public TakeSnapshotOperation(KeyspaceService service, Configuration parameters) {
this.service = service;
this.parameters = parameters;
}

public OperationResult invoke() {
OperationResult result = new OperationResult();
String snapshotName = parameters.getSimpleValue("snapshotName", "" + System.currentTimeMillis()).trim();
if (snapshotName.isEmpty()) {
result.setErrorMessage("Snapshot Name parameter cannot be an empty string");
return result;
}
String retentionStrategy = parameters.getSimpleValue("retentionStrategy", R_STRATEGY_KEEP_ALL);
Integer count = parameters.getSimple("count").getIntegerValue();
String deletionStrategy = parameters.getSimpleValue("deletionStrategy", D_STRATEGY_DEL);
String location = parameters.getSimpleValue("location");

// validate parameters
if (R_STRATEGY_KEEP_LASTN.equals(retentionStrategy) || R_STRATEGY_DEL_OLDERN.equals(retentionStrategy)) {
if (count == null) {
result.setErrorMessage("Invalid input parameters. Selected Retention Strategy [" + retentionStrategy
+ "] but 'count' parameter was null");
return result;
}
}
if (D_STRATEGY_MOVE.equals(deletionStrategy)) {
if (location == null) {
result.setErrorMessage("Invalid input parameters. Selected Deletion Strategy [" + deletionStrategy
+ "] but 'location' parameter was null");
return result;
}
File locationDir = new File(location);
if (!locationDir.exists()) {
try {
if (!locationDir.mkdirs()) {
result.setErrorMessage("Location [" + locationDir.getAbsolutePath()
+ "] did not exist and failed to be created");
return result;
}
} catch (Exception e) {
result.setErrorMessage("Location [" + locationDir.getAbsolutePath()
+ "] did not exist and failed to be created - " + e.getMessage());
return result;
}
}
if (!locationDir.isDirectory()) {
result.setErrorMessage("Location [" + locationDir.getAbsolutePath() + "] must be directory");
return result;
}
if (!locationDir.canWrite()) {
result.setErrorMessage("Location [" + locationDir.getAbsolutePath() + "] must be writable");
}
}

String[] keyspaces = service.getKeyspaces().toArray(new String[] {});
log.info("Taking snapshot of keyspaces " + Arrays.toString(keyspaces));
long startTime = System.currentTimeMillis();
service.takeSnapshot(keyspaces, snapshotName);
log.info("Snapshot taken in " + (System.currentTimeMillis() - startTime) + "ms");

if (R_STRATEGY_KEEP_ALL.equals(retentionStrategy)) { // do nothing
return result;
}

List<String> columnFamilyDirs = service.getColumnFamilyDirs();

// obtain list of snapshot dirs to be moved or deleted
List<String[]> eligibleSnapshots = findEligibleSnapshots(service.getKeySpaceDataFileLocations(),
columnFamilyDirs, retentionStrategy, count);

if (eligibleSnapshots.isEmpty()) {
return result;
}
if (D_STRATEGY_DEL.equals(deletionStrategy)) {
log.info("Strategy [" + deletionStrategy + "] is set, deleting " + eligibleSnapshots.size() + " snapshots");
for (String[] snapPath : eligibleSnapshots) {
File snapDir = new File(snapPath[0], snapPath[1]);
log.info("Deleting " + snapDir);
if (!FileUtils.deleteQuietly(snapDir)) {
log.warn("Failed to delete " + snapDir.getAbsolutePath());
}
}
}
if (D_STRATEGY_MOVE.equals(deletionStrategy)) {
log.info("Strategy [" + deletionStrategy + "] is set, moving " + eligibleSnapshots.size() + " snapshots");
for (String[] snapPath : eligibleSnapshots) {
File snapDir = new File(snapPath[0], snapPath[1]);
File snapTargetDir = new File(location, snapPath[1]);
log.info("Moving " + snapDir + " to " + snapTargetDir);
try {
FileUtils.moveDirectoryToDirectory(snapDir, snapTargetDir.getParentFile(), true);
} catch (IOException e) {
log.warn("Failed to move directory : " + e.getMessage());
}
}
}
return result;
}

/**
* find eligible snapshot dirs - to be deleted or moved. Dirs are returned as pairs, we need this - in case 'Move' is required we'll have to create
* snapshot subdirectory relative to target location
* @param dataDirs root dataDirs
* @param colFamilyDirs relative paths to dataDirs
* @param retentionStrategy
* @param count
* @return list of pairs <dataDir,snapShotDir>
*/
private List<String[]> findEligibleSnapshots(String[] dataDirs, List<String> colFamilyDirs,
String retentionStrategy, Integer count) {
List<String[]> eligibleSnapshots = new ArrayList<String[]>();
for (String dataRoot : dataDirs) {
for (String keySpace : colFamilyDirs) {
String colFamilyDir = keySpace + File.separator + "snapshots";
File keySpaceDir = new File(dataRoot, colFamilyDir);
if (keySpaceDir.exists()) { // might not exist in case there are several dataRoots
File[] snapshots = keySpaceDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
// sort, newest first
Arrays.sort(snapshots, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.lastModified() > o2.lastModified() ? -1 : 1;
}
});
if (R_STRATEGY_KEEP_LASTN.equals(retentionStrategy)) {
int index = 0;
for (File f : snapshots) {
if (index >= count) {
eligibleSnapshots.add(new String[] { dataRoot,
colFamilyDir + File.separator + f.getName() });
}
index++;
}
}
if (R_STRATEGY_DEL_OLDERN.equals(retentionStrategy)) {
long cutOff = System.currentTimeMillis() - (count * 86400L * 1000L);
for (File f : snapshots) {
if (f.lastModified() < cutOff) {
eligibleSnapshots.add(new String[] { dataRoot,
colFamilyDir + File.separator + f.getName() });
}
}
}
}
}
}
return eligibleSnapshots;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@
</c:list-property>
</parameters>
</operation>

<operation name="takeSnapshot"
description="Takes a snapshot of all keyspaces. A snapshot first flushes all in-memory writes to disk and then creates a hard
link of each SSTable file for the keyspace. Note that a column family can have multiple
SSTables on disk. By default snapshots are stored in the &lt;cassandra_data_dir&gt;/&lt;keyspace_name&gt;/&lt;column_family_name&gt;/snapshots
directory. On Linux/UNIX systems cassandra_data_dir defaults to /var/lib/cassandra/data">
<parameters>
<c:simple-property name="snapshotName" required="false" type="string" displayName="Snapshot Name" description="Snapshot name. If left empty current system time in milliseconds will be used as the snapshot name."/>
<c:simple-property name="retentionStrategy" required="true" type="string" default="Keep All" description="Retention Strategy defines what happens to snapshots after being created. Default is Keep All - nothing is cleaned up, Deletion Strategy is ignored. Keep Last N - newest N snaphosts remain, the rest of snapshots is cleaned up based on Deletion Strategy, N has to be specified using 'count' property. Delete Older Than N days - delete snapshots older than N days, rest of snapshots is cleaned up based on Deletion Strategy, N has to be specified using 'count' property">
<c:property-options>
<c:option value="Keep All"/>
<c:option value="Keep Last N"/>
<c:option value="Delete Older Than N days"/>
</c:property-options>
</c:simple-property>
<c:simple-property name="count" required="false" type="integer" description="Additional parameter to Retention Strategy, stands for N" />
<c:simple-property name="deletionStrategy" required="true" type="string" default="Delete" description="Deletion Strategy defines what happens to snapshots that don't match Retention Strategy. Default is Delete - snapshots will be deleted. Move - snapshots are moved to location specified by 'location' property.">
<c:property-options>
<c:option value="Delete"/>
<c:option value="Move"/>
</c:property-options>
</c:simple-property>
<c:simple-property name="location" required="false" type="string" description="Additional parameter to Deletion Strategy, specifies location to move snaphosts to, snads for L" />
</parameters>
</operation>

<server name="Cassandra Server JVM"
sourcePlugin="JMX" sourceType="JMX Server"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,9 @@ private Set<String> getAddreses(Configuration params) {
}

private void createSnapshots(Set<String> addressesToAdd, String snapshotPrefix) {
EmsConnection emsConnection = getEmsConnection();
KeyspaceService keyspaceService = new KeyspaceService(emsConnection);
keyspaceService.takeSnapshot(SYSTEM_KEYSPACE, snapshotPrefix + System.currentTimeMillis());
keyspaceService.takeSnapshot(SYSTEM_AUTH_KEYSPACE, snapshotPrefix + System.currentTimeMillis());
keyspaceService.takeSnapshot(RHQ_KEYSPACE, snapshotPrefix + System.currentTimeMillis());
KeyspaceService keyspaceService = new KeyspaceService(getEmsConnection());
keyspaceService.takeSnapshot(new String[] { SYSTEM_KEYSPACE, SYSTEM_AUTH_KEYSPACE, RHQ_KEYSPACE },
snapshotPrefix + System.currentTimeMillis());
}

private void reloadInternodeAuthConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,31 @@
</parameters>
</operation>

<operation name="takeSnapshot"
description="Takes a snapshot of all keyspaces. A snapshot first flushes all in-memory writes to disk and then creates a hard
link of each SSTable file for the keyspace. Note that a column family can have multiple
SSTables on disk. By default snapshots are stored in the &lt;cassandra_data_dir&gt;/&lt;keyspace_name&gt;/&lt;column_family_name&gt;/snapshots
directory. On Linux/UNIX systems cassandra_data_dir defaults to /var/lib/cassandra/data">
<parameters>
<c:simple-property name="snapshotName" required="false" type="string" displayName="Snapshot Name" description="Snapshot name. If left empty current system time in milliseconds will be used as the snapshot name."/>
<c:simple-property name="retentionStrategy" required="true" type="string" default="Keep All" description="Retention Strategy defines what happens to snapshots after being created. Default is Keep All - nothing is cleaned up, Deletion Strategy is ignored. Keep Last N - newest N snaphosts remain, the rest of snapshots is cleaned up based on Deletion Strategy, N has to be specified using 'count' property. Delete Older Than N days - delete snapshots older than N days, rest of snapshots is cleaned up based on Deletion Strategy, N has to be specified using 'count' property">
<c:property-options>
<c:option value="Keep All"/>
<c:option value="Keep Last N"/>
<c:option value="Delete Older Than N days"/>
</c:property-options>
</c:simple-property>
<c:simple-property name="count" required="false" type="integer" description="Additional parameter to Retention Strategy, stands for N" />
<c:simple-property name="deletionStrategy" required="true" type="string" default="Delete" description="Deletion Strategy defines what happens to snapshots that don't match Retention Strategy. Default is Delete - snapshots will be deleted. Move - snapshots are moved to location specified by 'location' property.">
<c:property-options>
<c:option value="Delete"/>
<c:option value="Move"/>
</c:property-options>
</c:simple-property>
<c:simple-property name="location" required="false" type="string" description="Additional parameter to Deletion Strategy, specifies location to move snaphosts to, snads for L" />
</parameters>
</operation>

<operation name="updateConfiguration" description="Updates the node configuration. Will require a separate server restart for the settings to take effect.">
<parameters>
<c:simple-property name="jmxPort" type="integer" required="false" description="JMX port JVM option."/>
Expand Down

0 comments on commit fe30df2

Please sign in to comment.