ONOS-7898 Action profile group/member refactoring

Also includes:
- New abstract P4Runtime codec implementation. Currently used for action
profile members/groups encoding/deconding, the plan is to handle all
other codecs via this.
- Improved read requests in P4RuntimeClientImpl
- Removed handling of max group size in P4Runtime driver. Instead, added
modified group translator to specify a max group size by using
information from the pipeline model.

Change-Id: I684bae0184d683bb448ba19863c561f9848479d2
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
index 5d7e93e..ff00a1a 100644
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
@@ -188,9 +188,9 @@
      * Performs the given write operation for the given action profile members
      * and pipeconf.
      *
-     * @param members   action profile members
-     * @param opType    write operation type
-     * @param pipeconf  the pipeconf currently deployed on the device
+     * @param members  action profile members
+     * @param opType   write operation type
+     * @param pipeconf the pipeconf currently deployed on the device
      * @return true if the operation was successful, false otherwise
      */
     CompletableFuture<Boolean> writeActionProfileMembers(
@@ -204,19 +204,15 @@
      * @param group         the action profile group
      * @param opType        write operation type
      * @param pipeconf      the pipeconf currently deployed on the device
-     * @param maxMemberSize the maximum number of members that can be added to
-     *                      the group. This is meaningful only if it's an INSERT
-     *                      operation, otherwise its value should be 0
      * @return true if the operation was successful, false otherwise
      */
     CompletableFuture<Boolean> writeActionProfileGroup(
             PiActionProfileGroup group,
             WriteOperationType opType,
-            PiPipeconf pipeconf,
-            int maxMemberSize);
+            PiPipeconf pipeconf);
 
     /**
-     * Dumps all groups currently installed in the given action profile.
+     * Dumps all groups for a given action profile.
      *
      * @param actionProfileId the action profile id
      * @param pipeconf        the pipeconf currently deployed on the device
@@ -226,13 +222,13 @@
             PiActionProfileId actionProfileId, PiPipeconf pipeconf);
 
     /**
-     * Dumps all action profile member IDs for a given action profile.
+     * Dumps all members for a given action profile.
      *
      * @param actionProfileId action profile ID
      * @param pipeconf        pipeconf
      * @return future of list of action profile member ID
      */
