New P4RuntimeClient implementation that supports batching and error reporting

The new client API supports batching and provides detailed response for
write requests (e.g. if entity already exists when inserting), which was
not possible with the old one.

This patch includes:
- New more efficient implementation of P4RuntimeClient (no more locking,
use native gRPC executor, use stub deadlines)
- Ported all codecs to new AbstractCodec-based implementation (needed to
implement codec cache in the future)
- Uses batching in P4RuntimeFlowRuleProgrammable and
P4RuntimeGroupActionProgrammable
- Minor changes to PI framework runtime classes

Change-Id: I3fac42057bb4e1389d761006a32600c786598683
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
new file mode 100644
index 0000000..29cddff
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.utils;
+
+
+import com.google.common.collect.Maps;
+import com.google.protobuf.Message;
+import p4.config.v1.P4InfoOuterClass.Action;
+import p4.config.v1.P4InfoOuterClass.ActionProfile;
+import p4.config.v1.P4InfoOuterClass.ControllerPacketMetadata;
+import p4.config.v1.P4InfoOuterClass.Counter;
+import p4.config.v1.P4InfoOuterClass.DirectCounter;
+import p4.config.v1.P4InfoOuterClass.DirectMeter;
+import p4.config.v1.P4InfoOuterClass.MatchField;
+import p4.config.v1.P4InfoOuterClass.Meter;
+import p4.config.v1.P4InfoOuterClass.P4Info;
+import p4.config.v1.P4InfoOuterClass.Preamble;
+import p4.config.v1.P4InfoOuterClass.Table;
+
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Utility class to easily retrieve information from a P4Info protobuf message.
+ */
+public final class P4InfoBrowser {
+
+    private final EntityBrowser<Table> tables = new EntityBrowser<>("table");
+    private final EntityBrowser<Action> actions = new EntityBrowser<>("action");
+    private final EntityBrowser<ActionProfile> actionProfiles = new EntityBrowser<>("action profile");
+    private final EntityBrowser<Counter> counters = new EntityBrowser<>("counter");
+    private final EntityBrowser<DirectCounter> directCounters = new EntityBrowser<>("direct counter");
+    private final EntityBrowser<Meter> meters = new EntityBrowser<>("meter");
+    private final EntityBrowser<DirectMeter> directMeters = new EntityBrowser<>("direct meter");
+    private final EntityBrowser<ControllerPacketMetadata> ctrlPktMetadatas =
+            new EntityBrowser<>("controller packet metadata");
+    private final Map<Integer, EntityBrowser<Action.Param>> actionParams = Maps.newHashMap();
+    private final Map<Integer, EntityBrowser<MatchField>> matchFields = Maps.newHashMap();
+    private final Map<Integer, EntityBrowser<ControllerPacketMetadata.Metadata>> ctrlPktMetadatasMetadata =
+            Maps.newHashMap();
+
+    /**
+     * Creates a new browser for the given P4Info.
+     *
+     * @param p4info P4Info protobuf message
+     */
+    public P4InfoBrowser(P4Info p4info) {
+        parseP4Info(p4info);
+    }
+
+    private void parseP4Info(P4Info p4info) {
+        p4info.getTablesList().forEach(
+                entity -> {
+                    tables.addWithPreamble(entity.getPreamble(), entity);
+                    // Index match fields.
+                    int tableId = entity.getPreamble().getId();
+                    String tableName = entity.getPreamble().getName();
+                    EntityBrowser<MatchField> matchFieldBrowser = new EntityBrowser<>(format(
+                            "match field for table '%s'", tableName));
+                    entity.getMatchFieldsList().forEach(m -> matchFieldBrowser.add(m.getName(), null, m.getId(), m));
+                    matchFields.put(tableId, matchFieldBrowser);
+                });
+
+        p4info.getActionsList().forEach(
+                entity -> {
+                    actions.addWithPreamble(entity.getPreamble(), entity);
+                    // Index action params.
+                    int actionId = entity.getPreamble().getId();
+                    String actionName = entity.getPreamble().getName();
+                    EntityBrowser<Action.Param> paramBrowser = new EntityBrowser<>(format(
+                            "param for action '%s'", actionName));
+                    entity.getParamsList().forEach(p -> paramBrowser.add(p.getName(), null, p.getId(), p));
+                    actionParams.put(actionId, paramBrowser);
+                });
+
+        p4info.getActionProfilesList().forEach(
+                entity -> actionProfiles.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getCountersList().forEach(
+                entity -> counters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getDirectCountersList().forEach(
+                entity -> directCounters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getMetersList().forEach(
+                entity -> meters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getDirectMetersList().forEach(
+                entity -> directMeters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getControllerPacketMetadataList().forEach(
+                entity -> {
+                    ctrlPktMetadatas.addWithPreamble(entity.getPreamble(), entity);
+                    // Index control packet metadata metadata.
+                    int ctrlPktMetadataId = entity.getPreamble().getId();
+                    String ctrlPktMetadataName = entity.getPreamble().getName();
+                    EntityBrowser<ControllerPacketMetadata.Metadata> metadataBrowser = new EntityBrowser<>(format(
+                            "metadata field for controller packet metadata '%s'", ctrlPktMetadataName));
+                    entity.getMetadataList().forEach(m -> metadataBrowser.add(m.getName(), null, m.getId(), m));
+                    ctrlPktMetadatasMetadata.put(ctrlPktMetadataId, metadataBrowser);
+                });
+    }
+
+    /**
+     * Returns a browser for tables.
+     *
+     * @return table browser
+     */
+    public EntityBrowser<Table> tables() {
+        return tables;
+    }
+
+    /**
+     * Returns a browser for actions.
+     *
+     * @return action browser
+     */
+    public EntityBrowser<Action> actions() {
+        return actions;
+    }
+
+    /**
+     * Returns a browser for action profiles.
+     *
+     * @return action profile browser
+     */
+    public EntityBrowser<ActionProfile> actionProfiles() {
+        return actionProfiles;
+    }
+
+    /**
+     * Returns a browser for counters.
+     *
+     * @return counter browser
+     */
+    public EntityBrowser<Counter> counters() {
+        return counters;
+    }
+
+    /**
+     * Returns a browser for direct counters.
+     *
+     * @return direct counter browser
+     */
+    public EntityBrowser<DirectCounter> directCounters() {
+        return directCounters;
+    }
+
+    /**
+     * Returns a browser for meters.
+     *
+     * @return meter browser
+     */
+    public EntityBrowser<Meter> meters() {
+        return meters;
+    }
+
+    /**
+     * Returns a browser for direct meters.
+     *
+     * @return table browser
+     */
+    public EntityBrowser<DirectMeter> directMeters() {
+        return directMeters;
+    }
+
+    /**
+     * Returns a browser for controller packet metadata.
+     *
+     * @return controller packet metadata browser
+     */
+    public EntityBrowser<ControllerPacketMetadata> controllerPacketMetadatas() {
+        return ctrlPktMetadatas;
+    }
+
+    /**
+     * Returns a browser for params of the given action.
+     *
+     * @param actionId action identifier
+     * @return action params browser
+     * @throws NotFoundException if the action cannot be found
+     */
+    public EntityBrowser<Action.Param> actionParams(int actionId) throws NotFoundException {
+        // Throws exception if action id is not found.
+        actions.getById(actionId);
+        return actionParams.get(actionId);
+    }
+
+    /**
+     * Returns a browser for match fields of the given table.
+     *
+     * @param tableId table identifier
+     * @return match field browser
+     * @throws NotFoundException if the table cannot be found
+     */
+    public EntityBrowser<MatchField> matchFields(int tableId) throws NotFoundException {
+        // Throws exception if action id is not found.
+        tables.getById(tableId);
+        return matchFields.get(tableId);
+    }
+
+    /**
+     * Returns a browser for metadata fields of the controller packet metadata.
+     *
+     * @param controllerPacketMetadataId controller packet metadata identifier
+     * @return metadata browser
+     * @throws NotFoundException controller packet metadata cannot be found
+     */
+    public EntityBrowser<ControllerPacketMetadata.Metadata> packetMetadatas(int controllerPacketMetadataId)
+            throws NotFoundException {
+        // Throws exception if controller packet metadata id is not found.
+        ctrlPktMetadatas.getById(controllerPacketMetadataId);
+        return ctrlPktMetadatasMetadata.get(controllerPacketMetadataId);
+    }
+
+    /**
+     * Browser of P4Info entities.
+     *
+     * @param <T> protobuf message type
+     */
+    public static final class EntityBrowser<T extends Message> {
+
+        private String entityName;
+        private final Map<String, T> names = Maps.newHashMap();
+        private final Map<String, String> aliasToNames = Maps.newHashMap();
+        private final Map<Integer, T> ids = Maps.newHashMap();
+
+        private EntityBrowser(String entityName) {
+            this.entityName = entityName;
+        }
+
+        /**
+         * Adds the given entity identified by the given name, alias (nullable) and id.
+         *
+         * @param name   entity name
+         * @param alias  entity alias or null
+         * @param id     entity id
+         * @param entity entity message
+         */
+        private void add(String name, String alias, int id, T entity) {
+            checkNotNull(name);
+            checkArgument(!name.isEmpty(), "Name cannot be empty");
+            checkNotNull(entity);
+            names.put(name, entity);
+            ids.put(id, entity);
+            if (alias != null && !alias.isEmpty()) {
+                aliasToNames.put(alias, name);
+            }
+        }
+
+        /**
+         * Adds the given entity identified by the given P4Info preamble.
+         *
+         * @param preamble P4Info preamble protobuf message
+         * @param entity   entity message
+         */
+        private void addWithPreamble(Preamble preamble, T entity) {
+            checkNotNull(preamble);
+            add(preamble.getName(), preamble.getAlias(), preamble.getId(), entity);
+        }
+
+        /**
+         * Returns true if the P4Info defines an entity with such name, false otherwise.
+         *
+         * @param name entity name
+         * @return boolean
+         */
+        public boolean hasName(String name) {
+            return names.containsKey(name);
+        }
+
+        /**
+         * Returns the entity identified by the given name, if present, otherwise, throws an exception.
+         *
+         * @param name entity name or alias
+         * @return entity message
+         * @throws NotFoundException if the entity cannot be found
+         */
+        public T getByName(String name) throws NotFoundException {
+            if (hasName(name)) {
+                return names.get(name);
+            } else {
+                final String hint = aliasToNames.containsKey(name)
+                        ? format("Did you mean '%s'? Make sure to use entity names in PI IDs, not aliases",
+                                 aliasToNames.get(name))
+                        : "";
+                throw new NotFoundException(entityName, name, hint);
+            }
+        }
+
+        /**
+         * Returns true if the P4Info defines an entity with such id, false otherwise.
+         *
+         * @param id entity id
+         * @return boolean
+         */
+        public boolean hasId(int id) {
+            return ids.containsKey(id);
+        }
+
+        /**
+         * Returns the entity identified by the given id, if present, otherwise, throws an exception.
+         *
+         * @param id entity id
+         * @return entity message
+         * @throws NotFoundException if the entity cannot be found
+         */
+        public T getById(int id) throws NotFoundException {
+            if (!hasId(id)) {
+                throw new NotFoundException(entityName, id);
+            }
+            return ids.get(id);
+        }
+    }
+
+    /**
+     * Signals tha an entity cannot be found in the P4Info.
+     */
+    public static final class NotFoundException extends Exception {
+
+        public NotFoundException(String entityName, String key, String hint) {
+            super(format(
+                    "No such %s in P4Info with name '%s'%s",
+                    entityName, key, hint.isEmpty() ? "" : " (" + hint + ")"));
+        }
+
+        public NotFoundException(String entityName, int id) {
+            super(format("No such %s in P4Info with id '%d'", entityName, id));
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
new file mode 100644
index 0000000..dec7438
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.p4runtime.ctl.utils;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Maps;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconfId;
+import org.slf4j.Logger;
+import p4.config.v1.P4InfoOuterClass.P4Info;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Utility class to deal with pipeconfs in the context of P4Runtime.
+ */
+public final class PipeconfHelper {
+
+    private static final int P4INFO_BROWSER_EXPIRE_TIME_IN_MIN = 10;
+    private static final Logger log = getLogger(PipeconfHelper.class);
+
+    private static final Cache<PiPipeconfId, P4InfoBrowser> BROWSERS = CacheBuilder.newBuilder()
+            .expireAfterAccess(P4INFO_BROWSER_EXPIRE_TIME_IN_MIN, TimeUnit.MINUTES)
+            .build();
+    private static final Map<PiPipeconfId, P4Info> P4INFOS = Maps.newConcurrentMap();
+
+    private PipeconfHelper() {
+        // hide.
+    }
+
+    /**
+     * Extracts and returns a P4Info protobuf message from the given pipeconf. If the pipeconf does not define any
+     * extension of type {@link PiPipeconf.ExtensionType#P4_INFO_TEXT}, returns null;
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info or null
+     */
+    public static P4Info getP4Info(PiPipeconf pipeconf) {
+        return P4INFOS.computeIfAbsent(pipeconf.id(), piPipeconfId -> {
+            if (!pipeconf.extension(P4_INFO_TEXT).isPresent()) {
+                log.warn("Missing P4Info extension in pipeconf {}", pipeconf.id());
+                return null;
+            }
+
+            InputStream p4InfoStream = pipeconf.extension(P4_INFO_TEXT).get();
+            P4Info.Builder p4iInfoBuilder = P4Info.newBuilder();
+            try {
+                TextFormat.getParser().merge(new InputStreamReader(p4InfoStream), ExtensionRegistry.getEmptyRegistry(),
+                                             p4iInfoBuilder);
+            } catch (IOException ex) {
+                log.warn("Unable to parse P4Info of pipeconf {}: {}", pipeconf.id(), ex.getMessage());
+                return null;
+            }
+
+            return p4iInfoBuilder.build();
+        });
+    }
+
+    /**
+     * Returns a P4Info browser for the given pipeconf. If the pipeconf does not define any extension of type
+     * {@link PiPipeconf.ExtensionType#P4_INFO_TEXT}, returns null;
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info browser or null
+     */
+    public static P4InfoBrowser getP4InfoBrowser(PiPipeconf pipeconf) {
+        try {
+            return BROWSERS.get(pipeconf.id(), () -> {
+                P4Info p4info = PipeconfHelper.getP4Info(pipeconf);
+                if (p4info == null) {
+                    return null;
+                } else {
+                    return new P4InfoBrowser(p4info);
+                }
+            });
+        } catch (ExecutionException e) {
+            log.error("Exception while accessing the P4InfoBrowser cache", e);
+            return null;
+        }
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java
new file mode 100644
index 0000000..ae09455
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Utility classes for the P4Runtime protocol subsystem.
+ */
+package org.onosproject.p4runtime.ctl.utils;