diff --git a/changes.md b/changes.md index 943d9fdc1a..89954d9b6b 100644 --- a/changes.md +++ b/changes.md @@ -218,6 +218,9 @@ Changes log their case (`Realm=`, `Charset=`), as required by RFC 9110. Issue #1455. - `ChallengeScheme.valueOf()` returned `HTTP_OAUTH` for the names of the Bearer and MAC schemes (`HTTP_Bearer`, `HTTP_MAC`) instead of `HTTP_OAUTH_BEARER` and `HTTP_OAUTH_MAC`. Issue #1618. + - The gRPC client could reject a call with `RESOURCE_EXHAUSTED` ("gRPC call limit exceeded") + right after the end of the previous one: the call slot was released after the caller was + notified. Issue #1622. - Misc - Fixed JDK HTTP client accessibility in core and WebSocket tests in Eclipse and VS Code, without adding a production module dependency. diff --git a/org.restlet.ext.grpc/src/main/java/org/restlet/ext/grpc/internal/RestletClientStream.java b/org.restlet.ext.grpc/src/main/java/org/restlet/ext/grpc/internal/RestletClientStream.java index d22ab4e941..d52ca2e315 100644 --- a/org.restlet.ext.grpc/src/main/java/org/restlet/ext/grpc/internal/RestletClientStream.java +++ b/org.restlet.ext.grpc/src/main/java/org/restlet/ext/grpc/internal/RestletClientStream.java @@ -380,14 +380,13 @@ protected void http2ProcessingFailed( @Override public void deframerClosed(boolean partial) { - try { - super.deframerClosed(partial); - } finally { - if (!done) { - done = true; - finished.accept(RestletClientStream.this); - } + // Release the call slot first: the SDK notifies the caller from super.deframerClosed(), + // and a caller that starts its next call right away must not find the slot still taken. + if (!done) { + done = true; + finished.accept(RestletClientStream.this); } + super.deframerClosed(partial); } } diff --git a/org.restlet.ext.grpc/src/test/java/org/restlet/ext/grpc/GrpcHttpValidationTestCase.java b/org.restlet.ext.grpc/src/test/java/org/restlet/ext/grpc/GrpcHttpValidationTestCase.java index 8d1f1d7f96..3321577c08 100644 --- a/org.restlet.ext.grpc/src/test/java/org/restlet/ext/grpc/GrpcHttpValidationTestCase.java +++ b/org.restlet.ext.grpc/src/test/java/org/restlet/ext/grpc/GrpcHttpValidationTestCase.java @@ -9,6 +9,7 @@ package org.restlet.ext.grpc; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -189,6 +190,41 @@ void malformedBinaryMetadataFailsExplicitly() throws Exception { assertRecovered(); } + /** + * The call slot must be free as soon as the caller is told that a call failed, otherwise the + * next call of the same thread is rejected with RESOURCE_EXHAUSTED. + */ + @Test + void failedCallReleasesItsSlotBeforeTheCallerIsNotified() throws Exception { + adapter.setMaxConcurrentCalls(1); + AtomicInteger requests = new AtomicInteger(); + start( + new Restlet() { + @Override + public void handle(Request request, Response response) { + if (requests.incrementAndGet() % 2 == 1) { + response.setEntity("", MediaType.valueOf("application/grpc")); + } else { + response.setEntity( + new ByteArrayRepresentation( + frame("recovered"), + MediaType.valueOf("application/grpc"))); + response.getTrailers().add("grpc-status", "0"); + } + } + }); + for (int i = 0; i < 200; i++) { + int call = i; + Status status = failure(); + // Only the slot matters here: depending on timing the failure is UNKNOWN or + // UNAVAILABLE. + assertNotEquals(Status.Code.OK, status.getCode(), () -> call + ": " + status); + assertNotEquals( + Status.Code.RESOURCE_EXHAUSTED, status.getCode(), () -> call + ": " + status); + assertRecovered(); + } + } + @Test void wrongContentTypeFailsAndRecovers() throws Exception { startMalformedThenHealthy(response -> response.setEntity("not grpc", MediaType.TEXT_PLAIN));