From cf56777ae4d3f5858e1938cf3c4da0ab2b5b4208 Mon Sep 17 00:00:00 2001 From: Kai Hudalla Date: Fri, 12 Apr 2019 13:31:06 +0200 Subject: [PATCH] [#780] Implement DownstreamSenderFactoryImpl. The DownstreamSenderFactory methods have been moved from HonoConnectionImpl to DownstreamSenderFactoryImpl. As part of this, the org.eclipse.hono.client.impl.*ClientImpl hierarchy has been refactored to use a HonoConnection instance instead of passing around multiple properties of the connection explicitly. This serves as the groundwork for implementing the other client factories analogously to the DownstreamSenderFactory. Signed-off-by: Kai Hudalla --- .../impl/VertxBasedAmqpProtocolAdapter.java | 6 +- .../VertxBasedAmqpProtocolAdapterTest.java | 24 +- .../coap/AbstractVertxBasedCoapAdapter.java | 6 +- .../AbstractVertxBasedCoapAdapterTest.java | 10 +- ...AbstractVertxBasedHttpProtocolAdapter.java | 6 +- ...ractVertxBasedHttpProtocolAdapterTest.java | 26 +- .../VertxBasedHttpProtocolAdapterTest.java | 10 +- ...AbstractVertxBasedMqttProtocolAdapter.java | 10 +- ...ractVertxBasedMqttProtocolAdapterTest.java | 28 +- .../hono/client/CommandConsumerFactory.java | 2 + .../hono/client/CommandResponseSender.java | 34 -- .../eclipse/hono/client/DownstreamSender.java | 169 ++++++++ .../hono/client/DownstreamSenderFactory.java | 17 +- .../eclipse/hono/client/HonoConnection.java | 141 ++++++- .../eclipse/hono/client/MessageSender.java | 142 ------- .../hono/client/impl/AbstractConsumer.java | 35 +- .../client/impl/AbstractDownstreamSender.java | 110 +++++ .../hono/client/impl/AbstractHonoClient.java | 295 ++----------- .../impl/AbstractHonoClientFactory.java | 129 ++++++ .../impl/AbstractRequestResponseClient.java | 146 ++----- .../hono/client/impl/AbstractSender.java | 81 +--- .../client/impl/AsyncCommandClientImpl.java | 43 +- .../AsyncCommandResponseConsumerImpl.java | 77 +--- .../client/impl/CachingClientFactory.java | 172 ++++++++ .../hono/client/impl/ClientFactory.java | 101 +++++ .../hono/client/impl/CommandClientImpl.java | 71 ++-- .../client/{ => impl}/CommandConsumer.java | 86 ++-- .../impl/CommandConsumerFactoryImpl.java | 27 +- .../impl/CommandResponseSenderImpl.java | 105 +---- .../client/impl/CredentialsClientImpl.java | 61 +-- .../impl/DownstreamSenderFactoryImpl.java | 85 ++++ .../hono/client/impl/EventConsumerImpl.java | 88 ++-- .../hono/client/impl/EventSenderImpl.java | 64 +-- .../hono/client/impl/HonoConnectionImpl.java | 387 ++++++++++++------ .../client/impl/RegistrationClientImpl.java | 72 ++-- .../client/impl/TelemetryConsumerImpl.java | 87 +--- .../hono/client/impl/TelemetrySenderImpl.java | 85 ++-- .../hono/client/impl/TenantClientImpl.java | 101 ++--- .../client/impl/AbstractHonoClientTest.java | 209 ---------- .../AbstractRequestResponseClientTest.java | 18 +- .../hono/client/impl/AbstractSenderTest.java | 23 +- .../client/impl/CachingClientFactoryTest.java | 103 +++++ .../client/impl/CommandClientImplTest.java | 18 +- .../impl/DownstreamSenderFactoryImplTest.java | 110 +++++ .../client/impl/EventConsumerImplTest.java | 109 +---- .../hono/client/impl/EventSenderImplTest.java | 31 +- .../client/impl/HonoClientUnitTestHelper.java | 44 ++ .../client/impl/HonoConnectionImplTest.java | 283 +++++++++++++ .../impl/RegistrationClientImplTest.java | 13 +- .../client/impl/TelemetrySenderImplTest.java | 25 +- .../client/impl/TenantClientImplTest.java | 12 +- .../hono/jmeter/client/HonoSender.java | 9 +- .../hono/service/AbstractAdapterConfig.java | 3 +- .../service/AbstractProtocolAdapterBase.java | 15 +- ...tMessageSenderConnectionEventProducer.java | 18 +- site/content/release-notes.md | 88 ++-- .../hono/tests/GenericMessageSender.java | 28 +- .../hono/tests/IntegrationTestHonoClient.java | 6 +- 58 files changed, 2336 insertions(+), 1968 deletions(-) create mode 100644 client/src/main/java/org/eclipse/hono/client/DownstreamSender.java create mode 100644 client/src/main/java/org/eclipse/hono/client/impl/AbstractDownstreamSender.java create mode 100644 client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClientFactory.java create mode 100644 client/src/main/java/org/eclipse/hono/client/impl/CachingClientFactory.java create mode 100644 client/src/main/java/org/eclipse/hono/client/impl/ClientFactory.java rename client/src/main/java/org/eclipse/hono/client/{ => impl}/CommandConsumer.java (66%) create mode 100644 client/src/main/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImpl.java create mode 100644 client/src/test/java/org/eclipse/hono/client/impl/CachingClientFactoryTest.java create mode 100644 client/src/test/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImplTest.java diff --git a/adapters/amqp-vertx/src/main/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapter.java b/adapters/amqp-vertx/src/main/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapter.java index fa3a07785b..3c9c00cdc6 100644 --- a/adapters/amqp-vertx/src/main/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapter.java +++ b/adapters/amqp-vertx/src/main/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapter.java @@ -37,8 +37,8 @@ import org.eclipse.hono.client.Command; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponse; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.service.AbstractProtocolAdapterBase; @@ -856,7 +856,7 @@ private Future uploadMessage( private Future doUploadMessage( final AmqpContext context, final ResourceIdentifier resource, - final Future senderFuture, + final Future senderFuture, final Span currentSpan) { LOG.trace("forwarding {} message", context.getEndpoint().getCanonicalName()); @@ -869,7 +869,7 @@ private Future doUploadMessage( return CompositeFuture.all(tenantEnabledFuture, tokenFuture, senderFuture) .compose(ok -> { - final MessageSender sender = senderFuture.result(); + final DownstreamSender sender = senderFuture.result(); final Message downstreamMessage = addProperties( context.getMessage(), ResourceIdentifier.from(context.getEndpoint().getCanonicalName(), resource.getTenantId(), resource.getResourceId()), diff --git a/adapters/amqp-vertx/src/test/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapterTest.java b/adapters/amqp-vertx/src/test/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapterTest.java index e81d5acd35..c4fd196778 100644 --- a/adapters/amqp-vertx/src/test/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapterTest.java +++ b/adapters/amqp-vertx/src/test/java/org/eclipse/hono/adapter/amqp/impl/VertxBasedAmqpProtocolAdapterTest.java @@ -51,10 +51,10 @@ import org.eclipse.hono.client.CommandResponse; import org.eclipse.hono.client.CommandResponseSender; import org.eclipse.hono.client.CredentialsClientFactory; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.TenantClient; @@ -227,7 +227,7 @@ public void testAdapterAcceptsAnonymousRelayReceiverOnly() { public void testUploadTelemetryWithAtMostOnceDeliverySemantics(final TestContext ctx) { // GIVEN an AMQP adapter with a configured server final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); - final MessageSender telemetrySender = givenATelemetrySenderForAnyTenant(); + final DownstreamSender telemetrySender = givenATelemetrySenderForAnyTenant(); when(telemetrySender.send(any(Message.class), (SpanContext) any())).thenReturn(Future.succeededFuture(mock(ProtonDelivery.class))); // which is enabled for a tenant @@ -265,7 +265,7 @@ public void testUploadTelemetryWithAtMostOnceDeliverySemantics(final TestContext public void testUploadTelemetryWithAtLeastOnceDeliverySemantics(final TestContext ctx) { // GIVEN an adapter configured to use a user-define server. final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); - final MessageSender telemetrySender = givenATelemetrySenderForAnyTenant(); + final DownstreamSender telemetrySender = givenATelemetrySenderForAnyTenant(); final Future downstreamDelivery = Future.future(); when(telemetrySender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(downstreamDelivery); @@ -311,7 +311,7 @@ public void testUploadTelemetryMessageFailsForDisabledAdapter(final TestContext // GIVEN an adapter configured to use a user-define server. final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); - final MessageSender telemetrySender = givenATelemetrySenderForAnyTenant(); + final DownstreamSender telemetrySender = givenATelemetrySenderForAnyTenant(); // AND given a tenant for which the AMQP Adapter is disabled givenAConfiguredTenant(TEST_TENANT_ID, false); @@ -352,7 +352,7 @@ public void testUploadEventFailsForGatewayOfDifferentTenant(final TestContext ct // GIVEN an adapter final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); - final MessageSender eventSender = givenAnEventSender(Future.future()); + final DownstreamSender eventSender = givenAnEventSender(Future.future()); // with an enabled tenant givenAConfiguredTenant(TEST_TENANT_ID, true); @@ -386,7 +386,7 @@ public void testAdapterOpensSenderLinkAndNotifyDownstreamApplication() { // GIVEN an AMQP adapter configured to use a user-defined server final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); final Future outcome = Future.future(); - final MessageSender eventSender = givenAnEventSender(outcome); + final DownstreamSender eventSender = givenAnEventSender(outcome); // WHEN an unauthenticated device opens a receiver link with a valid source address final ProtonConnection deviceConnection = mock(ProtonConnection.class); @@ -420,7 +420,7 @@ public void testAdapterClosesCommandConsumerWhenDeviceClosesReceiverLink() { // GIVEN an AMQP adapter final VertxBasedAmqpProtocolAdapter adapter = givenAnAmqpAdapter(); final Future outcome = Future.future(); - final MessageSender eventSender = givenAnEventSender(outcome); + final DownstreamSender eventSender = givenAnEventSender(outcome); // and a device that wants to receive commands final MessageConsumer commandConsumer = mock(MessageConsumer.class); @@ -496,7 +496,7 @@ public void testAdapterClosesCommandConsumerWhenConnectionToDeviceIsLost(final T private void testAdapterClosesCommandConsumer(final TestContext ctx, final Handler connectionLossTrigger) { // GIVEN an AMQP adapter - final MessageSender downstreamEventSender = givenAnEventSender(Future.succeededFuture()); + final DownstreamSender downstreamEventSender = givenAnEventSender(Future.succeededFuture()); final ProtonServer server = getAmqpServer(); final VertxBasedAmqpProtocolAdapter adapter = getAdapter(server); @@ -809,8 +809,8 @@ private Message getFakeMessage(final String to, final Buffer payload, final Stri return message; } - private MessageSender givenATelemetrySenderForAnyTenant() { - final MessageSender sender = mock(MessageSender.class); + private DownstreamSender givenATelemetrySenderForAnyTenant() { + final DownstreamSender sender = mock(DownstreamSender.class); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); return sender; } @@ -822,8 +822,8 @@ private CommandResponseSender givenACommandResponseSenderForAnyTenant() { return responseSender; } - private MessageSender givenAnEventSender(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + private DownstreamSender givenAnEventSender(final Future outcome) { + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(sender)); diff --git a/adapters/coap-vertx-base/src/main/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapter.java b/adapters/coap-vertx-base/src/main/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapter.java index 9b7e4b3672..29701877b7 100644 --- a/adapters/coap-vertx-base/src/main/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapter.java +++ b/adapters/coap-vertx-base/src/main/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapter.java @@ -37,7 +37,7 @@ import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.hono.auth.Device; import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.config.KeyLoader; import org.eclipse.hono.service.AbstractProtocolAdapterBase; import org.eclipse.hono.service.metric.MetricsTags; @@ -496,7 +496,7 @@ private void doUploadMessage( final boolean waitForOutcome, final Buffer payload, final String contentType, - final Future senderTracker, + final Future senderTracker, final MetricsTags.EndpointType endpoint) { if (contentType == null) { @@ -512,7 +512,7 @@ private void doUploadMessage( final Future tenantEnabledTracker = getTenantConfiguration(device.getTenantId(), null) .compose(tenantObject -> isAdapterEnabled(tenantObject)); CompositeFuture.all(tokenTracker, senderTracker, tenantEnabledTracker).compose(ok -> { - final MessageSender sender = senderTracker.result(); + final DownstreamSender sender = senderTracker.result(); final Message downstreamMessage = newMessage( ResourceIdentifier.from(endpoint.getCanonicalName(), device.getTenantId(), device.getDeviceId()), "/" + context.getExchange().getRequestOptions().getUriPathString(), diff --git a/adapters/coap-vertx-base/src/test/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapterTest.java b/adapters/coap-vertx-base/src/test/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapterTest.java index 2d204c7995..810376deac 100644 --- a/adapters/coap-vertx-base/src/test/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapterTest.java +++ b/adapters/coap-vertx-base/src/test/java/org/eclipse/hono/adapter/coap/AbstractVertxBasedCoapAdapterTest.java @@ -45,9 +45,9 @@ import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CredentialsClientFactory; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.TenantClient; @@ -312,7 +312,7 @@ public void testStartDoesNotInvokeOnStartupSuccessIfStartupFails(final TestConte public void testUploadTelemetryFailsForDisabledTenant() { // GIVEN an adapter - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); // which is disabled for tenant "my-tenant" final TenantObject myTenantConfig = TenantObject.from("my-tenant", true); @@ -528,7 +528,7 @@ protected void onStartupSuccess() { private void givenAnEventSenderForOutcome(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.sendAndWaitForOutcome(any(Message.class))).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(sender)); @@ -536,7 +536,7 @@ private void givenAnEventSenderForOutcome(final Future outcome) private void givenATelemetrySenderForOutcome(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.sendAndWaitForOutcome(any(Message.class))).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); @@ -544,7 +544,7 @@ private void givenATelemetrySenderForOutcome(final Future outcom private void givenATelemetrySender(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.send(any(Message.class))).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); diff --git a/adapters/http-vertx-base/src/main/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapter.java b/adapters/http-vertx-base/src/main/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapter.java index 4e3ab30989..73fa0012e3 100644 --- a/adapters/http-vertx-base/src/main/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapter.java +++ b/adapters/http-vertx-base/src/main/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapter.java @@ -28,8 +28,8 @@ import org.eclipse.hono.client.Command; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponse; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ResourceConflictException; import org.eclipse.hono.service.AbstractProtocolAdapterBase; import org.eclipse.hono.service.auth.DeviceUser; @@ -574,7 +574,7 @@ private void doUploadMessage( final String deviceId, final Buffer payload, final String contentType, - final Future senderTracker, + final Future senderTracker, final MetricsTags.EndpointType endpoint) { if (!isPayloadOfIndicatedType(payload, contentType)) { @@ -625,7 +625,7 @@ private void doUploadMessage( CompositeFuture.all(senderTracker, commandConsumerTracker) .compose(ok -> { - final MessageSender sender = senderTracker.result(); + final DownstreamSender sender = senderTracker.result(); final Integer ttd = Optional.ofNullable(commandConsumerTracker.result()).map(c -> ttdTracker.result()) .orElse(null); diff --git a/adapters/http-vertx-base/src/test/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapterTest.java b/adapters/http-vertx-base/src/test/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapterTest.java index 43cd9bd139..9eca2e5bc3 100644 --- a/adapters/http-vertx-base/src/test/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapterTest.java +++ b/adapters/http-vertx-base/src/test/java/org/eclipse/hono/adapter/http/AbstractVertxBasedHttpProtocolAdapterTest.java @@ -32,15 +32,15 @@ import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.CommandConsumer; import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponse; import org.eclipse.hono.client.CommandResponseSender; import org.eclipse.hono.client.CredentialsClientFactory; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.MessageConsumer; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.ResourceConflictException; @@ -112,7 +112,7 @@ public class AbstractVertxBasedHttpProtocolAdapterTest { private TenantClient tenantClient; private HttpProtocolAdapterProperties config; private CommandConsumerFactory commandConsumerFactory; - private CommandConsumer commandConsumer; + private MessageConsumer commandConsumer; private Vertx vertx; private Context context; private HttpAdapterMetrics metrics; @@ -161,7 +161,7 @@ public void setup() { when(registrationClientFactory.connect()).thenReturn(Future.succeededFuture(mock(HonoConnection.class))); when(registrationClientFactory.getOrCreateRegistrationClient(anyString())).thenReturn(Future.succeededFuture(regClient)); - commandConsumer = mock(CommandConsumer.class); + commandConsumer = mock(MessageConsumer.class); doAnswer(invocation -> { final Handler> resultHandler = invocation.getArgument(0); if (resultHandler != null) { @@ -264,7 +264,7 @@ public void testUploadTelemetryFailsForDisabledTenant() { // GIVEN an adapter final HttpServer server = getHttpServer(false); - final MessageSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); + final DownstreamSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); // which is disabled for tenant "my-tenant" final TenantObject myTenantConfig = TenantObject.from("my-tenant", true); @@ -310,7 +310,7 @@ public void testUploadTelemetryFailsForUnknownDevice() { // GIVEN an adapter final HttpServer server = getHttpServer(false); - final MessageSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); + final DownstreamSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); // with an enabled tenant final TenantObject myTenantConfig = TenantObject.from("my-tenant", true); @@ -642,7 +642,7 @@ public void testUploadTelemetryRemovesTtdIfCommandConsumerIsInUse() { // GIVEN an adapter with a downstream telemetry consumer attached final HttpServer server = getHttpServer(false); final AbstractVertxBasedHttpProtocolAdapter adapter = getAdapter(server, null); - final MessageSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); + final DownstreamSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); // WHEN a device publishes a telemetry message with a TTD final Buffer payload = Buffer.buffer("some payload"); @@ -689,7 +689,7 @@ public void testUploadEmptyNotificationSucceedsIfCommandConsumerIsInUse() { // GIVEN an adapter with a downstream event consumer attached final HttpServer server = getHttpServer(false); final AbstractVertxBasedHttpProtocolAdapter adapter = getAdapter(server, null); - final MessageSender sender = givenAnEventSenderForOutcome(Future.succeededFuture()); + final DownstreamSender sender = givenAnEventSenderForOutcome(Future.succeededFuture()); // WHEN a device publishes an empty notification event with a TTD final HttpServerResponse response = mock(HttpServerResponse.class); @@ -726,7 +726,7 @@ public void testUploadTelemetryUsesConfiguredMaxTtd() { // GIVEN an adapter with a downstream telemetry consumer attached final HttpServer server = getHttpServer(false); final AbstractVertxBasedHttpProtocolAdapter adapter = getAdapter(server, null); - final MessageSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); + final DownstreamSender sender = givenATelemetrySenderForOutcome(Future.succeededFuture()); // WHEN a device publishes a telemetry message that belongs to a tenant with // a max TTD of 20 secs @@ -866,18 +866,18 @@ private CommandResponseSender givenACommandResponseSenderForOutcome(final Future return sender; } - private MessageSender givenAnEventSenderForOutcome(final Future outcome) { + private DownstreamSender givenAnEventSenderForOutcome(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(sender)); return sender; } - private MessageSender givenATelemetrySenderForOutcome(final Future outcome) { + private DownstreamSender givenATelemetrySenderForOutcome(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.send(any(Message.class), (SpanContext) any())).thenReturn(outcome); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); diff --git a/adapters/http-vertx/src/test/java/org/eclipse/hono/adapter/http/impl/VertxBasedHttpProtocolAdapterTest.java b/adapters/http-vertx/src/test/java/org/eclipse/hono/adapter/http/impl/VertxBasedHttpProtocolAdapterTest.java index b9096e18c1..20cccb16cc 100644 --- a/adapters/http-vertx/src/test/java/org/eclipse/hono/adapter/http/impl/VertxBasedHttpProtocolAdapterTest.java +++ b/adapters/http-vertx/src/test/java/org/eclipse/hono/adapter/http/impl/VertxBasedHttpProtocolAdapterTest.java @@ -34,10 +34,10 @@ import org.eclipse.hono.client.CommandResponse; import org.eclipse.hono.client.CommandResponseSender; import org.eclipse.hono.client.CredentialsClientFactory; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.ServerErrorException; @@ -93,8 +93,8 @@ public class VertxBasedHttpProtocolAdapterTest { private static TenantClientFactory tenantClientFactory; private static CredentialsClientFactory credentialsClientFactory; private static DownstreamSenderFactory downstreamSenderFactory; - private static MessageSender telemetrySender; - private static MessageSender eventSender; + private static DownstreamSender telemetrySender; + private static DownstreamSender eventSender; private static RegistrationClientFactory registrationClientFactory; private static HonoClientBasedAuthProvider usernamePasswordAuthProvider; private static HttpProtocolAdapterProperties config; @@ -228,13 +228,13 @@ public void setUp() { when(commandConsumerFactory.createCommandConsumer(anyString(), anyString(), any(Handler.class), any(Handler.class))). thenReturn(Future.succeededFuture(commandConsumer)); - telemetrySender = mock(MessageSender.class); + telemetrySender = mock(DownstreamSender.class); when(telemetrySender.send(any(Message.class), (SpanContext) any())).thenReturn(Future.succeededFuture(mock(ProtonDelivery.class))); when(telemetrySender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn( Future.succeededFuture(mock(ProtonDelivery.class))); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(telemetrySender)); - eventSender = mock(MessageSender.class); + eventSender = mock(DownstreamSender.class); when(eventSender.send(any(Message.class), (SpanContext) any())).thenThrow(new UnsupportedOperationException()); when(eventSender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(Future.succeededFuture(mock(ProtonDelivery.class))); when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(eventSender)); diff --git a/adapters/mqtt-vertx-base/src/main/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapter.java b/adapters/mqtt-vertx-base/src/main/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapter.java index b43c39f913..b1bda14c6c 100644 --- a/adapters/mqtt-vertx-base/src/main/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapter.java +++ b/adapters/mqtt-vertx-base/src/main/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapter.java @@ -31,8 +31,8 @@ import org.eclipse.hono.client.Command; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponse; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.service.AbstractProtocolAdapterBase; import org.eclipse.hono.service.auth.DeviceUser; @@ -983,7 +983,7 @@ private Future uploadMessage( final String tenant, final String deviceId, final Buffer payload, - final Future senderTracker, + final Future senderTracker, final MetricsTags.EndpointType endpoint) { if (!isPayloadOfIndicatedType(payload, ctx.contentType())) { @@ -1008,7 +1008,7 @@ private Future uploadMessage( return CompositeFuture.all(tokenTracker, tenantEnabledTracker, senderTracker).compose(ok -> { - final MessageSender sender = senderTracker.result(); + final DownstreamSender sender = senderTracker.result(); final Message downstreamMessage = newMessage( ResourceIdentifier.from(endpoint.getCanonicalName(), tenant, deviceId), ctx.message().topicName(), @@ -1303,8 +1303,8 @@ private static void addRetainAnnotation(final MqttContext context, final Message private Future createLinks(final Device authenticatedDevice, final Span currentSpan) { - final Future telemetrySender = getTelemetrySender(authenticatedDevice.getTenantId()); - final Future eventSender = getEventSender(authenticatedDevice.getTenantId()); + final Future telemetrySender = getTelemetrySender(authenticatedDevice.getTenantId()); + final Future eventSender = getEventSender(authenticatedDevice.getTenantId()); return CompositeFuture .all(telemetrySender, eventSender) diff --git a/adapters/mqtt-vertx-base/src/test/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapterTest.java b/adapters/mqtt-vertx-base/src/test/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapterTest.java index 7a2a9f943a..de514fdf75 100644 --- a/adapters/mqtt-vertx-base/src/test/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapterTest.java +++ b/adapters/mqtt-vertx-base/src/test/java/org/eclipse/hono/adapter/mqtt/AbstractVertxBasedMqttProtocolAdapterTest.java @@ -42,10 +42,10 @@ import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CredentialsClientFactory; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.ServerErrorException; @@ -163,8 +163,8 @@ public void setup() { downstreamSenderFactory = mock(DownstreamSenderFactory.class); when(downstreamSenderFactory.isConnected()).thenReturn(Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE))); when(downstreamSenderFactory.connect()).thenReturn(Future.succeededFuture(mock(HonoConnection.class))); - when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(mock(MessageSender.class))); - when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(mock(MessageSender.class))); + when(downstreamSenderFactory.getOrCreateEventSender(anyString())).thenReturn(Future.succeededFuture(mock(DownstreamSender.class))); + when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(mock(DownstreamSender.class))); registrationClientFactory = mock(RegistrationClientFactory.class); when(registrationClientFactory.isConnected()).thenReturn( @@ -515,7 +515,7 @@ public void testUploadTelemetryMessageFailsForUnknownDevice(final TestContext ct // WHEN an unknown device publishes a telemetry message when(regClient.assertRegistration(eq("unknown"), any(), any())).thenReturn( Future.failedFuture(new ClientErrorException(HTTP_NOT_FOUND))); - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); final MqttPublishMessage msg = mock(MqttPublishMessage.class); @@ -561,7 +561,7 @@ public void testUploadTelemetryMessageFailsForDisabledTenant(final TestContext c when(tenantClient.get(eq("my-tenant"), (SpanContext) any())).thenReturn(Future.succeededFuture(myTenantConfig)); final AbstractVertxBasedMqttProtocolAdapter adapter = getAdapter(server); forceClientMocksToConnected(); - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(downstreamSenderFactory.getOrCreateTelemetrySender(anyString())).thenReturn(Future.succeededFuture(sender)); // WHEN a device of "my-tenant" publishes a telemetry message @@ -729,7 +729,7 @@ public void testOnUnauthenticatedMessageDoesNotSendPubAckOnFailure(final TestCon public void testUploadTelemetryMessageIncludesRetainAnnotation(final TestContext ctx) { // GIVEN an adapter with a downstream telemetry consumer - final MessageSender sender = givenAQoS1TelemetrySender(Future.succeededFuture()); + final DownstreamSender sender = givenAQoS1TelemetrySender(Future.succeededFuture()); final MqttServer server = getMqttServer(false); final AbstractVertxBasedMqttProtocolAdapter adapter = getAdapter(server); @@ -791,7 +791,7 @@ private void testOnSubscribeRegistersAndClosesConnection(final TestContext ctx, // GIVEN a device connected to an adapter final Future outcome = Future.succeededFuture(mock(ProtonDelivery.class)); - final MessageSender sender = givenAnEventSenderForOutcome(outcome); + final DownstreamSender sender = givenAnEventSenderForOutcome(outcome); final MqttServer server = getMqttServer(false); final AbstractVertxBasedMqttProtocolAdapter adapter = getAdapter(server); final MqttEndpoint endpoint = mockEndpoint(); @@ -838,7 +838,7 @@ public void testOnSubscribeIncludesStatusCodeForEachFilter(final TestContext ctx // GIVEN a device connected to an adapter final Future outcome = Future.succeededFuture(mock(ProtonDelivery.class)); - final MessageSender sender = givenAnEventSenderForOutcome(outcome); + final DownstreamSender sender = givenAnEventSenderForOutcome(outcome); final MqttServer server = getMqttServer(false); final AbstractVertxBasedMqttProtocolAdapter adapter = getAdapter(server); final MqttEndpoint endpoint = mockEndpoint(); @@ -1106,9 +1106,9 @@ protected Future onPublishedMessage(final MqttContext ctx) { return adapter; } - private MessageSender givenAnEventSenderForOutcome(final Future outcome) { + private DownstreamSender givenAnEventSenderForOutcome(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.getEndpoint()).thenReturn(EventConstants.EVENT_ENDPOINT); when(sender.send(any(Message.class), (SpanContext) any())).thenThrow(new UnsupportedOperationException()); when(sender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(outcome); @@ -1117,9 +1117,9 @@ private MessageSender givenAnEventSenderForOutcome(final Future return sender; } - private MessageSender givenAQoS0TelemetrySender() { + private DownstreamSender givenAQoS0TelemetrySender() { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.getEndpoint()).thenReturn(TelemetryConstants.TELEMETRY_ENDPOINT); when(sender.send(any(Message.class), (SpanContext) any())).thenReturn(Future.succeededFuture(mock(ProtonDelivery.class))); when(sender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenThrow(new UnsupportedOperationException()); @@ -1128,9 +1128,9 @@ private MessageSender givenAQoS0TelemetrySender() { return sender; } - private MessageSender givenAQoS1TelemetrySender(final Future outcome) { + private DownstreamSender givenAQoS1TelemetrySender(final Future outcome) { - final MessageSender sender = mock(MessageSender.class); + final DownstreamSender sender = mock(DownstreamSender.class); when(sender.getEndpoint()).thenReturn(TelemetryConstants.TELEMETRY_ENDPOINT); when(sender.send(any(Message.class), (SpanContext) any())).thenThrow(new UnsupportedOperationException()); when(sender.sendAndWaitForOutcome(any(Message.class), (SpanContext) any())).thenReturn(outcome); diff --git a/client/src/main/java/org/eclipse/hono/client/CommandConsumerFactory.java b/client/src/main/java/org/eclipse/hono/client/CommandConsumerFactory.java index 05cf41d673..dc71c6a179 100644 --- a/client/src/main/java/org/eclipse/hono/client/CommandConsumerFactory.java +++ b/client/src/main/java/org/eclipse/hono/client/CommandConsumerFactory.java @@ -13,6 +13,8 @@ package org.eclipse.hono.client; +import org.eclipse.hono.client.impl.CommandConsumer; + import io.vertx.core.Future; import io.vertx.core.Handler; diff --git a/client/src/main/java/org/eclipse/hono/client/CommandResponseSender.java b/client/src/main/java/org/eclipse/hono/client/CommandResponseSender.java index 03ee0ac8eb..67d70b6eda 100644 --- a/client/src/main/java/org/eclipse/hono/client/CommandResponseSender.java +++ b/client/src/main/java/org/eclipse/hono/client/CommandResponseSender.java @@ -12,11 +12,8 @@ *******************************************************************************/ package org.eclipse.hono.client; -import java.util.Map; - import io.opentracing.SpanContext; import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; import io.vertx.proton.ProtonDelivery; /** @@ -24,37 +21,6 @@ */ public interface CommandResponseSender extends MessageSender { - /** - * Sends a response message to a command back to the business application. - * - * @param correlationId The correlation id of the command. - * @param contentType The content type describing the response message's payload (may be {@code null}). - * @param payload The payload or {@code null}. - * @param properties The properties or {@code null}. - * @param status The status of the command, which was send to the device. - * @param context The currently active OpenTracing span or {@code null} if no - * span is currently active. An implementation should use this as the - * parent for any new span(s) it creates for tracing the execution of - * this operation. - * @return A future indicating the outcome of the operation. - *

- * The future will succeed if the message has been accepted (and settled) - * by the application. - *

- * The future will be failed with a {@link ServiceInvocationException} if the - * message could not be sent or has not been accepted by the application. - * @throws NullPointerException if any of tenantId, deviceId, replyId or correlationId is {@code null}. - * @deprecated Use {@link #sendCommandResponse(CommandResponse, SpanContext)} instead. - */ - @Deprecated - Future sendCommandResponse( - String correlationId, - String contentType, - Buffer payload, - Map properties, - int status, - SpanContext context); - /** * Sends a response message to a command back to the business application. * diff --git a/client/src/main/java/org/eclipse/hono/client/DownstreamSender.java b/client/src/main/java/org/eclipse/hono/client/DownstreamSender.java new file mode 100644 index 0000000000..7479dbe781 --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/DownstreamSender.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client; + +import java.util.Map; + +import io.vertx.core.Future; +import io.vertx.proton.ProtonDelivery; + +/** + * A client for sending messages to Hono's + * south bound Telemetry and Event APIs. + * + */ +public interface DownstreamSender extends MessageSender { + + /** + * Sends a message for a given device to the endpoint configured for this client. + * + * @param deviceId The id of the device. + *

+ * This parameter will be used as the value for the message's application property device_id. + * @param payload The data to send. + *

+ * The payload's byte representation will be contained in the message as an AMQP 1.0 + * Data section. + * @param contentType The content type of the payload. + *

+ * This parameter will be used as the value for the message's content-type property. + * If the content type specifies a particular character set, this character set will be used to + * encode the payload to its byte representation. Otherwise, UTF-8 will be used. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the message has been sent to the endpoint. + * The delivery contained in the future represents the delivery state at the time + * the future has been succeeded, i.e. for telemetry data it will be locally + * unsettled without any outcome yet. For events it will be locally + * and remotely settled and will contain the accepted outcome. + *

+ * The future will be failed with a {@link ServerErrorException} if the message + * could not be sent due to a lack of credit. + * If an event is sent which cannot be processed by the peer the future will + * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} + * depending on the reason for the failure to process the message. + * @throws NullPointerException if any of the parameters is {@code null}. + * @throws IllegalArgumentException if the content type specifies an unsupported character set. + */ + Future send(String deviceId, String payload, String contentType); + + /** + * Sends a message for a given device to the endpoint configured for this client. + * + * @param deviceId The id of the device. + *

+ * This parameter will be used as the value for the message's application property device_id. + * @param payload The data to send. + *

+ * The payload will be contained in the message as an AMQP 1.0 Data section. + * @param contentType The content type of the payload. + *

+ * This parameter will be used as the value for the message's content-type property. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the message has been sent to the endpoint. + * The delivery contained in the future represents the delivery state at the time + * the future has been succeeded, i.e. for telemetry data it will be locally + * unsettled without any outcome yet. For events it will be locally + * and remotely settled and will contain the accepted outcome. + *

+ * The future will be failed with a {@link ServerErrorException} if the message + * could not be sent due to a lack of credit. + * If an event is sent which cannot be processed by the peer the future will + * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} + * depending on the reason for the failure to process the message. + * @throws NullPointerException if any of the parameters is {@code null}. + * @throws IllegalArgumentException if the content type specifies an unsupported character set. + */ + Future send(String deviceId, byte[] payload, String contentType); + + /** + * Sends a message for a given device to the endpoint configured for this client. + * + * @param deviceId The id of the device. + *

+ * This parameter will be used as the value for the message's application property device_id. + * @param properties The application properties. + *

+ * AMQP application properties that can be used for carrying data in the message other than the payload + * @param payload The data to send. + *

+ * The payload's byte representation will be contained in the message as an AMQP 1.0 + * Data section. + * @param contentType The content type of the payload. + *

+ * This parameter will be used as the value for the message's content-type property. + * If the content type specifies a particular character set, this character set will be used to + * encode the payload to its byte representation. Otherwise, UTF-8 will be used. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the message has been sent to the endpoint. + * The delivery contained in the future represents the delivery state at the time + * the future has been succeeded, i.e. for telemetry data it will be locally + * unsettled without any outcome yet. For events it will be locally + * and remotely settled and will contain the accepted outcome. + *

+ * The future will be failed with a {@link ServerErrorException} if the message + * could not be sent due to a lack of credit. + * If an event is sent which cannot be processed by the peer the future will + * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} + * depending on the reason for the failure to process the message. + * @throws NullPointerException if any of device id, payload, content type or registration assertion + * is {@code null}. + * @throws IllegalArgumentException if the content type specifies an unsupported character set. + */ + Future send( + String deviceId, + Map properties, + String payload, + String contentType); + + /** + * Sends a message for a given device to the endpoint configured for this client. + * + * @param deviceId The id of the device. + *

+ * This parameter will be used as the value for the message's application property device_id. + * @param properties The application properties. + *

+ * AMQP application properties that can be used for carrying data in the message other than the payload + * @param payload The data to send. + *

+ * The payload will be contained in the message as an AMQP 1.0 Data section. + * @param contentType The content type of the payload. + *

+ * This parameter will be used as the value for the message's content-type property. + * @return A future indicating the outcome of the operation. + *

+ * The future will be succeeded if the message has been sent to the endpoint. + * The delivery contained in the future represents the delivery state at the time + * the future has been succeeded, i.e. for telemetry data it will be locally + * unsettled without any outcome yet. For events it will be locally + * and remotely settled and will contain the accepted outcome. + *

+ * The future will be failed with a {@link ServerErrorException} if the message + * could not be sent due to a lack of credit. + * If an event is sent which cannot be processed by the peer the future will + * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} + * depending on the reason for the failure to process the message. + * @throws NullPointerException if any of device id, payload, content type or registration assertion is {@code null}. + * @throws IllegalArgumentException if the content type specifies an unsupported character set. + */ + Future send( + String deviceId, + Map properties, + byte[] payload, + String contentType); +} diff --git a/client/src/main/java/org/eclipse/hono/client/DownstreamSenderFactory.java b/client/src/main/java/org/eclipse/hono/client/DownstreamSenderFactory.java index ceba0b16c8..0b619e50ce 100644 --- a/client/src/main/java/org/eclipse/hono/client/DownstreamSenderFactory.java +++ b/client/src/main/java/org/eclipse/hono/client/DownstreamSenderFactory.java @@ -14,6 +14,8 @@ package org.eclipse.hono.client; +import org.eclipse.hono.client.impl.DownstreamSenderFactoryImpl; + import io.vertx.core.Future; /** @@ -22,6 +24,17 @@ */ public interface DownstreamSenderFactory extends ConnectionLifecycle { + /** + * Creates a new factory for an existing connection. + * + * @param connection The connection to use. + * @return The factory. + * @throws NullPointerException if connection is {@code null} + */ + static DownstreamSenderFactory create(final HonoConnection connection) { + return new DownstreamSenderFactoryImpl(connection); + } + /** * Gets a client for sending data to Hono's south bound Telemetry API. *

@@ -34,7 +47,7 @@ public interface DownstreamSenderFactory extends ConnectionLifecycle { * create a sender for the same tenant is already being executed. * @throws NullPointerException if the tenant is {@code null}. */ - Future getOrCreateTelemetrySender(String tenantId); + Future getOrCreateTelemetrySender(String tenantId); /** * Gets a client for sending data to Hono's south bound Event API. @@ -48,5 +61,5 @@ public interface DownstreamSenderFactory extends ConnectionLifecycle { * create a sender for the same tenant is already being executed. * @throws NullPointerException if the tenant is {@code null}. */ - Future getOrCreateEventSender(String tenantId); + Future getOrCreateEventSender(String tenantId); } diff --git a/client/src/main/java/org/eclipse/hono/client/HonoConnection.java b/client/src/main/java/org/eclipse/hono/client/HonoConnection.java index 137adcbe07..1545940db5 100644 --- a/client/src/main/java/org/eclipse/hono/client/HonoConnection.java +++ b/client/src/main/java/org/eclipse/hono/client/HonoConnection.java @@ -17,12 +17,18 @@ import org.eclipse.hono.client.impl.HonoConnectionImpl; import org.eclipse.hono.config.ClientConfigProperties; +import io.opentracing.Tracer; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.proton.ProtonClientOptions; import io.vertx.proton.ProtonConnection; +import io.vertx.proton.ProtonLink; +import io.vertx.proton.ProtonMessageHandler; +import io.vertx.proton.ProtonQoS; +import io.vertx.proton.ProtonReceiver; +import io.vertx.proton.ProtonSender; /** * A factory for creating clients for Hono's arbitrary APIs. @@ -64,7 +70,6 @@ * the {@code Context}'s runOnContext method. */ public interface HonoConnection extends ConnectionLifecycle, - DownstreamSenderFactory, ApplicationClientFactory, CredentialsClientFactory, RegistrationClientFactory, @@ -86,6 +91,30 @@ static HonoConnection newConnection(final Vertx vertx, final ClientConfigPropert return new HonoConnectionImpl(vertx, clientConfigProperties); } + /** + * Gets the vert.x instance used by this connection. + *

+ * The returned instance may be used to e.g. schedule timers. + * + * @return The vert.x instance. + */ + Vertx getVertx(); + + /** + * Gets the OpenTracing {@code Tracer} used for tracking + * distributed interactions across process boundaries. + * + * @return The tracer. + */ + Tracer getTracer(); + + /** + * Gets the configuration properties used for creating this connection. + * + * @return The configuration. + */ + ClientConfigProperties getConfig(); + /** * {@inheritDoc} * @@ -267,4 +296,114 @@ Future connect( * AMQP open frame, {@code false} otherwise. */ boolean supportsCapability(Symbol capability); + + + /** + * Executes some code on the vert.x Context that has been used to establish the + * connection to the peer. + * + * @param The type of the result that the code produces. + * @param codeToRun The code to execute. The code is required to either complete or + * fail the future that is passed into the handler. + * @return The future passed into the handler for executing the code. The future + * thus indicates the outcome of executing the code. The future will + * be failed with a {@link ServerErrorException} if the context + * property is {@code null}. + */ + Future executeOrRunOnContext(Handler> codeToRun); + + /** + * Creates a sender link. + * + * @param targetAddress The target address of the link. + * @param qos The quality of service to use for the link. + * @param remoteCloseHook The handler to invoke when the link is closed by the peer (may be {@code null}). + * @return A future for the created link. The future will be completed once the link is open. + * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. + * @throws NullPointerException if any of the arguments other than close hook is {@code null}. + */ + Future createSender( + String targetAddress, + ProtonQoS qos, + Handler remoteCloseHook); + + /** + * Creates a receiver link. + *

+ * The receiver will be created with its autoAccept property set to {@code true} + * and with the connection's default pre-fetch size. + * + * @param sourceAddress The address to receive messages from. + * @param qos The quality of service to use for the link. + * @param messageHandler The handler to invoke with every message received. + * @param remoteCloseHook The handler to invoke when the link is closed at the peer's request (may be {@code null}). + * @return A future for the created link. The future will be completed once the link is open. + * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. + * @throws NullPointerException if any of the arguments other than close hook is {@code null}. + */ + Future createReceiver( + String sourceAddress, + ProtonQoS qos, + ProtonMessageHandler messageHandler, + Handler remoteCloseHook); + + /** + * Creates a receiver link. + *

+ * The receiver will be created with its autoAccept property set to {@code true}. + * + * @param sourceAddress The address to receive messages from. + * @param qos The quality of service to use for the link. + * @param messageHandler The handler to invoke with every message received. + * @param preFetchSize The number of credits to flow to the peer as soon as the link + * has been established. A value of 0 prevents pre-fetching and + * allows for manual flow control using the returned receiver's + * flow method. + * @param remoteCloseHook The handler to invoke when the link is closed at the peer's request (may be {@code null}). + * @return A future for the created link. The future will be completed once the link is open. + * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. + * @throws NullPointerException if any of the arguments other than close hook is {@code null}. + * @throws IllegalArgumentException if the pre-fetch size is < 0. + */ + Future createReceiver( + String sourceAddress, + ProtonQoS qos, + ProtonMessageHandler messageHandler, + int preFetchSize, + Handler remoteCloseHook); + + /** + * Closes an AMQP link and frees up its allocated resources. + *

+ * This method is equivalent to {@link #closeAndFree(ProtonLink, long, Handler)} + * but will use an implementation specific default time-out value. + * + * @param link The link to close. If {@code null}, the given handler is invoked immediately. + * @param closeHandler The handler to notify once the link has been closed. + * @throws NullPointerException if context or close handler are {@code null}. + */ + void closeAndFree(ProtonLink link, Handler closeHandler); + + /** + * Closes an AMQP link and frees up its allocated resources. + *

+ * This method will invoke the given handler as soon as + *

    + *
  • the peer's detach frame has been received or
  • + *
  • the given number of milliseconds have passed
  • + *
+ * Afterwards the link's resources are freed up. + * + * @param link The link to close. If {@code null}, the given handler is invoked immediately. + * @param detachTimeOut The maximum number of milliseconds to wait for the peer's + * detach frame or 0, if this method should wait indefinitely + * for the peer's detach frame. + * @param closeHandler The handler to notify once the link has been closed. + * @throws NullPointerException if context or close handler are {@code null}. + * @throws IllegalArgumentException if detach time-out is < 0. + */ + void closeAndFree( + ProtonLink link, + long detachTimeOut, + Handler closeHandler); } diff --git a/client/src/main/java/org/eclipse/hono/client/MessageSender.java b/client/src/main/java/org/eclipse/hono/client/MessageSender.java index 61f2d76e31..df18351627 100644 --- a/client/src/main/java/org/eclipse/hono/client/MessageSender.java +++ b/client/src/main/java/org/eclipse/hono/client/MessageSender.java @@ -13,8 +13,6 @@ package org.eclipse.hono.client; -import java.util.Map; - import org.apache.qpid.proton.message.Message; import io.opentracing.SpanContext; @@ -151,144 +149,4 @@ default Future send(Message message, SpanContext context) { default Future sendAndWaitForOutcome(Message message, SpanContext context) { return sendAndWaitForOutcome(message); } - - /** - * Sends a message for a given device to the endpoint configured for this client. - * - * @param deviceId The id of the device. - *

- * This parameter will be used as the value for the message's application property device_id. - * @param payload The data to send. - *

- * The payload's byte representation will be contained in the message as an AMQP 1.0 - * Data section. - * @param contentType The content type of the payload. - *

- * This parameter will be used as the value for the message's content-type property. - * If the content type specifies a particular character set, this character set will be used to - * encode the payload to its byte representation. Otherwise, UTF-8 will be used. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the message has been sent to the endpoint. - * The delivery contained in the future represents the delivery state at the time - * the future has been succeeded, i.e. for telemetry data it will be locally - * unsettled without any outcome yet. For events it will be locally - * and remotely settled and will contain the accepted outcome. - *

- * The future will be failed with a {@link ServerErrorException} if the message - * could not be sent due to a lack of credit. - * If an event is sent which cannot be processed by the peer the future will - * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} - * depending on the reason for the failure to process the message. - * @throws NullPointerException if any of the parameters are {@code null}. - * @throws IllegalArgumentException if the content type specifies an unsupported character set. - */ - Future send(String deviceId, String payload, String contentType); - - /** - * Sends a message for a given device to the endpoint configured for this client. - * - * @param deviceId The id of the device. - *

- * This parameter will be used as the value for the message's application property device_id. - * @param payload The data to send. - *

- * The payload will be contained in the message as an AMQP 1.0 Data section. - * @param contentType The content type of the payload. - *

- * This parameter will be used as the value for the message's content-type property. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the message has been sent to the endpoint. - * The delivery contained in the future represents the delivery state at the time - * the future has been succeeded, i.e. for telemetry data it will be locally - * unsettled without any outcome yet. For events it will be locally - * and remotely settled and will contain the accepted outcome. - *

- * The future will be failed with a {@link ServerErrorException} if the message - * could not be sent due to a lack of credit. - * If an event is sent which cannot be processed by the peer the future will - * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} - * depending on the reason for the failure to process the message. - * @throws NullPointerException if any of the parameters are {@code null}. - * @throws IllegalArgumentException if the content type specifies an unsupported character set. - */ - Future send(String deviceId, byte[] payload, String contentType); - - /** - * Sends a message for a given device to the endpoint configured for this client. - * - * @param deviceId The id of the device. - *

- * This parameter will be used as the value for the message's application property device_id. - * @param properties The application properties. - *

- * AMQP application properties that can be used for carrying data in the message other than the payload - * @param payload The data to send. - *

- * The payload's byte representation will be contained in the message as an AMQP 1.0 - * Data section. - * @param contentType The content type of the payload. - *

- * This parameter will be used as the value for the message's content-type property. - * If the content type specifies a particular character set, this character set will be used to - * encode the payload to its byte representation. Otherwise, UTF-8 will be used. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the message has been sent to the endpoint. - * The delivery contained in the future represents the delivery state at the time - * the future has been succeeded, i.e. for telemetry data it will be locally - * unsettled without any outcome yet. For events it will be locally - * and remotely settled and will contain the accepted outcome. - *

- * The future will be failed with a {@link ServerErrorException} if the message - * could not be sent due to a lack of credit. - * If an event is sent which cannot be processed by the peer the future will - * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} - * depending on the reason for the failure to process the message. - * @throws NullPointerException if any of device id, payload or content type are {@code null}. - * @throws IllegalArgumentException if the content type specifies an unsupported character set. - */ - Future send( - String deviceId, - Map properties, - String payload, - String contentType); - - /** - * Sends a message for a given device to the endpoint configured for this client. - * - * @param deviceId The id of the device. - *

- * This parameter will be used as the value for the message's application property device_id. - * @param properties The application properties. - *

- * AMQP application properties that can be used for carrying data in the message other than the payload - * @param payload The data to send. - *

- * The payload will be contained in the message as an AMQP 1.0 Data section. - * @param contentType The content type of the payload. - *

- * This parameter will be used as the value for the message's content-type property. - * @return A future indicating the outcome of the operation. - *

- * The future will be succeeded if the message has been sent to the endpoint. - * The delivery contained in the future represents the delivery state at the time - * the future has been succeeded, i.e. for telemetry data it will be locally - * unsettled without any outcome yet. For events it will be locally - * and remotely settled and will contain the accepted outcome. - *

- * The future will be failed with a {@link ServerErrorException} if the message - * could not be sent due to a lack of credit. - * If an event is sent which cannot be processed by the peer the future will - * be failed with either a {@code ServerErrorException} or a {@link ClientErrorException} - * depending on the reason for the failure to process the message. - * @throws NullPointerException if any of device id, payload or content type are {@code null}. - * @throws IllegalArgumentException if the content type specifies an unsupported character set. - */ - Future send( - String deviceId, - Map properties, - byte[] payload, - String contentType); } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractConsumer.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractConsumer.java index 3046b9da2c..1721350d13 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AbstractConsumer.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractConsumer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,16 +13,14 @@ package org.eclipse.hono.client.impl; -import io.opentracing.Tracer; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.client.MessageConsumer; + import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.proton.ProtonReceiver; -import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.config.ClientConfigProperties; - /** * Abstract client for consuming messages from a Hono server. */ @@ -33,31 +31,12 @@ public abstract class AbstractConsumer extends AbstractHonoClient implements Mes /** * Creates an abstract message consumer. * - * @param context The vert.x context to run all interactions with the server on. - * @param config The configuration properties to use. - * @param receiver The proton receiver link. - */ - public AbstractConsumer(final Context context, final ClientConfigProperties config, final ProtonReceiver receiver) { - - this(context, config, receiver, null); - } - - /** - * Creates an abstract message consumer. - * - * @param context The vert.x context to run all interactions with the server on. - * @param config The configuration properties to use. + * @param connection The connection to use. * @param receiver The proton receiver link. - * @param tracer The tracer to use for tracking the processing of received - * messages. If {@code null}, the *noop* tracer will be used. */ - public AbstractConsumer( - final Context context, - final ClientConfigProperties config, - final ProtonReceiver receiver, - final Tracer tracer) { + public AbstractConsumer(final HonoConnection connection, final ProtonReceiver receiver) { - super(context, config, tracer); + super(connection); this.receiver = receiver; } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractDownstreamSender.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractDownstreamSender.java new file mode 100644 index 0000000000..3aca706dbd --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractDownstreamSender.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.hono.client.impl; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.util.MessageHelper; + +import io.vertx.core.Future; +import io.vertx.proton.ProtonDelivery; +import io.vertx.proton.ProtonHelper; +import io.vertx.proton.ProtonSender; + +/** + * A Vertx-Proton based client for publishing messages to Hono. + */ +public abstract class AbstractDownstreamSender extends AbstractSender implements DownstreamSender { + + /** + * A counter to be used for creating message IDs. + */ + protected static final AtomicLong MESSAGE_COUNTER = new AtomicLong(); + + private static final Pattern CHARSET_PATTERN = Pattern.compile("^.*;charset=(.*)$"); + + /** + * Creates a new sender. + * + * @param connection The connection to use for interacting with the server. + * @param sender The sender link to send messages over. + * @param tenantId The identifier of the tenant that the + * devices belong to which have published the messages + * that this sender is used to send downstream. + * @param targetAddress The target address to send the messages to. + */ + protected AbstractDownstreamSender( + final HonoConnection connection, + final ProtonSender sender, + final String tenantId, + final String targetAddress) { + + super(connection, sender, tenantId, targetAddress); + } + + + @Override + public final Future send(final String deviceId, final byte[] payload, final String contentType) { + return send(deviceId, null, payload, contentType); + } + + @Override + public final Future send(final String deviceId, final String payload, final String contentType) { + return send(deviceId, null, payload, contentType); + } + + @Override + public final Future send(final String deviceId, final Map properties, final String payload, final String contentType) { + Objects.requireNonNull(payload); + final Charset charset = getCharsetForContentType(Objects.requireNonNull(contentType)); + return send(deviceId, properties, payload.getBytes(charset), contentType); + } + + @Override + public final Future send(final String deviceId, final Map properties, final byte[] payload, final String contentType) { + Objects.requireNonNull(deviceId); + Objects.requireNonNull(payload); + Objects.requireNonNull(contentType); + + final Message msg = ProtonHelper.message(); + msg.setAddress(getTo(deviceId)); + MessageHelper.setPayload(msg, contentType, payload); + setApplicationProperties(msg, properties); + addProperties(msg, deviceId); + return send(msg); + } + + private void addProperties(final Message msg, final String deviceId) { + MessageHelper.addDeviceId(msg, deviceId); + } + + private Charset getCharsetForContentType(final String contentType) { + + final Matcher m = CHARSET_PATTERN.matcher(contentType); + if (m.matches()) { + return Charset.forName(m.group(1)); + } else { + return StandardCharsets.UTF_8; + } + } +} diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClient.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClient.java index 7e6e1f9a93..a24e7b388a 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClient.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClient.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,41 +13,27 @@ package org.eclipse.hono.client.impl; -import java.net.HttpURLConnection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; -import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServiceInvocationException; -import org.eclipse.hono.client.StatusCodeMapper; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.tracing.TracingHelper; -import org.eclipse.hono.util.HonoProtonHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.opentracing.References; import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; -import io.opentracing.noop.NoopTracerFactory; import io.opentracing.tag.Tags; -import io.vertx.core.Context; -import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; -import io.vertx.proton.ProtonLink; -import io.vertx.proton.ProtonMessageHandler; -import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonReceiver; import io.vertx.proton.ProtonSender; @@ -62,18 +48,14 @@ public abstract class AbstractHonoClient { private static final Logger LOG = LoggerFactory.getLogger(AbstractHonoClient.class); /** - * The vertx context to run all interactions with the server on. + * The connection to the server. */ - protected final Context context; - /** - * The configuration properties for this client. - */ - protected final ClientConfigProperties config; + protected final HonoConnection connection; /** * The vertx-proton object used for sending messages to the server. */ - protected ProtonSender sender; + protected ProtonSender sender; /** * The vertx-proton object used for receiving messages from the server. */ @@ -82,52 +64,15 @@ public abstract class AbstractHonoClient { * The capabilities offered by the peer. */ protected List offeredCapabilities = Collections.emptyList(); - /** - * The OpenTracing tracer to use for tracking request processing - * across process boundaries. - */ - protected Tracer tracer; - - /** - * Creates a client for a vert.x context. - * - * @param context The context to run all interactions with the server on. - * @param config The configuration properties to use. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - protected AbstractHonoClient(final Context context, final ClientConfigProperties config) { - this(context, config, null); - } - - /** - * Creates a client for a vert.x context. - * - * @param context The context to run all interactions with the server on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing - * across process boundaries. - * @throws NullPointerException if context or config are {@code null}. - */ - protected AbstractHonoClient(final Context context, final ClientConfigProperties config, final Tracer tracer) { - this.context = Objects.requireNonNull(context); - this.config = Objects.requireNonNull(config); - this.tracer = Optional.ofNullable(tracer).orElse(NoopTracerFactory.create()); - } /** - * Executes some code on the vert.x Context that has been used to establish the - * connection to the peer. + * Creates a client for a connection. * - * @param The type of the result that the code produces. - * @param codeToRun The code to execute. The code is required to either complete or - * fail the future that is passed into the handler. - * @return The future passed into the handler for executing the code. The future - * thus indicates the outcome of executing the code. + * @param connection The connection to use. + * @throws NullPointerException if any of the parameters are {@code null}. */ - protected final Future executeOrRunOnContext( - final Handler> codeToRun) { - - return HonoProtonHelper.executeOrRunOnContext(context, codeToRun); + protected AbstractHonoClient(final HonoConnection connection) { + this.connection = Objects.requireNonNull(connection); } /** @@ -193,11 +138,11 @@ protected final Span newFollowingSpan(final SpanContext parent, final String ope private Span newSpan(final SpanContext parent, final String referenceType, final String operationName) { - return tracer.buildSpan(operationName) + return connection.getTracer().buildSpan(operationName) .addReference(referenceType, parent) .withTag(Tags.COMPONENT.getKey(), "hono-client") - .withTag(Tags.PEER_HOSTNAME.getKey(), config.getHost()) - .withTag(Tags.PEER_PORT.getKey(), config.getPort()) + .withTag(Tags.PEER_HOSTNAME.getKey(), connection.getConfig().getHost()) + .withTag(Tags.PEER_PORT.getKey(), connection.getConfig().getPort()) .start(); } @@ -231,15 +176,19 @@ protected final void closeLinks(final Handler closeHandler) { Objects.requireNonNull(closeHandler); - if (sender != null) { - LOG.debug("locally closing sender link [{}]", sender.getTarget().getAddress()); - } - HonoProtonHelper.closeAndFree(context, sender, senderClosed -> { + final Handler closeReceiver = s -> { if (receiver != null) { LOG.debug("locally closing receiver link [{}]", receiver.getSource().getAddress()); } - HonoProtonHelper.closeAndFree(context, receiver, receiverClosed -> closeHandler.handle(null)); - }); + connection.closeAndFree(receiver, receiverClosed -> closeHandler.handle(null)); + }; + + if (sender != null) { + LOG.debug("locally closing sender link [{}]", sender.getTarget().getAddress()); + connection.closeAndFree(sender, senderClosed -> closeReceiver.handle(null)); + } else if (receiver != null) { + closeReceiver.handle(null); + } } /** @@ -272,202 +221,4 @@ protected static final void setApplicationProperties(final Message msg, final Ma msg.setApplicationProperties(applicationProperties); } } - - /** - * Creates a sender link. - * - * @param ctx The vert.x context to use for establishing the link. - * @param clientConfig The configuration properties to use. - * @param con The connection to create the link for. - * @param targetAddress The target address of the link. - * @param qos The quality of service to use for the link. - * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @return A future for the created link. The future will be completed once the link is open. - * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. - * @throws NullPointerException if any of the arguments other than close hook is {@code null}. - */ - protected static final Future createSender( - final Context ctx, - final ClientConfigProperties clientConfig, - final ProtonConnection con, - final String targetAddress, - final ProtonQoS qos, - final Handler closeHook) { - - Objects.requireNonNull(ctx); - Objects.requireNonNull(clientConfig); - Objects.requireNonNull(con); - Objects.requireNonNull(targetAddress); - Objects.requireNonNull(qos); - - return HonoProtonHelper.executeOrRunOnContext(ctx, result -> { - - final ProtonSender sender = con.createSender(targetAddress); - sender.setQoS(qos); - sender.setAutoSettle(true); - sender.openHandler(senderOpen -> { - - // we only "try" to complete/fail the result future because - // it may already have been failed if the connection broke - // away after we have sent our attach frame but before we have - // received the peer's attach frame - - if (senderOpen.failed()) { - // this means that we have received the peer's attach - // and the subsequent detach frame in one TCP read - final ErrorCondition error = sender.getRemoteCondition(); - if (error == null) { - LOG.debug("opening sender [{}] failed", targetAddress, senderOpen.cause()); - result.tryFail(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND, "cannot open sender", senderOpen.cause())); - } else { - LOG.debug("opening sender [{}] failed: {} - {}", targetAddress, error.getCondition(), error.getDescription()); - result.tryFail(StatusCodeMapper.from(error)); - } - - } else if (HonoProtonHelper.isLinkEstablished(sender)) { - - LOG.debug("sender open [target: {}, sendQueueFull: {}]", targetAddress, sender.sendQueueFull()); - // wait on credits a little time, if not already given - if (sender.getCredit() <= 0) { - ctx.owner().setTimer(clientConfig.getFlowLatency(), timerID -> { - LOG.debug("sender [target: {}] has {} credits after grace period of {}ms", targetAddress, - sender.getCredit(), clientConfig.getFlowLatency()); - result.tryComplete(sender); - }); - } else { - result.tryComplete(sender); - } - - } else { - // this means that the peer did not create a local terminus for the link - // and will send a detach frame for closing the link very shortly - // see AMQP 1.0 spec section 2.6.3 - LOG.debug("peer did not create terminus for target [{}] and will detach the link", targetAddress); - result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); - } - }); - HonoProtonHelper.setDetachHandler(sender, remoteDetached -> onRemoteDetach(sender, con.getRemoteContainer(), false, closeHook)); - HonoProtonHelper.setCloseHandler(sender, remoteClosed -> onRemoteDetach(sender, con.getRemoteContainer(), true, closeHook)); - sender.open(); - ctx.owner().setTimer(clientConfig.getLinkEstablishmentTimeout(), tid -> onTimeOut(sender, clientConfig, result)); - }); - } - - /** - * Creates a receiver link. - *

- * The receiver will be created with its autoAccept property set to {@code true}. - * - * @param ctx The vert.x context to use for establishing the link. - * @param clientConfig The configuration properties to use. - * @param con The connection to create the link for. - * @param sourceAddress The address to receive messages from. - * @param qos The quality of service to use for the link. - * @param messageHandler The handler to invoke with every message received. - * @param remoteCloseHook The handler to invoke when the link is closed at the peer's request (may be {@code null}). - * @return A future for the created link. The future will be completed once the link is open. - * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. - * @throws NullPointerException if any of the arguments other than close hook is {@code null}. - */ - protected static final Future createReceiver( - final Context ctx, - final ClientConfigProperties clientConfig, - final ProtonConnection con, - final String sourceAddress, - final ProtonQoS qos, - final ProtonMessageHandler messageHandler, - final Handler remoteCloseHook) { - - Objects.requireNonNull(ctx); - Objects.requireNonNull(clientConfig); - Objects.requireNonNull(con); - Objects.requireNonNull(sourceAddress); - Objects.requireNonNull(qos); - Objects.requireNonNull(messageHandler); - - return HonoProtonHelper.executeOrRunOnContext(ctx, result -> { - final ProtonReceiver receiver = con.createReceiver(sourceAddress); - receiver.setAutoAccept(true); - receiver.setQoS(qos); - receiver.setPrefetch(clientConfig.getInitialCredits()); - receiver.handler((delivery, message) -> { - messageHandler.handle(delivery, message); - if (LOG.isTraceEnabled()) { - final int remainingCredits = receiver.getCredit() - receiver.getQueued(); - LOG.trace("handling message [remotely settled: {}, queued messages: {}, remaining credit: {}]", - delivery.remotelySettled(), receiver.getQueued(), remainingCredits); - } - }); - receiver.openHandler(recvOpen -> { - - // we only "try" to complete/fail the result future because - // it may already have been failed if the connection broke - // away after we have sent our attach frame but before we have - // received the peer's attach frame - - if (recvOpen.failed()) { - // this means that we have received the peer's attach - // and the subsequent detach frame in one TCP read - final ErrorCondition error = receiver.getRemoteCondition(); - if (error == null) { - LOG.debug("opening receiver [{}] failed", sourceAddress, recvOpen.cause()); - result.tryFail(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND, "cannot open receiver", recvOpen.cause())); - } else { - LOG.debug("opening receiver [{}] failed: {} - {}", sourceAddress, error.getCondition(), error.getDescription()); - result.tryFail(StatusCodeMapper.from(error)); - } - } else if (HonoProtonHelper.isLinkEstablished(receiver)) { - LOG.debug("receiver open [source: {}]", sourceAddress); - result.tryComplete(recvOpen.result()); - } else { - // this means that the peer did not create a local terminus for the link - // and will send a detach frame for closing the link very shortly - // see AMQP 1.0 spec section 2.6.3 - LOG.debug("peer did not create terminus for source [{}] and will detach the link", sourceAddress); - result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); - } - }); - HonoProtonHelper.setDetachHandler(receiver, remoteDetached -> onRemoteDetach(receiver, con.getRemoteContainer(), false, remoteCloseHook)); - HonoProtonHelper.setCloseHandler(receiver, remoteClosed -> onRemoteDetach(receiver, con.getRemoteContainer(), true, remoteCloseHook)); - receiver.open(); - ctx.owner().setTimer(clientConfig.getLinkEstablishmentTimeout(), tid -> onTimeOut(receiver, clientConfig, result)); - }); - } - - private static void onTimeOut( - final ProtonLink link, - final ClientConfigProperties clientConfig, - final Future result) { - - if (link.isOpen() && !HonoProtonHelper.isLinkEstablished(link)) { - LOG.info("link establishment [peer: {}] timed out after {}ms", - clientConfig.getHost(), clientConfig.getLinkEstablishmentTimeout()); - link.close(); - link.free(); - result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); - } - } - - private static void onRemoteDetach( - final ProtonLink link, - final String remoteContainer, - final boolean closed, - final Handler closeHook) { - - final ErrorCondition error = link.getRemoteCondition(); - final String type = link instanceof ProtonSender ? "sender" : "receiver"; - final String address = link instanceof ProtonSender ? link.getTarget().getAddress() : - link.getSource().getAddress(); - if (error == null) { - LOG.debug("{} [{}] detached (with closed={}) by peer [{}]", - type, address, closed, remoteContainer); - } else { - LOG.debug("{} [{}] detached (with closed={}) by peer [{}]: {} - {}", - type, address, closed, remoteContainer, error.getCondition(), error.getDescription()); - } - link.close(); - if (HonoProtonHelper.isLinkEstablished(link) && closeHook != null) { - closeHook.handle(address); - } - } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClientFactory.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClientFactory.java new file mode 100644 index 0000000000..81521ae7f5 --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractHonoClientFactory.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import java.util.Objects; + +import org.eclipse.hono.client.ConnectionLifecycle; +import org.eclipse.hono.client.DisconnectListener; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.client.ReconnectListener; +import org.eclipse.hono.client.ServerErrorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; + +/** + * A base class for implementing client factories. + */ +abstract class AbstractHonoClientFactory implements ConnectionLifecycle { + + /** + * A logger to be shared with subclasses. + */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + /** + * The connection to use for interacting with Hono. + */ + protected final HonoConnection connection; + + /** + * @param connection The connection to use. + */ + AbstractHonoClientFactory(final HonoConnection connection) { + this.connection = Objects.requireNonNull(connection); + this.connection.addDisconnectListener(con -> onDisconnect()); + } + + /** + * {@inheritDoc} + *

+ * Simply delegates to {@link HonoConnection#addDisconnectListener(DisconnectListener)}. + */ + @Override + public void addDisconnectListener(final DisconnectListener listener) { + connection.addDisconnectListener(listener); + } + + /** + * {@inheritDoc} + *

+ * Simply delegates to {@link HonoConnection#addReconnectListener(ReconnectListener)}. + */ + @Override + public void addReconnectListener(final ReconnectListener listener) { + connection.addReconnectListener(listener); + } + + /** + * {@inheritDoc} + *

+ * Simply delegates to {@link HonoConnection#connect()}. + */ + @Override + public Future connect() { + return connection.connect(); + } + + /** + * Checks whether this client is connected to the service. + *

+ * Simply delegates to {@link HonoConnection#isConnected()}. + * + * @return A succeeded future if this factory is connected. + * Otherwise, the future will be failed with a + * {@link ServerErrorException}. + */ + @Override + public final Future isConnected() { + return connection.isConnected(); + } + + /** + * {@inheritDoc} + *

+ * This default implementation simply delegates to {@link HonoConnection#disconnect()}. + */ + @Override + public void disconnect() { + connection.disconnect(); + } + + /** + * {@inheritDoc} + *

+ * This default implementation simply delegates to {@link HonoConnection#disconnect(Handler)}. + */ + @Override + public void disconnect(final Handler> completionHandler) { + connection.disconnect(completionHandler); + } + + /** + * Invoked when the underlying connection to the Hono server + * is lost unexpectedly. + *

+ * This default implementation does nothing. + * Subclasses should override this method in order to clean + * up any state that may have become stale with the loss + * of the connection. + */ + protected void onDisconnect() { + // do nothing + } +} diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractRequestResponseClient.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractRequestResponseClient.java index 9dad43290c..ef6dbfc66f 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AbstractRequestResponseClient.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractRequestResponseClient.java @@ -30,12 +30,12 @@ import org.apache.qpid.proton.message.Message; import org.eclipse.hono.cache.ExpiringValueCache; import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RequestResponseClient; import org.eclipse.hono.client.RequestResponseClientConfigProperties; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.client.StatusCodeMapper; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; @@ -46,14 +46,11 @@ import org.slf4j.LoggerFactory; import io.opentracing.Span; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonQoS; @@ -95,23 +92,6 @@ public abstract class AbstractRequestResponseClient - * This constructor simply invokes - * {@link #AbstractRequestResponseClient(Context, ClientConfigProperties, Tracer, String)} - * with {@code null} as the tracer. - * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tenantId The tenant that the client should be scoped to or {@code null} if the - * client should not be scoped to a tenant. - * @throws NullPointerException if any of context or configuration are {@code null}. - */ - protected AbstractRequestResponseClient(final Context context, final ClientConfigProperties config, final String tenantId) { - this(context, config, (Tracer) null, tenantId); - } - /** * Creates a request-response client. *

@@ -124,25 +104,20 @@ protected AbstractRequestResponseClient(final Context context, final ClientConfi * The latter address is also used as the value of the reply-to * property of all request messages sent by this client. *

- * The client will be ready to use after invoking {@link #createLinks(ProtonConnection)} or - * {@link #createLinks(ProtonConnection, Handler, Handler)} only. + * The client will be ready to use after invoking {@link #createLinks()} or + * {@link #createLinks(Handler, Handler)} only. * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing across process - * boundaries or {@code null} to disable tracing. + * @param connection The connection to the service. * @param tenantId The tenant that the client should be scoped to or {@code null} if the * client should not be scoped to a tenant. * @throws NullPointerException if any of context or configuration are {@code null}. */ protected AbstractRequestResponseClient( - final Context context, - final ClientConfigProperties config, - final Tracer tracer, + final HonoConnection connection, final String tenantId) { - super(context, config, tracer); - this.requestTimeoutMillis = config.getRequestTimeout(); + super(connection); + this.requestTimeoutMillis = connection.getConfig().getRequestTimeout(); if (tenantId == null) { this.targetAddress = getName(); this.replyToAddress = String.format("%s/%s", getName(), UUID.randomUUID()); @@ -153,29 +128,6 @@ protected AbstractRequestResponseClient( this.tenantId = tenantId; } - /** - * Creates a request-response client. - *

- * This methods simply invokes - * {@link #AbstractRequestResponseClient(Context, ClientConfigProperties, Tracer, String, String, String)} - * with {@code null} as the tracer. - * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tenantId The tenant that the device belongs to. - * @param deviceId The device to create the client for. - * @param replyId The replyId to use in the reply-to address. - * @throws NullPointerException if any of the parameters are {@code null}. - */ - protected AbstractRequestResponseClient( - final Context context, - final ClientConfigProperties config, - final String tenantId, - final String deviceId, - final String replyId) { - this(context, config, null, tenantId, deviceId, replyId); - } - /** * Creates a request-response client. *

@@ -188,32 +140,27 @@ protected AbstractRequestResponseClient( * The latter address is also used as the value of the reply-to * property of all request messages sent by this client. *

- * The client will be ready to use after invoking {@link #createLinks(ProtonConnection)} or - * {@link #createLinks(ProtonConnection, Handler, Handler)} only. + * The client will be ready to use after invoking {@link #createLinks()} or + * {@link #createLinks(Handler, Handler)} only. * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing across process - * boundaries or {@code null} to disable tracing. + * @param connection The connection to the service. * @param tenantId The tenant that the device belongs to. * @param deviceId The device to create the client for. * @param replyId The replyId to use in the reply-to address. * @throws NullPointerException if any of the parameters other than tracer are {@code null}. */ protected AbstractRequestResponseClient( - final Context context, - final ClientConfigProperties config, - final Tracer tracer, + final HonoConnection connection, final String tenantId, final String deviceId, final String replyId) { - super(context, config, tracer); + super(connection); Objects.requireNonNull(tenantId); Objects.requireNonNull(deviceId); Objects.requireNonNull(replyId); - this.requestTimeoutMillis = config.getRequestTimeout(); + this.requestTimeoutMillis = connection.getConfig().getRequestTimeout(); this.targetAddress = String.format("%s/%s/%s", getName(), tenantId, deviceId); this.replyToAddress = String.format("%s/%s/%s/%s", getName(), tenantId, deviceId, replyId); this.tenantId = tenantId; @@ -222,8 +169,7 @@ protected AbstractRequestResponseClient( /** * Creates a request-response client for a sender and receiver link. * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. + * @param connection The connection to the service. * @param tenantId The tenant that the client should be scoped to or {@code null} if the * client should not be scoped to a tenant. * @param sender The AMQP 1.0 link to use for sending requests to the peer. @@ -231,38 +177,12 @@ protected AbstractRequestResponseClient( * @throws NullPointerException if any of the parameters other than tenant are {@code null}. */ protected AbstractRequestResponseClient( - final Context context, - final ClientConfigProperties config, - final String tenantId, - final ProtonSender sender, - final ProtonReceiver receiver) { - - this(context, config, null, tenantId, sender, receiver); - } - - /** - * Creates a request-response client for a sender and receiver link. - * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing across process - * boundaries or {@code null} to disable tracing. - * @param tenantId The tenant that the client should be scoped to or {@code null} if the - * client should not be scoped to a tenant. - * @param sender The AMQP 1.0 link to use for sending requests to the peer. - * @param receiver The AMQP 1.0 link to use for receiving responses from the peer. - * @throws NullPointerException if any of the parameters other than tracer or tenant - * are {@code null}. - */ - protected AbstractRequestResponseClient( - final Context context, - final ClientConfigProperties config, - final Tracer tracer, + final HonoConnection connection, final String tenantId, final ProtonSender sender, final ProtonReceiver receiver) { - this(context, config, tracer, tenantId); + this(connection, tenantId); this.sender = Objects.requireNonNull(sender); this.receiver = Objects.requireNonNull(receiver); } @@ -292,8 +212,8 @@ public final void setResponseCache(final ExpiringValueCache cache) { * @return The timeout period in seconds. */ protected final long getResponseCacheDefaultTimeout() { - if (config instanceof RequestResponseClientConfigProperties) { - return ((RequestResponseClientConfigProperties) config).getResponseCacheDefaultTimeout(); + if (connection.getConfig() instanceof RequestResponseClientConfigProperties) { + return ((RequestResponseClientConfigProperties) connection.getConfig()).getResponseCacheDefaultTimeout(); } else { return RequestResponseClientConfigProperties.DEFAULT_RESPONSE_CACHE_TIMEOUT; } @@ -387,50 +307,46 @@ protected abstract R getResult( * Creates the sender and receiver links to the peer for sending requests * and receiving responses. * - * @param con The AMQP 1.0 connection to the peer. * @return A future indicating the outcome. The future will succeed if the links * have been created. * @throws NullPointerException if con is {@code null}. */ - protected final Future createLinks(final ProtonConnection con) { - return createLinks(con, null, null); + protected final Future createLinks() { + return createLinks(null, null); } /** * Creates the sender and receiver links to the peer for sending requests * and receiving responses. * - * @param con The AMQP 1.0 connection to the peer. * @param senderCloseHook A handler to invoke if the peer closes the sender link unexpectedly. * @param receiverCloseHook A handler to invoke if the peer closes the receiver link unexpectedly. * @return A future indicating the outcome. The future will succeed if the links * have been created. * @throws NullPointerException if connection is {@code null}. */ - protected final Future createLinks(final ProtonConnection con, final Handler senderCloseHook, + protected final Future createLinks(final Handler senderCloseHook, final Handler receiverCloseHook) { - Objects.requireNonNull(con); - - return createReceiver(con, replyToAddress, receiverCloseHook) + return createReceiver(replyToAddress, receiverCloseHook) .compose(recv -> { this.receiver = recv; - return createSender(con, targetAddress, senderCloseHook); + return createSender(targetAddress, senderCloseHook); }).compose(sender -> { - LOG.debug("request-response client for peer [{}] created", con.getRemoteContainer()); + LOG.debug("request-response client for peer [{}] created", connection.getConfig().getHost()); this.sender = sender; return Future.succeededFuture(); }); } - private Future createSender(final ProtonConnection con, final String targetAddress, final Handler closeHook) { + private Future createSender(final String targetAddress, final Handler closeHook) { - return AbstractHonoClient.createSender(context, config, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook); + return connection.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook); } - private Future createReceiver(final ProtonConnection con, final String sourceAddress, final Handler closeHook) { + private Future createReceiver(final String sourceAddress, final Handler closeHook) { - return AbstractHonoClient.createReceiver(context, config, con, sourceAddress, ProtonQoS.AT_LEAST_ONCE, this::handleResponse, closeHook); + return connection.createReceiver(sourceAddress, ProtonQoS.AT_LEAST_ONCE, this::handleResponse, closeHook); } /** @@ -790,7 +706,7 @@ protected final void sendRequest( currentSpan.setTag(MessageHelper.APP_PROPERTY_TENANT_ID, tenantId); } - executeOrRunOnContext(res -> { + connection.executeOrRunOnContext(res -> { if (sender.sendQueueFull()) { LOG.debug("cannot send request to peer, no credit left for link [target: {}]", targetAddress); Tags.HTTP_STATUS.set(currentSpan, HttpURLConnection.HTTP_UNAVAILABLE); @@ -807,7 +723,7 @@ protected final void sendRequest( details.put(TracingHelper.TAG_QOS.getKey(), sender.getQoS().toString()); currentSpan.log(details); final TriTuple>, Object, Span> handler = TriTuple.of(resultHandler, cacheKey, currentSpan); - TracingHelper.injectSpanContext(tracer, currentSpan.context(), request); + TracingHelper.injectSpanContext(connection.getTracer(), currentSpan.context(), request); replyMap.put(correlationId, handler); sender.send(request, deliveryUpdated -> { @@ -851,7 +767,7 @@ protected final void sendRequest( } }); if (requestTimeoutMillis > 0) { - context.owner().setTimer(requestTimeoutMillis, tid -> { + connection.getVertx().setTimer(requestTimeoutMillis, tid -> { cancelRequest(correlationId, Future.failedFuture(new ServerErrorException( HttpURLConnection.HTTP_UNAVAILABLE, "request timed out after " + requestTimeoutMillis + "ms"))); }); diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AbstractSender.java b/client/src/main/java/org/eclipse/hono/client/impl/AbstractSender.java index 1fcd730721..a0793ef992 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AbstractSender.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AbstractSender.java @@ -14,8 +14,6 @@ package org.eclipse.hono.client.impl; import java.net.HttpURLConnection; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -23,8 +21,6 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.qpid.proton.amqp.messaging.Accepted; import org.apache.qpid.proton.amqp.messaging.Modified; @@ -33,6 +29,7 @@ import org.apache.qpid.proton.amqp.transport.DeliveryState; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.ServiceInvocationException; @@ -44,14 +41,11 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.proton.ProtonDelivery; -import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonSender; /** @@ -64,8 +58,6 @@ public abstract class AbstractSender extends AbstractHonoClient implements Messa */ protected static final AtomicLong MESSAGE_COUNTER = new AtomicLong(); - private static final Pattern CHARSET_PATTERN = Pattern.compile("^.*;charset=(.*)$"); - /** * A logger to be shared with subclasses. */ @@ -85,24 +77,20 @@ public abstract class AbstractSender extends AbstractHonoClient implements Messa /** * Creates a new sender. * - * @param config The configuration properties to use. + * @param connection The connection to use for interacting with the server. * @param sender The sender link to send messages over. * @param tenantId The identifier of the tenant that the * devices belong to which have published the messages * that this sender is used to send downstream. * @param targetAddress The target address to send the messages to. - * @param context The vert.x context to use for sending the messages. - * @param tracer The tracer to use. */ protected AbstractSender( - final ClientConfigProperties config, + final HonoConnection connection, final ProtonSender sender, final String tenantId, - final String targetAddress, - final Context context, - final Tracer tracer) { + final String targetAddress) { - super(context, config, tracer); + super(connection); this.sender = Objects.requireNonNull(sender); this.tenantId = Objects.requireNonNull(tenantId); this.targetAddress = targetAddress; @@ -166,9 +154,9 @@ public final Future send(final Message rawMessage, final SpanCon Tags.MESSAGE_BUS_DESTINATION.set(span, targetAddress); span.setTag(MessageHelper.APP_PROPERTY_TENANT_ID, tenantId); span.setTag(MessageHelper.APP_PROPERTY_DEVICE_ID, MessageHelper.getDeviceId(rawMessage)); - TracingHelper.injectSpanContext(tracer, span.context(), rawMessage); + TracingHelper.injectSpanContext(connection.getTracer(), span.context(), rawMessage); - return executeOrRunOnContext(result -> { + return connection.executeOrRunOnContext(result -> { if (sender.sendQueueFull()) { final ServiceInvocationException e = new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no credit available"); logError(span, e); @@ -180,37 +168,6 @@ public final Future send(final Message rawMessage, final SpanCon }); } - @Override - public final Future send(final String deviceId, final byte[] payload, final String contentType) { - return send(deviceId, null, payload, contentType); - } - - @Override - public final Future send(final String deviceId, final String payload, final String contentType) { - return send(deviceId, null, payload, contentType); - } - - @Override - public final Future send(final String deviceId, final Map properties, final String payload, final String contentType) { - Objects.requireNonNull(payload); - final Charset charset = getCharsetForContentType(Objects.requireNonNull(contentType)); - return send(deviceId, properties, payload.getBytes(charset), contentType); - } - - @Override - public final Future send(final String deviceId, final Map properties, final byte[] payload, final String contentType) { - Objects.requireNonNull(deviceId); - Objects.requireNonNull(payload); - Objects.requireNonNull(contentType); - - final Message msg = ProtonHelper.message(); - msg.setAddress(getTo(deviceId)); - MessageHelper.setPayload(msg, contentType, payload); - setApplicationProperties(msg, properties); - addProperties(msg, deviceId); - return send(msg); - } - /** * Sends an AMQP 1.0 message to the peer this client is configured for. *

@@ -268,20 +225,6 @@ protected final Span startSpan(final Message message) { */ protected abstract String getTo(String deviceId); - private void addProperties(final Message msg, final String deviceId) { - MessageHelper.addDeviceId(msg, deviceId); - } - - private Charset getCharsetForContentType(final String contentType) { - - final Matcher m = CHARSET_PATTERN.matcher(contentType); - if (m.matches()) { - return Charset.forName(m.group(1)); - } else { - return StandardCharsets.UTF_8; - } - } - /** * Sends an AMQP 1.0 message to the peer this client is configured for * and waits for the outcome of the transfer. @@ -314,14 +257,14 @@ protected Future sendMessageAndWaitForOutcome(final Message mess details.put(TracingHelper.TAG_QOS.getKey(), sender.getQoS().toString()); currentSpan.log(details); - final Long timerId = config.getSendMessageTimeout() > 0 - ? context.owner().setTimer(config.getSendMessageTimeout(), id -> { + final Long timerId = connection.getConfig().getSendMessageTimeout() > 0 + ? connection.getVertx().setTimer(connection.getConfig().getSendMessageTimeout(), id -> { if (!result.isComplete()) { final ServerErrorException exception = new ServerErrorException( HttpURLConnection.HTTP_UNAVAILABLE, - "waiting for delivery update timed out after " + config.getSendMessageTimeout() + "ms"); + "waiting for delivery update timed out after " + connection.getConfig().getSendMessageTimeout() + "ms"); LOG.debug("waiting for delivery update timed out for message [message ID: {}] after {}ms", - messageId, config.getSendMessageTimeout()); + messageId, connection.getConfig().getSendMessageTimeout()); result.fail(exception); } }) @@ -329,7 +272,7 @@ protected Future sendMessageAndWaitForOutcome(final Message mess sender.send(message, deliveryUpdated -> { if (timerId != null) { - context.owner().cancelTimer(timerId); + connection.getVertx().cancelTimer(timerId); } final DeliveryState remoteState = deliveryUpdated.getRemoteState(); if (result.isComplete()) { diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandClientImpl.java index fb0a2f7a50..ba8e8cf879 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandClientImpl.java @@ -18,19 +18,16 @@ import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.AsyncCommandClient; -import org.eclipse.hono.config.ClientConfigProperties; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.util.CommandConstants; import org.eclipse.hono.util.MessageHelper; import io.opentracing.Span; import io.opentracing.SpanContext; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonQoS; @@ -42,12 +39,11 @@ public class AsyncCommandClientImpl extends AbstractSender implements AsyncCommandClient { private AsyncCommandClientImpl( - final ClientConfigProperties config, + final HonoConnection con, final ProtonSender sender, final String tenantId, - final String targetAddress, - final Context context) { - super(config, sender, tenantId, targetAddress, context, null); + final String targetAddress) { + super(con, sender, tenantId, targetAddress); } @Override @@ -57,7 +53,7 @@ protected Future sendMessage(final Message message, final Span c @Override protected Span startSpan(final SpanContext parent, final Message message) { - if (tracer == null) { + if (connection.getTracer() == null) { throw new IllegalStateException("no tracer configured"); } else { final Span span = newFollowingSpan(parent, "sending async command"); @@ -110,41 +106,30 @@ public Future sendAsyncCommand(final String command, final String contentT } /** - * Creates a new async command client for a tenant and device. + * Creates a new asynchronous command client for a tenant and device. *

* The instance created is scoped to the given device. In particular, the sender link's target address is set to * control/${tenantId}/${deviceId}. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to the Hono server. * @param tenantId The tenant that the device belongs to. * @param deviceId The device to create the client for. * @param closeHook A handler to invoke if the peer closes the sender link unexpectedly. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @throws NullPointerException if any of context, clientConfig, con, tenantId, deviceId or creationHandler are - * {@code null}. + * @return A future indicating the outcome. + * @throws NullPointerException if any of connection, tenantId or deviceId are {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, final String deviceId, - final Handler closeHook, - final Handler> creationHandler) { + final Handler closeHook) { - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); Objects.requireNonNull(deviceId); - Objects.requireNonNull(creationHandler); final String targetAddress = AsyncCommandClientImpl.getTargetAddress(tenantId, deviceId); - createSender(context, clientConfig, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook) - .compose(sender -> Future. succeededFuture( - new AsyncCommandClientImpl(clientConfig, sender, tenantId, targetAddress, context))) - .setHandler(creationHandler); + return con.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook) + .compose(sender -> Future.succeededFuture(new AsyncCommandClientImpl(con, sender, tenantId, targetAddress))); } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandResponseConsumerImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandResponseConsumerImpl.java index 619a90257a..60c6f3e362 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandResponseConsumerImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/AsyncCommandResponseConsumerImpl.java @@ -17,16 +17,12 @@ import java.util.function.BiConsumer; import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.CommandConstants; -import org.eclipse.hono.util.Constants; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonReceiver; @@ -37,84 +33,39 @@ public class AsyncCommandResponseConsumerImpl extends AbstractConsumer implements MessageConsumer { private static final String ASYNC_COMMAND_RESPONSE_ADDRESS_TEMPLATE = CommandConstants.COMMAND_ENDPOINT - + "%s%s%s%s"; + + "/%s/%s"; - private AsyncCommandResponseConsumerImpl(final Context context, final ClientConfigProperties config, + private AsyncCommandResponseConsumerImpl( + final HonoConnection con, final ProtonReceiver receiver) { - super(context, config, receiver); + super(con, receiver); } /** - * Creates a new async command response consumer for a tenant for commands with the given {@code replyId}. + * Creates a new asynchronous command response consumer for a tenant for commands with the given {@code replyId}. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to the Hono server. * @param tenantId The tenant to consume async command responses for. * @param replyId The {@code replyId} of commands to consume async responses for. * @param asyncCommandResponseConsumer The consumer to invoke with async command response received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @throws NullPointerException if any of the parameters is {@code null}. + * @return A future indicating the outcome. + * @throws NullPointerException if any of the parameters except close hook are {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, final String replyId, final BiConsumer asyncCommandResponseConsumer, - final Handler> creationHandler, final Handler closeHook) { - create(context, clientConfig, con, tenantId, replyId, Constants.DEFAULT_PATH_SEPARATOR, asyncCommandResponseConsumer, - creationHandler, closeHook); - } - - /** - * Creates a new async command response consumer for a tenant for commands with the given {@code replyId}. - * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. - * @param tenantId The tenant to consume async command responses for. - * @param replyId The {@code replyId} of commands to consume async responses for. - * @param pathSeparator The address path separator character used by the server. - * @param asyncCommandResponseConsumer The consumer to invoke with async command response received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @throws NullPointerException if any of the parameters is {@code null}. - */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, - final String tenantId, - final String replyId, - final String pathSeparator, - final BiConsumer asyncCommandResponseConsumer, - final Handler> creationHandler, - final Handler closeHook) { - - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); Objects.requireNonNull(replyId); - Objects.requireNonNull(pathSeparator); Objects.requireNonNull(asyncCommandResponseConsumer); - Objects.requireNonNull(creationHandler); - createReceiver(context, clientConfig, con, String.format(ASYNC_COMMAND_RESPONSE_ADDRESS_TEMPLATE, pathSeparator, - tenantId, pathSeparator, replyId), - ProtonQoS.AT_LEAST_ONCE, asyncCommandResponseConsumer::accept, closeHook).setHandler(created -> { - if (created.succeeded()) { - creationHandler.handle(Future.succeededFuture( - new AsyncCommandResponseConsumerImpl(context, clientConfig, created.result()))); - } else { - creationHandler.handle(Future.failedFuture(created.cause())); - } - }); + final String sourceAddress = String.format(ASYNC_COMMAND_RESPONSE_ADDRESS_TEMPLATE, tenantId, replyId); + return con.createReceiver(sourceAddress, ProtonQoS.AT_LEAST_ONCE, asyncCommandResponseConsumer::accept, closeHook) + .compose(recv -> Future.succeededFuture((MessageConsumer) new AsyncCommandResponseConsumerImpl(con, recv))); } - } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/CachingClientFactory.java b/client/src/main/java/org/eclipse/hono/client/impl/CachingClientFactory.java new file mode 100644 index 0000000000..c4e3712128 --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/impl/CachingClientFactory.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.client.ServiceInvocationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; + +/** + * A factory for creating clients. + *

+ * The getOrCreateClient method makes sure that the creation attempt + * fails if the clearState method is being invoked. + *

+ * Created clients are being cached. + * + * @param The type of client to be created. + */ +class CachingClientFactory extends ClientFactory { + + private static final Logger log = LoggerFactory.getLogger(CachingClientFactory.class); + + private final Predicate livenessCheck; + /** + * The clients that can be used to send messages. + * The target address is used as the key, e.g. telemetry/DEFAULT_TENANT. + */ + private final Map activeClients = new HashMap<>(); + /** + * The locks for guarding the creation of new instances. + */ + private final Map creationLocks = new HashMap<>(); + + /** + * @param contextSupplier A supplier of the vert.x context to run on. + * @param livenessCheck A predicate for checking if a cached client is usable. + */ + CachingClientFactory(final Predicate livenessCheck) { + this.livenessCheck = Objects.requireNonNull(livenessCheck); + } + + /** + * Removes a client from the cache. + * + * @param key The key of the client to remove. + */ + public void removeClient(final String key) { + activeClients.remove(key); + } + + /** + * Removes a client from the cache. + * + * @param key The key of the client to remove. + * @param postProcessor A handler to invoke with the removed client. + */ + public void removeClient(final String key, final Handler postProcessor) { + final T client = activeClients.remove(key); + if (client != null) { + postProcessor.handle(client); + } + } + + /** + * Clears the cache. + */ + @Override + protected void doClearState() { + activeClients.clear(); + creationLocks.clear(); + } + + public boolean isEmpty() { + return activeClients.isEmpty() && creationLocks.isEmpty() && creationRequests.isEmpty(); + } + + /** + * Gets an existing client. + * + * @param key The key to look up. + * @return The client or {@code null} if the cache does + * not contain the key. + */ + public T getClient(final String key) { + return activeClients.get(key); + } + + /** + * Gets an existing or creates a new client. + *

+ * This method first tries to look up an already existing + * client using the given key. If no client exists yet, a new + * instance is created using the given factory and put to the cache. + * + * @param key The key to cache the client under. + * @param clientInstanceSupplier The factory to use for creating a + * new client (if necessary). + * @param result The handler to invoke with the outcome of the creation attempt. + * The handler will be invoked with a succeeded future containing + * the client or with a failed future containing a + * {@link ServiceInvocationException} if no client could be + * created using the factory. + */ + public void getOrCreateClient( + final String key, + final Supplier> clientInstanceSupplier, + final Handler> result) { + + final T sender = activeClients.get(key); + + if (sender != null && livenessCheck.test(sender)) { + log.debug("reusing cached client [{}]", key); + result.handle(Future.succeededFuture(sender)); + } else if (!creationLocks.computeIfAbsent(key, k -> Boolean.FALSE)) { + // register a handler to be notified if the underlying connection to the server fails + // so that we can fail the result handler passed in + final Handler connectionFailureHandler = connectionLost -> { + // remove lock so that next attempt to open a sender doesn't fail + creationLocks.remove(key); + result.handle(Future.failedFuture( + new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no connection to service"))); + }; + creationRequests.add(connectionFailureHandler); + creationLocks.put(key, Boolean.TRUE); + log.debug("creating new client for [{}]", key); + + clientInstanceSupplier.get().setHandler(creationAttempt -> { + creationLocks.remove(key); + creationRequests.remove(connectionFailureHandler); + if (creationAttempt.succeeded()) { + final T newClient = creationAttempt.result(); + log.debug("successfully created new client for [{}]", key); + activeClients.put(key, newClient); + result.handle(Future.succeededFuture(newClient)); + } else { + log.debug("failed to create new client for [{}]", key, creationAttempt.cause()); + activeClients.remove(key); + result.handle(Future.failedFuture(creationAttempt.cause())); + } + }); + + } else { + log.debug("already trying to create a client for [{}]", key); + result.handle(Future.failedFuture(new ServerErrorException( + HttpURLConnection.HTTP_UNAVAILABLE, "already creating client for key"))); + } + } +} diff --git a/client/src/main/java/org/eclipse/hono/client/impl/ClientFactory.java b/client/src/main/java/org/eclipse/hono/client/impl/ClientFactory.java new file mode 100644 index 0000000000..7fdb7a9ff2 --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/impl/ClientFactory.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +import org.eclipse.hono.client.ServerErrorException; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; + +/** + * A factory for creating clients. + *

+ * The createClient method makes sure that the creation attempt + * fails if the clearState method is being invoked. + * + * @param The type of client to be created. + */ +class ClientFactory { + + /** + * The current requests for creating an instance. + */ + protected final List> creationRequests = new ArrayList<>(); + + /** + * Clears all state. + *

+ * Simply invokes clearState(Void). + */ + public final void clearState() { + clearState(null); + } + + /** + * Clears all state. + *

+ * All pending creation requests are failed. + * + * @param v Dummy parameter required so that this + * method can be used as a {@code Handler}. + */ + public final void clearState(final Void v) { + failAllCreationRequests(); + doClearState(); + } + + private void failAllCreationRequests() { + + for (final Iterator> iter = creationRequests.iterator(); iter.hasNext();) { + iter.next().handle(null); + iter.remove(); + } + } + + protected void doClearState() { + // do nothing + } + + /** + * Creates a new client instance. + * + * @param clientSupplier The factory to use for creating a new instance. + * @param result The handler to invoke with the outcome of the creation attempt. + */ + public final void createClient( + final Supplier> clientSupplier, + final Handler> result) { + + // register a handler to be notified if the underlying connection to the server fails + // so that we can fail the result handler + final Handler connectionFailureHandler = connectionLost -> { + result.handle(Future.failedFuture( + new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "connection to server lost"))); + }; + creationRequests.add(connectionFailureHandler); + + clientSupplier.get().setHandler(attempt -> { + creationRequests.remove(connectionFailureHandler); + result.handle(attempt); + }); + } +} diff --git a/client/src/main/java/org/eclipse/hono/client/impl/CommandClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/CommandClientImpl.java index 604729e9e5..bb1a800fa6 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/CommandClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/CommandClientImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -17,30 +17,27 @@ import java.util.Map; import java.util.Objects; -import io.opentracing.Span; -import io.opentracing.tag.Tags; -import io.vertx.proton.ProtonHelper; - import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.CommandClient; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.StatusCodeMapper; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.tracing.TracingHelper; +import org.eclipse.hono.util.BufferResult; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.CommandConstants; -import org.eclipse.hono.util.BufferResult; import org.eclipse.hono.util.MessageHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; +import io.opentracing.Span; +import io.opentracing.tag.Tags; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.proton.ProtonConnection; +import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonReceiver; import io.vertx.proton.ProtonSender; @@ -55,33 +52,30 @@ public class CommandClientImpl extends AbstractRequestResponseClient * The client will be ready to use after invoking {@link #createLinks(ProtonConnection)} or * {@link #createLinks(ProtonConnection, Handler, Handler)} only. * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. + * @param connection The connection to Hono. * @param tenantId The tenant that the device belongs to. * @param deviceId The device to create the client for. * @param replyId The replyId to use in the reply-to address. * @throws NullPointerException if any of the parameters are {@code null}. */ CommandClientImpl( - final Context context, - final ClientConfigProperties config, + final HonoConnection connection, final String tenantId, final String deviceId, final String replyId) { - super(context, config, tenantId, deviceId, replyId); + super(connection, tenantId, deviceId, replyId); } /** - * Creates a request-response client. + * Creates a client for sending commands to devices. * - * @param context The vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. + * @param connection The connection to Hono. * @param tenantId The tenant that the device belongs to. * @param deviceId The device to create the client for. * @param replyId The replyId to use in the reply-to address. @@ -90,15 +84,14 @@ public class CommandClientImpl extends AbstractRequestResponseClient sendOneWayCommand(final String command, final String content * This address is also used as the value of the reply-to * property of all command request messages sent by this client. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to Hono. * @param tenantId The tenant that the device belongs to. * @param deviceId The device to create the client for. * @param replyId The replyId to use in the reply-to address. * @param senderCloseHook A handler to invoke if the peer closes the sender link unexpectedly. * @param receiverCloseHook A handler to invoke if the peer closes the receiver link unexpectedly. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. + * @return A future indicating the outcome. * @throws NullPointerException if any of the parameters are {@code null}. */ - public static final void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static final Future create( + final HonoConnection con, final String tenantId, final String deviceId, final String replyId, final Handler senderCloseHook, - final Handler receiverCloseHook, - final Handler> creationHandler) { - - final CommandClientImpl client = new CommandClientImpl(context, clientConfig, tenantId, deviceId, replyId); - client.createLinks(con, senderCloseHook, receiverCloseHook).setHandler(s -> { - if (s.succeeded()) { - LOG.debug("successfully created command client for [{}]", tenantId); - creationHandler.handle(Future.succeededFuture(client)); - } else { - LOG.debug("failed to create command client for [{}]", tenantId, s.cause()); - creationHandler.handle(Future.failedFuture(s.cause())); - } - }); + final Handler receiverCloseHook) { + + final CommandClientImpl client = new CommandClientImpl(con, tenantId, deviceId, replyId); + return client.createLinks(senderCloseHook, receiverCloseHook) + .map(ok -> { + LOG.debug("successfully created command client for [{}]", tenantId); + return (CommandClient) client; + }).recover(t -> { + LOG.debug("failed to create command client for [{}]", tenantId, t); + return Future.failedFuture(t); + }); } } diff --git a/client/src/main/java/org/eclipse/hono/client/CommandConsumer.java b/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumer.java similarity index 66% rename from client/src/main/java/org/eclipse/hono/client/CommandConsumer.java rename to client/src/main/java/org/eclipse/hono/client/impl/CommandConsumer.java index 6cffdb85ba..9baa76f239 100644 --- a/client/src/main/java/org/eclipse/hono/client/CommandConsumer.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumer.java @@ -10,15 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ -package org.eclipse.hono.client; +package org.eclipse.hono.client.impl; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -import org.eclipse.hono.client.impl.AbstractConsumer; -import org.eclipse.hono.config.ClientConfigProperties; +import org.eclipse.hono.client.Command; +import org.eclipse.hono.client.CommandContext; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.CommandConstants; import org.eclipse.hono.util.MessageHelper; @@ -32,11 +33,8 @@ import io.opentracing.Tracer; import io.opentracing.log.Fields; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonReceiver; @@ -47,13 +45,8 @@ public class CommandConsumer extends AbstractConsumer { private static final Logger LOG = LoggerFactory.getLogger(CommandConsumer.class); - private CommandConsumer( - final Context context, - final ClientConfigProperties config, - final ProtonReceiver protonReceiver, - final Tracer tracer) { - - super(context, config, protonReceiver, tracer); + private CommandConsumer(final HonoConnection connection, final ProtonReceiver receiver) { + super(connection, receiver); } /** @@ -66,9 +59,7 @@ private CommandConsumer( * However, the sender will be issued one credit on link establishment. * * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param tenantId The tenant to consume commands from. * @param deviceId The device for which the commands should be consumed. * @param commandHandler The handler to invoke for each command received. @@ -80,51 +71,36 @@ private CommandConsumer( * @param remoteCloseHandler A handler to be invoked after the link has been closed * at the remote peer's request. The handler will be invoked with the * link's source address. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @param tracer The tracer to use for tracking the processing of received - * messages. If {@code null}, OpenTracing's {@code NoopTracer} will - * be used. - * @throws NullPointerException if any of the parameters other than tracer are {@code null}. + * @return A future indicating the outcome of the creation attempt. + * @throws NullPointerException if any of the parameters are {@code null}. */ - public static final void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static final Future create( + final HonoConnection con, final String tenantId, final String deviceId, final Handler commandHandler, final Handler localCloseHandler, - final Handler remoteCloseHandler, - final Handler> creationHandler, - final Tracer tracer) { + final Handler remoteCloseHandler) { - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); Objects.requireNonNull(deviceId); Objects.requireNonNull(commandHandler); Objects.requireNonNull(remoteCloseHandler); - Objects.requireNonNull(creationHandler); LOG.trace("creating new command consumer [tenant-id: {}, device-id: {}]", tenantId, deviceId); final String address = ResourceIdentifier.from(CommandConstants.COMMAND_ENDPOINT, tenantId, deviceId).toString(); - final ClientConfigProperties props = new ClientConfigProperties(clientConfig); - props.setInitialCredits(0); final AtomicReference receiverRef = new AtomicReference<>(); - createReceiver( - context, - props, - con, + return con.createReceiver( address, ProtonQoS.AT_LEAST_ONCE, (delivery, msg) -> { final Command command = Command.from(msg, tenantId, deviceId); - + final Tracer tracer = con.getTracer(); // try to extract Span context from incoming message final SpanContext spanContext = TracingHelper.extractSpanContext(tracer, msg); // start a Span to use for tracing the delivery of the command to the device @@ -150,28 +126,26 @@ public static final void create( currentSpan.log(items); commandHandler.handle(CommandContext.from(command, delivery, receiverRef.get(), currentSpan)); }, + 0, // no pre-fetching sourceAddress -> { LOG.debug("command receiver link [tenant-id: {}, device-id: {}] closed remotely", tenantId, deviceId); remoteCloseHandler.handle(sourceAddress); - }).setHandler(s -> { - - if (s.succeeded()) { - final ProtonReceiver receiver = s.result(); - LOG.debug("successfully created command consumer [{}]", address); - receiverRef.set(receiver); - receiver.flow(1); // allow sender to send one command - final CommandConsumer consumer = new CommandConsumer(context, props, receiver, tracer); - consumer.setLocalCloseHandler(sourceAddress -> { - LOG.debug("command receiver link [tenant-id: {}, device-id: {}] closed locally", - tenantId, deviceId); - localCloseHandler.handle(sourceAddress); - }); - creationHandler.handle(Future.succeededFuture(consumer)); - } else { - LOG.debug("failed to create command consumer [tenant-id: {}, device-id: {}]", tenantId, deviceId, s.cause()); - creationHandler.handle(Future.failedFuture(s.cause())); - } + }).map(receiver -> { + LOG.debug("successfully created command consumer [{}]", address); + receiverRef.set(receiver); + receiver.flow(1); // allow sender to send one command + final CommandConsumer consumer = new CommandConsumer(con, receiver); + consumer.setLocalCloseHandler(sourceAddress -> { + LOG.debug("command receiver link [tenant-id: {}, device-id: {}] closed locally", + tenantId, deviceId); + localCloseHandler.handle(sourceAddress); + }); + return consumer; + }).recover(t -> { + LOG.debug("failed to create command consumer [tenant-id: {}, device-id: {}]", + tenantId, deviceId, t); + return Future.failedFuture(t); }); } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumerFactoryImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumerFactoryImpl.java index 46526c13f7..ca52834aff 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumerFactoryImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/CommandConsumerFactoryImpl.java @@ -20,7 +20,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.hono.auth.Device; -import org.eclipse.hono.client.CommandConsumer; import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponseSender; @@ -208,12 +207,9 @@ private Future newCommandConsumer( final Handler remoteCloseHandler) { return checkConnected().compose(con -> { - final Future result = Future.future(); final String key = Device.asAddress(tenantId, deviceId); - CommandConsumer.create( - context, - clientConfigProperties, - connection, + return CommandConsumer.create( + this, tenantId, deviceId, commandConsumer, @@ -225,10 +221,8 @@ private Future newCommandConsumer( sourceAddress -> { // remote close hook commandConsumers.remove(key); remoteCloseHandler.handle(null); - }, - result, - getTracer()); - return result; + }) + .map(c -> (MessageConsumer) c); }); } @@ -267,16 +261,9 @@ public Future getCommandResponseSender( Objects.requireNonNull(replyId); return executeOrRunOnContext(result -> { - checkConnected().setHandler(check -> { - if (check.succeeded()) { - CommandResponseSenderImpl.create(context, clientConfigProperties, connection, tenantId, replyId, - onSenderClosed -> {}, - result, - getTracer()); - } else { - result.fail(check.cause()); - } - }); + checkConnected() + .compose(ok -> CommandResponseSenderImpl.create(this, tenantId, replyId, onSenderClosed -> {})) + .setHandler(result); }); } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/CommandResponseSenderImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/CommandResponseSenderImpl.java index 4a502056a3..64d4be64e2 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/CommandResponseSenderImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/CommandResponseSenderImpl.java @@ -12,33 +12,27 @@ *******************************************************************************/ package org.eclipse.hono.client.impl; -import java.util.Map; import java.util.Objects; -import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.CommandResponseSender; import org.eclipse.hono.client.CommandResponse; +import org.eclipse.hono.client.CommandResponseSender; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.CommandConstants; -import org.eclipse.hono.util.MessageHelper; + import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; -import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonSender; /** - * The response sender for a received command. + * A wrapper around an AMQP link for sending response messages to + * commands downstream. */ public class CommandResponseSenderImpl extends AbstractSender implements CommandResponseSender { @@ -49,14 +43,12 @@ public class CommandResponseSenderImpl extends AbstractSender implements Command public static final long DEFAULT_COMMAND_FLOW_LATENCY = 200L; // ms CommandResponseSenderImpl( - final ClientConfigProperties config, + final HonoConnection connection, final ProtonSender sender, final String tenantId, - final String targetAddress, - final Context context, - final Tracer tracer) { + final String targetAddress) { - super(config, sender, tenantId, targetAddress, context, tracer); + super(connection, sender, tenantId, targetAddress); } @Override @@ -88,25 +80,6 @@ static final String getTargetAddress(final String tenantId, final String replyId return String.format("%s/%s/%s", CommandConstants.COMMAND_ENDPOINT, tenantId, replyId); } - /** - * {@inheritDoc} - */ - @Deprecated - @Override - public Future sendCommandResponse( - final String correlationId, - final String contentType, - final Buffer payload, - final Map properties, - final int status, - final SpanContext context) { - - LOG.trace("sending command response [correlationId: {}, status: {}]", correlationId, status); - return sendAndWaitForOutcome( - createResponseMessage(targetAddress, correlationId, contentType, payload, properties, status), - context); - } - /** * {@inheritDoc} */ @@ -121,28 +94,6 @@ public Future sendCommandResponse(final CommandResponse commandR return sendAndWaitForOutcome(message, context); } - private static Message createResponseMessage( - final String targetAddress, - final String correlationId, - final String contentType, - final Buffer payload, - final Map properties, - final int status) { - - Objects.requireNonNull(targetAddress); - Objects.requireNonNull(correlationId); - final Message msg = ProtonHelper.message(); - msg.setCorrelationId(correlationId); - msg.setAddress(targetAddress); - MessageHelper.setPayload(msg, contentType, payload); - if (properties != null) { - msg.setApplicationProperties(new ApplicationProperties(properties)); - } - MessageHelper.setCreationTime(msg); - MessageHelper.addProperty(msg, MessageHelper.APP_PROPERTY_STATUS, status); - return msg; - } - /** * Creates a new sender to send responses for commands back to the business application. *

@@ -152,55 +103,39 @@ private static Message createResponseMessage( * smaller than the default * * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param con The connection to the AMQP network. * @param tenantId The tenant that the command response will be send for and the device belongs to. * @param replyId The reply id as the unique postfix of the replyTo address. * @param closeHook A handler to invoke if the peer closes the link unexpectedly. - * @param creationHandler The handler to invoke with the result of the creation attempt. - * @param tracer The tracer to use for tracking the processing of received messages. If {@code null}, OpenTracing's - * {@code NoopTracer} will be used. - * @throws NullPointerException if any of context, clientConfig, con, tenantId, deviceId or replyId are - * {@code null}. + * @return A future indicating the result of the creation attempt. + * @throws NullPointerException if any of con, tenantId or replyId are {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, final String replyId, - final Handler closeHook, - final Handler> creationHandler, - final Tracer tracer) { + final Handler closeHook) { - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); Objects.requireNonNull(replyId); final String targetAddress = CommandResponseSenderImpl.getTargetAddress(tenantId, replyId); - final ClientConfigProperties props = new ClientConfigProperties(clientConfig); + final ClientConfigProperties props = new ClientConfigProperties(con.getConfig()); if (props.getFlowLatency() < DEFAULT_COMMAND_FLOW_LATENCY) { props.setFlowLatency(DEFAULT_COMMAND_FLOW_LATENCY); } - createSender(context, props, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook) - .map(sender -> (CommandResponseSender) new CommandResponseSenderImpl(clientConfig, sender, tenantId, - targetAddress, context, tracer)) - .setHandler(creationHandler); + return con.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook) + .map(sender -> (CommandResponseSender) new CommandResponseSenderImpl(con, sender, tenantId, + targetAddress)); } @Override protected Span startSpan(final SpanContext parent, final Message rawMessage) { - if (tracer == null) { - throw new IllegalStateException("no tracer configured"); - } else { - final Span span = newChildSpan(parent, "forward Command response"); - Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT); - return span; - } + final Span span = newChildSpan(parent, "forward Command response"); + Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT); + return span; } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/CredentialsClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/CredentialsClientImpl.java index 122d8baf3b..620d62a09f 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/CredentialsClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/CredentialsClientImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -21,8 +21,8 @@ import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.CredentialsClient; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.StatusCodeMapper; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.CredentialsConstants; import org.eclipse.hono.util.CredentialsObject; @@ -36,15 +36,11 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; -import io.vertx.proton.ProtonConnection; /** * A Vertx-Proton based client for Hono's Credentials API. @@ -60,20 +56,15 @@ public class CredentialsClientImpl extends AbstractRequestResponseClient + * The client will be ready to use after invoking {@link #createLinks()} or + * {@link #createLinks(Handler, Handler)} only. * - * @param context The vert.x context to use for interacting with the service. - * @param config The configuration properties. + * @param connection The connection to Hono. * @param tenantId The identifier of the tenant for which the client should be created. - * @param tracer The OpenTracing tracer to use for tracking the processing of - * requests across process boundaries or {@code null} to disable tracing. */ - protected CredentialsClientImpl( - final Context context, - final ClientConfigProperties config, - final String tenantId, - final Tracer tracer) { - - super(context, config, tracer, tenantId); + CredentialsClientImpl(final HonoConnection connection, final String tenantId) { + super(connection, tenantId); } @Override @@ -126,37 +117,29 @@ public static final String getTargetAddress(final String tenantId) { /** * Creates a new credentials client for a tenant. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param tracer The tracer instance. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param tenantId The tenant for which credentials are handled. * @param senderCloseHook A handler to invoke if the peer closes the sender link unexpectedly. * @param receiverCloseHook A handler to invoke if the peer closes the receiver link unexpectedly. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. + * @return A future indicating the outcome of the creation attempt. * @throws NullPointerException if any of the parameters is {@code null}. */ - public static final void create( - final Context context, - final ClientConfigProperties clientConfig, - final Tracer tracer, - final ProtonConnection con, + public static final Future create( + final HonoConnection con, final String tenantId, final Handler senderCloseHook, - final Handler receiverCloseHook, - final Handler> creationHandler) { + final Handler receiverCloseHook) { LOG.debug("creating new credentials client for [{}]", tenantId); - final CredentialsClientImpl client = new CredentialsClientImpl(context, clientConfig, tenantId, tracer); - client.createLinks(con, senderCloseHook, receiverCloseHook).setHandler(s -> { - if (s.succeeded()) { - LOG.debug("successfully created credentials client for [{}]", tenantId); - creationHandler.handle(Future.succeededFuture(client)); - } else { - LOG.debug("failed to create credentials client for [{}]", tenantId, s.cause()); - creationHandler.handle(Future.failedFuture(s.cause())); - } - }); + final CredentialsClientImpl client = new CredentialsClientImpl(con, tenantId); + return client.createLinks(senderCloseHook, receiverCloseHook) + .map(ok -> { + LOG.debug("successfully created credentials client for [{}]", tenantId); + return (CredentialsClient) client; + }).recover(t -> { + LOG.debug("failed to create credentials client for [{}]", tenantId, t); + return Future.failedFuture(t); + }); } /** diff --git a/client/src/main/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImpl.java new file mode 100644 index 0000000000..cbe0ad783d --- /dev/null +++ b/client/src/main/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImpl.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import java.util.Objects; + +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.DownstreamSenderFactory; +import org.eclipse.hono.client.HonoConnection; + +import io.vertx.core.Future; + + +/** + * A factory for creating downstream senders. + * + */ +public class DownstreamSenderFactoryImpl extends AbstractHonoClientFactory implements DownstreamSenderFactory { + + private final CachingClientFactory clientFactory; + + /** + * @param connection The connection to use. + */ + public DownstreamSenderFactoryImpl(final HonoConnection connection) { + super(connection); + clientFactory = new CachingClientFactory<>(s -> s.isOpen()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onDisconnect() { + clientFactory.clearState(); + } + + /** + * {@inheritDoc} + */ + @Override + public final Future getOrCreateTelemetrySender(final String tenantId) { + + Objects.requireNonNull(tenantId); + return connection.executeOrRunOnContext(result -> { + clientFactory.getOrCreateClient( + TelemetrySenderImpl.getTargetAddress(tenantId, null), + () -> TelemetrySenderImpl.create(connection, tenantId, + onSenderClosed -> { + clientFactory.removeClient(TelemetrySenderImpl.getTargetAddress(tenantId, null)); + }), + result); + }); + } + + /** + * {@inheritDoc} + */ + @Override + public final Future getOrCreateEventSender(final String tenantId) { + + Objects.requireNonNull(tenantId); + return connection.executeOrRunOnContext(result -> { + clientFactory.getOrCreateClient( + EventSenderImpl.getTargetAddress(tenantId, null), + () -> EventSenderImpl.create(connection, tenantId, + onSenderClosed -> { + clientFactory.removeClient(EventSenderImpl.getTargetAddress(tenantId, null)); + }), + result); + }); + } +} diff --git a/client/src/main/java/org/eclipse/hono/client/impl/EventConsumerImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/EventConsumerImpl.java index 80156ee757..b0f3554ee7 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/EventConsumerImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/EventConsumerImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,98 +13,56 @@ package org.eclipse.hono.client.impl; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; +import java.util.Objects; +import java.util.function.BiConsumer; + +import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.client.MessageConsumer; +import org.eclipse.hono.util.EventConstants; + import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonReceiver; -import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.config.ClientConfigProperties; -import org.eclipse.hono.util.Constants; -import org.eclipse.hono.util.EventConstants; - -import java.util.Objects; -import java.util.function.BiConsumer; /** * A Vertx-Proton based client for consuming event messages from a Hono server. */ public class EventConsumerImpl extends AbstractConsumer implements MessageConsumer { - private static final String EVENT_ADDRESS_TEMPLATE = EventConstants.EVENT_ENDPOINT + "%s%s"; - - private EventConsumerImpl(final Context context, final ClientConfigProperties config, final ProtonReceiver receiver) { - super(context, config, receiver); - } - - /** - * Creates a new event consumer for a tenant. - * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. - * @param tenantId The tenant to consumer events for. - * @param eventConsumer The consumer to invoke with each event received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @throws NullPointerException if any of the parameters except the closeHook is {@code null}. - */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, - final String tenantId, - final BiConsumer eventConsumer, - final Handler> creationHandler, - final Handler closeHook) { - - create(context, clientConfig, con, tenantId, Constants.DEFAULT_PATH_SEPARATOR, eventConsumer, creationHandler, closeHook); + private EventConsumerImpl(final HonoConnection connection, final ProtonReceiver receiver) { + super(connection, receiver); } /** * Creates a new event consumer for a tenant. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param tenantId The tenant to consumer events for. - * @param pathSeparator The address path separator character used by the server. * @param eventConsumer The consumer to invoke with each event received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @throws NullPointerException if any of the parameters except the closeHook is {@code null}. + * @return A future indicating the outcome. + * @throws NullPointerException if any of the parameters except the closeHook are {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, - final String pathSeparator, final BiConsumer eventConsumer, - final Handler> creationHandler, final Handler closeHook) { - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); - Objects.requireNonNull(pathSeparator); Objects.requireNonNull(eventConsumer); - Objects.requireNonNull(creationHandler); - createReceiver(context, clientConfig, con, String.format(EVENT_ADDRESS_TEMPLATE, pathSeparator, tenantId), - ProtonQoS.AT_LEAST_ONCE, eventConsumer::accept, closeHook).setHandler(created -> { - if (created.succeeded()) { - creationHandler.handle(Future.succeededFuture( - new EventConsumerImpl(context, clientConfig, created.result()))); - } else { - creationHandler.handle(Future.failedFuture(created.cause())); - } - }); + final String sourceAddress = String.format("%s/%s", EventConstants.EVENT_ENDPOINT, tenantId); + return con.createReceiver( + sourceAddress, + ProtonQoS.AT_LEAST_ONCE, + eventConsumer::accept, + closeHook) + .compose(receiver -> Future.succeededFuture(new EventConsumerImpl(con, receiver))); } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/EventSenderImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/EventSenderImpl.java index c43335d78b..b7bb56c695 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/EventSenderImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/EventSenderImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -16,20 +16,17 @@ import java.util.Objects; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.EventConstants; import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonSender; @@ -37,16 +34,15 @@ /** * A Vertx-Proton based client for publishing event messages to a Hono server. */ -public final class EventSenderImpl extends AbstractSender { +public final class EventSenderImpl extends AbstractDownstreamSender { - EventSenderImpl(final ClientConfigProperties config, final ProtonSender sender, final String tenantId, - final String targetAddress, final Context context) { - this(config, sender, tenantId, targetAddress, context, null); - } + EventSenderImpl( + final HonoConnection con, + final ProtonSender sender, + final String tenantId, + final String targetAddress) { - EventSenderImpl(final ClientConfigProperties config, final ProtonSender sender, final String tenantId, - final String targetAddress, final Context context, final Tracer tracer) { - super(config, sender, tenantId, targetAddress, context, tracer); + super(con, sender, tenantId, targetAddress); } /** @@ -79,40 +75,24 @@ protected String getTo(final String deviceId) { /** * Creates a new sender for publishing events to a Hono server. * - * @param context The vertx context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param con The connection to the Hono server. * @param tenantId The tenant that the events will be published for. - * @param deviceId The device that the events will be published for or {@code null} - * if the events are going to be be produced by arbitrary devices of the - * tenant. * @param closeHook The handler to invoke when the Hono server closes the sender. The sender's * target address is provided as an argument to the handler. - * @param creationHandler The handler to invoke with the result of the creation attempt. - * @param tracer The OpenTracing {@code Tracer} to keep track of the messages sent - * by the sender returned. + * @return A future indicating the outcome. * @throws NullPointerException if any of context, connection, tenant or handler is {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, - final String deviceId, - final Handler closeHook, - final Handler> creationHandler, - final Tracer tracer) { + final Handler closeHook) { - Objects.requireNonNull(context); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); - Objects.requireNonNull(creationHandler); - final String targetAddress = getTargetAddress(tenantId, deviceId); - createSender(context, clientConfig, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook).compose(sender -> { - return Future. succeededFuture( - new EventSenderImpl(clientConfig, sender, tenantId, targetAddress, context, tracer)); - }).setHandler(creationHandler); + final String targetAddress = getTargetAddress(tenantId, null); + return con.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook) + .compose(sender -> Future.succeededFuture(new EventSenderImpl(con, sender, tenantId, targetAddress))); } /** @@ -171,12 +151,8 @@ protected Future sendMessage(final Message message, final Span c @Override protected Span startSpan(final SpanContext parent, final Message rawMessage) { - if (tracer == null) { - throw new IllegalStateException("no tracer configured"); - } else { - final Span span = newChildSpan(parent, "forward Event"); - Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); - return span; - } + final Span span = newChildSpan(parent, "forward Event"); + Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); + return span; } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/HonoConnectionImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/HonoConnectionImpl.java index 9a9eb861a5..776db685ff 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/HonoConnectionImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/HonoConnectionImpl.java @@ -34,6 +34,7 @@ import javax.security.sasl.AuthenticationException; import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.cache.CacheProvider; import org.eclipse.hono.client.AsyncCommandClient; @@ -49,6 +50,7 @@ import org.eclipse.hono.client.RequestResponseClient; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.ServiceInvocationException; +import org.eclipse.hono.client.StatusCodeMapper; import org.eclipse.hono.client.TenantClient; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.connection.ConnectionFactory; @@ -70,6 +72,11 @@ import io.vertx.proton.ProtonClientOptions; import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; +import io.vertx.proton.ProtonLink; +import io.vertx.proton.ProtonMessageHandler; +import io.vertx.proton.ProtonQoS; +import io.vertx.proton.ProtonReceiver; +import io.vertx.proton.ProtonSender; import io.vertx.proton.sasl.SaslSystemException; /** @@ -185,6 +192,14 @@ public final void setCacheProvider(final CacheProvider cacheProvider) { this.cacheProvider = Objects.requireNonNull(cacheProvider); } + /** + * {@inheritDoc} + */ + @Override + public final Vertx getVertx() { + return vertx; + } + /** * Sets the OpenTracing {@code Tracer} to use for tracing messages * published by devices across Hono's components. @@ -210,6 +225,11 @@ public final Tracer getTracer() { return tracer; } + @Override + public final ClientConfigProperties getConfig() { + return clientConfigProperties; + } + @Override public final void addDisconnectListener(final DisconnectListener listener) { disconnectListeners.add(listener); @@ -232,7 +252,8 @@ public final void addReconnectListener(final ReconnectListener listener) { * be failed with a {@link ServerErrorException} if the context * property is {@code null}. */ - protected final Future executeOrRunOnContext(final Handler> codeToRun) { + @Override + public final Future executeOrRunOnContext(final Handler> codeToRun) { if (context == null) { // this means that the connection to the peer is not established (yet) @@ -582,60 +603,6 @@ private void failConnectionAttempt(final Throwable connectionFailureCause, final } } - /** - * {@inheritDoc} - */ - @Override - public final Future getOrCreateTelemetrySender(final String tenantId) { - - Objects.requireNonNull(tenantId); - return getOrCreateSender( - getResourcesKeyForSender(TelemetrySenderImpl.class, TelemetrySenderImpl.getTargetAddress(tenantId, - null)), - () -> createTelemetrySender(tenantId)); - } - - private Future createTelemetrySender(final String tenantId) { - - return checkConnected().compose(connected -> { - final Future result = Future.future(); - TelemetrySenderImpl.create(context, clientConfigProperties, connection, tenantId, null, - onSenderClosed -> { - activeSenders.remove(getResourcesKeyForSender(TelemetrySenderImpl.class, - TelemetrySenderImpl.getTargetAddress(tenantId, - null))); - }, - result.completer(), tracer); - return result; - }); - } - - /** - * {@inheritDoc} - */ - @Override - public final Future getOrCreateEventSender(final String tenantId) { - - Objects.requireNonNull(tenantId); - return getOrCreateSender( - getResourcesKeyForSender(EventSenderImpl.class, EventSenderImpl.getTargetAddress(tenantId, null)), - () -> createEventSender(tenantId)); - } - - private Future createEventSender(final String tenantId) { - - return checkConnected().compose(connected -> { - final Future result = Future.future(); - EventSenderImpl.create(context, clientConfigProperties, connection, tenantId, null, - onSenderClosed -> { - activeSenders.remove(getResourcesKeyForSender(EventSenderImpl.class, - EventSenderImpl.getTargetAddress(tenantId, null))); - }, - result.completer(), tracer); - return result; - }); - } - /** * Gets an existing or creates a new message sender. *

@@ -732,13 +699,9 @@ private Future newTelemetryConsumer( final Consumer messageConsumer, final Handler closeHandler) { - return checkConnected().compose(con -> { - final Future result = Future.future(); - TelemetryConsumerImpl.create(context, clientConfigProperties, connection, tenantId, - connectionFactory.getPathSeparator(), messageConsumer, result.completer(), - closeHook -> closeHandler.handle(null)); - return result; - }); + return checkConnected().compose(con -> + TelemetryConsumerImpl.create(this, tenantId, messageConsumer, closeHook -> closeHandler.handle(null)) + ); } /** @@ -772,13 +735,9 @@ private Future newEventConsumer( final BiConsumer messageConsumer, final Handler closeHandler) { - return checkConnected().compose(con -> { - final Future result = Future.future(); - EventConsumerImpl.create(context, clientConfigProperties, connection, tenantId, - connectionFactory.getPathSeparator(), messageConsumer, result.completer(), - closeHook -> closeHandler.handle(null)); - return result; - }); + return checkConnected().compose(con -> + EventConsumerImpl.create(this, tenantId, messageConsumer, closeHook -> closeHandler.handle(null)) + ); } /** @@ -815,11 +774,12 @@ private Future newAsyncCommandResponseConsumer( final Handler closeHandler) { return checkConnected().compose(con -> { - final Future result = Future.future(); - AsyncCommandResponseConsumerImpl.create(context, clientConfigProperties, connection, tenantId, replyId, - connectionFactory.getPathSeparator(), messageConsumer, result.completer(), + return AsyncCommandResponseConsumerImpl.create( + this, + tenantId, + replyId, + messageConsumer, closeHook -> closeHandler.handle(null)); - return result; }); } @@ -892,17 +852,12 @@ protected Future newCredentialsClient(final String tenant return checkConnected().compose(connected -> { - final Future result = Future.future(); - CredentialsClientImpl.create( - context, - clientConfigProperties, - tracer, - connection, + return CredentialsClientImpl.create( + this, tenantId, this::removeCredentialsClient, - this::removeCredentialsClient, - result.completer()); - return result.map(client -> (RequestResponseClient) client); + this::removeCredentialsClient) + .map(client -> (RequestResponseClient) client); }); } @@ -961,18 +916,13 @@ protected Future newRegistrationClient(final String tenan return checkConnected().compose(connected -> { - final Future result = Future.future(); - RegistrationClientImpl.create( - context, - clientConfigProperties, + return RegistrationClientImpl.create( cacheProvider, - tracer, - connection, + this, tenantId, this::removeRegistrationClient, - this::removeRegistrationClient, - result.completer()); - return result.map(client -> (RequestResponseClient) client); + this::removeRegistrationClient) + .map(client -> (RequestResponseClient) client); }); } @@ -1015,17 +965,12 @@ protected Future newTenantClient() { return checkConnected().compose(connected -> { - final Future result = Future.future(); - TenantClientImpl.create( - context, - clientConfigProperties, + return TenantClientImpl.create( cacheProvider, - tracer, - connection, - this::removeTenantClient, + this, this::removeTenantClient, - result.completer()); - return result.map(client -> (RequestResponseClient) client); + this::removeTenantClient) + .map(client -> (RequestResponseClient) client); }); } @@ -1078,16 +1023,14 @@ public Future getOrCreateCommandClient(final String tenantId, fin private Future newCommandClient(final String tenantId, final String deviceId, final String replyId) { return checkConnected().compose(connected -> { - final Future result = Future.future(); - CommandClientImpl.create( - context, - clientConfigProperties, - connection, - tenantId, deviceId, replyId, - this::removeActiveRequestResponseClient, + return CommandClientImpl.create( + this, + tenantId, + deviceId, + replyId, this::removeActiveRequestResponseClient, - result.completer()); - return result.map(client -> (RequestResponseClient) client); + this::removeActiveRequestResponseClient) + .map(client -> (RequestResponseClient) client); }); } @@ -1104,15 +1047,16 @@ public Future getOrCreateAsyncCommandClient(final String ten private Future newAsyncCommandClient(final String tenantId, final String deviceId) { return checkConnected().compose(connected -> { - final Future result = Future.future(); - AsyncCommandClientImpl.create(context, clientConfigProperties, connection, tenantId, deviceId, + return AsyncCommandClientImpl.create( + this, + tenantId, + deviceId, onSenderClosed -> { activeSenders.remove(getResourcesKeyForSender(AsyncCommandClientImpl.class, AsyncCommandClientImpl.getTargetAddress(tenantId, deviceId))); - }, - result.completer()); - return result.map(client -> (MessageSender) client); + }) + .map(client -> (MessageSender) client); }); } @@ -1173,6 +1117,221 @@ private void getOrCreateRequestResponseClient( } } + /** + * {@inheritDoc} + * + * This method simply invokes {@link HonoProtonHelper#closeAndFree(Context, ProtonLink, Handler)} + * with this connection's vert.x context. + * + * @param link The link to close. If {@code null}, the given handler is invoked immediately. + * @param closeHandler The handler to notify once the link has been closed. + * @throws NullPointerException if context or close handler are {@code null}. + */ + @Override + public void closeAndFree( + final ProtonLink link, + final Handler closeHandler) { + + HonoProtonHelper.closeAndFree(context, link, closeHandler); + } + + /** + * {@inheritDoc} + * + * This method simply invokes {@link HonoProtonHelper#closeAndFree(Context, ProtonLink, long, Handler)} + * with this connection's vert.x context. + */ + @Override + public void closeAndFree( + final ProtonLink link, + final long detachTimeOut, + final Handler closeHandler) { + + HonoProtonHelper.closeAndFree(context, link, detachTimeOut, closeHandler); + } + + /** + * Creates a sender link. + * + * @param targetAddress The target address of the link. + * @param qos The quality of service to use for the link. + * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). + * @return A future for the created link. The future will be completed once the link is open. + * The future will fail with a {@link ServiceInvocationException} if the link cannot be opened. + * @throws NullPointerException if any of the arguments other than close hook is {@code null}. + */ + @Override + public final Future createSender( + final String targetAddress, + final ProtonQoS qos, + final Handler closeHook) { + + Objects.requireNonNull(targetAddress); + Objects.requireNonNull(qos); + + return executeOrRunOnContext(result -> { + + final ProtonSender sender = connection.createSender(targetAddress); + sender.setQoS(qos); + sender.setAutoSettle(true); + sender.openHandler(senderOpen -> { + + // we only "try" to complete/fail the result future because + // it may already have been failed if the connection broke + // away after we have sent our attach frame but before we have + // received the peer's attach frame + + if (senderOpen.failed()) { + // this means that we have received the peer's attach + // and the subsequent detach frame in one TCP read + final ErrorCondition error = sender.getRemoteCondition(); + if (error == null) { + log.debug("opening sender [{}] failed", targetAddress, senderOpen.cause()); + result.tryFail(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND, "cannot open sender", senderOpen.cause())); + } else { + log.debug("opening sender [{}] failed: {} - {}", targetAddress, error.getCondition(), error.getDescription()); + result.tryFail(StatusCodeMapper.from(error)); + } + + } else if (HonoProtonHelper.isLinkEstablished(sender)) { + + log.debug("sender open [target: {}, sendQueueFull: {}]", targetAddress, sender.sendQueueFull()); + // wait on credits a little time, if not already given + if (sender.getCredit() <= 0) { + vertx.setTimer(clientConfigProperties.getFlowLatency(), timerID -> { + log.debug("sender [target: {}] has {} credits after grace period of {}ms", targetAddress, + sender.getCredit(), clientConfigProperties.getFlowLatency()); + result.tryComplete(sender); + }); + } else { + result.tryComplete(sender); + } + + } else { + // this means that the peer did not create a local terminus for the link + // and will send a detach frame for closing the link very shortly + // see AMQP 1.0 spec section 2.6.3 + log.debug("peer did not create terminus for target [{}] and will detach the link", targetAddress); + result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); + } + }); + HonoProtonHelper.setDetachHandler(sender, remoteDetached -> onRemoteDetach(sender, connection.getRemoteContainer(), false, closeHook)); + HonoProtonHelper.setCloseHandler(sender, remoteClosed -> onRemoteDetach(sender, connection.getRemoteContainer(), true, closeHook)); + sender.open(); + vertx.setTimer(clientConfigProperties.getLinkEstablishmentTimeout(), tid -> onTimeOut(sender, clientConfigProperties, result)); + }); + } + + @Override + public Future createReceiver( + final String sourceAddress, + final ProtonQoS qos, + final ProtonMessageHandler messageHandler, + final Handler remoteCloseHook) { + return createReceiver(sourceAddress, qos, messageHandler, clientConfigProperties.getInitialCredits(), remoteCloseHook); + } + + @Override + public Future createReceiver( + final String sourceAddress, + final ProtonQoS qos, + final ProtonMessageHandler messageHandler, + final int preFetchSize, + final Handler remoteCloseHook) { + + Objects.requireNonNull(sourceAddress); + Objects.requireNonNull(qos); + Objects.requireNonNull(messageHandler); + if (preFetchSize < 0) { + throw new IllegalArgumentException("pre-fetch size must be >= 0"); + } + + return executeOrRunOnContext(result -> { + final ProtonReceiver receiver = connection.createReceiver(sourceAddress); + receiver.setAutoAccept(true); + receiver.setQoS(qos); + receiver.setPrefetch(preFetchSize); + receiver.handler((delivery, message) -> { + messageHandler.handle(delivery, message); + if (log.isTraceEnabled()) { + final int remainingCredits = receiver.getCredit() - receiver.getQueued(); + log.trace("handling message [remotely settled: {}, queued messages: {}, remaining credit: {}]", + delivery.remotelySettled(), receiver.getQueued(), remainingCredits); + } + }); + receiver.openHandler(recvOpen -> { + + // we only "try" to complete/fail the result future because + // it may already have been failed if the connection broke + // away after we have sent our attach frame but before we have + // received the peer's attach frame + + if (recvOpen.failed()) { + // this means that we have received the peer's attach + // and the subsequent detach frame in one TCP read + final ErrorCondition error = receiver.getRemoteCondition(); + if (error == null) { + log.debug("opening receiver [{}] failed", sourceAddress, recvOpen.cause()); + result.tryFail(new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND, "cannot open receiver", recvOpen.cause())); + } else { + log.debug("opening receiver [{}] failed: {} - {}", sourceAddress, error.getCondition(), error.getDescription()); + result.tryFail(StatusCodeMapper.from(error)); + } + } else if (HonoProtonHelper.isLinkEstablished(receiver)) { + log.debug("receiver open [source: {}]", sourceAddress); + result.tryComplete(recvOpen.result()); + } else { + // this means that the peer did not create a local terminus for the link + // and will send a detach frame for closing the link very shortly + // see AMQP 1.0 spec section 2.6.3 + log.debug("peer did not create terminus for source [{}] and will detach the link", sourceAddress); + result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); + } + }); + HonoProtonHelper.setDetachHandler(receiver, remoteDetached -> onRemoteDetach(receiver, connection.getRemoteContainer(), false, remoteCloseHook)); + HonoProtonHelper.setCloseHandler(receiver, remoteClosed -> onRemoteDetach(receiver, connection.getRemoteContainer(), true, remoteCloseHook)); + receiver.open(); + vertx.setTimer(clientConfigProperties.getLinkEstablishmentTimeout(), tid -> onTimeOut(receiver, clientConfigProperties, result)); + }); + } + + private void onTimeOut( + final ProtonLink link, + final ClientConfigProperties clientConfig, + final Future result) { + + if (link.isOpen() && !HonoProtonHelper.isLinkEstablished(link)) { + log.info("link establishment [peer: {}] timed out after {}ms", + clientConfig.getHost(), clientConfig.getLinkEstablishmentTimeout()); + link.close(); + link.free(); + result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE)); + } + } + + private void onRemoteDetach( + final ProtonLink link, + final String remoteContainer, + final boolean closed, + final Handler closeHook) { + + final ErrorCondition error = link.getRemoteCondition(); + final String type = link instanceof ProtonSender ? "sender" : "receiver"; + final String address = link instanceof ProtonSender ? link.getTarget().getAddress() : + link.getSource().getAddress(); + if (error == null) { + log.debug("{} [{}] detached (with closed={}) by peer [{}]", + type, address, closed, remoteContainer); + } else { + log.debug("{} [{}] detached (with closed={}) by peer [{}]: {} - {}", + type, address, closed, remoteContainer, error.getCondition(), error.getDescription()); + } + link.close(); + if (HonoProtonHelper.isLinkEstablished(link) && closeHook != null) { + closeHook.handle(address); + } + } + /** * {@inheritDoc} */ diff --git a/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java index d8d6843468..30975b139b 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -23,9 +23,9 @@ import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; import org.eclipse.hono.cache.CacheProvider; import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.StatusCodeMapper; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; @@ -37,16 +37,12 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonObject; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonReceiver; import io.vertx.proton.ProtonSender; @@ -60,33 +56,32 @@ public class RegistrationClientImpl extends AbstractRequestResponseClient + * The client will be ready to use after invoking {@link #createLinks()} or + * {@link #createLinks(Handler, Handler)} only. * - * @param context The vert.x context to use for interacting with the service. - * @param config The configuration properties. + * @param connection The connection to Hono. * @param tenantId The identifier of the tenant for which the client should be created. */ - protected RegistrationClientImpl(final Context context, final ClientConfigProperties config, final String tenantId) { - this(context, config, null, tenantId); - } - - private RegistrationClientImpl(final Context context, final ClientConfigProperties config, final Tracer tracer, final String tenantId) { - - super(context, config, tracer, tenantId); + protected RegistrationClientImpl(final HonoConnection connection, final String tenantId) { + super(connection, tenantId); } /** * Creates a new client for accessing the Device Registration service. * - * @param context The vert.x context to use for interacting with the service. - * @param config The configuration properties. + * @param connection The connection to Hono. * @param tenantId The identifier of the tenant for which the client should be created. * @param sender The AMQP link to use for sending requests to the service. * @param receiver The AMQP link to use for receiving responses from the service. */ - protected RegistrationClientImpl(final Context context, final ClientConfigProperties config, final String tenantId, - final ProtonSender sender, final ProtonReceiver receiver) { + protected RegistrationClientImpl( + final HonoConnection connection, + final String tenantId, + final ProtonSender sender, + final ProtonReceiver receiver) { - super(context, config, tenantId, sender, receiver); + super(connection, tenantId, sender, receiver); } /** @@ -135,44 +130,35 @@ protected final RegistrationResult getResult( /** * Creates a new registration client for a tenant. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param cacheProvider A factory for cache instances for registration results. If {@code null} * the client will not cache any results from the Device Registration service. - * @param tracer The tracer to use for tracking request processing - * across process boundaries. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param tenantId The tenant to consumer events for. * @param senderCloseHook A handler to invoke if the peer closes the sender link unexpectedly. * @param receiverCloseHook A handler to invoke if the peer closes the receiver link unexpectedly. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. + * @return A future indicating the outcome of the creation attempt. * @throws NullPointerException if any of the parameters other than cache provider is {@code null}. */ - public static final void create( - final Context context, - final ClientConfigProperties clientConfig, + public static final Future create( final CacheProvider cacheProvider, - final Tracer tracer, - final ProtonConnection con, + final HonoConnection con, final String tenantId, final Handler senderCloseHook, - final Handler receiverCloseHook, - final Handler> creationHandler) { + final Handler receiverCloseHook) { LOG.debug("creating new registration client for [{}]", tenantId); - final RegistrationClientImpl client = new RegistrationClientImpl(context, clientConfig, tracer, tenantId); + final RegistrationClientImpl client = new RegistrationClientImpl(con, tenantId); if (cacheProvider != null) { client.setResponseCache(cacheProvider.getCache(RegistrationClientImpl.getTargetAddress(tenantId))); } - client.createLinks(con, senderCloseHook, receiverCloseHook).setHandler(s -> { - if (s.succeeded()) { - LOG.debug("successfully created registration client for [{}]", tenantId); - creationHandler.handle(Future.succeededFuture(client)); - } else { - LOG.debug("failed to create registration client for [{}]", tenantId, s.cause()); - creationHandler.handle(Future.failedFuture(s.cause())); - } - }); + return client.createLinks(senderCloseHook, receiverCloseHook) + .map(ok -> { + LOG.debug("successfully created registration client for [{}]", tenantId); + return (RegistrationClient) client; + }).recover(t -> { + LOG.debug("failed to create registration client for [{}]", tenantId, t); + return Future.failedFuture(t); + }); } private Map createDeviceIdProperties(final String deviceId) { diff --git a/client/src/main/java/org/eclipse/hono/client/impl/TelemetryConsumerImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/TelemetryConsumerImpl.java index 68736ad0c8..47845296e3 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/TelemetryConsumerImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/TelemetryConsumerImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,97 +13,54 @@ package org.eclipse.hono.client.impl; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; -import io.vertx.proton.ProtonQoS; -import io.vertx.proton.ProtonReceiver; +import java.util.Objects; +import java.util.function.Consumer; + import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.config.ClientConfigProperties; -import org.eclipse.hono.util.Constants; import org.eclipse.hono.util.TelemetryConstants; -import java.util.Objects; -import java.util.function.Consumer; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.proton.ProtonQoS; +import io.vertx.proton.ProtonReceiver; /** * A Vertx-Proton based client for consuming telemetry data from a Hono server. */ public class TelemetryConsumerImpl extends AbstractConsumer implements MessageConsumer { - private static final String TELEMETRY_ADDRESS_TEMPLATE = TelemetryConstants.TELEMETRY_ENDPOINT + "%s%s"; - - private TelemetryConsumerImpl(final Context context, final ClientConfigProperties config, final ProtonReceiver receiver) { - super(context, config, receiver); + private TelemetryConsumerImpl(final HonoConnection connection, final ProtonReceiver receiver) { + super(connection, receiver); } /** * Creates a new telemetry data consumer for a tenant. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param tenantId The tenant to consumer events for. * @param telemetryConsumer The consumer to invoke with each telemetry message received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). + * @return A future indicating the outcome. * @throws NullPointerException if any of the parameters is {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, final Consumer telemetryConsumer, - final Handler> creationHandler, final Handler closeHook ) { - create(context, clientConfig, con, tenantId, Constants.DEFAULT_PATH_SEPARATOR, telemetryConsumer, creationHandler, closeHook); - } - - /** - * Creates a new telemetry data consumer for a tenant. - * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. - * @param con The AMQP connection to the server. - * @param tenantId The tenant to consumer events for. - * @param pathSeparator The address path separator character used by the server. - * @param telemetryConsumer The consumer to invoke with each telemetry message received. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @param closeHook The handler to invoke when the link is closed by the peer (may be {@code null}). - * @throws NullPointerException if any of the parameters is {@code null}. - */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, - final String tenantId, - final String pathSeparator, - final Consumer telemetryConsumer, - final Handler> creationHandler, - final Handler closeHook) { - - Objects.requireNonNull(context); - Objects.requireNonNull(clientConfig); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); - Objects.requireNonNull(pathSeparator); Objects.requireNonNull(telemetryConsumer); - Objects.requireNonNull(creationHandler); - createReceiver(context, clientConfig, con, String.format(TELEMETRY_ADDRESS_TEMPLATE, pathSeparator, tenantId), ProtonQoS.AT_LEAST_ONCE, - (delivery, message) -> telemetryConsumer.accept(message), closeHook).setHandler(created -> { - if (created.succeeded()) { - creationHandler.handle(Future.succeededFuture( - new TelemetryConsumerImpl(context, clientConfig, created.result()))); - } else { - creationHandler.handle(Future.failedFuture(created.cause())); - } - }); + final String sourceAddress = String.format("%s/%s", TelemetryConstants.TELEMETRY_ENDPOINT, tenantId); + return con.createReceiver( + sourceAddress, + ProtonQoS.AT_LEAST_ONCE, + (delivery, message) -> telemetryConsumer.accept(message), + closeHook) + .compose(receiver -> Future.succeededFuture(new TelemetryConsumerImpl(con, receiver))); } - } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/TelemetrySenderImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/TelemetrySenderImpl.java index ad3b1b1d19..c7f8036507 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/TelemetrySenderImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/TelemetrySenderImpl.java @@ -25,7 +25,8 @@ import org.apache.qpid.proton.amqp.messaging.Released; import org.apache.qpid.proton.amqp.transport.DeliveryState; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.config.ClientConfigProperties; @@ -35,14 +36,10 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; import io.opentracing.log.Fields; import io.opentracing.tag.Tags; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonSender; @@ -50,16 +47,15 @@ /** * A Vertx-Proton based client for uploading telemetry data to a Hono server. */ -public final class TelemetrySenderImpl extends AbstractSender { +public final class TelemetrySenderImpl extends AbstractDownstreamSender { - TelemetrySenderImpl(final ClientConfigProperties config, final ProtonSender sender, final String tenantId, - final String targetAddress, final Context context) { - this(config, sender, tenantId, targetAddress, context, null); - } + TelemetrySenderImpl( + final HonoConnection con, + final ProtonSender sender, + final String tenantId, + final String targetAddress) { - TelemetrySenderImpl(final ClientConfigProperties config, final ProtonSender sender, final String tenantId, - final String targetAddress, final Context context, final Tracer tracer) { - super(config, sender, tenantId, targetAddress, context, tracer); + super(con, sender, tenantId, targetAddress); } /** @@ -93,40 +89,24 @@ protected String getTo(final String deviceId) { /** * Creates a new sender for publishing telemetry data to a Hono server. * - * @param context The vertx context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param con The connection to the Hono server. * @param tenantId The tenant that the telemetry data will be uploaded for. - * @param deviceId The device that the telemetry data will be uploaded for or {@code null} - * if the data to be uploaded will be produced by arbitrary devices of the - * tenant. - * @param closeHook The handler to invoke when the Hono server closes the sender. The sender's - * target address is provided as an argument to the handler. - * @param creationHandler The handler to invoke with the result of the creation attempt. - * @param tracer The OpenTracing {@code Tracer} to keep track of the messages sent - * by the sender returned. + * @param remoteCloseHook The handler to invoke when the Hono server closes the sender. The sender's + * target address is provided as an argument to the handler. + * @return A future indicating the outcome. * @throws NullPointerException if any of context, connection, tenant or handler is {@code null}. */ - public static void create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + public static Future create( + final HonoConnection con, final String tenantId, - final String deviceId, - final Handler closeHook, - final Handler> creationHandler, - final Tracer tracer) { + final Handler remoteCloseHook) { - Objects.requireNonNull(context); Objects.requireNonNull(con); Objects.requireNonNull(tenantId); - Objects.requireNonNull(creationHandler); - final String targetAddress = getTargetAddress(tenantId, deviceId); - createSender(context, clientConfig, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook).compose(sender -> { - return Future. succeededFuture( - new TelemetrySenderImpl(clientConfig, sender, tenantId, targetAddress, context, tracer)); - }).setHandler(creationHandler); + final String targetAddress = getTargetAddress(tenantId, null); + return con.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, remoteCloseHook) + .compose(sender -> Future.succeededFuture(new TelemetrySenderImpl(con, sender, tenantId, targetAddress))); } /** @@ -152,9 +132,9 @@ public Future sendAndWaitForOutcome(final Message rawMessage, fi Tags.MESSAGE_BUS_DESTINATION.set(span, targetAddress); span.setTag(MessageHelper.APP_PROPERTY_TENANT_ID, tenantId); span.setTag(MessageHelper.APP_PROPERTY_DEVICE_ID, MessageHelper.getDeviceId(rawMessage)); - TracingHelper.injectSpanContext(tracer, span.context(), rawMessage); + TracingHelper.injectSpanContext(connection.getTracer(), span.context(), rawMessage); - return executeOrRunOnContext(result -> { + return connection.executeOrRunOnContext(result -> { if (sender.sendQueueFull()) { final ServiceInvocationException e = new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no credit available"); logError(span, e); @@ -198,9 +178,10 @@ protected Future sendMessage(final Message message, final Span c details.put(TracingHelper.TAG_QOS.getKey(), sender.getQoS().toString()); currentSpan.log(details); + final ClientConfigProperties config = connection.getConfig(); final AtomicBoolean timeoutReached = new AtomicBoolean(false); final Long timerId = config.getSendMessageTimeout() > 0 - ? context.owner().setTimer(config.getSendMessageTimeout(), id -> { + ? connection.getVertx().setTimer(config.getSendMessageTimeout(), id -> { if (timeoutReached.compareAndSet(false, true)) { final ServerErrorException exception = new ServerErrorException( HttpURLConnection.HTTP_UNAVAILABLE, @@ -216,7 +197,7 @@ protected Future sendMessage(final Message message, final Span c final ProtonDelivery result = sender.send(message, deliveryUpdated -> { if (timerId != null) { - context.owner().cancelTimer(timerId); + connection.getVertx().cancelTimer(timerId); } final DeliveryState remoteState = deliveryUpdated.getRemoteState(); if (timeoutReached.get()) { @@ -272,23 +253,15 @@ protected Future sendMessage(final Message message, final Span c @Override protected Span startSpan(final SpanContext parent, final Message rawMessage) { - if (tracer == null) { - throw new IllegalStateException("no tracer configured"); - } else { - final Span span = newFollowingSpan(parent, "forward Telemetry data"); - Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); - return span; - } + final Span span = newFollowingSpan(parent, "forward Telemetry data"); + Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); + return span; } private Span startChildSpan(final SpanContext parent, final Message rawMessage) { - if (tracer == null) { - throw new IllegalStateException("no tracer configured"); - } else { - final Span span = newChildSpan(parent, "forward Telemetry data"); - Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); - return span; - } + final Span span = newChildSpan(parent, "forward Telemetry data"); + Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_PRODUCER); + return span; } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/TenantClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/TenantClientImpl.java index 84bdb4fa44..698e8199f1 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/TenantClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/TenantClientImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -25,9 +25,9 @@ import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; import org.eclipse.hono.cache.CacheProvider; import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.StatusCodeMapper; import org.eclipse.hono.client.TenantClient; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; @@ -44,11 +44,7 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.opentracing.Tracer; -import io.opentracing.noop.NoopTracerFactory; import io.opentracing.tag.StringTag; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; @@ -69,58 +65,32 @@ public class TenantClientImpl extends AbstractRequestResponseClient + * The client will be ready to use after invoking {@link #createLinks(ProtonConnection)} or + * {@link #createLinks(ProtonConnection, Handler, Handler)} only. * - * @param context The Vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @throws NullPointerException if any of the parameters is {@code null}. + * @param connection The connection to Hono. + * @throws NullPointerException if any of the parameters are {@code null}. */ - protected TenantClientImpl(final Context context, final ClientConfigProperties config) { - this(context, config, NoopTracerFactory.create()); + TenantClientImpl(final HonoConnection connection) { + super(connection, null); } /** - * Creates a tenant API client. + * Creates a client for invoking operations of the Tenant API. * - * @param context The Vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing - * across process boundaries. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - protected TenantClientImpl(final Context context, final ClientConfigProperties config, final Tracer tracer) { - super(context, config, tracer, null); - } - - /** - * Creates a tenant API client. - * - * @param context The Vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. + * @param connection The connection to Hono. * @param sender The AMQP 1.0 link to use for sending requests to the peer. * @param receiver The AMQP 1.0 link to use for receiving responses from the peer. * @throws NullPointerException if any of the parameters is {@code null}. */ - protected TenantClientImpl(final Context context, final ClientConfigProperties config, - final ProtonSender sender, final ProtonReceiver receiver) { + protected TenantClientImpl( + final HonoConnection connection, + final ProtonSender sender, + final ProtonReceiver receiver) { - this(context, config, null, sender, receiver); - } - - /** - * Creates a tenant API client. - * - * @param context The Vert.x context to run message exchanges with the peer on. - * @param config The configuration properties to use. - * @param tracer The tracer to use for tracking request processing - * across process boundaries. - * @param sender The AMQP 1.0 link to use for sending requests to the peer. - * @param receiver The AMQP 1.0 link to use for receiving responses from the peer. - * @throws NullPointerException if any of the parameters is {@code null}. - */ - protected TenantClientImpl(final Context context, final ClientConfigProperties config, - final Tracer tracer, final ProtonSender sender, final ProtonReceiver receiver) { - super(context, config, tracer, null, sender, receiver); + super(connection, null, sender, receiver); } @Override @@ -172,42 +142,33 @@ public static final String getTargetAddress() { /** * Creates a new tenant client. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param cacheProvider A factory for cache instances for tenant configuration results. If {@code null} * the client will not cache any results from the Tenant service. - * @param tracer The tracer to use for tracking request processing - * across process boundaries. - * @param con The AMQP connection to the server. + * @param con The connection to the server. * @param senderCloseHook A handler to invoke if the peer closes the sender link unexpectedly. * @param receiverCloseHook A handler to invoke if the peer closes the receiver link unexpectedly. - * @param creationHandler The handler to invoke with the outcome of the creation attempt. - * @throws NullPointerException if any of the parameters, except for senderCloseHook and receiverCloseHook, is {@code null}. + * @return A future indicating the outcome of the creation attempt. + * @throws NullPointerException if any of the parameters, except for senderCloseHook and receiverCloseHook are {@code null}. */ - public static final void create( - final Context context, - final ClientConfigProperties clientConfig, + public static final Future create( final CacheProvider cacheProvider, - final Tracer tracer, - final ProtonConnection con, + final HonoConnection con, final Handler senderCloseHook, - final Handler receiverCloseHook, - final Handler> creationHandler) { + final Handler receiverCloseHook) { LOG.debug("creating new tenant client"); - final TenantClientImpl client = new TenantClientImpl(context, clientConfig, tracer); + final TenantClientImpl client = new TenantClientImpl(con); if (cacheProvider != null) { client.setResponseCache(cacheProvider.getCache(TenantClientImpl.getTargetAddress())); } - client.createLinks(con, senderCloseHook, receiverCloseHook).setHandler(s -> { - if (s.succeeded()) { + return client.createLinks(senderCloseHook, receiverCloseHook) + .map(ok -> { LOG.debug("successfully created tenant client"); - creationHandler.handle(Future.succeededFuture(client)); - } else { - LOG.debug("failed to create tenant client", s.cause()); - creationHandler.handle(Future.failedFuture(s.cause())); - } - }); + return (TenantClient) client; + }).recover(t -> { + LOG.debug("failed to create tenant client", t); + return Future.failedFuture(t); + }); } /** diff --git a/client/src/test/java/org/eclipse/hono/client/impl/AbstractHonoClientTest.java b/client/src/test/java/org/eclipse/hono/client/impl/AbstractHonoClientTest.java index a0230e4f50..195a47584d 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/AbstractHonoClientTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/AbstractHonoClientTest.java @@ -15,48 +15,22 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import java.net.HttpURLConnection; import java.util.HashMap; import java.util.Map; -import java.util.function.Predicate; -import java.util.function.Supplier; import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; -import org.apache.qpid.proton.amqp.transport.AmqpError; -import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.ServerErrorException; -import org.eclipse.hono.client.ServiceInvocationException; -import org.eclipse.hono.config.ClientConfigProperties; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import io.vertx.core.Context; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; -import io.vertx.proton.ProtonConnection; -import io.vertx.proton.ProtonQoS; -import io.vertx.proton.ProtonReceiver; -import io.vertx.proton.ProtonSender; /** @@ -72,189 +46,6 @@ public class AbstractHonoClientTest { @Rule public final Timeout timeout = Timeout.seconds(5); - private Vertx vertx; - private Context context; - private ClientConfigProperties props; - private Handler closeHook; - - /** - * Sets up the fixture. - */ - @SuppressWarnings("unchecked") - @Before - public void setUp() { - vertx = mock(Vertx.class); - props = new ClientConfigProperties(); - context = HonoClientUnitTestHelper.mockContext(vertx); - closeHook = mock(Handler.class); - } - - /** - * Verifies that the attempt to create a sender fails with a - * {@code ServiceInvocationException} if the remote peer refuses - * to open the link with an error condition. - * - * @param ctx The vert.x test context. - */ - @Test - public void testCreateSenderFailsForErrorCondition(final TestContext ctx) { - - testCreateSenderFails(() -> (ErrorCondition) new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "unauthorized"), cause -> { - return cause instanceof ServiceInvocationException; - }); - } - - /** - * Verifies that the attempt to create a sender fails with a - * {@code ClientErrorException} if the remote peer refuses - * to open the link without an error condition. - * - * @param ctx The vert.x test context. - */ - @Test - public void testCreateSenderFailsWithoutErrorCondition(final TestContext ctx) { - - testCreateSenderFails(() -> (ErrorCondition) null, cause -> { - return cause instanceof ClientErrorException && - ((ClientErrorException) cause).getErrorCode() == HttpURLConnection.HTTP_NOT_FOUND; - }); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void testCreateSenderFails(final Supplier errorSupplier, final Predicate failureAssertion) { - - final ProtonSender sender = mock(ProtonSender.class); - when(sender.getRemoteCondition()).thenReturn(errorSupplier.get()); - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createSender(anyString())).thenReturn(sender); - - final Future result = AbstractHonoClient.createSender(context, props, con, - "target", ProtonQoS.AT_LEAST_ONCE, closeHook); - - verify(vertx).setTimer(eq(props.getLinkEstablishmentTimeout()), any(Handler.class)); - final ArgumentCaptor openHandler = ArgumentCaptor.forClass(Handler.class); - verify(sender).openHandler(openHandler.capture()); - openHandler.getValue().handle(Future.failedFuture(new IllegalStateException())); - assertTrue(result.failed()); - assertTrue(failureAssertion.test(result.cause())); - verify(closeHook, never()).handle(anyString()); - } - - /** - * Verifies that the attempt to create a sender fails with a - * {@code ServerErrorException} if the remote peer doesn't - * send its attach frame in time. - * - * @param ctx The vert.x test context. - */ - @SuppressWarnings("unchecked") - @Test - public void testCreateSenderFailsOnTimeout(final TestContext ctx) { - - final ProtonSender sender = mock(ProtonSender.class); - when(sender.isOpen()).thenReturn(Boolean.TRUE); - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createSender(anyString())).thenReturn(sender); - // run any timer immediately - doAnswer(invocation -> { - final Handler handler = invocation.getArgument(1); - handler.handle(null); - return 1L; - }).when(vertx).setTimer(anyLong(), any(Handler.class)); - - final Future result = AbstractHonoClient.createSender(context, props, con, - "target", ProtonQoS.AT_LEAST_ONCE, closeHook); - assertTrue(result.failed()); - assertThat(((ServerErrorException) result.cause()).getErrorCode(), is(HttpURLConnection.HTTP_UNAVAILABLE)); - verify(sender).open(); - verify(sender).close(); - verify(sender).free(); - verify(closeHook, never()).handle(anyString()); - } - - /** - * Verifies that the attempt to create a receiver fails with a - * {@code ServiceInvocationException} if the remote peer refuses - * to open the link with an error condition. - * - * @param ctx The vert.x test context. - */ - @Test - public void testCreateReceiverFailsForErrorCondition(final TestContext ctx) { - - testCreateReceiverFails(() -> new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "unauthorized"), cause -> { - return cause instanceof ServiceInvocationException; - }); - } - - /** - * Verifies that the attempt to create a receiver fails with a - * {@code ClientErrorException} if the remote peer refuses - * to open the link without an error condition. - * - * @param ctx The vert.x test context. - */ - @Test - public void testCreateReceiverFailsWithoutErrorCondition(final TestContext ctx) { - - testCreateReceiverFails(() -> (ErrorCondition) null, cause -> { - return cause instanceof ClientErrorException && - ((ClientErrorException) cause).getErrorCode() == HttpURLConnection.HTTP_NOT_FOUND; - }); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void testCreateReceiverFails(final Supplier errorSupplier, final Predicate failureAssertion) { - - final ProtonReceiver receiver = mock(ProtonReceiver.class); - when(receiver.getRemoteCondition()).thenReturn(errorSupplier.get()); - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createReceiver(anyString())).thenReturn(receiver); - - final Future result = AbstractHonoClient.createReceiver(context, props, con, - "source", ProtonQoS.AT_LEAST_ONCE, (delivery, msg) -> {}, closeHook); - - verify(vertx).setTimer(eq(props.getLinkEstablishmentTimeout()), any(Handler.class)); - final ArgumentCaptor openHandler = ArgumentCaptor.forClass(Handler.class); - verify(receiver).openHandler(openHandler.capture()); - openHandler.getValue().handle(Future.failedFuture(new IllegalStateException())); - assertTrue(result.failed()); - assertTrue(failureAssertion.test(result.cause())); - verify(closeHook, never()).handle(anyString()); - } - - /** - * Verifies that the attempt to create a receiver fails with a - * {@code ServerErrorException} if the remote peer doesn't - * send its attach frame in time. - * - * @param ctx The vert.x test context. - */ - @SuppressWarnings("unchecked") - @Test - public void testCreateReceiverFailsOnTimeout(final TestContext ctx) { - - final ProtonReceiver receiver = mock(ProtonReceiver.class); - when(receiver.isOpen()).thenReturn(Boolean.TRUE); - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createReceiver(anyString())).thenReturn(receiver); - // run any timer immediately - doAnswer(invocation -> { - final Handler handler = invocation.getArgument(1); - handler.handle(null); - return 1L; - }).when(vertx).setTimer(anyLong(), any(Handler.class)); - - final Future result = AbstractHonoClient.createReceiver(context, props, con, - "source", ProtonQoS.AT_LEAST_ONCE, (delivery, msg) -> {}, closeHook); - assertTrue(result.failed()); - assertThat(((ServerErrorException) result.cause()).getErrorCode(), is(HttpURLConnection.HTTP_UNAVAILABLE)); - verify(receiver).open(); - verify(receiver).close(); - verify(receiver).free(); - verify(closeHook, never()).handle(anyString()); - } - /** * Verifies that the given application properties are propagated to * the message. diff --git a/client/src/test/java/org/eclipse/hono/client/impl/AbstractRequestResponseClientTest.java b/client/src/test/java/org/eclipse/hono/client/impl/AbstractRequestResponseClientTest.java index a612154c42..f9556784fa 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/AbstractRequestResponseClientTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/AbstractRequestResponseClientTest.java @@ -13,12 +13,18 @@ package org.eclipse.hono.client.impl; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.net.HttpURLConnection; import java.time.Duration; @@ -33,9 +39,9 @@ import org.apache.qpid.proton.amqp.transport.Target; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.cache.ExpiringValueCache; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RequestResponseClientConfigProperties; import org.eclipse.hono.client.ServerErrorException; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; import org.junit.Before; @@ -47,7 +53,6 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -79,7 +84,6 @@ public class AbstractRequestResponseClientTest { private AbstractRequestResponseClient client; private ExpiringValueCache cache; private Vertx vertx; - private Context context; private ProtonReceiver receiver; private ProtonSender sender; @@ -92,7 +96,6 @@ public class AbstractRequestResponseClientTest { public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); receiver = HonoClientUnitTestHelper.mockProtonReceiver(); sender = HonoClientUnitTestHelper.mockProtonSender(); @@ -467,7 +470,8 @@ public void testSendOneWayRequestSucceedsOnAcceptedMessage(final TestContext ctx private AbstractRequestResponseClient getClient(final String tenant, final ProtonSender sender, final ProtonReceiver receiver) { - return new AbstractRequestResponseClient(context, new ClientConfigProperties(), tenant, sender, receiver) { + final HonoConnection connection = HonoClientUnitTestHelper.mockHonoConnection(vertx); + return new AbstractRequestResponseClient(connection, tenant, sender, receiver) { @Override protected String getName() { diff --git a/client/src/test/java/org/eclipse/hono/client/impl/AbstractSenderTest.java b/client/src/test/java/org/eclipse/hono/client/impl/AbstractSenderTest.java index 5ee5bcc790..b4936d73e6 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/AbstractSenderTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/AbstractSenderTest.java @@ -29,8 +29,8 @@ import java.net.HttpURLConnection; import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServerErrorException; -import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.MessageHelper; import org.junit.Before; import org.junit.Test; @@ -38,11 +38,11 @@ import io.opentracing.Span; import io.opentracing.SpanContext; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.proton.ProtonDelivery; +import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonSender; @@ -53,8 +53,6 @@ public class AbstractSenderTest { private ProtonSender protonSender; - private ClientConfigProperties config; - private Context context; private Vertx vertx; /** @@ -63,9 +61,7 @@ public class AbstractSenderTest { @Before public void setUp() { protonSender = HonoClientUnitTestHelper.mockProtonSender(); - config = new ClientConfigProperties(); vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); } /** @@ -84,7 +80,10 @@ public void testSendMessageRemovesRegistrationAssertion() { final AbstractSender sender = newSender("tenant", "endpoint"); // WHEN sending a message - sender.send("device", "some payload", "application/text"); + final Message msg = ProtonHelper.message("some payload"); + msg.setContentType("application/text"); + MessageHelper.addDeviceId(msg, "device"); + sender.send(msg); // THEN the message is sent without the registration assertion final ArgumentCaptor sentMessage = ArgumentCaptor.forClass(Message.class); @@ -103,7 +102,8 @@ public void testSendMessageFailsOnLackOfCredit() { final AbstractSender sender = newSender("tenant", "endpoint"); // WHEN trying to send a message - final Future result = sender.send("device", "some payload", "application/text"); + final Message msg = ProtonHelper.message("test"); + final Future result = sender.send(msg); // THEN the message is not sent assertFalse(result.succeeded()); @@ -155,13 +155,12 @@ public void testCredits() { private AbstractSender newSender(final String tenantId, final String targetAddress) { + final HonoConnection connection = HonoClientUnitTestHelper.mockHonoConnection(vertx); return new AbstractSender( - config, + connection, protonSender, tenantId, - targetAddress, - context, - null) { + targetAddress) { @Override public String getEndpoint() { diff --git a/client/src/test/java/org/eclipse/hono/client/impl/CachingClientFactoryTest.java b/client/src/test/java/org/eclipse/hono/client/impl/CachingClientFactoryTest.java new file mode 100644 index 0000000000..d145638efa --- /dev/null +++ b/client/src/test/java/org/eclipse/hono/client/impl/CachingClientFactoryTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import java.net.HttpURLConnection; + +import org.eclipse.hono.client.ServerErrorException; +import org.eclipse.hono.client.ServiceInvocationException; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.vertx.core.Future; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; + + +/** + * Tests verifying behavior of CachingClientFactory. + * + */ +@RunWith(VertxUnitRunner.class) +public class CachingClientFactoryTest { + + /** + * Verifies that a concurrent request to create a client fails the given + * future for tracking the attempt. + * + * @param ctx The helper to use for running async tests. + */ + @Test + public void testGetOrCreateClientFailsIfInvokedConcurrently(final TestContext ctx) { + + // GIVEN a factory that already creates a client for key "bumlux" + final CachingClientFactory factory = new CachingClientFactory<>(o -> true); + final Future creationResult = Future.future(); + factory.getOrCreateClient( + "bumlux", + () -> Future.future(), + creationResult); + + // WHEN an additional, concurrent attempt is made to create a client for the same key + factory.getOrCreateClient( + "bumlux", + () -> { + ctx.fail("should not create client concurrently"); + return Future.succeededFuture(); + }, ctx.asyncAssertFailure(t -> { + // THEN the concurrent attempt fails without any attempt being made to create another client + ctx.assertTrue(t instanceof ServerErrorException); + ctx.assertEquals( + HttpURLConnection.HTTP_UNAVAILABLE, + ServiceInvocationException.extractStatusCode(t)); + })); + } + + /** + * Verifies that a request to create a client is failed immediately when + * the factory's clearState method is invoked. + * + * @param ctx The Vertx test context. + */ + @Test + public void testGetOrCreateClientFailsWhenStateIsCleared(final TestContext ctx) { + + // GIVEN a factory that tries to create a client for key "tenant" + final CachingClientFactory factory = new CachingClientFactory<>(o -> true); + final Async supplierInvocation = ctx.async(); + + final Future creationAttempt = Future.future(); + factory.getOrCreateClient( + "tenant", + () -> { + supplierInvocation.complete(); + return Future.future(); + }, creationAttempt); + + // WHEN the factory's state is being cleared + supplierInvocation.await(); + factory.clearState(); + + // THEN all creation requests are failed + ctx.assertTrue(creationAttempt.failed()); + + // and the next request to create a client for the same key succeeds + factory.getOrCreateClient( + "tenant", + () -> Future.succeededFuture(new Object()), + ctx.asyncAssertSuccess()); + } +} diff --git a/client/src/test/java/org/eclipse/hono/client/impl/CommandClientImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/CommandClientImplTest.java index d9dc8a7c2f..8a75c25dcd 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/CommandClientImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/CommandClientImplTest.java @@ -21,8 +21,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import java.util.HashMap; +import java.util.Map; + import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RequestResponseClientConfigProperties; +import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.util.Constants; import org.junit.Before; import org.junit.Rule; @@ -31,7 +36,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -40,9 +44,6 @@ import io.vertx.proton.ProtonReceiver; import io.vertx.proton.ProtonSender; -import java.util.HashMap; -import java.util.Map; - /** * Tests verifying behavior of {@link CommandClientImpl}. * @@ -59,7 +60,6 @@ public class CommandClientImplTest { public Timeout globalTimeout = Timeout.seconds(3); private Vertx vertx; - private Context context; private ProtonSender sender; private ProtonReceiver receiver; private CommandClientImpl client; @@ -71,14 +71,12 @@ public class CommandClientImplTest { public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); receiver = HonoClientUnitTestHelper.mockProtonReceiver(); sender = HonoClientUnitTestHelper.mockProtonSender(); - - final RequestResponseClientConfigProperties config = new RequestResponseClientConfigProperties(); + final ClientConfigProperties config = new RequestResponseClientConfigProperties(); + final HonoConnection connection = HonoClientUnitTestHelper.mockHonoConnection(vertx, config); client = new CommandClientImpl( - context, - config, + connection, Constants.DEFAULT_TENANT, DEVICE_ID, REPLY_ID, diff --git a/client/src/test/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImplTest.java new file mode 100644 index 0000000000..02a2d2c71d --- /dev/null +++ b/client/src/test/java/org/eclipse/hono/client/impl/DownstreamSenderFactoryImplTest.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.client.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.hono.client.DisconnectListener; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.client.ServerErrorException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.proton.ProtonQoS; +import io.vertx.proton.ProtonSender; + + +/** + * Tests verifying behavior of {@link DownstreamSenderFactoryImpl}. + * + */ +@RunWith(VertxUnitRunner.class) +public class DownstreamSenderFactoryImplTest { + + private HonoConnection connection; + private DownstreamSenderFactoryImpl factory; + + /** + * Sets up the fixture. + */ + @Before + public void setUp() { + final Vertx vertx = mock(Vertx.class); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx); + factory = new DownstreamSenderFactoryImpl(connection); + } + + /** + * Verifies that a concurrent request to create a sender fails the given future for tracking the attempt. + * + * @param ctx The helper to use for running async tests. + */ + @SuppressWarnings("unchecked") + @Test + public void testGetTelemetrySenderFailsIfInvokedConcurrently(final TestContext ctx) { + + // GIVEN a factory that already tries to create a telemetry sender for "tenant" + final Future sender = Future.future(); + when(connection.createSender(anyString(), any(ProtonQoS.class), any(Handler.class))).thenReturn(sender); + final Future result = factory.getOrCreateTelemetrySender("telemetry/tenant"); + assertFalse(result.isComplete()); + + // WHEN an additional, concurrent attempt is made to create a telemetry sender for "tenant" + factory.getOrCreateTelemetrySender("telemetry/tenant").setHandler(ctx.asyncAssertFailure(t -> { + // THEN the concurrent attempt fails without any attempt being made to create another sender + ctx.assertTrue(ServerErrorException.class.isInstance(t)); + })); + sender.complete(mock(ProtonSender.class)); + assertTrue(result.isComplete()); + } + + /** + * Verifies that a request to create a sender is failed immediately when the + * underlying connection to the server fails. + */ + @SuppressWarnings("unchecked") + @Test + public void testGetTelemetrySenderFailsOnConnectionFailure() { + + // GIVEN a factory that tries to create a telemetry sender for "tenant" + final Future sender = Future.future(); + when(connection.createSender(anyString(), any(ProtonQoS.class), any(Handler.class))).thenReturn(sender); + final ArgumentCaptor disconnectHandler = ArgumentCaptor.forClass(DisconnectListener.class); + verify(connection).addDisconnectListener(disconnectHandler.capture()); + + final Future result = factory.getOrCreateTelemetrySender("telemetry/tenant"); + assertFalse(result.isComplete()); + + // WHEN the underlying connection fails + disconnectHandler.getValue().onDisconnect(connection);; + + // THEN all creation requests are failed + assertTrue(result.failed()); + } +} diff --git a/client/src/test/java/org/eclipse/hono/client/impl/EventConsumerImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/EventConsumerImplTest.java index d0631817e6..41e0e12a67 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/EventConsumerImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/EventConsumerImplTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,15 +13,19 @@ package org.eclipse.hono.client.impl; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.function.BiConsumer; -import io.vertx.ext.unit.junit.Timeout; import org.apache.qpid.proton.amqp.messaging.Released; import org.apache.qpid.proton.amqp.transport.Source; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.config.ClientConfigProperties; +import org.eclipse.hono.client.HonoConnection; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -29,15 +33,13 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.Timeout; import io.vertx.ext.unit.junit.VertxUnitRunner; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonHelper; import io.vertx.proton.ProtonMessageHandler; @@ -59,7 +61,7 @@ public class EventConsumerImplTest { public Timeout timeout = Timeout.seconds(5); private Vertx vertx; - private Context context; + private HonoConnection connection; /** * Initializes fixture. @@ -67,7 +69,7 @@ public class EventConsumerImplTest { @Before public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx); } /** @@ -99,25 +101,22 @@ public void testCreateRegistersBiConsumerAsMessageHandler(final TestContext ctx) when(receiver.getRemoteSource()).thenReturn(source); when(receiver.getRemoteQoS()).thenReturn(ProtonQoS.AT_LEAST_ONCE); - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createReceiver(anyString())).thenReturn(receiver); + when(connection.createReceiver( + anyString(), + any(ProtonQoS.class), + any(ProtonMessageHandler.class), + any(Handler.class))).thenReturn(Future.succeededFuture(receiver)); final Async consumerCreation = ctx.async(); EventConsumerImpl.create( - context, - new ClientConfigProperties(), - con, + connection, "tenant", eventConsumer, - ctx.asyncAssertSuccess(rec -> consumerCreation.complete()), - remoteDetach -> {}); + remoteDetach -> {}).setHandler(ctx.asyncAssertSuccess(rec -> consumerCreation.complete())); final ArgumentCaptor messageHandler = ArgumentCaptor.forClass(ProtonMessageHandler.class); - verify(receiver).handler(messageHandler.capture()); - // wait for peer's attach frame - final ArgumentCaptor>> openHandlerCaptor = ArgumentCaptor.forClass(Handler.class); - verify(receiver).openHandler(openHandlerCaptor.capture()); - openHandlerCaptor.getValue().handle(Future.succeededFuture(receiver)); + verify(connection).createReceiver(eq("event/tenant"), eq(ProtonQoS.AT_LEAST_ONCE), + messageHandler.capture(), any(Handler.class)); consumerCreation.await(); // WHEN an event is received @@ -128,72 +127,4 @@ public void testCreateRegistersBiConsumerAsMessageHandler(final TestContext ctx) // THEN the message is released and settled verify(delivery).disposition(any(Released.class), eq(Boolean.TRUE)); } - - /** - * Verifies that the close handler on a consumer calls the registered close hook. - * - * @param ctx The test context. - */ - @Test - public void testCloseHandlerCallsCloseHook(final TestContext ctx) { - testHandlerCallsCloseHook(ctx, (receiver, captor) -> verify(receiver).closeHandler(captor.capture())); - } - - /** - * Verifies that the detach handler on a consumer calls the registered close hook. - * - * @param ctx The test context. - */ - @Test - public void testDetachHandlerCallsCloseHook(final TestContext ctx) { - testHandlerCallsCloseHook(ctx, (receiver, captor) -> verify(receiver).detachHandler(captor.capture())); - } - - @SuppressWarnings({ "unchecked" }) - private void testHandlerCallsCloseHook( - final TestContext ctx, - final BiConsumer>>> handlerCaptor) { - - // GIVEN an open event consumer - final BiConsumer eventConsumer = mock(BiConsumer.class); - final Source source = mock(Source.class); - when(source.getAddress()).thenReturn("source/address"); - final ProtonReceiver receiver = mock(ProtonReceiver.class); - when(receiver.isOpen()).thenReturn(Boolean.TRUE); - when(receiver.getSource()).thenReturn(source); - when(receiver.getRemoteSource()).thenReturn(source); - - final ProtonConnection con = mock(ProtonConnection.class); - when(con.createReceiver(anyString())).thenReturn(receiver); - - final Handler closeHook = mock(Handler.class); - final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Handler.class); - - final Async consumerCreation = ctx.async(); - EventConsumerImpl.create( - context, - new ClientConfigProperties(), - con, - "source/address", - eventConsumer, - ctx.asyncAssertSuccess(rec -> consumerCreation.complete()), - closeHook); - - // wait for peer's attach frame - final ArgumentCaptor>> openHandlerCaptor = ArgumentCaptor.forClass(Handler.class); - verify(receiver).openHandler(openHandlerCaptor.capture()); - openHandlerCaptor.getValue().handle(Future.succeededFuture(receiver)); - consumerCreation.await(); - - // WHEN the peer sends a detach frame - handlerCaptor.accept(receiver, captor); - captor.getValue().handle(Future.succeededFuture(receiver)); - - // THEN the close hook is called - verify(closeHook).handle(any()); - - // and the receiver link is closed - verify(receiver).close(); - verify(receiver).free(); - } } diff --git a/client/src/test/java/org/eclipse/hono/client/impl/EventSenderImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/EventSenderImplTest.java index 5e765583c6..e762bc9ff9 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/EventSenderImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/EventSenderImplTest.java @@ -15,17 +15,20 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.concurrent.atomic.AtomicReference; -import io.vertx.core.Context; -import io.vertx.core.Vertx; -import io.vertx.proton.ProtonSender; import org.apache.qpid.proton.amqp.messaging.Accepted; import org.apache.qpid.proton.amqp.messaging.Rejected; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.config.ClientConfigProperties; import org.junit.Before; import org.junit.Test; @@ -33,10 +36,12 @@ import io.vertx.core.Future; import io.vertx.core.Handler; +import io.vertx.core.Vertx; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonHelper; +import io.vertx.proton.ProtonSender; /** * Tests verifying behavior of {@link EventSenderImpl}. @@ -46,10 +51,9 @@ public class EventSenderImplTest { private Vertx vertx; - private Context context; private ProtonSender sender; - private ClientConfigProperties config; + private HonoConnection connection; /** * Sets up the fixture. @@ -58,10 +62,9 @@ public class EventSenderImplTest { public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); sender = HonoClientUnitTestHelper.mockProtonSender(); - config = new ClientConfigProperties(); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx, config); } /** @@ -76,7 +79,7 @@ public void testSendMessageWaitsForAcceptedOutcome(final TestContext ctx) { // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.FALSE); - final MessageSender messageSender = new EventSenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new EventSenderImpl(connection, sender, "tenant", "telemetry/tenant"); final AtomicReference> handlerRef = new AtomicReference<>(); doAnswer(invocation -> { handlerRef.set(invocation.getArgument(1)); @@ -111,7 +114,7 @@ public void testSendMessageFailsForRejectedOutcome(final TestContext ctx) { // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.FALSE); - final MessageSender messageSender = new EventSenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new EventSenderImpl(connection, sender, "tenant", "telemetry/tenant"); final AtomicReference> handlerRef = new AtomicReference<>(); doAnswer(invocation -> { handlerRef.set(invocation.getArgument(1)); @@ -144,7 +147,7 @@ public void testSendAndWaitForOutcomeFailsOnLackOfCredit() { // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.TRUE); - final MessageSender messageSender = new EventSenderImpl(config, sender, "tenant", "event/tenant", context); + final DownstreamSender messageSender = new EventSenderImpl(connection, sender, "tenant", "event/tenant"); // WHEN trying to send a message final Message event = ProtonHelper.message("event/tenant", "hello"); @@ -166,7 +169,7 @@ public void testSendMarksMessageAsDurable(final TestContext ctx) { // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.FALSE); - final MessageSender messageSender = new EventSenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new EventSenderImpl(connection, sender, "tenant", "telemetry/tenant"); when(sender.send(any(Message.class), any(Handler.class))).thenReturn(mock(ProtonDelivery.class)); // WHEN trying to send a message @@ -190,7 +193,7 @@ public void testSendAndWaitForOutcomeMarksMessageAsDurable(final TestContext ctx // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.FALSE); - final MessageSender messageSender = new EventSenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new EventSenderImpl(connection, sender, "tenant", "telemetry/tenant"); when(sender.send(any(Message.class), any(Handler.class))).thenReturn(mock(ProtonDelivery.class)); // WHEN trying to send a message diff --git a/client/src/test/java/org/eclipse/hono/client/impl/HonoClientUnitTestHelper.java b/client/src/test/java/org/eclipse/hono/client/impl/HonoClientUnitTestHelper.java index 7fff0e8d79..78b70becc7 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/HonoClientUnitTestHelper.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/HonoClientUnitTestHelper.java @@ -14,8 +14,11 @@ package org.eclipse.hono.client.impl; import io.opentracing.Span; +import io.opentracing.Tracer; import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.noop.NoopTracerFactory; import io.vertx.core.Context; +import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.proton.ProtonQoS; @@ -29,6 +32,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.eclipse.hono.client.HonoConnection; +import org.eclipse.hono.config.ClientConfigProperties; import org.mockito.Mockito; /** @@ -104,4 +109,43 @@ public static SpanBuilder mockSpanBuilder(final Span spanToCreate) { when(spanBuilder.start()).thenReturn(spanToCreate); return spanBuilder; } + + /** + * Creates a mocked Hono connection that returns a + * Noop Tracer. + *

+ * Invokes {@link #mockHonoConnection(Vertx, ClientConfigProperties)} + * with default {@link ClientConfigProperties}. + * + * @param vertx The vert.x instance to use. + * @return The connection. + */ + public static HonoConnection mockHonoConnection(final Vertx vertx) { + return mockHonoConnection(vertx, new ClientConfigProperties()); + } + + /** + * Creates a mocked Hono connection that returns a + * Noop Tracer. + * + * @param vertx The vert.x instance to use. + * @param props The client properties to use. + * @return The connection. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static HonoConnection mockHonoConnection(final Vertx vertx, final ClientConfigProperties props) { + + final Tracer tracer = NoopTracerFactory.create(); + final HonoConnection connection = mock(HonoConnection.class); + when(connection.getVertx()).thenReturn(vertx); + when(connection.getConfig()).thenReturn(props); + when(connection.getTracer()).thenReturn(tracer); + when(connection.executeOrRunOnContext(any(Handler.class))).then(invocation -> { + final Future result = Future.future(); + final Handler handler = invocation.getArgument(0); + handler.handle(result); + return result; + }); + return connection; + } } diff --git a/client/src/test/java/org/eclipse/hono/client/impl/HonoConnectionImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/HonoConnectionImplTest.java index 11ec0873b4..15c01435eb 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/HonoConnectionImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/HonoConnectionImplTest.java @@ -13,19 +13,30 @@ package org.eclipse.hono.client.impl; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.net.HttpURLConnection; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; import javax.security.sasl.AuthenticationException; +import org.apache.qpid.proton.amqp.transport.AmqpError; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.amqp.transport.Source; import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageSender; @@ -52,6 +63,10 @@ import io.vertx.ext.unit.junit.VertxUnitRunner; import io.vertx.proton.ProtonClientOptions; import io.vertx.proton.ProtonConnection; +import io.vertx.proton.ProtonMessageHandler; +import io.vertx.proton.ProtonQoS; +import io.vertx.proton.ProtonReceiver; +import io.vertx.proton.ProtonSender; import io.vertx.proton.sasl.SaslSystemException; /** @@ -82,6 +97,7 @@ public void setUp() { vertx = mock(Vertx.class); final Context context = HonoClientUnitTestHelper.mockContext(vertx); when(vertx.getOrCreateContext()).thenReturn(context); + // run any timer immediately when(vertx.setTimer(anyLong(), any(Handler.class))).thenAnswer(invocation -> { final Handler handler = invocation.getArgument(1); handler.handle(null); @@ -609,4 +625,271 @@ public void testClientDoesNotTriggerReconnectionAfterShutdown(final TestContext ctx.assertTrue(connectAttempts.get() == 3); })); } + + /** + * Verifies that the close handler set on a receiver link calls + * the close hook passed in when creating the receiver. + * + * @param ctx The test context. + */ + @Test + public void testCloseHandlerCallsCloseHook(final TestContext ctx) { + testHandlerCallsCloseHook(ctx, (receiver, captor) -> verify(receiver).closeHandler(captor.capture())); + } + + /** + * Verifies that the detach handler set on a receiver link calls + * the close hook passed in when creating the receiver. + * + * @param ctx The test context. + */ + @Test + public void testDetachHandlerCallsCloseHook(final TestContext ctx) { + testHandlerCallsCloseHook(ctx, (receiver, captor) -> verify(receiver).detachHandler(captor.capture())); + } + + @SuppressWarnings("unchecked") + private void testHandlerCallsCloseHook( + final TestContext ctx, + final BiConsumer>>> handlerCaptor) { + + // GIVEN an established connection + final Async connectAttempt = ctx.async(); + honoConnection.connect().setHandler(ctx.asyncAssertSuccess(ok -> connectAttempt.complete())); + connectAttempt.await(); + final Source source = mock(Source.class); + when(source.getAddress()).thenReturn("source/address"); + final ProtonReceiver receiver = mock(ProtonReceiver.class); + when(receiver.isOpen()).thenReturn(Boolean.TRUE); + when(receiver.getSource()).thenReturn(source); + when(receiver.getRemoteSource()).thenReturn(source); + when(con.createReceiver(anyString())).thenReturn(receiver); + + // WHEN creating a receiver link with a close hook + final Handler remoteCloseHook = mock(Handler.class); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Handler.class); + + final Async consumerCreation = ctx.async(); + honoConnection.createReceiver( + "source", + ProtonQoS.AT_LEAST_ONCE, + mock(ProtonMessageHandler.class), + remoteCloseHook).setHandler(ctx.asyncAssertSuccess(rec -> consumerCreation.complete())); + + // wait for peer's attach frame + final ArgumentCaptor>> openHandlerCaptor = ArgumentCaptor.forClass(Handler.class); + verify(receiver).openHandler(openHandlerCaptor.capture()); + openHandlerCaptor.getValue().handle(Future.succeededFuture(receiver)); + consumerCreation.await(); + + // WHEN the peer sends a detach frame + handlerCaptor.accept(receiver, captor); + captor.getValue().handle(Future.succeededFuture(receiver)); + + // THEN the close hook is called + verify(remoteCloseHook).handle(any()); + + // and the receiver link is closed + verify(receiver).close(); + verify(receiver).free(); + } + + /** + * Verifies that the attempt to create a receiver fails with a + * {@code ServiceInvocationException} if the remote peer refuses + * to open the link with an error condition. + * + * @param ctx The vert.x test context. + */ + @Test + public void testCreateReceiverFailsForErrorCondition(final TestContext ctx) { + + testCreateReceiverFails(ctx, () -> new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "unauthorized"), cause -> { + return cause instanceof ServiceInvocationException; + }); + } + + /** + * Verifies that the attempt to create a receiver fails with a + * {@code ClientErrorException} if the remote peer refuses + * to open the link without an error condition. + * + * @param ctx The vert.x test context. + */ + @Test + public void testCreateReceiverFailsWithoutErrorCondition(final TestContext ctx) { + + testCreateReceiverFails(ctx, () -> (ErrorCondition) null, cause -> { + return cause instanceof ClientErrorException && + ((ClientErrorException) cause).getErrorCode() == HttpURLConnection.HTTP_NOT_FOUND; + }); + } + + @SuppressWarnings({ "unchecked" }) + private void testCreateReceiverFails( + final TestContext ctx, + final Supplier errorSupplier, + final Predicate failureAssertion) { + + final ProtonReceiver receiver = mock(ProtonReceiver.class); + when(receiver.getRemoteCondition()).thenReturn(errorSupplier.get()); + when(con.createReceiver(anyString())).thenReturn(receiver); + final Handler remoteCloseHook = mock(Handler.class); + when(vertx.setTimer(anyLong(), any(Handler.class))).thenAnswer(invocation -> { + // do not run timers immediately + return 0L; + }); + + // GIVEN an established connection + final Async connectAttempt = ctx.async(); + honoConnection.connect().setHandler(ctx.asyncAssertSuccess(ok -> connectAttempt.complete())); + connectAttempt.await(); + + // WHEN creating a receiver + final Future result = honoConnection.createReceiver( + "source", ProtonQoS.AT_LEAST_ONCE, (delivery, msg) -> {}, remoteCloseHook); + + // THEN link establishment is failed after the configured amount of time + verify(vertx).setTimer(eq(props.getLinkEstablishmentTimeout()), any(Handler.class)); + // and when the peer rejects to open the link + final ArgumentCaptor>> openHandler = ArgumentCaptor.forClass(Handler.class); + verify(receiver).openHandler(openHandler.capture()); + openHandler.getValue().handle(Future.failedFuture(new IllegalStateException())); + // THEN the attempt is failed + assertTrue(result.failed()); + // with the expected error condition + assertTrue(failureAssertion.test(result.cause())); + verify(remoteCloseHook, never()).handle(anyString()); + } + + /** + * Verifies that the attempt to create a receiver fails with a + * {@code ServerErrorException} if the remote peer doesn't + * send its attach frame in time. + * + * @param ctx The vert.x test context. + */ + @SuppressWarnings("unchecked") + @Test + public void testCreateReceiverFailsOnTimeout(final TestContext ctx) { + + final ProtonReceiver receiver = mock(ProtonReceiver.class); + when(receiver.isOpen()).thenReturn(Boolean.TRUE); + when(con.createReceiver(anyString())).thenReturn(receiver); + final Handler remoteCloseHook = mock(Handler.class); + + // GIVEN an established connection + final Async connectAttempt = ctx.async(); + honoConnection.connect().setHandler(ctx.asyncAssertSuccess(ok -> connectAttempt.complete())); + connectAttempt.await(); + + final Future result = honoConnection.createReceiver( + "source", ProtonQoS.AT_LEAST_ONCE, (delivery, msg) -> {}, remoteCloseHook); + assertTrue(result.failed()); + assertThat(((ServerErrorException) result.cause()).getErrorCode(), is(HttpURLConnection.HTTP_UNAVAILABLE)); + verify(receiver).open(); + verify(receiver).close(); + verify(receiver).free(); + verify(remoteCloseHook, never()).handle(anyString()); + } + + /** + * Verifies that the attempt to create a sender fails with a + * {@code ServiceInvocationException} if the remote peer refuses + * to open the link with an error condition. + * + * @param ctx The vert.x test context. + */ + @Test + public void testCreateSenderFailsForErrorCondition(final TestContext ctx) { + + testCreateSenderFails( + ctx, + () -> (ErrorCondition) new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "unauthorized"), + cause -> { + return cause instanceof ServiceInvocationException; + }); + } + + /** + * Verifies that the attempt to create a sender fails with a + * {@code ClientErrorException} if the remote peer refuses + * to open the link without an error condition. + * + * @param ctx The vert.x test context. + */ + @Test + public void testCreateSenderFailsWithoutErrorCondition(final TestContext ctx) { + + testCreateSenderFails( + ctx, + () -> (ErrorCondition) null, + cause -> { + return cause instanceof ClientErrorException && + ((ClientErrorException) cause).getErrorCode() == HttpURLConnection.HTTP_NOT_FOUND; + }); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void testCreateSenderFails( + final TestContext ctx, + final Supplier errorSupplier, + final Predicate failureAssertion) { + + final ProtonSender sender = mock(ProtonSender.class); + when(sender.getRemoteCondition()).thenReturn(errorSupplier.get()); + when(con.createSender(anyString())).thenReturn(sender); + final Handler remoteCloseHook = mock(Handler.class); + when(vertx.setTimer(anyLong(), any(Handler.class))).thenAnswer(invocation -> { + // do not run timers immediately + return 0L; + }); + + // GIVEN an established connection + final Async connectAttempt = ctx.async(); + honoConnection.connect().setHandler(ctx.asyncAssertSuccess(ok -> connectAttempt.complete())); + connectAttempt.await(); + + final Future result = honoConnection.createSender( + "target", ProtonQoS.AT_LEAST_ONCE, remoteCloseHook); + + verify(vertx).setTimer(eq(props.getLinkEstablishmentTimeout()), any(Handler.class)); + final ArgumentCaptor openHandler = ArgumentCaptor.forClass(Handler.class); + verify(sender).openHandler(openHandler.capture()); + openHandler.getValue().handle(Future.failedFuture(new IllegalStateException())); + assertTrue(result.failed()); + assertTrue(failureAssertion.test(result.cause())); + verify(remoteCloseHook, never()).handle(anyString()); + } + + /** + * Verifies that the attempt to create a sender fails with a + * {@code ServerErrorException} if the remote peer doesn't + * send its attach frame in time. + * + * @param ctx The vert.x test context. + */ + @SuppressWarnings("unchecked") + @Test + public void testCreateSenderFailsOnTimeout(final TestContext ctx) { + + final ProtonSender sender = mock(ProtonSender.class); + when(sender.isOpen()).thenReturn(Boolean.TRUE); + when(con.createSender(anyString())).thenReturn(sender); + final Handler remoteCloseHook = mock(Handler.class); + + // GIVEN an established connection + final Async connectAttempt = ctx.async(); + honoConnection.connect().setHandler(ctx.asyncAssertSuccess(ok -> connectAttempt.complete())); + connectAttempt.await(); + + final Future result = honoConnection.createSender( + "target", ProtonQoS.AT_LEAST_ONCE, remoteCloseHook); + assertTrue(result.failed()); + assertThat(((ServerErrorException) result.cause()).getErrorCode(), is(HttpURLConnection.HTTP_UNAVAILABLE)); + verify(sender).open(); + verify(sender).close(); + verify(sender).free(); + verify(remoteCloseHook, never()).handle(anyString()); + } } diff --git a/client/src/test/java/org/eclipse/hono/client/impl/RegistrationClientImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/RegistrationClientImplTest.java index 8984d4c7db..cc895541ae 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/RegistrationClientImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/RegistrationClientImplTest.java @@ -17,7 +17,10 @@ import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.net.HttpURLConnection; import java.sql.Date; @@ -26,6 +29,7 @@ import org.apache.qpid.proton.message.Message; import org.eclipse.hono.cache.ExpiringValueCache; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RequestResponseClientConfigProperties; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; @@ -41,7 +45,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; @@ -68,10 +71,10 @@ public class RegistrationClientImplTest { public Timeout globalTimeout = Timeout.seconds(5); private Vertx vertx; - private Context context; private ProtonSender sender; private RegistrationClientImpl client; private ExpiringValueCache cache; + private HonoConnection connection; /** * Sets up the fixture. @@ -81,13 +84,13 @@ public class RegistrationClientImplTest { public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); final ProtonReceiver receiver = HonoClientUnitTestHelper.mockProtonReceiver(); sender = HonoClientUnitTestHelper.mockProtonSender(); cache = mock(ExpiringValueCache.class); final RequestResponseClientConfigProperties config = new RequestResponseClientConfigProperties(); - client = new RegistrationClientImpl(context, config, "tenant", sender, receiver); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx, config); + client = new RegistrationClientImpl(connection, "tenant", sender, receiver); } /** diff --git a/client/src/test/java/org/eclipse/hono/client/impl/TelemetrySenderImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/TelemetrySenderImplTest.java index deda5a0288..8f2a465e92 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/TelemetrySenderImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/TelemetrySenderImplTest.java @@ -14,20 +14,27 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.concurrent.atomic.AtomicReference; import org.apache.qpid.proton.amqp.messaging.Rejected; import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.client.MessageSender; +import org.eclipse.hono.client.DownstreamSender; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.config.ClientConfigProperties; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import io.opentracing.Span; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -45,10 +52,9 @@ public class TelemetrySenderImplTest { private Vertx vertx; - private Context context; private ProtonSender sender; - private ClientConfigProperties config; + private HonoConnection connection; /** * Sets up the fixture. @@ -57,10 +63,9 @@ public class TelemetrySenderImplTest { public void setUp() { vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); sender = HonoClientUnitTestHelper.mockProtonSender(); - config = new ClientConfigProperties(); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx, config); } /** @@ -75,7 +80,7 @@ public void testSendMessageDoesNotWaitForAcceptedOutcome(final TestContext ctx) // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.FALSE); - final MessageSender messageSender = new TelemetrySenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new TelemetrySenderImpl(connection, sender, "tenant", "telemetry/tenant"); final AtomicReference> handlerRef = new AtomicReference<>(); doAnswer(invocation -> { handlerRef.set(invocation.getArgument(1)); @@ -105,7 +110,7 @@ public void testSendAndWaitForOutcomeFailsOnLackOfCredit() { // GIVEN a sender that has credit when(sender.sendQueueFull()).thenReturn(Boolean.TRUE); - final MessageSender messageSender = new TelemetrySenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new TelemetrySenderImpl(connection, sender, "tenant", "telemetry/tenant"); // WHEN trying to send a message final Message event = ProtonHelper.message("telemetry/tenant", "hello"); @@ -133,7 +138,7 @@ public void testSendMessageFailsOnTimeout() { handler.handle(timerId); return timerId; }); - final MessageSender messageSender = new TelemetrySenderImpl(config, sender, "tenant", "telemetry/tenant", context); + final DownstreamSender messageSender = new TelemetrySenderImpl(connection, sender, "tenant", "telemetry/tenant"); // WHEN sending a message final Message message = mock(Message.class); diff --git a/client/src/test/java/org/eclipse/hono/client/impl/TenantClientImplTest.java b/client/src/test/java/org/eclipse/hono/client/impl/TenantClientImplTest.java index cb4c8e8f71..9cc151a176 100644 --- a/client/src/test/java/org/eclipse/hono/client/impl/TenantClientImplTest.java +++ b/client/src/test/java/org/eclipse/hono/client/impl/TenantClientImplTest.java @@ -34,6 +34,7 @@ import org.apache.qpid.proton.amqp.messaging.Rejected; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.cache.ExpiringValueCache; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RequestResponseClientConfigProperties; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.MessageHelper; @@ -54,7 +55,6 @@ import io.opentracing.Tracer; import io.opentracing.Tracer.SpanBuilder; import io.opentracing.tag.Tags; -import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; @@ -81,12 +81,12 @@ public class TenantClientImplTest { public Timeout globalTimeout = Timeout.seconds(5); private Vertx vertx; - private Context context; private ProtonSender sender; private TenantClientImpl client; private ExpiringValueCache> cache; private Tracer tracer; private Span span; + private HonoConnection connection; /** * Sets up the fixture. @@ -105,13 +105,15 @@ public void setUp() { when(tracer.buildSpan(anyString())).thenReturn(spanBuilder); vertx = mock(Vertx.class); - context = HonoClientUnitTestHelper.mockContext(vertx); final ProtonReceiver receiver = HonoClientUnitTestHelper.mockProtonReceiver(); sender = HonoClientUnitTestHelper.mockProtonSender(); - cache = mock(ExpiringValueCache.class); final RequestResponseClientConfigProperties config = new RequestResponseClientConfigProperties(); - client = new TenantClientImpl(context, config, tracer, sender, receiver); + connection = HonoClientUnitTestHelper.mockHonoConnection(vertx, config); + when(connection.getTracer()).thenReturn(tracer); + + cache = mock(ExpiringValueCache.class); + client = new TenantClientImpl(connection, sender, receiver); } /** diff --git a/jmeter/src/main/java/org/eclipse/hono/jmeter/client/HonoSender.java b/jmeter/src/main/java/org/eclipse/hono/jmeter/client/HonoSender.java index 7327a76681..6d941c7baa 100644 --- a/jmeter/src/main/java/org/eclipse/hono/jmeter/client/HonoSender.java +++ b/jmeter/src/main/java/org/eclipse/hono/jmeter/client/HonoSender.java @@ -25,11 +25,10 @@ import org.apache.jmeter.samplers.SampleResult; import org.apache.qpid.proton.message.Message; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ServiceInvocationException; -import org.eclipse.hono.client.impl.HonoConnectionImpl; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.jmeter.HonoSampler; import org.eclipse.hono.jmeter.HonoSenderSampler; @@ -81,7 +80,7 @@ public HonoSender(final HonoSenderSampler sampler) { honoProps.setPassword(sampler.getPwd()); honoProps.setTrustStorePath(sampler.getTrustStorePath()); honoProps.setReconnectAttempts(MAX_RECONNECT_ATTEMPTS); - downstreamSenderFactory = new HonoConnectionImpl(vertx, honoProps); + downstreamSenderFactory = DownstreamSenderFactory.create(HonoConnection.newConnection(vertx, honoProps)); } /** @@ -130,7 +129,7 @@ private Future connectToAmqpMessagingNetwork() { }); } - private Future getSender(final String endpoint, final String tenant) { + private Future getSender(final String endpoint, final String tenant) { if (endpoint.equals(HonoSampler.Endpoint.telemetry.toString())) { LOGGER.trace("getting telemetry sender for tenant [{}]", tenant); @@ -207,7 +206,7 @@ public void send(final SampleResult sampleResult, final String deviceId, final b final String endpoint = sampler.getEndpoint(); final String tenant = sampler.getTenant(); - final Future senderFuture = getSender(endpoint, tenant); + final Future senderFuture = getSender(endpoint, tenant); final CompletableFuture tracker = new CompletableFuture<>(); final Future deliveryTracker = Future.future(); deliveryTracker.setHandler(s -> { diff --git a/service-base/src/main/java/org/eclipse/hono/service/AbstractAdapterConfig.java b/service-base/src/main/java/org/eclipse/hono/service/AbstractAdapterConfig.java index 318cdc887a..068f3cf966 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/AbstractAdapterConfig.java +++ b/service-base/src/main/java/org/eclipse/hono/service/AbstractAdapterConfig.java @@ -19,6 +19,7 @@ import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CredentialsClientFactory; import org.eclipse.hono.client.DownstreamSenderFactory; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.RegistrationClientFactory; import org.eclipse.hono.client.RequestResponseClientConfigProperties; import org.eclipse.hono.client.TenantClientFactory; @@ -126,7 +127,7 @@ protected void customizeDownstreamSenderFactoryConfig(final ClientConfigProperti @Bean @Scope("prototype") public DownstreamSenderFactory downstreamSenderFactory() { - return new HonoConnectionImpl(vertx(), downstreamSenderFactoryConfig()); + return DownstreamSenderFactory.create(HonoConnection.newConnection(vertx(), downstreamSenderFactoryConfig())); } /** diff --git a/service-base/src/main/java/org/eclipse/hono/service/AbstractProtocolAdapterBase.java b/service-base/src/main/java/org/eclipse/hono/service/AbstractProtocolAdapterBase.java index e3c8e6a808..ee7a0e7f29 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/AbstractProtocolAdapterBase.java +++ b/service-base/src/main/java/org/eclipse/hono/service/AbstractProtocolAdapterBase.java @@ -20,7 +20,6 @@ import org.apache.qpid.proton.message.Message; import org.eclipse.hono.auth.Device; import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.client.CommandConsumer; import org.eclipse.hono.client.CommandConsumerFactory; import org.eclipse.hono.client.CommandContext; import org.eclipse.hono.client.CommandResponse; @@ -28,10 +27,10 @@ import org.eclipse.hono.client.ConnectionLifecycle; import org.eclipse.hono.client.CredentialsClientFactory; import org.eclipse.hono.client.DisconnectListener; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.MessageConsumer; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.ReconnectListener; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RegistrationClientFactory; @@ -738,7 +737,7 @@ protected final Future createCommandConsumer( * * @param tenantId The tenant that the device belongs to. * @param deviceId The identifier of the device. - * @deprecated This method will be removed in Hono 1.0. Use {@link CommandConsumer#close(Handler)} instead. + * @deprecated This method will be removed in Hono 1.0. Use {@link MessageConsumer#close(Handler)} instead. */ @Deprecated protected final void closeCommandConsumer(final String tenantId, final String deviceId) { @@ -808,7 +807,7 @@ protected final Future sendCommandResponse( * @param tenantId The tenant to send the telemetry data for. * @return The client. */ - protected final Future getTelemetrySender(final String tenantId) { + protected final Future getTelemetrySender(final String tenantId) { return getDownstreamSenderFactory().getOrCreateTelemetrySender(tenantId); } @@ -818,7 +817,7 @@ protected final Future getTelemetrySender(final String tenantId) * @param tenantId The tenant to send the events for. * @return The client. */ - protected final Future getEventSender(final String tenantId) { + protected final Future getEventSender(final String tenantId) { return getDownstreamSenderFactory().getOrCreateEventSender(tenantId); } @@ -1365,11 +1364,11 @@ protected final Future sendTtdEvent( final Future tokenTracker = getRegistrationAssertion(tenant, deviceId, authenticatedDevice, context); final Future tenantConfigTracker = getTenantConfiguration(tenant, context); - final Future senderTracker = getEventSender(tenant); + final Future senderTracker = getEventSender(tenant); return CompositeFuture.all(tokenTracker, tenantConfigTracker, senderTracker).compose(ok -> { if (tenantConfigTracker.result().isAdapterEnabled(getTypeName())) { - final MessageSender sender = senderTracker.result(); + final DownstreamSender sender = senderTracker.result(); final Message msg = newMessage( ResourceIdentifier.from(EventConstants.EVENT_ENDPOINT, tenant, deviceId), EventConstants.EVENT_ENDPOINT, @@ -1403,7 +1402,7 @@ protected boolean isPayloadOfIndicatedType(final Buffer payload, final String co } /** - * This method may be set as the close handler of the {@link CommandConsumer}. + * This method may be set as the close handler of the command consumer. *

* The implementation only logs that the link was closed and does not try to reopen it. Any other functionality must be * implemented by overwriting the method in a subclass. diff --git a/service-base/src/main/java/org/eclipse/hono/service/monitoring/AbstractMessageSenderConnectionEventProducer.java b/service-base/src/main/java/org/eclipse/hono/service/monitoring/AbstractMessageSenderConnectionEventProducer.java index 1c50d8470d..d4f56b9f50 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/monitoring/AbstractMessageSenderConnectionEventProducer.java +++ b/service-base/src/main/java/org/eclipse/hono/service/monitoring/AbstractMessageSenderConnectionEventProducer.java @@ -17,32 +17,30 @@ import java.util.function.BiFunction; import org.eclipse.hono.auth.Device; +import org.eclipse.hono.client.DownstreamSender; import org.eclipse.hono.client.DownstreamSenderFactory; -import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.util.EventConstants; import io.vertx.core.Future; import io.vertx.core.json.JsonObject; /** - * A connection event producer based on a {@link MessageSender}. + * A connection event producer based on a {@link DownstreamSender}. */ public abstract class AbstractMessageSenderConnectionEventProducer implements ConnectionEventProducer { /** - * The function to derive the {@link MessageSender} from the provided sender factory. + * The function to derive the sender from the provided sender factory. */ - private final BiFunction> messageSenderSource; + private final BiFunction> messageSenderSource; /** - * A {@link ConnectionEventProducer} which will send events using a provided {@link MessageSender}. + * Creates an event producer which will send events using a downstream sender. * - * @param messageSenderSource A function to get the {@link MessageSender} of the {@code messageSenderClient} which - * should be used for actually sending the events. + * @param messageSenderSource A function to get a sender for a tenant. */ protected AbstractMessageSenderConnectionEventProducer( - final BiFunction> messageSenderSource) { + final BiFunction> messageSenderSource) { Objects.requireNonNull(messageSenderSource); @@ -104,7 +102,7 @@ private Future sendNotificationEvent( }); } - private Future getOrCreateSender(final DownstreamSenderFactory messageSenderClient, final String tenant) { + private Future getOrCreateSender(final DownstreamSenderFactory messageSenderClient, final String tenant) { return messageSenderSource.apply(messageSenderClient, tenant); } } diff --git a/site/content/release-notes.md b/site/content/release-notes.md index 0ba1616eaa..fa3cc730d0 100644 --- a/site/content/release-notes.md +++ b/site/content/release-notes.md @@ -25,49 +25,44 @@ title = "Release Notes" ### API Changes -* The optional operations defined by the Tenant, Device Registration and Credentials API - have been deprecated. They will be removed from Hono 1.0 altogether. - A new HTTP based API will be defined instead which can then be used to *manage* the content - of a device registry. -* The `org.eclipse.hono.client.MessageSender` interface's *send* methods have been changed - to no longer accept a *registration assertion token* which became obsolete with the removal - of the *Hono Messaging* component. The *isRegistrationAssertionRequired* method has also been - removed from the interface. -* Consequently, the `org.eclipse.hono.service.AbstractProtocolAdapterBase` class's - *newMessage* and *addProperties* methods no longer require a boolean parameter indicating - whether to include the assertion token in the message being created/amended. - Custom protocol adapters should simply omit the corresponding parameter. -* The `org.eclipse.hono.service.AbstractProtocolAdapterBase` class now uses - `org.eclipse.hono.client.CommandConsumerFactory` instead of - `org.eclipse.hono.client.CommandConnection` for creating - `org.eclipse.hono.client.CommandConsumer` instances. - The *setCommandConnection* and *getCommandConnection* methods have been - renamed to *setCommandConsumerFactory* and *getCommandConsumerFactory* - correspondingly. -* The `org.eclipse.hono.service.AbstractProtocolAdapterBase` class now uses - `org.eclipse.hono.client.TenantClientFactory` instead of - `org.eclipse.hono.client.HonoClient` for creating `org.eclipse.hono.client.TenantClient` - instances. - The *setTenantServiceClient* and *getTenantServiceClient* methods have been - renamed to *setTenantClientFactory* and *getTenantClientFactory* correspondingly. -* The `org.eclipse.hono.service.AbstractProtocolAdapterBase` class now uses - `org.eclipse.hono.client.RegistrationClientFactory` instead of - `org.eclipse.hono.client.HonoClient` for creating - `org.eclipse.hono.client.RegistrationClient` instances. - The *setRegistrationServiceClient* and *getRegistrationServiceClient* methods have been - renamed to *setRegistrationClientFactory* and *getRegistrationClientFactory* correspondingly. -* The `org.eclipse.hono.service.AbstractProtocolAdapterBase` class now uses - `org.eclipse.hono.client.CredentialsClientFactory` instead of - `org.eclipse.hono.client.HonoClient` for creating - `org.eclipse.hono.client.CredentialsClient` instances. - The *setCredentialsServiceClient* and *getCredentialsServiceClient* methods have been - renamed to *setCredentialsClientFactory* and *getCredentialsClientFactory* correspondingly. -* The `org.eclipse.hono.service.AbstractProtocolAdapterBase` class now uses - `org.eclipse.hono.client.DownstreamSendertFactory` instead of - `org.eclipse.hono.client.HonoClient` for creating - `org.eclipse.hono.client.MessageSender` instances. - The *setHonoMessagingClient* and *getHonoMessagingClient* methods have been - renamed to *setDownstreamSenderFactory* and *getDownstreamSenderFactory* correspondingly. +* Several changes have been made to the `org.eclipse.hono.client.MessageSender` interface: + * The *send* methods have been changed to no longer accept a *registration assertion token* + which became obsolete with the removal of the *Hono Messaging* component. + * The *isRegistrationAssertionRequired* method has been removed from the interface. + * All *send* method variants which accept specific message parameters have been moved into + the new `org.eclipse.hono.client.DownstreamSender` interface which extends + `MessageSender`. +* Several changes have been made to the `org.eclipse.hono.service.AbstractProtocolAdapterBase` + class: + * The *newMessage* and *addProperties* methods no longer require a boolean parameter indicating + whether to include the assertion token in the message being created/amended. + Custom protocol adapters should simply omit the corresponding parameter. + * The base class now uses `org.eclipse.hono.client.CommandConsumerFactory` instead of + `org.eclipse.hono.client.CommandConnection` for creating + `org.eclipse.hono.client.CommandConsumer` instances. + The *setCommandConnection* and *getCommandConnection* methods have been + renamed to *setCommandConsumerFactory* and *getCommandConsumerFactory* + correspondingly. + * The base class now uses `org.eclipse.hono.client.TenantClientFactory` instead of + `org.eclipse.hono.client.HonoClient` for creating `org.eclipse.hono.client.TenantClient` + instances. + The *setTenantServiceClient* and *getTenantServiceClient* methods have been + renamed to *setTenantClientFactory* and *getTenantClientFactory* correspondingly. + * The base class now uses `org.eclipse.hono.client.RegistrationClientFactory` instead of + `org.eclipse.hono.client.HonoClient` for creating + `org.eclipse.hono.client.RegistrationClient` instances. + The *setRegistrationServiceClient* and *getRegistrationServiceClient* methods have been + renamed to *setRegistrationClientFactory* and *getRegistrationClientFactory* correspondingly. + * The base class now uses `org.eclipse.hono.client.CredentialsClientFactory` instead of + `org.eclipse.hono.client.HonoClient` for creating + `org.eclipse.hono.client.CredentialsClient` instances. + The *setCredentialsServiceClient* and *getCredentialsServiceClient* methods have been + renamed to *setCredentialsClientFactory* and *getCredentialsClientFactory* correspondingly. + * The base class now uses `org.eclipse.hono.client.DownstreamSendertFactory` instead of + `org.eclipse.hono.client.HonoClient` for creating + `org.eclipse.hono.client.DownstreamSender` instances. + The *setHonoMessagingClient* and *getHonoMessagingClient* methods have been + renamed to *setDownstreamSenderFactory* and *getDownstreamSenderFactory* correspondingly. * The `org.eclipse.hono.service.auth.device.UsernamePasswordAuthProvider` and the `org.eclipse.hono.service.auth.device.X509AuthProvider` now accept a `org.eclipse.hono.client.CredentialsClientFactory` instead of a @@ -81,6 +76,13 @@ title = "Release Notes" `org.eclipse.hono.client.HonoConnection` to better reflect its sole responsibility for establishing (and maintaining) the connection to a Hono service endpoint. +### Depreciations + +* The optional operations defined by the Tenant, Device Registration and Credentials API + have been deprecated. They will be removed from Hono 1.0 altogether. + A new HTTP based API will be defined instead which can then be used to *manage* the content + of a device registry. + ## 1.0-M1 ### New Features diff --git a/tests/src/test/java/org/eclipse/hono/tests/GenericMessageSender.java b/tests/src/test/java/org/eclipse/hono/tests/GenericMessageSender.java index d1c1977685..7da5cb446f 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/GenericMessageSender.java +++ b/tests/src/test/java/org/eclipse/hono/tests/GenericMessageSender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2018, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -21,14 +21,12 @@ import org.apache.qpid.proton.amqp.messaging.Released; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.ClientErrorException; +import org.eclipse.hono.client.HonoConnection; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.impl.AbstractHonoClient; -import org.eclipse.hono.config.ClientConfigProperties; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; import io.vertx.proton.ProtonQoS; import io.vertx.proton.ProtonSender; @@ -43,24 +41,20 @@ public class GenericMessageSender extends AbstractHonoClient { /** * Creates a sender. * - * @param config The configuration properties to use. + * @param con The connection to the Hono server. * @param sender The sender link to send messages over. - * @param context The vert.x context to use for sending the messages. */ public GenericMessageSender( - final Context context, - final ClientConfigProperties config, + final HonoConnection con, final ProtonSender sender) { - super(context, config); + super(con); this.sender = sender; } /** * Creates a new sender for sending messages. * - * @param context The vert.x context to run all interactions with the server on. - * @param clientConfig The configuration properties to use. * @param con The connection to the peer. * @param targetAddress The target address of the sender. * @param closeHook The handler to invoke when the Hono server closes the sender. The sender's @@ -69,19 +63,15 @@ public GenericMessageSender( * @throws NullPointerException if any of context, connection, tenant or handler is {@code null}. */ public static Future create( - final Context context, - final ClientConfigProperties clientConfig, - final ProtonConnection con, + final HonoConnection con, final String targetAddress, final Handler closeHook) { - Objects.requireNonNull(context); Objects.requireNonNull(con); Objects.requireNonNull(targetAddress); - Objects.requireNonNull(clientConfig); - return createSender(context, clientConfig, con, targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook).map(sender -> { - return new GenericMessageSender(context, clientConfig, sender); + return con.createSender(targetAddress, ProtonQoS.AT_LEAST_ONCE, closeHook).map(sender -> { + return new GenericMessageSender(con, sender); }); } @@ -110,7 +100,7 @@ public void close() { */ public Future sendAndWaitForOutcome(final Message message) { final Future result = Future.future(); - context.runOnContext(go -> { + connection.executeOrRunOnContext(go -> { if (sender.isOpen() && sender.getCredit() > 0) { sender.send(message, updatedDelivery -> { if (updatedDelivery.getRemoteState() instanceof Accepted) { diff --git a/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestHonoClient.java b/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestHonoClient.java index 9f83cc54e3..21c2382f19 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestHonoClient.java +++ b/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestHonoClient.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Contributors to the Eclipse Foundation + * Copyright (c) 2018, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -51,9 +51,7 @@ public Future createGenericMessageSender(final String targ Objects.requireNonNull(targetAddress); return GenericMessageSender.create( - context, - clientConfigProperties, - connection, + this, targetAddress, s -> {}); }