-    CompletableFuture<List<PiActionProfileMemberId>> dumpActionProfileMemberIds(
+    CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf);
 
     /**
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
new file mode 100644
index 0000000..a76ab7c
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
@@ -0,0 +1,218 @@
+/*
+ * 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;
+
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Abstract implementation of a codec that translates PI entities into P4Runtime
+ * protobuf messages and vice versa.
+ *
+ * @param <P> PI entity class
+ * @param <M> P4Runtime protobuf message class
+ */
+abstract class AbstractP4RuntimeCodec<P extends PiEntity, M extends Message> {
+
+    protected final Logger log = getLogger(this.getClass());
+
+    protected abstract M encode(P piEntity, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract P decode(M message, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a P4Runtime protobuf message that is equivalent to the given PI
+     * entity for the given pipeconf.
+     *
+     * @param piEntity PI entity instance
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encode(P piEntity, PiPipeconf pipeconf)
+            throws CodecException {
+        try {
+            return encode(piEntity, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a PI entity instance that is equivalent to the P4Runtime protobuf
+     * message for the given pipeconf.
+     *
+     * @param message  P4Runtime protobuf message
+     * @param pipeconf pipeconf pipeconf
+     * @return PI entity instance
+     * @throws CodecException if the given protobuf message cannot be decoded
+     *                        (see exception message)
+     */
+    public P decode(M message, PiPipeconf pipeconf)
+            throws CodecException {
+        try {
+            return decode(message, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Same as {@link #encode(PiEntity, PiPipeconf)} but returns null in case of
+     * exceptions, while the error message is logged.
+     *
+     * @param piEntity PI entity instance
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     */
+    public M encodeOrNull(P piEntity, PiPipeconf pipeconf) {
+        try {
+            return encode(piEntity, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to encode {}: {} [{}]",
+                      piEntity.getClass().getSimpleName(),
+                      e.getMessage(), piEntity.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Same as {@link #decode(Message, PiPipeconf)} but returns null in case of
+     * exceptions, while the error message is logged.
+     *
+     * @param message  P4Runtime protobuf message
+     * @param pipeconf pipeconf pipeconf
+     * @return PI entity instance
+     */
+    public P decodeOrNull(M message, PiPipeconf pipeconf) {
+        try {
+            return decode(message, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to decode {}: {} [{}]",
+                      message.getClass().getSimpleName(),
+                      e.getMessage(), TextFormat.shortDebugString(message));
+            return null;
+        }
+    }
+
+    /**
+     * Encodes the given list of PI entities, skipping those that cannot be
+     * encoded, in which case an error message is logged. For this reason, the
+     * returned list might have different size than the returned one.
+     *
+     * @param piEntities list of PI entities
+     * @param pipeconf   pipeconf
+     * @return list of P4Runtime protobuf messages
+     */
+    public List<M> encodeAll(List<P> piEntities, PiPipeconf pipeconf) {
+        checkNotNull(piEntities);
+        return piEntities.stream()
+                .map(p -> encodeOrNull(p, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Decodes the given list of P4Runtime protobuf messages, skipping those
+     * that cannot be decoded, on which case an error message is logged. For
+     * this reason, the returned list might have different size than the
+     * returned one.
+     *
+     * @param messages list of protobuf messages
+     * @param pipeconf pipeconf
+     * @return list of PI entities
+     */
+    public List<P> decodeAll(List<M> messages, PiPipeconf pipeconf) {
+        checkNotNull(messages);
+        return messages.stream()
+                .map(m -> decodeOrNull(m, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Same as {@link #encodeAll(List, PiPipeconf)} but throws an exception if
+     * one or more of the given PI entities cannot be encoded. The returned list
+     * is guaranteed to have same size and order as the given one.
+     *
+     * @param piEntities list of PI entities
+     * @param pipeconf   pipeconf
+     * @return list of protobuf messages
+     * @throws CodecException if one or more of the given PI entities cannot be
+     *                        encoded
+     */
+    public List<M> encodeAllOrFail(List<P> piEntities, PiPipeconf pipeconf)
+            throws CodecException {
+        final List<M> messages = encodeAll(piEntities, pipeconf);
+        if (piEntities.size() != messages.size()) {
+            throw new CodecException(format(
+                    "Unable to encode %d entities of %d given " +
+                            "(see previous logs for details)",
+                    piEntities.size() - messages.size(), piEntities.size()));
+        }
+        return messages;
+    }
+
+    /**
+     * Same as {@link #decodeAll(List, PiPipeconf)} but throws an exception if
+     * one or more of the given protobuf messages cannot be decoded. The
+     * returned list is guaranteed to have same size and order as the given
+     * one.
+     *
+     * @param messages list of protobuf messages
+     * @param pipeconf pipeconf
+     * @return list of PI entities
+     * @throws CodecException if one or more of the given protobuf messages
+     *                        cannot be decoded
+     */
+    public List<P> decodeAllOrFail(List<M> messages, PiPipeconf pipeconf)
+            throws CodecException {
+        final List<P> piEntities = decodeAll(messages, pipeconf);
+        if (messages.size() != piEntities.size()) {
+            throw new CodecException(format(
+                    "Unable to decode %d messages of %d given " +
+                            "(see previous logs for details)",
+                    messages.size() - piEntities.size(), messages.size()));
+        }
+        return piEntities;
+    }
+
+    private P4InfoBrowser browserOrFail(PiPipeconf pipeconf) throws CodecException {
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new CodecException(format(
+                    "Unable to get P4InfoBrowser for pipeconf %s", pipeconf.id()));
+        }
+        return browser;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
new file mode 100644
index 0000000..684ef04
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
+
+/**
+ * Codec for P4Runtime ActionProfileGroup.
+ */
+final class ActionProfileGroupCodec
+        extends AbstractP4RuntimeCodec<PiActionProfileGroup, ActionProfileGroup> {
+
+    @Override
+    public ActionProfileGroup encode(
+            PiActionProfileGroup piGroup, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+
+        final int p4ActionProfileId = browser.actionProfiles()
+                .getByName(piGroup.actionProfile().id())
+                .getPreamble().getId();
+        final ActionProfileGroup.Builder msgBuilder = ActionProfileGroup.newBuilder()
+                .setGroupId(piGroup.id().id())
+                .setActionProfileId(p4ActionProfileId)
+                .setMaxSize(piGroup.maxSize());
+        piGroup.members().forEach(m -> {
+            // TODO: currently we don't set "watch" field
+            ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
+                    .setMemberId(m.id().id())
+                    .setWeight(m.weight())
+                    .build();
+            msgBuilder.addMembers(member);
+        });
+
+        return msgBuilder.build();
+    }
+
+    @Override
+    public PiActionProfileGroup decode(
+            ActionProfileGroup msg, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        final PiActionProfileGroup.Builder piGroupBuilder = PiActionProfileGroup.builder()
+                .withActionProfileId(PiActionProfileId.of(
+                        browser.actionProfiles()
+                                .getById(msg.getActionProfileId())
+                                .getPreamble().getName()))
+                .withId(PiActionProfileGroupId.of(msg.getGroupId()))
+                .withMaxSize(msg.getMaxSize());
+
+        msg.getMembersList().forEach(m -> {
+            int weight = m.getWeight();
+            if (weight < 1) {
+                // FIXME: currently PI has a bug which will always return weight 0
+                // ONOS won't accept group buckets with weight 0
+                log.warn("Decoding ActionProfileGroup with 'weight' " +
+                                 "field {}, will set to 1", weight);
+                weight = 1;
+            }
+            piGroupBuilder.addMember(PiActionProfileMemberId.of(
+                    m.getMemberId()), weight);
+        });
+        return piGroupBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
deleted file mode 100644
index 414a0e6..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2017-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;
-
-import com.google.common.collect.Maps;
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileGroup;
-import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup.Member;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-
-import java.util.Collection;
-import java.util.Map;
-
-import static java.lang.String.format;
-
-/**
- * Encoder/Decoder for action profile group.
- */
-final class ActionProfileGroupEncoder {
-
-    private ActionProfileGroupEncoder() {
-        // hide default constructor
-    }
-
-    /**
-     * Encode a PI action profile group to a action profile group.
-     *
-     * @param piActionGroup the action profile group
-     * @param pipeconf      the pipeconf
-     * @param maxMemberSize the max member size of action group
-     * @return a action profile group encoded from PI action profile group
-     * @throws P4InfoBrowser.NotFoundException if can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 if can't find P4Info from
-     *                                         pipeconf
-     */
-    static ActionProfileGroup encode(PiActionProfileGroup piActionGroup, PiPipeconf pipeconf, int maxMemberSize)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        PiActionProfileId piActionProfileId = piActionGroup.actionProfileId();
-        P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
-                .getByName(piActionProfileId.id());
-        int actionProfileId = actionProfile.getPreamble().getId();
-        ActionProfileGroup.Builder actionProfileGroupBuilder = ActionProfileGroup.newBuilder()
-                .setGroupId(piActionGroup.id().id())
-                .setActionProfileId(actionProfileId);
-
-        piActionGroup.members().forEach(m -> {
-            // TODO: currently we don't set "watch" field of member
-            Member member = Member.newBuilder()
-                    .setMemberId(m.id().id())
-                    .setWeight(m.weight())
-                    .build();
-            actionProfileGroupBuilder.addMembers(member);
-        });
-
-        if (maxMemberSize > 0) {
-            actionProfileGroupBuilder.setMaxSize(maxMemberSize);
-        }
-
-        return actionProfileGroupBuilder.build();
-    }
-
-    /**
-     * Decode an action profile group with members information to a PI action
-     * profile group.
-     *
-     * @param actionProfileGroup the action profile group
-     * @param members            members of the action profile group
-     * @param pipeconf           the pipeconf
-     * @return decoded PI action profile group
-     * @throws P4InfoBrowser.NotFoundException if can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 if can't find P4Info from
-     *                                         pipeconf
-     */
-    static PiActionProfileGroup decode(ActionProfileGroup actionProfileGroup,
-                                       Collection<ActionProfileMember> members,
-                                       PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-        PiActionProfileGroup.Builder piActionGroupBuilder = PiActionProfileGroup.builder();
-
-        P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
-                .getById(actionProfileGroup.getActionProfileId());
-        PiActionProfileId piActionProfileId = PiActionProfileId.of(actionProfile.getPreamble().getName());
-
-        piActionGroupBuilder
-                .withActionProfileId(piActionProfileId)
-                .withId(PiActionProfileGroupId.of(actionProfileGroup.getGroupId()));
-
-        Map<Integer, Integer> memberWeights = Maps.newHashMap();
-        actionProfileGroup.getMembersList().forEach(member -> {
-            int weight = member.getWeight();
-            if (weight < 1) {
-                // FIXME: currently PI has a bug which will always return weight 0
-                // ONOS won't accept group buckets with weight 0
-                weight = 1;
-            }
-            memberWeights.put(member.getMemberId(), weight);
-        });
-
-        for (ActionProfileMember member : members) {
-            if (!memberWeights.containsKey(member.getMemberId())) {
-                // Not a member of this group, ignore.
-                continue;
-            }
-            int weight = memberWeights.get(member.getMemberId());
-            piActionGroupBuilder.addMember(ActionProfileMemberEncoder.decode(member, weight, pipeconf));
-        }
-
-        return piActionGroupBuilder.build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
new file mode 100644
index 0000000..9567b0a
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
+
+import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decodeActionMsg;
+import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encodePiAction;
+/**
+ * Codec for P4Runtime ActionProfileMember.
+ */
+final class ActionProfileMemberCodec
+        extends AbstractP4RuntimeCodec<PiActionProfileMember, ActionProfileMember> {
+
+    @Override
+    public ActionProfileMember encode(PiActionProfileMember piEntity,
+                                      PiPipeconf pipeconf,
+                                      P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final ActionProfileMember.Builder actionProfileMemberBuilder =
+                ActionProfileMember.newBuilder();
+        // Member ID
+        actionProfileMemberBuilder.setMemberId(piEntity.id().id());
+        // Action profile ID
+        P4InfoOuterClass.ActionProfile actionProfile =
+                browser.actionProfiles().getByName(piEntity.actionProfile().id());
+        final int actionProfileId = actionProfile.getPreamble().getId();
+        actionProfileMemberBuilder.setActionProfileId(actionProfileId);
+        // Action
+        final P4RuntimeOuterClass.Action action = encodePiAction(piEntity.action(), browser);
+        actionProfileMemberBuilder.setAction(action);
+        return actionProfileMemberBuilder.build();
+    }
+
+    @Override
+    public PiActionProfileMember decode(ActionProfileMember message,
+                                        PiPipeconf pipeconf,
+                                        P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final PiActionProfileId actionProfileId = PiActionProfileId.of(
+                browser.actionProfiles()
+                        .getById(message.getActionProfileId())
+                        .getPreamble()
+                        .getName());
+        return PiActionProfileMember.builder()
+                .forActionProfile(actionProfileId)
+                .withId(PiActionProfileMemberId.of(message.getMemberId()))
+                .withAction(decodeActionMsg(message.getAction(), browser))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
deleted file mode 100644
index 30dd43c..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2017-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;
-
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileMember;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-
-import static java.lang.String.format;
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decodeActionMsg;
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encodePiAction;
-
-/**
- * Encoder/Decoder of action profile member.
- */
-final class ActionProfileMemberEncoder {
-    private ActionProfileMemberEncoder() {
-        // Hide default constructor
-    }
-
-    /**
-     * Encode a PiActionProfileMember to a ActionProfileMember.
-     *
-     * @param member    the member to encode
-     * @param pipeconf  the pipeconf, as encode spec
-     * @return encoded member
-     * @throws P4InfoBrowser.NotFoundException can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 can't find P4Info from pipeconf
-     */
-    static ActionProfileMember encode(PiActionProfileMember member,
-                                      PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        ActionProfileMember.Builder actionProfileMemberBuilder =
-                ActionProfileMember.newBuilder();
-
-        // member id
-        actionProfileMemberBuilder.setMemberId(member.id().id());
-
-        // action profile id
-        P4InfoOuterClass.ActionProfile actionProfile =
-                browser.actionProfiles().getByName(member.actionProfile().id());
-
-        int actionProfileId = actionProfile.getPreamble().getId();
-        actionProfileMemberBuilder.setActionProfileId(actionProfileId);
-
-        // Action
-        P4RuntimeOuterClass.Action action = encodePiAction(member.action(), browser);
-        actionProfileMemberBuilder.setAction(action);
-
-        return actionProfileMemberBuilder.build();
-    }
-
-    /**
-     * Decode an action profile member to PI action profile member.
-     *
-     * @param member   the action profile member
-     * @param weight   the weight of the member
-     * @param pipeconf the pipeconf, as decode spec
-     * @return decoded PI action profile member
-     * @throws P4InfoBrowser.NotFoundException can't find definition of action
-     *                                         from P4 info
-     * @throws EncodeException                 can't get P4 info browser from
-     *                                         pipeconf
-     */
-    static PiActionProfileMember decode(ActionProfileMember member,
-                                        int weight,
-                                        PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        final PiActionProfileId actionProfileId = PiActionProfileId.of(
-                browser.actionProfiles()
-                        .getById(member.getActionProfileId())
-                        .getPreamble()
-                        .getName());
-
-        return PiActionProfileMember.builder()
-                .forActionProfile(actionProfileId)
-                .withId(PiActionProfileMemberId.of(member.getMemberId()))
-                .withWeight(weight)
-                .withAction(decodeActionMsg(member.getAction(), browser))
-                .build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
similarity index 65%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
index 51abbb5..89d5510 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * 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.
@@ -17,11 +17,15 @@
 package org.onosproject.p4runtime.ctl;
 
 /**
- * Signals that the proto message cannot be build.
+ * Signals an error during encoding/decoding of a PI entity/protobuf message.
  */
-final class EncodeException extends Exception {
+public final class CodecException extends Exception {
 
-    EncodeException(String explanation) {
+    /**
+     * Ceeates anew exception with the given explanation message.
+     * @param explanation explanation
+     */
+    public CodecException(String explanation) {
         super(explanation);
     }
 }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
index 6c29062..3765d24 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
@@ -80,7 +80,7 @@
                 .map(cellId -> {
                     try {
                         return encodePiCounterCellId(cellId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode PI counter cell id: {}", e.getMessage());
                         return null;
                     }
@@ -114,7 +114,7 @@
                 .map(counterId -> {
                     try {
                         return readAllCellsEntity(counterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode counter ID to read-all-cells entity: {}",
                                  e.getMessage());
                         return null;
@@ -152,7 +152,7 @@
                 .map(entity -> {
                     try {
                         return decodeCounterEntity(entity, pipeconf, browser);
-                    } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
+                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
                         log.warn("Unable to decode counter entity message: {}",
                                  e.getMessage());
                         return null;
@@ -165,7 +165,7 @@
     private static Entity encodePiCounterCellId(PiCounterCellId cellId,
                                                 PiPipeconf pipeconf,
                                                 P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int counterId;
         Entity entity;
@@ -193,7 +193,7 @@
                         .build();
                 break;
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Unrecognized PI counter cell ID type '%s'",
                         cellId.counterType()));
         }
@@ -204,10 +204,10 @@
     private static Entity readAllCellsEntity(PiCounterId counterId,
                                              PiPipeconf pipeconf,
                                              P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         if (!pipeconf.pipelineModel().counter(counterId).isPresent()) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "not such counter '%s' in pipeline model", counterId));
         }
         final PiCounterType counterType = pipeconf.pipelineModel()
@@ -228,7 +228,7 @@
                 final PiTableId tableId = pipeconf.pipelineModel()
                         .counter(counterId).get().table();
                 if (tableId == null) {
-                    throw new EncodeException(format(
+                    throw new CodecException(format(
                             "null table for direct counter '%s'", counterId));
                 }
                 final int p4TableId = browser.tables().getByName(tableId.id())
@@ -243,7 +243,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "unrecognized PI counter type '%s'", counterType));
         }
     }
@@ -251,7 +251,7 @@
     private static PiCounterCell decodeCounterEntity(Entity entity,
                                                      PiPipeconf pipeconf,
                                                      P4InfoBrowser browser)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         CounterData counterData;
         PiCounterCellId piCellId;
@@ -271,7 +271,7 @@
             piCellId = PiCounterCellId.ofDirect(piTableEntry);
             counterData = entity.getDirectCounterEntry().getData();
         } else {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "Unrecognized entity type '%s' in P4Runtime message",
                     entity.getEntityCase().name()));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
index a825488..44f27d1 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
@@ -80,7 +80,7 @@
                 .map(cellConfig -> {
                     try {
                         return encodePiMeterCellConfig(cellConfig, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode PI meter cell id: {}", e.getMessage());
                         log.debug("exception", e);
                         return null;
@@ -115,7 +115,7 @@
                 .map(meterId -> {
                     try {
                         return readAllCellsEntity(meterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode meter ID to read-all-cells entity: {}",
                                  e.getMessage());
                         return null;
@@ -152,7 +152,7 @@
                 .map(entity -> {
                     try {
                         return decodeMeterEntity(entity, pipeconf, browser);
-                    } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
+                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
                         log.warn("Unable to decode meter entity message: {}", e.getMessage());
                         return null;
                     }
@@ -164,7 +164,7 @@
     private static Entity encodePiMeterCellConfig(PiMeterCellConfig config,
                                                   PiPipeconf pipeconf,
                                                   P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int meterId;
         Entity entity;
@@ -198,7 +198,7 @@
             // When reading meter cells.
             meterConfig = null;
         } else {
-            throw new EncodeException("number of meter bands should be either 2 or 0");
+            throw new CodecException("number of meter bands should be either 2 or 0");
         }
 
         switch (config.cellId().meterType()) {
@@ -226,8 +226,8 @@
                         .setDirectMeterEntry(dirEntryBuilder.build()).build();
                 break;
             default:
-                throw new EncodeException(format("unrecognized PI meter type '%s'",
-                                                 config.cellId().meterType()));
+                throw new CodecException(format("unrecognized PI meter type '%s'",
+                                                config.cellId().meterType()));
         }
 
         return entity;
@@ -236,10 +236,10 @@
     private static Entity readAllCellsEntity(PiMeterId meterId,
                                              PiPipeconf pipeconf,
                                              P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         if (!pipeconf.pipelineModel().meter(meterId).isPresent()) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "not such meter '%s' in pipeline model", meterId));
         }
         final PiMeterType meterType = pipeconf.pipelineModel()
@@ -260,7 +260,7 @@
                 final PiTableId tableId = pipeconf.pipelineModel()
                         .meter(meterId).get().table();
                 if (tableId == null) {
-                    throw new EncodeException(format(
+                    throw new CodecException(format(
                             "null table for direct meter '%s'", meterId));
                 }
                 final int p4TableId = browser.tables().getByName(tableId.id())
@@ -275,7 +275,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "unrecognized PI meter type '%s'", meterType));
         }
     }
@@ -283,7 +283,7 @@
     private static PiMeterCellConfig decodeMeterEntity(Entity entity,
                                                        PiPipeconf pipeconf,
                                                        P4InfoBrowser browser)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         MeterConfig meterConfig;
         PiMeterCellId piCellId;
@@ -304,7 +304,7 @@
             piCellId = PiMeterCellId.ofDirect(piTableEntry);
             meterConfig = entity.getDirectMeterEntry().getConfig();
         } else {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "unrecognized entity type '%s' in P4Runtime message",
                     entity.getEntityCase().name()));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
index f2ececb..5f55c1f 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
@@ -40,9 +40,9 @@
      *
      * @param piEntry PiMulticastGroupEntry
      * @return P4Runtime MulticastGroupEntry message
-     * @throws EncodeException if the PiMulticastGroupEntry cannot be encoded.
+     * @throws CodecException if the PiMulticastGroupEntry cannot be encoded.
      */
-    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) throws EncodeException {
+    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) throws CodecException {
         final MulticastGroupEntry.Builder msgBuilder = MulticastGroupEntry.newBuilder();
         msgBuilder.setMulticastGroupId(piEntry.groupId());
         for (PiPreReplica replica : piEntry.replicas()) {
@@ -50,7 +50,7 @@
             try {
                 p4PortId = Math.toIntExact(replica.egressPort().toLong());
             } catch (ArithmeticException e) {
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Cannot cast 64bit port value '%s' to 32bit",
                         replica.egressPort()));
             }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
index 78ea9d9..6ac478f 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
@@ -16,10 +16,8 @@
 
 package org.onosproject.p4runtime.ctl;
 
-import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
@@ -86,14 +84,21 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
+import java.util.stream.Stream;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.lang.String.format;
 import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static org.onosproject.p4runtime.ctl.P4RuntimeCodecs.CODECS;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_GROUP;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_MEMBER;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_COUNTER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_METER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.METER_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.PACKET_REPLICATION_ENGINE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.PacketIn;
@@ -106,10 +111,13 @@
  */
 final class P4RuntimeClientImpl extends AbstractGrpcClient implements P4RuntimeClient {
 
+    private static final String MISSING_P4INFO_BROWSER = "Unable to get a P4Info browser for pipeconf {}";
+
     private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
-            Metadata.Key.of("grpc-status-details-bin",
-                            ProtoLiteUtils.metadataMarshaller(
-                                    com.google.rpc.Status.getDefaultInstance()));
+            Metadata.Key.of(
+                    "grpc-status-details-bin",
+                    ProtoLiteUtils.metadataMarshaller(
+                            com.google.rpc.Status.getDefaultInstance()));
 
     private static final Map<WriteOperationType, Update.Type> UPDATE_TYPES = ImmutableMap.of(
             WriteOperationType.UNSPECIFIED, Update.Type.UNSPECIFIED,
@@ -229,9 +237,8 @@
     @Override
     public CompletableFuture<Boolean> writeActionProfileGroup(PiActionProfileGroup group,
                                                               WriteOperationType opType,
-                                                              PiPipeconf pipeconf,
-                                                       int maxMemberSize) {
-        return supplyInContext(() -> doWriteActionProfileGroup(group, opType, pipeconf, maxMemberSize),
+                                                              PiPipeconf pipeconf) {
+        return supplyInContext(() -> doWriteActionProfileGroup(group, opType, pipeconf),
                                "writeActionProfileGroup-" + opType.name());
     }
 
@@ -243,10 +250,10 @@
     }
 
     @Override
-    public CompletableFuture<List<PiActionProfileMemberId>> dumpActionProfileMemberIds(
+    public CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpActionProfileMemberIds(actionProfileId, pipeconf),
-                               "dumpActionProfileMemberIds-" + actionProfileId.id());
+        return supplyInContext(() -> doDumpActionProfileMembers(actionProfileId, pipeconf),
+                               "dumpActionProfileMembers-" + actionProfileId.id());
     }
 
     @Override
@@ -484,8 +491,8 @@
                                                             .build())
                                          .setType(UPDATE_TYPES.get(opType))
                                          .build())
-                    .collect(Collectors.toList());
-        } catch (EncodeException e) {
+                    .collect(toList());
+        } catch (CodecException e) {
             log.error("Unable to encode table entries, aborting {} operation: {}",
                       opType.name(), e.getMessage());
             return false;
@@ -507,7 +514,7 @@
         } else {
             P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
             if (browser == null) {
-                log.warn("Unable to get a P4Info browser for pipeconf {}", pipeconf);
+                log.error(MISSING_P4INFO_BROWSER, pipeconf);
                 return Collections.emptyList();
             }
             piTableIds.forEach(piTableId -> {
@@ -523,36 +530,18 @@
             return Collections.emptyList();
         }
 
-        ReadRequest.Builder requestMsgBuilder = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId);
-        tableIds.forEach(tableId -> requestMsgBuilder.addEntities(
-                Entity.newBuilder()
-                        .setTableEntry(
-                                TableEntry.newBuilder()
-                                        .setTableId(tableId)
-                                        .setIsDefaultAction(defaultEntries)
-                                        .setCounterData(P4RuntimeOuterClass.CounterData.getDefaultInstance())
-                                        .build())
+        final List<Entity> entities = tableIds.stream()
+                .map(tableId ->  TableEntry.newBuilder()
+                        .setTableId(tableId)
+                        .setIsDefaultAction(defaultEntries)
+                        .setCounterData(P4RuntimeOuterClass.CounterData.getDefaultInstance())
                         .build())
-                .build());
+                .map(e -> Entity.newBuilder().setTableEntry(e).build())
+                .collect(toList());
 
-        Iterator<ReadResponse> responses;
-        try {
-            responses = blockingStub.read(requestMsgBuilder.build());
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to dump tables from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        Iterable<ReadResponse> responseIterable = () -> responses;
-        List<TableEntry> tableEntryMsgs = StreamSupport
-                .stream(responseIterable.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(entity -> entity.getEntityCase() == TABLE_ENTRY)
+        final List<TableEntry> tableEntryMsgs = blockingRead(entities, TABLE_ENTRY)
                 .map(Entity::getTableEntry)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} entries from {} tables on {}...",
                   tableEntryMsgs.size(), tableIds.size(), deviceId);
@@ -641,61 +630,32 @@
     private List<PiCounterCell> doReadCounterEntities(
             List<Entity> counterEntities, PiPipeconf pipeconf) {
 
-        if (counterEntities.size() == 0) {
-            return Collections.emptyList();
-        }
-
-        final ReadRequest request = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addAllEntities(counterEntities)
-                .build();
-
-        final Iterable<ReadResponse> responses;
-        try {
-            responses = () -> blockingStub.read(request);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read counter cells from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        List<Entity> entities = StreamSupport.stream(responses.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .collect(Collectors.toList());
+        final List<Entity> entities = blockingRead(
+                counterEntities, COUNTER_ENTRY, DIRECT_COUNTER_ENTRY)
+                .collect(toList());
 
         return CounterEntryCodec.decodeCounterEntities(entities, pipeconf);
     }
 
     private boolean doWriteActionProfileMembers(List<PiActionProfileMember> members,
                                                 WriteOperationType opType, PiPipeconf pipeconf) {
-        final List<ActionProfileMember> actionProfileMembers = Lists.newArrayList();
-
-        for (PiActionProfileMember member : members) {
-            try {
-                actionProfileMembers.add(ActionProfileMemberEncoder.encode(member, pipeconf));
-            } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-                log.warn("Unable to encode action profile member, aborting {} operation: {} [{}]",
-                         opType.name(), e.getMessage(), member.toString());
-                return false;
-            }
+        final List<ActionProfileMember> actionProfileMembers;
+        try {
+            actionProfileMembers = CODECS.actionProfileMember()
+                    .encodeAllOrFail(members, pipeconf);
+        } catch (CodecException e) {
+            log.warn("Unable to {} action profile members: {}",
+                     opType.name(), e.getMessage());
+            return false;
         }
-
         final List<Update> updateMsgs = actionProfileMembers.stream()
-                .map(actionProfileMember ->
-                             Update.newBuilder()
-                                     .setEntity(Entity.newBuilder()
-                                                        .setActionProfileMember(actionProfileMember)
-                                                        .build())
-                                     .setType(UPDATE_TYPES.get(opType))
-                                     .build())
-                .collect(Collectors.toList());
-
-        if (updateMsgs.size() == 0) {
-            // Nothing to update.
-            return true;
-        }
-
+                .map(m -> Update.newBuilder()
+                        .setEntity(Entity.newBuilder()
+                                           .setActionProfileMember(m)
+                                           .build())
+                        .setType(UPDATE_TYPES.get(opType))
+                        .build())
+                .collect(toList());
         return write(updateMsgs, members, opType, "action profile member");
     }
 
@@ -705,7 +665,7 @@
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, aborting dump action profile", pipeconf);
+            log.warn(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -721,117 +681,29 @@
             return Collections.emptyList();
         }
 
-        // Prepare read request to read all groups from the given action profile.
-        final ReadRequest groupRequestMsg = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(Entity.newBuilder()
-                                     .setActionProfileGroup(
-                                             ActionProfileGroup.newBuilder()
-                                                     .setActionProfileId(actionProfileId)
-                                                     .build())
-                                     .build())
+        // Read all groups from the given action profile.
+        final Entity entityToRead = Entity.newBuilder()
+                .setActionProfileGroup(
+                        ActionProfileGroup.newBuilder()
+                                .setActionProfileId(actionProfileId)
+                                .build())
                 .build();
-
-        // Read groups.
-        final Iterator<ReadResponse> groupResponses;
-        try {
-            groupResponses = blockingStub.read(groupRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to dump action profile {} from {}: {}", piActionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        final List<ActionProfileGroup> groupMsgs = Tools.stream(() -> groupResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(entity -> entity.getEntityCase() == ACTION_PROFILE_GROUP)
+        final List<ActionProfileGroup> groupMsgs = blockingRead(entityToRead, ACTION_PROFILE_GROUP)
                 .map(Entity::getActionProfileGroup)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} groups from action profile {} on {}...",
                   groupMsgs.size(), piActionProfileId.id(), deviceId);
 
-        // Returned groups contain only a minimal description of their members.
-        // We need to issue a new request to get the full description of each member.
-
-        // Keep a map of all member IDs for each group ID, will need it later.
-        final Multimap<Integer, Integer> groupIdToMemberIdsMap = HashMultimap.create();
-        groupMsgs.forEach(g -> groupIdToMemberIdsMap.putAll(
-                g.getGroupId(),
-                g.getMembersList().stream()
-                        .map(ActionProfileGroup.Member::getMemberId)
-                        .collect(Collectors.toList())));
-
-        // Prepare one big read request to read all members in one shot.
-        final Set<Entity> entityMsgs = groupMsgs.stream()
-                .flatMap(g -> g.getMembersList().stream())
-                .map(ActionProfileGroup.Member::getMemberId)
-                // Prevent issuing many read requests for the same member.
-                .distinct()
-                .map(id -> ActionProfileMember.newBuilder()
-                        .setActionProfileId(actionProfileId)
-                        .setMemberId(id)
-                        .build())
-                .map(m -> Entity.newBuilder()
-                        .setActionProfileMember(m)
-                        .build())
-                .collect(Collectors.toSet());
-        final ReadRequest memberRequestMsg = ReadRequest.newBuilder().setDeviceId(p4DeviceId)
-                .addAllEntities(entityMsgs)
-                .build();
-
-        // Read members.
-        final Iterator<ReadResponse> memberResponses;
-        try {
-            memberResponses = blockingStub.read(memberRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read members of action profile {} from {}: {}",
-                     piActionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        final Multimap<Integer, ActionProfileMember> groupIdToMembersMap = HashMultimap.create();
-        Tools.stream(() -> memberResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
-                .map(Entity::getActionProfileMember)
-                .forEach(member -> groupIdToMemberIdsMap.asMap()
-                        // Get all group IDs that contain this member.
-                        .entrySet()
-                        .stream()
-                        .filter(entry -> entry.getValue().contains(member.getMemberId()))
-                        .map(Map.Entry::getKey)
-                        .forEach(gid -> groupIdToMembersMap.put(gid, member)));
-
-        log.debug("Retrieved {} members from action profile {} on {}...",
-                  groupIdToMembersMap.size(), piActionProfileId.id(), deviceId);
-
-        return groupMsgs.stream()
-                .map(groupMsg -> {
-                    try {
-                        return ActionProfileGroupEncoder.decode(groupMsg,
-                                                                groupIdToMembersMap.get(groupMsg.getGroupId()),
-                                                                pipeconf);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
-                        log.warn("Unable to decode group: {}\n {}", e.getMessage(), groupMsg);
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+        return CODECS.actionProfileGroup().decodeAll(groupMsgs, pipeconf);
     }
 
-    private List<PiActionProfileMemberId> doDumpActionProfileMemberIds(
+    private List<PiActionProfileMember> doDumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, " +
-                             "aborting cleanup of action profile members",
-                     pipeconf);
+            log.error(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -843,40 +715,24 @@
                     .getPreamble()
                     .getId();
         } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to cleanup action profile members: {}", e.getMessage());
+            log.warn("Unable to dump action profile members: {}", e.getMessage());
             return Collections.emptyList();
         }
 
-        final ReadRequest memberRequestMsg = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(Entity.newBuilder().setActionProfileMember(
+        Entity entityToRead = Entity.newBuilder()
+                .setActionProfileMember(
                         ActionProfileMember.newBuilder()
                                 .setActionProfileId(p4ActProfId)
-                                .build()).build())
+                                .build())
                 .build();
-
-        // Read members.
-        final Iterator<ReadResponse> memberResponses;
-        try {
-            memberResponses = blockingStub.read(memberRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read members of action profile {} from {}: {}",
-                     actionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        return Tools.stream(() -> memberResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
+        final List<ActionProfileMember> memberMsgs = blockingRead(entityToRead, ACTION_PROFILE_MEMBER)
                 .map(Entity::getActionProfileMember)
-                // Perhaps not needed, but better to double check to avoid
-                // removing members of other groups.
-                .filter(m -> m.getActionProfileId() == p4ActProfId)
-                .map(ActionProfileMember::getMemberId)
-                .map(PiActionProfileMemberId::of)
-                .collect(Collectors.toList());
+                .collect(toList());
+
+        log.debug("Retrieved {} members from action profile {} on {}...",
+                  memberMsgs.size(), actionProfileId.id(), deviceId);
+
+        return CODECS.actionProfileMember().decodeAll(memberMsgs, pipeconf);
     }
 
     private List<PiActionProfileMemberId> doRemoveActionProfileMembers(
@@ -890,9 +746,7 @@
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, " +
-                             "aborting cleanup of action profile members",
-                     pipeconf);
+            log.error(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -912,7 +766,7 @@
                 .map(m -> Entity.newBuilder().setActionProfileMember(m).build())
                 .map(e -> Update.newBuilder().setEntity(e)
                         .setType(Update.Type.DELETE).build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Removing {} members of action profile '{}'...",
                   memberIds.size(), actionProfileId);
@@ -923,23 +777,19 @@
     }
 
     private boolean doWriteActionProfileGroup(
-            PiActionProfileGroup group, WriteOperationType opType, PiPipeconf pipeconf,
-                                       int maxMemberSize) {
-        final ActionProfileGroup actionProfileGroup;
-        if (opType == P4RuntimeClient.WriteOperationType.INSERT && maxMemberSize < group.members().size()) {
-            log.warn("Unable to encode group, since group member larger than maximum member size");
-            return false;
-        }
+            PiActionProfileGroup group, WriteOperationType opType, PiPipeconf pipeconf) {
+        final ActionProfileGroup groupMsg;
         try {
-            actionProfileGroup = ActionProfileGroupEncoder.encode(group, pipeconf, maxMemberSize);
-        } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to encode group, aborting {} operation: {}", e.getMessage(), opType.name());
+            groupMsg = CODECS.actionProfileGroup().encode(group, pipeconf);
+        } catch (CodecException e) {
+            log.warn("Unable to encode group, aborting {} operation: {}",
+                     opType.name(), e.getMessage());
             return false;
         }
 
         final Update updateMsg = Update.newBuilder()
                 .setEntity(Entity.newBuilder()
-                                   .setActionProfileGroup(actionProfileGroup)
+                                   .setActionProfileGroup(groupMsg)
                                    .build())
                 .setType(UPDATE_TYPES.get(opType))
                 .build();
@@ -961,7 +811,7 @@
                 .map(cellId -> PiMeterCellConfig.builder()
                         .withMeterCellId(cellId)
                         .build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         return doReadMeterEntities(MeterEntryCodec.encodePiMeterCellConfigs(
                 piMeterCellConfigs, pipeconf), pipeconf);
@@ -970,30 +820,9 @@
     private List<PiMeterCellConfig> doReadMeterEntities(
             List<Entity> entitiesToRead, PiPipeconf pipeconf) {
 
-        if (entitiesToRead.size() == 0) {
-            return Collections.emptyList();
-        }
-
-        final ReadRequest request = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addAllEntities(entitiesToRead)
-                .build();
-
-        final Iterable<ReadResponse> responses;
-        try {
-            responses = () -> blockingStub.read(request);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read meter cells: {}", e.getMessage());
-            log.debug("exception", e);
-            return Collections.emptyList();
-        }
-
-        List<Entity> responseEntities = StreamSupport
-                .stream(responses.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .collect(Collectors.toList());
+        final List<Entity> responseEntities = blockingRead(
+                entitiesToRead, METER_ENTRY, DIRECT_METER_ENTRY)
+                .collect(toList());
 
         return MeterEntryCodec.decodeMeterEntities(responseEntities, pipeconf);
     }
@@ -1007,7 +836,7 @@
                                      .setEntity(meterEntryMsg)
                                      .setType(UPDATE_TYPES.get(WriteOperationType.MODIFY))
                                      .build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         if (updateMsgs.size() == 0) {
             return true;
@@ -1024,7 +853,7 @@
                 .map(piEntry -> {
                     try {
                         return MulticastGroupEntryCodec.encode(piEntry);
-                    } catch (EncodeException e) {
+                    } catch (CodecException e) {
                         log.warn("Unable to encode PiMulticastGroupEntry: {}", e.getMessage());
                         return null;
                     }
@@ -1040,7 +869,7 @@
                         .setEntity(entityMsg)
                         .setType(UPDATE_TYPES.get(opType))
                         .build())
-                .collect(Collectors.toList());
+                .collect(toList());
         return write(updateMsgs, entries, opType, "multicast group entry");
     }
 
@@ -1055,32 +884,12 @@
                                 .build())
                 .build();
 
-        final ReadRequest req = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(entity)
-                .build();
-
-        Iterator<ReadResponse> responses;
-        try {
-            responses = blockingStub.read(req);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read multicast group entries from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        Iterable<ReadResponse> responseIterable = () -> responses;
-        final List<PiMulticastGroupEntry> mcEntries = StreamSupport
-                .stream(responseIterable.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase()
-                        .equals(PACKET_REPLICATION_ENGINE_ENTRY))
+        final List<PiMulticastGroupEntry> mcEntries = blockingRead(entity, PACKET_REPLICATION_ENGINE_ENTRY)
                 .map(Entity::getPacketReplicationEngineEntry)
                 .filter(e -> e.getTypeCase().equals(MULTICAST_GROUP_ENTRY))
                 .map(PacketReplicationEngineEntry::getMulticastGroupEntry)
                 .map(MulticastGroupEntryCodec::decode)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} multicast group entries from {}...",
                   mcEntries.size(), deviceId);
@@ -1126,6 +935,46 @@
                 .build();
     }
 
+    private Stream<Entity> blockingRead(Entity entity, Entity.EntityCase entityCase) {
+        return blockingRead(singletonList(entity), entityCase);
+    }
+
+    private Stream<Entity> blockingRead(Iterable<Entity> entities,
+                                        Entity.EntityCase... entityCases) {
+        // Build read request making sure we are reading what declared.
+        final ReadRequest.Builder reqBuilder = ReadRequest.newBuilder()
+                .setDeviceId(p4DeviceId);
+        final Set<Entity.EntityCase> entityCaseSet = Sets.newHashSet(entityCases);
+        for (Entity e : entities) {
+            checkArgument(entityCaseSet.contains(e.getEntityCase()),
+                          "Entity case mismatch");
+            reqBuilder.addEntities(e);
+        }
+        final ReadRequest readRequest = reqBuilder.build();
+        if (readRequest.getEntitiesCount() == 0) {
+            return Stream.empty();
+        }
+        // Issue read.
+        final Iterator<ReadResponse> responseIterator;
+        try {
+            responseIterator = blockingStub.read(readRequest);
+        } catch (StatusRuntimeException e) {
+            checkGrpcException(e);
+            final String caseString = entityCaseSet.stream()
+                    .map(Entity.EntityCase::name)
+                    .collect(joining("/"));
+            log.warn("Unable to read {} from {}: {}",
+                     caseString, deviceId, e.getMessage());
+            log.debug("Exception during read", e);
+            return Stream.empty();
+        }
+        // Filter results.
+        return Tools.stream(() -> responseIterator)
+                .map(ReadResponse::getEntitiesList)
+                .flatMap(List::stream)
+                .filter(e -> entityCaseSet.contains(e.getEntityCase()));
+    }
+
     protected Void doShutdown() {
         streamChannelManager.complete();
         return super.doShutdown();
@@ -1195,7 +1044,7 @@
                     }
                 })
                 .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+                .collect(toList());
     }
 
     private String parseP4Error(P4RuntimeOuterClass.Error err) {
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
new file mode 100644
index 0000000..1126fef
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * Utility class that provides access to P4Runtime codec instances.
+ */
+final class P4RuntimeCodecs {
+
+    static final P4RuntimeCodecs CODECS = new P4RuntimeCodecs();
+
+    private final ActionProfileMemberCodec actionProfileMember;
+    private final ActionProfileGroupCodec actionProfileGroup;
+
+    private P4RuntimeCodecs() {
+        this.actionProfileMember = new ActionProfileMemberCodec();
+        this.actionProfileGroup = new ActionProfileGroupCodec();
+    }
+
+    ActionProfileMemberCodec actionProfileMember() {
+        return actionProfileMember;
+    }
+
+    ActionProfileGroupCodec actionProfileGroup() {
+        return actionProfileGroup;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
index 5dafe2b..604b1f0 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
@@ -31,20 +31,21 @@
     }
 
     static void assertSize(String entityDescr, ByteString value, int bitWidth)
-            throws EncodeException {
+            throws CodecException {
 
         int byteWidth = (int) Math.ceil((float) bitWidth / 8);
         if (value.size() != byteWidth) {
-            throw new EncodeException(format("Wrong size for %s, expected %d bytes, but found %d",
-                                             entityDescr, byteWidth, value.size()));
+            throw new CodecException(format(
+                    "Wrong size for %s, expected %d bytes, but found %d",
+                    entityDescr, byteWidth, value.size()));
         }
     }
 
     static void assertPrefixLen(String entityDescr, int prefixLength, int bitWidth)
-            throws EncodeException {
+            throws CodecException {
 
         if (prefixLength > bitWidth) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "wrong prefix length for %s, field size is %d bits, but found one is %d",
                     entityDescr, bitWidth, prefixLength));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
index 3294a6d..357e41d 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
@@ -81,16 +81,16 @@
      * @param piTableEntries PI table entries
      * @param pipeconf       PI pipeconf
      * @return collection of P4Runtime table entry protobuf messages
-     * @throws EncodeException if a PI table entry cannot be encoded
+     * @throws CodecException if a PI table entry cannot be encoded
      */
     static List<TableEntry> encode(List<PiTableEntry> piTableEntries,
                                                 PiPipeconf pipeconf)
-            throws EncodeException {
+            throws CodecException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
 
         if (browser == null) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
 
@@ -100,7 +100,7 @@
             try {
                 tableEntryMsgListBuilder.add(encodePiTableEntry(piTableEntry, browser));
             } catch (P4InfoBrowser.NotFoundException e) {
-                throw new EncodeException(e.getMessage());
+                throw new CodecException(e.getMessage());
             }
         }
 
@@ -113,15 +113,15 @@
      * @param piTableEntry table entry
      * @param pipeconf     pipeconf
      * @return encoded table entry message
-     * @throws EncodeException                 if entry cannot be encoded
+     * @throws CodecException                 if entry cannot be encoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static TableEntry encode(PiTableEntry piTableEntry, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            throw new EncodeException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
+            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
 
         return encodePiTableEntry(piTableEntry, browser);
@@ -152,7 +152,7 @@
         for (TableEntry tableEntryMsg : tableEntryMsgs) {
             try {
                 piTableEntryListBuilder.add(decodeTableEntryMsg(tableEntryMsg, browser));
-            } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+            } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                 log.error("Unable to decode table entry message: {}", e.getMessage());
             }
         }
@@ -166,15 +166,15 @@
      * @param tableEntryMsg table entry message
      * @param pipeconf      pipeconf
      * @return decoded PI table entry
-     * @throws EncodeException                 if message cannot be decoded
+     * @throws CodecException                 if message cannot be decoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static PiTableEntry decode(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            throw new EncodeException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
+            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
         return decodeTableEntryMsg(tableEntryMsg, browser);
     }
@@ -188,11 +188,11 @@
      * @param matchKey match key
      * @param pipeconf pipeconf
      * @return table entry message
-     * @throws EncodeException                 if message cannot be encoded
+     * @throws CodecException                 if message cannot be encoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static TableEntry encode(PiTableId tableId, PiMatchKey matchKey, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
@@ -215,7 +215,7 @@
     }
 
     private static TableEntry encodePiTableEntry(PiTableEntry piTableEntry, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
 
@@ -259,7 +259,7 @@
     }
 
     private static PiTableEntry decodeTableEntryMsg(TableEntry tableEntryMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         PiTableEntry.Builder piTableEntryBuilder = PiTableEntry.builder();
 
@@ -295,7 +295,7 @@
 
     private static FieldMatch encodePiFieldMatch(PiFieldMatch piFieldMatch, P4InfoOuterClass.Table tableInfo,
                                                  P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         FieldMatch.Builder fieldMatchMsgBuilder = FieldMatch.newBuilder();
 
@@ -359,7 +359,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Building of match type %s not implemented", piFieldMatch.type()));
         }
     }
@@ -370,11 +370,11 @@
      * @param tableEntryMsg table entry message
      * @param pipeconf      pipeconf
      * @return PI match key
-     * @throws EncodeException                 if message cannot be decoded
+     * @throws CodecException                 if message cannot be decoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static PiMatchKey decodeMatchKey(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         P4InfoOuterClass.Table tableInfo = browser.tables().getById(tableEntryMsg.getTableId());
         if (tableEntryMsg.getMatchCount() == 0) {
@@ -386,7 +386,7 @@
 
     private static PiMatchKey decodeFieldMatchMsgs(List<FieldMatch> fieldMatchs, P4InfoOuterClass.Table tableInfo,
                                                    P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         // Match key for field matches.
         PiMatchKey.Builder piMatchKeyBuilder = PiMatchKey.builder();
         for (FieldMatch fieldMatchMsg : fieldMatchs) {
@@ -397,7 +397,7 @@
 
     private static PiFieldMatch decodeFieldMatchMsg(FieldMatch fieldMatchMsg, P4InfoOuterClass.Table tableInfo,
                                                     P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int tableId = tableInfo.getPreamble().getId();
         String fieldMatchName = browser.matchFields(tableId).getById(fieldMatchMsg.getFieldId()).getName();
@@ -426,13 +426,13 @@
                 ImmutableByteSequence rangeLowValue = copyFrom(rangeFieldMatch.getLow().asReadOnlyByteBuffer());
                 return new PiRangeFieldMatch(headerFieldId, rangeLowValue, rangeHighValue);
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Decoding of field match type '%s' not implemented", typeCase.name()));
         }
     }
 
     static TableAction encodePiTableAction(PiTableAction piTableAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         checkNotNull(piTableAction, "Cannot encode null PiTableAction");
         TableAction.Builder tableActionMsgBuilder = TableAction.newBuilder();
 
@@ -451,7 +451,7 @@
                 tableActionMsgBuilder.setActionProfileMemberId(actionProfileMemberId.id());
                 break;
             default:
-                throw new EncodeException(
+                throw new CodecException(
                         format("Building of table action type %s not implemented", piTableAction.type()));
         }
 
@@ -459,7 +459,7 @@
     }
 
     static PiTableAction decodeTableActionMsg(TableAction tableActionMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         TableAction.TypeCase typeCase = tableActionMsg.getTypeCase();
         switch (typeCase) {
             case ACTION:
@@ -470,13 +470,13 @@
             case ACTION_PROFILE_MEMBER_ID:
                 return PiActionProfileMemberId.of(tableActionMsg.getActionProfileMemberId());
             default:
-                throw new EncodeException(
+                throw new CodecException(
                         format("Decoding of table action type %s not implemented", typeCase.name()));
         }
     }
 
     static Action encodePiAction(PiAction piAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int actionId = browser.actions().getByName(piAction.id().toString()).getPreamble().getId();
 
diff --git a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
index b51a2c3..a7f7183 100644
--- a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
+++ b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
@@ -84,15 +84,19 @@
     private static final PiActionParamId PORT_PARAM_ID = PiActionParamId.of("port");
     private static final int BASE_MEM_ID = 65535;
     private static final List<Integer> MEMBER_IDS = ImmutableList.of(65536, 65537, 65538);
-    private static final List<PiActionProfileMember> GROUP_MEMBERS =
+    private static final List<PiActionProfileMember> GROUP_MEMBER_INSTANCES =
             Lists.newArrayList(
                     outputMember((short) 1),
                     outputMember((short) 2),
                     outputMember((short) 3)
             );
+    private static final List<PiActionProfileGroup.WeightedMember> GROUP_WEIGHTED_MEMBERS =
+            GROUP_MEMBER_INSTANCES.stream()
+                    .map(m -> new PiActionProfileGroup.WeightedMember(m, DEFAULT_MEMBER_WEIGHT))
+                    .collect(Collectors.toList());
     private static final PiActionProfileGroup GROUP = PiActionProfileGroup.builder()
             .withId(GROUP_ID)
-            .addMembers(GROUP_MEMBERS)
+            .addMembers(GROUP_MEMBER_INSTANCES)
             .withActionProfileId(ACT_PROF_ID)
             .build();
     private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:p4runtime:1");
@@ -121,7 +125,6 @@
                 .forActionProfile(ACT_PROF_ID)
                 .withAction(piAction)
                 .withId(PiActionProfileMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
                 .build();
     }
 
@@ -163,7 +166,7 @@
     @Test
     public void testInsertPiActionProfileGroup() throws Exception {
         CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
-        client.writeActionProfileGroup(GROUP, INSERT, PIPECONF, 3);
+        client.writeActionProfileGroup(GROUP, INSERT, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
         WriteRequest result = p4RuntimeServerImpl.getWriteReqs().get(0);
         assertEquals(1, result.getDeviceId());
@@ -192,7 +195,7 @@
     @Test
     public void testInsertPiActionMembers() throws Exception {
         CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
-        client.writeActionProfileMembers(GROUP_MEMBERS, INSERT, PIPECONF);
+        client.writeActionProfileMembers(GROUP_MEMBER_INSTANCES, INSERT, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
         WriteRequest result = p4RuntimeServerImpl.getWriteReqs().get(0);
         assertEquals(1, result.getDeviceId());
@@ -224,15 +227,41 @@
                 .setGroupId(GROUP_ID.id())
                 .setActionProfileId(P4_INFO_ACT_PROF_ID);
 
-        List<ActionProfileMember> members = Lists.newArrayList();
-
         MEMBER_IDS.forEach(id -> {
             ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
                     .setMemberId(id)
                     .setWeight(DEFAULT_MEMBER_WEIGHT)
                     .build();
             group.addMembers(member);
+        });
 
+        List<ReadResponse> responses = Lists.newArrayList();
+        responses.add(ReadResponse.newBuilder()
+                              .addEntities(Entity.newBuilder().setActionProfileGroup(group))
+                              .build()
+        );
+
+        p4RuntimeServerImpl.willReturnReadResult(responses);
+        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
+        CompletableFuture<List<PiActionProfileGroup>> groupsComplete = client.dumpActionProfileGroups(
+                ACT_PROF_ID, PIPECONF);
+        complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+
+        Collection<PiActionProfileGroup> groups = groupsComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+        assertEquals(1, groups.size());
+        PiActionProfileGroup piActionGroup = groups.iterator().next();
+        assertEquals(ACT_PROF_ID, piActionGroup.actionProfile());
+        assertEquals(GROUP_ID, piActionGroup.id());
+        assertEquals(3, piActionGroup.members().size());
+        assertTrue(GROUP_WEIGHTED_MEMBERS.containsAll(piActionGroup.members()));
+        assertTrue(piActionGroup.members().containsAll(GROUP_WEIGHTED_MEMBERS));
+    }
+
+    @Test
+    public void testReadMembers() throws Exception {
+        List<ActionProfileMember> members = Lists.newArrayList();
+
+        MEMBER_IDS.forEach(id -> {
             byte outPort = (byte) (id - BASE_MEM_ID);
             ByteString bs = ByteString.copyFrom(new byte[]{0, outPort});
             Action.Param param = Action.Param.newBuilder()
@@ -256,11 +285,6 @@
 
         List<ReadResponse> responses = Lists.newArrayList();
         responses.add(ReadResponse.newBuilder()
-                              .addEntities(Entity.newBuilder().setActionProfileGroup(group))
-                              .build()
-        );
-
-        responses.add(ReadResponse.newBuilder()
                               .addAllEntities(members.stream()
                                                       .map(m -> Entity.newBuilder()
                                                               .setActionProfileMember(m).build())
@@ -268,18 +292,14 @@
                               .build());
 
         p4RuntimeServerImpl.willReturnReadResult(responses);
-        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(2);
-        CompletableFuture<List<PiActionProfileGroup>> groupsComplete = client.dumpActionProfileGroups(
+        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
+        CompletableFuture<List<PiActionProfileMember>> membersComplete = client.dumpActionProfileMembers(
                 ACT_PROF_ID, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
 
-        Collection<PiActionProfileGroup> groups = groupsComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
-        assertEquals(1, groups.size());
-        PiActionProfileGroup piActionGroup = groups.iterator().next();
-        assertEquals(ACT_PROF_ID, piActionGroup.actionProfileId());
-        assertEquals(GROUP_ID, piActionGroup.id());
-        assertEquals(3, piActionGroup.members().size());
-        assertTrue(GROUP_MEMBERS.containsAll(piActionGroup.members()));
-        assertTrue(piActionGroup.members().containsAll(GROUP_MEMBERS));
+        Collection<PiActionProfileMember> piMembers = membersComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+        assertEquals(3, piMembers.size());
+        assertTrue(GROUP_MEMBER_INSTANCES.containsAll(piMembers));
+        assertTrue(piMembers.containsAll(GROUP_MEMBER_INSTANCES));
     }
 }
diff --git a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
index 5417d44..443d407 100644
--- a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
+++ b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
@@ -32,14 +32,17 @@
     private final PiActionProfileId id;
     private final ImmutableSet<PiTableId> tables;
     private final boolean hasSelector;
-    private final long maxSize;
+    private final long size;
+    private final int maxGroupSize;
 
     P4ActionProfileModel(PiActionProfileId id,
-                         ImmutableSet<PiTableId> tables, boolean hasSelector, long maxSize) {
+                         ImmutableSet<PiTableId> tables, boolean hasSelector,
+                         long size, int maxGroupSize) {
         this.id = id;
         this.tables = tables;
         this.hasSelector = hasSelector;
-        this.maxSize = maxSize;
+        this.size = size;
+        this.maxGroupSize = maxGroupSize;
     }
 
     @Override
@@ -58,13 +61,18 @@
     }
 
     @Override
-    public long maxSize() {
-        return maxSize;
+    public long size() {
+        return size;
+    }
+
+    @Override
+    public int maxGroupSize() {
+        return maxGroupSize;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(id, tables, hasSelector, maxSize);
+        return Objects.hash(id, tables, hasSelector, size, maxGroupSize);
     }
 
     @Override
@@ -79,6 +87,7 @@
         return Objects.equals(this.id, other.id)
                 && Objects.equals(this.tables, other.tables)
                 && Objects.equals(this.hasSelector, other.hasSelector)
-                && Objects.equals(this.maxSize, other.maxSize);
+                && Objects.equals(this.size, other.size)
+                && Objects.equals(this.maxGroupSize, other.maxGroupSize);
     }
 }
diff --git a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
index ff49928..ef8cb36 100644
--- a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
+++ b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
@@ -332,7 +332,8 @@
                             PiActionProfileId.of(actProfileMsg.getPreamble().getName()),
                             tableIdSetBuilder.build(),
                             actProfileMsg.getWithSelector(),
-                            actProfileMsg.getSize()));
+                            actProfileMsg.getSize(),
+                            actProfileMsg.getMaxGroupSize()));
         }
         return actProfileMap;
     }
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
index 3042414..021d341 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
@@ -21,7 +21,6 @@
 import org.onosproject.net.pi.model.PiActionProfileId;
 import org.onosproject.net.pi.model.PiTableId;
 
-
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
@@ -52,17 +51,17 @@
     private final PiActionProfileId id2 = PiActionProfileId.of("name2");
 
     private final P4ActionProfileModel metadataModel = new P4ActionProfileModel(id, tables,
-                                                                                true, 64);
+                                                                                true, 64, 10);
     private final P4ActionProfileModel sameAsMetadataModel = new P4ActionProfileModel(id, sameAsTables,
-                                                                                      true, 64);
+                                                                                      true, 64, 10);
     private final P4ActionProfileModel metadataModel2 = new P4ActionProfileModel(id, tables2,
-                                                                                 true, 64);
+                                                                                 true, 64, 10);
     private final P4ActionProfileModel metadataModel3 = new P4ActionProfileModel(id2, tables,
-                                                                                 true, 64);
+                                                                                 true, 64, 10);
     private final P4ActionProfileModel metadataModel4 = new P4ActionProfileModel(id, tables,
-                                                                                 false, 64);
+                                                                                 false, 64, 10);
     private final P4ActionProfileModel metadataModel5 = new P4ActionProfileModel(id, tables,
-                                                                                 true, 32);
+                                                                                 true, 32, 5);
 
     /**
      * Checks that the P4ActionProfileModel class is immutable.
@@ -85,4 +84,4 @@
                 .addEqualityGroup(metadataModel5)
                 .testEquals();
     }
-}
\ No newline at end of file
+}
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
index 3a803d4..9d696e2 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
@@ -70,6 +70,7 @@
 
     private static final Long DEFAULT_MAX_TABLE_SIZE = 1024L;
     private static final Long DEFAULT_MAX_ACTION_PROFILE_SIZE = 64L;
+    private static final int DEFAULT_MAX_GROUP_SIZE = 0;
 
     /**
      * Tests parse method.
@@ -205,7 +206,8 @@
         ImmutableSet<PiTableId> tableIds = new ImmutableSet.Builder<PiTableId>().add(tableId).build();
         PiActionProfileId actionProfileId = PiActionProfileId.of("wcmp_control.wcmp_selector");
         PiActionProfileModel wcmpSelector3 = new P4ActionProfileModel(actionProfileId, tableIds,
-                                                                      true, DEFAULT_MAX_ACTION_PROFILE_SIZE);
+                                                                      true, DEFAULT_MAX_ACTION_PROFILE_SIZE,
+                                                                      DEFAULT_MAX_GROUP_SIZE);
         PiActionProfileModel wcmpSelector = model.actionProfiles(actionProfileId).orElse(null);
         PiActionProfileModel wcmpSelector2 = model2.actionProfiles(actionProfileId).orElse(null);
 
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
index da10762..8576e84 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
@@ -75,12 +75,17 @@
     private static final long ACTION_MAX_SIZE_1 = 100;
     private static final long ACTION_MAX_SIZE_2 = 200;
 
+    private static final int ACTION_MAX_GROUP_SIZE_1 = 10;
+    private static final int ACTION_MAX_GROUP_SIZE_2 = 20;
+
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_1 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_1, ACTION_TABLES_1,
-                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1);
+                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1,
+                                     ACTION_MAX_GROUP_SIZE_1);
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_2 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_2, ACTION_TABLES_2,
-                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2);
+                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2,
+                                     ACTION_MAX_GROUP_SIZE_2);
 
     /* Counters */
     private static final PiCounterId PI_COUNTER_ID_1 = PiCounterId.of("Counter1");
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
index 89b10d0..797abe4 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
@@ -67,12 +67,17 @@
     private static final long ACTION_MAX_SIZE_1 = 100;
     private static final long ACTION_MAX_SIZE_2 = 200;
 
+    private static final int ACTION_MAX_GROUP_SIZE_1 = 10;
+    private static final int ACTION_MAX_GROUP_SIZE_2 = 20;
+
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_1 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_1, ACTION_TABLES_1,
-                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1);
+                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1,
+                                     ACTION_MAX_GROUP_SIZE_1);
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_2 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_2, ACTION_TABLES_2,
-                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2);
+                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2,
+                                     ACTION_MAX_GROUP_SIZE_2);
 
     /* Counters */
     private static final PiCounterId PI_COUNTER_ID_1 = PiCounterId.of("Counter1");