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

Commit

Permalink
Bug 1074632 - RFE: Manage storage node snapshots
Browse files Browse the repository at this point in the history
Added "Take Snapshot" operation to StorageNode resource. This operation
takes several other parameters and it can either move or delete older
snapshots. Basically it allows to
 - keep N latest snapshots and move/delete older ones
 - keep snapshots not older than N days and move/delete the older ones
  • Loading branch information
Libor Zoubek committed Jul 22, 2014
1 parent 21ce2d7 commit 2ac7010
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 2ac7010

Please sign in to comment.