From 7f9f3ff7b299ad6761aabf0a33ea5b375fcaf87a Mon Sep 17 00:00:00 2001
From: jpercivall
Date: Fri, 1 Apr 2016 17:27:42 -0400
Subject: [PATCH 1/6] NIFI-1582 added state to UpdateAttribute as well as
updated a few parts that hadn't be touched in years (referenced the
'FlowFileMetadataEnhancer' processor'. Also added a 'NUMBER_VALIDATOR' to
StandardValidators
---
.../processor/util/StandardValidators.java | 20 ++
.../attributes/UpdateAttribute.java | 291 ++++++++++++------
.../additionalDetails.html | 70 ++++-
.../attributes/TestUpdateAttribute.java | 229 +++++++++++++-
4 files changed, 494 insertions(+), 116 deletions(-)
diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java
index a577bc8118cc..772aa8e97c5e 100644
--- a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java
+++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/processor/util/StandardValidators.java
@@ -23,6 +23,8 @@
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.time.Instant;
+import java.text.NumberFormat;
+import java.text.ParseException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@@ -126,6 +128,24 @@ public ValidationResult validate(final String subject, final String value, final
}
};
+ public static final Validator NUMBER_VALIDATOR = new Validator() {
+ @Override
+ public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+ if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+ return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+ }
+
+ String reason = null;
+ try {
+ NumberFormat.getInstance().parse(value);
+ } catch (ParseException e) {
+ reason = "not a valid Number";
+ }
+
+ return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+ }
+ };
+
public static final Validator PORT_VALIDATOR = createLongValidator(1, 65535, true);
/**
diff --git a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/java/org/apache/nifi/processors/attributes/UpdateAttribute.java b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/java/org/apache/nifi/processors/attributes/UpdateAttribute.java
index 08f4ee9ebc2d..ca130d4b7378 100644
--- a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/java/org/apache/nifi/processors/attributes/UpdateAttribute.java
+++ b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/java/org/apache/nifi/processors/attributes/UpdateAttribute.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.processors.attributes;
+import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
@@ -37,15 +38,21 @@
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.Validator;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.components.state.StateMap;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.flowfile.attributes.CoreAttributes;
@@ -66,67 +73,20 @@
import org.apache.nifi.update.attributes.Rule;
import org.apache.nifi.update.attributes.serde.CriteriaSerDe;
-/**
- * This processor supports updating flowfile attributes and can do so
- * conditionally or unconditionally. It can also delete flowfile attributes
- * that match a regular expression.
- *
- * Like the FlowFileMetadataEnhancer, it can
- * be configured with an arbitrary number of optional properties to define how
- * attributes should be updated. Each optional property represents an action
- * that is applied to all incoming flow files. An action is comprised of an
- * attribute key and a format string. The format string supports the following
- * parameters.
- *
- * - %1 - is the random generated UUID.
- * - %2 - is the current calendar time.
- * - ${"attribute.key") - is the flow file attribute value of the key
- * contained within the brackets.
- *
- *
- * When creating the optional properties, enter the attribute key as the
- * property name and the desired format string as the value. The optional
- * properties are considered default actions and are applied unconditionally.
- *
- * In addition to the default actions, this processor has a user interface (UI)
- * where conditional actions can be specified. In the UI, rules can be created.
- * Rules are comprised of an arbitrary number of conditions and actions. In
- * order for a rule to be activated, all conditions must evaluate to true.
- *
- * A rule condition is comprised of an attribute key and a regular expression. A
- * condition evaluates to true when the flowfile contains the attribute
- * specified and it's value matches the specified regular expression.
- *
- * A rule action follows the same definition as a rule above. It includes an
- * attribute key and a format string. The format string supports the same
- * parameters defined above.
- *
- * When a rule is activated (because conditions evaluate to true), all actions
- * in that rule are executed. Once each action has been applied, any remaining
- * default actions will be applied. This means that if rule action and a default
- * action modify the same attribute, only the rule action will execute. Default
- * actions will only execute when the attribute in question is not modified as
- * part of an activated rule.
- *
- * The incoming flow file is cloned for each rule that is activated. If no rule
- * is activated, any default actions are applied to the original flowfile and it
- * is transferred.
- *
- * This processor only supports a SUCCESS relationship.
- *
- * Note: In order for configuration changes made in the custom UI to take
- * effect, the processor must be stopped and started.
- */
@EventDriven
@SideEffectFree
+@SupportsBatching
@InputRequirement(Requirement.INPUT_REQUIRED)
-@Tags({"attributes", "modification", "update", "delete", "Attribute Expression Language"})
+@Tags({"attributes", "modification", "update", "delete", "Attribute Expression Language", "state"})
@CapabilityDescription("Updates the Attributes for a FlowFile by using the Attribute Expression Language and/or deletes the attributes based on a regular expression")
@DynamicProperty(name = "A FlowFile attribute to update", value = "The value to set it to", supportsExpressionLanguage = true,
description = "Updates a FlowFile attribute specified by the Dynamic Property's key with the value specified by the Dynamic Property's value")
@WritesAttribute(attribute = "See additional details", description = "This processor may write or remove zero or more attributes as described in additional details")
+@Stateful(scopes = {Scope.LOCAL, Scope.CLUSTER}, description = "Gives the option to store values not only on the FlowFile but as stateful variables to be referenced in a recursive manner." +
+ "State is stored either local or clustered depend on the property.")
public class UpdateAttribute extends AbstractProcessor implements Searchable {
+ private Scope scope = null;
private final AtomicReference criteriaCache = new AtomicReference<>(null);
private final ConcurrentMap propertyValues = new ConcurrentHashMap<>();
@@ -162,19 +122,46 @@ public ValidationResult validate(String subject, String input, ValidationContext
// static properties
public static final PropertyDescriptor DELETE_ATTRIBUTES = new PropertyDescriptor.Builder()
.name("Delete Attributes Expression")
- .description("Regular expression for attributes to be deleted from flowfiles.")
+ .description("Regular expression for attributes to be deleted from FlowFiles.")
.required(false)
.addValidator(DELETE_PROPERTY_VALIDATOR)
.expressionLanguageSupported(true)
.build();
+
+ public static final AllowableValue LOCATION_STATELESS = new AllowableValue("Stateless", "Stateless", "Do not store state.");
+ public static final AllowableValue LOCATION_LOCAL = new AllowableValue("Local", "Local", "Store the state locally.");
+ public static final AllowableValue LOCATION_CLUSTER = new AllowableValue("Cluster", "Cluster", "Store the state at the cluster level.");
+
+ public static final PropertyDescriptor STATE_LOCATION = new PropertyDescriptor.Builder()
+ .name("State Location")
+ .description("Select where or not state will be store and if so, where to store it. Selecting 'Stateless' will offer the default functionality of purely updating the attributes on a " +
+ "FlowFile in a stateless manner. Selecting 'Local' or 'Cluster' will not only store the attributes on the FlowFile but also in the Processors state. See the 'Stateful Usage' " +
+ "topic of the 'Additional Details' section of this processor's documentation for more information")
+ .required(true)
+ .allowableValues(LOCATION_STATELESS, LOCATION_LOCAL, LOCATION_CLUSTER)
+ .defaultValue(LOCATION_STATELESS.getValue())
+ .build();
+ public static final PropertyDescriptor STATEFUL_VARIABLES_INIT_VALUE = new PropertyDescriptor.Builder()
+ .name("Stateful Variables Initial Value")
+ .description("If using state to set/reference variables then this value is used to set the initial value of the stateful variable. This will only be used in the @OnScheduled method " +
+ "when state does not contain a value for the variable.")
+ .required(false)
+ .defaultValue("0")
+ .addValidator(StandardValidators.NUMBER_VALIDATOR)
+ .build();
+
// relationships
public static final Relationship REL_SUCCESS = new Relationship.Builder()
- .description("All FlowFiles are routed to this relationship").name("success").build();
+ .description("All successful FlowFiles are routed to this relationship").name("success").build();
+ public static final Relationship REL_FAILED_SET_STATE = new Relationship.Builder()
+ .description("A failure to set the state after adding the attributes to the FlowFile will route the FlowFile here. If the processor is set to 'Stateless' then all FlowFiles will " +
+ "route to success").name("set state fail").build();
public UpdateAttribute() {
final Set relationshipSet = new HashSet<>();
relationshipSet.add(REL_SUCCESS);
+ relationshipSet.add(REL_FAILED_SET_STATE);
relationships = Collections.unmodifiableSet(relationshipSet);
}
@@ -187,24 +174,85 @@ public Set getRelationships() {
protected List getSupportedPropertyDescriptors() {
List descriptors = new ArrayList<>();
descriptors.add(DELETE_ATTRIBUTES);
+ descriptors.add(STATE_LOCATION);
+ descriptors.add(STATEFUL_VARIABLES_INIT_VALUE);
return Collections.unmodifiableList(descriptors);
}
@Override
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
- return new PropertyDescriptor.Builder()
- .name(propertyDescriptorName)
- .required(false)
- .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
- .addValidator(StandardValidators.ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR)
- .expressionLanguageSupported(true)
- .dynamic(true)
- .build();
+ if(scope != null){
+ return new PropertyDescriptor.Builder()
+ .name(propertyDescriptorName)
+ .required(false)
+ .addValidator(StandardValidators.ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(true)
+ .dynamic(true)
+ .build();
+ } else {
+ return new PropertyDescriptor.Builder()
+ .name(propertyDescriptorName)
+ .required(false)
+ .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
+ .addValidator(StandardValidators.ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR)
+ .expressionLanguageSupported(true)
+ .dynamic(true)
+ .build();
+ }
+ }
+
+ @Override
+ public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) {
+ super.onPropertyModified(descriptor, oldValue, newValue);
+
+ if (descriptor.equals(STATE_LOCATION)) {
+ if (LOCATION_CLUSTER.getValue().equalsIgnoreCase(newValue)) {
+ scope = Scope.CLUSTER;
+ } else if (LOCATION_LOCAL.getValue().equalsIgnoreCase(newValue)) {
+ scope = Scope.LOCAL;
+ } else {
+ scope = null;
+ }
+ }
}
@OnScheduled
- public void clearPropertyValueMap() {
+ public void onScheduled(final ProcessContext context) throws IOException {
+ criteriaCache.set(CriteriaSerDe.deserialize(context.getAnnotationData()));
+
propertyValues.clear();
+
+ if(scope != null) {
+ StateManager stateManager = context.getStateManager();
+ StateMap state = stateManager.getState(scope);
+ HashMap tempMap = new HashMap<>();
+ tempMap.putAll(state.toMap());
+ String initValue = context.getProperty(STATEFUL_VARIABLES_INIT_VALUE).getValue();
+
+ // Initialize the stateful default actions
+ for (PropertyDescriptor entry : context.getProperties().keySet()) {
+ if (entry.isDynamic()) {
+ if(!tempMap.containsKey(entry.getName()+"_state")) {
+ tempMap.put(entry.getName() + "_state", initValue);
+ }
+ }
+ }
+
+ // Initialize the stateful actions if the criteria exists
+ final Criteria criteria = criteriaCache.get();
+ if (criteria != null) {
+ for (Rule rule : criteria.getRules()) {
+ for (Action action : rule.getActions()) {
+ if (!tempMap.containsKey(action.getAttribute() + "_state")) {
+ tempMap.put(action.getAttribute() + "_state", initValue);
+ }
+ }
+ }
+ }
+
+ context.getStateManager().setState(tempMap, scope);
+ }
}
@Override
@@ -325,20 +373,12 @@ public Collection search(final SearchContext context) {
}
}
- @OnScheduled
- public void parseAnnotationData(final ProcessContext context) {
- criteriaCache.set(CriteriaSerDe.deserialize(context.getAnnotationData()));
- }
-
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) {
final ComponentLog logger = getLogger();
final Criteria criteria = criteriaCache.get();
- List flowFiles = session.get(100);
- if (flowFiles.isEmpty()) {
- return;
- }
+ FlowFile flowFile = session.get();
final Map properties = context.getProperties();
@@ -353,45 +393,70 @@ public void onTrigger(final ProcessContext context, final ProcessSession session
// because is the original flowfile is used for all matching rules. in this
// case the order of the matching rules is preserved in the list
final Map> matchedRules = new HashMap<>();
+ Map statefulAttributes = null;
+
+ matchedRules.clear();
- for (FlowFile flowFile : flowFiles) {
- matchedRules.clear();
+ try {
+ if (scope != null) {
+ statefulAttributes = new HashMap<>(context.getStateManager().getState(scope).toMap());
+ } else {
+ statefulAttributes = null;
+ }
+ } catch (IOException e) {
+ logger.error("Failed to update attributes for {} due to failing to get state; transferring FlowFile back to '{}'", new Object[]{flowFile, Relationship.SELF.getName()}, e);
+ session.transfer(flowFile);
+ context.yield();
+ return;
+ }
- // if there is update criteria specified, evaluate it
- if (criteria != null && evaluateCriteria(session, context, criteria, flowFile, matchedRules)) {
- // apply the actions for each rule and transfer the flowfile
- for (final Map.Entry> entry : matchedRules.entrySet()) {
- FlowFile match = entry.getKey();
- final List rules = entry.getValue();
+ // if there is update criteria specified, evaluate it
+ if (criteria != null && evaluateCriteria(session, context, criteria, flowFile, matchedRules, statefulAttributes)) {
+ // apply the actions for each rule and transfer the flowfile
+ for (final Map.Entry> entry : matchedRules.entrySet()) {
+ FlowFile match = entry.getKey();
+ final List rules = entry.getValue();
- // execute each matching rule(s)
- match = executeActions(session, context, rules, defaultActions, match);
+ // execute each matching rule(s)
+ try {
+ match = executeActions(session, context, rules, defaultActions, match, statefulAttributes);
logger.info("Updated attributes for {}; transferring to '{}'", new Object[]{match, REL_SUCCESS.getName()});
// transfer the match
session.getProvenanceReporter().modifyAttributes(match);
session.transfer(match, REL_SUCCESS);
+ } catch (IOException e) {
+ logger.error("Failed to update attributes for {} due to a failure to set the state afterwards; transferring to '{}'", new Object[]{match, REL_FAILED_SET_STATE.getName()}, e);
+ session.transfer(match, REL_FAILED_SET_STATE);
+ return;
}
- } else {
- // transfer the flowfile to no match (that has the default actions applied)
- flowFile = executeActions(session, context, null, defaultActions, flowFile);
+ }
+ } else {
+ // transfer the flowfile to no match (that has the default actions applied)
+ try {
+ flowFile = executeActions(session, context, null, defaultActions, flowFile, statefulAttributes);
logger.info("Updated attributes for {}; transferring to '{}'", new Object[]{flowFile, REL_SUCCESS.getName()});
session.getProvenanceReporter().modifyAttributes(flowFile);
session.transfer(flowFile, REL_SUCCESS);
+ } catch (IOException e) {
+ logger.error("Failed to update attributes for {} due to failures setting state afterwards; transferring to '{}'", new Object[]{flowFile, REL_FAILED_SET_STATE.getName()}, e);
+ session.transfer(flowFile, REL_FAILED_SET_STATE);
+ return;
}
}
}
//Evaluates the specified Criteria on the specified flowfile. Clones the
// specified flow file for each rule that is applied.
- private boolean evaluateCriteria(final ProcessSession session, final ProcessContext context, final Criteria criteria, final FlowFile flowfile, final Map> matchedRules) {
- final ComponentLog logger = getLogger();
+ private boolean evaluateCriteria(final ProcessSession session, final ProcessContext context, final Criteria criteria, final FlowFile flowfile, final Map> matchedRules, final Map statefulAttributes) {
+ final ComponentLog logger = getLogger();
final List rules = criteria.getRules();
// consider each rule and hold a copy of the flowfile for each matched rule
for (final Rule rule : rules) {
// evaluate the rule
- if (evaluateRule(context, rule, flowfile)) {
+ if (evaluateRule(context, rule, flowfile, statefulAttributes)) {
final FlowFile flowfileToUse;
// determine if we should use the original flow file or clone
@@ -421,12 +486,12 @@ private boolean evaluateCriteria(final ProcessSession session, final ProcessCont
}
//Evaluates the specified rule on the specified flowfile.
- private boolean evaluateRule(final ProcessContext context, final Rule rule, FlowFile flowfile) {
+ private boolean evaluateRule(final ProcessContext context, final Rule rule, FlowFile flowfile, final Map statefulAttributes) {
// go through each condition
for (final Condition condition : rule.getConditions()) {
// fail if any condition is not met
- if (!evaluateCondition(context, condition, flowfile)) {
+ if (!evaluateCondition(context, condition, flowfile, statefulAttributes)) {
return false;
}
}
@@ -448,18 +513,19 @@ private PropertyValue getPropertyValue(final String text, final ProcessContext c
}
//Evaluates the specified condition on the specified flowfile.
- private boolean evaluateCondition(final ProcessContext context, final Condition condition, final FlowFile flowfile) {
+ private boolean evaluateCondition(final ProcessContext context, final Condition condition, final FlowFile flowfile, final Map statefulAttributes) {
try {
// evaluate the expression for the given flow file
- return getPropertyValue(condition.getExpression(), context).evaluateAttributeExpressions(flowfile).asBoolean();
+ return getPropertyValue(condition.getExpression(), context).evaluateAttributeExpressions(flowfile, statefulAttributes).asBoolean();
} catch (final ProcessException pe) {
throw new ProcessException(String.format("Unable to evaluate condition '%s': %s.", condition.getExpression(), pe), pe);
}
}
// Executes the specified action on the specified flowfile.
- private FlowFile executeActions(final ProcessSession session, final ProcessContext context, final List rules, final Map defaultActions, final FlowFile flowfile) {
- final ComponentLog logger = getLogger();
+ private FlowFile executeActions(final ProcessSession session, final ProcessContext context, final List rules, final Map defaultActions, final FlowFile flowfile,
+ final Map statefulAttributes) throws IOException {
+ final ComponentLog logger = getLogger();
final Map actions = new HashMap<>(defaultActions);
final String ruleName = (rules == null || rules.isEmpty()) ? "default" : rules.get(rules.size() - 1).getName();
@@ -489,17 +555,32 @@ private FlowFile executeActions(final ProcessSession session, final ProcessConte
final Map attributesToUpdate = new HashMap<>(actions.size());
final Set attributesToDelete = new HashSet<>(actions.size());
+ final Map statefulAttributesToSet;
+
+ if (statefulAttributes != null){
+ statefulAttributesToSet = new HashMap<>();
+ } else {
+ statefulAttributesToSet = null;
+ }
+
+
// go through each action
for (final Action action : actions.values()) {
if (!action.getAttribute().equals(DELETE_ATTRIBUTES.getName())) {
try {
- final String newAttributeValue = getPropertyValue(action.getValue(), context).evaluateAttributeExpressions(flowfile).getValue();
+ final String newAttributeValue = getPropertyValue(action.getValue(), context).evaluateAttributeExpressions(flowfile, statefulAttributes).getValue();
// log if appropriate
if (logger.isDebugEnabled()) {
logger.debug(String.format("%s setting attribute '%s' = '%s' for %s per rule '%s'.", this, action.getAttribute(), newAttributeValue, flowfile, ruleName));
}
+ if (statefulAttributesToSet != null) {
+ if(!action.getAttribute().equals("UpdateAttribute.matchedRule")) {
+ statefulAttributesToSet.put(action.getAttribute() + "_state", newAttributeValue);
+ }
+ }
+
attributesToUpdate.put(action.getAttribute(), newAttributeValue);
} catch (final ProcessException pe) {
throw new ProcessException(String.format("Unable to evaluate new value for attribute '%s': %s.", action.getAttribute(), pe), pe);
@@ -545,8 +626,14 @@ private FlowFile executeActions(final ProcessSession session, final ProcessConte
}
}
- // update and delete the flowfile attributes
- return session.removeAllAttributes(session.putAllAttributes(flowfile, attributesToUpdate), attributesToDelete);
+ // update and delete the FlowFile attributes
+ FlowFile returnFlowfile = session.removeAllAttributes(session.putAllAttributes(flowfile, attributesToUpdate), attributesToDelete);
+
+ if(statefulAttributesToSet != null) {
+ context.getStateManager().setState(statefulAttributesToSet, scope);
+ }
+
+ return returnFlowfile;
}
// Gets the default actions.
@@ -554,10 +641,12 @@ private Map getDefaultActions(final Map defaultActions = new HashMap<>();
for (final Map.Entry entry : properties.entrySet()) {
- final Action action = new Action();
- action.setAttribute(entry.getKey().getName());
- action.setValue(entry.getValue());
- defaultActions.put(action.getAttribute(), action);
+ if(entry.getKey() != STATE_LOCATION && entry.getKey() != STATEFUL_VARIABLES_INIT_VALUE) {
+ final Action action = new Action();
+ action.setAttribute(entry.getKey().getName());
+ action.setValue(entry.getValue());
+ defaultActions.put(action.getAttribute(), action);
+ }
}
return defaultActions;
diff --git a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/resources/docs/org.apache.nifi.processors.attributes.UpdateAttribute/additionalDetails.html b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/resources/docs/org.apache.nifi.processors.attributes.UpdateAttribute/additionalDetails.html
index cd4d34fca80a..89db3a3f275c 100644
--- a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/resources/docs/org.apache.nifi.processors.attributes.UpdateAttribute/additionalDetails.html
+++ b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-processor/src/main/resources/docs/org.apache.nifi.processors.attributes.UpdateAttribute/additionalDetails.html
@@ -247,6 +247,68 @@ Description:
Once all changes have been saved in the Advanced UI, the UI can be closed using the X in the top right corner.
+
+ Stateful Usage
+
+
+
+ By selecting either the "Local" or "Cluster" option for the "State Location" property UpdateAttribute will not only store the evaluated properties as attributes of the FlowFile but
+ also as stateful variables to be referenced in a recursive fashion. This enables the processor to calculate things like the sum or count of incoming FlowFiles. A dynamic property can be
+ referenced as a stateful variable like so:
+
+
+ - Dynamic Property
+
+ - key : theCount
+ - value : ${theCount_state:plus(1)}
+
+
+
+
+ This example will keep a count of the total number of FlowFiles that have passed through the processor. To use logic on top of State, simply use the "Advanced Usage" of UpdateAttribute.
+ All Actions will be stored as stateful attributes as well as being added to FlowFiles. Using the "Advanced Usage" it is possible to keep track of things like a maximum value of the
+ flow so far. This would be done by having a condition of "${maxValue_state:lt(${value})}" and an action of attribute:"maxValue", value:"${value}".
+
+ The "Stateful Variables Initial Value" property is used to initialize the stateful variables. Some logic rules will require a very high initial value, like using the Advanced rules to
+ determine the minimum value.
+
+
+ If stateful properties reference other stateful properties then the value for the other stateful properties will be an iteration behind. For example, attempting to calculate the
+ average of the incoming stream requires the sum and count. If all three properties are set in the same UpdateAttribute (like below) then the Average will always not include the most
+ recent values of count and sum:
+
+
+ - Count
+
+ - key : theCount
+ - value : ${theCount_state:plus(1)}
+
+
+
+ - Sum
+
+ - key : theSum
+ - value : ${theSum_state:plus(${flowfileValue})}
+
+
+
+ - Average
+
+ - key : theAverage
+ - value : ${theSum_state:divide(theCount_state)}
+
+
+
+
+ Instead, since average only relies on theCount and theSum attributes (which are added to the FlowFile as well) there should be a following Stateless UpdateAttribute which properly
+ calculates the average.
+
+ In the event that the processor is unable to get the state at the beginning of the onTrigger, the FlowFile will be pushed back to the originating relationship and the processor will yield.
+ If the processor is able to get the state at the beginning of the onTrigger but unable to set the state after adding attributes to the FlowFile, the FlowFile will be transferred to
+ "set state fail". This is normally due to the state not being the most up to date version (another thread has replaced the state with another version). In most use-cases this relationship
+ should loop back to the processor since the only affected attributes will be overwritten.
+
+
Properties:
@@ -267,7 +329,13 @@ Description:
success
- If the processor successfully updates the specified attribute(s), then the FlowFile follows this relationship.
-
+
+
+ set state fail
+
+ - If the processor is running statefully, and fails to set the state after adding attributes to the FlowFile, then the FlowFile will be routed to this relationship.
+
+