changeset 1016:d25e9c9f0e28

Filters generalization in progress
author Devel 2
date Wed, 25 Mar 2020 11:42:32 +0100
parents 518e5e10d714
children f8c9c14d507f
files stress-tester/src/main/java/com/passus/st/client/filter/SequenceFilter.java stress-tester/src/main/java/com/passus/st/client/filter/SequenceListener.java stress-tester/src/main/java/com/passus/st/client/http/ReporterDestination.java stress-tester/src/main/java/com/passus/st/client/http/filter/HttpSequenceFilter.java stress-tester/src/main/java/com/passus/st/client/http/filter/HttpSequenceListener.java stress-tester/src/test/java/com/passus/st/client/filter/SequenceFilterTest.java stress-tester/src/test/java/com/passus/st/client/http/filter/HttpSequenceFilterTest.java
diffstat 7 files changed, 661 insertions(+), 653 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stress-tester/src/main/java/com/passus/st/client/filter/SequenceFilter.java	Wed Mar 25 11:42:32 2020 +0100
@@ -0,0 +1,482 @@
+package com.passus.st.client.filter;
+
+import com.passus.commons.Assert;
+import com.passus.commons.annotations.Plugin;
+import com.passus.commons.metric.MapMetric;
+import com.passus.commons.service.Registry;
+import com.passus.commons.time.SystemTimeGenerator;
+import com.passus.commons.time.TimeAware;
+import com.passus.commons.time.TimeGenerator;
+import com.passus.config.*;
+import com.passus.config.annotations.NodeDefinitionCreate;
+import com.passus.config.schema.*;
+import com.passus.config.validation.Errors;
+import com.passus.filter.ValueExtractor;
+import com.passus.filter.ValueExtractorParser;
+import com.passus.filter.config.PredicateNodeTransformer;
+import com.passus.st.client.FlowContext;
+import com.passus.st.client.http.ReporterDestination;
+import com.passus.st.filter.Transformers;
+import com.passus.st.plugin.PluginConstants;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.*;
+import java.util.function.Predicate;
+
+import static com.passus.config.schema.ConfigurationSchemaBuilder.*;
+
+/**
+ * @author mikolaj.podbielski
+ */
+@NodeDefinitionCreate(SequenceFilter.NodeDefCreator.class)
+@Plugin(name = SequenceFilter.TYPE, category = PluginConstants.CATEGORY_FLOW_FILTER)
+public class SequenceFilter implements FlowFilter, TimeAware {
+
+    public static class SequenceItem {
+
+        private final Predicate predicate;
+        private boolean mustOccur = true;
+        private long time = 10_000;
+        private int num;
+        private String alias;
+        private String[] aliases;
+
+        public SequenceItem(Predicate predicate, int num) {
+            this(predicate);
+            this.num = num;
+        }
+
+        public SequenceItem(Predicate predicate) {
+            if (predicate == null) {
+                throw new NullPointerException();
+            }
+            this.predicate = predicate;
+        }
+
+        public Predicate getPredicate() {
+            return predicate;
+        }
+
+        public int getNum() {
+            return num;
+        }
+
+        public void setNum(int num) {
+            this.num = num;
+        }
+
+        public long getTime() {
+            return time;
+        }
+
+        public void setTime(long time) {
+            if (time <= 0) {
+                throw new IllegalArgumentException("Time must be greater than zero.");
+            }
+            this.time = time;
+        }
+
+        public String getAlias() {
+            return alias;
+        }
+
+        public void setAlias(String alias) {
+            this.alias = alias;
+            aliases = null;
+        }
+
+        public boolean isMustOccur() {
+            return mustOccur;
+        }
+
+        public void setMustOccur(boolean mustOccur) {
+            this.mustOccur = mustOccur;
+        }
+
+        private boolean match(Map<String, Object> value) {
+            return predicate.test(value);
+        }
+
+        private String[] getAliases() {
+            if (aliases == null) {
+                if (alias != null) {
+                    aliases = new String[2];
+                    aliases[1] = alias;
+                } else {
+                    aliases = new String[1];
+                }
+
+                aliases[0] = "_i" + num;
+            }
+
+            return aliases;
+        }
+    }
+
+    private static class SeqChainItem {
+
+        private final SequenceItem seq;
+        private SeqChainItem next;
+        private final long startTime;
+        private final long endTime;
+        private boolean active = true;
+
+        public SeqChainItem(SequenceItem seq, long startTime) {
+            this.seq = seq;
+            this.startTime = startTime;
+            this.endTime = startTime + seq.time;
+        }
+
+        public void setNext(SeqChainItem next) {
+            this.next = next;
+        }
+
+        public long getStartTime() {
+            return startTime;
+        }
+
+        public long getEndTime() {
+            return endTime;
+        }
+
+        public boolean match(Map<String, Object> value) {
+            return seq.match(value);
+        }
+
+        public SeqChainItem getNext() {
+            return next;
+        }
+
+        public boolean inTimeRange(long time) {
+            return (startTime <= time && time < endTime);
+        }
+    }
+
+    private static class SeqChain {
+
+        private SeqChainItem seqItem;
+
+        private final Map<String, Object> valueMap;
+//        private Filterable value = new FilterableMap();
+//        private FlowMarker flowMarker;
+
+        public SeqChain(SequenceItem[] seqItems, long startTime, Map<String, Object> valueMap) {
+            this.valueMap = valueMap;
+
+            SeqChainItem curItem = new SeqChainItem(seqItems[1], startTime);
+            seqItem = curItem;
+
+            for (int i = 2; i < seqItems.length; i++) {
+                SeqChainItem nexItem = new SeqChainItem(seqItems[i], curItem.getStartTime());
+                curItem.setNext(nexItem);
+                curItem = nexItem;
+            }
+        }
+
+        public void updateValue(MessageWrapper wrapper) {
+            valueMap.put("req", wrapper.getReq());
+            valueMap.put("resp", wrapper.getResp());
+        }
+
+        public void persistsValue(MessageWrapper wrapper, String[] aliases) {
+            for (String alias : aliases) {
+                valueMap.put(alias, wrapper);
+            }
+        }
+
+        public boolean hasNext() {
+            return (seqItem != null && seqItem.getNext() != null);
+        }
+
+        public boolean next() {
+            if (seqItem == null) {
+                return false;
+            }
+
+            seqItem = seqItem.getNext();
+            return true;
+        }
+
+        public boolean rewind(long time) {
+            if (seqItem == null) {
+                return false;
+            }
+
+            while (hasNext()) {
+                if (seqItem.inTimeRange(time)) {
+                    return true;
+                } else if (seqItem.seq.mustOccur) {
+                    return false;
+                }
+
+                next();
+            }
+
+            return false;
+        }
+    }
+
+    public static final String TYPE = "sequence";
+
+    private final List<SeqChain> chains = new ArrayList<>();
+    private SequenceListener listener = (e) -> {
+    };
+    private SequenceItem[] seqItems;
+    private Map<String, ValueExtractor> values = Collections.EMPTY_MAP;
+
+    private final FlowFilterFactory filterFactory;
+
+    private TimeGenerator timeGenerator = new SystemTimeGenerator();
+
+    public SequenceFilter() {
+        this(FlowFilterFactory.DEFAULT_FACTORY);
+    }
+
+    public SequenceFilter(FlowFilterFactory filterFactory) {
+        Assert.notNull(filterFactory, "filterFactory");
+        this.filterFactory = filterFactory;
+    }
+
+    public SequenceListener getListener() {
+        return listener;
+    }
+
+    public void setListener(SequenceListener listener) {
+        this.listener = listener;
+    }
+
+    public SequenceItem[] getSeqItems() {
+        return seqItems;
+    }
+
+    public void setSeqItems(SequenceItem[] seqItems) {
+        this.seqItems = seqItems;
+    }
+
+    public Map<String, ValueExtractor> getValues() {
+        return values;
+    }
+
+    public void setValues(Map<String, ValueExtractor> values) {
+        this.values = values;
+    }
+
+    @Override
+    public TimeGenerator getTimeGenerator() {
+        return timeGenerator;
+    }
+
+    @Override
+    public void setTimeGenerator(TimeGenerator timeGenerator) {
+        Assert.notNull(timeGenerator, "timeGenerator");
+        this.timeGenerator = timeGenerator;
+    }
+
+    private void processValue(long now, MessageWrapper value) {
+        Iterator<SeqChain> iterator = chains.iterator();
+        while (iterator.hasNext()) {
+            SeqChain chain = iterator.next();
+
+            if (chain.seqItem.startTime <= now) {
+                if (!chain.seqItem.inTimeRange(now) && !chain.rewind(now)) {
+                    chain.persistsValue(value, chain.seqItem.seq.getAliases());
+                    fireEvent(chain);
+                    iterator.remove();
+                } else if (chain.seqItem.active) {
+                    chain.updateValue(value);
+
+                    if (chain.seqItem.match(chain.valueMap)) {
+                        if (!chain.seqItem.seq.mustOccur) {
+                            iterator.remove();
+                        } else {
+                            chain.persistsValue(value, chain.seqItem.seq.getAliases());
+                            if (!chain.hasNext()) {
+                                fireEvent(chain);
+                                iterator.remove();
+                            } else if (!chain.next()) {
+                                fireEvent(chain);
+                                iterator.remove();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        final Map<String, Object> valueMap = new HashMap<>();
+        valueMap.put("req", value.getReq());
+        valueMap.put("resp", value.getResp());
+
+        if (seqItems[0].match(valueMap)) {
+            SeqChain chain = new SeqChain(seqItems, now, valueMap);
+            chain.persistsValue(value, seqItems[0].getAliases());
+            chains.add(chain);
+        }
+    }
+
+    private void fireEvent(SeqChain chain) {
+        Map<String, Serializable> result;
+
+        if (values.isEmpty()) {
+            result = Collections.emptyMap();
+        } else {
+            result = new HashMap<>();
+            Map<String, Object> persisted = chain.valueMap;
+            for (Map.Entry<String, ValueExtractor> e : values.entrySet()) {
+                Object value = e.getValue().extract(persisted);
+                if (value instanceof Serializable) {
+                    result.put(e.getKey(), (Serializable) value);
+                }
+            }
+        }
+
+        listener.sequenceDetected(new MapMetric("sequence", result));
+    }
+
+    @Override
+    public int filterInbound(Object req, Object resp, FlowContext context) {
+        MessageWrapper wrapper = filterFactory.createWrapper(req, resp, context);
+        processValue(timeGenerator.currentTimeMillis(), wrapper);
+        return DUNNO;
+    }
+
+    @Override
+    public void reset() {
+        chains.clear();
+    }
+
+    @Override
+    public SequenceFilter instanceForWorker(int index) {
+        SequenceFilter filter = new SequenceFilter();
+        filter.listener = listener;
+        filter.seqItems = seqItems;
+        filter.values = values;
+        return filter;
+    }
+
+    @Override
+    public void configure(Configuration config, ConfigurationContext context) {
+        List<SequenceItem> itemsList = (List<SequenceItem>) config.get("sequence");
+        seqItems = itemsList.toArray(new SequenceItem[itemsList.size()]);
+        for (int i = 0; i < seqItems.length; ++i) {
+            seqItems[i].setNum(i);
+        }
+
+        values = (Map<String, ValueExtractor>) config.get("values", Collections.EMPTY_MAP);
+        ReporterDestination reporterDestination = Registry.getInstance().get(
+                ReporterDestination.SERVICE_NAME, ReporterDestination.class);
+        if (reporterDestination != null) {
+            listener = reporterDestination;
+        }
+    }
+
+    public static class NodeDefCreator implements NodeDefinitionCreator {
+
+        @Override
+        public NodeDefinition create() {
+            MapNodeDefinition elementDef = mapDef(
+                    tupleDef("match", new MessagePredicateNodeDefinition()),
+                    tupleDef("mustOccur", valueDef(Boolean.class)).setRequired(false),
+                    tupleDef("time", valueDef(Long.class)).setRequired(false),
+                    tupleDef("alias", valueDef()).setRequired(false)
+            );
+            elementDef.setTransformer(new SequencesNodeTransformer());
+            // TODO: mustOccur and time required in all steps but first
+
+            return mapDef(
+                    tupleDef("sequence", new ListNodeDefinition(elementDef).setMinNumberOfValues(2)),
+                    tupleDef("values", new MappingNodeDefinition().setTransformer(new ValuesNodeTransformer())).setRequired(false)
+            );
+        }
+
+    }
+
+    private static class SequencesNodeTransformer implements NodeTransformer<CNode> {
+
+        private static final PredicateNodeTransformer TRANSFORMER = Transformers.PREDICATE;
+
+        @Override
+        public CNode transform(CNode node, Errors errors, ConfigurationContext context) {
+            try {
+                CMapNode mapNode = (CMapNode) node;
+                List<CTupleNode> tupleNodes = mapNode.getChildren();
+
+                Predicate predicate = null;
+                boolean mustOccur = true;
+                long time = 0;
+                String alias = null;
+                for (CTupleNode tupleNode : tupleNodes) {
+                    String name = tupleNode.getName();
+                    CNode valueNode = tupleNode.getNode();
+                    switch (name) {
+                        case "match":
+                            predicate = TRANSFORMER.transform(valueNode);
+                            break;
+                        case "mustOccur":
+                            mustOccur = (Boolean) getValue(valueNode);
+                            break;
+                        case "time":
+                            time = (Long) getValue(valueNode);
+                            break;
+                        case "alias":
+                            alias = (String) getValue(valueNode);
+                            break;
+                    }
+                }
+                SequenceItem item = new SequenceItem(predicate);
+                if (time > 0) {
+                    item.setTime(time);
+                }
+                item.setMustOccur(mustOccur);
+                item.setAlias(alias);
+                return new CValueNode(item);
+            } catch (Exception ex) {
+                return node;
+            }
+        }
+
+        @Override
+        public CNode reverseTransform(CNode node, Errors errors, ConfigurationContext context) {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+    }
+
+    private static class ValuesNodeTransformer implements NodeTransformer<CNode> {
+
+        @Override
+        public CNode transform(CNode node, Errors errors, ConfigurationContext context) {
+            Map<String, ValueExtractor> result = new HashMap<>();
+
+            CMapNode mapNode = (CMapNode) node;
+            List<CTupleNode> tupleNodes = mapNode.getChildren();
+            for (CTupleNode tupleNode : tupleNodes) {
+                String name = tupleNode.getName();
+                CValueNode valueNode = (CValueNode) tupleNode.getNode();
+                String value = (String) valueNode.getValue();
+                try {
+                    errors.pushNestedPath(name);
+                    ValueExtractor extractor = ValueExtractorParser.DEFAULT.parse(value);
+                    result.put(name, extractor);
+                } catch (ParseException ex) {
+                    errors.reject(tupleNode, "Invalid expression: \"%s\"", value);
+                } finally {
+                    errors.popNestedPath();
+                }
+            }
+            return new CValueNode(result);
+        }
+
+        @Override
+        public CNode reverseTransform(CNode node, Errors errors, ConfigurationContext context) {
+            throw new UnsupportedOperationException("Impossible."); // ValueExtractor
+        }
+
+    }
+
+    private static Object getValue(CNode node) {
+        CValueNode valueNode = (CValueNode) node;
+        return valueNode.getValue();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stress-tester/src/main/java/com/passus/st/client/filter/SequenceListener.java	Wed Mar 25 11:42:32 2020 +0100
@@ -0,0 +1,12 @@
+package com.passus.st.client.filter;
+
+import com.passus.commons.metric.MapMetric;
+
+/**
+ *
+ * @author mikolaj.podbielski
+ */
+public interface SequenceListener {
+
+    void sequenceDetected(MapMetric sequence);
+}
--- a/stress-tester/src/main/java/com/passus/st/client/http/ReporterDestination.java	Wed Mar 25 11:32:36 2020 +0100
+++ b/stress-tester/src/main/java/com/passus/st/client/http/ReporterDestination.java	Wed Mar 25 11:42:32 2020 +0100
@@ -9,7 +9,7 @@
 import com.passus.net.http.HttpMessage;
 import com.passus.st.client.ClientListener;
 import com.passus.st.client.FlowContext;
-import com.passus.st.client.http.filter.HttpSequenceListener;
+import com.passus.st.client.filter.SequenceListener;
 import com.passus.st.metric.MetricsCollectionHandler;
 
 import java.util.Arrays;
@@ -20,7 +20,7 @@
 /**
  * @author mikolaj.podbielski
  */
-public interface ReporterDestination extends Service, ClientListener, HttpSequenceListener, MetricsCollectionHandler, Configurable {
+public interface ReporterDestination extends Service, ClientListener, SequenceListener, MetricsCollectionHandler, Configurable {
 
     public static final String SERVICE_NAME = "ReporterDestination";
 
--- a/stress-tester/src/main/java/com/passus/st/client/http/filter/HttpSequenceFilter.java	Wed Mar 25 11:32:36 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,471 +0,0 @@
-package com.passus.st.client.http.filter;
-
-import com.passus.commons.Assert;
-import com.passus.commons.annotations.Plugin;
-import com.passus.commons.metric.MapMetric;
-import com.passus.commons.service.Registry;
-import com.passus.config.*;
-import com.passus.config.annotations.NodeDefinitionCreate;
-import com.passus.config.schema.*;
-import com.passus.config.validation.Errors;
-import com.passus.filter.ValueExtractor;
-import com.passus.filter.ValueExtractorParser;
-import com.passus.filter.config.PredicateNodeTransformer;
-import com.passus.net.http.HttpRequest;
-import com.passus.net.http.HttpResponse;
-import com.passus.st.client.FlowContext;
-import com.passus.st.client.filter.FlowFilterFactory;
-import com.passus.st.client.filter.MessagePredicateNodeDefinition;
-import com.passus.st.client.filter.MessageWrapper;
-import com.passus.st.client.http.ReporterDestination;
-import com.passus.st.filter.Transformers;
-import com.passus.st.plugin.PluginConstants;
-
-import java.io.Serializable;
-import java.text.ParseException;
-import java.util.*;
-import java.util.function.Predicate;
-
-import static com.passus.config.schema.ConfigurationSchemaBuilder.*;
-
-/**
- * @author mikolaj.podbielski
- */
-@NodeDefinitionCreate(HttpSequenceFilter.NodeDefCreator.class)
-@Plugin(name = HttpSequenceFilter.TYPE, category = PluginConstants.CATEGORY_FLOW_FILTER)
-public class HttpSequenceFilter extends HttpFilter {
-
-    public static class SequenceItem {
-
-        private final Predicate predicate;
-        private boolean mustOccur = true;
-        private long time = 10_000;
-        private int num;
-        private String alias;
-        private String[] aliases;
-
-        public SequenceItem(Predicate predicate, int num) {
-            this(predicate);
-            this.num = num;
-        }
-
-        public SequenceItem(Predicate predicate) {
-            if (predicate == null) {
-                throw new NullPointerException();
-            }
-            this.predicate = predicate;
-        }
-
-        public Predicate getPredicate() {
-            return predicate;
-        }
-
-        public int getNum() {
-            return num;
-        }
-
-        public void setNum(int num) {
-            this.num = num;
-        }
-
-        public long getTime() {
-            return time;
-        }
-
-        public void setTime(long time) {
-            if (time <= 0) {
-                throw new IllegalArgumentException("Time must be greater than zero.");
-            }
-            this.time = time;
-        }
-
-        public String getAlias() {
-            return alias;
-        }
-
-        public void setAlias(String alias) {
-            this.alias = alias;
-            aliases = null;
-        }
-
-        public boolean isMustOccur() {
-            return mustOccur;
-        }
-
-        public void setMustOccur(boolean mustOccur) {
-            this.mustOccur = mustOccur;
-        }
-
-        private boolean match(Map<String, Object> value) {
-            return predicate.test(value);
-        }
-
-        private String[] getAliases() {
-            if (aliases == null) {
-                if (alias != null) {
-                    aliases = new String[2];
-                    aliases[1] = alias;
-                } else {
-                    aliases = new String[1];
-                }
-
-                aliases[0] = "_i" + num;
-            }
-
-            return aliases;
-        }
-    }
-
-    private static class SeqChainItem {
-
-        private final SequenceItem seq;
-        private SeqChainItem next;
-        private final long startTime;
-        private final long endTime;
-        private boolean active = true;
-
-        public SeqChainItem(SequenceItem seq, long startTime) {
-            this.seq = seq;
-            this.startTime = startTime;
-            this.endTime = startTime + seq.time;
-        }
-
-        public void setNext(SeqChainItem next) {
-            this.next = next;
-        }
-
-        public long getStartTime() {
-            return startTime;
-        }
-
-        public long getEndTime() {
-            return endTime;
-        }
-
-        public boolean match(Map<String, Object> value) {
-            return seq.match(value);
-        }
-
-        public SeqChainItem getNext() {
-            return next;
-        }
-
-        public boolean inTimeRange(long time) {
-            return (startTime <= time && time < endTime);
-        }
-    }
-
-    private static class SeqChain {
-
-        private SeqChainItem seqItem;
-
-        private final Map<String, Object> valueMap;
-//        private Filterable value = new FilterableMap();
-//        private FlowMarker flowMarker;
-
-        public SeqChain(SequenceItem[] seqItems, long startTime, Map<String, Object> valueMap) {
-            this.valueMap = valueMap;
-
-            SeqChainItem curItem = new SeqChainItem(seqItems[1], startTime);
-            seqItem = curItem;
-
-            for (int i = 2; i < seqItems.length; i++) {
-                SeqChainItem nexItem = new SeqChainItem(seqItems[i], curItem.getStartTime());
-                curItem.setNext(nexItem);
-                curItem = nexItem;
-            }
-        }
-
-        public void updateValue(MessageWrapper wrapper) {
-            valueMap.put("req", wrapper.getReq());
-            valueMap.put("resp", wrapper.getResp());
-        }
-
-        public void persistsValue(MessageWrapper wrapper, String[] aliases) {
-            for (String alias : aliases) {
-                valueMap.put(alias, wrapper);
-            }
-        }
-
-        public boolean hasNext() {
-            return (seqItem != null && seqItem.getNext() != null);
-        }
-
-        public boolean next() {
-            if (seqItem == null) {
-                return false;
-            }
-
-            seqItem = seqItem.getNext();
-            return true;
-        }
-
-        public boolean rewind(long time) {
-            if (seqItem == null) {
-                return false;
-            }
-
-            while (hasNext()) {
-                if (seqItem.inTimeRange(time)) {
-                    return true;
-                } else if (seqItem.seq.mustOccur) {
-                    return false;
-                }
-
-                next();
-            }
-
-            return false;
-        }
-    }
-
-    public static final String TYPE = "sequence";
-
-    private final List<SeqChain> chains = new ArrayList<>();
-    private HttpSequenceListener listener = (e) -> {
-    };
-    private SequenceItem[] seqItems;
-    private Map<String, ValueExtractor> values = Collections.EMPTY_MAP;
-
-    private final FlowFilterFactory filterFactory;
-
-    public HttpSequenceFilter() {
-        this(FlowFilterFactory.DEFAULT_FACTORY);
-    }
-
-    public HttpSequenceFilter(FlowFilterFactory filterFactory) {
-        Assert.notNull(filterFactory, "filterFactory");
-        this.filterFactory = filterFactory;
-    }
-
-    public HttpSequenceListener getListener() {
-        return listener;
-    }
-
-    public void setListener(HttpSequenceListener listener) {
-        this.listener = listener;
-    }
-
-    public SequenceItem[] getSeqItems() {
-        return seqItems;
-    }
-
-    public void setSeqItems(SequenceItem[] seqItems) {
-        this.seqItems = seqItems;
-    }
-
-    public Map<String, ValueExtractor> getValues() {
-        return values;
-    }
-
-    public void setValues(Map<String, ValueExtractor> values) {
-        this.values = values;
-    }
-
-    private void processValue(long now, MessageWrapper value) {
-        Iterator<SeqChain> iterator = chains.iterator();
-        while (iterator.hasNext()) {
-            SeqChain chain = iterator.next();
-
-            if (chain.seqItem.startTime <= now) {
-                if (!chain.seqItem.inTimeRange(now) && !chain.rewind(now)) {
-                    chain.persistsValue(value, chain.seqItem.seq.getAliases());
-                    fireEvent(chain);
-                    iterator.remove();
-                } else if (chain.seqItem.active) {
-                    chain.updateValue(value);
-
-                    if (chain.seqItem.match(chain.valueMap)) {
-                        if (!chain.seqItem.seq.mustOccur) {
-                            iterator.remove();
-                        } else {
-                            chain.persistsValue(value, chain.seqItem.seq.getAliases());
-                            if (!chain.hasNext()) {
-                                fireEvent(chain);
-                                iterator.remove();
-                            } else if (!chain.next()) {
-                                fireEvent(chain);
-                                iterator.remove();
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        final Map<String, Object> valueMap = new HashMap<>();
-        valueMap.put("req", value.getReq());
-        valueMap.put("resp", value.getResp());
-
-        if (seqItems[0].match(valueMap)) {
-            SeqChain chain = new SeqChain(seqItems, now, valueMap);
-            chain.persistsValue(value, seqItems[0].getAliases());
-            chains.add(chain);
-        }
-    }
-
-    private void fireEvent(SeqChain chain) {
-        Map<String, Serializable> result;
-
-        if (values.isEmpty()) {
-            result = Collections.emptyMap();
-        } else {
-            result = new HashMap<>();
-            Map<String, Object> persisted = chain.valueMap;
-            for (Map.Entry<String, ValueExtractor> e : values.entrySet()) {
-                Object value = e.getValue().extract(persisted);
-                if (value instanceof Serializable) {
-                    result.put(e.getKey(), (Serializable) value);
-                }
-            }
-        }
-
-        listener.sequenceDetected(new MapMetric("sequence", result));
-    }
-
-    @Override
-    public int filterInbound(HttpRequest req, HttpResponse resp, FlowContext context) {
-        MessageWrapper wrapper = filterFactory.createWrapper(req, resp, context);
-        processValue(resp.getTimestamp(), wrapper);
-        return DUNNO;
-    }
-
-    @Override
-    public void reset() {
-        chains.clear();
-    }
-
-    @Override
-    public HttpSequenceFilter instanceForWorker(int index) {
-        HttpSequenceFilter filter = new HttpSequenceFilter();
-        filter.listener = listener;
-        filter.seqItems = seqItems;
-        filter.values = values;
-        return filter;
-    }
-
-    @Override
-    public void configure(Configuration config, ConfigurationContext context) {
-        List<SequenceItem> itemsList = (List<SequenceItem>) config.get("sequence");
-        seqItems = itemsList.toArray(new SequenceItem[itemsList.size()]);
-        for (int i = 0; i < seqItems.length; ++i) {
-            seqItems[i].setNum(i);
-        }
-
-        values = (Map<String, ValueExtractor>) config.get("values", Collections.EMPTY_MAP);
-        ReporterDestination reporterDestination = Registry.getInstance().get(
-                ReporterDestination.SERVICE_NAME, ReporterDestination.class);
-        if (reporterDestination != null) {
-            listener = reporterDestination;
-        }
-    }
-
-    public static class NodeDefCreator implements NodeDefinitionCreator {
-
-        @Override
-        public NodeDefinition create() {
-            MapNodeDefinition elementDef = mapDef(
-                    tupleDef("match", new MessagePredicateNodeDefinition()),
-                    tupleDef("mustOccur", valueDef(Boolean.class)).setRequired(false),
-                    tupleDef("time", valueDef(Long.class)).setRequired(false),
-                    tupleDef("alias", valueDef()).setRequired(false)
-            );
-            elementDef.setTransformer(new SequencesNodeTransformer());
-            // TODO: mustOccur and time required in all steps but first
-
-            return mapDef(
-                    tupleDef("sequence", new ListNodeDefinition(elementDef).setMinNumberOfValues(2)),
-                    tupleDef("values", new MappingNodeDefinition().setTransformer(new ValuesNodeTransformer())).setRequired(false)
-            );
-        }
-
-    }
-
-    private static class SequencesNodeTransformer implements NodeTransformer<CNode> {
-
-        private static final PredicateNodeTransformer TRANSFORMER = Transformers.PREDICATE;
-
-        @Override
-        public CNode transform(CNode node, Errors errors, ConfigurationContext context) {
-            try {
-                CMapNode mapNode = (CMapNode) node;
-                List<CTupleNode> tupleNodes = mapNode.getChildren();
-
-                Predicate predicate = null;
-                boolean mustOccur = true;
-                long time = 0;
-                String alias = null;
-                for (CTupleNode tupleNode : tupleNodes) {
-                    String name = tupleNode.getName();
-                    CNode valueNode = tupleNode.getNode();
-                    switch (name) {
-                        case "match":
-                            predicate = TRANSFORMER.transform(valueNode);
-                            break;
-                        case "mustOccur":
-                            mustOccur = (Boolean) getValue(valueNode);
-                            break;
-                        case "time":
-                            time = (Long) getValue(valueNode);
-                            break;
-                        case "alias":
-                            alias = (String) getValue(valueNode);
-                            break;
-                    }
-                }
-                SequenceItem item = new SequenceItem(predicate);
-                if (time > 0) {
-                    item.setTime(time);
-                }
-                item.setMustOccur(mustOccur);
-                item.setAlias(alias);
-                return new CValueNode(item);
-            } catch (Exception ex) {
-                return node;
-            }
-        }
-
-        @Override
-        public CNode reverseTransform(CNode node, Errors errors, ConfigurationContext context) {
-            throw new UnsupportedOperationException("Not supported yet.");
-        }
-
-    }
-
-    private static class ValuesNodeTransformer implements NodeTransformer<CNode> {
-
-        @Override
-        public CNode transform(CNode node, Errors errors, ConfigurationContext context) {
-            Map<String, ValueExtractor> result = new HashMap<>();
-
-            CMapNode mapNode = (CMapNode) node;
-            List<CTupleNode> tupleNodes = mapNode.getChildren();
-            for (CTupleNode tupleNode : tupleNodes) {
-                String name = tupleNode.getName();
-                CValueNode valueNode = (CValueNode) tupleNode.getNode();
-                String value = (String) valueNode.getValue();
-                try {
-                    errors.pushNestedPath(name);
-                    ValueExtractor extractor = ValueExtractorParser.DEFAULT.parse(value);
-                    result.put(name, extractor);
-                } catch (ParseException ex) {
-                    errors.reject(tupleNode, "Invalid expression: \"%s\"", value);
-                } finally {
-                    errors.popNestedPath();
-                }
-            }
-            return new CValueNode(result);
-        }
-
-        @Override
-        public CNode reverseTransform(CNode node, Errors errors, ConfigurationContext context) {
-            throw new UnsupportedOperationException("Impossible."); // ValueExtractor
-        }
-
-    }
-
-    private static Object getValue(CNode node) {
-        CValueNode valueNode = (CValueNode) node;
-        return valueNode.getValue();
-    }
-}
--- a/stress-tester/src/main/java/com/passus/st/client/http/filter/HttpSequenceListener.java	Wed Mar 25 11:32:36 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-package com.passus.st.client.http.filter;
-
-import com.passus.commons.metric.MapMetric;
-
-/**
- *
- * @author mikolaj.podbielski
- */
-public interface HttpSequenceListener {
-
-    void sequenceDetected(MapMetric sequence);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stress-tester/src/test/java/com/passus/st/client/filter/SequenceFilterTest.java	Wed Mar 25 11:42:32 2020 +0100
@@ -0,0 +1,165 @@
+package com.passus.st.client.filter;
+
+import com.passus.commons.metric.MapMetric;
+import com.passus.commons.utils.ResourceUtils;
+import com.passus.config.validation.Errors;
+import com.passus.filter.AndPredicate;
+import com.passus.filter.BeanValueExtractor;
+import com.passus.filter.ComparisonOperator;
+import com.passus.filter.ComparisonPredicate;
+import com.passus.filter.UnmutableValueExtractor;
+import com.passus.filter.ValueExtractor;
+import com.passus.filter.ValueExtractorParser;
+import com.passus.filter.config.PredicateNodeTransformer;
+import com.passus.net.http.HttpRequest;
+import com.passus.net.http.HttpRequestBuilder;
+import com.passus.net.http.HttpResponse;
+import com.passus.net.http.HttpResponseBuilder;
+import com.passus.st.AppUtils;
+import com.passus.st.client.filter.SequenceFilter.SequenceItem;
+import com.passus.st.filter.Transformers;
+import java.io.File;
+import java.io.Serializable;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import static org.testng.AssertJUnit.*;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * @author mikolaj.podbielski
+ */
+public class SequenceFilterTest {
+
+    private static final PredicateNodeTransformer TRANSFORMER = Transformers.PREDICATE;
+
+    @BeforeClass
+    public static void beforeClass() {
+        AppUtils.registerAll();
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        AppUtils.unregisterAll();
+    }
+
+    @Test
+    public void testFilterInbound() throws Exception {
+        HttpRequest req0 = HttpRequestBuilder.get("http://example.com/res1").build();
+        HttpResponse resp0 = HttpResponseBuilder.ok().cookie("id", "123").build();
+
+        HttpRequest req1 = HttpRequestBuilder.get("http://example.com/res2")
+                .cookie("id", "123").header("Xyz", "abc").build();
+        HttpResponse resp1 = HttpResponseBuilder.ok().build();
+
+        TestSequenceListener listener = new TestSequenceListener();
+
+        SequenceItem[] seqItems = {
+            item("{\"@req.uri\": \"/res1\", \"@resp.getCookie('id')\": {$neq: \"0\"}}", 0),
+            item("{\"@req.getHeader('xyz')\": \"abc\", \"@req.getCookie('id')\": \"@_i0.resp.getCookie('id')\"}", 1, "last"),};
+
+        Map<String, ValueExtractor> values = new HashMap<>();
+        values.put("code", value("@_i0.resp.status.code"));
+        values.put("xhost", value("@_i0.req.url.host"));
+        values.put("header", value("@_i1.req.getHeader('xyz')"));
+        values.put("cookie", value("@last.req.getCookie('id')"));
+        values.put("seqName", value("example sequence"));
+
+        SequenceFilter filter = new SequenceFilter().instanceForWorker(0);
+        filter.setListener(listener);
+        filter.setSeqItems(seqItems);
+        filter.setValues(values);
+
+        filter.filterInbound(req0, resp0, null);
+        filter.filterInbound(req1, resp1, null);
+
+        assertEquals(1, listener.events.size());
+        Map<String, Serializable> extracted = listener.events.get(0).getAttributesValue();
+        assertEquals(200, extracted.get("code"));
+        assertEquals("example.com", extracted.get("xhost").toString());
+        assertEquals("abc", extracted.get("header").toString());
+        assertEquals("123", extracted.get("cookie").toString());
+        assertEquals("example sequence", extracted.get("seqName").toString());
+    }
+
+    private static SequenceItem item(String predicateString, int num) throws Exception {
+        Predicate predicate = TRANSFORMER.transform(predicateString);
+        return new SequenceItem(predicate, num);
+    }
+
+    private static SequenceItem item(String predicateString, int num, String alias) throws Exception {
+        Predicate predicate = TRANSFORMER.transform(predicateString);
+        SequenceItem si = new SequenceItem(predicate, num);
+        si.setAlias(alias);
+        return si;
+    }
+
+    private static ValueExtractor value(String s) throws ParseException {
+        return ValueExtractorParser.DEFAULT.parse(s);
+    }
+
+    @Test
+    public void testConfigure() throws Exception {
+        File file = ResourceUtils.getFile("com/passus/st/client/http/filter/sequence.yml");
+        String filterConfig = new String(Files.readAllBytes(Paths.get(file.toURI())));
+
+        Errors errors = new Errors();
+        List<FlowFilter> filters = FlowFiltersConfigurator.getFilters(filterConfig, errors, null);
+        FilterTestUtils.printErrors(errors);
+
+        assertEquals(0, errors.getErrorCount());
+        assertEquals(1, filters.size());
+        assertTrue(filters.get(0) instanceof SequenceFilter);
+        SequenceFilter filter = (SequenceFilter) filters.get(0);
+
+        SequenceItem[] seqItems = filter.getSeqItems();
+        assertEquals(2, seqItems.length);
+        assertSeqItemValue(seqItems[0], 0, 10000, null);
+        assertSeqItemValue(seqItems[1], 1, 20000, "last");
+
+        AndPredicate p0 = (AndPredicate) seqItems[0].getPredicate();
+        assertEquals(2, p0.getSubPredicates().size());
+        ComparisonPredicate sp1 = (ComparisonPredicate) p0.getSubPredicates().get(1);
+        assertEquals(ComparisonOperator.NOT_EQUAL, sp1.getOperator());
+
+        assertEquals("resp.getCookie('id')", sp1.getFieldName());
+
+        UnmutableValueExtractor uve = (UnmutableValueExtractor) sp1.getPattern();
+        assertEquals(123, uve.extract(null));
+
+        Map<String, ValueExtractor> values = filter.getValues();
+        assertEquals(4, values.size());
+        ValueExtractor extractor = values.get("header");
+        assertTrue(extractor instanceof BeanValueExtractor);
+        BeanValueExtractor bve = (BeanValueExtractor) extractor;
+        assertEquals("last.req.getHeader('xyz')", bve.getFieldName());
+    }
+
+    private static void assertSeqItemValue(SequenceItem item, int num, long time, String alias) {
+        assertEquals("num", num, item.getNum());
+        assertEquals("time", time, item.getTime());
+        assertEquals("alias", alias, item.getAlias());
+    }
+
+    public static class TestSequenceListener implements SequenceListener {
+
+        ArrayList<MapMetric> events = new ArrayList<>();
+
+        @Override
+        public void sequenceDetected(MapMetric sequence) {
+            events.add(sequence);
+        }
+
+        public void reset() {
+            events.clear();
+        }
+    }
+}
--- a/stress-tester/src/test/java/com/passus/st/client/http/filter/HttpSequenceFilterTest.java	Wed Mar 25 11:32:36 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-package com.passus.st.client.http.filter;
-
-import com.passus.commons.metric.MapMetric;
-import com.passus.commons.utils.ResourceUtils;
-import com.passus.config.validation.Errors;
-import com.passus.filter.AndPredicate;
-import com.passus.filter.BeanValueExtractor;
-import com.passus.filter.ComparisonOperator;
-import com.passus.filter.ComparisonPredicate;
-import com.passus.filter.UnmutableValueExtractor;
-import com.passus.filter.ValueExtractor;
-import com.passus.filter.ValueExtractorParser;
-import com.passus.filter.config.PredicateNodeTransformer;
-import com.passus.net.http.HttpRequest;
-import com.passus.net.http.HttpRequestBuilder;
-import com.passus.net.http.HttpResponse;
-import com.passus.net.http.HttpResponseBuilder;
-import com.passus.st.AppUtils;
-import com.passus.st.client.filter.FilterTestUtils;
-import com.passus.st.client.filter.FlowFilter;
-import com.passus.st.client.filter.FlowFiltersConfigurator;
-import com.passus.st.client.http.filter.HttpSequenceFilter.SequenceItem;
-import com.passus.st.filter.Transformers;
-import java.io.File;
-import java.io.Serializable;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-import static org.testng.AssertJUnit.*;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-/**
- *
- * @author mikolaj.podbielski
- */
-public class HttpSequenceFilterTest {
-
-    private static final PredicateNodeTransformer TRANSFORMER = Transformers.PREDICATE;
-
-    @BeforeClass
-    public static void beforeClass() {
-        AppUtils.registerAll();
-    }
-
-    @AfterClass
-    public static void afterClass() {
-        AppUtils.unregisterAll();
-    }
-
-    @Test
-    public void testFilterInbound() throws Exception {
-        HttpRequest req0 = HttpRequestBuilder.get("http://example.com/res1").build();
-        HttpResponse resp0 = HttpResponseBuilder.ok().cookie("id", "123").build();
-
-        HttpRequest req1 = HttpRequestBuilder.get("http://example.com/res2")
-                .cookie("id", "123").header("Xyz", "abc").build();
-        HttpResponse resp1 = HttpResponseBuilder.ok().build();
-
-        TestHttpSequenceListener listener = new TestHttpSequenceListener();
-
-        SequenceItem[] seqItems = {
-            item("{\"@req.uri\": \"/res1\", \"@resp.getCookie('id')\": {$neq: \"0\"}}", 0),
-            item("{\"@req.getHeader('xyz')\": \"abc\", \"@req.getCookie('id')\": \"@_i0.resp.getCookie('id')\"}", 1, "last"),};
-
-        Map<String, ValueExtractor> values = new HashMap<>();
-        values.put("code", value("@_i0.resp.status.code"));
-        values.put("xhost", value("@_i0.req.url.host"));
-        values.put("header", value("@_i1.req.getHeader('xyz')"));
-        values.put("cookie", value("@last.req.getCookie('id')"));
-        values.put("seqName", value("example sequence"));
-
-        HttpSequenceFilter filter = new HttpSequenceFilter().instanceForWorker(0);
-        filter.setListener(listener);
-        filter.setSeqItems(seqItems);
-        filter.setValues(values);
-
-        filter.filterInbound(req0, resp0, null);
-        filter.filterInbound(req1, resp1, null);
-
-        assertEquals(1, listener.events.size());
-        Map<String, Serializable> extracted = listener.events.get(0).getAttributesValue();
-        assertEquals(200, extracted.get("code"));
-        assertEquals("example.com", extracted.get("xhost").toString());
-        assertEquals("abc", extracted.get("header").toString());
-        assertEquals("123", extracted.get("cookie").toString());
-        assertEquals("example sequence", extracted.get("seqName").toString());
-    }
-
-    private static SequenceItem item(String predicateString, int num) throws Exception {
-        Predicate predicate = TRANSFORMER.transform(predicateString);
-        return new SequenceItem(predicate, num);
-    }
-
-    private static SequenceItem item(String predicateString, int num, String alias) throws Exception {
-        Predicate predicate = TRANSFORMER.transform(predicateString);
-        SequenceItem si = new SequenceItem(predicate, num);
-        si.setAlias(alias);
-        return si;
-    }
-
-    private static ValueExtractor value(String s) throws ParseException {
-        return ValueExtractorParser.DEFAULT.parse(s);
-    }
-
-    @Test
-    public void testConfigure() throws Exception {
-        File file = ResourceUtils.getFile("com/passus/st/client/http/filter/sequence.yml");
-        String filterConfig = new String(Files.readAllBytes(Paths.get(file.toURI())));
-
-        Errors errors = new Errors();
-        List<FlowFilter> filters = FlowFiltersConfigurator.getFilters(filterConfig, errors, null);
-        FilterTestUtils.printErrors(errors);
-
-        assertEquals(0, errors.getErrorCount());
-        assertEquals(1, filters.size());
-        assertTrue(filters.get(0) instanceof HttpSequenceFilter);
-        HttpSequenceFilter filter = (HttpSequenceFilter) filters.get(0);
-
-        SequenceItem[] seqItems = filter.getSeqItems();
-        assertEquals(2, seqItems.length);
-        assertSeqItemValue(seqItems[0], 0, 10000, null);
-        assertSeqItemValue(seqItems[1], 1, 20000, "last");
-
-        AndPredicate p0 = (AndPredicate) seqItems[0].getPredicate();
-        assertEquals(2, p0.getSubPredicates().size());
-        ComparisonPredicate sp1 = (ComparisonPredicate) p0.getSubPredicates().get(1);
-        assertEquals(ComparisonOperator.NOT_EQUAL, sp1.getOperator());
-
-        assertEquals("resp.getCookie('id')", sp1.getFieldName());
-
-        UnmutableValueExtractor uve = (UnmutableValueExtractor) sp1.getPattern();
-        assertEquals(123, uve.extract(null));
-
-        Map<String, ValueExtractor> values = filter.getValues();
-        assertEquals(4, values.size());
-        ValueExtractor extractor = values.get("header");
-        assertTrue(extractor instanceof BeanValueExtractor);
-        BeanValueExtractor bve = (BeanValueExtractor) extractor;
-        assertEquals("last.req.getHeader('xyz')", bve.getFieldName());
-    }
-
-    private static void assertSeqItemValue(SequenceItem item, int num, long time, String alias) {
-        assertEquals("num", num, item.getNum());
-        assertEquals("time", time, item.getTime());
-        assertEquals("alias", alias, item.getAlias());
-    }
-
-    public static class TestHttpSequenceListener implements HttpSequenceListener {
-
-        ArrayList<MapMetric> events = new ArrayList<>();
-
-        @Override
-        public void sequenceDetected(MapMetric sequence) {
-            events.add(sequence);
-        }
-
-        public void reset() {
-            events.clear();
-        }
-    }
-}