blob: 404ab8052309786cf05586277bc6389039c8ba61 [file] [log] [blame]
Carmelo Cascone4c289b72019-01-22 15:30:45 -08001/*
2 * Copyright 2019-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.p4runtime.ctl.client;
18
19import com.google.common.base.MoreObjects;
20import com.google.common.collect.ArrayListMultimap;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.ImmutableListMultimap;
23import com.google.common.collect.ListMultimap;
24import com.google.common.collect.Lists;
25import com.google.common.collect.Maps;
26import com.google.protobuf.Any;
27import com.google.protobuf.InvalidProtocolBufferException;
28import com.google.protobuf.TextFormat;
29import io.grpc.Metadata;
30import io.grpc.Status;
31import io.grpc.StatusRuntimeException;
32import io.grpc.protobuf.lite.ProtoLiteUtils;
33import org.onosproject.net.DeviceId;
34import org.onosproject.net.pi.runtime.PiEntity;
35import org.onosproject.net.pi.runtime.PiEntityType;
36import org.onosproject.net.pi.runtime.PiHandle;
37import org.onosproject.p4runtime.api.P4RuntimeWriteClient;
38import org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType;
39import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteEntityResponse;
40import org.onosproject.p4runtime.api.P4RuntimeWriteClient.WriteResponseStatus;
41import org.slf4j.Logger;
42import p4.v1.P4RuntimeOuterClass;
43
44import java.util.Collection;
45import java.util.Collections;
46import java.util.List;
47import java.util.Map;
48
49import static com.google.common.base.Preconditions.checkNotNull;
50import static java.lang.String.format;
51import static java.util.stream.Collectors.toList;
52import static org.slf4j.LoggerFactory.getLogger;
53
54/**
55 * Handles the creation of WriteResponse and parsing of P4Runtime errors
56 * received from server, as well as logging of RPC errors.
57 */
58final class WriteResponseImpl implements P4RuntimeWriteClient.WriteResponse {
59
60 private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
61 Metadata.Key.of(
62 "grpc-status-details-bin",
63 ProtoLiteUtils.metadataMarshaller(
64 com.google.rpc.Status.getDefaultInstance()));
65
66 static final WriteResponseImpl EMPTY = new WriteResponseImpl(
67 ImmutableList.of(), ImmutableListMultimap.of());
68
69 private static final Logger log = getLogger(WriteResponseImpl.class);
70
71 private final ImmutableList<WriteEntityResponse> entityResponses;
72 private final ImmutableListMultimap<WriteResponseStatus, WriteEntityResponse> statusMultimap;
73
74 private WriteResponseImpl(
75 ImmutableList<WriteEntityResponse> allResponses,
76 ImmutableListMultimap<WriteResponseStatus, WriteEntityResponse> statusMultimap) {
77 this.entityResponses = allResponses;
78 this.statusMultimap = statusMultimap;
79 }
80
81 @Override
82 public boolean isSuccess() {
83 return success().size() == all().size();
84 }
85
86 @Override
87 public Collection<WriteEntityResponse> all() {
88 return entityResponses;
89 }
90
91 @Override
92 public Collection<WriteEntityResponse> success() {
93 return statusMultimap.get(WriteResponseStatus.OK);
94 }
95
96 @Override
97 public Collection<WriteEntityResponse> failed() {
98 return isSuccess()
99 ? Collections.emptyList()
100 : entityResponses.stream().filter(r -> !r.isSuccess()).collect(toList());
101 }
102
103 @Override
104 public Collection<WriteEntityResponse> status(
105 WriteResponseStatus status) {
106 checkNotNull(status);
107 return statusMultimap.get(status);
108 }
109
110 /**
111 * Returns a new response builder for the given device.
112 *
113 * @param deviceId device ID
114 * @return response builder
115 */
116 static Builder builder(DeviceId deviceId) {
117 return new Builder(deviceId);
118 }
119
120 /**
121 * Builder of P4RuntimeWriteResponseImpl.
122 */
123 static final class Builder {
124
125 private final DeviceId deviceId;
126 private final Map<Integer, WriteEntityResponseImpl> pendingResponses =
127 Maps.newHashMap();
128 private final List<WriteEntityResponse> allResponses =
129 Lists.newArrayList();
130 private final ListMultimap<WriteResponseStatus, WriteEntityResponse> statusMap =
131 ArrayListMultimap.create();
132
133 private Builder(DeviceId deviceId) {
134 this.deviceId = deviceId;
135 }
136
137 void addPendingResponse(PiHandle handle, PiEntity entity, UpdateType updateType) {
138 synchronized (this) {
139 final WriteEntityResponseImpl resp = new WriteEntityResponseImpl(
140 handle, entity, updateType);
141 allResponses.add(resp);
142 pendingResponses.put(pendingResponses.size(), resp);
143 }
144 }
145
146 void addFailedResponse(PiHandle handle, PiEntity entity, UpdateType updateType,
147 String explanation, WriteResponseStatus status) {
148 synchronized (this) {
149 final WriteEntityResponseImpl resp = new WriteEntityResponseImpl(
150 handle, entity, updateType)
151 .withFailure(explanation, status);
152 allResponses.add(resp);
153 }
154 }
155
156 WriteResponseImpl buildAsIs() {
157 synchronized (this) {
158 if (!pendingResponses.isEmpty()) {
159 log.warn("Detected partial response from {}, " +
160 "{} of {} total entities are in status PENDING",
161 deviceId, pendingResponses.size(), allResponses.size());
162 }
163 return new WriteResponseImpl(
164 ImmutableList.copyOf(allResponses),
165 ImmutableListMultimap.copyOf(statusMap));
166 }
167 }
168
169 WriteResponseImpl setSuccessAllAndBuild() {
170 synchronized (this) {
171 pendingResponses.values().forEach(this::doSetSuccess);
172 pendingResponses.clear();
173 return buildAsIs();
174 }
175 }
176
177 WriteResponseImpl setErrorsAndBuild(Throwable throwable) {
178 synchronized (this) {
179 return doSetErrorsAndBuild(throwable);
180 }
181 }
182
183 private void setSuccess(int index) {
184 synchronized (this) {
185 final WriteEntityResponseImpl resp = pendingResponses.remove(index);
186 if (resp != null) {
187 doSetSuccess(resp);
188 } else {
189 log.error("Missing pending response at index {}", index);
190 }
191 }
192 }
193
194 private void doSetSuccess(WriteEntityResponseImpl resp) {
195 resp.setSuccess();
196 statusMap.put(WriteResponseStatus.OK, resp);
197 }
198
199 private void setFailure(int index,
200 String explanation,
201 WriteResponseStatus status) {
202 synchronized (this) {
203 final WriteEntityResponseImpl resp = pendingResponses.remove(index);
204 if (resp != null) {
205 resp.withFailure(explanation, status);
206 statusMap.put(status, resp);
207 log.warn("Unable to {} {} on {}: {} {} [{}]",
208 resp.updateType(),
209 resp.entityType().humanReadableName(),
210 deviceId,
211 status, explanation,
212 resp.entity() != null ? resp.entity() : resp.handle());
213 } else {
214 log.error("Missing pending response at index {}", index);
215 }
216 }
217 }
218
219 private WriteResponseImpl doSetErrorsAndBuild(Throwable throwable) {
220 if (!(throwable instanceof StatusRuntimeException)) {
221 // Leave all entity responses in pending state.
222 return buildAsIs();
223 }
224 final StatusRuntimeException sre = (StatusRuntimeException) throwable;
225 if (!sre.getStatus().equals(Status.UNKNOWN)) {
226 // Error trailers expected only if status is UNKNOWN.
227 return buildAsIs();
228 }
229 // Extract error details.
230 if (!sre.getTrailers().containsKey(STATUS_DETAILS_KEY)) {
231 log.warn("Cannot parse write error details from {}, " +
232 "missing status trailers in StatusRuntimeException",
233 deviceId);
234 return buildAsIs();
235 }
236 com.google.rpc.Status status = sre.getTrailers().get(STATUS_DETAILS_KEY);
237 if (status == null) {
238 log.warn("Cannot parse write error details from {}, " +
239 "found NULL status trailers in StatusRuntimeException",
240 deviceId);
241 return buildAsIs();
242 }
243 final boolean reconcilable = status.getDetailsList().size() == pendingResponses.size();
244 // We expect one error for each entity...
245 if (!reconcilable) {
246 log.warn("Unable to reconcile write error details from {}, " +
247 "sent {} updates, but server returned {} errors",
248 deviceId, pendingResponses.size(), status.getDetailsList().size());
249 }
250 // ...in the same order as in the request.
251 int index = 0;
252 for (Any any : status.getDetailsList()) {
253 // Set response entities only if reconcilable, otherwise log.
254 unpackP4Error(index, any, reconcilable);
255 index += 1;
256 }
257 return buildAsIs();
258 }
259
260 private void unpackP4Error(int index, Any any, boolean reconcilable) {
261 final P4RuntimeOuterClass.Error p4Error;
262 try {
263 p4Error = any.unpack(P4RuntimeOuterClass.Error.class);
264 } catch (InvalidProtocolBufferException e) {
265 final String unpackErr = format(
266 "P4Runtime Error message format not recognized [%s]",
267 TextFormat.shortDebugString(any));
268 if (reconcilable) {
269 setFailure(index, unpackErr, WriteResponseStatus.OTHER_ERROR);
270 } else {
271 log.warn(unpackErr);
272 }
273 return;
274 }
275 // Map gRPC status codes to our WriteResponseStatus codes.
276 final Status.Code p4Code = Status.fromCodeValue(
277 p4Error.getCanonicalCode()).getCode();
278 final WriteResponseStatus ourCode;
279 switch (p4Code) {
280 case OK:
281 if (reconcilable) {
282 setSuccess(index);
283 }
284 return;
285 case NOT_FOUND:
286 ourCode = WriteResponseStatus.NOT_FOUND;
287 break;
288 case ALREADY_EXISTS:
289 ourCode = WriteResponseStatus.ALREADY_EXIST;
290 break;
291 default:
292 ourCode = WriteResponseStatus.OTHER_ERROR;
293 break;
294 }
295 // Put the p4Code in the explanation only if ourCode is OTHER_ERROR.
296 final String explanationCode = ourCode == WriteResponseStatus.OTHER_ERROR
297 ? p4Code.name() + " " : "";
298 final String details = p4Error.hasDetails()
299 ? ", " + p4Error.getDetails().toString() : "";
300 final String explanation = format(
301 "%s%s%s (%s:%d)", explanationCode, p4Error.getMessage(),
302 details, p4Error.getSpace(), p4Error.getCode());
303 if (reconcilable) {
304 setFailure(index, explanation, ourCode);
305 } else {
306 log.warn("P4Runtime write error: {}", explanation);
307 }
308 }
309 }
310
311 /**
312 * Internal implementation of WriteEntityResponse.
313 */
314 private static final class WriteEntityResponseImpl implements WriteEntityResponse {
315
316 private final PiHandle handle;
317 private final PiEntity entity;
318 private final UpdateType updateType;
319
320 private WriteResponseStatus status = WriteResponseStatus.PENDING;
321 private String explanation;
322 private Throwable throwable;
323
324 private WriteEntityResponseImpl(PiHandle handle, PiEntity entity, UpdateType updateType) {
325 this.handle = handle;
326 this.entity = entity;
327 this.updateType = updateType;
328 }
329
330 private WriteEntityResponseImpl withFailure(
331 String explanation, WriteResponseStatus status) {
332 this.status = status;
333 this.explanation = explanation;
334 this.throwable = null;
335 return this;
336 }
337
338 private void setSuccess() {
339 this.status = WriteResponseStatus.OK;
340 }
341
342 @Override
343 public PiHandle handle() {
344 return handle;
345 }
346
347 @Override
348 public PiEntity entity() {
349 return entity;
350 }
351
352 @Override
353 public UpdateType updateType() {
354 return updateType;
355 }
356
357 @Override
358 public PiEntityType entityType() {
359 return handle.entityType();
360 }
361
362 @Override
363 public boolean isSuccess() {
364 return status().equals(WriteResponseStatus.OK);
365 }
366
367 @Override
368 public WriteResponseStatus status() {
369 return status;
370 }
371
372 @Override
373 public String explanation() {
374 return explanation;
375 }
376
377 @Override
378 public Throwable throwable() {
379 return throwable;
380 }
381
382 @Override
383 public String toString() {
384 return MoreObjects.toStringHelper(this)
385 .add("handle", handle)
386 .add("entity", entity)
387 .add("updateType", updateType)
388 .add("status", status)
389 .add("explanation", explanation)
390 .add("throwable", throwable)
391 .toString();
392 }
393 }
394}