diff mbox series

[ovs-dev,v6,1/1] ofproto-dpif-trace: Improve conjunctive match tracing.

Message ID 20231115094733.12404-1-nmiki@yahoo-corp.jp
State Accepted
Commit 7b514aba0e91c535024508624724a83a3df87b71
Delegated to: Ilya Maximets
Headers show
Series [ovs-dev,v6,1/1] ofproto-dpif-trace: Improve conjunctive match tracing. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/intel-ovs-compilation success test: success

Commit Message

Nobuhiro MIKI Nov. 15, 2023, 9:47 a.m. UTC
A conjunctive flow consists of two or more multiple flows with
conjunction actions. When input to the ofproto/trace command
matches a conjunctive flow, it outputs flows of all dimensions.

Acked-by: Simon Horman <horms@ovn.org>
Signed-off-by: Nobuhiro MIKI <nmiki@yahoo-corp.jp>
---
v6:
* Support multiple tables with resubmit action
v5:
* Support tunnel metadata
v4:
* Fix conj_id matching
* Fix priority matching
* Add a new test
v3:
* Remove struct flow changes.
* Use struct 'cls_rule' instead of struct 'flow'.
* Add priority and id conditionals for 'soft' arrays.
* Use 'minimask' in struct 'cls_rule' as mask.
* Use hmapx instead of ovs_list to store conj flows.
* Passe 'conj_flows' as an argument only when tracing.
v2:
* Reimplemented v1 with a safer and cleaner approach,
  since v1 was a messy implementation that rewrote const variables.
---
 lib/classifier.c             | 51 ++++++++++++++++---
 lib/classifier.h             |  4 +-
 lib/ovs-router.c             |  5 +-
 lib/tnl-ports.c              |  6 +--
 ofproto/ofproto-dpif-xlate.c | 67 +++++++++++++++++++++---
 ofproto/ofproto-dpif.c       | 25 ++++++---
 ofproto/ofproto-dpif.h       |  3 +-
 tests/classifier.at          | 99 ++++++++++++++++++++++++++++++++++++
 tests/test-classifier.c      |  8 +--
 9 files changed, 238 insertions(+), 30 deletions(-)

Comments

Ilya Maximets Nov. 16, 2023, 8:42 p.m. UTC | #1
On 11/15/23 10:47, Nobuhiro MIKI wrote:
> A conjunctive flow consists of two or more multiple flows with
> conjunction actions. When input to the ofproto/trace command
> matches a conjunctive flow, it outputs flows of all dimensions.
> 
> Acked-by: Simon Horman <horms@ovn.org>
> Signed-off-by: Nobuhiro MIKI <nmiki@yahoo-corp.jp>
> ---
> v6:
> * Support multiple tables with resubmit action
> v5:
> * Support tunnel metadata
> v4:
> * Fix conj_id matching
> * Fix priority matching
> * Add a new test
> v3:
> * Remove struct flow changes.
> * Use struct 'cls_rule' instead of struct 'flow'.
> * Add priority and id conditionals for 'soft' arrays.
> * Use 'minimask' in struct 'cls_rule' as mask.
> * Use hmapx instead of ovs_list to store conj flows.
> * Passe 'conj_flows' as an argument only when tracing.
> v2:
> * Reimplemented v1 with a safer and cleaner approach,
>   since v1 was a messy implementation that rewrote const variables.
> ---
>  lib/classifier.c             | 51 ++++++++++++++++---
>  lib/classifier.h             |  4 +-
>  lib/ovs-router.c             |  5 +-
>  lib/tnl-ports.c              |  6 +--
>  ofproto/ofproto-dpif-xlate.c | 67 +++++++++++++++++++++---
>  ofproto/ofproto-dpif.c       | 25 ++++++---
>  ofproto/ofproto-dpif.h       |  3 +-
>  tests/classifier.at          | 99 ++++++++++++++++++++++++++++++++++++
>  tests/test-classifier.c      |  8 +--
>  9 files changed, 238 insertions(+), 30 deletions(-)

Thanks for the update and the added tests!

