package com.thinkbiganalytics.nifi.v2.core.metadata;
/*-
* #%L
* thinkbig-nifi-core-service
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed 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.
* #L%
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.thinkbiganalytics.metadata.rest.client.MetadataClient;
import com.thinkbiganalytics.metadata.rest.model.feed.InitializationStatus;
import com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder;
import com.thinkbiganalytics.nifi.core.api.metadata.WaterMarkActiveException;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.exception.ProcessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class MetadataClientRecorder implements MetadataRecorder {
private static final Logger log = LoggerFactory.getLogger(MetadataClientRecorder.class);
private static final String CURRENT_WATER_MARKS_ATTR = "activeWaterMarks";
private static final ObjectReader WATER_MARKS_READER = new ObjectMapper().reader().forType(Map.class);
private static final ObjectWriter WATER_MARKS_WRITER = new ObjectMapper().writer().forType(Map.class);
private MetadataClient client;
private Set<String> activeWaterMarks = Collections.synchronizedSet(new HashSet<>());
private Map<String, InitializationStatus> activeInitStatuses = Collections.synchronizedMap(new HashMap<>());
/**
* constructor creates a MetadataClientRecorder with the default URI constant
*/
public MetadataClientRecorder() {
this(URI.create("http://localhost:8420/api/v1/metadata"));
}
/**
* constructor creates a MetadataClientRecorder with the URI provided
*
* @param baseUri the REST endpoint of the Metadata recorder
*/
public MetadataClientRecorder(URI baseUri) {
this(new MetadataClient(baseUri));
}
/**
* constructor creates a MetadataClientProvider with the required {@link MetadataClientRecorder}
*
* @param client the MetadataClient will be used to connect with the Metadata store
*/
public MetadataClientRecorder(MetadataClient client) {
this.client = client;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#loadWaterMark(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String, java.lang.String)
*/
@Override
public FlowFile loadWaterMark(ProcessSession session, FlowFile ff, String feedId, String waterMarkName, String parameterName, String defaultValue) throws WaterMarkActiveException {
recordActiveWaterMark(feedId, waterMarkName);
String value = getHighWaterMarkValue(feedId, waterMarkName).orElse(defaultValue);
FlowFile resultFF = addCurrentWaterMarksAttr(session, ff, waterMarkName, parameterName);
resultFF = session.putAttribute(resultFF, parameterName, value);
return session.putAttribute(resultFF, initValueParameterName(parameterName), value);
}
private void recordActiveWaterMark(String feedId, String waterMarkName) throws WaterMarkActiveException {
String feedWaterMarkName = asFeedWaterMarkName(feedId, waterMarkName);
boolean added = this.activeWaterMarks.add(feedWaterMarkName);
if (!added) {
throw new WaterMarkActiveException(waterMarkName);
}
}
private boolean releaseActiveWaterMark(String feedId, String waterMarkName) {
return this.activeWaterMarks.remove(asFeedWaterMarkName(feedId, waterMarkName));
}
private String asFeedWaterMarkName(String feedId, String waterMarkName) {
return feedId + "." + waterMarkName;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#recordWaterMark(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String, java.lang.String, java.io.Serializable)
*/
@Override
public FlowFile recordWaterMark(ProcessSession session, FlowFile ff, String feedId, String waterMarkName, String parameterName, String newValue) {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
if (actives.containsKey(waterMarkName)) {
return session.putAttribute(ff, parameterName, newValue);
} else {
throw new IllegalStateException("No active high-water mark named \"" + waterMarkName + "\"");
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#commitWaterMark(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String, java.lang.String)
*/
@Override
public FlowFile commitWaterMark(ProcessSession session, FlowFile ff, String feedId, String waterMarkName) {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
FlowFile resultFF = ff;
if (actives.containsKey(waterMarkName)) {
String parameterName = actives.remove(waterMarkName);
try {
String value = ff.getAttribute(parameterName);
updateHighWaterMarkValue(feedId, waterMarkName, value);
} finally {
releaseActiveWaterMark(feedId, waterMarkName);
resultFF = setCurrentWaterMarksAttr(session, resultFF, actives);
}
return resultFF;
} else {
throw new IllegalStateException("No active high-water mark named \"" + waterMarkName + "\"");
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#commitAllWaterMarks(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String)
*/
@Override
public FlowFile commitAllWaterMarks(ProcessSession session, FlowFile ff, String feedId) {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
FlowFile resultFF = ff;
// TODO do more efficiently
for (String waterMarkName : new HashSet<String>(actives.keySet())) {
resultFF = commitWaterMark(session, resultFF, feedId, waterMarkName);
}
return resultFF;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#releaseWaterMark(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String, java.lang.String)
*/
@Override
public FlowFile releaseWaterMark(ProcessSession session, FlowFile ff, String feedId, String waterMarkName) {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
FlowFile resultFF = ff;
if (actives.containsKey(waterMarkName)) {
String parameterName = actives.remove(waterMarkName);
try {
String value = getHighWaterMarkValue(feedId, waterMarkName)
.orElse(ff.getAttribute(initValueParameterName(parameterName)));
resultFF = session.putAttribute(resultFF, parameterName, value);
} finally {
releaseActiveWaterMark(feedId, waterMarkName);
resultFF = setCurrentWaterMarksAttr(session, resultFF, actives);
}
return resultFF;
} else {
throw new IllegalStateException("No active high-water mark named \"" + waterMarkName + "\"");
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#releaseAllWaterMarks(org.apache.nifi.processor.ProcessSession, org.apache.nifi.flowfile.FlowFile, java.lang.String)
*/
@Override
public FlowFile releaseAllWaterMarks(ProcessSession session, FlowFile ff, String feedId) {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
FlowFile resultFF = ff;
for (String waterMarkName : new HashSet<String>(actives.keySet())) {
resultFF = releaseWaterMark(session, resultFF, feedId, waterMarkName);
}
return resultFF;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#getFeedInitializationStatus(java.lang.String)
*/
@Override
public Optional<InitializationStatus> getInitializationStatus(String feedId) {
// Defer to the local active state first
Optional<InitializationStatus> option = Optional.ofNullable(this.activeInitStatuses.get(feedId));
return option.isPresent() ? option : Optional.ofNullable(this.client.getCurrentInitStatus(feedId));
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#startFeedInitialization(java.lang.String)
*/
@Override
public InitializationStatus startFeedInitialization(String feedId) {
InitializationStatus status = new InitializationStatus(InitializationStatus.State.IN_PROGRESS);
this.activeInitStatuses.put(feedId, status);
return status;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#completeFeedInitialization(java.lang.String)
*/
@Override
public InitializationStatus completeFeedInitialization(String feedId) {
InitializationStatus status = new InitializationStatus(InitializationStatus.State.SUCCESS);
try {
this.client.updateCurrentInitStatus(feedId, status);
this.activeInitStatuses.remove(feedId);
return status;
} catch (Exception e) {
log.error("Failed to update metadata with feed initialization completion status: {}, feed: {}", status.getState(), feedId, e);
throw new ProcessException("Failed to update metadata with feed initialization completion status: " + status + ", feed: " + feedId, e);
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.nifi.core.api.metadata.MetadataRecorder#failFeedInitialization(java.lang.String)
*/
@Override
public InitializationStatus failFeedInitialization(String feedId) {
InitializationStatus status = new InitializationStatus(InitializationStatus.State.FAILED);
try {
this.client.updateCurrentInitStatus(feedId, status);
this.activeInitStatuses.remove(feedId);
return status;
} catch (Exception e) {
log.error("Failed to update feed initialization completion status: {}, feed: {}", status.getState(), feedId);
this.activeInitStatuses.put(feedId, status);
return status;
}
}
@Override
public void updateFeedStatus(ProcessSession session, FlowFile ff, String statusMsg) {
// TODO Auto-generated method stub
}
private Map<String, String> getCurrentWaterMarksAttr(FlowFile ff) {
try {
String activeStr = ff.getAttribute(CURRENT_WATER_MARKS_ATTR);
if (activeStr == null) {
return new HashMap<>();
} else {
return WATER_MARKS_READER.readValue(activeStr);
}
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException(e);
}
}
private FlowFile addCurrentWaterMarksAttr(ProcessSession session, FlowFile ff, String waterMarkName, String parameterName) throws WaterMarkActiveException {
Map<String, String> actives = getCurrentWaterMarksAttr(ff);
actives.put(waterMarkName, parameterName);
return setCurrentWaterMarksAttr(session, ff, actives);
}
private FlowFile setCurrentWaterMarksAttr(ProcessSession session, FlowFile ff, Map<String, String> actives) {
try {
return session.putAttribute(ff, CURRENT_WATER_MARKS_ATTR, WATER_MARKS_WRITER.writeValueAsString(actives));
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException(e);
}
}
private Optional<String> getHighWaterMarkValue(String feedId, String waterMarkName) {
return this.client.getHighWaterMarkValue(feedId, waterMarkName);
}
private void updateHighWaterMarkValue(String feedId, String waterMarkName, String value) {
this.client.updateHighWaterMarkValue(feedId, waterMarkName, value);
}
private String initValueParameterName(String parameterName) {
return parameterName + ".original";
}
}