blob: f24c71778f06cb93b7f8ab3747b65d9e2952a819 [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()) {
Carmelo Casconeb203b642019-02-06 17:23:05 -0800159 log.warn("Partial response from {}, {} of {} total " +
160 "entities are in status PENDING",
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800161 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
Carmelo Casconeb203b642019-02-06 17:23:05 -0800177 WriteResponseImpl setFailAllAndBuild(Throwable throwable) {
178 synchronized (this) {
179 pendingResponses.values().forEach(r -> r.setFailure(throwable));
180 pendingResponses.clear();
181 return buildAsIs();
182 }
183 }
184
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800185 WriteResponseImpl setErrorsAndBuild(Throwable throwable) {
186 synchronized (this) {
187 return doSetErrorsAndBuild(throwable);
188 }
189 }
190
191 private void setSuccess(int index) {
192 synchronized (this) {
193 final WriteEntityResponseImpl resp = pendingResponses.remove(index);
194 if (resp != null) {
195 doSetSuccess(resp);
196 } else {
197 log.error("Missing pending response at index {}", index);
198 }
199 }
200 }
201
202 private void doSetSuccess(WriteEntityResponseImpl resp) {
203 resp.setSuccess();
204 statusMap.put(WriteResponseStatus.OK, resp);
205 }
206
207 private void setFailure(int index,
208 String explanation,
209 WriteResponseStatus status) {
210 synchronized (this) {
211 final WriteEntityResponseImpl resp = pendingResponses.remove(index);
212 if (resp != null) {
213 resp.withFailure(explanation, status);
214 statusMap.put(status, resp);
215 log.warn("Unable to {} {} on {}: {} {} [{}]",
216 resp.updateType(),
217 resp.entityType().humanReadableName(),
218 deviceId,
219 status, explanation,
220 resp.entity() != null ? resp.entity() : resp.handle());
221 } else {
222 log.error("Missing pending response at index {}", index);
223 }
224 }
225 }
226
227 private WriteResponseImpl doSetErrorsAndBuild(Throwable throwable) {
228 if (!(throwable instanceof StatusRuntimeException)) {
229 // Leave all entity responses in pending state.
Carmelo Casconeb203b642019-02-06 17:23:05 -0800230 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800231 }
232 final StatusRuntimeException sre = (StatusRuntimeException) throwable;
233 if (!sre.getStatus().equals(Status.UNKNOWN)) {
234 // Error trailers expected only if status is UNKNOWN.
Carmelo Casconeb203b642019-02-06 17:23:05 -0800235 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800236 }
237 // Extract error details.
238 if (!sre.getTrailers().containsKey(STATUS_DETAILS_KEY)) {
239 log.warn("Cannot parse write error details from {}, " +
240 "missing status trailers in StatusRuntimeException",
241 deviceId);
Carmelo Casconeb203b642019-02-06 17:23:05 -0800242 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800243 }
244 com.google.rpc.Status status = sre.getTrailers().get(STATUS_DETAILS_KEY);
245 if (status == null) {
246 log.warn("Cannot parse write error details from {}, " +
247 "found NULL status trailers in StatusRuntimeException",
248 deviceId);
Carmelo Casconeb203b642019-02-06 17:23:05 -0800249 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800250 }
251 final boolean reconcilable = status.getDetailsList().size() == pendingResponses.size();
252 // We expect one error for each entity...
253 if (!reconcilable) {
254 log.warn("Unable to reconcile write error details from {}, " +
255 "sent {} updates, but server returned {} errors",
256 deviceId, pendingResponses.size(), status.getDetailsList().size());
257 }
258 // ...in the same order as in the request.
259 int index = 0;
260 for (Any any : status.getDetailsList()) {
261 // Set response entities only if reconcilable, otherwise log.
262 unpackP4Error(index, any, reconcilable);
263 index += 1;
264 }
265 return buildAsIs();
266 }
267
268 private void unpackP4Error(int index, Any any, boolean reconcilable) {
269 final P4RuntimeOuterClass.Error p4Error;
270 try {
271 p4Error = any.unpack(P4RuntimeOuterClass.Error.class);
272 } catch (InvalidProtocolBufferException e) {
273 final String unpackErr = format(
274 "P4Runtime Error message format not recognized [%s]",
275 TextFormat.shortDebugString(any));
276 if (reconcilable) {
277 setFailure(index, unpackErr, WriteResponseStatus.OTHER_ERROR);
278 } else {
279 log.warn(unpackErr);
280 }
281 return;
282 }
283 // Map gRPC status codes to our WriteResponseStatus codes.
284 final Status.Code p4Code = Status.fromCodeValue(
285 p4Error.getCanonicalCode()).getCode();
286 final WriteResponseStatus ourCode;
287 switch (p4Code) {
288 case OK:
289 if (reconcilable) {
290 setSuccess(index);
291 }
292 return;
293 case NOT_FOUND:
294 ourCode = WriteResponseStatus.NOT_FOUND;
295 break;
296 case ALREADY_EXISTS:
297 ourCode = WriteResponseStatus.ALREADY_EXIST;
298 break;
299 default:
300 ourCode = WriteResponseStatus.OTHER_ERROR;
301 break;
302 }
303 // Put the p4Code in the explanation only if ourCode is OTHER_ERROR.
304 final String explanationCode = ourCode == WriteResponseStatus.OTHER_ERROR
305 ? p4Code.name() + " " : "";
306 final String details = p4Error.hasDetails()
307 ? ", " + p4Error.getDetails().toString() : "";
308 final String explanation = format(
309 "%s%s%s (%s:%d)", explanationCode, p4Error.getMessage(),
310 details, p4Error.getSpace(), p4Error.getCode());
311 if (reconcilable) {
312 setFailure(index, explanation, ourCode);
313 } else {
314 log.warn("P4Runtime write error: {}", explanation);
315 }
316 }
317 }
318
319 /**
320 * Internal implementation of WriteEntityResponse.
321 */
322 private static final class WriteEntityResponseImpl implements WriteEntityResponse {
323
324 private final PiHandle handle;
325 private final PiEntity entity;
326 private final UpdateType updateType;
327
328 private WriteResponseStatus status = WriteResponseStatus.PENDING;
329 private String explanation;
330 private Throwable throwable;
331
332 private WriteEntityResponseImpl(PiHandle handle, PiEntity entity, UpdateType updateType) {
333 this.handle = handle;
334 this.entity = entity;
335 this.updateType = updateType;
336 }
337
338 private WriteEntityResponseImpl withFailure(
339 String explanation, WriteResponseStatus status) {
340 this.status = status;
341 this.explanation = explanation;
342 this.throwable = null;
343 return this;
344 }
345
346 private void setSuccess() {
347 this.status = WriteResponseStatus.OK;
348 }
349
Carmelo Casconeb203b642019-02-06 17:23:05 -0800350 private void setFailure(Throwable throwable) {
351 this.status = WriteResponseStatus.OTHER_ERROR;
352 this.explanation = throwable.toString();
353 this.throwable = throwable;
354 }
355
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800356 @Override
357 public PiHandle handle() {
358 return handle;
359 }
360
361 @Override
362 public PiEntity entity() {
363 return entity;
364 }
365
366 @Override
367 public UpdateType updateType() {
368 return updateType;
369 }
370
371 @Override
372 public PiEntityType entityType() {
373 return handle.entityType();
374 }
375
376 @Override
377 public boolean isSuccess() {
378 return status().equals(WriteResponseStatus.OK);
379 }
380
381 @Override
382 public WriteResponseStatus status() {
383 return status;
384 }
385
386 @Override
387 public String explanation() {
388 return explanation;
389 }
390
391 @Override
392 public Throwable throwable() {
393 return throwable;
394 }
395
396 @Override
397 public String toString() {
398 return MoreObjects.toStringHelper(this)
399 .add("handle", handle)
400 .add("entity", entity)
401 .add("updateType", updateType)
402 .add("status", status)
403 .add("explanation", explanation)
404 .add("throwable", throwable)
405 .toString();
406 }
407 }
408}