blob: 35ee1322894184a50b7b15f6ecbfb224af57f272 [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;
Carmelo Cascone61469462019-03-05 23:59:11 -080038import org.onosproject.p4runtime.api.P4RuntimeWriteClient.EntityUpdateRequest;
39import org.onosproject.p4runtime.api.P4RuntimeWriteClient.EntityUpdateResponse;
40import org.onosproject.p4runtime.api.P4RuntimeWriteClient.EntityUpdateStatus;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080041import org.onosproject.p4runtime.api.P4RuntimeWriteClient.UpdateType;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080042import org.slf4j.Logger;
43import p4.v1.P4RuntimeOuterClass;
44
45import java.util.Collection;
46import java.util.Collections;
47import java.util.List;
48import java.util.Map;
49
Carmelo Cascone61469462019-03-05 23:59:11 -080050import static com.google.common.base.Preconditions.checkArgument;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080051import static com.google.common.base.Preconditions.checkNotNull;
Carmelo Cascone61469462019-03-05 23:59:11 -080052import static com.google.common.base.Preconditions.checkState;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080053import static java.lang.String.format;
54import static java.util.stream.Collectors.toList;
55import static org.slf4j.LoggerFactory.getLogger;
56
57/**
58 * Handles the creation of WriteResponse and parsing of P4Runtime errors
59 * received from server, as well as logging of RPC errors.
60 */
61final class WriteResponseImpl implements P4RuntimeWriteClient.WriteResponse {
62
63 private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
64 Metadata.Key.of(
65 "grpc-status-details-bin",
66 ProtoLiteUtils.metadataMarshaller(
67 com.google.rpc.Status.getDefaultInstance()));
68
69 static final WriteResponseImpl EMPTY = new WriteResponseImpl(
70 ImmutableList.of(), ImmutableListMultimap.of());
71
72 private static final Logger log = getLogger(WriteResponseImpl.class);
73
Carmelo Cascone61469462019-03-05 23:59:11 -080074 private final ImmutableList<EntityUpdateResponse> entityResponses;
75 private final ImmutableListMultimap<EntityUpdateStatus, EntityUpdateResponse> statusMultimap;
Carmelo Cascone4c289b72019-01-22 15:30:45 -080076
77 private WriteResponseImpl(
Carmelo Cascone61469462019-03-05 23:59:11 -080078 ImmutableList<EntityUpdateResponse> allResponses,
79 ImmutableListMultimap<EntityUpdateStatus, EntityUpdateResponse> statusMultimap) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -080080 this.entityResponses = allResponses;
81 this.statusMultimap = statusMultimap;
82 }
83
84 @Override
85 public boolean isSuccess() {
86 return success().size() == all().size();
87 }
88
89 @Override
Carmelo Cascone61469462019-03-05 23:59:11 -080090 public Collection<EntityUpdateResponse> all() {
Carmelo Cascone4c289b72019-01-22 15:30:45 -080091 return entityResponses;
92 }
93
94 @Override
Carmelo Cascone61469462019-03-05 23:59:11 -080095 public Collection<EntityUpdateResponse> success() {
96 return statusMultimap.get(EntityUpdateStatus.OK);
Carmelo Cascone4c289b72019-01-22 15:30:45 -080097 }
98
99 @Override
Carmelo Cascone61469462019-03-05 23:59:11 -0800100 public Collection<EntityUpdateResponse> failed() {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800101 return isSuccess()
102 ? Collections.emptyList()
103 : entityResponses.stream().filter(r -> !r.isSuccess()).collect(toList());
104 }
105
106 @Override
Carmelo Cascone61469462019-03-05 23:59:11 -0800107 public Collection<EntityUpdateResponse> status(
108 EntityUpdateStatus status) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800109 checkNotNull(status);
110 return statusMultimap.get(status);
111 }
112
113 /**
114 * Returns a new response builder for the given device.
115 *
116 * @param deviceId device ID
117 * @return response builder
118 */
119 static Builder builder(DeviceId deviceId) {
120 return new Builder(deviceId);
121 }
122
123 /**
124 * Builder of P4RuntimeWriteResponseImpl.
125 */
126 static final class Builder {
127
128 private final DeviceId deviceId;
Carmelo Cascone61469462019-03-05 23:59:11 -0800129 private final Map<Integer, EntityUpdateResponseImpl> pendingResponses =
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800130 Maps.newHashMap();
Carmelo Cascone61469462019-03-05 23:59:11 -0800131 private final List<EntityUpdateResponse> allResponses =
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800132 Lists.newArrayList();
Carmelo Cascone61469462019-03-05 23:59:11 -0800133 private final ListMultimap<EntityUpdateStatus, EntityUpdateResponse> statusMap =
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800134 ArrayListMultimap.create();
135
136 private Builder(DeviceId deviceId) {
137 this.deviceId = deviceId;
138 }
139
140 void addPendingResponse(PiHandle handle, PiEntity entity, UpdateType updateType) {
141 synchronized (this) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800142 final EntityUpdateResponseImpl resp = new EntityUpdateResponseImpl(
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800143 handle, entity, updateType);
144 allResponses.add(resp);
145 pendingResponses.put(pendingResponses.size(), resp);
146 }
147 }
148
149 void addFailedResponse(PiHandle handle, PiEntity entity, UpdateType updateType,
Carmelo Cascone61469462019-03-05 23:59:11 -0800150 String explanation, EntityUpdateStatus status) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800151 synchronized (this) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800152 final EntityUpdateResponseImpl resp = new EntityUpdateResponseImpl(
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800153 handle, entity, updateType)
154 .withFailure(explanation, status);
155 allResponses.add(resp);
156 }
157 }
158
Carmelo Cascone61469462019-03-05 23:59:11 -0800159 Collection<EntityUpdateRequest> pendingUpdates() {
160 return ImmutableList.copyOf(pendingResponses.values());
161 }
162
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800163 WriteResponseImpl buildAsIs() {
164 synchronized (this) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800165 final long pendingCount = pendingResponses.values().stream()
166 .filter(r -> r.status() == EntityUpdateStatus.PENDING)
167 .count();
168 if (pendingCount > 0) {
Carmelo Casconeb203b642019-02-06 17:23:05 -0800169 log.warn("Partial response from {}, {} of {} total " +
Carmelo Cascone61469462019-03-05 23:59:11 -0800170 "updates are still in status PENDING",
171 deviceId, pendingCount, allResponses.size());
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800172 }
173 return new WriteResponseImpl(
174 ImmutableList.copyOf(allResponses),
175 ImmutableListMultimap.copyOf(statusMap));
176 }
177 }
178
179 WriteResponseImpl setSuccessAllAndBuild() {
180 synchronized (this) {
181 pendingResponses.values().forEach(this::doSetSuccess);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800182 return buildAsIs();
183 }
184 }
185
Carmelo Casconeb203b642019-02-06 17:23:05 -0800186 WriteResponseImpl setFailAllAndBuild(Throwable throwable) {
187 synchronized (this) {
188 pendingResponses.values().forEach(r -> r.setFailure(throwable));
Carmelo Casconeb203b642019-02-06 17:23:05 -0800189 return buildAsIs();
190 }
191 }
192
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800193 WriteResponseImpl setErrorsAndBuild(Throwable throwable) {
194 synchronized (this) {
195 return doSetErrorsAndBuild(throwable);
196 }
197 }
198
199 private void setSuccess(int index) {
200 synchronized (this) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800201 final EntityUpdateResponseImpl resp = pendingResponses.get(index);
202 if (resp != null && resp.status == EntityUpdateStatus.PENDING) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800203 doSetSuccess(resp);
204 } else {
205 log.error("Missing pending response at index {}", index);
206 }
207 }
208 }
209
Carmelo Cascone61469462019-03-05 23:59:11 -0800210 private void doSetSuccess(EntityUpdateResponseImpl resp) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800211 resp.setSuccess();
Carmelo Cascone61469462019-03-05 23:59:11 -0800212 statusMap.put(EntityUpdateStatus.OK, resp);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800213 }
214
215 private void setFailure(int index,
216 String explanation,
Carmelo Cascone61469462019-03-05 23:59:11 -0800217 EntityUpdateStatus status) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800218 synchronized (this) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800219 final EntityUpdateResponseImpl resp = pendingResponses.get(index);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800220 if (resp != null) {
221 resp.withFailure(explanation, status);
222 statusMap.put(status, resp);
223 log.warn("Unable to {} {} on {}: {} {} [{}]",
224 resp.updateType(),
225 resp.entityType().humanReadableName(),
226 deviceId,
227 status, explanation,
228 resp.entity() != null ? resp.entity() : resp.handle());
229 } else {
230 log.error("Missing pending response at index {}", index);
231 }
232 }
233 }
234
235 private WriteResponseImpl doSetErrorsAndBuild(Throwable throwable) {
236 if (!(throwable instanceof StatusRuntimeException)) {
237 // Leave all entity responses in pending state.
Carmelo Casconeb203b642019-02-06 17:23:05 -0800238 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800239 }
240 final StatusRuntimeException sre = (StatusRuntimeException) throwable;
241 if (!sre.getStatus().equals(Status.UNKNOWN)) {
242 // Error trailers expected only if status is UNKNOWN.
Carmelo Casconeb203b642019-02-06 17:23:05 -0800243 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800244 }
245 // Extract error details.
246 if (!sre.getTrailers().containsKey(STATUS_DETAILS_KEY)) {
247 log.warn("Cannot parse write error details from {}, " +
248 "missing status trailers in StatusRuntimeException",
249 deviceId);
Carmelo Casconeb203b642019-02-06 17:23:05 -0800250 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800251 }
252 com.google.rpc.Status status = sre.getTrailers().get(STATUS_DETAILS_KEY);
253 if (status == null) {
254 log.warn("Cannot parse write error details from {}, " +
255 "found NULL status trailers in StatusRuntimeException",
256 deviceId);
Carmelo Casconeb203b642019-02-06 17:23:05 -0800257 return setFailAllAndBuild(throwable);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800258 }
259 final boolean reconcilable = status.getDetailsList().size() == pendingResponses.size();
260 // We expect one error for each entity...
261 if (!reconcilable) {
262 log.warn("Unable to reconcile write error details from {}, " +
263 "sent {} updates, but server returned {} errors",
264 deviceId, pendingResponses.size(), status.getDetailsList().size());
265 }
266 // ...in the same order as in the request.
267 int index = 0;
268 for (Any any : status.getDetailsList()) {
269 // Set response entities only if reconcilable, otherwise log.
270 unpackP4Error(index, any, reconcilable);
271 index += 1;
272 }
273 return buildAsIs();
274 }
275
276 private void unpackP4Error(int index, Any any, boolean reconcilable) {
277 final P4RuntimeOuterClass.Error p4Error;
278 try {
279 p4Error = any.unpack(P4RuntimeOuterClass.Error.class);
280 } catch (InvalidProtocolBufferException e) {
281 final String unpackErr = format(
282 "P4Runtime Error message format not recognized [%s]",
283 TextFormat.shortDebugString(any));
284 if (reconcilable) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800285 setFailure(index, unpackErr, EntityUpdateStatus.OTHER_ERROR);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800286 } else {
287 log.warn(unpackErr);
288 }
289 return;
290 }
291 // Map gRPC status codes to our WriteResponseStatus codes.
292 final Status.Code p4Code = Status.fromCodeValue(
293 p4Error.getCanonicalCode()).getCode();
Carmelo Cascone61469462019-03-05 23:59:11 -0800294 final EntityUpdateStatus ourCode;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800295 switch (p4Code) {
296 case OK:
297 if (reconcilable) {
298 setSuccess(index);
299 }
300 return;
301 case NOT_FOUND:
Carmelo Cascone61469462019-03-05 23:59:11 -0800302 ourCode = EntityUpdateStatus.NOT_FOUND;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800303 break;
304 case ALREADY_EXISTS:
Carmelo Cascone61469462019-03-05 23:59:11 -0800305 ourCode = EntityUpdateStatus.ALREADY_EXIST;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800306 break;
307 default:
Carmelo Cascone61469462019-03-05 23:59:11 -0800308 ourCode = EntityUpdateStatus.OTHER_ERROR;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800309 break;
310 }
311 // Put the p4Code in the explanation only if ourCode is OTHER_ERROR.
Carmelo Cascone61469462019-03-05 23:59:11 -0800312 final String explanationCode = ourCode == EntityUpdateStatus.OTHER_ERROR
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800313 ? p4Code.name() + " " : "";
314 final String details = p4Error.hasDetails()
315 ? ", " + p4Error.getDetails().toString() : "";
316 final String explanation = format(
317 "%s%s%s (%s:%d)", explanationCode, p4Error.getMessage(),
318 details, p4Error.getSpace(), p4Error.getCode());
319 if (reconcilable) {
320 setFailure(index, explanation, ourCode);
321 } else {
322 log.warn("P4Runtime write error: {}", explanation);
323 }
324 }
325 }
326
327 /**
Carmelo Cascone61469462019-03-05 23:59:11 -0800328 * Internal implementation of EntityUpdateResponse.
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800329 */
Carmelo Cascone61469462019-03-05 23:59:11 -0800330 private static final class EntityUpdateResponseImpl implements EntityUpdateResponse {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800331
332 private final PiHandle handle;
333 private final PiEntity entity;
334 private final UpdateType updateType;
335
Carmelo Cascone61469462019-03-05 23:59:11 -0800336 private EntityUpdateStatus status = EntityUpdateStatus.PENDING;
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800337 private String explanation;
338 private Throwable throwable;
339
Carmelo Cascone61469462019-03-05 23:59:11 -0800340 private EntityUpdateResponseImpl(PiHandle handle, PiEntity entity, UpdateType updateType) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800341 this.handle = handle;
342 this.entity = entity;
343 this.updateType = updateType;
344 }
345
Carmelo Cascone61469462019-03-05 23:59:11 -0800346 private EntityUpdateResponseImpl withFailure(
347 String explanation, EntityUpdateStatus status) {
348 setStatus(status);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800349 this.explanation = explanation;
350 this.throwable = null;
351 return this;
352 }
353
354 private void setSuccess() {
Carmelo Cascone61469462019-03-05 23:59:11 -0800355 setStatus(EntityUpdateStatus.OK);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800356 }
357
Carmelo Casconeb203b642019-02-06 17:23:05 -0800358 private void setFailure(Throwable throwable) {
Carmelo Cascone61469462019-03-05 23:59:11 -0800359 setStatus(EntityUpdateStatus.OTHER_ERROR);
Carmelo Casconeb203b642019-02-06 17:23:05 -0800360 this.explanation = throwable.toString();
361 this.throwable = throwable;
362 }
363
Carmelo Cascone61469462019-03-05 23:59:11 -0800364 private void setStatus(EntityUpdateStatus newStatus) {
365 checkState(this.status == EntityUpdateStatus.PENDING,
366 "Cannot set status for non-pending update");
367 checkArgument(newStatus != EntityUpdateStatus.PENDING,
368 "newStatus must be different than pending");
369 this.status = newStatus;
370 }
371
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800372 @Override
373 public PiHandle handle() {
374 return handle;
375 }
376
377 @Override
378 public PiEntity entity() {
379 return entity;
380 }
381
382 @Override
383 public UpdateType updateType() {
384 return updateType;
385 }
386
387 @Override
388 public PiEntityType entityType() {
389 return handle.entityType();
390 }
391
392 @Override
393 public boolean isSuccess() {
Carmelo Cascone61469462019-03-05 23:59:11 -0800394 return status().equals(EntityUpdateStatus.OK);
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800395 }
396
397 @Override
Carmelo Cascone61469462019-03-05 23:59:11 -0800398 public EntityUpdateStatus status() {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800399 return status;
400 }
401
402 @Override
403 public String explanation() {
404 return explanation;
405 }
406
407 @Override
408 public Throwable throwable() {
409 return throwable;
410 }
411
412 @Override
413 public String toString() {
414 return MoreObjects.toStringHelper(this)
415 .add("handle", handle)
416 .add("entity", entity)
417 .add("updateType", updateType)
418 .add("status", status)
419 .add("explanation", explanation)
420 .add("throwable", throwable)
421 .toString();
422 }
423 }
424}