/*
* Copyright 2013-2016 EMC Corporation. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* or in the "license" file accompanying this file. This file 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.emc.ecs.sync.storage.cas;
import com.emc.ecs.sync.config.ConfigurationException;
import com.emc.ecs.sync.config.storage.CasConfig;
import com.emc.ecs.sync.filter.SyncFilter;
import com.emc.ecs.sync.model.ObjectMetadata;
import com.emc.ecs.sync.model.ObjectSummary;
import com.emc.ecs.sync.model.SyncObject;
import com.emc.ecs.sync.storage.AbstractStorage;
import com.emc.ecs.sync.storage.ObjectNotFoundException;
import com.emc.ecs.sync.storage.SyncStorage;
import com.emc.ecs.sync.util.*;
import com.emc.object.util.ProgressInputStream;
import com.filepool.fplibrary.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingDeque;
public class CasStorage extends AbstractStorage<CasConfig> {
private static final Logger log = LoggerFactory.getLogger(CasStorage.class);
private static final String OPERATION_FETCH_QUERY_RESULT = "CasFetchQueryResult";
private static final String OPERATION_OPEN_CLIP = "CasOpenClip";
private static final String OPERATION_READ_CDF = "CasReadCdf";
private static final String OPERATION_WRITE_CDF = "CasWriteCdf";
private static final String OPERATION_STREAM_BLOB = "CasStreamBlob";
private static final String OPERATION_WRITE_CLIP = "CasWriteClip";
private static final String OPERATION_SIZE_CLIP = "CasGetClipSize";
private static final int CLIP_OPTIONS = 0;
static void safeClose(FPTag tag, String clipId, int tagNum) {
try {
if (tag != null) tag.Close();
} catch (FPLibraryException e) {
log.warn("could not close tag " + clipId + "." + tagNum + ": " + summarizeError(e), e);
} catch (Throwable t) {
log.warn("could not close tag " + clipId + "." + tagNum, t);
}
}
static void safeClose(FPClip clip, String clipId) {
try {
if (clip != null) clip.Close();
} catch (FPLibraryException e) {
log.warn("could not close clip " + clipId + ": " + summarizeError(e), e);
} catch (Throwable t) {
log.warn("could not close clip " + clipId, t);
}
}
static void safeClose(ClipTag tag, String clipId) {
try {
if (tag != null) tag.close();
} catch (Throwable t) {
log.warn("could not close tag " + clipId + "." + tag.getTagNum(), t);
}
}
private FPPool pool;
private Date queryStartTime;
private Date queryEndTime;
private String lastResultCreateTime;
private EnhancedThreadPoolExecutor blobReadExecutor;
@Override
public void configure(SyncStorage source, Iterator<SyncFilter> filters, SyncStorage target) {
super.configure(source, filters, target);
if (this == target && !(source instanceof CasStorage))
throw new ConfigurationException("CasStorage as a target is currently only compatible with CasStorage as a source");
Assert.hasText(config.getConnectionString());
try {
if (pool == null) {
FPPool.RegisterApplication(config.getApplicationName(), config.getApplicationVersion());
FPPool.setGlobalOption(FPLibraryConstants.FP_OPTION_MAXCONNECTIONS, 999); // maximum allowed
pool = new FPPool(config.getConnectionString());
pool.setOption(FPLibraryConstants.FP_OPTION_BUFFERSIZE, 100 * 1024); // 100k (max embedded blob)
}
// Check connection
FPPool.PoolInfo info = pool.getPoolInfo();
log.info("Connected to {} ({}) using CAS v.{}",
info.getClusterName(), info.getClusterID(), info.getVersion());
// verify we have appropriate privileges
if (this == source) {
if (pool.getCapability(FPLibraryConstants.FP_READ, FPLibraryConstants.FP_ALLOWED).equals("False"))
throw new ConfigurationException("READ is not allowed for this pool connection");
if (getOptions().isDeleteSource()
&& pool.getCapability(FPLibraryConstants.FP_DELETE, FPLibraryConstants.FP_ALLOWED).equals("False"))
throw new ConfigurationException("DELETE is not allowed for this pool connection");
if (getOptions().isDeleteSource() && config.isPrivilegedDelete()
&& pool.getCapability(FPLibraryConstants.FP_PRIVILEGEDDELETE, FPLibraryConstants.FP_ALLOWED).equals("False"))
throw new ConfigurationException("PRIVILEGED-DELETE is not allowed for this pool connection");
}
if (this == target) {
if (pool.getCapability(FPLibraryConstants.FP_WRITE, FPLibraryConstants.FP_ALLOWED).equals("False"))
throw new ConfigurationException("WRITE is not allowed for this pool connection");
}
if (config.getQueryStartTime() != null) {
queryStartTime = Iso8601Util.parse(config.getQueryStartTime());
if (queryStartTime == null) throw new ConfigurationException("could not parse query-start-time");
}
if (config.getQueryEndTime() != null) {
queryEndTime = Iso8601Util.parse(config.getQueryEndTime());
if (queryEndTime == null) throw new ConfigurationException("could not parse query-end-time");
if (queryStartTime != null && queryStartTime.after(queryEndTime))
throw new ConfigurationException("query-start-time is after query-end-time");
}
} catch (FPLibraryException e) {
throw new ConfigurationException("error creating pool: " + summarizeError(e), e);
}
blobReadExecutor = new EnhancedThreadPoolExecutor(options.getThreadCount(),
new LinkedBlockingDeque<Runnable>(options.getThreadCount()), "blob-read-pool");
}
@Override
public String getRelativePath(String identifier, boolean directory) {
return identifier;
}
@Override
public String getIdentifier(String relativePath, boolean directory) {
return relativePath;
}
@Override
protected ObjectSummary createSummary(final String identifier) {
log.debug("sizing {}...", identifier);
ObjectSummary summary = time(new Function<ObjectSummary>() {
@Override
public ObjectSummary call() {
try {
FPClip clip = new FPClip(pool, identifier, FPLibraryConstants.FP_OPEN_FLAT);
long size = clip.getTotalSize();
clip.Close();
return new ObjectSummary(identifier, false, size);
} catch (FPLibraryException e) {
throw new RuntimeException(e);
}
}
}, OPERATION_SIZE_CLIP);
log.debug("size of {} is {} bytes", identifier, summary.getSize());
return summary;
}
@Override
public Iterable<ObjectSummary> allObjects() {
return new Iterable<ObjectSummary>() {
@Override
public Iterator<ObjectSummary> iterator() {
try {
// verify we have appropriate privileges
if (pool.getCapability(FPLibraryConstants.FP_CLIPENUMERATION, FPLibraryConstants.FP_ALLOWED).equals("False"))
throw new ConfigurationException("QUERY is not supported for this pool connection.");
final FPQueryExpression query = new FPQueryExpression();
query.setStartTime(queryStartTime == null ? 0 : queryStartTime.getTime());
query.setEndTime(queryEndTime == null ? -1 : queryEndTime.getTime());
query.setType(FPLibraryConstants.FP_QUERY_TYPE_EXISTING);
query.selectField("creation.date");
query.selectField("totalsize");
return new ReadOnlyIterator<ObjectSummary>() {
final FPPoolQuery poolQuery = new FPPoolQuery(pool, query);
@Override
protected ObjectSummary getNextObject() {
try {
FPQueryResult queryResult;
while (true) {
queryResult = time(new Callable<FPQueryResult>() {
@Override
public FPQueryResult call() throws Exception {
return poolQuery.FetchResult();
}
}, OPERATION_FETCH_QUERY_RESULT);
try {
switch (queryResult.getResultCode()) {
case FPLibraryConstants.FP_QUERY_RESULT_CODE_OK:
log.debug("query result OK; creating ReadClipTask.");
long totalSize = Long.parseLong(queryResult.getField("totalsize"));
lastResultCreateTime = queryResult.getField("creation.date");
return new ObjectSummary(queryResult.getClipID(), false, totalSize);
case FPLibraryConstants.FP_QUERY_RESULT_CODE_INCOMPLETE:
log.info("received FP_QUERY_RESULT_CODE_INCOMPLETE error, invalid C-Clip, trying again.");
break;
case FPLibraryConstants.FP_QUERY_RESULT_CODE_COMPLETE:
log.info("received FP_QUERY_RESULT_CODE_COMPLETE, there should have been a previous "
+ "FP_QUERY_RESULT_CODE_INCOMPLETE error reported.");
break;
case FPLibraryConstants.FP_QUERY_RESULT_CODE_PROGRESS:
log.info("received FP_QUERY_RESULT_CODE_PROGRESS, continuing.");
break;
case FPLibraryConstants.FP_QUERY_RESULT_CODE_ERROR:
log.info("received FP_QUERY_RESULT_CODE_ERROR error, retrying again");
break;
case FPLibraryConstants.FP_QUERY_RESULT_CODE_END:
log.warn("end of query reached.");
try {
poolQuery.Close();
} catch (Throwable t) {
log.warn("could not close query: " + t.getMessage());
}
return null;
case FPLibraryConstants.FP_QUERY_RESULT_CODE_ABORT:
// query aborted due to server side issue or start time
// is later than server time.
throw new RuntimeException("received FP_QUERY_RESULT_CODE_ABORT error, exiting.");
default:
throw new RuntimeException("received error: " + queryResult.getResultCode());
}
} finally {
try {
queryResult.Close();
} catch (Throwable t) {
log.warn("could not close query result: " + t.getMessage());
}
}
} //while
} catch (Exception e) {
if (lastResultCreateTime != null)
log.error("last query result create-date: " + lastResultCreateTime);
try {
poolQuery.Close();
} catch (Throwable t) {
log.warn("could not close query: " + t.getMessage());
}
if (e instanceof RuntimeException) throw (RuntimeException) e;
throw new RuntimeException(e);
}
}
};
} catch (FPLibraryException e) {
throw new RuntimeException(summarizeError(e), e);
}
}
};
}
@Override
public Iterable<ObjectSummary> children(ObjectSummary parent) {
return new Iterable<ObjectSummary>() {
@Override
public Iterator<ObjectSummary> iterator() {
return Collections.emptyIterator();
}
};
}
@Override
public SyncObject loadObject(final String identifier) throws ObjectNotFoundException {
FPClip clip = null;
try {
// check existence first (if this is the target, it probably doesn't exist!)
if (!FPClip.Exists(pool, identifier)) throw new ObjectNotFoundException(identifier);
// open the clip
final FPClip fClip = clip = TimingUtil.time(getOptions(), OPERATION_OPEN_CLIP, new Callable<FPClip>() {
@Override
public FPClip call() throws Exception {
return new FPClip(pool, identifier, FPLibraryConstants.FP_OPEN_FLAT);
}
});
// pull the CDF
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
TimingUtil.time(getOptions(), OPERATION_READ_CDF, new Callable<Void>() {
@Override
public Void call() throws Exception {
fClip.RawRead(baos);
return null;
}
});
byte[] cdfData = baos.toByteArray();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(clip.getTotalSize());
metadata.setModificationTime(new Date(clip.getCreationDate()));
return new ClipSyncObject(this, identifier, clip, cdfData, metadata, blobReadExecutor);
} catch (ObjectNotFoundException e) {
throw e;
} catch (Exception e) {
safeClose(clip, identifier);
if (e instanceof FPLibraryException)
throw new RuntimeException(summarizeError((FPLibraryException) e), e);
else if (e instanceof RuntimeException) throw (RuntimeException) e;
else throw new RuntimeException(e);
}
}
@Override
public String createObject(SyncObject object) {
if (!(object instanceof ClipSyncObject))
throw new UnsupportedOperationException("sync object was not a CAS clip");
ClipSyncObject clipObject = (ClipSyncObject) object;
final String clipId = object.getRelativePath();
FPClip clip = null;
FPTag tag = null;
int targetTagNum = 0;
try (final InputStream cdfIn = clipObject.getDataStream()) {
// first clone the clip via CDF raw write
clip = TimingUtil.time(getOptions(), OPERATION_WRITE_CDF, new Callable<FPClip>() {
@Override
public FPClip call() throws Exception {
return new FPClip(pool, clipId, cdfIn, CLIP_OPTIONS);
}
});
// next write the blobs
for (ClipTag sourceTag : clipObject.getTags()) {
try (ClipTag sTag = sourceTag) { // close each source tag as we go, to conserve native (CAS SDK) memory
tag = clip.FetchNext(); // this should sync the tag indexes
if (sTag.isBlobAttached()) { // only stream if the tag has a blob
timedStreamBlob(tag, sTag);
}
tag.Close();
tag = null;
}
}
// finalize the clip
final FPClip fClip = clip;
String destClipId = TimingUtil.time(getOptions(), OPERATION_WRITE_CLIP, new Callable<String>() {
@Override
public String call() throws Exception {
return fClip.Write();
}
});
if (!destClipId.equals(clipId))
throw new RuntimeException(String.format("clip IDs do not match\n [%s != %s]", clipId, destClipId));
log.debug("Wrote source {} to dest {}", clipId, destClipId);
return destClipId;
} catch (Throwable t) {
if (t instanceof RuntimeException) throw (RuntimeException) t;
if (t instanceof FPLibraryException)
throw new RuntimeException("Failed to store object: " + summarizeError((FPLibraryException) t), t);
throw new RuntimeException("Failed to store object: " + t.getMessage(), t);
} finally {
// close current tag ref
safeClose(tag, clipId, targetTagNum);
// close clip
safeClose(clip, clipId);
}
}
@Override
public void updateObject(String identifier, SyncObject object) {
log.warn("attempt to update existing CAS clip {}", identifier);
createObject(object);
}
@Override
public void delete(String identifier) {
try {
long OPTS = config.isPrivilegedDelete() ? FPLibraryConstants.FP_OPTION_DELETE_PRIVILEGED : FPLibraryConstants.FP_OPTION_DEFAULT_OPTIONS;
FPClip.AuditedDelete(pool, identifier, config.getDeleteReason(), OPTS);
} catch (FPLibraryException e) {
throw new RuntimeException(summarizeError(e), e);
}
}
@Override
public void close() {
super.close();
if (pool != null) try {
pool.Close();
} catch (Throwable t) {
log.warn("could not close pool: " + t.getMessage());
}
pool = null;
blobReadExecutor.shutdown();
}
private void timedStreamBlob(final FPTag tag, final ClipTag blob) throws Exception {
TimingUtil.time(getOptions(), OPERATION_STREAM_BLOB, new Callable<Void>() {
@Override
public Void call() throws Exception {
InputStream sourceStream = blob.getBlobInputStream();
if (getOptions().isMonitorPerformance())
sourceStream = new ProgressInputStream(sourceStream, new PerformanceListener(getWriteWindow()));
try (InputStream stream = sourceStream) {
tag.BlobWrite(stream);
}
return null;
}
});
}
static String summarizeError(FPLibraryException e) {
return String.format("CAS Error %s/%s: %s", e.getErrorCode(), e.getErrorString(), e.getMessage());
}
}