This version looks good to me and works fine in all the tested cases.
Applied.

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/lib/classifier.c b/lib/classifier.c
index 18dbfc83ad44..0729bd190243 100644
--- a/lib/classifier.c
+++ b/lib/classifier.c
@@ -853,6 +853,32 @@  trie_ctx_init(struct trie_ctx *ctx, const struct cls_trie *trie)
     ctx->lookup_done = false;
 }
 
+static void
+insert_conj_flows(struct hmapx *conj_flows, uint32_t id, int priority,
+                  struct cls_conjunction_set **soft, size_t n_soft)
+{
+    struct cls_conjunction_set *conj_set;
+
+    if (!conj_flows) {
+        return;
+    }
+
+    for (size_t i = 0; i < n_soft; i++) {
+        conj_set = soft[i];
+
+        if (conj_set->priority != priority) {
+            continue;
+        }
+
+        for (size_t j = 0; j < conj_set->n; j++) {
+            if (conj_set->conj[j].id == id) {
+                hmapx_add(conj_flows, (void *) (conj_set->match->cls_rule));
+                break;
+            }
+        }
+    }
+}
+
 struct conjunctive_match {
     struct hmap_node hmap_node;
     uint32_t id;
@@ -933,11 +959,15 @@  free_conjunctive_matches(struct hmap *matches,
  * recursion within this function itself.
  *
  * 'flow' is non-const to allow for temporary modifications during the lookup.
- * Any changes are restored before returning. */
+ * Any changes are restored before returning.
+ *
+ * 'conj_flows' is an optional parameter.  If it is non-null, the matching
+ * conjunctive flows are inserted. */
 static const struct cls_rule *
 classifier_lookup__(const struct classifier *cls, ovs_version_t version,
                     struct flow *flow, struct flow_wildcards *wc,
-                    bool allow_conjunctive_matches)
+                    bool allow_conjunctive_matches,
+                    struct hmapx *conj_flows)
 {
     struct trie_ctx trie_ctx[CLS_MAX_TRIES];
     const struct cls_match *match;
@@ -1097,10 +1127,15 @@  classifier_lookup__(const struct classifier *cls, ovs_version_t version,
                 const struct cls_rule *rule;
 
                 flow->conj_id = id;
-                rule = classifier_lookup__(cls, version, flow, wc, false);
+                rule = classifier_lookup__(cls, version, flow, wc, false,
+                                           NULL);
                 flow->conj_id = saved_conj_id;
 
                 if (rule) {
+                    if (allow_conjunctive_matches) {
+                        insert_conj_flows(conj_flows, id, soft_pri, soft,
+                                          n_soft);
+                    }
                     free_conjunctive_matches(&matches,
                                              cm_stubs, ARRAY_SIZE(cm_stubs));
                     if (soft != soft_stub) {
@@ -1161,12 +1196,16 @@  classifier_lookup__(const struct classifier *cls, ovs_version_t version,
  * flow_wildcards_init_catchall()).
  *
  * 'flow' is non-const to allow for temporary modifications during the lookup.
- * Any changes are restored before returning. */
+ * Any changes are restored before returning.
+ *
+ * 'conj_flows' is an optional parameter.  If it is non-null, the matching
+ * conjunctive flows are inserted. */
 const struct cls_rule *
 classifier_lookup(const struct classifier *cls, ovs_version_t version,
-                  struct flow *flow, struct flow_wildcards *wc)
+                  struct flow *flow, struct flow_wildcards *wc,
+                  struct hmapx *conj_flows)
 {
-    return classifier_lookup__(cls, version, flow, wc, true);
+    return classifier_lookup__(cls, version, flow, wc, true, conj_flows);
 }
 
 /* Finds and returns a rule in 'cls' with exactly the same priority and
diff --git a/lib/classifier.h b/lib/classifier.h
index f646a8f7429b..f55a2cba998e 100644
--- a/lib/classifier.h
+++ b/lib/classifier.h
@@ -299,6 +299,7 @@ 
  * parallel to the rule's removal. */
 
 #include "cmap.h"
+#include "hmapx.h"
 #include "openvswitch/match.h"
 #include "openvswitch/meta-flow.h"
 #include "pvector.h"
@@ -398,7 +399,8 @@  static inline void classifier_publish(struct classifier *);
  * and each other. */
 const struct cls_rule *classifier_lookup(const struct classifier *,
                                          ovs_version_t, struct flow *,
-                                         struct flow_wildcards *);
+                                         struct flow_wildcards *,
+                                         struct hmapx *conj_flows);
 bool classifier_rule_overlaps(const struct classifier *,
                               const struct cls_rule *, ovs_version_t);
 const struct cls_rule *classifier_find_rule_exactly(const struct classifier *,
diff --git a/lib/ovs-router.c b/lib/ovs-router.c
index 7c04bb0e6b14..ca014d80ed31 100644
--- a/lib/ovs-router.c
+++ b/lib/ovs-router.c
@@ -115,7 +115,8 @@  ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
         const struct cls_rule *cr_src;
         struct flow flow_src = {.ipv6_dst = *src, .pkt_mark = mark};
 
-        cr_src = classifier_lookup(&cls, OVS_VERSION_MAX, &flow_src, NULL);
+        cr_src = classifier_lookup(&cls, OVS_VERSION_MAX, &flow_src, NULL,
+                                   NULL);
         if (cr_src) {
             struct ovs_router_entry *p_src = ovs_router_entry_cast(cr_src);
             if (!p_src->local) {
@@ -126,7 +127,7 @@  ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
         }
     }
 
-    cr = classifier_lookup(&cls, OVS_VERSION_MAX, &flow, NULL);
+    cr = classifier_lookup(&cls, OVS_VERSION_MAX, &flow, NULL, NULL);
     if (cr) {
         struct ovs_router_entry *p = ovs_router_entry_cast(cr);
 
diff --git a/lib/tnl-ports.c b/lib/tnl-ports.c
index f16409a0bf08..bb0b0b0c55f4 100644
--- a/lib/tnl-ports.c
+++ b/lib/tnl-ports.c
@@ -112,7 +112,7 @@  map_insert(odp_port_t port, struct eth_addr mac, struct in6_addr *addr,
     tnl_port_init_flow(&match.flow, mac, addr, nw_proto, tp_port);
 
     do {
-        cr = classifier_lookup(&cls, OVS_VERSION_MAX, &match.flow, NULL);
+        cr = classifier_lookup(&cls, OVS_VERSION_MAX, &match.flow, NULL, NULL);
         p = tnl_port_cast(cr);
         /* Try again if the rule was released before we get the reference. */
     } while (p && !ovs_refcount_try_ref_rcu(&p->ref_cnt));
@@ -247,7 +247,7 @@  map_delete(struct eth_addr mac, struct in6_addr *addr,
 
     tnl_port_init_flow(&flow, mac, addr, nw_proto, tp_port);
 
-    cr = classifier_lookup(&cls, OVS_VERSION_MAX, &flow, NULL);
+    cr = classifier_lookup(&cls, OVS_VERSION_MAX, &flow, NULL, NULL);
     tnl_port_unref(cr);
 }
 
@@ -305,7 +305,7 @@  odp_port_t
 tnl_port_map_lookup(struct flow *flow, struct flow_wildcards *wc)
 {
     const struct cls_rule *cr = classifier_lookup(&cls, OVS_VERSION_MAX, flow,
-                                                  wc);
+                                                  wc, NULL);
 
     return (cr) ? tnl_port_cast(cr)->portno : ODPP_NONE;
 }
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index e243773307b7..289f8a7361d2 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -229,6 +229,9 @@  struct xlate_ctx {
      * wants actions. */
     struct ofpbuf *odp_actions;
 
+    /* Set of matching conjunctive flows, or NULL. */
+    struct hmapx *conj_flows;
+
     /* Statistics maintained by xlate_table_action().
      *
      * These statistics limit the amount of work that a single flow
@@ -866,6 +869,34 @@  xlate_report_action_set(const struct xlate_ctx *ctx, const char *verb)
     }
 }
 
+static void
+xlate_report_conj_matches(const struct xlate_ctx *ctx,
+                          const struct ofputil_port_map *map)
+{
+    struct ds s = DS_EMPTY_INITIALIZER;
+    struct hmapx_node *node;
+    struct cls_rule *rule;
+
+    /* NOTE: The conj flows have meaning in order.  For each flow that is a
+     * component of conj flows, 'k' in 'conjunction(id, k/n)' represents the
+     * dimension.  When there are multiple flows with the same id, it may be
+     * implicitly expected that they would be output in ascending order of 'k'.
+     *
+     * However, because of the use of hmapx strucutre and the fact that the
+     * classifier returns them in arbitrary order, they are output in arbitrary
+     * order here. */
+    HMAPX_FOR_EACH (node, ctx->conj_flows) {
+        ds_clear(&s);
+
+        rule = node->data;
+
+        cls_rule_format(rule, ofproto_get_tun_tab(&ctx->xin->ofproto->up),
+                        map, &s);
+        xlate_report(ctx, OFT_DETAIL, "conj. %s", ds_cstr(&s));
+    }
+
+    ds_destroy(&s);
+}
 
 /* If tracing is enabled in 'ctx', appends a node representing 'rule' (in
  * OpenFlow table 'table_id') to the trace and makes this node the parent for
@@ -882,6 +913,8 @@  xlate_report_table(const struct xlate_ctx *ctx, struct rule_dpif *rule,
         return;
     }
 
+    struct ofputil_port_map map = OFPUTIL_PORT_MAP_INITIALIZER(&map);
+
     struct ds s = DS_EMPTY_INITIALIZER;
     ds_put_format(&s, "%2d. ", table_id);
     if (rule == ctx->xin->ofproto->miss_rule) {
@@ -892,8 +925,6 @@  xlate_report_table(const struct xlate_ctx *ctx, struct rule_dpif *rule,
         ds_put_cstr(&s, "Packets are IP fragments and "
                     "the fragment handling mode is \"drop\".");
     } else {
-        struct ofputil_port_map map = OFPUTIL_PORT_MAP_INITIALIZER(&map);
-
         if (ctx->xin->names) {
             struct ofproto_dpif *ofprotop;
             ofprotop = ofproto_dpif_lookup_by_name(ctx->xbridge->name);
@@ -904,8 +935,6 @@  xlate_report_table(const struct xlate_ctx *ctx, struct rule_dpif *rule,
                          ofproto_get_tun_tab(&ctx->xin->ofproto->up),
                          &map, &s, OFP_DEFAULT_PRIORITY);
 
-        ofputil_port_map_destroy(&map);
-
         if (ds_last(&s) != ' ') {
             ds_put_cstr(&s, ", ");
         }
@@ -918,6 +947,9 @@  xlate_report_table(const struct xlate_ctx *ctx, struct rule_dpif *rule,
     ctx->xin->trace = &oftrace_report(ctx->xin->trace, OFT_TABLE,
                                       ds_cstr(&s))->subs;
     ds_destroy(&s);
+
+    xlate_report_conj_matches(ctx, &map);
+    ofputil_port_map_destroy(&map);
 }
 
 /* If tracing is enabled in 'ctx', adds an OFT_DETAIL trace node to 'ctx'
@@ -4653,7 +4685,7 @@  xlate_table_action(struct xlate_ctx *ctx, ofp_port_t in_port, uint8_t table_id,
                                            ctx->xin->resubmit_stats,
                                            &ctx->table_id, in_port,
                                            may_packet_in, honor_table_miss,
-                                           ctx->xin->xcache);
+                                           ctx->xin->xcache, ctx->conj_flows);
         /* Swap back. */
         if (with_ct_orig) {
             tuple_swap(&ctx->xin->flow, ctx->wc);
@@ -4674,6 +4706,11 @@  xlate_table_action(struct xlate_ctx *ctx, ofp_port_t in_port, uint8_t table_id,
 
             struct ovs_list *old_trace = ctx->xin->trace;
             xlate_report_table(ctx, rule, table_id);
+
+            if (OVS_UNLIKELY(ctx->xin->trace)) {
+                hmapx_clear(ctx->conj_flows);
+            }
+
             xlate_recursively(ctx, rule, table_id <= old_table_id,
                               is_last_action, xlator);
             ctx->xin->trace = old_trace;
@@ -8044,6 +8081,13 @@  xlate_actions(struct xlate_in *xin, struct xlate_out *xout)
 
     COVERAGE_INC(xlate_actions);
 
+    ctx.conj_flows = NULL;
+
+    if (OVS_UNLIKELY(xin->trace)) {
+        ctx.conj_flows = xzalloc(sizeof *ctx.conj_flows);
+        hmapx_init(ctx.conj_flows);
+    }
+
     xin->trace = xlate_report(&ctx, OFT_BRIDGE, "bridge(\"%s\")",
                               xbridge->name);
     if (xin->frozen_state) {
@@ -8181,7 +8225,8 @@  xlate_actions(struct xlate_in *xin, struct xlate_out *xout)
         ctx.rule = rule_dpif_lookup_from_table(
             ctx.xbridge->ofproto, ctx.xin->tables_version, flow, ctx.wc,
             ctx.xin->resubmit_stats, &ctx.table_id,
-            flow->in_port.ofp_port, true, true, ctx.xin->xcache);
+            flow->in_port.ofp_port, true, true, ctx.xin->xcache,
+            ctx.conj_flows);
         if (ctx.xin->resubmit_stats) {
             rule_dpif_credit_stats(ctx.rule, ctx.xin->resubmit_stats, false);
         }
@@ -8194,6 +8239,10 @@  xlate_actions(struct xlate_in *xin, struct xlate_out *xout)
         }
 
         xlate_report_table(&ctx, ctx.rule, ctx.table_id);
+
+        if (OVS_UNLIKELY(ctx.xin->trace)) {
+            hmapx_clear(ctx.conj_flows);
+        }
     }
 
     /* Tunnel stats only for not-thawed packets. */
@@ -8375,6 +8424,12 @@  exit:
     ofpbuf_uninit(&scratch_actions);
     ofpbuf_delete(ctx.encap_data);
 
+    /* Clean up 'conj_flows' as it is no longer needed. */
+    if (OVS_UNLIKELY(xin->trace)) {
+        hmapx_destroy(ctx.conj_flows);
+        free(ctx.conj_flows);
+    }
+
     /* Make sure we return a "drop flow" in case of an error. */
     if (ctx.error) {
         xout->slow = 0;
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index ba5706f6adcc..808a5c23d67e 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -4383,15 +4383,20 @@  ofproto_dpif_get_tables_version(struct ofproto_dpif *ofproto)
  * a reference.
  *
  * 'flow' is non-const to allow for temporary modifications during the lookup.
- * Any changes are restored before returning. */
+ * Any changes are restored before returning.
+ *
+ * 'conj_flows' is an optional parameter.  If it is non-null, the matching
+ * conjunctive flows are inserted. */
 static struct rule_dpif *
 rule_dpif_lookup_in_table(struct ofproto_dpif *ofproto, ovs_version_t version,
                           uint8_t table_id, struct flow *flow,
-                          struct flow_wildcards *wc)
+                          struct flow_wildcards *wc,
+                          struct hmapx *conj_flows)
 {
     struct classifier *cls = &ofproto->up.tables[table_id].cls;
     return rule_dpif_cast(rule_from_cls_rule(classifier_lookup(cls, version,
-                                                               flow, wc)));
+                                                               flow, wc,
+                                                               conj_flows)));
 }
 
 void
@@ -4433,7 +4438,10 @@  ofproto_dpif_credit_table_stats(struct ofproto_dpif *ofproto, uint8_t table_id,
  * 'in_port'.  This is needed for resubmit action support.
  *
  * 'flow' is non-const to allow for temporary modifications during the lookup.
- * Any changes are restored before returning. */
+ * Any changes are restored before returning.
+ *
+ * 'conj_flows' is an optional parameter.  If it is non-null, the matching
+ * conjunctive flows are inserted. */
 struct rule_dpif *
 rule_dpif_lookup_from_table(struct ofproto_dpif *ofproto,
                             ovs_version_t version, struct flow *flow,
@@ -4441,7 +4449,8 @@  rule_dpif_lookup_from_table(struct ofproto_dpif *ofproto,
                             const struct dpif_flow_stats *stats,
                             uint8_t *table_id, ofp_port_t in_port,
                             bool may_packet_in, bool honor_table_miss,
-                            struct xlate_cache *xcache)
+                            struct xlate_cache *xcache,
+                            struct hmapx *conj_flows)
 {
     ovs_be16 old_tp_src = flow->tp_src, old_tp_dst = flow->tp_dst;
     ofp_port_t old_in_port = flow->in_port.ofp_port;
@@ -4497,7 +4506,8 @@  rule_dpif_lookup_from_table(struct ofproto_dpif *ofproto,
          next_id++, next_id += (next_id == TBL_INTERNAL))
     {
         *table_id = next_id;
-        rule = rule_dpif_lookup_in_table(ofproto, version, next_id, flow, wc);
+        rule = rule_dpif_lookup_in_table(ofproto, version, next_id, flow, wc,
+                                         conj_flows);
         if (stats) {
             struct oftable *tbl = &ofproto->up.tables[next_id];
             unsigned long orig;
@@ -6680,7 +6690,8 @@  ofproto_dpif_add_internal_flow(struct ofproto_dpif *ofproto,
 
     rule = rule_dpif_lookup_in_table(ofproto,
                                      ofproto_dpif_get_tables_version(ofproto),
-                                     TBL_INTERNAL, &match->flow, &match->wc);
+                                     TBL_INTERNAL, &match->flow, &match->wc,
+                                     NULL);
     if (rule) {
         *rulep = &rule->up;
     } else {
diff --git a/ofproto/ofproto-dpif.h b/ofproto/ofproto-dpif.h
index d8e0cd37ac5b..1fe22ab41bd9 100644
--- a/ofproto/ofproto-dpif.h
+++ b/ofproto/ofproto-dpif.h
@@ -103,7 +103,8 @@  struct rule_dpif *rule_dpif_lookup_from_table(struct ofproto_dpif *,
                                               ofp_port_t in_port,
                                               bool may_packet_in,
                                               bool honor_table_miss,
-                                              struct xlate_cache *);
+                                              struct xlate_cache *,
+                                              struct hmapx *conj_flows);
 
 void rule_dpif_credit_stats(struct rule_dpif *,
                             const struct dpif_flow_stats *, bool);
diff --git a/tests/classifier.at b/tests/classifier.at
index de2705653e00..93a13f32b13d 100644
--- a/tests/classifier.at
+++ b/tests/classifier.at
@@ -276,6 +276,13 @@  for src in 0 1 2 3 4 5 6 7; do
         AT_CHECK([ovs-appctl ofproto/trace br0 "in_port=1,dl_type=0x0800,nw_src=10.0.0.$src,nw_dst=10.0.0.$dst"], [0], [stdout])
         AT_CHECK_UNQUOTED([tail -1 stdout], [0], [Datapath actions: $out
 ])
+        dnl Check detailed output for conjunctive match.
+        if test $out = 3; then
+            AT_CHECK_UNQUOTED([cat stdout | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=100,ip,nw_dst=10.0.0.$dst
+     -> conj. priority=100,ip,nw_src=10.0.0.$src
+])
+        fi
     done
 done
 OVS_VSWITCHD_STOP
@@ -418,6 +425,98 @@  ovs-ofctl: "conjunction" actions may be used along with "note" but not any other
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([conjunctive match with same priority])
+OVS_VSWITCHD_START
+add_of_ports br0 1 2
+AT_DATA([flows.txt], [dnl
+conj_id=1,actions=2
+conj_id=2,actions=drop
+
+priority=10,ip,ip_dst=10.0.0.1,actions=conjunction(1,1/2)
+priority=10,ip,ip_src=10.0.0.2,actions=conjunction(1,2/2)
+priority=10,ip,ip_dst=10.0.0.3,actions=conjunction(2,1/2)
+priority=10,ip,in_port=1,actions=conjunction(2,2/2)
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+# Check that "priority=10,ip,in_port=1,actions=conjunction(2,2/2)" is
+# correctly excluded from the output.
+AT_CHECK([ovs-appctl ofproto/trace br0 "in_port=1,dl_type=0x0800,nw_dst=10.0.0.1,nw_src=10.0.0.2" | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=10,ip,nw_dst=10.0.0.1
+     -> conj. priority=10,ip,nw_src=10.0.0.2
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conjunctive match with metadata])
+OVS_VSWITCHD_START
+AT_CHECK([ovs-ofctl add-tlv-map br0 "{class=0xffff,type=0,len=4}->tun_metadata0"])
+AT_CHECK([ovs-ofctl add-tlv-map br0 "{class=0xffff,type=1,len=8}->tun_metadata1"])
+AT_DATA([flows.txt], [dnl
+conj_id=7,actions=drop
+
+priority=5,tun_metadata0=0x1,actions=conjunction(7,1/2)
+priority=5,tun_metadata1=0x2,actions=conjunction(7,2/2)
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+# Check that tunnel metadata is included in the output.
+AT_CHECK([ovs-appctl ofproto/trace br0 "tun_metadata0=0x1,tun_metadata1=0x2,in_port=br0" | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=5,tun_metadata0=0x1
+     -> conj. priority=5,tun_metadata1=0x2
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conjunctive match with or without port map])
+OVS_VSWITCHD_START
+add_of_ports br0 1 2
+AT_DATA([flows.txt], [dnl
+conj_id=1,actions=drop
+conj_id=2,actions=drop
+
+priority=10,ip,actions=conjunction(1,1/2),conjunction(2,1/2)
+priority=10,in_port=p1,actions=conjunction(1,2/2)
+priority=10,in_port=p2,actions=conjunction(1,2/2)
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+AT_CHECK([ovs-appctl ofproto/trace br0 "ip,in_port=p1" --names | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=10,in_port=p1
+     -> conj. priority=10,ip
+])
+AT_CHECK([ovs-appctl ofproto/trace br0 "ip,in_port=p2" | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=10,in_port=2
+     -> conj. priority=10,ip
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conjunctive match with resubmit])
+OVS_VSWITCHD_START
+add_of_ports br0 1 2
+AT_DATA([flows.txt], [dnl
+conj_id=1,actions=resubmit(,2)
+priority=10,ip,actions=conjunction(1,1/2)
+priority=10,in_port=p1,actions=conjunction(1,2/2)
+priority=10,in_port=p2,actions=conjunction(1,2/2)
+
+table=2,conj_id=7,actions=resubmit(,3)
+table=2,priority=20,ip,actions=conjunction(7,1/2)
+table=2,priority=20,in_port=p1,actions=conjunction(7,2/2)
+table=2,priority=20,in_port=p2,actions=conjunction(7,2/2)
+
+table=3,actions=drop
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+# Check that conj_flows are reset for each table and that they are output
+# exactly once.
+AT_CHECK([ovs-appctl ofproto/trace br0 "ip,in_port=p1" --names | grep conj\\. | sort], [0], [dnl
+     -> conj. priority=10,in_port=p1
+     -> conj. priority=10,ip
+     -> conj. priority=20,in_port=p1
+     -> conj. priority=20,ip
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 # Flow classifier a packet with excess of padding.
 AT_SETUP([flow classifier - packet with extra padding])
 OVS_VSWITCHD_START
diff --git a/tests/test-classifier.c b/tests/test-classifier.c
index cff00c8fa35e..2c1604a01e2e 100644
--- a/tests/test-classifier.c
+++ b/tests/test-classifier.c
@@ -441,7 +441,7 @@  compare_classifiers(struct classifier *cls, size_t n_invisible_rules,
         /* This assertion is here to suppress a GCC 4.9 array-bounds warning */
         ovs_assert(cls->n_tries <= CLS_MAX_TRIES);
 
-        cr0 = classifier_lookup(cls, version, &flow, &wc);
+        cr0 = classifier_lookup(cls, version, &flow, &wc, NULL);
         cr1 = tcls_lookup(tcls, &flow);
         assert((cr0 == NULL) == (cr1 == NULL));
         if (cr0 != NULL) {
@@ -454,7 +454,7 @@  compare_classifiers(struct classifier *cls, size_t n_invisible_rules,
             /* Make sure the rule should have been visible. */
             assert(cls_rule_visible_in_version(cr0, version));
         }
-        cr2 = classifier_lookup(cls, version, &flow, NULL);
+        cr2 = classifier_lookup(cls, version, &flow, NULL, NULL);
         assert(cr2 == cr0);
     }
 }
@@ -1370,10 +1370,10 @@  lookup_classifier(void *aux_)
         if (aux->use_wc) {
             flow_wildcards_init_catchall(&wc);
             cr = classifier_lookup(aux->cls, version, &aux->lookup_flows[x],
-                                   &wc);
+                                   &wc, NULL);
         } else {
             cr = classifier_lookup(aux->cls, version, &aux->lookup_flows[x],
-                                   NULL);
+                                   NULL, NULL);
         }
         if (cr) {
             hits++;