From 59ac22d492773b434c692ec6e1c31c4bc2b5d90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Mon, 23 Sep 2019 10:02:20 +0200 Subject: [PATCH] pluggable HTTP protocols, add netty-http-rx, adapted from https://github.com/ReactiveX/RxNetty --- gradle.properties | 9 +- netty-http-client-api/build.gradle | 3 + .../xbib/netty/http/client/api}/BackOff.java | 2 +- .../http/client/api}/ExceptionListener.java | 2 +- .../client/api/HttpChannelInitializer.java | 10 + .../org/xbib/netty/http/client/api}/Pool.java | 2 +- .../http/client/api/ProtocolProvider.java | 10 + .../xbib/netty/http/client/api}/Request.java | 141 ++- .../http/client/api}/ResponseListener.java | 2 +- .../netty/http/client/api}/Transport.java | 3 +- .../netty/http/client/api}/UserAgent.java | 2 +- .../netty/http/client/api/package-info.java | 4 + .../netty/http/client/rest/RestClient.java | 4 +- netty-http-client/build.gradle | 2 +- .../org/xbib/netty/http/client/Client.java | 178 ++-- .../xbib/netty/http/client/ClientConfig.java | 6 +- .../xbib/netty/http/client/Http1Provider.java | 23 + .../xbib/netty/http/client/Http2Provider.java | 23 + ...izer.java => Http1ChannelInitializer.java} | 15 +- .../handler/http/HttpResponseHandler.java | 2 +- .../http2/Http2ChannelInitializer.java | 13 +- .../handler/http2/Http2ResponseHandler.java | 2 +- .../http/client/listener/CookieListener.java | 9 - .../http/client/listener/StatusListener.java | 10 - .../http/client/listener/package-info.java | 4 - .../http/client/pool/BoundedChannelPool.java | 7 +- .../http/client/retry/ExponentialBackOff.java | 2 + .../http/client/transport/BaseTransport.java | 57 +- ...HttpTransport.java => Http1Transport.java} | 54 +- .../http/client/transport/Http2Transport.java | 37 +- ...bib.netty.http.client.api.ProtocolProvider | 2 + .../client/test/CompletableFutureTest.java | 2 +- .../http/client/test/RequestBuilderTest.java | 2 +- .../http/client/test/SecureHttpTest.java | 83 -- .../http/client/test/akamai/AkamaiTest.java | 6 +- .../test/{ => conscrypt}/ConscryptTest.java | 9 +- .../test/cookie/ClientCookieDecoderTest.java | 2 +- .../{ => cookie}/CookieSetterHttpBinTest.java | 21 +- .../http/client/test/http1/GoogleTest.java | 100 ++ .../client/test/{ => http1}/Http1Test.java | 30 +- .../client/test/{ => http1}/XbibTest.java | 28 +- .../http/client/test/http2/GoogleTest.java | 30 + .../Http2PushTest.java | 8 +- .../http/client/test/pool/EpollTest.java | 2 +- .../netty/http/client/test/pool/NioTest.java | 2 +- .../netty/http/client/test/pool/PoolTest.java | 10 +- .../client/test/pool/PooledClientTest.java | 8 +- .../test/retry/ExponentialBackOffTest.java | 2 +- .../http/client/test/retry/MockBackOff.java | 4 +- .../client/test/retry/MockBackOffTest.java | 2 +- .../http/client/test/webtide/WebtideTest.java | 17 +- .../http/common/DefaultHttpResponse.java | 13 +- .../xbib/netty/http/common/HttpAddress.java | 18 +- .../netty/http/common/HttpParameters.java | 70 +- .../xbib/netty/http/common/HttpResponse.java | 3 + .../netty/http/common/util/DateTimeUtil.java | 10 +- netty-http-rx/NOTICE.txt | 5 + netty-http-rx/build.gradle | 7 + .../java/io/reactivex/netty/HandlerNames.java | 46 + .../main/java/io/reactivex/netty/RxNetty.java | 97 ++ .../AbstractConnectionToChannelBridge.java | 392 ++++++++ .../netty/channel/AllocatingTransformer.java | 72 ++ .../netty/channel/AppendTransformerEvent.java | 40 + .../netty/channel/AutoReleaseOperator.java | 49 + .../channel/BackpressureManagingHandler.java | 710 +++++++++++++ .../netty/channel/BytesInspector.java | 99 ++ .../netty/channel/ChannelOperations.java | 285 ++++++ .../netty/channel/ChannelSubscriberEvent.java | 45 + .../reactivex/netty/channel/Connection.java | 316 ++++++ .../ConnectionCreationFailedEvent.java | 45 + .../netty/channel/ConnectionImpl.java | 240 +++++ .../ConnectionInputSubscriberEvent.java | 64 ++ ...ConnectionInputSubscriberReplaceEvent.java | 33 + .../ConnectionInputSubscriberResetEvent.java | 26 + .../channel/ConnectionSubscriberEvent.java | 44 + .../netty/channel/ContentSource.java | 96 ++ .../channel/DefaultChannelOperations.java | 368 +++++++ .../channel/DetachedChannelPipeline.java | 387 +++++++ .../channel/DisposableContentSource.java | 134 +++ .../netty/channel/EmitConnectionEvent.java | 40 + .../netty/channel/FlushSelectorOperator.java | 57 ++ .../netty/channel/MarkAwarePipeline.java | 471 +++++++++ .../SubscriberToChannelFutureBridge.java | 70 ++ .../netty/channel/WriteTransformations.java | 98 ++ .../netty/channel/WriteTransformer.java | 78 ++ .../events/ConnectionEventListener.java | 124 +++ .../events/ConnectionEventPublisher.java | 238 +++++ .../netty/client/ChannelProvider.java | 27 + .../netty/client/ChannelProviderFactory.java | 29 + .../ClientConnectionToChannelBridge.java | 202 ++++ .../reactivex/netty/client/ClientState.java | 505 ++++++++++ .../netty/client/ConnectionProvider.java | 38 + .../client/ConnectionProviderFactory.java | 26 + .../netty/client/ConnectionRequest.java | 42 + .../java/io/reactivex/netty/client/Host.java | 70 ++ .../reactivex/netty/client/HostConnector.java | 110 ++ .../client/events/ClientEventListener.java | 111 +++ .../SingleHostConnectionProvider.java | 51 + .../loadbalancer/AbstractP2CStrategy.java | 113 +++ .../client/loadbalancer/HostCollector.java | 50 + .../netty/client/loadbalancer/HostHolder.java | 65 ++ .../loadbalancer/LoadBalancerFactory.java | 94 ++ .../loadbalancer/LoadBalancingStrategy.java | 31 + .../loadbalancer/NoBufferHostCollector.java | 74 ++ .../NoHostsAvailableException.java | 55 + ...mpositePoolLimitDeterminationStrategy.java | 75 ++ .../pool/FIFOIdleConnectionsHolder.java | 75 ++ .../client/pool/IdleConnectionsHolder.java | 72 ++ .../pool/MaxConnectionsBasedStrategy.java | 107 ++ .../netty/client/pool/PoolConfig.java | 93 ++ .../client/pool/PoolExhaustedException.java | 37 + .../pool/PoolLimitDeterminationStrategy.java | 48 + .../netty/client/pool/PooledConnection.java | 373 +++++++ .../client/pool/PooledConnectionProvider.java | 63 ++ .../pool/PooledConnectionProviderImpl.java | 431 ++++++++ .../pool/PreferCurrentEventLoopHolder.java | 168 ++++ .../SingleHostPoolingProviderFactory.java | 60 ++ ...boundedPoolLimitDeterminationStrategy.java | 41 + .../java/io/reactivex/netty/events/Clock.java | 140 +++ .../netty/events/EventAttributeKeys.java | 39 + .../reactivex/netty/events/EventListener.java | 105 ++ .../netty/events/EventPublisher.java | 32 + .../reactivex/netty/events/EventSource.java | 34 + .../events/ListenerInvocationException.java | 65 ++ .../netty/events/ListenersHolder.java | 406 ++++++++ .../events/internal/SafeEventListener.java | 26 + .../internal/ExecuteInEventloopAction.java | 39 + .../internal/InternalReadTimeoutHandler.java | 247 +++++ .../netty/internal/VoidToAnythingCast.java | 33 + .../netty/protocol/http/CookiesHolder.java | 95 ++ .../netty/protocol/http/HttpHandlerNames.java | 51 + .../netty/protocol/http/TrailingHeaders.java | 116 +++ .../protocol/http/client/HttpClient.java | 363 +++++++ .../protocol/http/client/HttpClientImpl.java | 312 ++++++ .../client/HttpClientInterceptorChain.java | 82 ++ .../HttpClientInterceptorChainImpl.java | 56 ++ .../http/client/HttpClientRequest.java | 575 +++++++++++ .../http/client/HttpClientResponse.java | 410 ++++++++ .../http/client/HttpRedirectException.java | 64 ++ .../http/client/InterceptingHttpClient.java | 136 +++ .../client/InterceptingHttpClientImpl.java | 100 ++ .../protocol/http/client/Interceptor.java | 37 + .../protocol/http/client/RequestProvider.java | 41 + .../http/client/TransformingInterceptor.java | 37 + .../events/HttpClientEventPublisher.java | 343 +++++++ .../events/HttpClientEventsListener.java | 99 ++ .../events/SafeHttpClientEventsListener.java | 306 ++++++ .../client/internal/HttpChannelProvider.java | 54 + .../internal/HttpChannelProviderFactory.java | 55 + .../internal/HttpClientRequestImpl.java | 542 ++++++++++ .../internal/HttpClientResponseImpl.java | 353 +++++++ .../HttpClientToConnectionBridge.java | 193 ++++ .../http/client/internal/RawRequest.java | 244 +++++ .../http/client/internal/Redirector.java | 201 ++++ .../client/internal/UnusableConnection.java | 194 ++++ .../loadbalancer/EWMABasedP2CStrategy.java | 111 +++ .../AbstractHttpConnectionBridge.java | 555 +++++++++++ .../internal/HttpContentSubscriberEvent.java | 32 + .../http/internal/HttpMessageFormatter.java | 72 ++ .../http/internal/OperatorTrailer.java | 77 ++ .../http/internal/UnsafeEmptySubscriber.java | 52 + .../http/server/ContentWriterImpl.java | 218 ++++ .../http/server/FailedContentWriter.java | 126 +++ .../http/server/HttpConnectionHandler.java | 207 ++++ .../protocol/http/server/HttpServer.java | 445 +++++++++ .../protocol/http/server/HttpServerImpl.java | 228 +++++ .../server/HttpServerInterceptorChain.java | 245 +++++ .../http/server/HttpServerRequest.java | 441 ++++++++ .../http/server/HttpServerRequestImpl.java | 307 ++++++ .../http/server/HttpServerResponse.java | 388 ++++++++ .../http/server/HttpServerResponseImpl.java | 432 ++++++++ .../server/HttpServerToConnectionBridge.java | 180 ++++ .../protocol/http/server/RequestHandler.java | 39 + .../http/server/ResponseContentWriter.java | 334 +++++++ .../protocol/http/server/UriInfoHolder.java | 79 ++ .../events/HttpServerEventPublisher.java | 304 ++++++ .../events/HttpServerEventsListener.java | 107 ++ .../events/SafeHttpServerEventsListener.java | 263 +++++ .../protocol/http/sse/ServerSentEvent.java | 329 ++++++ .../sse/client/ServerSentEventDecoder.java | 355 +++++++ .../sse/server/ServerSentEventEncoder.java | 123 +++ .../util/HttpContentStringLineDecoder.java | 53 + .../protocol/http/ws/WebSocketConnection.java | 159 +++ ...peratorCacheSingleWebsocketConnection.java | 142 +++ .../http/ws/client/WebSocketRequest.java | 51 + .../http/ws/client/WebSocketResponse.java | 43 + .../http/ws/client/Ws7To13UpgradeHandler.java | 149 +++ .../client/internal/WebSocketRequestImpl.java | 127 +++ .../internal/WebSocketResponseImpl.java | 268 +++++ .../protocol/http/ws/internal/WsUtils.java | 93 ++ .../http/ws/server/V7to13Handshaker.java | 162 +++ .../http/ws/server/WebSocketHandler.java | 36 + .../http/ws/server/WebSocketHandlers.java | 54 + .../http/ws/server/WebSocketHandshaker.java | 152 +++ .../http/ws/server/Ws7To13UpgradeHandler.java | 198 ++++ .../netty/protocol/tcp/TcpHandlerNames.java | 41 + .../tcp/client/ConnectionRequestImpl.java | 34 + .../tcp/client/InterceptingTcpClient.java | 41 + .../tcp/client/InterceptingTcpClientImpl.java | 53 + .../protocol/tcp/client/Interceptor.java | 39 + .../netty/protocol/tcp/client/TcpClient.java | 361 +++++++ .../protocol/tcp/client/TcpClientImpl.java | 361 +++++++ .../tcp/client/TcpClientInterceptorChain.java | 82 ++ .../client/TcpClientInterceptorChainImpl.java | 58 ++ .../tcp/client/TransformingInterceptor.java | 41 + .../events/SafeTcpClientEventListener.java | 237 +++++ .../client/events/TcpClientEventListener.java | 27 + .../events/TcpClientEventPublisher.java | 280 ++++++ .../client/internal/TcpChannelProvider.java | 58 ++ .../internal/TcpChannelProviderFactory.java | 45 + .../tcp/server/ConnectionHandler.java | 38 + .../netty/protocol/tcp/server/TcpServer.java | 442 ++++++++ .../TcpServerConnectionToChannelBridge.java | 157 +++ .../protocol/tcp/server/TcpServerImpl.java | 281 ++++++ .../tcp/server/TcpServerInterceptorChain.java | 257 +++++ .../protocol/tcp/server/TcpServerState.java | 155 +++ .../events/SafeTcpServerEventListener.java | 187 ++++ .../server/events/TcpServerEventListener.java | 60 ++ .../events/TcpServerEventPublisher.java | 199 ++++ .../reactivex/netty/server/ServerState.java | 194 ++++ .../reactivex/netty/ssl/DefaultSslCodec.java | 66 ++ .../java/io/reactivex/netty/ssl/SslCodec.java | 91 ++ .../threads/PreferCurrentEventLoopGroup.java | 215 ++++ .../netty/threads/RxDefaultThreadFactory.java | 26 + .../netty/threads/RxEventLoopProvider.java | 83 ++ .../threads/RxJavaEventloopScheduler.java | 130 +++ .../RxJavaNettyBasedSchedulersHook.java | 50 + .../netty/threads/SingleNioLoopProvider.java | 147 +++ .../io/reactivex/netty/util/CollectBytes.java | 108 ++ .../io/reactivex/netty/util/LineReader.java | 135 +++ .../netty/util/LoggingHandlerFactory.java | 72 ++ .../netty/util/StringLineDecoder.java | 42 + .../netty/util/UnicastBufferingSubject.java | 286 ++++++ ...AbstractConnectionToChannelBridgeTest.java | 310 ++++++ .../BackpressureManagingHandlerTest.java | 458 +++++++++ .../channel/BytesWriteInterceptorTest.java | 425 ++++++++ .../netty/channel/ConnectionImplTest.java | 199 ++++ .../netty/channel/ContentSourceRule.java | 100 ++ .../netty/channel/ContentSourceTest.java | 69 ++ .../channel/DefaultChannelOperationsTest.java | 393 ++++++++ .../channel/DetachedChannelPipelineTest.java | 234 +++++ .../netty/channel/ReadProducerTest.java | 149 +++ .../SubscriberToChannelFutureBridgeTest.java | 111 +++ .../channel/WriteStreamSubscriberTest.java | 285 ++++++ .../netty/channel/WriteTransformerTest.java | 149 +++ .../events/ConnectionEventPublisherTest.java | 196 ++++ .../netty/client/ClientStateTest.java | 306 ++++++ .../reactivex/netty/client/SslClientTest.java | 68 ++ .../loadbalancer/AbstractP2CStrategyTest.java | 181 ++++ .../loadbalancer/LoadBalancerFactoryTest.java | 196 ++++ .../pool/FIFOIdleConnectionsHolderTest.java | 159 +++ .../client/pool/PoolLimitStrategyTest.java | 119 +++ .../PooledConnectionProviderImplTest.java | 382 +++++++ .../PreferCurrentEventLoopHolderTest.java | 226 +++++ .../netty/events/ListenersHolderRule.java | 81 ++ .../netty/events/ListenersHolderTest.java | 564 +++++++++++ .../protocol/http/CookiesHolderTest.java | 88 ++ .../http/client/EventListenerTest.java | 143 +++ .../http/client/HttpClientPoolTest.java | 186 ++++ .../protocol/http/client/HttpClientRule.java | 314 ++++++ .../protocol/http/client/HttpClientTest.java | 259 +++++ .../http/client/HttpRedirectTest.java | 274 +++++ .../http/client/RedirectOperatorTest.java | 166 ++++ .../events/HttpClientEventPublisherTest.java | 309 ++++++ .../events/HttpClientEventsListenerImpl.java | 274 +++++ .../client/events/HttpClientEventsTest.java | 66 ++ .../internal/HttpClientRequestImplTest.java | 940 ++++++++++++++++++ .../AbstractHttpConnectionBridgeTest.java | 522 ++++++++++ .../protocol/http/server/CookieTest.java | 84 ++ .../protocol/http/server/Http10Test.java | 70 ++ .../http/server/HttpEndToEndTest.java | 63 ++ .../http/server/HttpServerRequestUriTest.java | 87 ++ .../protocol/http/server/HttpServerRule.java | 237 +++++ .../protocol/http/server/HttpServerTest.java | 108 ++ .../HttpServerToConnectionBridgeTest.java | 45 + .../protocol/http/server/PipeliningTest.java | 92 ++ .../events/HttpServerEventPublisherTest.java | 284 ++++++ .../events/HttpServerEventsListenerImpl.java | 233 +++++ .../http/sse/ServerSentEventEndToEndTest.java | 128 +++ .../netty/protocol/http/sse/SseTestUtil.java | 74 ++ .../client/ServerSentEventDecoderTest.java | 259 +++++ .../server/ServerSentEventEncoderTest.java | 139 +++ ...torCacheSingleWebsocketConnectionTest.java | 230 +++++ .../WSEagerInputSubscriptionHandlerTest.java | 77 ++ .../tcp/client/EventListenerTest.java | 116 +++ .../client/MockTcpClientEventListener.java | 192 ++++ .../client/PoolingWithRealChannelTest.java | 229 +++++ .../tcp/client/TcpClientImplTest.java | 217 ++++ .../protocol/tcp/client/TcpClientRule.java | 156 +++ .../events/TcpClientEventPublisherTest.java | 267 +++++ .../client/events/TcpClientEventsTest.java | 51 + ...UnexpectedConnectionHandlerErrorsTest.java | 118 +++ .../events/TcpServerEventPublisherTest.java | 204 ++++ .../netty/test/util/FlushSelector.java | 38 + .../netty/test/util/InboundRequestFeeder.java | 105 ++ .../test/util/MockClientEventListener.java | 235 +++++ .../util/MockConnectionEventListener.java | 200 ++++ .../netty/test/util/MockEventListener.java | 164 +++ .../netty/test/util/MockEventPublisher.java | 58 ++ .../MockPoolLimitDeterminationStrategy.java | 59 ++ .../netty/test/util/MockProducer.java | 58 ++ .../test/util/MockTcpServerEventListener.java | 169 ++++ .../test/util/TcpConnectionRequestMock.java | 34 + .../util/TrackableMetricEventsListener.java | 127 +++ .../EmbeddedChannelPipelineDelegate.java | 438 ++++++++ .../embedded/EmbeddedChannelProvider.java | 103 ++ .../embedded/EmbeddedChannelWithFeeder.java | 53 + .../embedded/EmbeddedConnectionProvider.java | 64 ++ .../PreferCurrentEventLoopGroupTest.java | 45 + .../threads/RxJavaEventloopSchedulerTest.java | 152 +++ .../netty/util/CollectBytesTest.java | 211 ++++ .../reactivex/netty/util/LineReaderTest.java | 127 +++ .../util/UnicastBufferingSubjectTest.java | 195 ++++ .../src/test/resources/log4j.properties | 21 + netty-http-server-api/build.gradle | 3 + .../xbib/netty/http/server/api}/Endpoint.java | 9 +- .../http/server/api/EndpointDescriptor.java | 6 + .../http/server/api/EndpointDispatcher.java | 9 + .../xbib/netty/http/server/api/Filter.java | 9 +- .../server/api/HttpChannelInitializer.java | 10 + .../http/server/api/ProtocolProvider.java | 10 + .../xbib/netty/http/server/api}/Resource.java | 2 +- .../netty/http/server/api}/ServerRequest.java | 5 +- .../http/server/api}/ServerResponse.java | 4 +- .../netty/http/server/api}/Transport.java | 2 +- .../http/server/api}/annotation/Endpoint.java | 6 +- netty-http-server/build.gradle | 1 + .../org/xbib/netty/http/server/Domain.java | 22 +- .../xbib/netty/http/server/Http1Provider.java | 23 + .../xbib/netty/http/server/Http2Provider.java | 23 + .../org/xbib/netty/http/server/Server.java | 253 +++-- .../xbib/netty/http/server/ServerConfig.java | 77 +- .../server/cookie/ServerCookieEncoder.java | 2 +- .../server/endpoint/EndpointDescriptor.java | 4 - .../server/endpoint/EndpointDispatcher.java | 12 - .../http/server/endpoint/HttpEndpoint.java | 74 +- .../endpoint/HttpEndpointDescriptor.java | 10 +- .../server/endpoint/HttpEndpointResolver.java | 33 +- .../endpoint/service/ClassLoaderService.java | 30 +- .../server/endpoint/service/EmptyService.java | 11 - .../server/endpoint/service/FileService.java | 19 +- .../endpoint/service/MappedFileService.java | 50 - .../endpoint/service/MethodService.java | 10 +- .../endpoint/service/ResourceService.java | 39 +- .../server/handler/IdleTimeoutHandler.java | 4 +- ...izer.java => Http1ChannelInitializer.java} | 37 +- .../http2/Http2ChannelInitializer.java | 15 +- .../Http2StreamFrameToHttpObjectCodec.java | 9 +- .../http/server/transport/BaseTransport.java | 5 +- .../http/server/transport/Http1Transport.java | 43 + .../server/transport/Http2ServerResponse.java | 4 +- .../http/server/transport/Http2Transport.java | 24 +- .../server/transport/HttpServerRequest.java | 90 +- .../server/transport/HttpServerResponse.java | 4 +- .../http/server/transport/HttpTransport.java | 49 - ...bib.netty.http.server.api.ProtocolProvider | 2 + ...ServerTest.java => BindExceptionTest.java} | 4 +- .../{ServerTest.java => RunServerTest.java} | 4 +- .../http/server/test/ThreadLeakTest.java | 2 +- .../TransportLayerSecurityServerTest.java | 14 +- .../test/endpoint/ClassloaderServiceTest.java | 13 +- .../server/test/endpoint/EndpointTest.java | 86 +- .../server/test/endpoint/FileServiceTest.java | 22 +- .../test/endpoint/SecureFileServiceTest.java | 14 +- .../test/hacks/HttpPipeliningHandlerTest.java | 2 +- .../CleartextTest.java} | 35 +- .../EncryptedTest.java} | 25 +- .../server/test/{ => http1}/FlushTest.java | 17 +- .../http/server/test/http1/PostTest.java | 244 +++++ .../netty/http/server/test/http1/PutTest.java | 121 +++ .../server/test/{ => http1}/StreamTest.java | 11 +- .../CleartextTest.java} | 31 +- .../EncryptedTest.java} | 25 +- .../http/server/test/http2/FlushTest.java | 87 ++ .../server/test/{ => http2}/PostTest.java | 137 +-- .../netty/http/server/test/http2/PutTest.java | 121 +++ .../http/server/test/http2/StreamTest.java | 64 ++ settings.gradle | 4 +- 378 files changed, 44782 insertions(+), 1046 deletions(-) create mode 100644 netty-http-client-api/build.gradle rename {netty-http-client/src/main/java/org/xbib/netty/http/client/retry => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/BackOff.java (97%) rename {netty-http-client/src/main/java/org/xbib/netty/http/client/listener => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/ExceptionListener.java (70%) create mode 100644 netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/HttpChannelInitializer.java rename {netty-http-client/src/main/java/org/xbib/netty/http/client/pool => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/Pool.java (87%) create mode 100644 netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ProtocolProvider.java rename {netty-http-client/src/main/java/org/xbib/netty/http/client => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/Request.java (83%) rename {netty-http-client/src/main/java/org/xbib/netty/http/client/listener => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/ResponseListener.java (78%) rename {netty-http-client/src/main/java/org/xbib/netty/http/client/transport => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/Transport.java (94%) rename {netty-http-client/src/main/java/org/xbib/netty/http/client => netty-http-client-api/src/main/java/org/xbib/netty/http/client/api}/UserAgent.java (96%) create mode 100644 netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/package-info.java create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/Http1Provider.java create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/Http2Provider.java rename netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/{HttpChannelInitializer.java => Http1ChannelInitializer.java} (87%) delete mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java delete mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java delete mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java rename netty-http-client/src/main/java/org/xbib/netty/http/client/transport/{HttpTransport.java => Http1Transport.java} (76%) create mode 100644 netty-http-client/src/main/resources/META-INF/services/org.xbib.netty.http.client.api.ProtocolProvider delete mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttpTest.java rename netty-http-client/src/test/java/org/xbib/netty/http/client/test/{ => conscrypt}/ConscryptTest.java (87%) rename netty-http-client/src/test/java/org/xbib/netty/http/client/test/{ => cookie}/CookieSetterHttpBinTest.java (61%) create mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/GoogleTest.java rename netty-http-client/src/test/java/org/xbib/netty/http/client/test/{ => http1}/Http1Test.java (85%) rename netty-http-client/src/test/java/org/xbib/netty/http/client/test/{ => http1}/XbibTest.java (89%) create mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2/GoogleTest.java rename netty-http-client/src/test/java/org/xbib/netty/http/client/test/{htt2push => http2push}/Http2PushTest.java (88%) create mode 100644 netty-http-rx/NOTICE.txt create mode 100644 netty-http-rx/build.gradle create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/HandlerNames.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/AllocatingTransformer.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/AppendTransformerEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/AutoReleaseOperator.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/BackpressureManagingHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/BytesInspector.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelOperations.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelSubscriberEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/Connection.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionCreationFailedEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberReplaceEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberResetEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionSubscriberEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/ContentSource.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/DefaultChannelOperations.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/DetachedChannelPipeline.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/DisposableContentSource.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/EmitConnectionEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/FlushSelectorOperator.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/MarkAwarePipeline.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformations.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformer.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProviderFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ClientConnectionToChannelBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ClientState.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProviderFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionRequest.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/Host.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/HostConnector.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/events/ClientEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/internal/SingleHostConnectionProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostCollector.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancingStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoBufferHostCollector.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoHostsAvailableException.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/CompositePoolLimitDeterminationStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/IdleConnectionsHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/MaxConnectionsBasedStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolConfig.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolExhaustedException.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolLimitDeterminationStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnection.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProviderImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/SingleHostPoolingProviderFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/client/pool/UnboundedPoolLimitDeterminationStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/Clock.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/EventAttributeKeys.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/EventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/EventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/EventSource.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/ListenerInvocationException.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/ListenersHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/events/internal/SafeEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/internal/ExecuteInEventloopAction.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/internal/InternalReadTimeoutHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/internal/VoidToAnythingCast.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/CookiesHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/HttpHandlerNames.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/TrailingHeaders.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClient.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChain.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChainImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientRequest.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientResponse.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpRedirectException.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClient.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClientImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/Interceptor.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/RequestProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/TransformingInterceptor.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/SafeHttpClientEventsListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProviderFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientResponseImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientToConnectionBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/RawRequest.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/Redirector.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/UnusableConnection.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/loadbalancer/EWMABasedP2CStrategy.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpContentSubscriberEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpMessageFormatter.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/OperatorTrailer.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/UnsafeEmptySubscriber.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ContentWriterImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/FailedContentWriter.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpConnectionHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServer.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerInterceptorChain.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequest.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequestImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponse.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponseImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/RequestHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ResponseContentWriter.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/UriInfoHolder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/SafeHttpServerEventsListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/ServerSentEvent.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/util/HttpContentStringLineDecoder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/WebSocketConnection.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnection.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketRequest.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketResponse.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/Ws7To13UpgradeHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketRequestImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketResponseImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/internal/WsUtils.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/V7to13Handshaker.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandlers.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandshaker.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/Ws7To13UpgradeHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/TcpHandlerNames.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/ConnectionRequestImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClient.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClientImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/Interceptor.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClient.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChain.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChainImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TransformingInterceptor.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/SafeTcpClientEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProviderFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/ConnectionHandler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServer.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerConnectionToChannelBridge.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerImpl.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerInterceptorChain.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerState.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/SafeTcpServerEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventListener.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisher.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/server/ServerState.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/ssl/DefaultSslCodec.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/ssl/SslCodec.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroup.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/RxDefaultThreadFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/RxEventLoopProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaEventloopScheduler.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaNettyBasedSchedulersHook.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/threads/SingleNioLoopProvider.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/util/CollectBytes.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/util/LineReader.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/util/LoggingHandlerFactory.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/util/StringLineDecoder.java create mode 100644 netty-http-rx/src/main/java/io/reactivex/netty/util/UnicastBufferingSubject.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridgeTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/BackpressureManagingHandlerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/BytesWriteInterceptorTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/ConnectionImplTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceRule.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/DefaultChannelOperationsTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/DetachedChannelPipelineTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/ReadProducerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridgeTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteStreamSubscriberTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteTransformerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/channel/events/ConnectionEventPublisherTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/ClientStateTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/SslClientTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategyTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactoryTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PoolLimitStrategyTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PooledConnectionProviderImplTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderRule.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/CookiesHolderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/EventListenerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientPoolTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientRule.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpRedirectTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/RedirectOperatorTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisherTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListenerImpl.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImplTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridgeTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/CookieTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/Http10Test.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpEndToEndTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRequestUriTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRule.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridgeTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/PipeliningTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisherTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListenerImpl.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/ServerSentEventEndToEndTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/SseTestUtil.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnectionTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/server/WSEagerInputSubscriptionHandlerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/EventListenerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/MockTcpClientEventListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/PoolingWithRealChannelTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientImplTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientRule.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisherTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventsTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/UnexpectedConnectionHandlerErrorsTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisherTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/FlushSelector.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/InboundRequestFeeder.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockClientEventListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockConnectionEventListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventPublisher.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockPoolLimitDeterminationStrategy.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockProducer.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockTcpServerEventListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/TcpConnectionRequestMock.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/TrackableMetricEventsListener.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelPipelineDelegate.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelProvider.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelWithFeeder.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedConnectionProvider.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroupTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/threads/RxJavaEventloopSchedulerTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/util/CollectBytesTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/util/LineReaderTest.java create mode 100644 netty-http-rx/src/test/java/io/reactivex/netty/util/UnicastBufferingSubjectTest.java create mode 100644 netty-http-rx/src/test/resources/log4j.properties create mode 100644 netty-http-server-api/build.gradle rename {netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/Endpoint.java (57%) create mode 100644 netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDescriptor.java create mode 100644 netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDispatcher.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Filter.java (60%) create mode 100644 netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/HttpChannelInitializer.java create mode 100644 netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ProtocolProvider.java rename {netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/Resource.java (81%) rename {netty-http-server/src/main/java/org/xbib/netty/http/server => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/ServerRequest.java (87%) rename {netty-http-server/src/main/java/org/xbib/netty/http/server => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/ServerResponse.java (98%) rename {netty-http-server/src/main/java/org/xbib/netty/http/server/transport => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/Transport.java (93%) rename {netty-http-server/src/main/java/org/xbib/netty/http/server => netty-http-server-api/src/main/java/org/xbib/netty/http/server/api}/annotation/Endpoint.java (83%) create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/Http1Provider.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/Http2Provider.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/{HttpChannelInitializer.java => Http1ChannelInitializer.java} (78%) create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1Transport.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java create mode 100644 netty-http-server/src/main/resources/META-INF/services/org.xbib.netty.http.server.api.ProtocolProvider rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{DoubleServerTest.java => BindExceptionTest.java} (93%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{ServerTest.java => RunServerTest.java} (90%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{CleartextHttp1Test.java => http1/CleartextTest.java} (88%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{SecureHttp1Test.java => http1/EncryptedTest.java} (91%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{ => http1}/FlushTest.java (88%) create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PostTest.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PutTest.java rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{ => http1}/StreamTest.java (90%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{CleartextHttp2Test.java => http2/CleartextTest.java} (95%) rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{SecureHttp2Test.java => http2/EncryptedTest.java} (92%) create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/FlushTest.java rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{ => http2}/PostTest.java (57%) create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PutTest.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/StreamTest.java diff --git a/gradle.properties b/gradle.properties index a90966a..327e5f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.xbib name = netty-http -version = 4.1.41.0 +version = 4.1.41.1 # netty netty.version = 4.1.41.Final tcnative.version = 2.0.25.Final # for netty-http-common -xbib-net-url.version = 2.0.1 +xbib-net-url.version = 2.0.2 # for netty-http-server bouncycastle.version = 1.62 @@ -18,11 +18,16 @@ reactivestreams.version = 1.0.2 # for netty-http-server-rest xbib-guice.version = 4.0.4 +# for rx +reactivex.version = 1.2.+ + # test junit.version = 5.5.1 junit4.version = 4.12 conscrypt.version = 2.2.1 jackson.version = 2.9.9 +hamcrest.version = 1.3 +mockito.version = 1.10.19 # doc asciidoclet.version = 1.5.4 diff --git a/netty-http-client-api/build.gradle b/netty-http-client-api/build.gradle new file mode 100644 index 0000000..34cdeeb --- /dev/null +++ b/netty-http-client-api/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compile project(":netty-http-common") +} \ No newline at end of file diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/BackOff.java similarity index 97% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/BackOff.java index bd1ca97..0fc78f3 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/BackOff.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.retry; +package org.xbib.netty.http.client.api; /** * Back-off policy when retrying an operation. diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ExceptionListener.java similarity index 70% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ExceptionListener.java index eb93af6..12f0e75 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ExceptionListener.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.listener; +package org.xbib.netty.http.client.api; @FunctionalInterface public interface ExceptionListener { diff --git a/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/HttpChannelInitializer.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/HttpChannelInitializer.java new file mode 100644 index 0000000..83f5057 --- /dev/null +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/HttpChannelInitializer.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.client.api; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; + +public interface HttpChannelInitializer extends ChannelHandler { + + void initChannel(Channel channel); + +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Pool.java similarity index 87% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Pool.java index 4f0be05..1828902 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Pool.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.pool; +package org.xbib.netty.http.client.api; import java.io.Closeable; diff --git a/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ProtocolProvider.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ProtocolProvider.java new file mode 100644 index 0000000..6ba410d --- /dev/null +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ProtocolProvider.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.client.api; + +public interface ProtocolProvider { + + boolean supportsMajorVersion(int majorVersion); + + Class initializerClass(); + + Class transportClass(); +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Request.java similarity index 83% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Request.java index fe98ba1..f39e939 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Request.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client; +package org.xbib.netty.http.client.api; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -13,21 +13,17 @@ import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.QueryStringEncoder; - import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.util.AsciiString; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; import org.xbib.net.URL; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.listener.StatusListener; -import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.cookie.Cookie; +import java.nio.charset.Charset; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnmappableCharacterException; @@ -45,7 +41,7 @@ import java.util.concurrent.CompletableFuture; /** * HTTP client request. */ -public class Request { +public final class Request { private final URL url; @@ -73,18 +69,14 @@ public class Request { private final BackOff backOff; - private CompletableFuture completableFuture; + private CompletableFuture completableFuture; private ResponseListener responseListener; - private CookieListener cookieListener; - - private StatusListener statusListener; - private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod, HttpHeaders headers, Collection cookies, ByteBuf content, long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, - boolean isBackOff, BackOff backOff) { + boolean isBackOff, BackOff backOff, ResponseListener responseListener) { this.url = url; this.uri = uri; this.httpVersion = httpVersion; @@ -98,6 +90,7 @@ public class Request { this.redirectCount = redirectCount; this.isBackOff = isBackOff; this.backOff = backOff; + this.responseListener = responseListener; } public URL url() { @@ -182,41 +175,26 @@ public class Request { "]"; } - public Request setCompletableFuture(CompletableFuture completableFuture) { + public Request setCompletableFuture(CompletableFuture completableFuture) { this.completableFuture = completableFuture; return this; } - public CompletableFuture getCompletableFuture() { + public CompletableFuture getCompletableFuture() { return completableFuture; } - - public Request setCookieListener(CookieListener cookieListener) { - this.cookieListener = cookieListener; - return this; - } - - public CookieListener getCookieListener() { - return cookieListener; - } - - public Request setStatusListener(StatusListener statusListener) { - this.statusListener = statusListener; - return this; - } - - public StatusListener getStatusListener() { - return statusListener; - } - - public Request setResponseListener(ResponseListener responseListener) { + public void setResponseListener(ResponseListener responseListener) { this.responseListener = responseListener; - return this; } - public ResponseListener getResponseListener() { - return responseListener; + public void onResponse(HttpResponse httpResponse) { + if (responseListener != null) { + responseListener.onResponse(httpResponse); + } + if (completableFuture != null) { + completableFuture.complete(this); + } } public static Builder get() { @@ -259,6 +237,15 @@ public class Request { return builder(PooledByteBufAllocator.DEFAULT, httpMethod); } + public static Builder builder(HttpMethod httpMethod, Request request) { + return builder(PooledByteBufAllocator.DEFAULT, httpMethod) + .setVersion(request.httpVersion) + .uri(request.uri) + .setHeaders(request.headers) + .content(request.content) + .setResponseListener(request.responseListener); + } + public static Builder builder(ByteBufAllocator allocator, HttpMethod httpMethod) { return new Builder(allocator).setMethod(httpMethod); } @@ -293,7 +280,7 @@ public class Request { private final Collection cookies; - private final PercentEncoder encoder; + private PercentEncoder encoder; private HttpMethod httpMethod; @@ -311,6 +298,8 @@ public class Request { private String uri; + private CharSequence contentType; + private HttpParameters uriParameters; private HttpParameters formParameters; @@ -327,6 +316,8 @@ public class Request { private BackOff backOff; + private ResponseListener responseListener; + Builder(ByteBufAllocator allocator) { this.allocator = allocator; this.httpMethod = DEFAULT_METHOD; @@ -341,9 +332,8 @@ public class Request { this.headers = new DefaultHttpHeaders(); this.removeHeaders = new ArrayList<>(); this.cookies = new HashSet<>(); - this.encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); this.uriParameters = new HttpParameters(); - this.formParameters = new HttpParameters(DEFAULT_FORM_CONTENT_TYPE); + charset(StandardCharsets.UTF_8); } public Builder setMethod(HttpMethod httpMethod) { @@ -420,22 +410,57 @@ public class Request { return this; } + public Builder charset(Charset charset) { + this.encoder = PercentEncoders.getQueryEncoder(charset); + this.formParameters = new HttpParameters(DEFAULT_FORM_CONTENT_TYPE); + return this; + } + + public Builder contentType(CharSequence contentType) { + Objects.requireNonNull(contentType); + this.contentType = contentType; + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + public Builder contentType(CharSequence contentType, Charset charset) { + Objects.requireNonNull(contentType); + Objects.requireNonNull(charset); + this.contentType = contentType; + charset(charset); + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=" + charset.name().toLowerCase()); + return this; + } + public Builder addParameter(String name, String value) { - try { - uriParameters.add(encoder.encode(name), encoder.encode(value)); - } catch (MalformedInputException | UnmappableCharacterException e) { - throw new IllegalArgumentException(e); - } + Objects.requireNonNull(name); + Objects.requireNonNull(value); + uriParameters.add(encode(contentType, name), encode(contentType, value)); return this; } public Builder addFormParameter(String name, String value) { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + formParameters.add(encode(contentType, name), encode(contentType, value)); + return this; + } + + private String encode(CharSequence contentType, String value) { + if (value == null) { + return null; + } try { - formParameters.add(encoder.encode(name), encoder.encode(value)); + String encodedValue = encoder.encode(value); + // https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4 + if (HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.equals(contentType)) { + encodedValue = encodedValue.replace("%20", "+"); + } + return encodedValue; } catch (MalformedInputException | UnmappableCharacterException e) { + // should never be reached throw new IllegalArgumentException(e); } - return this; } public Builder addCookie(Cookie cookie) { @@ -443,11 +468,6 @@ public class Request { return this; } - public Builder contentType(String contentType) { - addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); - return this; - } - public Builder acceptGzip(boolean gzip) { this.gzip = gzip; return this; @@ -513,12 +533,17 @@ public class Request { return this; } - public Builder content(CharSequence charSequence, String contentType) { + public Builder content(CharSequence charSequence, CharSequence contentType) { content(charSequence.toString().getBytes(HttpUtil.getCharset(contentType, StandardCharsets.UTF_8)), AsciiString.of(contentType)); return this; } + public Builder content(CharSequence charSequence, CharSequence contentType, Charset charset) { + content(charSequence.toString().getBytes(charset), AsciiString.of(contentType)); + return this; + } + public Builder content(byte[] buf, String contentType) { content(buf, AsciiString.of(contentType)); return this; @@ -529,6 +554,11 @@ public class Request { return this; } + public Builder setResponseListener(ResponseListener responseListener) { + this.responseListener = responseListener; + return this; + } + public Request build() { DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true); if (url != null) { @@ -602,7 +632,8 @@ public class Request { validatedHeaders.remove(headerName); } return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content, - timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff); + timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff, + responseListener); } private void addHeader(AsciiString name, Object value) { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ResponseListener.java similarity index 78% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ResponseListener.java index 808ba48..0dd6114 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/ResponseListener.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.listener; +package org.xbib.netty.http.client.api; import org.xbib.netty.http.common.HttpResponse; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Transport.java similarity index 94% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Transport.java index 90a11c4..ee53a8a 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/Transport.java @@ -1,11 +1,10 @@ -package org.xbib.netty.http.client.transport; +package org.xbib.netty.http.client.api; import io.netty.channel.Channel; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.util.AttributeKey; -import org.xbib.netty.http.client.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.cookie.CookieBox; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/UserAgent.java similarity index 96% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java rename to netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/UserAgent.java index f76e58a..045c3a0 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/UserAgent.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client; +package org.xbib.netty.http.client.api; import io.netty.bootstrap.Bootstrap; diff --git a/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/package-info.java b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/package-info.java new file mode 100644 index 0000000..11abacd --- /dev/null +++ b/netty-http-client-api/src/main/java/org/xbib/netty/http/client/api/package-info.java @@ -0,0 +1,4 @@ +/** + * Listeners for Netty HTTP client. + */ +package org.xbib.netty.http.client.api; diff --git a/netty-http-client-rest/src/main/java/org/xbib/netty/http/client/rest/RestClient.java b/netty-http-client-rest/src/main/java/org/xbib/netty/http/client/rest/RestClient.java index 3b6ea13..656b5f5 100644 --- a/netty-http-client-rest/src/main/java/org/xbib/netty/http/client/rest/RestClient.java +++ b/netty-http-client-rest/src/main/java/org/xbib/netty/http/client/rest/RestClient.java @@ -6,7 +6,7 @@ import io.netty.handler.codec.http.HttpMethod; import org.xbib.net.URL; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpResponse; import java.io.IOException; @@ -95,7 +95,7 @@ public class RestClient { requestBuilder.content(byteBuf); try { client.newTransport(HttpAddress.http1(url)) - .execute(requestBuilder.build().setResponseListener(restClient::setResponse)).close(); + .execute(requestBuilder.setResponseListener(restClient::setResponse).build()).close(); } catch (Exception e) { throw new IOException(e); } diff --git a/netty-http-client/build.gradle b/netty-http-client/build.gradle index 7d5d0b8..3878976 100644 --- a/netty-http-client/build.gradle +++ b/netty-http-client/build.gradle @@ -1,6 +1,6 @@ dependencies { - compile project(":netty-http-common") + compile project(":netty-http-client-api") compile "io.netty:netty-handler-proxy:${project.property('netty.version')}" compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" testCompile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java index 6046a96..5e9f753 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java @@ -3,7 +3,6 @@ package org.xbib.netty.http.client; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; -import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.WriteBufferWaterMark; @@ -25,12 +24,11 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import org.xbib.netty.http.client.handler.http.HttpChannelInitializer; -import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.client.api.HttpChannelInitializer; +import org.xbib.netty.http.client.api.ProtocolProvider; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.client.pool.BoundedChannelPool; -import org.xbib.netty.http.client.transport.Http2Transport; -import org.xbib.netty.http.client.transport.HttpTransport; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.NetworkUtils; @@ -44,6 +42,7 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.security.KeyStoreException; import java.security.Provider; @@ -52,6 +51,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Queue; +import java.util.ServiceLoader; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; @@ -96,6 +96,8 @@ public final class Client implements AutoCloseable { private final Queue transports; + private final List> protocolProviders; + private BoundedChannelPool pool; public Client() { @@ -106,10 +108,16 @@ public final class Client implements AutoCloseable { this(clientConfig, null, null, null); } + @SuppressWarnings("unchecked") public Client(ClientConfig clientConfig, ByteBufAllocator byteBufAllocator, EventLoopGroup eventLoopGroup, Class socketChannelClass) { Objects.requireNonNull(clientConfig); this.clientConfig = clientConfig; + this.protocolProviders = new ArrayList<>(); + for (ProtocolProvider provider : ServiceLoader.load(ProtocolProvider.class)) { + protocolProviders.add(provider); + logger.log(Level.INFO, "protocol provider up: " + provider.transportClass() ); + } initializeTrustManagerFactory(clientConfig); this.byteBufAllocator = byteBufAllocator != null ? byteBufAllocator : ByteBufAllocator.DEFAULT; @@ -162,6 +170,10 @@ public final class Client implements AutoCloseable { return new Builder(); } + public List> getProtocolProviders() { + return protocolProviders; + } + public ClientConfig getClientConfig() { return clientConfig; } @@ -200,18 +212,36 @@ public final class Client implements AutoCloseable { } public Transport newTransport(HttpAddress httpAddress) { - Transport transport; + Transport transport = null; if (httpAddress != null) { - if (httpAddress.getVersion().majorVersion() == 1) { - transport = new HttpTransport(this, httpAddress); - } else { - transport = new Http2Transport(this, httpAddress); + for (ProtocolProvider protocolProvider : protocolProviders) { + if (protocolProvider.supportsMajorVersion(httpAddress.getVersion().majorVersion())) { + try { + transport = protocolProvider.transportClass() + .getConstructor(Client.class, HttpAddress.class).newInstance(this, httpAddress); + break; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(); + } + } + } + if (transport == null) { + throw new UnsupportedOperationException("no protocol support for " + httpAddress); } } else if (hasPooledConnections()) { - if (pool.getVersion().majorVersion() == 1) { - transport = new HttpTransport(this, null); - } else { - transport = new Http2Transport(this, null); + for (ProtocolProvider protocolProvider : protocolProviders) { + if (protocolProvider.supportsMajorVersion(pool.getVersion().majorVersion())) { + try { + transport = protocolProvider.transportClass() + .getConstructor(Client.class, HttpAddress.class).newInstance(this, null); + break; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(); + } + } + } + if (transport == null) { + throw new UnsupportedOperationException("no pool protocol support for " + pool.getVersion().majorVersion()); } } else { throw new IllegalStateException("no address given to connect to"); @@ -226,13 +256,10 @@ public final class Client implements AutoCloseable { HttpVersion httpVersion = httpAddress.getVersion(); SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator); - ChannelInitializer initializer; - if (httpVersion.majorVersion() == 1) { - initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory, - new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory)); - } else { - initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory); - } + HttpChannelInitializer initializerTwo = + findChannelInitializer(2, httpAddress, sslHandlerFactory, null); + HttpChannelInitializer initializer = + findChannelInitializer(httpVersion.majorVersion(), httpAddress, sslHandlerFactory, initializerTwo); try { channel = bootstrap.handler(initializer) .connect(httpAddress.getInetSocketAddress()).sync().await().channel(); @@ -273,6 +300,15 @@ public final class Client implements AutoCloseable { .execute(request); } + /** + * Execute a request and return a {@link CompletableFuture}. + * + * @param request the request + * @param supplier the function for the response + * @param the result of the function for the response + * @return the completable future + * @throws IOException if the request fails to be executed. + */ public CompletableFuture execute(Request request, Function supplier) throws IOException { return newTransport(HttpAddress.of(request.url(), request.httpVersion())) @@ -294,7 +330,7 @@ public final class Client implements AutoCloseable { } /** - * Retry request by following a back-off strategy. + * Retry request. * * @param transport the transport to retry * @param request the request to retry @@ -345,6 +381,24 @@ public final class Client implements AutoCloseable { } } + private HttpChannelInitializer findChannelInitializer(int majorVersion, + HttpAddress httpAddress, + SslHandlerFactory sslHandlerFactory, + HttpChannelInitializer helper) { + for (ProtocolProvider protocolProvider : protocolProviders) { + if (protocolProvider.supportsMajorVersion(majorVersion)) { + try { + return protocolProvider.initializerClass() + .getConstructor(ClientConfig.class, HttpAddress.class, SslHandlerFactory.class, HttpChannelInitializer.class) + .newInstance(clientConfig, httpAddress, sslHandlerFactory, helper); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(); + } + } + } + throw new IllegalStateException("no channel initializer found for major version " + majorVersion); + } + /** * Initialize trust manager factory once per client lifecycle. * @param clientConfig the client config @@ -360,40 +414,8 @@ public final class Client implements AutoCloseable { } } - private static SslHandler newSslHandler(SslContext sslContext, - ClientConfig clientConfig, ByteBufAllocator allocator, HttpAddress httpAddress) { - InetSocketAddress peer = httpAddress.getInetSocketAddress(); - SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort()); - SSLEngine engine = sslHandler.engine(); - List serverNames = clientConfig.getServerNamesForIdentification(); - if (serverNames.isEmpty()) { - serverNames = Collections.singletonList(peer.getHostName()); - } - SSLParameters params = engine.getSSLParameters(); - // use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm - params.setEndpointIdentificationAlgorithm("HTTPS"); - List sniServerNames = new ArrayList<>(); - for (String serverName : serverNames) { - sniServerNames.add(new SNIHostName(serverName)); - } - params.setServerNames(sniServerNames); - engine.setSSLParameters(params); - switch (clientConfig.getClientAuthMode()) { - case NEED: - engine.setNeedClientAuth(true); - break; - case WANT: - engine.setWantClientAuth(true); - break; - default: - break; - } - engine.setEnabledProtocols(clientConfig.getProtocols()); - return sslHandler; - } - private static SslContext newSslContext(ClientConfig clientConfig, HttpVersion httpVersion) throws SSLException { - // Conscrypt? + // Conscrypt support? SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() .sslProvider(clientConfig.getSslProvider()) .ciphers(clientConfig.getCiphers(), clientConfig.getCipherSuiteFilter()) @@ -449,16 +471,11 @@ public final class Client implements AutoCloseable { SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator); - Http2ChannelInitializer http2ChannelInitializer = - new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory); - if (httpVersion.majorVersion() == 1) { - HttpChannelInitializer initializer = - new HttpChannelInitializer(clientConfig, httpAddress, - sslHandlerFactory, http2ChannelInitializer); - initializer.initChannel(channel); - } else { - http2ChannelInitializer.initChannel(channel); - } + HttpChannelInitializer initializerTwo = + findChannelInitializer(2, httpAddress, sslHandlerFactory, null); + HttpChannelInitializer initializer = + findChannelInitializer(httpVersion.majorVersion(), httpAddress, sslHandlerFactory, initializerTwo); + initializer.initChannel(channel); } } @@ -481,7 +498,34 @@ public final class Client implements AutoCloseable { } public SslHandler create() { - return newSslHandler(sslContext, clientConfig, allocator, httpAddress); + InetSocketAddress peer = httpAddress.getInetSocketAddress(); + SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort()); + SSLEngine engine = sslHandler.engine(); + List serverNames = clientConfig.getServerNamesForIdentification(); + if (serverNames.isEmpty()) { + serverNames = Collections.singletonList(peer.getHostName()); + } + SSLParameters params = engine.getSSLParameters(); + // use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm + params.setEndpointIdentificationAlgorithm("HTTPS"); + List sniServerNames = new ArrayList<>(); + for (String serverName : serverNames) { + sniServerNames.add(new SNIHostName(serverName)); + } + params.setServerNames(sniServerNames); + engine.setSSLParameters(params); + switch (clientConfig.getClientAuthMode()) { + case NEED: + engine.setNeedClientAuth(true); + break; + case WANT: + engine.setWantClientAuth(true); + break; + default: + break; + } + engine.setEnabledProtocols(clientConfig.getProtocols()); + return sslHandler; } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java index 0bffeed..47fcea9 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java @@ -8,9 +8,8 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.SslProvider; -import org.xbib.netty.http.client.pool.BoundedChannelPool; -import org.xbib.netty.http.client.pool.Pool; -import org.xbib.netty.http.client.retry.BackOff; +import org.xbib.netty.http.client.api.Pool; +import org.xbib.netty.http.client.api.BackOff; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.security.SecurityUtil; @@ -20,7 +19,6 @@ import java.security.KeyStore; import java.security.Provider; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; public class ClientConfig { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Http1Provider.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Http1Provider.java new file mode 100644 index 0000000..f0ce2b4 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Http1Provider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.client; + +import org.xbib.netty.http.client.api.ProtocolProvider; +import org.xbib.netty.http.client.handler.http.Http1ChannelInitializer; +import org.xbib.netty.http.client.transport.Http1Transport; + +public class Http1Provider implements ProtocolProvider { + + @Override + public boolean supportsMajorVersion(int majorVersion) { + return majorVersion == 1; + } + + @Override + public Class initializerClass() { + return Http1ChannelInitializer.class; + } + + @Override + public Class transportClass() { + return Http1Transport.class; + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Http2Provider.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Http2Provider.java new file mode 100644 index 0000000..1e1f744 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Http2Provider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.client; + +import org.xbib.netty.http.client.api.ProtocolProvider; +import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.client.transport.Http2Transport; + +public class Http2Provider implements ProtocolProvider { + + @Override + public boolean supportsMajorVersion(int majorVersion) { + return majorVersion == 2; + } + + @Override + public Class initializerClass() { + return Http2ChannelInitializer.class; + } + + @Override + public Class transportClass() { + return Http2Transport.class; + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/Http1ChannelInitializer.java similarity index 87% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/Http1ChannelInitializer.java index c13fcb1..c7c22fa 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/Http1ChannelInitializer.java @@ -13,15 +13,16 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandler; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.ClientConfig; +import org.xbib.netty.http.client.api.HttpChannelInitializer; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.common.HttpAddress; import java.util.logging.Level; import java.util.logging.Logger; -public class HttpChannelInitializer extends ChannelInitializer { +public class Http1ChannelInitializer extends ChannelInitializer implements HttpChannelInitializer { - private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); + private static final Logger logger = Logger.getLogger(Http1ChannelInitializer.class.getName()); private final ClientConfig clientConfig; @@ -33,14 +34,14 @@ public class HttpChannelInitializer extends ChannelInitializer { private final Http2ChannelInitializer http2ChannelInitializer; - public HttpChannelInitializer(ClientConfig clientConfig, - HttpAddress httpAddress, - Client.SslHandlerFactory sslHandlerFactory, - Http2ChannelInitializer http2ChannelInitializer) { + public Http1ChannelInitializer(ClientConfig clientConfig, + HttpAddress httpAddress, + Client.SslHandlerFactory sslHandlerFactory, + HttpChannelInitializer http2ChannelInitializer) { this.clientConfig = clientConfig; this.httpAddress = httpAddress; this.sslHandlerFactory = sslHandlerFactory; - this.http2ChannelInitializer = http2ChannelInitializer; + this.http2ChannelInitializer = (Http2ChannelInitializer) http2ChannelInitializer; this.httpResponseHandler = new HttpResponseHandler(); } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java index 9ab053d..e6b96f3 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java @@ -4,7 +4,7 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpResponse; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Transport; @ChannelHandler.Sharable public class HttpResponseHandler extends SimpleChannelInboundHandler { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java index 6d66a8a..0aa8692 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java @@ -5,9 +5,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.DelimiterBasedFrameDecoder; -import io.netty.handler.codec.Delimiters; -import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; import io.netty.handler.codec.http2.Http2FrameLogger; @@ -17,14 +14,15 @@ import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; import io.netty.handler.logging.LogLevel; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.ClientConfig; +import org.xbib.netty.http.client.api.HttpChannelInitializer; import org.xbib.netty.http.client.handler.http.TrafficLoggingHandler; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import java.util.logging.Level; import java.util.logging.Logger; -public class Http2ChannelInitializer extends ChannelInitializer { +public class Http2ChannelInitializer extends ChannelInitializer implements HttpChannelInitializer { private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); @@ -35,8 +33,9 @@ public class Http2ChannelInitializer extends ChannelInitializer { private final Client.SslHandlerFactory sslHandlerFactory; public Http2ChannelInitializer(ClientConfig clientConfig, - HttpAddress httpAddress, - Client.SslHandlerFactory sslHandlerFactory) { + HttpAddress httpAddress, + Client.SslHandlerFactory sslHandlerFactory, + HttpChannelInitializer unusedInitializer) { this.clientConfig = clientConfig; this.httpAddress = httpAddress; this.sslHandlerFactory = sslHandlerFactory; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java index 918422a..f66bb6e 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java @@ -5,7 +5,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http2.HttpConversionUtil; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Transport; @ChannelHandler.Sharable public class Http2ResponseHandler extends SimpleChannelInboundHandler { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java deleted file mode 100644 index 0049a33..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.xbib.netty.http.client.listener; - -import org.xbib.netty.http.common.cookie.Cookie; - -@FunctionalInterface -public interface CookieListener { - - void onCookie(Cookie cookie); -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java deleted file mode 100644 index a26ffec..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.xbib.netty.http.client.listener; - - -import org.xbib.netty.http.common.HttpStatus; - -@FunctionalInterface -public interface StatusListener { - - void onStatus(HttpStatus httpStatus); -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java deleted file mode 100644 index b1e55f4..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Listeners for Netty HTTP client. - */ -package org.xbib.netty.http.client.listener; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java index ce4458a..b4cc9ee 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java @@ -11,6 +11,7 @@ import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; import io.netty.util.AttributeKey; +import org.xbib.netty.http.client.api.Pool; import org.xbib.netty.http.common.PoolKey; import java.io.IOException; @@ -65,8 +66,10 @@ public class BoundedChannelPool implements Pool { private PoolKeySelector poolKeySelector; /** - * @param semaphore the concurrency level - * @param httpVersion the HTTP version of the pool connections + * A bounded channel pool. + * + * @param semaphore the level of concurrency + * @param httpVersion the HTTP version of the pool connections * @param nodes the endpoint nodes, any element may contain the port (followed after ":") * to override the defaultPort argument * @param bootstrap bootstrap instance diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java index 7a64ce6..6cd4d27 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java @@ -1,5 +1,7 @@ package org.xbib.netty.http.client.retry; +import org.xbib.netty.http.client.api.BackOff; + /** * Implementation of {@link BackOff} that increases the back off period for each retry attempt using * a randomization function that grows exponentially. diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index 19731ff..ec89462 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -8,9 +8,10 @@ import org.xbib.net.PercentDecoder; import org.xbib.net.URL; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.retry.BackOff; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.BackOff; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.CookieBox; @@ -35,7 +36,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -abstract class BaseTransport implements Transport { +public abstract class BaseTransport implements Transport { private static final Logger logger = Logger.getLogger(BaseTransport.class.getName()); @@ -51,7 +52,7 @@ abstract class BaseTransport implements Transport { private SSLSession sslSession; - final Map channelFlowMap; + final Map flowMap; final SortedMap requests; @@ -61,7 +62,7 @@ abstract class BaseTransport implements Transport { this.client = client; this.httpAddress = httpAddress; this.channels = new ConcurrentHashMap<>(); - this.channelFlowMap = new ConcurrentHashMap<>(); + this.flowMap = new ConcurrentHashMap<>(); this.requests = new ConcurrentSkipListMap<>(); } @@ -71,7 +72,8 @@ abstract class BaseTransport implements Transport { } /** - * Experimental method for executing in a wrapping completable future. + * Method for executing in a wrapping completable future. + * * @param request request * @param supplier supplier * @param supplier result @@ -98,7 +100,7 @@ abstract class BaseTransport implements Transport { if (!channels.isEmpty()) { get(); } - for (Flow flow : channelFlowMap.values()) { + for (Flow flow : flowMap.values()) { flow.close(); } channels.clear(); @@ -128,7 +130,7 @@ abstract class BaseTransport implements Transport { } logger.log(Level.SEVERE, "failing: " + throwable.getMessage(), throwable); this.throwable = throwable; - for (Flow flow : channelFlowMap.values()) { + for (Flow flow : flowMap.values()) { flow.fail(throwable); } } @@ -143,14 +145,15 @@ abstract class BaseTransport implements Transport { if (channels.isEmpty()) { return this; } - for (Map.Entry entry : channelFlowMap.entrySet()) { + for (Map.Entry entry : flowMap.entrySet()) { Flow flow = entry.getValue(); if (!flow.isClosed()) { for (Integer key : flow.keys()) { + String requestKey = getRequestKey(entry.getKey(), key); try { flow.get(key).get(value, timeUnit); + completeRequest(requestKey); } catch (Exception e) { - String requestKey = getRequestKey(entry.getKey(), key); if (requestKey != null) { Request request = requests.get(requestKey); if (request != null && request.getCompletableFuture() != null) { @@ -180,7 +183,7 @@ abstract class BaseTransport implements Transport { if (channels.isEmpty()) { return; } - for (Map.Entry entry : channelFlowMap.entrySet()) { + for (Map.Entry entry : flowMap.entrySet()) { Flow flow = entry.getValue(); for (Integer key : flow.keys()) { try { @@ -205,7 +208,7 @@ abstract class BaseTransport implements Transport { logger.log(Level.WARNING, e.getMessage(), e); } }); - channelFlowMap.clear(); + flowMap.clear(); channels.clear(); requests.clear(); } @@ -280,18 +283,13 @@ abstract class BaseTransport implements Transport { logger.log(Level.FINE, "found redirect location: " + location); URL redirUrl = URL.base(request.url()).resolve(location); HttpMethod method = httpResponse.getStatus().getCode() == 303 ? HttpMethod.GET : request.httpMethod(); - Request.Builder newHttpRequestBuilder = Request.builder(method) - .url(redirUrl) - .setVersion(request.httpVersion()) - .setHeaders(request.headers()) - .content(request.content()); + Request.Builder newHttpRequestBuilder = Request.builder(method, request) + .url(redirUrl); request.url().getQueryParams().forEach(pair -> newHttpRequestBuilder.addParameter(pair.getFirst(), pair.getSecond()) ); request.cookies().forEach(newHttpRequestBuilder::addCookie); Request newHttpRequest = newHttpRequestBuilder.build(); - newHttpRequest.setResponseListener(request.getResponseListener()); - newHttpRequest.setCookieListener(request.getCookieListener()); StringBuilder hostAndPort = new StringBuilder(); hostAndPort.append(redirUrl.getHost()); if (redirUrl.getPort() != null) { @@ -324,7 +322,8 @@ abstract class BaseTransport implements Transport { return null; } if (request.isBackOff()) { - BackOff backOff = request.getBackOff() != null ? request.getBackOff() : + BackOff backOff = request.getBackOff() != null ? + request.getBackOff() : client.getClientConfig().getBackOff(); int status = httpResponse.getStatus ().getCode(); switch (status) { @@ -356,6 +355,24 @@ abstract class BaseTransport implements Transport { return null; } + private void completeRequest(String requestKey) { + if (requestKey != null) { + Request request = requests.get(requestKey); + if (request != null && request.getCompletableFuture() != null) { + request.getCompletableFuture().complete(request); + } + } + } + + private void completeRequestExceptionally(String requestKey, Throwable throwable) { + if (requestKey != null) { + Request request = requests.get(requestKey); + if (request != null && request.getCompletableFuture() != null) { + request.getCompletableFuture().completeExceptionally(throwable); + } + } + } + @Override public void setCookieBox(CookieBox cookieBox) { this.cookieBox = cookieBox; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java similarity index 76% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java index 5184ec0..8a4f315 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java @@ -10,14 +10,13 @@ import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.client.cookie.ClientCookieDecoder; import org.xbib.netty.http.client.cookie.ClientCookieEncoder; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.StatusListener; import org.xbib.netty.http.common.DefaultHttpResponse; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.cookie.Cookie; @@ -28,11 +27,11 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; -public class HttpTransport extends BaseTransport { +public class Http1Transport extends BaseTransport { - private static final Logger logger = Logger.getLogger(HttpTransport.class.getName()); + private static final Logger logger = Logger.getLogger(Http1Transport.class.getName()); - public HttpTransport(Client client, HttpAddress httpAddress) { + public Http1Transport(Client client, HttpAddress httpAddress) { super(client, httpAddress); } @@ -43,7 +42,7 @@ public class HttpTransport extends BaseTransport { return this; } final String channelId = channel.id().toString(); - channelFlowMap.putIfAbsent(channelId, new Flow()); + flowMap.putIfAbsent(channelId, new Flow()); // Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230. // The "origin form" requires a "Host" header. // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. @@ -52,7 +51,7 @@ public class HttpTransport extends BaseTransport { FullHttpRequest fullHttpRequest = request.content() == null ? new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, request.content()); - final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); + final Integer streamId = flowMap.get(channelId).nextStreamId(); if (streamId == null) { throw new IllegalStateException(); } @@ -87,29 +86,24 @@ public class HttpTransport extends BaseTransport { logger.log(Level.WARNING, "no request present for responding"); return; } - HttpResponse httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse); - client.getResponseCounter().incrementAndGet(); + String requestKey = requests.lastKey(); + Request request; + DefaultHttpResponse httpResponse = null; try { // streamID is expected to be null, last request on memory is expected to be current, remove request from memory - Request request = requests.remove(requests.lastKey()); + request = requests.get(requestKey); if (request != null) { - StatusListener statusListener = request.getStatusListener(); - if (statusListener != null) { - statusListener.onStatus(httpResponse.getStatus()); - } - for (String cookieString : httpResponse.getHeaders().getAllHeaders(HttpHeaderNames.SET_COOKIE)) { + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); addCookie(cookie); - CookieListener cookieListener = request.getCookieListener(); - if (cookieListener != null) { - cookieListener.onCookie(cookie); - } - } - ResponseListener responseListener = request.getResponseListener(); - if (responseListener != null) { - responseListener.onResponse(httpResponse); } + httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse, getCookieBox()); + request.onResponse(httpResponse); + client.getResponseCounter().incrementAndGet(); + } else { + logger.log(Level.WARNING, "unable to find request for response"); } + // check for retry / continue try { Request retryRequest = retry(request, httpResponse); if (retryRequest != null) { @@ -125,8 +119,9 @@ public class HttpTransport extends BaseTransport { } catch (URLSyntaxException | IOException e) { logger.log(Level.WARNING, e.getMessage(), e); } + // acknowledge success String channelId = channel.id().toString(); - Flow flow = channelFlowMap.get(channelId); + Flow flow = flowMap.get(channelId); if (flow == null) { return; } @@ -135,7 +130,12 @@ public class HttpTransport extends BaseTransport { promise.complete(true); } } finally { - httpResponse.release(); + if (requestKey != null) { + requests.remove(requestKey); + } + if (httpResponse != null) { + httpResponse.release(); + } } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java index 2d01a96..7a44ddc 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java @@ -17,17 +17,14 @@ import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.util.AsciiString; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.client.cookie.ClientCookieDecoder; import org.xbib.netty.http.client.cookie.ClientCookieEncoder; import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.StatusListener; import org.xbib.netty.http.common.DefaultHttpResponse; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.common.HttpResponse; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.cookie.Cookie; import java.io.IOException; @@ -74,7 +71,7 @@ public class Http2Transport extends BaseTransport { return this; } final String channelId = channel.id().toString(); - channelFlowMap.putIfAbsent(channelId, new Flow()); + flowMap.putIfAbsent(channelId, new Flow()); Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel) .handler(initializer).open().syncUninterruptibly().getNow(); AsciiString method = request.httpMethod().asciiName(); @@ -83,7 +80,7 @@ public class Http2Transport extends BaseTransport { String path = request.relative().isEmpty() ? "/" : request.relative(); Http2Headers http2Headers = new DefaultHttp2Headers() .method(method).scheme(scheme).authority(authority).path(path); - final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); + final Integer streamId = flowMap.get(channelId).nextStreamId(); if (streamId == null) { throw new IllegalStateException(); } @@ -146,14 +143,14 @@ public class Http2Transport extends BaseTransport { logger.log(Level.WARNING, "stream ID is null?"); return; } - DefaultHttpResponse httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse); + DefaultHttpResponse httpResponse = null; client.getResponseCounter().incrementAndGet(); try { // format of childchan channel ID is "/" String channelId = channel.id().toString(); int pos = channelId.indexOf('/'); channelId = pos > 0 ? channelId.substring(0, pos) : channelId; - Flow flow = channelFlowMap.get(channelId); + Flow flow = flowMap.get(channelId); if (flow == null) { // should never happen since we keep the channelFlowMap around if (logger.isLoggable(Level.WARNING)) { @@ -172,24 +169,14 @@ public class Http2Transport extends BaseTransport { promise.completeExceptionally(new IllegalStateException("no request")); } } else { - StatusListener statusListener = request.getStatusListener(); - if (statusListener != null) { - statusListener.onStatus(httpResponse.getStatus()); - } - for (String cookieString : httpResponse.getHeaders().getAllHeaders(HttpHeaderNames.SET_COOKIE)) { + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); addCookie(cookie); - CookieListener cookieListener = request.getCookieListener(); - if (cookieListener != null) { - cookieListener.onCookie(cookie); - } } + httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse, getCookieBox()); CompletableFuture promise = flow.get(streamId); try { - ResponseListener responseListener = request.getResponseListener(); - if (responseListener != null) { - responseListener.onResponse(httpResponse); - } + request.onResponse(httpResponse); Request retryRequest = retry(request, httpResponse); if (retryRequest != null) { // retry transport, wait for completion @@ -218,14 +205,16 @@ public class Http2Transport extends BaseTransport { } } } finally { - httpResponse.release(); + if (httpResponse != null) { + httpResponse.release(); + } } } @Override public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) { String channelId = channel.id().toString(); - channelFlowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>()); + flowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>()); String requestKey = getRequestKey(channel.id().toString(), streamId); requests.put(requestKey, requests.get(requestKey)); } diff --git a/netty-http-client/src/main/resources/META-INF/services/org.xbib.netty.http.client.api.ProtocolProvider b/netty-http-client/src/main/resources/META-INF/services/org.xbib.netty.http.client.api.ProtocolProvider new file mode 100644 index 0000000..2e1190b --- /dev/null +++ b/netty-http-client/src/main/resources/META-INF/services/org.xbib.netty.http.client.api.ProtocolProvider @@ -0,0 +1,2 @@ +org.xbib.netty.http.client.Http1Provider +org.xbib.netty.http.client.Http2Provider \ No newline at end of file diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java index 7b8cce7..36d6092 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java @@ -2,7 +2,7 @@ package org.xbib.netty.http.client.test; import org.junit.jupiter.api.Test; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpResponse; import java.io.IOException; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java index eb92374..518bc89 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java @@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; import org.xbib.net.URL; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import java.net.URI; import java.nio.charset.StandardCharsets; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttpTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttpTest.java deleted file mode 100644 index d83e4f9..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttpTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.xbib.netty.http.client.test; - -import io.netty.handler.codec.http.HttpMethod; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.logging.Level; -import java.util.logging.Logger; - -@ExtendWith(NettyHttpTestExtension.class) -class SecureHttpTest { - - private static final Logger logger = Logger.getLogger(SecureHttpTest.class.getName()); - - @Test - void testHttp1WithTlsV13() throws Exception { - Client client = Client.builder() - .setTlsProtocols(new String[] { "TLSv1.3" }) - .build(); - try { - Request request = Request.get().url("https://www.google.com/").build() - .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + - resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8) + - " status=" + resp.getStatus())); - client.execute(request).get(); - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testSequentialRequests() throws Exception { - Client client = Client.builder() - .build(); - try { - Request request1 = Request.get().url("https://google.com").build() - .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP 1.1 response: " + - resp.getBodyAsString(StandardCharsets.UTF_8))); - client.execute(request1).get(); - - // TODO decompression of frames - Request request2 = Request.get().url("https://google.com").setVersion("HTTP/2.0").build() - .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + - resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8))); - client.execute(request2).get(); - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testParallelRequests() throws IOException { - Client client = Client.builder() - .build(); - try { - Request request1 = Request.builder(HttpMethod.GET) - .url("https://google.com").setVersion("HTTP/1.1") - .build() - .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + - resp.getHeaders() + - " status=" + resp.getStatus())); - Request request2 = Request.builder(HttpMethod.GET) - .url("https://google.com").setVersion("HTTP/1.1") - .build() - .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + - resp.getHeaders() + - " status=" + resp.getStatus())); - - for (int i = 0; i < 10; i++) { - client.execute(request1); - client.execute(request2); - } - - } finally { - client.shutdownGracefully(); - } - } - -} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java index d4ae1f5..c2acccc 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java @@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test.akamai; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.client.test.NettyHttpTestExtension; import java.io.IOException; @@ -38,11 +38,11 @@ public class AkamaiTest { .url("https://http2.akamai.com/demo/h2_demo_frame.html") //.url("https://http2.akamai.com/") .setVersion("HTTP/2.0") - .build() .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus().getCode() + resp.getHeaders() + " " + resp.getBodyAsString(StandardCharsets.UTF_8)); - }); + }) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/conscrypt/ConscryptTest.java similarity index 87% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/conscrypt/ConscryptTest.java index 5209d6c..799cb6b 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/conscrypt/ConscryptTest.java @@ -1,10 +1,11 @@ -package org.xbib.netty.http.client.test; +package org.xbib.netty.http.client.test.conscrypt; import org.conscrypt.Conscrypt; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.test.NettyHttpTestExtension; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -34,11 +35,11 @@ class ConscryptTest { Request request = Request.get() .url("https://google.com") .setVersion("HTTP/1.1") - .build() .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus() + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); - }); + }) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java index 9ea09e3..601161a 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java @@ -25,7 +25,7 @@ class ClientCookieDecoderTest { void testDecodingSingleCookieV0() { long millis = System.currentTimeMillis() + 50000; String cookieString = "myCookie=myValue;expires=" + - DateTimeUtil.formatMillis(millis) + + DateTimeUtil.formatRfc1123(millis) + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;"; Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/CookieSetterHttpBinTest.java similarity index 61% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/CookieSetterHttpBinTest.java index a79fe28..b1dc0ef 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/CookieSetterHttpBinTest.java @@ -1,12 +1,16 @@ -package org.xbib.netty.http.client.test; +package org.xbib.netty.http.client.test.cookie; +import static org.junit.Assert.assertTrue; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.test.NettyHttpTestExtension; +import org.xbib.netty.http.common.cookie.Cookie; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -31,18 +35,25 @@ class CookieSetterHttpBinTest { @Test void testHttpBinCookies() throws IOException { Client client = new Client(); + AtomicBoolean success = new AtomicBoolean(); try { Request request = Request.get() .url("http://httpbin.org/cookies/set?name=value") - .build() - .setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie: " + cookie.toString())) .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus() + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); - }); + for (Cookie cookie : resp.getCookies().keySet()) { + logger.log(Level.INFO, "got cookie: " + cookie.toString()); + if ("name".equals(cookie.name()) && ("value".equals(cookie.value()))) { + success.set(true); + } + } + }) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); } + assertTrue(success.get()); } } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/GoogleTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/GoogleTest.java new file mode 100644 index 0000000..05ad670 --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/GoogleTest.java @@ -0,0 +1,100 @@ +package org.xbib.netty.http.client.test.http1; + +import io.netty.handler.codec.http.HttpMethod; +import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.test.NettyHttpTestExtension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(NettyHttpTestExtension.class) +class GoogleTest { + + private static final Logger logger = Logger.getLogger(GoogleTest.class.getName()); + + @Test + void testHttp1WithTlsV13() throws Exception { + AtomicBoolean success = new AtomicBoolean(); + Client client = Client.builder() + .setTlsProtocols(new String[] { "TLSv1.3" }) + .build(); + try { + Request request = Request.get().url("https://www.google.com/") + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response: " + + resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8) + + " status=" + resp.getStatus()); + success.set(true); + }) + .build(); + client.execute(request).get(); + } finally { + client.shutdownGracefully(); + } + assertTrue(success.get()); + } + + @Test + void testSequentialRequests() throws Exception { + AtomicBoolean success = new AtomicBoolean(); + Client client = Client.builder() + .build(); + try { + Request request1 = Request.get().url("https://google.com") + .setResponseListener(resp -> { + logger.log(Level.INFO, "got HTTP 1.1 response: " + + resp.getBodyAsString(StandardCharsets.UTF_8)); + success.set(true); + }) + .build(); + client.execute(request1).get(); + } finally { + client.shutdownGracefully(); + } + assertTrue(success.get()); + } + + @Test + void testParallelRequests() throws IOException { + AtomicBoolean success1 = new AtomicBoolean(); + AtomicBoolean success2 = new AtomicBoolean(); + Client client = Client.builder() + .build(); + try { + Request request1 = Request.builder(HttpMethod.GET) + .url("https://google.com").setVersion("HTTP/1.1") + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response: " + + resp.getHeaders() + + " status=" + resp.getStatus()); + success1.set(true); + }) + .build(); + Request request2 = Request.builder(HttpMethod.GET) + .url("https://google.com").setVersion("HTTP/1.1") + .setResponseListener(resp -> { + logger.log(Level.INFO, "got response: " + + resp.getHeaders() + + " status=" + resp.getStatus()); + success2.set(true); + }) + .build(); + for (int i = 0; i < 10; i++) { + client.execute(request1); + client.execute(request2); + } + } finally { + client.shutdownGracefully(); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } + +} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/Http1Test.java similarity index 85% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/Http1Test.java index 6666b27..90cb6d7 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/Http1Test.java @@ -1,10 +1,11 @@ -package org.xbib.netty.http.client.test; +package org.xbib.netty.http.client.test.http1; import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.test.NettyHttpTestExtension; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -21,11 +22,12 @@ class Http1Test { Client client = Client.builder() .build(); try { - Request request = Request.get().url("http://xbib.org").build() + Request request = Request.get().url("http://xbib.org") .setResponseListener(resp -> logger.log(Level.FINE, "got response: " + resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8) + - " status=" + resp.getStatus())); + " status=" + resp.getStatus())) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); @@ -37,14 +39,15 @@ class Http1Test { Client client = Client.builder() .build(); try { - Request request1 = Request.get().url("http://xbib.org").build() + Request request1 = Request.get().url("http://xbib.org") .setResponseListener(resp -> logger.log(Level.FINE, "got response: " + - resp.getBodyAsString(StandardCharsets.UTF_8))); + resp.getBodyAsString(StandardCharsets.UTF_8))) + .build(); client.execute(request1).get(); - - Request request2 = Request.get().url("http://google.com").setVersion("HTTP/1.1").build() + Request request2 = Request.get().url("http://google.com").setVersion("HTTP/1.1") .setResponseListener(resp -> logger.log(Level.FINE, "got response: " + - resp.getBodyAsString(StandardCharsets.UTF_8))); + resp.getBodyAsString(StandardCharsets.UTF_8))) + .build(); client.execute(request2).get(); } finally { client.shutdownGracefully(); @@ -58,15 +61,14 @@ class Http1Test { try { Request request1 = Request.builder(HttpMethod.GET) .url("http://xbib.org").setVersion("HTTP/1.1") - .build() .setResponseListener(resp -> logger.log(Level.FINE, "got response: " + - resp.getHeaders() + " status=" +resp.getStatus())); + resp.getHeaders() + " status=" +resp.getStatus())) + .build(); Request request2 = Request.builder(HttpMethod.GET) .url("http://xbib.org").setVersion("HTTP/1.1") - .build() .setResponseListener(resp -> logger.log(Level.FINE, "got response: " + - resp.getHeaders() + " status=" +resp.getStatus())); - + resp.getHeaders() + " status=" +resp.getStatus())) + .build(); for (int i = 0; i < 10; i++) { client.execute(request1); client.execute(request2); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/XbibTest.java similarity index 89% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/XbibTest.java index 06b45c3..15156f2 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http1/XbibTest.java @@ -1,9 +1,9 @@ -package org.xbib.netty.http.client.test; +package org.xbib.netty.http.client.test.http1; import io.netty.handler.proxy.HttpProxyHandler; import org.junit.jupiter.api.Test; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpResponse; import java.io.IOException; @@ -23,11 +23,11 @@ class XbibTest { Client client = new Client(); try { Request request = Request.get().url("http://xbib.org") - .build() .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus() + " response = " + resp.getBodyAsString(StandardCharsets.UTF_8)); - }); + }) + .build(); client.execute(request); } finally { client.shutdownGracefully(); @@ -74,9 +74,9 @@ class XbibTest { try { httpClient.execute(Request.get() .url("http://xbib.org") - .build() .setResponseListener(resp -> logger.log(Level.INFO, "status = " + resp.getStatus() + - " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)))) + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8))) + .build()) .get(); } finally { httpClient.shutdownGracefully(); @@ -91,11 +91,10 @@ class XbibTest { httpClient.execute(Request.get() .url("http://xbib.org") .setTimeoutInMillis(10) - .build() .setResponseListener(resp -> - logger.log(Level.INFO, "status = " + resp.getStatus() + - " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)) - )) + logger.log(Level.INFO, "status = " + resp.getStatus() + + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8))) + .build()) .get(); } finally { httpClient.shutdownGracefully(); @@ -109,21 +108,20 @@ class XbibTest { httpClient.execute(Request.get() .setVersion("HTTP/1.1") .url("http://xbib.org") - .build() .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus() + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); - })) + }) + .build()) .get(); - httpClient.execute(Request.get() .setVersion("HTTP/1.1") .url("http://xbib.org") - .build() .setResponseListener(resp -> { logger.log(Level.INFO, "status = " + resp.getStatus() + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); - })) + }) + .build()) .get(); } finally { httpClient.shutdownGracefully(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2/GoogleTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2/GoogleTest.java new file mode 100644 index 0000000..cf3636f --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2/GoogleTest.java @@ -0,0 +1,30 @@ +package org.xbib.netty.http.client.test.http2; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class GoogleTest { + + private static final Logger logger = Logger.getLogger(GoogleTest.class.getName()); + + @Test + void testSequentialRequests() throws Exception { + Client client = Client.builder() + .build(); + try { + // TODO decompression of frames + Request request2 = Request.get().url("https://google.com").setVersion("HTTP/2.0") + .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + + resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8))) + .build(); + client.execute(request2).get(); + } finally { + client.shutdownGracefully(); + } + } + +} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2push/Http2PushTest.java similarity index 88% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2push/Http2PushTest.java index b26d21e..09a05b1 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/http2push/Http2PushTest.java @@ -1,11 +1,11 @@ -package org.xbib.netty.http.client.test.htt2push; +package org.xbib.netty.http.client.test.http2push; import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.client.test.NettyHttpTestExtension; import java.io.IOException; @@ -27,9 +27,9 @@ class Http2PushTest { try { Request request = Request.builder(HttpMethod.GET) .url(url).setVersion("HTTP/2.0") - .build() .setResponseListener(resp -> logger.log(Level.INFO, - "got response: " + resp.getHeaders() + " status=" + resp.getStatus())); + "got response: " + resp.getHeaders() + " status=" + resp.getStatus())) + .build(); client.execute(request).get(); } finally { diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java index 2cf9ba2..a60e36d 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.pool.Pool; +import org.xbib.netty.http.client.api.Pool; import org.xbib.netty.http.client.pool.BoundedChannelPool; import java.io.Closeable; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java index 4adb343..3d758d9 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.pool.Pool; +import org.xbib.netty.http.client.api.Pool; import org.xbib.netty.http.client.pool.BoundedChannelPool; import java.io.Closeable; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java index fb8444d..f0beac3 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.pool.BoundedChannelPool; -import org.xbib.netty.http.client.pool.Pool; +import org.xbib.netty.http.client.api.Pool; import java.util.ArrayList; import java.util.List; @@ -47,10 +47,10 @@ class PoolTest { ServerBootstrap serverBootstrap = new ServerBootstrap() .group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) { - } + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + } }); Channel serverChannel = serverBootstrap.bind("localhost", 8008).sync().channel(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java index 1b63e32..b50d4d8 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java @@ -5,10 +5,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.net.URL; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.api.ResponseListener; import org.xbib.netty.http.client.test.NettyHttpTestExtension; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpResponse; import java.io.IOException; @@ -49,8 +49,8 @@ class PooledClientTest { for (int i = 0; i < loop; i++) { Request request = Request.get().setVersion(httpAddress.getVersion()) .url(url.toString()) - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); client.newTransport().execute(request).get(); } logger.log(Level.INFO, "done " + Thread.currentThread()); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java index 168f312..f7296ba 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java @@ -1,7 +1,7 @@ package org.xbib.netty.http.client.test.retry; import org.junit.jupiter.api.Test; -import org.xbib.netty.http.client.retry.BackOff; +import org.xbib.netty.http.client.api.BackOff; import org.xbib.netty.http.client.retry.ExponentialBackOff; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java index e72c4db..a48ca9a 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java @@ -1,8 +1,6 @@ package org.xbib.netty.http.client.test.retry; -import org.xbib.netty.http.client.retry.BackOff; - -import java.io.IOException; +import org.xbib.netty.http.client.api.BackOff; /** * Mock for {@link BackOff} that always returns a fixed number. diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java index 8989c60..2043d53 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java @@ -1,7 +1,7 @@ package org.xbib.netty.http.client.test.retry; import org.junit.jupiter.api.Test; -import org.xbib.netty.http.client.retry.BackOff; +import org.xbib.netty.http.client.api.BackOff; import java.io.IOException; diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java index 8c3e3eb..7f9c691 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java @@ -4,7 +4,7 @@ import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.client.test.NettyHttpTestExtension; import java.io.IOException; @@ -21,8 +21,9 @@ class WebtideTest { Client client = Client.builder() .build(); try { - Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0").build() - .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg)); + Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0") + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg)) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); @@ -35,16 +36,14 @@ class WebtideTest { try { Request request1 = Request.builder(HttpMethod.GET) .url("https://webtide.com").setVersion("HTTP/2.0") - .build() .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + - resp.getHeaders() + " status=" + resp.getStatus())); - + resp.getHeaders() + " status=" + resp.getStatus())) + .build(); Request request2 = Request.builder(HttpMethod.GET) .url("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0") - .build() .setResponseListener(resp -> logger.log(Level.INFO, "got response: " + - resp.getHeaders() + " status=" +resp.getStatus())); - + resp.getHeaders() + " status=" +resp.getStatus())) + .build(); client.execute(request1).execute(request2); } finally { client.shutdownGracefully(); diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/DefaultHttpResponse.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/DefaultHttpResponse.java index 5f5ea92..088eba0 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/DefaultHttpResponse.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/DefaultHttpResponse.java @@ -4,6 +4,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.handler.codec.http.FullHttpResponse; +import org.xbib.netty.http.common.cookie.CookieBox; import java.io.InputStream; import java.nio.charset.Charset; @@ -17,11 +18,16 @@ public class DefaultHttpResponse implements HttpResponse { private final HttpHeaders httpHeaders; - public DefaultHttpResponse(HttpAddress httpAddress, FullHttpResponse fullHttpResponse) { + private final CookieBox cookieBox; + + public DefaultHttpResponse(HttpAddress httpAddress, + FullHttpResponse fullHttpResponse, + CookieBox cookieBox) { this.httpAddress = httpAddress; this.fullHttpResponse = fullHttpResponse.retain(); this.httpStatus = new HttpStatus(this.fullHttpResponse.status()); this.httpHeaders = new DefaultHttpHeaders(this.fullHttpResponse.headers()); + this.cookieBox = cookieBox; } @Override @@ -39,6 +45,11 @@ public class DefaultHttpResponse implements HttpResponse { return httpHeaders; } + @Override + public CookieBox getCookies() { + return cookieBox; + } + @Override public ByteBuf getBody() { return fullHttpResponse.content(); diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java index 7667bcd..347b23b 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java @@ -10,6 +10,8 @@ import java.net.InetSocketAddress; */ public class HttpAddress implements PoolKey { + public static final HttpVersion HTTP_1_1 = HttpVersion.valueOf("HTTP/1.1"); + public static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); private final String host; @@ -23,19 +25,19 @@ public class HttpAddress implements PoolKey { private InetSocketAddress inetSocketAddress; public static HttpAddress http1(String host) { - return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); + return new HttpAddress(host, 80, HTTP_1_1, false); } public static HttpAddress http1(String host, int port) { - return new HttpAddress(host, port, HttpVersion.HTTP_1_1, false); + return new HttpAddress(host, port, HTTP_1_1, false); } public static HttpAddress secureHttp1(String host) { - return new HttpAddress(host, 443, HttpVersion.HTTP_1_1, true); + return new HttpAddress(host, 443, HTTP_1_1, true); } public static HttpAddress secureHttp1(String host, int port) { - return new HttpAddress(host, port, HttpVersion.HTTP_1_1, true); + return new HttpAddress(host, port, HTTP_1_1, true); } public static HttpAddress http2(String host) { @@ -55,7 +57,7 @@ public class HttpAddress implements PoolKey { } public static HttpAddress http1(URL url) { - return new HttpAddress(url, HttpVersion.HTTP_1_1); + return new HttpAddress(url, HTTP_1_1); } public static HttpAddress http2(URL url) { @@ -63,7 +65,7 @@ public class HttpAddress implements PoolKey { } public static HttpAddress of(URL url) { - return new HttpAddress(url, HttpVersion.HTTP_1_1); + return new HttpAddress(url, HTTP_1_1); } public static HttpAddress of(URL url, HttpVersion httpVersion) { @@ -95,7 +97,9 @@ public class HttpAddress implements PoolKey { } public URL base() { - return isSecure() ? URL.https().host(host).port(port).build() : URL.http().host(host).port(port).build(); + return isSecure() ? + URL.https().host(host).port(port).build() : + URL.http().host(host).port(port).build(); } public HttpVersion getVersion() { diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java index a91c8bd..0daee88 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java @@ -1,11 +1,14 @@ package org.xbib.netty.http.common; +import io.netty.handler.codec.http.HttpHeaderValues; import org.xbib.net.PercentDecoder; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; import org.xbib.netty.http.common.util.LimitedSet; import org.xbib.netty.http.common.util.LimitedTreeMap; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnmappableCharacterException; @@ -34,8 +37,6 @@ public class HttpParameters implements Map> { private static final String AMPERSAND = "&"; - private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; - private final int maxParam; private final int sizeLimit; @@ -48,24 +49,30 @@ public class HttpParameters implements Map> { private final PercentDecoder percentDecoder; - private final String contentType; + private final CharSequence contentType; + + private final Charset charset; public HttpParameters() { - this(1024, 1024, 65536, APPLICATION_X_WWW_FORM_URLENCODED); + this(1024, 1024, 65536, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8); } public HttpParameters(String contentType) { - this(1024, 1024, 65536, contentType); + this(1024, 1024, 65536, + contentType, StandardCharsets.UTF_8); } - public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit, String contentType) { + public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit, + CharSequence contentType, Charset charset) { this.maxParam = maxParam; this.sizeLimit = sizeLimit; this.elementSizeLimit = elementSizeLimit; this.map = new LimitedTreeMap<>(maxParam); - this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + this.percentEncoder = PercentEncoders.getQueryEncoder(charset); this.percentDecoder = new PercentDecoder(); this.contentType = contentType; + this.charset = charset; } @Override @@ -139,8 +146,7 @@ public class HttpParameters implements Map> { return map.entrySet(); } - public SortedSet put(String key, SortedSet values, boolean percentEncode) - throws MalformedInputException, UnmappableCharacterException { + public SortedSet put(String key, SortedSet values, boolean percentEncode) { if (percentEncode) { remove(key); for (String v : values) { @@ -158,11 +164,8 @@ public class HttpParameters implements Map> { * @param key the parameter name * @param value the parameter value * @return the value - * @throws MalformedInputException if input is malformed - * @throws UnmappableCharacterException if characters are unmappable */ - public String add(String key, String value) - throws MalformedInputException, UnmappableCharacterException { + public String add(String key, String value) { return add(key, value, false); } @@ -175,21 +178,23 @@ public class HttpParameters implements Map> { * @param percentEncode whether key and value should be percent encoded before being * inserted into the map * @return the value - * @throws MalformedInputException if input is malformed - * @throws UnmappableCharacterException if characters are unmappable */ - public String add(String key, String value, boolean percentEncode) - throws MalformedInputException, UnmappableCharacterException { - String k = percentEncode ? percentEncoder.encode(key) : key; - SortedSet values = map.get(k); - if (values == null) { - values = new LimitedSet<>(sizeLimit, elementSizeLimit); - map.put(k, values); - } + public String add(String key, String value, boolean percentEncode) { String v = null; - if (value != null) { - v = percentEncode ? percentEncoder.encode(value) : value; - values.add(v); + try { + String k = percentEncode ? percentEncoder.encode(key) : key; + SortedSet values = map.get(k); + if (values == null) { + values = new LimitedSet<>(sizeLimit, elementSizeLimit); + map.put(k, values); + } + + if (value != null) { + v = percentEncode ? percentEncoder.encode(value) : value; + values.add(v); + } + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); } return v; } @@ -201,11 +206,8 @@ public class HttpParameters implements Map> { * @param key the parameter name * @param nullString can be anything, but probably... null? * @return null - * @throws MalformedInputException if input is malformed - * @throws UnmappableCharacterException if characters are unmappable */ - public String addNull(String key, String nullString) - throws MalformedInputException, UnmappableCharacterException { + public String addNull(String key, String nullString) { return add(key, nullString); } @@ -220,8 +222,7 @@ public class HttpParameters implements Map> { } } - public void addAll(String[] keyValuePairs, boolean percentEncode) - throws MalformedInputException, UnmappableCharacterException { + public void addAll(String[] keyValuePairs, boolean percentEncode) { for (int i = 0; i < keyValuePairs.length - 1; i += 2) { add(keyValuePairs[i], keyValuePairs[i + 1], percentEncode); } @@ -274,7 +275,7 @@ public class HttpParameters implements Map> { return percentDecoder.decode(value); } - public String getContentType() { + public CharSequence getContentType() { return contentType; } @@ -340,7 +341,8 @@ public class HttpParameters implements Map> { } public HttpParameters getOAuthParameters() { - HttpParameters oauthParams = new HttpParameters(maxParam, sizeLimit, elementSizeLimit, contentType); + HttpParameters oauthParams = + new HttpParameters(maxParam, sizeLimit, elementSizeLimit, contentType, StandardCharsets.UTF_8); entrySet().stream().filter(entry -> entry.getKey().startsWith("oauth_") || entry.getKey().startsWith("x_oauth_")) .forEach(entry -> oauthParams.put(entry.getKey(), entry.getValue())); return oauthParams; diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java index 5ec8255..790631c 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java @@ -2,6 +2,7 @@ package org.xbib.netty.http.common; import io.netty.buffer.ByteBuf; +import org.xbib.netty.http.common.cookie.CookieBox; import java.io.InputStream; import java.nio.charset.Charset; @@ -13,6 +14,8 @@ public interface HttpResponse { HttpHeaders getHeaders(); + CookieBox getCookies(); + ByteBuf getBody(); InputStream getBodyAsStream(); diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtil.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtil.java index c07b535..ee90fd4 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtil.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtil.java @@ -21,16 +21,12 @@ public class DateTimeUtil { private DateTimeUtil() { } - public static String formatInstant(Instant instant) { + public static String formatRfc1123(Instant instant) { return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); } - public static String formatMillis(long millis) { - return formatInstant(Instant.ofEpochMilli(millis)); - } - - public static String formatSeconds(long seconds) { - return formatInstant(Instant.now().plusSeconds(seconds)); + public static String formatRfc1123(long millis) { + return formatRfc1123(Instant.ofEpochMilli(millis)); } // RFC 2616 allows RFC 1123, RFC 1036, ASCII time diff --git a/netty-http-rx/NOTICE.txt b/netty-http-rx/NOTICE.txt new file mode 100644 index 0000000..3241e88 --- /dev/null +++ b/netty-http-rx/NOTICE.txt @@ -0,0 +1,5 @@ +This work is based on + +https://github.com/ReactiveX/RxNetty + +(branch 0.5.x as of 22-Sep-2019) diff --git a/netty-http-rx/build.gradle b/netty-http-rx/build.gradle new file mode 100644 index 0000000..b20db0d --- /dev/null +++ b/netty-http-rx/build.gradle @@ -0,0 +1,7 @@ +dependencies { + compile "io.netty:netty-codec-http:${project.property('netty.version')}" + compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" + compile "io.reactivex:rxjava:${project.property('reactivex.version')}" + testCompile "org.hamcrest:hamcrest-all:${project.property('hamcrest.version')}" + testCompile "org.mockito:mockito-all:${project.property('mockito.version')}" +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/HandlerNames.java b/netty-http-rx/src/main/java/io/reactivex/netty/HandlerNames.java new file mode 100644 index 0000000..14bf1f7 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/HandlerNames.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty; + +/** + * A list of all handler names added by the framework. This is just to ensure consistency in naming. + */ +public enum HandlerNames { + + SslHandler("ssl-handler"), + SslConnectionEmissionHandler("ssl-connection-emitter"), + WireLogging("wire-logging-handler"), + WriteTransformer("write-transformer"), + ClientReadTimeoutHandler("client-read-timeout-handler"), + ClientChannelActiveBufferingHandler("client-channel-active-buffer-handler"), + ; + + private final String name; + + HandlerNames(String name) { + this.name = qualify(name); + } + + public String getName() { + return name; + } + + private static String qualify(String name) { + return "_rx_netty_" + name; + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java b/netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java new file mode 100644 index 0000000..1eb4986 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java @@ -0,0 +1,97 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty; + +import io.reactivex.netty.threads.RxEventLoopProvider; +import io.reactivex.netty.threads.SingleNioLoopProvider; + +public final class RxNetty { + + private static volatile RxEventLoopProvider rxEventLoopProvider = new SingleNioLoopProvider(Runtime.getRuntime().availableProcessors()); + + private static volatile boolean usingNativeTransport; + private static volatile boolean disableEventPublishing; + + private RxNetty() { + } + + /** + * An implementation of {@link RxEventLoopProvider} to be used by all clients and servers created after this call. + * + * @param provider New provider to use. + * + * @return Existing provider. + */ + public static RxEventLoopProvider useEventLoopProvider(RxEventLoopProvider provider) { + RxEventLoopProvider oldProvider = rxEventLoopProvider; + rxEventLoopProvider = provider; + return oldProvider; + } + + public static RxEventLoopProvider getRxEventLoopProvider() { + return rxEventLoopProvider; + } + + /** + * A global flag to start using netty's native protocol + * if applicable for a client or server. + * + * This does not evaluate whether the native transport is available for the OS or not. + * + * So, this method should be called conditionally when the caller is sure that the OS supports the native protocol. + * + * Alternatively, this can be done selectively per client and server instance. + */ + public static void useNativeTransportIfApplicable() { + usingNativeTransport = true; + } + + /** + * A global flag to disable the effects of calling {@link #useNativeTransportIfApplicable()} + */ + public static void disableNativeTransport() { + usingNativeTransport = false; + } + + /** + * Enables publishing of events for RxNetty. + */ + public static void enableEventPublishing() { + disableEventPublishing = false; + } + + /** + * Disables publishing of events for RxNetty. + */ + public static void disableEventPublishing() { + disableEventPublishing = true; + } + + /** + * Returns {@code true} if event publishing is disabled. + * + * @return {@code true} if event publishing is disabled. + */ + public static boolean isEventPublishingDisabled() { + return disableEventPublishing; + } + + public static boolean isUsingNativeTransport() { + return usingNativeTransport; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridge.java new file mode 100644 index 0000000..e5bd22c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridge.java @@ -0,0 +1,392 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.EmptyArrays; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.events.EventPublisher; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Producer; +import rx.Subscriber; +import rx.exceptions.MissingBackpressureException; + +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +/** + * A bridge between a {@link Connection} instance and the associated {@link Channel}. + * + * All operations on {@link Connection} will pass through this bridge to an appropriate action on the {@link Channel} + * + *

Lazy {@link Connection#getInput()} subscription

+ * + * Lazy subscriptions are allowed on {@link Connection#getInput()} if and only if the channel is configured to + * not read data automatically (i.e. {@link ChannelOption#AUTO_READ} is set to {@code false}). Otherwise, + * if {@link Connection#getInput()} is subscribed lazily, the subscriber always receives an error. The content + * in this case is disposed upon reading. + * + * @param Type read from the connection held by this handler. + * @param Type written to the connection held by this handler. + */ +public abstract class AbstractConnectionToChannelBridge extends BackpressureManagingHandler { + + private static final Logger logger = Logger.getLogger(AbstractConnectionToChannelBridge.class.getName()); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException ONLY_ONE_CONN_SUB_ALLOWED = + new IllegalStateException("Only one subscriber allowed for connection observable."); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException ONLY_ONE_CONN_INPUT_SUB_ALLOWED = + new IllegalStateException("Only one subscriber allowed for connection input."); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException LAZY_CONN_INPUT_SUB = + new IllegalStateException("Channel is set to auto-read but the subscription was lazy."); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + + static { + ONLY_ONE_CONN_INPUT_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + ONLY_ONE_CONN_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + LAZY_CONN_INPUT_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + + private final AttributeKey eventListenerAttributeKey; + private final AttributeKey eventPublisherAttributeKey; + + protected ConnectionEventListener eventListener; + protected EventPublisher eventPublisher; + private Subscriber newChannelSub; + private ReadProducer readProducer; + private boolean raiseErrorOnInputSubscription; + private boolean connectionEmitted; + + protected AbstractConnectionToChannelBridge(String thisHandlerName, ConnectionEventListener eventListener, + EventPublisher eventPublisher) { + super(thisHandlerName); + if (null == eventListener) { + throw new IllegalArgumentException("Event listener can not be null."); + } + if (null == eventPublisher) { + throw new IllegalArgumentException("Event publisher can not be null."); + } + this.eventListener = eventListener; + this.eventPublisher = eventPublisher; + eventListenerAttributeKey = null; + eventPublisherAttributeKey = null; + } + + protected AbstractConnectionToChannelBridge(String thisHandlerName, + AttributeKey eventListenerAttributeKey, + AttributeKey eventPublisherAttributeKey) { + super(thisHandlerName); + this.eventListenerAttributeKey = eventListenerAttributeKey; + this.eventPublisherAttributeKey = eventPublisherAttributeKey; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + if (null == eventListener && null == eventPublisher) { + eventListener = ctx.channel().attr(eventListenerAttributeKey).get(); + eventPublisher = ctx.channel().attr(eventPublisherAttributeKey).get(); + } + + if (null == eventPublisher) { + logger.log(Level.SEVERE, "No Event publisher bound to the channel, closing channel."); + ctx.channel().close(); + return; + } + + if (eventPublisher.publishingEnabled() && null == eventListener) { + logger.log(Level.SEVERE, "No Event listener bound to the channel and publising is enabled, closing channel."); + ctx.channel().close(); + return; + } + + ctx.pipeline().addFirst(new BytesInspector(eventPublisher, eventListener)); + + super.handlerAdded(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (!connectionEmitted && isValidToEmit(newChannelSub)) { + emitNewConnection(ctx.channel()); + connectionEmitted = true; + } + super.channelInactive(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + + if (isValidToEmitToReadSubscriber(readProducer)) { + /*If the subscriber is still active, then it expects data but the channel is closed.*/ + readProducer.sendOnError(CLOSED_CHANNEL_EXCEPTION); + } + + super.channelUnregistered(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof EmitConnectionEvent) { + if (!connectionEmitted) { + emitNewConnection(ctx.channel()); + connectionEmitted = true; + } + } else if (evt instanceof ConnectionCreationFailedEvent) { + if (isValidToEmit(newChannelSub)) { + newChannelSub.onError(((ConnectionCreationFailedEvent)evt).getThrowable()); + } + } else if (evt instanceof ChannelSubscriberEvent) { + @SuppressWarnings("unchecked") + final ChannelSubscriberEvent channelSubscriberEvent = (ChannelSubscriberEvent) evt; + + newConnectionSubscriber(channelSubscriberEvent); + } else if (evt instanceof ConnectionInputSubscriberEvent) { + @SuppressWarnings("unchecked") + ConnectionInputSubscriberEvent event = (ConnectionInputSubscriberEvent) evt; + + newConnectionInputSubscriber(ctx.channel(), event.getSubscriber(), false); + } else if (evt instanceof ConnectionInputSubscriberResetEvent) { + resetConnectionInputSubscriber(); + } else if (evt instanceof ConnectionInputSubscriberReplaceEvent) { + @SuppressWarnings("unchecked") + ConnectionInputSubscriberReplaceEvent event = (ConnectionInputSubscriberReplaceEvent) evt; + replaceConnectionInputSubscriber(ctx.channel(), event); + } + + super.userEventTriggered(ctx, evt); + } + + @SuppressWarnings("unchecked") + @Override + public void newMessage(ChannelHandlerContext ctx, Object msg) { + if (isValidToEmitToReadSubscriber(readProducer)) { + try { + readProducer.sendOnNext((R) msg); + } catch (ClassCastException e) { + ReferenceCountUtil.release(msg); // Since, this was not sent to the subscriber, release the msg. + readProducer.sendOnError(e); + } + } else { + logger.log(Level.WARNING, "Data received on channel, but no subscriber registered. Discarding data. Message class: " + + msg.getClass().getName() + ", channel: " + ctx.channel()); + ReferenceCountUtil.release(msg); // No consumer of the message, so discard. + } + } + + @Override + public boolean shouldReadMore(ChannelHandlerContext ctx) { + return null != readProducer && readProducer.shouldReadMore(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (!connectionEmitted && isValidToEmit(newChannelSub)) { + newChannelSub.onError(cause); + } else if (isValidToEmitToReadSubscriber(readProducer)) { + readProducer.sendOnError(cause); + } else { + logger.log(Level.INFO, "Exception in the pipeline and none of the subscribers are active.", cause); + } + } + + protected static boolean isValidToEmit(Subscriber subscriber) { + return null != subscriber && !subscriber.isUnsubscribed(); + } + + private static boolean isValidToEmitToReadSubscriber(ReadProducer readProducer) { + return null != readProducer && !readProducer.subscriber.isUnsubscribed(); + } + + protected boolean connectionInputSubscriberExists(Channel channel) { + assert channel.eventLoop().inEventLoop(); + + return null != readProducer && null != readProducer.subscriber && !readProducer.subscriber.isUnsubscribed(); + } + + protected void onNewReadSubscriber(Subscriber subscriber) { + // NOOP + } + + protected final void checkEagerSubscriptionIfConfigured(Channel channel) { + if (channel.config().isAutoRead() && null == readProducer) { + // If the channel is set to auto-read and there is no eager subscription then, we should raise errors + // when a subscriber arrives. + raiseErrorOnInputSubscription = true; + final Subscriber discardAll = ConnectionInputSubscriberEvent.discardAllInput() + .getSubscriber(); + final ReadProducer producer = new ReadProducer<>(discardAll, channel); + discardAll.setProducer(producer); + readProducer = producer; + } + } + + protected final Subscriber getNewChannelSub() { + return newChannelSub; + } + + private void emitNewConnection(Channel channel) { + if (isValidToEmit(newChannelSub)) { + try { + newChannelSub.onNext(channel); + connectionEmitted = true; + checkEagerSubscriptionIfConfigured(channel); + newChannelSub.onCompleted(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Error emitting a new connection. Closing this channel.", e); + channel.close(); + } + } else { + channel.close(); // Closing the connection if not sent to a subscriber. + } + } + + private void resetConnectionInputSubscriber() { + final Subscriber connInputSub = null == readProducer? null : readProducer.subscriber; + if (isValidToEmit(connInputSub)) { + connInputSub.onCompleted(); + } + raiseErrorOnInputSubscription = false; + readProducer = null; // A subsequent event should set it to the desired subscriber. + } + + private void newConnectionInputSubscriber(final Channel channel, final Subscriber subscriber, + boolean replace) { + final Subscriber connInputSub = null == readProducer ? null : readProducer.subscriber; + if (isValidToEmit(connInputSub)) { + if (!replace) { + /*Allow only once concurrent input subscriber but allow concatenated subscribers*/ + subscriber.onError(ONLY_ONE_CONN_INPUT_SUB_ALLOWED); + } else { + setNewReadProducer(channel, subscriber); + connInputSub.onCompleted(); + } + } else if (raiseErrorOnInputSubscription) { + subscriber.onError(LAZY_CONN_INPUT_SUB); + } else { + setNewReadProducer(channel, subscriber); + } + } + + private void setNewReadProducer(Channel channel, Subscriber subscriber) { + final ReadProducer producer = new ReadProducer<>(subscriber, channel); + subscriber.setProducer(producer); + onNewReadSubscriber(subscriber); + readProducer = producer; + } + + private void replaceConnectionInputSubscriber(Channel channel, ConnectionInputSubscriberReplaceEvent event) { + ConnectionInputSubscriberEvent newSubEvent = event.getNewSubEvent(); + newConnectionInputSubscriber(channel, newSubEvent.getSubscriber(), + true); + } + + private void newConnectionSubscriber(ChannelSubscriberEvent event) { + if (null == newChannelSub) { + newChannelSub = event.getSubscriber(); + } else { + event.getSubscriber().onError(ONLY_ONE_CONN_SUB_ALLOWED); + } + } + + /*Visible for testing*/ static final class ReadProducer extends RequestReadIfRequiredEvent implements Producer { + + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater REQUEST_UPDATER = + AtomicLongFieldUpdater.newUpdater(ReadProducer.class, "requested");/*Updater for requested*/ + private volatile long requested; // Updated by REQUEST_UPDATER, required to be volatile. + + private final Subscriber subscriber; + private final Channel channel; + + /*Visible for testing*/ ReadProducer(Subscriber subscriber, Channel channel) { + this.subscriber = subscriber; + this.channel = channel; + } + + @Override + public void request(long n) { + if (Long.MAX_VALUE != requested) { + if (Long.MAX_VALUE == n) { + // Now turning off backpressure + REQUEST_UPDATER.set(this, Long.MAX_VALUE); + } else { + // add n to field but check for overflow + while (true) { + final long current = requested; + long next = current + n; + // check for overflow + if (next < 0) { + next = Long.MAX_VALUE; + } + if (REQUEST_UPDATER.compareAndSet(this, current, next)) { + break; + } + } + } + } + + if (!channel.config().isAutoRead()) { + channel.pipeline().fireUserEventTriggered(this); + } + } + + public void sendOnError(Throwable throwable) { + subscriber.onError(throwable); + } + + public void sendOnComplete() { + subscriber.onCompleted(); + } + + public void sendOnNext(T nextItem) { + if (requested > 0) { + if (REQUEST_UPDATER.get(this) != Long.MAX_VALUE) { + REQUEST_UPDATER.decrementAndGet(this); + } + subscriber.onNext(nextItem); + } else { + subscriber.onError(new MissingBackpressureException( + "Received more data on the channel than demanded by the subscriber.")); + } + } + + @Override + protected boolean shouldReadMore(ChannelHandlerContext ctx) { + return !subscriber.isUnsubscribed() && REQUEST_UPDATER.get(this) > 0; + } + + /*Visible for testing*/long getRequested() { + return requested; + } + + @Override + public String toString() { + return "ReadProducer{" + "requested=" + requested + '}'; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/AllocatingTransformer.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AllocatingTransformer.java new file mode 100644 index 0000000..01b0fa2 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AllocatingTransformer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.internal.TypeParameterMatcher; +import rx.annotations.Beta; + +import java.util.List; + +/** + * A transformer to be used for modifying the type of objects written on a {@link Connection}. + * + *

Why is this required?

+ * + * The type of an object can usually be transformed using {@code Observable.map()}, however, while writing on a + * {@link Connection}, typically one requires to allocate buffers. Although a {@code Connection} provides a way to + * retrieve the {@link ByteBufAllocator} via the {@code Channel}, allocating buffers from outside the eventloop will + * lead to buffer bloats as the allocators will typically use thread-local buffer pools.

+ * + * This transformer is always invoked from within the eventloop and hence does not have buffer bloating issues, even + * when transformations happen outside the eventloop. + * + * @param Source type. + * @param Target type. + */ +@Beta +public abstract class AllocatingTransformer { + + private final TypeParameterMatcher matcher; + + protected AllocatingTransformer() { + matcher = TypeParameterMatcher.find(this, AllocatingTransformer.class, "T"); + } + + /** + * Asserts whether the passed message can be transformed using this transformer. + * + * @param msg Message to transform. + * + * @return {@code true} if the message can be transformed. + */ + protected boolean acceptMessage(Object msg) { + return matcher.match(msg); + } + + /** + * Transforms the passed message and adds the output to the returned list. + * + * @param toTransform Message to transform. + * @param allocator Allocating for allocating buffers, if required. + * + * @return Output of the transformation. + */ + public abstract List transform(T toTransform, ByteBufAllocator allocator); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/AppendTransformerEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AppendTransformerEvent.java new file mode 100644 index 0000000..9cbc39e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AppendTransformerEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +/** + * An event to register a custom transformer of data written on a channel. + * + * @param Source type for the transformer. + * @param Target type for the transformer. + */ +public final class AppendTransformerEvent { + + private final AllocatingTransformer transformer; + + public AppendTransformerEvent(AllocatingTransformer transformer) { + if (null == transformer) { + throw new NullPointerException("Transformer can not be null."); + } + this.transformer = transformer; + } + + public AllocatingTransformer getTransformer() { + return transformer; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/AutoReleaseOperator.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AutoReleaseOperator.java new file mode 100644 index 0000000..dcf977f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/AutoReleaseOperator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.util.ReferenceCountUtil; +import rx.Observable.Operator; +import rx.Subscriber; + +class AutoReleaseOperator implements Operator { + + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(subscriber) { + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(T t) { + try { + subscriber.onNext(t); + } finally { + ReferenceCountUtil.release(t); + } + } + }; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/BackpressureManagingHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/BackpressureManagingHandler.java new file mode 100644 index 0000000..6b7a53a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/BackpressureManagingHandler.java @@ -0,0 +1,710 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.RecyclableArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Scheduler; +import rx.Subscriber; +import rx.functions.Action0; +import rx.schedulers.Schedulers; +import rx.subscriptions.Subscriptions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +public abstract class BackpressureManagingHandler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(BackpressureManagingHandler.class.getName()); + + /*Visible for testing*/ enum State { + ReadRequested, + Reading, + Buffering, + DrainingBuffer, + Stopped, + } + + private RecyclableArrayList buffer; + private int currentBufferIndex; + private State currentState = State.Buffering; /*Buffer unless explicitly asked to read*/ + private boolean continueDraining; + private final BytesWriteInterceptor bytesWriteInterceptor; + + protected BackpressureManagingHandler(String thisHandlerName) { + bytesWriteInterceptor = new BytesWriteInterceptor(thisHandlerName); + } + + @SuppressWarnings("fallthrough") + @Override + public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + + if (State.Stopped != currentState && !shouldReadMore(ctx)) { + currentState = State.Buffering; + } + + switch (currentState) { + case ReadRequested: + currentState = State.Reading; + case Reading: + newMessage(ctx, msg); + break; + case Buffering: + case DrainingBuffer: + if (null == buffer) { + buffer = RecyclableArrayList.newInstance(); + } + buffer.add(msg); + break; + case Stopped: + logger.log(Level.WARNING, "Message read after handler removed, discarding the same. Message class: " + + msg.getClass().getName()); + ReferenceCountUtil.release(msg); + break; + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.pipeline().addFirst(bytesWriteInterceptor); + currentState = State.Buffering; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + /*On shut down, all the handlers are removed from the pipeline, so we don't need to explicitly remove the + additional handlers added in handlerAdded()*/ + currentState = State.Stopped; + if (null != buffer) { + if (!buffer.isEmpty()) { + for (Object item : buffer) { + ReferenceCountUtil.release(item); + } + } + buffer.recycle(); + buffer = null; + } + } + + @Override + public final void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + + switch (currentState) { + case ReadRequested: + /*Nothing read from the last request, forward to read() and let it take the decision on what to do.*/ + break; + case Reading: + /* + * After read completion, move to Buffering, unless an explicit read is issued, which moves to an + * appropriate state. + */ + currentState = State.Buffering; + break; + case Buffering: + /*Keep buffering, unless the buffer drains and more items are requested*/ + break; + case DrainingBuffer: + /*Keep draining, unless the buffer drains and more items are requested*/ + break; + case Stopped: + break; + } + + ctx.fireChannelReadComplete(); + + if (!ctx.channel().config().isAutoRead() && shouldReadMore(ctx)) { + read(ctx); + } + } + + @Override + public final void read(ChannelHandlerContext ctx) throws Exception { + switch (currentState) { + case ReadRequested: + /*Nothing read since last request, but requested more, so push the demand upstream.*/ + ctx.read(); + break; + case Reading: + /* + * We are already reading data and the read has not completed as that would move the state to buffering. + * So, ignore this read, or otherwise, read is requested on the channel, unnecessarily. + */ + break; + case Buffering: + /* + * We were buffering and now a read was requested, so start draining the buffer. + */ + currentState = State.DrainingBuffer; + continueDraining = true; + /* + * Looping here to drain, instead of having it done via readComplete -> read -> readComplete loop to reduce + * call stack depth. Otherwise, the stackdepth is proportional to number of items in the buffer and hence + * for large buffers will overflow stack. + */ + while (continueDraining && null != buffer && currentBufferIndex < buffer.size()) { + Object nextItem = buffer.get(currentBufferIndex++); + newMessage(ctx, nextItem); /*Send the next message.*/ + /* + * If there is more read demand then that should come as part of read complete or later as another + * read (this method) invocation. */ + continueDraining = false; + channelReadComplete(ctx); + } + + if (continueDraining) { + if (null != buffer) { + /*Outstanding read demand and buffer is empty, so recycle the buffer and pass the read upstream.*/ + recycleBuffer(); + } + /* + * Since, continueDraining is true and we have broken out of the drain loop, it means that there are no + * items in the buffer and there is more read demand. Switch to read requested and send the read demand + * downstream. + */ + currentState = State.ReadRequested; + ctx.read(); + } else { + /* + * There is no more demand, so set the state to buffering and so another read invocation can start + * draining. + */ + currentState = State.Buffering; + /*If buffer is empty, then recycle.*/ + if (null != buffer && currentBufferIndex >= buffer.size()) { + recycleBuffer(); + } + } + break; + case DrainingBuffer: + /*Already draining buffer, so break the call stack and let the caller keep draining.*/ + continueDraining = true; + break; + case Stopped: + /*Invalid, pass it downstream.*/ + ctx.read(); + break; + } + } + + /** + * Intercepts a write on the channel. The following message types are handled: + * + *

    +
  • String: If the pipeline is not configured to write a String, this converts the string to a {@link io.netty.buffer.ByteBuf} and + then writes it on the channel.
  • +
  • byte[]: If the pipeline is not configured to write a byte[], this converts the byte[] to a {@link io.netty.buffer.ByteBuf} and + then writes it on the channel.
  • +
  • Observable: Subscribes to the {@link Observable} and writes all items, requesting the next item if and only if + the channel is writable as indicated by {@link Channel#isWritable()}
  • +
+ * + * @param ctx Channel handler context. + * @param msg Message to write. + * @param promise Promise for the completion of write. + * + * @throws Exception If there is an error handling this write. + */ + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof Observable) { + @SuppressWarnings("rawtypes") + Observable observable = (Observable) msg; /*One can write heterogeneous objects on a channel.*/ + final WriteStreamSubscriber subscriber = bytesWriteInterceptor.newSubscriber(ctx, promise); + subscriber.subscribeTo(observable); + } else { + ctx.write(msg, promise); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof RequestReadIfRequiredEvent) { + RequestReadIfRequiredEvent requestReadIfRequiredEvent = (RequestReadIfRequiredEvent) evt; + if (requestReadIfRequiredEvent.shouldReadMore(ctx)) { + read(ctx); + } + } + + super.userEventTriggered(ctx, evt); + } + + protected abstract void newMessage(ChannelHandlerContext ctx, Object msg); + + protected abstract boolean shouldReadMore(ChannelHandlerContext ctx); + + /*Visible for testing*/ RecyclableArrayList getBuffer() { + return buffer; + } + + /*Visible for testing*/ int getCurrentBufferIndex() { + return currentBufferIndex; + } + + /*Visible for testing*/ State getCurrentState() { + return currentState; + } + + private void recycleBuffer() { + buffer.recycle(); + currentBufferIndex = 0; + buffer = null; + } + + protected static abstract class RequestReadIfRequiredEvent { + + protected abstract boolean shouldReadMore(ChannelHandlerContext ctx); + } + + /** + * This handler inspects write to see if a write made it to {@link BytesWriteInterceptor} inline with a write call. + * The reasons why a write would not make it to the channel, would be: + *
    +
  • If there is a handler in the pipeline that runs in a different group.
  • +
  • If there is a handler that collects many items to produce a single item.
  • +
+ * + * When a write did not reach the {@link BytesWriteInterceptor}, no request for more items will be generated and + * we could get into a deadlock where a handler is waiting for more items (collect case) but no more items arrive as + * no more request is generated. In order to avoid this deadlock, this handler will detect the situation and + * trigger more request in this case. + * + * Why a separate handler? + * + * This needs to be different than {@link BytesWriteInterceptor} as we need it immediately after + * {@link BackpressureManagingHandler} so that no other handler eats a write and {@link BytesWriteInterceptor} is + * always the first handler in the pipeline to be right before the channel and hence maintain proper demand. + */ + static final class WriteInspector extends ChannelDuplexHandler { + + private final BytesWriteInterceptor bytesWriteInterceptor; + + WriteInspector(BytesWriteInterceptor bytesWriteInterceptor) { + this.bytesWriteInterceptor = bytesWriteInterceptor; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + /*Both these handlers always run in the same executor, so it's safe to access this variable.*/ + bytesWriteInterceptor.messageReceived = false; /*reset flag for this write*/ + ctx.write(msg, promise); + if (!bytesWriteInterceptor.messageReceived) { + bytesWriteInterceptor.requestMoreIfWritable(ctx.channel()); + } + } + } + + /** + * Regulates write->request more->write process on the channel. + * + * Why is this a separate handler? + * The sole purpose of this handler is to request more items from each of the Observable streams producing items to + * write. It is important to request more items only when the current item is written on the channel i.e. added to + * the ChannelOutboundBuffer. If we request more from outside the pipeline (from WriteStreamSubscriber.onNext()) + * then it may so happen that the onNext is not from within this eventloop and hence instead of being written to + * the channel, is added to the task queue of the EventLoop. Requesting more items in such a case, would mean we + * keep adding the writes to the eventloop queue and not on the channel buffer. This would mean that the channel + * writability would not truly indicate the buffer. + */ + /*Visible for testing*/ static final class BytesWriteInterceptor extends ChannelDuplexHandler implements Runnable { + + /*Visible for testing*/ static final String WRITE_INSPECTOR_HANDLER_NAME = "write-inspector"; + /*Visible for testing*/ static final int MAX_PER_SUBSCRIBER_REQUEST = 64; + + /* + * Since, unsubscribes can happen on a different thread, this has to be thread-safe. + */ + private final ConcurrentLinkedQueue subscribers = new ConcurrentLinkedQueue<>(); + private final String parentHandlerName; + + /* This should always be access from the eventloop and can be used to manage state before and after a write to + * see if a write started from {@link WriteInspector} made it to this handler. + */ + private boolean messageReceived; + + /** + * The intent here is to equally divide the request to all subscribers but do not put a hard-bound on whether + * the subscribers are actually adhering to the limit (by not throwing MissingBackpressureException). This keeps + * the request distribution simple and still give opprotunities for subscribers to optimize (increase the limit) + * if there is a signal that the consumption is slower than the producer. + * + * Worst case of this scheme is request-1 per subscriber which happens when there are as many subscribers as + * the max limit. + */ + private int perSubscriberMaxRequest = MAX_PER_SUBSCRIBER_REQUEST; + private Channel channel; + private boolean removeTaskScheduled; // Guarded by this + + BytesWriteInterceptor(String parentHandlerName) { + this.parentHandlerName = parentHandlerName; + } + + @Override + public void write(ChannelHandlerContext ctx, final Object msg, ChannelPromise promise) throws Exception { + ctx.write(msg, promise); + messageReceived = true; + requestMoreIfWritable(ctx.channel()); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + channel = ctx.channel(); + WriteInspector writeInspector = new WriteInspector(this); + ChannelHandler parent = ctx.pipeline().get(parentHandlerName); + if (null != parent) { + ctx.pipeline().addBefore(parentHandlerName, WRITE_INSPECTOR_HANDLER_NAME, writeInspector); + } + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isWritable()) { + requestMoreIfWritable(ctx.channel()); + } + super.channelWritabilityChanged(ctx); + } + + public WriteStreamSubscriber newSubscriber(final ChannelHandlerContext ctx, ChannelPromise promise) { + int currentSubCount = subscribers.size(); + recalculateMaxPerSubscriber(currentSubCount, currentSubCount + 1); + + final WriteStreamSubscriber sub = new WriteStreamSubscriber(ctx, promise, perSubscriberMaxRequest); + sub.add(Subscriptions.create(new Action0() { + @Override + public void call() { + boolean _schedule; + /*Schedule the task once as the task runs through and removes all unsubscribed subscribers*/ + synchronized (BytesWriteInterceptor.this) { + _schedule = !removeTaskScheduled; + removeTaskScheduled = true; + } + if (_schedule) { + ctx.channel().eventLoop().execute(BytesWriteInterceptor.this); + } + } + })); + + subscribers.add(sub); + return sub; + } + + /*Visible for testing*/List getSubscribers() { + return Collections.unmodifiableList(new ArrayList<>(subscribers)); + } + + private void requestMoreIfWritable(Channel channel) { + assert channel.eventLoop().inEventLoop(); + + for (WriteStreamSubscriber subscriber: subscribers) { + if (!subscriber.isUnsubscribed() && channel.isWritable()) { + subscriber.requestMoreIfNeeded(perSubscriberMaxRequest); + } + } + } + + @Override + public void run() { + synchronized (this) { + removeTaskScheduled = false; + } + int oldSubCount = subscribers.size(); + for (Iterator iterator = subscribers.iterator(); iterator.hasNext(); ) { + WriteStreamSubscriber subscriber = iterator.next(); + if (subscriber.isUnsubscribed()) { + iterator.remove(); + } + } + int newSubCount = subscribers.size(); + recalculateMaxPerSubscriber(oldSubCount, newSubCount); + } + + /** + * Called from within the eventloop, whenever the subscriber queue is modified. This modifies the per subscriber + * request limit by equally distributing the demand. Minimum demand to any subscriber is 1. + */ + private void recalculateMaxPerSubscriber(int oldSubCount, int newSubCount) { + assert channel.eventLoop().inEventLoop(); + perSubscriberMaxRequest = newSubCount == 0 || oldSubCount == 0 + ? MAX_PER_SUBSCRIBER_REQUEST + : perSubscriberMaxRequest * oldSubCount / newSubCount; + + perSubscriberMaxRequest = Math.max(1, perSubscriberMaxRequest); + + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Channel " + channel + + " modifying per subscriber max request. Old subscribers count " + oldSubCount + + " new subscribers count " + newSubCount + + " new Value {} " + perSubscriberMaxRequest); + } + } + } + + /** + * Backpressure enabled subscriber to an Observable written on this channel. This connects the promise for writing + * the Observable to all the promises created per write (per onNext). + */ + /*Visible for testing*/static class WriteStreamSubscriber extends Subscriber { + + private final ChannelHandlerContext ctx; + private final ChannelPromise overarchingWritePromise; + + private final int initialRequest; + private long maxBufferSize; + private long pending; /*Guarded by guard*/ + private long lowWaterMark; + + private final Object guard = new Object(); + private boolean isDone; /*Guarded by guard*/ + private Scheduler.Worker writeWorker; /*Guarded by guard*/ + private boolean atleastOneWriteEnqueued; /*Guarded by guard*/ + private int enqueued; /*Guarded by guard*/ + + private boolean isPromiseCompletedOnWriteComplete; /*Guarded by guard. Only transition should be false->true*/ + + private int listeningTo; + + /*Visible for testing*/ WriteStreamSubscriber(ChannelHandlerContext ctx, ChannelPromise promise, + int initialRequest) { + this.ctx = ctx; + overarchingWritePromise = promise; + this.initialRequest = initialRequest; + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isCancelled()) { + unsubscribe(); /*Unsubscribe from source if the promise is cancelled.*/ + } + } + }); + } + + @Override + public void onStart() { + requestMoreIfNeeded(initialRequest); + } + + @Override + public void onCompleted() { + onTermination(null); + } + + @Override + public void onError(Throwable e) { + onTermination(e); + } + + @Override + public void onNext(Object nextItem) { + final boolean enqueue; + boolean inEL = ctx.channel().eventLoop().inEventLoop(); + + synchronized (guard) { + pending--; + if (null == writeWorker) { + if (!inEL) { + atleastOneWriteEnqueued = true; + } + if (atleastOneWriteEnqueued) { + writeWorker = Schedulers.computation().createWorker(); + } + } + + enqueue = null != writeWorker && (inEL || enqueued > 0); + + if (enqueue) { + enqueued++; + } + } + + final ChannelFuture channelFuture = enqueue ? enqueueWrite(nextItem) : ctx.write(nextItem); + + synchronized (guard) { + listeningTo++; + } + + channelFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + + if (overarchingWritePromise.isDone()) { + /* + * Overarching promise will be done if and only if there was an error or all futures have + * completed. In both cases, this callback is useless, hence return from here. + * IOW, if we are here, it can be two cases: + * + * - There has already been a write that has failed. So, the promise is done with failure. + * - There was a write that arrived after termination of the Observable. + * + * Two above isn't possible as per Rx contract. + * One above is possible but is not of any consequence w.r.t this listener as this listener does + * not give callbacks to specific writes + */ + return; + } + + boolean _isPromiseCompletedOnWriteComplete; + + /* + * The intent here is to NOT give listener callbacks via promise completion within the sync block. + * So, a co-ordination b/w the thread sending Observable terminal event and thread sending write + * completion event is required. + * The only work to be done in the Observable terminal event thread is to whether the + * overarchingWritePromise is to be completed or not. + * The errors are not buffered, so the overarchingWritePromise is completed in this callback w/o + * knowing whether any more writes will arive or not. + * This co-oridantion is done via the flag isPromiseCompletedOnWriteComplete + */ + synchronized (guard) { + listeningTo--; + if (0 == listeningTo && isDone) { + /* + * If the listening count is 0 and no more items will arrive, this thread wins the race of + * completing the overarchingWritePromise + */ + isPromiseCompletedOnWriteComplete = true; + } + _isPromiseCompletedOnWriteComplete = isPromiseCompletedOnWriteComplete; + } + + /* + * Exceptions are not buffered but completion is only sent when there are no more items to be + * received for write. + */ + if (!future.isSuccess()) { + overarchingWritePromise.tryFailure(future.cause()); + /* + * Unsubscribe this subscriber when write fails as we are completing the promise which is + * attached to the listener of the write results. + */ + unsubscribe(); + } else if (_isPromiseCompletedOnWriteComplete) { /*Once set to true, never goes back to false.*/ + /*Complete only when no more items will arrive and all writes are completed*/ + overarchingWritePromise.trySuccess(); + } + } + }); + } + + private ChannelFuture enqueueWrite(final Object nextItem) { + final ChannelPromise toReturn = ctx.channel().newPromise(); + writeWorker.schedule(new Action0() { + @Override + public void call() { + ctx.write(nextItem, toReturn); + synchronized (guard) { + enqueued--; + } + } + }); + return toReturn; + } + + private void onTermination(Throwable throwableIfAny) { + int _listeningTo; + boolean _shouldCompletePromise; + final boolean enqueueFlush; + + /* + * The intent here is to NOT give listener callbacks via promise completion within the sync block. + * So, a co-ordination b/w the thread sending Observable terminal event and thread sending write + * completion event is required. + * The only work to be done in the Observable terminal event thread is to whether the + * overarchingWritePromise is to be completed or not. + * The errors are not buffered, so the overarchingWritePromise is completed in this callback w/o + * knowing whether any more writes will arive or not. + * This co-oridantion is done via the flag isPromiseCompletedOnWriteComplete + */ + synchronized (guard) { + enqueueFlush = atleastOneWriteEnqueued; + isDone = true; + _listeningTo = listeningTo; + /* + * Flag to indicate whether the write complete thread won the race and will complete the + * overarchingWritePromise + */ + _shouldCompletePromise = 0 == _listeningTo && !isPromiseCompletedOnWriteComplete; + } + + if (enqueueFlush) { + writeWorker.schedule(new Action0() { + @Override + public void call() { + ctx.flush(); + } + }); + } + + if (null != throwableIfAny) { + overarchingWritePromise.tryFailure(throwableIfAny); + } else { + if (_shouldCompletePromise) { + overarchingWritePromise.trySuccess(); + } + } + } + + /** + * Signals this subscriber to request more data from upstream, optionally modifying the max buffer size or max + * requests upstream. This will request more either if the new buffer size is greater than existing or pending + * items from upstream are less than the low water mark (which is half the max size). + * + * @param newMaxBufferSize New max buffer size, ignored if it is the same as existing. + */ + /*Visible for testing*/void requestMoreIfNeeded(long newMaxBufferSize) { + long toRequest = 0; + + synchronized (guard) { + if (newMaxBufferSize > maxBufferSize) { + // Applicable only when request up is not triggered by pending < lowWaterMark. + toRequest = newMaxBufferSize - maxBufferSize; + } + + maxBufferSize = newMaxBufferSize; + lowWaterMark = maxBufferSize / 2; + + if (pending < lowWaterMark) { + // Intentionally overwrites the existing toRequest as this includes all required changes. + toRequest = maxBufferSize - pending; + } + + pending += toRequest; + } + + if (toRequest > 0) { + request(toRequest); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public void subscribeTo(Observable observable) { + observable.subscribe(this); /*Need safe subscription as this is the subscriber and not a sub passed in*/ + } + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/BytesInspector.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/BytesInspector.java new file mode 100644 index 0000000..481aafb --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/BytesInspector.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.FileRegion; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.events.EventPublisher; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BytesInspector extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(BytesInspector.class.getName()); + + private final ConnectionEventListener eventListener; + private final EventPublisher eventPublisher; + + public BytesInspector(EventPublisher eventPublisher, ConnectionEventListener eventListener) { + this.eventPublisher = eventPublisher; + this.eventListener = eventListener; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + if (ByteBuf.class.isAssignableFrom(msg.getClass())) { + publishBytesRead((ByteBuf) msg); + } else if (ByteBufHolder.class.isAssignableFrom(msg.getClass())) { + ByteBufHolder holder = (ByteBufHolder) msg; + publishBytesRead(holder.content()); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to publish bytes read metrics event. This does *not* stop the pipeline processing.", e); + } finally { + super.channelRead(ctx, msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + try { + if (ByteBuf.class.isAssignableFrom(msg.getClass())) { + publishBytesWritten(((ByteBuf) msg).readableBytes(), promise); + } else if (ByteBufHolder.class.isAssignableFrom(msg.getClass())) { + publishBytesWritten(((ByteBufHolder)msg).content().readableBytes(), promise); + } else if (FileRegion.class.isAssignableFrom(msg.getClass())) { + publishBytesWritten(((FileRegion) msg).count(), promise); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to publish bytes write metrics event. This does *not* stop the pipeline processing.", e); + } finally { + super.write(ctx, msg, promise); + } + } + + @SuppressWarnings("unchecked") + protected void publishBytesWritten(final long bytesToWrite, ChannelPromise promise) { + if (bytesToWrite <= 0) { + return; + } + + if (eventPublisher.publishingEnabled()) { + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + eventListener.onByteWritten(bytesToWrite); + } + }); + } + } + + @SuppressWarnings("unchecked") + protected void publishBytesRead(ByteBuf byteBuf) { + if (null != byteBuf) { + eventListener.onByteRead(byteBuf.readableBytes()); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelOperations.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelOperations.java new file mode 100644 index 0000000..0b98c7c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelOperations.java @@ -0,0 +1,285 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.FileRegion; +import io.netty.util.AttributeKey; +import rx.Observable; +import rx.functions.Func1; + +/** + * A list of user initiated operations that can be done on a channel. + * + * @param Type of data that can be written on the associated channel. + */ +public interface ChannelOperations { + + /** + * Flush selector that always returns true. + */ + Func1 FLUSH_ON_EACH_STRING = new Func1() { + @Override + public Boolean call(String next) { + return true; + } + }; + + /** + * Flush selector that always returns true. + */ + Func1 FLUSH_ON_EACH_BYTES = new Func1() { + @Override + public Boolean call(byte[] next) { + return true; + } + }; + + /** + * Flush selector that always returns true. + */ + Func1 FLUSH_ON_EACH_FILE_REGION = new Func1() { + @Override + public Boolean call(FileRegion next) { + return true; + } + }; + AttributeKey FLUSH_ONLY_ON_READ_COMPLETE = + AttributeKey.valueOf("_rxnetyy-flush-only-on-read-complete"); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush.

+ * + * All writes will be flushed on completion of the passed {@code Observable} + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + Observable write(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + Observable write(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + Observable writeAndFlushOnEach(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush.

+ * + * All writes will be flushed on completion of the passed {@code Observable} + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + Observable writeString(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + Observable writeString(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + Observable writeStringAndFlushOnEach(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush.

+ * + * All writes will be flushed on completion of the passed {@code Observable} + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + Observable writeBytes(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + Observable writeBytes(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + Observable writeBytesAndFlushOnEach(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush.

+ * + * All writes will be flushed on completion of the passed {@code Observable} + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + Observable writeFileRegion(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + Observable writeFileRegion(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + Observable writeFileRegionAndFlushOnEach(Observable msgs); + + /** + * Modifies the underneath channel to enable writing new type of objects that will be transformed using the passed + * {@code transformer} + * + * @param transformer Transformer to transform objects written to the channel. + * + * @param The target type of the transformer. + * + * @return A new instance of {@code ChannelOperations} that accepts the transformed type to write. + */ + ChannelOperations transformWrite(AllocatingTransformer transformer); + + /** + * Flushes any pending writes on this connection by calling {@link Channel#flush()}. This can be used for + * implementing any custom flusing strategies that otherwise can not be implemented by methods like + * {@link #write(Observable, Func1)}. + */ + void flush(); + + /** + * Flushes any pending writes and closes the connection. Same as calling {@code close(true)} + * + * @return {@link Observable} representing the result of close. + */ + Observable close(); + + /** + * Closes this channel after flushing all pending writes. + * + * @return {@link Observable} representing the result of close and flush. + */ + Observable close(boolean flush); + + /** + * Closes the connection immediately. Same as calling {@link #close()} and subscribing to the returned + * {@code Observable} + */ + void closeNow(); + + /** + * Returns an {@link Observable} that completes when this connection is closed. + * + * @return An {@link Observable} that completes when this connection is closed. + */ + Observable closeListener(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelSubscriberEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelSubscriberEvent.java new file mode 100644 index 0000000..8a2e104 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ChannelSubscriberEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import rx.Subscriber; + +/** + * An event to communicate the subscriber of a new channel created by {@link AbstractConnectionToChannelBridge}. + * + *

Connection reuse

+ * + * For cases, where the {@link Connection} is pooled, reuse should be indicated explicitly via + * {@link ConnectionInputSubscriberResetEvent}. There can be multiple {@link ConnectionInputSubscriberResetEvent}s + * sent to the same channel and hence the same instance of {@link AbstractConnectionToChannelBridge}. + * + * @param Type read from the connection held by the event. + * @param Type written to the connection held by the event. + */ +public class ChannelSubscriberEvent { + + private final Subscriber subscriber; + + public ChannelSubscriberEvent(Subscriber subscriber) { + this.subscriber = subscriber; + } + + public Subscriber getSubscriber() { + return subscriber; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/Connection.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/Connection.java new file mode 100644 index 0000000..cd7d86d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/Connection.java @@ -0,0 +1,316 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.EventExecutorGroup; +import rx.Observable; +import rx.Observable.Transformer; +import rx.functions.Action1; +import rx.functions.Func1; + +/** + * An abstraction over netty's channel providing Rx APIs. + * + *

Reading data

+ * + * Unless, {@link ChannelOption#AUTO_READ} is set to {@code true} on the underneath channel, data will be read from the + * connection if and only if there is a subscription to the input stream returned by {@link #getInput()}. + * In case, the input data is not required to be consumed, one should call {@link #ignoreInput()}, otherwise, data will + * never be read from the channel. + * + * @param Type of object that is read from this connection. + * @param Type of object that is written to this connection. + */ +public abstract class Connection implements ChannelOperations { + + public static final AttributeKey CONNECTION_ATTRIBUTE_KEY = AttributeKey.valueOf("rx-netty-conn-attr"); + + private final Channel nettyChannel; + private final ContentSource contentSource; + protected final MarkAwarePipeline markAwarePipeline; + + protected Connection(final Channel nettyChannel) { + if (null == nettyChannel) { + throw new IllegalArgumentException("Channel can not be null"); + } + this.nettyChannel = nettyChannel; + markAwarePipeline = new MarkAwarePipeline(nettyChannel.pipeline()); + contentSource = new ContentSource<>(nettyChannel, ConnectionInputSubscriberEvent::new); + } + + protected Connection(Connection toCopy) { + nettyChannel = toCopy.nettyChannel; + markAwarePipeline = toCopy.markAwarePipeline; + contentSource = toCopy.contentSource; + } + + protected Connection(Connection toCopy, ContentSource contentSource) { + nettyChannel = toCopy.nettyChannel; + markAwarePipeline = toCopy.markAwarePipeline; + this.contentSource = contentSource; + } + + /** + * Returns a stream of data that is read from the connection. + * + * Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the + * underneath channel, if there is a subscriber to the input. + * In case, input is not required to be read, call {@link #ignoreInput()} + * + * @return The stream of data that is read from the connection. + */ + public ContentSource getInput() { + return contentSource; + } + + /** + * Ignores all input on this connection. + * + * Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the + * underneath channel, if there is a subscriber to the input. So, upon recieving this connection, either one should + * call this method or eventually subscribe to the stream returned by {@link #getInput()} + * + * @return An {@link Observable}, subscription to which will discard the input. This {@code Observable} will + * error/complete when the input errors/completes and unsubscription from here will unsubscribe from the content. + */ + public Observable ignoreInput() { + return getInput().map(new Func1() { + @Override + public Void call(R r) { + ReferenceCountUtil.release(r); + return null; + } + }).ignoreElements(); + } + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at + * the first position of the pipeline as specified by {@link ChannelPipeline#addFirst(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param name Name of the handler. + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerFirst(String name, ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at + * the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param name the name of the handler to append + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerFirst(EventExecutorGroup group, String name, + ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at + * the last position of the pipeline as specified by {@link ChannelPipeline#addLast(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param name Name of the handler. + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerLast(String name, ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at + * the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param name the name of the handler to append + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerLast(EventExecutorGroup group, String name, + ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerBefore(String baseName, String name, + ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerAfter(String baseName, String name, + ChannelHandler handler); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handler Handler instance to add. + * + * @return {@code this}. + */ + public abstract Connection addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, ChannelHandler handler); + + /** + * Configures the {@link ChannelPipeline} for this channel, using the passed {@code pipelineConfigurator}. + * + * @param pipelineConfigurator Action to configure {@link ChannelPipeline}. + * + * @return {@code this}. + */ + public abstract Connection pipelineConfigurator(Action1 pipelineConfigurator); + + /** + * Transforms this connection's input stream using the passed {@code transformer} to create a new + * {@code Connection} instance. + * + * @param transformer Transformer to transform the input stream. + * + * @param New type of the input stream. + * + * @return A new connection instance with the transformed read stream. + */ + public abstract Connection transformRead(Transformer transformer); + + /** + * Transforms this connection to enable writing a different object type. + * + * @param transformer Transformer to transform objects written to the channel. + * + * @param New object types to be written to the connection. + * + * @return A new connection instance with the new write type. + */ + public abstract Connection transformWrite(AllocatingTransformer transformer); + + /** + * Returns the {@link MarkAwarePipeline} for this connection, changes to which can be reverted at any point in time. + */ + public MarkAwarePipeline getResettableChannelPipeline() { + return markAwarePipeline; + } + + /** + * Returns the {@link ChannelPipeline} for this connection. + * + * @return {@link ChannelPipeline} for this connection. + */ + public ChannelPipeline getChannelPipeline() { + return nettyChannel.pipeline(); + } + + /** + * Returns the underlying netty {@link Channel} for this connection. + * + *

Why unsafe?

+ * + * It is advisable to use this connection abstraction for all interactions with the channel, however, advanced users + * may find directly using the netty channel useful in some cases. + * + * @return The underlying netty {@link Channel} for this connection. + */ + public Channel unsafeNettyChannel() { + return nettyChannel; + } + + /* + * In order to make sure that the connection is correctly initialized, the listener needs to be added post + * constructor. Otherwise, there is a race-condition of the channel closed before the connection is completely + * created and the Connection.close() call on channel close can access the Connection object which isn't + * constructed completely. IOW, "this" escapes from the constructor if the listener is added in the constructor. + */ + protected void connectCloseToChannelClose() { + nettyChannel.closeFuture() + .addListener((ChannelFutureListener) future -> { + closeNow(); // Close this connection when the channel is closed. + }); + nettyChannel.attr(CONNECTION_ATTRIBUTE_KEY).set(this); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionCreationFailedEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionCreationFailedEvent.java new file mode 100644 index 0000000..afb93b6 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionCreationFailedEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; + +/** + * An event to indicate to {@link AbstractConnectionToChannelBridge} that the subscriber as published by + * {@link ChannelSubscriberEvent} should be informed of a connection creation failure, instead of a new connection. + * + *

Why do we need this?

+ * + * Since, emitting a connection can include a handshake for protocols such as TLS/SSL, it is not so that a new + * {@link io.reactivex.netty.channel.Connection} should be emitted as soon as the channel is active (i.e. inside + * {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}). + * For this reason, this event leaves it to the pipeline or any other entity outside to decide, when is the rite time to + * determine that a connection for a channel has failed creation. + */ +public final class ConnectionCreationFailedEvent { + + private final Throwable throwable; + + public ConnectionCreationFailedEvent(Throwable throwable) { + this.throwable = throwable; + } + + public Throwable getThrowable() { + return throwable; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionImpl.java new file mode 100644 index 0000000..30126ff --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionImpl.java @@ -0,0 +1,240 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.FileRegion; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import rx.Observable; +import rx.Observable.Transformer; +import rx.functions.Action1; +import rx.functions.Func1; + +/** + * An implementation of {@link Connection} delegating all {@link ChannelOperations} methods to + * {@link DefaultChannelOperations}. + */ +public final class ConnectionImpl extends Connection { + + private final ChannelOperations delegate; + + private ConnectionImpl(Channel nettyChannel, ConnectionEventListener eventListener, EventPublisher eventPublisher) { + super(nettyChannel); + delegate = new DefaultChannelOperations<>(nettyChannel, eventListener, eventPublisher); + } + + private ConnectionImpl(Channel nettyChannel, ChannelOperations delegate) { + super(nettyChannel); + this.delegate = delegate; + } + + private ConnectionImpl(ConnectionImpl toCopy, ContentSource contentSource, ChannelOperations delegate) { + super(toCopy, contentSource); + this.delegate = delegate; + } + + @Override + public Observable write(Observable msgs) { + return delegate.write(msgs); + } + + @Override + public Observable write(Observable msgs, Func1 flushSelector) { + return delegate.write(msgs, flushSelector); + } + + @Override + public Observable writeAndFlushOnEach(Observable msgs) { + return delegate.writeAndFlushOnEach(msgs); + } + + @Override + public Observable writeString(Observable msgs) { + return delegate.writeString(msgs); + } + + @Override + public Observable writeString(Observable msgs, Func1 flushSelector) { + return delegate.writeString(msgs, flushSelector); + } + + @Override + public Observable writeStringAndFlushOnEach(Observable msgs) { + return delegate.writeStringAndFlushOnEach(msgs); + } + + @Override + public Observable writeBytes(Observable msgs) { + return delegate.writeBytes(msgs); + } + + @Override + public Observable writeBytes(Observable msgs, + Func1 flushSelector) { + return delegate.writeBytes(msgs, flushSelector); + } + + @Override + public Observable writeBytesAndFlushOnEach(Observable msgs) { + return delegate.writeBytesAndFlushOnEach(msgs); + } + + @Override + public Observable writeFileRegion(Observable msgs) { + return delegate.writeFileRegion(msgs); + } + + @Override + public Observable writeFileRegion(Observable msgs, + Func1 flushSelector) { + return delegate.writeFileRegion(msgs, flushSelector); + } + + @Override + public Observable writeFileRegionAndFlushOnEach(Observable msgs) { + return delegate.writeFileRegionAndFlushOnEach(msgs); + } + + @Override + public void flush() { + delegate.flush(); + } + + @Override + public Observable close() { + return delegate.close(); + } + + @Override + public Observable close(boolean flush) { + return delegate.close(flush); + } + + @Override + public void closeNow() { + delegate.closeNow(); + } + + @Override + public Observable closeListener() { + return delegate.closeListener(); + } + + public static ConnectionImpl fromChannel(Channel nettyChannel) { + EventPublisher ep = nettyChannel.attr(EventAttributeKeys.EVENT_PUBLISHER).get(); + if (null == ep) { + throw new IllegalArgumentException("No event publisher set in the channel."); + } + + ConnectionEventListener l = null; + if (ep.publishingEnabled()) { + l = nettyChannel.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).get(); + if (null == l) { + throw new IllegalArgumentException("No event listener set in the channel."); + } + } + + final ConnectionImpl toReturn = new ConnectionImpl<>(nettyChannel, l, ep); + toReturn.connectCloseToChannelClose(); + return toReturn; + } + + /*Visible for testing*/static ConnectionImpl create(Channel nettyChannel, + ChannelOperations delegate) { + final ConnectionImpl toReturn = new ConnectionImpl<>(nettyChannel, delegate); + toReturn.connectCloseToChannelClose(); + return toReturn; + } + + @Override + public Connection addChannelHandlerFirst(String name, ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addFirst(name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerFirst(EventExecutorGroup group, String name, + ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addFirst(group, name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerLast(String name, ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addLast(name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerLast(EventExecutorGroup group, String name, + ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addLast(group, name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerBefore(String baseName, String name, ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addBefore(baseName, name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addBefore(group, baseName, name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerAfter(String baseName, String name, ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addAfter(baseName, name, handler); + return cast(); + } + + @Override + public Connection addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + ChannelHandler handler) { + getResettableChannelPipeline().markIfNotYetMarked().addAfter(group, baseName, name, handler); + return cast(); + } + + @Override + public Connection pipelineConfigurator(Action1 pipelineConfigurator) { + pipelineConfigurator.call(getResettableChannelPipeline().markIfNotYetMarked()); + return cast(); + } + + @Override + public Connection transformRead(Transformer transformer) { + return new ConnectionImpl<>(this, getInput().transform(transformer), delegate); + } + + @Override + public Connection transformWrite(AllocatingTransformer transformer) { + return new ConnectionImpl<>(this, getInput(), delegate.transformWrite(transformer)); + } + + @SuppressWarnings("unchecked") + protected Connection cast() { + return (Connection) this; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberEvent.java new file mode 100644 index 0000000..5bdd87c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberEvent.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.util.ReferenceCountUtil; +import rx.Subscriber; +import rx.functions.Action1; +import rx.observers.Subscribers; + +/** + * An event to communicate the subscriber of the associated connection input stream created by + * {@link AbstractConnectionToChannelBridge}. + * + *

Multiple events on the same channel

+ * + * Multiple instance of this event can be sent on the same channel, provided that there is a + * {@link ConnectionInputSubscriberResetEvent} between two consecutive {@link ConnectionInputSubscriberEvent}s + * + * @param Type read from the connection held by the event. + * @param Type written to the connection held by the event. + */ +public final class ConnectionInputSubscriberEvent { + + private final Subscriber subscriber; + + public ConnectionInputSubscriberEvent(Subscriber subscriber) { + if (null == subscriber) { + throw new NullPointerException("Subscriber can not be null"); + } + this.subscriber = subscriber; + } + + public Subscriber getSubscriber() { + return subscriber; + } + + public static ConnectionInputSubscriberEvent discardAllInput() { + return new ConnectionInputSubscriberEvent<>(Subscribers.create(new Action1() { + @Override + public void call(II msg) { + ReferenceCountUtil.release(msg); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // Empty as we are discarding input anyways. + } + })); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberReplaceEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberReplaceEvent.java new file mode 100644 index 0000000..31ee184 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberReplaceEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +/** + * This event is an indication to atomically replace existing connection input subscriber, if any, with another. + */ +public class ConnectionInputSubscriberReplaceEvent { + + private final ConnectionInputSubscriberEvent newSubEvent; + + public ConnectionInputSubscriberReplaceEvent(ConnectionInputSubscriberEvent newSubEvent) { + this.newSubEvent = newSubEvent; + } + + public ConnectionInputSubscriberEvent getNewSubEvent() { + return newSubEvent; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberResetEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberResetEvent.java new file mode 100644 index 0000000..dd1227b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionInputSubscriberResetEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +/** + * This event is an indication that there will be multiple subscribers to the connection input stream. This event + * must be sent as many times as the subscribers to the input. This typically will be the case for client-side + * connections when a channel is pooled and reused. + */ +public interface ConnectionInputSubscriberResetEvent { + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionSubscriberEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionSubscriberEvent.java new file mode 100644 index 0000000..205c9e8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ConnectionSubscriberEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import rx.Subscriber; + +/** + * An event to communicate the subscriber of a new connection created by {@link AbstractConnectionToChannelBridge}. + * + *

Connection reuse

+ * + * For cases, where the {@link Connection} is pooled, reuse should be indicated explicitly via + * {@link ConnectionInputSubscriberResetEvent}. There can be multiple {@link ConnectionInputSubscriberResetEvent}s + * sent to the same channel and hence the same instance of {@link AbstractConnectionToChannelBridge}. + * + * @param Type read from the connection held by the event. + * @param Type written to the connection held by the event. + */ +public class ConnectionSubscriberEvent { + + private final Subscriber> subscriber; + + public ConnectionSubscriberEvent(Subscriber> subscriber) { + this.subscriber = subscriber; + } + + public Subscriber> getSubscriber() { + return subscriber; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/ContentSource.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ContentSource.java new file mode 100644 index 0000000..73d0122 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/ContentSource.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.Channel; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Func1; + +/** + * A source for any content/data read from a channel. + * + *

Managing {@link ByteBuf} lifecycle.

+ * + * If this source emits {@link ByteBuf} or a {@link ByteBufHolder}, using {@link #autoRelease()} will release the buffer + * after emitting it from this source. + * + *

Replaying content

+ * + * Since, the content read from a channel is not re-readable, this also provides a {@link #replayable()} function that + * produces a source which can be subscribed multiple times to replay the same data. This is specially useful if the + * content read from one channel is written on to another with an option to retry. + * + * @param + */ +public final class ContentSource extends Observable { + + private ContentSource(final Observable source) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + source.unsafeSubscribe(subscriber); + } + }); + } + + public ContentSource(final Channel channel, final Func1, Object> subscriptionEventFactory) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + channel.pipeline() + .fireUserEventTriggered(subscriptionEventFactory.call(subscriber)); + } + }); + } + + public ContentSource(final Throwable error) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.onError(error); + } + }); + } + + /** + * If this source emits {@link ByteBuf} or {@link ByteBufHolder} then using this operator will release the buffer + * after it is emitted from this source. + * + * @return A new instance of the stream with auto-release enabled. + */ + public Observable autoRelease() { + return this.lift(new AutoReleaseOperator()); + } + + /** + * This provides a replayable content source that only subscribes once to the actual content and then caches it, + * till {@link DisposableContentSource#dispose()} is called. + * + * @return A new replayable content source. + */ + public DisposableContentSource replayable() { + return DisposableContentSource.createNew(this); + } + + public ContentSource transform(Transformer transformer) { + return new ContentSource<>(transformer.call(this)); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/DefaultChannelOperations.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DefaultChannelOperations.java new file mode 100644 index 0000000..e65aeab --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DefaultChannelOperations.java @@ -0,0 +1,368 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.FileRegion; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventPublisher; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Actions; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import static java.util.concurrent.TimeUnit.*; + +/** + * Default implementation for {@link ChannelOperations}. + * + * @param Type of data that can be written on the associated channel. + */ +public class DefaultChannelOperations implements ChannelOperations { + + private static final Logger logger = Logger.getLogger(DefaultChannelOperations.class.getName()); + + /** Field updater for closeIssued. */ + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater CLOSE_ISSUED_UPDATER + = AtomicIntegerFieldUpdater.newUpdater(DefaultChannelOperations.class, "closeIssued"); + @SuppressWarnings("unused") + private volatile int closeIssued; // updated by the atomic updater, so required to be volatile. + + private final Channel nettyChannel; + private final ConnectionEventListener eventListener; + private final EventPublisher eventPublisher; + + private final Observable closeObservable; + private final Observable flushAndCloseObservable; + + private final Func1 flushOnEachSelector = new Func1() { + @Override + public Boolean call(W w) { + return true; + } + }; + + public DefaultChannelOperations(final Channel nettyChannel, ConnectionEventListener eventListener, + EventPublisher eventPublisher) { + this.nettyChannel = nettyChannel; + this.eventListener = eventListener; + this.eventPublisher = eventPublisher; + closeObservable = Observable.create(new OnSubscribeForClose(nettyChannel)); + flushAndCloseObservable = closeObservable.doOnSubscribe(new Action0() { + @Override + public void call() { + flush(); + } + }); + } + + @Override + public Observable write(final Observable msgs) { + return _write(msgs); + } + + @Override + public Observable write(Observable msgs, final Func1 flushSelector) { + return _write(msgs, flushSelector); + } + + @Override + public Observable writeAndFlushOnEach(Observable msgs) { + return _write(msgs, flushOnEachSelector); + } + + @Override + public Observable writeString(Observable msgs) { + return _write(msgs); + } + + @Override + public Observable writeString(Observable msgs, Func1 flushSelector) { + return _write(msgs, flushSelector); + } + + @Override + public Observable writeStringAndFlushOnEach(Observable msgs) { + return writeString(msgs, FLUSH_ON_EACH_STRING); + } + + @Override + public Observable writeBytes(Observable msgs) { + return _write(msgs); + } + + @Override + public Observable writeBytes(Observable msgs, Func1 flushSelector) { + return _write(msgs, flushSelector); + } + + @Override + public Observable writeBytesAndFlushOnEach(Observable msgs) { + return _write(msgs, FLUSH_ON_EACH_BYTES); + } + + @Override + public Observable writeFileRegion(Observable msgs) { + return _write(msgs); + } + + @Override + public Observable writeFileRegion(Observable msgs, Func1 flushSelector) { + return _write(msgs, flushSelector); + } + + @Override + public Observable writeFileRegionAndFlushOnEach(Observable msgs) { + return writeFileRegion(msgs, FLUSH_ON_EACH_FILE_REGION); + } + + @Override + public ChannelOperations transformWrite(AllocatingTransformer transformer) { + nettyChannel.pipeline().fireUserEventTriggered(new AppendTransformerEvent<>(transformer)); + return new DefaultChannelOperations<>(nettyChannel, eventListener, eventPublisher); + } + + @Override + public void flush() { + if (eventPublisher.publishingEnabled()) { + final long startTimeNanos = Clock.newStartTimeNanos(); + eventListener.onFlushStart(); + if (nettyChannel.eventLoop().inEventLoop()) { + _flushInEventloop(startTimeNanos); + } else { + nettyChannel.eventLoop() + .execute(new Runnable() { + @Override + public void run() { + _flushInEventloop(startTimeNanos); + } + }); + } + } else { + nettyChannel.flush(); + } + } + + @Override + public Observable close() { + return close(true); + } + + @Override + public Observable close(boolean flush) { + return flush ? flushAndCloseObservable : closeObservable; + } + + @Override + public void closeNow() { + close().subscribe(Actions.empty(), new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Error closing connection.", throwable); + } + }); + } + + @Override + public Observable closeListener() { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + final SubscriberToChannelFutureBridge l = new SubscriberToChannelFutureBridge() { + + @Override + protected void doOnSuccess(ChannelFuture future) { + subscriber.onCompleted(); + } + + @Override + protected void doOnFailure(ChannelFuture future, Throwable cause) { + subscriber.onCompleted(); + } + }; + l.bridge(nettyChannel.closeFuture(), subscriber); + } + }); + } + + private Observable _write(final Observable msgs, Func1 flushSelector) { + return _write(msgs.lift(new FlushSelectorOperator<>(flushSelector, this))); + } + + private void _flushInEventloop(long startTimeNanos) { + assert nettyChannel.eventLoop().inEventLoop(); + nettyChannel.flush(); // Flush is sync when from eventloop. + eventListener.onFlushComplete(Clock.onEndNanos(startTimeNanos), NANOSECONDS); + } + + private Observable _write(final Observable msgs) { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + + final long startTimeNanos = Clock.newStartTimeNanos(); + + if (eventPublisher.publishingEnabled()) { + eventListener.onWriteStart(); + } + + /* + * If a write happens from outside the eventloop, it does not wakeup the selector, till a flush happens. + * In absence of a selector wakeup, this write will be delayed by the selector sleep interval. + * The code below makes sure that the selector is woken up on a write (by executing a task that does + * the write) + */ + if (nettyChannel.eventLoop().inEventLoop()) { + _writeStreamToChannel(subscriber, startTimeNanos); + } else { + nettyChannel.eventLoop() + .execute(new Runnable() { + @Override + public void run() { + _writeStreamToChannel(subscriber, startTimeNanos); + } + }); + } + } + + private void _writeStreamToChannel(final Subscriber subscriber, final long startTimeNanos) { + final ChannelFuture writeFuture = nettyChannel.write(msgs.doOnCompleted(new Action0() { + @Override + public void call() { + Boolean shdNotFlush = nettyChannel.attr(FLUSH_ONLY_ON_READ_COMPLETE).get(); + if (null == shdNotFlush || !shdNotFlush) { + flush(); + } + } + })); + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + writeFuture.cancel(false); // cancel write on unsubscribe. + } + })); + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (subscriber.isUnsubscribed()) { + /*short-circuit if subscriber is unsubscribed*/ + return; + } + + if (future.isSuccess()) { + if (eventPublisher.publishingEnabled()) { + eventListener.onWriteSuccess(Clock.onEndNanos(startTimeNanos), NANOSECONDS); + } + subscriber.onCompleted(); + } else { + if (eventPublisher.publishingEnabled()) { + eventListener.onWriteFailed(Clock.onEndNanos(startTimeNanos), NANOSECONDS, + future.cause()); + } + subscriber.onError(future.cause()); + } + } + }); + } + }); + } + + private class OnSubscribeForClose implements OnSubscribe { + + private final Channel nettyChannel; + + public OnSubscribeForClose(Channel nettyChannel) { + this.nettyChannel = nettyChannel; + } + + @Override + @SuppressWarnings("unchecked") + public void call(final Subscriber subscriber) { + + final long closeStartTimeNanos = Clock.newStartTimeNanos(); + + final ChannelCloseListener closeListener; + if (CLOSE_ISSUED_UPDATER.compareAndSet(DefaultChannelOperations.this, 0, 1)) { + if (eventPublisher.publishingEnabled()) { + eventListener.onConnectionCloseStart(); + } + + nettyChannel.close(); // close only once. + + closeListener = new ChannelCloseListener(eventListener, eventPublisher, closeStartTimeNanos, + subscriber); + } else { + closeListener = new ChannelCloseListener(subscriber); + } + + closeListener.bridge(nettyChannel.closeFuture(), subscriber); + } + + private class ChannelCloseListener extends SubscriberToChannelFutureBridge { + + private final long closeStartTimeNanos; + private final Subscriber subscriber; + private final ConnectionEventListener eventListener; + private final EventPublisher eventPublisher; + + public ChannelCloseListener(ConnectionEventListener eventListener, EventPublisher eventPublisher, + long closeStartTimeNanos, Subscriber subscriber) { + this.eventListener = eventListener; + this.eventPublisher = eventPublisher; + this.closeStartTimeNanos = closeStartTimeNanos; + this.subscriber = subscriber; + } + + public ChannelCloseListener(Subscriber subscriber) { + this(null, null, -1, subscriber); + } + + @Override + protected void doOnSuccess(ChannelFuture future) { + if (null != eventListener && eventPublisher.publishingEnabled()) { + eventListener.onConnectionCloseSuccess(Clock.onEndNanos(closeStartTimeNanos), NANOSECONDS); + } + if (!subscriber.isUnsubscribed()) { + subscriber.onCompleted(); + } + } + + @Override + protected void doOnFailure(ChannelFuture future, Throwable cause) { + if (null != eventListener && eventPublisher.publishingEnabled()) { + eventListener.onConnectionCloseFailed(Clock.onEndNanos(closeStartTimeNanos), NANOSECONDS, + future.cause()); + } + if (!subscriber.isUnsubscribed()) { + subscriber.onError(future.cause()); + } + } + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/DetachedChannelPipeline.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DetachedChannelPipeline.java new file mode 100644 index 0000000..7851602 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DetachedChannelPipeline.java @@ -0,0 +1,387 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.util.concurrent.EventExecutorGroup; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.functions.Action1; +import rx.functions.Func0; + +import java.util.LinkedList; +import java.util.Map.Entry; +import java.util.NoSuchElementException; + +/** + * An implementation of {@link ChannelPipeline} which is detached from a channel and provides a + * {@link #addToChannel(Channel)} method to be invoked when this pipeline handlers are to be added to an actual channel + * pipeline. + * + * This must NOT be used on an actual channel, it does not support any channel operations. It only supports pipeline + * modification operations. + */ +public class DetachedChannelPipeline { + + private static final Logger logger = Logger.getLogger(DetachedChannelPipeline.class.getName()); + + private final LinkedList holdersInOrder; + + private final Action1 nullableTail; + + public DetachedChannelPipeline() { + this(null); + } + + public DetachedChannelPipeline(final Action1 nullableTail) { + this.nullableTail = nullableTail; + holdersInOrder = new LinkedList<>(); + } + + private DetachedChannelPipeline(final DetachedChannelPipeline copyFrom, + final Action1 nullableTail) { + this.nullableTail = nullableTail; + holdersInOrder = new LinkedList<>(); + synchronized (copyFrom.holdersInOrder) { + for (HandlerHolder handlerHolder : copyFrom.holdersInOrder) { + holdersInOrder.addLast(handlerHolder); + } + } + } + + public ChannelInitializer getChannelInitializer() { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + final ChannelPipeline pipeline = ch.pipeline(); + synchronized (holdersInOrder) { + unguardedCopyToPipeline(pipeline); + } + } + }; + } + + public void addToChannel(Channel channel) { + final ChannelPipeline pipeline = channel.pipeline(); + synchronized (holdersInOrder) { + unguardedCopyToPipeline(pipeline); + } + } + + public DetachedChannelPipeline copy() { + return copy(null); + } + + public DetachedChannelPipeline copy(Action1 newTail) { + return new DetachedChannelPipeline(this, newTail); + } + + public DetachedChannelPipeline addFirst(String name, Func0 handlerFactory) { + return _guardedAddFirst(new HandlerHolder(name, handlerFactory)); + } + + public DetachedChannelPipeline addFirst(EventExecutorGroup group, + String name, Func0 handlerFactory) { + return _guardedAddFirst(new HandlerHolder(name, handlerFactory, group)); + } + + public DetachedChannelPipeline addLast(String name, Func0 handlerFactory) { + return _guardedAddLast(new HandlerHolder(name, handlerFactory)); + } + + public DetachedChannelPipeline addLast(EventExecutorGroup group, String name, Func0 handlerFactory) { + return _guardedAddLast(new HandlerHolder(name, handlerFactory, group)); + } + + public DetachedChannelPipeline addBefore(String baseName, String name, Func0 handlerFactory) { + return _guardedAddBefore(baseName, new HandlerHolder(name, handlerFactory)); + } + + public DetachedChannelPipeline addBefore(EventExecutorGroup group, String baseName, String name, Func0 handlerFactory) { + return _guardedAddBefore(baseName, new HandlerHolder(name, handlerFactory, group)); + } + + public DetachedChannelPipeline addAfter(String baseName, String name, Func0 handlerFactory) { + return _guardedAddAfter(baseName, new HandlerHolder(name, handlerFactory)); + } + + public DetachedChannelPipeline addAfter(EventExecutorGroup group, String baseName, String name, Func0 handlerFactory) { + return _guardedAddAfter(baseName, new HandlerHolder(name, handlerFactory, group)); + } + + @SafeVarargs + public final DetachedChannelPipeline addFirst(Func0... handlerFactories) { + synchronized (holdersInOrder) { + for (int i = handlerFactories.length - 1; i >= 0; i--) { + Func0 handlerFactory = handlerFactories[i]; + holdersInOrder.addFirst(new HandlerHolder(handlerFactory)); + } + } + return this; + } + + @SafeVarargs + public final DetachedChannelPipeline addFirst(EventExecutorGroup group, Func0... handlerFactories) { + synchronized (holdersInOrder) { + for (int i = handlerFactories.length - 1; i >= 0; i--) { + Func0 handlerFactory = handlerFactories[i]; + holdersInOrder.addFirst(new HandlerHolder(null, handlerFactory, group)); + } + } + return this; + } + + @SafeVarargs + public final DetachedChannelPipeline addLast(Func0... handlerFactories) { + for (Func0 handlerFactory : handlerFactories) { + _guardedAddLast(new HandlerHolder(handlerFactory)); + } + return this; + } + + @SafeVarargs + public final DetachedChannelPipeline addLast(EventExecutorGroup group, Func0... handlerFactories) { + for (Func0 handlerFactory : handlerFactories) { + _guardedAddLast(new HandlerHolder(null, handlerFactory, group)); + } + return this; + } + + public DetachedChannelPipeline configure(Action1 configurator) { + _guardedAddLast(new HandlerHolder(configurator)); + return this; + } + + public void copyTo(ChannelPipeline pipeline) { + synchronized (holdersInOrder) { + unguardedCopyToPipeline(pipeline); + } + } + + /*Visible for testing*/ LinkedList getHoldersInOrder() { + return holdersInOrder; + } + + private void unguardedCopyToPipeline(ChannelPipeline pipeline) { /*To be guarded by lock on holders*/ + for (HandlerHolder holder : holdersInOrder) { + if (holder.hasPipelineConfigurator()) { + holder.getPipelineConfigurator().call(pipeline); + continue; + } + + if (holder.hasGroup()) { + if (holder.hasName()) { + pipeline.addLast(holder.getGroupIfConfigured(), holder.getNameIfConfigured(), + holder.getHandlerFactoryIfConfigured().call()); + } else { + pipeline.addLast(holder.getGroupIfConfigured(), holder.getHandlerFactoryIfConfigured().call()); + } + } else if (holder.hasName()) { + pipeline.addLast(holder.getNameIfConfigured(), holder.getHandlerFactoryIfConfigured().call()); + } else { + pipeline.addLast(holder.getHandlerFactoryIfConfigured().call()); + } + } + + if (null != nullableTail) { + nullableTail.call(pipeline); // This is the last handler to be added to the pipeline always. + } + + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Channel pipeline in initializer: " + pipelineToString(pipeline)); + } + } + + private HandlerHolder unguardedFindHandlerByName(String baseName, boolean leniant) { + for (HandlerHolder handlerHolder : holdersInOrder) { + if (handlerHolder.hasName() && handlerHolder.getNameIfConfigured().equals(baseName)) { + return handlerHolder; + } + } + if (leniant) { + return null; + } else { + throw new NoSuchElementException("No handler with name: " + baseName + " configured in the pipeline."); + } + } + + private DetachedChannelPipeline _guardedAddFirst(HandlerHolder toAdd) { + synchronized (holdersInOrder) { + holdersInOrder.addFirst(toAdd); + } + return this; + } + + private DetachedChannelPipeline _guardedAddLast(HandlerHolder toAdd) { + synchronized (holdersInOrder) { + holdersInOrder.addLast(toAdd); + } + return this; + } + + private DetachedChannelPipeline _guardedAddBefore(String baseName, HandlerHolder toAdd) { + synchronized (holdersInOrder) { + HandlerHolder before = unguardedFindHandlerByName(baseName, false); + final int indexOfBefore = holdersInOrder.indexOf(before); + holdersInOrder.add(indexOfBefore, toAdd); + } + return this; + } + + private DetachedChannelPipeline _guardedAddAfter(String baseName, HandlerHolder toAdd) { + synchronized (holdersInOrder) { + HandlerHolder after = unguardedFindHandlerByName(baseName, false); + final int indexOfAfter = holdersInOrder.indexOf(after); + holdersInOrder.add(indexOfAfter + 1, toAdd); + } + return this; + } + + private static String pipelineToString(ChannelPipeline pipeline) { + StringBuilder builder = new StringBuilder(); + for (Entry handlerEntry : pipeline) { + if (builder.length() == 0) { + builder.append("[\n"); + } else { + builder.append(" ==> "); + } + builder.append("{ name =>") + .append(handlerEntry.getKey()) + .append(", handler => ") + .append(handlerEntry.getValue()) + .append("}\n") + ; + } + + if (builder.length() > 0) { + builder.append("}\n"); + } + return builder.toString(); + } + + /** + * A holder class for holding handler information, required to add handlers to the actual pipeline. + */ + /*Visible for testing*/ static class HandlerHolder { + + private final String nameIfConfigured; + private final Func0 handlerFactoryIfConfigured; + private final Action1 pipelineConfigurator; + private final EventExecutorGroup groupIfConfigured; + + HandlerHolder(Action1 pipelineConfigurator) { + this.pipelineConfigurator = pipelineConfigurator; + nameIfConfigured = null; + handlerFactoryIfConfigured = null; + groupIfConfigured = null; + } + + HandlerHolder(Func0 handlerFactory) { + this(null, handlerFactory); + } + + HandlerHolder(String name, Func0 handlerFactory) { + this(name, handlerFactory, null); + } + + HandlerHolder(String name, Func0 handlerFactory, EventExecutorGroup group) { + nameIfConfigured = name; + handlerFactoryIfConfigured = handlerFactory; + groupIfConfigured = group; + pipelineConfigurator = null; + } + + public String getNameIfConfigured() { + return nameIfConfigured; + } + + public boolean hasName() { + return null != nameIfConfigured; + } + + public Func0 getHandlerFactoryIfConfigured() { + return handlerFactoryIfConfigured; + } + + public EventExecutorGroup getGroupIfConfigured() { + return groupIfConfigured; + } + + public boolean hasGroup() { + return null != groupIfConfigured; + } + + public Action1 getPipelineConfigurator() { + return pipelineConfigurator; + } + + public boolean hasPipelineConfigurator() { + return null != pipelineConfigurator; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HandlerHolder)) { + return false; + } + + HandlerHolder that = (HandlerHolder) o; + + if (groupIfConfigured != null? !groupIfConfigured.equals(that.groupIfConfigured) : + that.groupIfConfigured != null) { + return false; + } + if (handlerFactoryIfConfigured != null? + !handlerFactoryIfConfigured.equals(that.handlerFactoryIfConfigured) : + that.handlerFactoryIfConfigured != null) { + return false; + } + if (nameIfConfigured != null? !nameIfConfigured.equals(that.nameIfConfigured) : + that.nameIfConfigured != null) { + return false; + } + if (pipelineConfigurator != null? !pipelineConfigurator.equals(that.pipelineConfigurator) : + that.pipelineConfigurator != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = nameIfConfigured != null? nameIfConfigured.hashCode() : 0; + result = 31 * result + (handlerFactoryIfConfigured != null? handlerFactoryIfConfigured.hashCode() : 0); + result = 31 * result + (pipelineConfigurator != null? pipelineConfigurator.hashCode() : 0); + result = 31 * result + (groupIfConfigured != null? groupIfConfigured.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HandlerHolder{" + "nameIfConfigured='" + nameIfConfigured + '\'' + ", handlerFactoryIfConfigured=" + + handlerFactoryIfConfigured + ", pipelineConfigurator=" + pipelineConfigurator + + ", groupIfConfigured=" + groupIfConfigured + '}'; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/DisposableContentSource.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DisposableContentSource.java new file mode 100644 index 0000000..f698256 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/DisposableContentSource.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.util.ReferenceCountUtil; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action1; +import rx.observables.ConnectableObservable; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Similar to {@link ContentSource} but supports multicast to multiple subscriptions. This source, subscribes upstream + * once and then caches the content, till the time {@link #dispose()} is called. + * + *

Managing {@link ByteBuf} lifecycle.

+ * + * If this source emits {@link ByteBuf} or a {@link ByteBufHolder}, using {@link #autoRelease()} will release the buffer + * after emitting it from this source. + * + * Every subscriber to this source must manage it's own lifecycle of the items it receives i.e. the buffers must be + * released by every subscriber post processing. + * + *

Disposing the source

+ * + * It is mandatory to call {@link #dispose()} on this source when no more subscriptions are required. Failure to do so, + * will cause a buffer leak as this source, caches the contents till disposed. + * + * Typically, {@link #dispose()} can be called as an {@link Subscriber#unsubscribe()} action. + * + * @param Type of objects emitted by this source. + */ +public final class DisposableContentSource extends Observable { + + private final OnSubscribeImpl onSubscribe; + + private DisposableContentSource(final OnSubscribeImpl onSubscribe) { + super(onSubscribe); + this.onSubscribe = onSubscribe; + } + + /** + * If this source emits {@link ByteBuf} or {@link ByteBufHolder} then using this operator will release the buffer + * after it is emitted from this source. + * + * @return A new instance of the stream with auto-release enabled. + */ + public Observable autoRelease() { + return this.lift(new AutoReleaseOperator()); + } + + /** + * Disposes this source. + */ + public void dispose() { + if (onSubscribe.disposed.compareAndSet(false, true)) { + for (Object chunk : onSubscribe.chunks) { + ReferenceCountUtil.release(chunk); + } + onSubscribe.chunks.clear(); + } + } + + static DisposableContentSource createNew(Observable source) { + final ArrayList chunks = new ArrayList<>(); + ConnectableObservable replay = source.doOnNext(new Action1() { + @Override + public void call(X x) { + chunks.add(x); + } + }).replay(); + return new DisposableContentSource<>(new OnSubscribeImpl(replay, chunks)); + } + + private static class OnSubscribeImpl implements OnSubscribe { + + private final ConnectableObservable source; + private final ArrayList chunks; + private boolean subscribed; + private final AtomicBoolean disposed = new AtomicBoolean(); + + public OnSubscribeImpl(ConnectableObservable source, ArrayList chunks) { + this.source = source; + this.chunks = chunks; + } + + @Override + public void call(Subscriber subscriber) { + + if (disposed.get()) { + subscriber.onError(new IllegalStateException("Content source is already disposed.")); + } + + boolean connectNow = false; + + synchronized (this) { + if (!subscribed) { + connectNow = true; + subscribed = true; + } + } + + source.doOnNext(new Action1() { + @Override + public void call(T msg) { + ReferenceCountUtil.retain(msg); + } + }).unsafeSubscribe(subscriber); + + if (connectNow) { + source.connect(); + } + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/EmitConnectionEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/EmitConnectionEvent.java new file mode 100644 index 0000000..690be4c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/EmitConnectionEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; + +/** + * An event to indicate to {@link AbstractConnectionToChannelBridge} that the channel is ready to emit a new + * {@link io.reactivex.netty.channel.Connection} to the subscriber as published by {@link ChannelSubscriberEvent} + * + *

Why do we need this?

+ * + * Since, emitting a connection can include a handshake for protocols such as TLS/SSL, it is not so that a new + * {@link io.reactivex.netty.channel.Connection} should be emitted as soon as the channel is active (i.e. inside + * {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}). + * For this reason, this event leaves it to the pipeline or any other entity outside to decide, when is the rite time to + * emit a connection. + */ +public final class EmitConnectionEvent { + + public static final EmitConnectionEvent INSTANCE = new EmitConnectionEvent(); + + private EmitConnectionEvent() { + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/FlushSelectorOperator.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/FlushSelectorOperator.java new file mode 100644 index 0000000..cfaec37 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/FlushSelectorOperator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import rx.Observable.Operator; +import rx.Subscriber; +import rx.functions.Func1; + +public class FlushSelectorOperator implements Operator { + + private final Func1 flushSelector; + private final ChannelOperations channelOps; + + public FlushSelectorOperator(Func1 flushSelector, ChannelOperations channelOps) { + this.flushSelector = flushSelector; + this.channelOps = channelOps; + } + + @Override + public Subscriber call(final Subscriber subscriber) { + + return new Subscriber(subscriber) { + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(T next) { + subscriber.onNext(next); + /*Call the selector _after_ writing an element*/ + if (flushSelector.call(next)) { + channelOps.flush(); + } + } + }; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/MarkAwarePipeline.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/MarkAwarePipeline.java new file mode 100644 index 0000000..3f290da --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/MarkAwarePipeline.java @@ -0,0 +1,471 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundInvoker; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.EventExecutorGroup; + +import java.net.SocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * An implementation of {@link ChannelPipeline} that allows a mark-reset scheme for {@link ChannelHandler}s. This allows + * temporary modifications to the underlying {@link ChannelPipeline} instance for usecases like pooled connections, + * server response upgrades, etc. + * + * This only supports a single mark at a time, although mark-reset-mark cycles can be repeated any number of times. + * + *

Usage:

+ * + * To start recording resetable changes, call {@link #mark()} and to reset back to the state before {@link #mark()} was + * called, call {@link #reset()} + * + *

Thread safety

+ * + * All operations of {@link ChannelPipeline} are delegated to the passed {@link ChannelPipeline} instance while + * creation. {@link #mark()} and {@link #reset()} uses the same mutex as {@link ChannelPipeline} for synchronization + * across different method calls. + */ +public final class MarkAwarePipeline implements ChannelPipeline { + + private boolean marked; // Guarded by this + + private final ChannelPipeline delegate; + + public MarkAwarePipeline(ChannelPipeline delegate) { + this.delegate = delegate; + } + + /** + * Marks this pipeline and record further changes which can be reverted by calling {@link #reset()} + * + * @throws IllegalStateException If this method is called more than once without calling {@link #reset()} in + * between. + */ + public synchronized MarkAwarePipeline mark() { + if (marked) { + throw new IllegalStateException("Pipeline does not support nested marks."); + } + return this; + } + + /** + * Marks this pipeline and record further changes which can be reverted by calling {@link #reset()} + * + * @throws IllegalStateException If this method is called more than once without calling {@link #reset()} in + * between. + */ + public synchronized MarkAwarePipeline markIfNotYetMarked() { + if (!marked) { + return mark(); + } + return this; + } + + /** + * If {@link #mark()} was called before, resets the pipeline to the state it was before calling {@link #mark()}. + * Otherwise, ignores the reset. + */ + public synchronized MarkAwarePipeline reset() { + if (!marked) { + return this; /*If there is no mark, there is nothing to reset.*/ + } + + marked = false; + + return this; + } + + public synchronized boolean isMarked() { + return marked; + } + + @Override + public ChannelPipeline addFirst(String name, ChannelHandler handler) { + delegate.addFirst(name, handler); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup group, + String name, ChannelHandler handler) { + delegate.addFirst(group, name, handler); + return this; + } + + @Override + public ChannelPipeline addLast(String name, ChannelHandler handler) { + delegate.addLast(name, handler); + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup group, + String name, ChannelHandler handler) { + delegate.addLast(group, name, handler); + return this; + } + + @Override + public ChannelPipeline addBefore(String baseName, String name, + ChannelHandler handler) { + return delegate.addBefore(baseName, name, handler); + } + + @Override + public ChannelPipeline addBefore(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + delegate.addBefore(group, baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addAfter(String baseName, String name, + ChannelHandler handler) { + delegate.addAfter(baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addAfter(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + delegate.addAfter(group, baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addFirst(ChannelHandler... handlers) { + delegate.addFirst(handlers); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup group, + ChannelHandler... handlers) { + delegate.addFirst(group, handlers); + return this; + } + + @Override + public ChannelPipeline addLast(ChannelHandler... handlers) { + delegate.addLast(handlers); + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup group, + ChannelHandler... handlers) { + delegate.addLast(group, handlers); + return this; + } + + @Override + public ChannelPipeline remove(ChannelHandler handler) { + delegate.remove(handler); + return this; + } + + @Override + public ChannelHandler remove(String name) { + return delegate.remove(name); + } + + @Override + public T remove(Class handlerType) { + return delegate.remove(handlerType); + } + + @Override + public ChannelHandler removeFirst() { + return delegate.removeFirst(); + } + + @Override + public ChannelHandler removeLast() { + return delegate.removeLast(); + } + + @Override + public ChannelPipeline replace(ChannelHandler oldHandler, + String newName, ChannelHandler newHandler) { + delegate.replace(oldHandler, newName, newHandler); + return this; + } + + @Override + public ChannelHandler replace(String oldName, String newName, + ChannelHandler newHandler) { + return delegate.replace(oldName, newName, newHandler); + } + + @Override + public T replace(Class oldHandlerType, String newName, + ChannelHandler newHandler) { + return delegate.replace(oldHandlerType, newName, newHandler); + } + + @Override + public ChannelHandler first() { + return delegate.first(); + } + + @Override + public ChannelHandlerContext firstContext() { + return delegate.firstContext(); + } + + @Override + public ChannelHandler last() { + return delegate.last(); + } + + @Override + public ChannelHandlerContext lastContext() { + return delegate.lastContext(); + } + + @Override + public ChannelHandler get(String name) { + return delegate.get(name); + } + + @Override + public T get(Class handlerType) { + return delegate.get(handlerType); + } + + @Override + public ChannelHandlerContext context(ChannelHandler handler) { + return delegate.context(handler); + } + + @Override + public ChannelHandlerContext context(String name) { + return delegate.context(name); + } + + @Override + public ChannelHandlerContext context(Class handlerType) { + return delegate.context(handlerType); + } + + @Override + public Channel channel() { + return delegate.channel(); + } + + @Override + public List names() { + return delegate.names(); + } + + @Override + public Map toMap() { + return delegate.toMap(); + } + + @Override + public ChannelPipeline fireChannelRegistered() { + delegate.fireChannelRegistered(); + return this; + } + + @Override + public ChannelPipeline fireChannelUnregistered() { + delegate.fireChannelUnregistered(); + return this; + } + + @Override + public ChannelPipeline fireChannelActive() { + delegate.fireChannelActive(); + return this; + } + + @Override + public ChannelPipeline fireChannelInactive() { + delegate.fireChannelInactive(); + return this; + } + + @Override + public ChannelPipeline fireExceptionCaught(Throwable cause) { + delegate.fireExceptionCaught(cause); + return this; + } + + @Override + public ChannelPipeline fireUserEventTriggered(Object event) { + delegate.fireUserEventTriggered(event); + return this; + } + + @Override + public ChannelPipeline fireChannelRead(Object msg) { + delegate.fireChannelRead(msg); + return this; + } + + @Override + public ChannelPipeline fireChannelReadComplete() { + delegate.fireChannelReadComplete(); + return this; + } + + @Override + public ChannelPipeline fireChannelWritabilityChanged() { + delegate.fireChannelWritabilityChanged(); + return this; + } + + @Override + public ChannelFuture bind(SocketAddress localAddress) { + return delegate.bind(localAddress); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress) { + return delegate.connect(remoteAddress); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + SocketAddress localAddress) { + return delegate.connect(remoteAddress, localAddress); + } + + @Override + public ChannelFuture disconnect() { + return delegate.disconnect(); + } + + @Override + public ChannelFuture close() { + return delegate.close(); + } + + @Override + public ChannelFuture deregister() { + return delegate.deregister(); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress, + ChannelPromise promise) { + return delegate.bind(localAddress, promise); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + ChannelPromise promise) { + return delegate.connect(remoteAddress, promise); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + SocketAddress localAddress, + ChannelPromise promise) { + return delegate.connect(remoteAddress, localAddress, promise); + } + + @Override + public ChannelFuture disconnect(ChannelPromise promise) { + return delegate.disconnect(promise); + } + + @Override + public ChannelFuture close(ChannelPromise promise) { + return delegate.close(promise); + } + + @Override + public ChannelFuture deregister(ChannelPromise promise) { + return delegate.deregister(promise); + } + + @Override + public ChannelOutboundInvoker read() { + return delegate.read(); + } + + @Override + public ChannelFuture write(Object msg) { + return delegate.write(msg); + } + + @Override + public ChannelFuture write(Object msg, ChannelPromise promise) { + return delegate.write(msg, promise); + } + + @Override + public ChannelPipeline flush() { + return delegate.flush(); + } + + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return delegate.writeAndFlush(msg, promise); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + return delegate.writeAndFlush(msg); + } + + @Override + public ChannelPromise newPromise() { + return delegate.newPromise(); + } + + @Override + public ChannelProgressivePromise newProgressivePromise() { + return delegate.newProgressivePromise(); + } + + @Override + public ChannelFuture newSucceededFuture() { + return delegate.newSucceededFuture(); + } + + @Override + public ChannelFuture newFailedFuture(Throwable cause) { + return delegate.newFailedFuture(cause); + } + + @Override + public ChannelPromise voidPromise() { + return delegate.voidPromise(); + } + + @Override + public Iterator> iterator() { + return delegate.iterator(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridge.java new file mode 100644 index 0000000..b5588e9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridge.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import rx.Subscriber; +import rx.functions.Action0; +import rx.subscriptions.Subscriptions; + +/** + * A bridge to connect a {@link Subscriber} to a {@link ChannelFuture} so that when the {@code subscriber} is + * unsubscribed, the listener will get removed from the {@code future}. Failure to do so for futures that are long + * living, eg: {@link Channel#closeFuture()} will lead to a memory leak where the attached listener will be in the + * listener queue of the future till the channel closes. + * + * In order to bridge the future and subscriber, {@link #bridge(ChannelFuture, Subscriber)} must be called. + */ +public abstract class SubscriberToChannelFutureBridge implements ChannelFutureListener { + + @Override + public final void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + doOnSuccess(future); + } else { + doOnFailure(future, future.cause()); + } + } + + protected abstract void doOnSuccess(ChannelFuture future); + + protected abstract void doOnFailure(ChannelFuture future, Throwable cause); + + /** + * Bridges the passed subscriber and future, which means the following: + * + *
    +
  • Add this listener to the passed future.
  • +
  • Add a callback to the subscriber, such that on unsubscribe this listener is removed from the future.
  • +
+ * + * @param future Future to bridge. + * @param subscriber Subscriber to connect to the future. + */ + public void bridge(final ChannelFuture future, Subscriber subscriber) { + future.addListener(this); + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + future.removeListener(SubscriberToChannelFutureBridge.this); + } + })); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformations.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformations.java new file mode 100644 index 0000000..fce5748 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformations.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBufAllocator; + +import java.util.LinkedList; +import java.util.List; + +/** + * A holder for all transformations that are applied on a channel. Out of the box, it comes with a {@code String} and + * {@code byte[]} transformer to {@code ByteBuf}. Additional transformations can be applied using + * {@link #appendTransformer(AllocatingTransformer)}. + */ +public class WriteTransformations { + + private TransformerChain transformers; + + public boolean transform(Object msg, ByteBufAllocator allocator, List out) { + + boolean transformed = false; + + if (msg instanceof String) { + out.add(allocator.buffer().writeBytes(((String) msg).getBytes())); + transformed = true; + } else if (msg instanceof byte[]) { + out.add(allocator.buffer().writeBytes((byte[]) msg)); + transformed = true; + } else if (null != transformers && transformers.acceptMessage(msg)) { + out.addAll(transformers.transform(msg, allocator)); + transformed = true; + } + + return transformed; + } + + public void appendTransformer(AllocatingTransformer transformer) { + transformers = new TransformerChain(transformer, transformers); + } + + public void resetTransformations() { + transformers = null; + } + + public boolean acceptMessage(Object msg) { + return msg instanceof String || msg instanceof byte[] || null != transformers && transformers.acceptMessage(msg); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static class TransformerChain extends AllocatingTransformer { + + private final AllocatingTransformer start; + private final AllocatingTransformer next; + + public TransformerChain(AllocatingTransformer start, AllocatingTransformer next) { + this.start = start; + this.next = next; + } + + @Override + public List transform(Object toTransform, ByteBufAllocator allocator) { + if (null == next) { + return start.transform(toTransform, allocator); + } + + List transformed = start.transform(toTransform, allocator); + if (transformed.size() == 1) { + return next.transform(transformed.get(0), allocator); + } else { + final LinkedList toReturn = new LinkedList(); + for (Object nextItem : transformed) { + toReturn.addAll(next.transform(nextItem, allocator)); + } + return toReturn; + } + } + + @Override + protected boolean acceptMessage(Object msg) { + return start.acceptMessage(msg); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformer.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformer.java new file mode 100644 index 0000000..786739a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/WriteTransformer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.util.ReferenceCountUtil; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; + +import java.util.List; + +/** + * A {@link ChannelHandler} that transforms objects written to this channel.

+ * + * Any {@code String} or {@code byte[]} written to the channel are converted to {@code ByteBuf} if no other + * {@link AllocatingTransformer} is added that accepts these types. + * + * If the last added {@link AllocatingTransformer} accepts the written message, then invoke all added transformers and + * skip the primitive conversions. + */ +public class WriteTransformer extends MessageToMessageCodec { + + private final WriteTransformations transformations = new WriteTransformations(); + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return false; + } + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return true;// Always return true and let the encode do the checking as opposed to be done at both places. + } + + @Override + protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + if (!transformations.transform(msg, ctx.alloc(), out)) { + /* + * M2MCodec will release the passed message after encode but we are adding the same object to out. + * So, the message needs to be retained and subsequently released by the next consumer in the pipeline. + */ + out.add(ReferenceCountUtil.retain(msg)); + } + } + + @Override + protected void decode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + // Never decode (acceptInbound) always returns false. + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof AppendTransformerEvent) { + @SuppressWarnings("rawtypes") + AppendTransformerEvent ate = (AppendTransformerEvent) evt; + transformations.appendTransformer(ate.getTransformer()); + } else if(evt instanceof ConnectionReuseEvent) { + transformations.resetTransformations(); + } + + super.userEventTriggered(ctx, evt); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventListener.java new file mode 100644 index 0000000..7e5e9f4 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventListener.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel.events; + +import io.reactivex.netty.events.EventListener; + +import java.util.concurrent.TimeUnit; + +/** + * An event listener for all events releated to a {@link io.reactivex.netty.channel.Connection} + */ +public abstract class ConnectionEventListener implements EventListener { + + /** + * Event whenever any bytes are read on any open connection. + * + * @param bytesRead Number of bytes read. + */ + @SuppressWarnings("unused") + public void onByteRead(long bytesRead) { } + + /** + * Event whenever any bytes are successfully written on any open connection. + * + * @param bytesWritten Number of bytes written. + */ + @SuppressWarnings("unused") + public void onByteWritten(long bytesWritten) { } + + /** + * Event whenever a flush is issued on a connection. + */ + public void onFlushStart() {} + + /** + * Event whenever flush completes. + * + * @param duration Duration between flush start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onFlushComplete(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever a write is issued on a connection. + */ + public void onWriteStart() {} + + /** + * Event whenever data is written successfully on a connection. Use {@link #onByteWritten(long)} to capture number + * of bytes written. + * + * @param duration Duration between write start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onWriteSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever a write failed on a connection. + * + * @param duration Duration between write start and failure. + * @param timeUnit Timeunit for the duration. + * @param throwable Error that caused the failure.. + */ + @SuppressWarnings("unused") + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + /** + * Event whenever a close of any connection is issued. This event will only be fired when the physical connection + * is closed and not when a pooled connection is closed and put back in the pool. + */ + @SuppressWarnings("unused") + public void onConnectionCloseStart() {} + + /** + * Event whenever a close of any connection is successful. + * + * @param duration Duration between close start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever a connection close failed. + * + * @param duration Duration between close start and failure. + * @param timeUnit Timeunit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + @Override + public void onCustomEvent(Object event) { } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { } + + @Override + public void onCompleted() { } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventPublisher.java new file mode 100644 index 0000000..87f28cf --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/channel/events/ConnectionEventPublisher.java @@ -0,0 +1,238 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel.events; + +import io.reactivex.netty.events.EventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.events.ListenersHolder; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.functions.Action5; + +import java.util.concurrent.TimeUnit; + +/** + * A publisher which is both {@link EventSource} and {@link EventListener} for connection events. + * + * @param Type of listener to expect. + */ +public final class ConnectionEventPublisher extends ConnectionEventListener + implements EventSource, EventPublisher { + + private final Action2 bytesReadAction = new Action2() { + @Override + public void call(T l, Long bytesRead) { + l.onByteRead(bytesRead); + } + }; + + private final Action2 bytesWrittenAction = new Action2() { + @Override + public void call(T l, Long bytesWritten) { + l.onByteWritten(bytesWritten); + } + }; + + private final Action1 flushStartAction = new Action1() { + @Override + public void call(T l) { + l.onFlushStart(); + } + }; + + private final Action3 flushCompleteAction = new Action3() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit) { + l.onFlushComplete(duration, timeUnit); + } + }; + + private final Action1 writeStartAction = new Action1() { + @Override + public void call(T l) { + l.onWriteStart(); + } + }; + + private final Action3 writeSuccessAction = new Action3() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit) { + l.onWriteSuccess(duration, timeUnit); + } + }; + + private final Action4 writeFailedAction = + new Action4() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onWriteFailed(duration, timeUnit, t); + } + }; + + private final Action1 closeStartAction = new Action1() { + @Override + public void call(T l) { + l.onConnectionCloseStart(); + } + }; + + private final Action3 closeSuccessAction = new Action3() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit) { + l.onConnectionCloseSuccess(duration, timeUnit); + } + }; + + private final Action4 closeFailedAction = + new Action4() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onConnectionCloseFailed(duration, timeUnit, t); + } + }; + + private final Action2 customEventAction = new Action2() { + @Override + public void call(T l, Object event) { + l.onCustomEvent(event); + } + }; + + private final Action3 customEventErrorAction = new Action3() { + @Override + public void call(T l, Throwable throwable, Object event) { + l.onCustomEvent(event, throwable); + } + }; + + private final Action4 customEventDurationAction = new Action4() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit, Object event) { + l.onCustomEvent(event, duration, timeUnit); + } + }; + + private final Action5 customEventDurationErrAction = + new Action5() { + @Override + public void call(T l, Long duration, TimeUnit timeUnit, Throwable throwable, Object event) { + l.onCustomEvent(event, duration, timeUnit, throwable); + } + }; + + private final ListenersHolder listeners; + + public ConnectionEventPublisher() { + listeners = new ListenersHolder<>(); + } + + public ConnectionEventPublisher(ConnectionEventPublisher toCopy) { + listeners = toCopy.listeners.copy(); + } + + @Override + public void onByteRead(final long bytesRead) { + listeners.invokeListeners(bytesReadAction, bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + listeners.invokeListeners(bytesWrittenAction, bytesWritten); + } + + @Override + public void onFlushStart() { + listeners.invokeListeners(flushStartAction); + } + + @Override + public void onFlushComplete(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(flushCompleteAction, duration, timeUnit); + } + + @Override + public void onWriteStart() { + listeners.invokeListeners(writeStartAction); + } + + @Override + public void onWriteSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(writeSuccessAction, duration, timeUnit); + } + + @Override + public void onWriteFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(writeFailedAction, duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseStart() { + listeners.invokeListeners(closeStartAction); + } + + @Override + public void onConnectionCloseSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(closeSuccessAction, duration, timeUnit); + } + + @Override + public void onConnectionCloseFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(closeFailedAction, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event) { + listeners.invokeListeners(customEventAction, event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + listeners.invokeListeners(customEventDurationAction, duration, timeUnit, event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + listeners.invokeListeners(customEventDurationErrAction, duration, timeUnit, throwable, event); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + listeners.invokeListeners(customEventErrorAction, throwable, event); + } + + @Override + public Subscription subscribe(T listener) { + return listeners.subscribe(listener); + } + + @Override + public boolean publishingEnabled() { + return listeners.publishingEnabled(); + } + + public ConnectionEventPublisher copy() { + return new ConnectionEventPublisher<>(this); + } + + /*Visible for testing*/ ListenersHolder getListeners() { + return listeners; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProvider.java new file mode 100644 index 0000000..ad63f27 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client; + +import io.netty.channel.Channel; +import rx.Observable; + +public interface ChannelProvider { + + Observable newChannel(Observable input); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProviderFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProviderFactory.java new file mode 100644 index 0000000..7d6c78a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ChannelProviderFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client; + +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; + +public interface ChannelProviderFactory { + + ChannelProvider newProvider(Host host, EventSource eventSource, + EventPublisher publisher, ClientEventListener clientPublisher); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientConnectionToChannelBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientConnectionToChannelBridge.java new file mode 100644 index 0000000..1f9b443 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientConnectionToChannelBridge.java @@ -0,0 +1,202 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.util.AttributeKey; +import io.reactivex.netty.channel.AbstractConnectionToChannelBridge; +import io.reactivex.netty.channel.ChannelSubscriberEvent; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionInputSubscriberResetEvent; +import io.reactivex.netty.channel.EmitConnectionEvent; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.client.pool.PooledConnection; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.internal.ExecuteInEventloopAction; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Actions; +import rx.observers.SafeSubscriber; +import rx.subscriptions.Subscriptions; + +/** + * An implementation of {@link AbstractConnectionToChannelBridge} for clients. + * + *

Reuse

+ * + * A channel can be reused for multiple operations, provided the reuses is signalled by {@link ConnectionReuseEvent}. + * Failure to do so, will result in errors on the {@link Subscriber} trying to reuse the channel. + * A typical reuse should have the following events: + * +
+    ChannelSubscriberEvent => ConnectionInputSubscriberEvent => ConnectionReuseEvent =>
+    ConnectionInputSubscriberEvent => ConnectionReuseEvent => ConnectionInputSubscriberEvent
+ 
+ * + * @param Type read from the connection held by this handler. + * @param Type written to the connection held by this handler. + */ +public class ClientConnectionToChannelBridge extends AbstractConnectionToChannelBridge { + + public static final AttributeKey DISCARD_CONNECTION = AttributeKey.valueOf("rxnetty_discard_connection"); + + private static final Logger logger = Logger.getLogger(ClientConnectionToChannelBridge.class.getName()); + private static final String HANDLER_NAME = "client-conn-channel-bridge"; + + private EventPublisher eventPublisher; + private ClientEventListener eventListener; + private final boolean isSecure; + private Channel channel; + + private ClientConnectionToChannelBridge(boolean isSecure) { + super(HANDLER_NAME, EventAttributeKeys.CONNECTION_EVENT_LISTENER, EventAttributeKeys.EVENT_PUBLISHER); + this.isSecure = isSecure; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + channel = ctx.channel(); + eventPublisher = channel.attr(EventAttributeKeys.EVENT_PUBLISHER).get(); + eventListener = ctx.channel().attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).get(); + + if (null == eventPublisher) { + logger.log(Level.SEVERE, "No Event publisher bound to the channel, closing channel."); + ctx.channel().close(); + return; + } + + if (eventPublisher.publishingEnabled() && null == eventListener) { + logger.log(Level.SEVERE, "No Event listener bound to the channel and event publishing is enabled., closing channel."); + ctx.channel().close(); + return; + } + + super.handlerAdded(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + if (!isSecure) {/*When secure, the event is triggered post SSL handshake via the SslCodec*/ + userEventTriggered(ctx, EmitConnectionEvent.INSTANCE); + } + super.channelActive(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + + super.userEventTriggered(ctx, evt); // Super handles ConnectionInputSubscriberResetEvent to reset the subscriber. + + if (evt instanceof ConnectionReuseEvent) { + @SuppressWarnings("unchecked") + ConnectionReuseEvent event = (ConnectionReuseEvent) evt; + + newConnectionReuseEvent(ctx.channel(), event); + } + } + + @Override + protected void onNewReadSubscriber(Subscriber subscriber) { + // Unsubscribe from the input closes the connection as there can only be one subscriber to the + // input and, if nothing is read, it means, nobody is using the connection. + // For fire-and-forget usecases, one should explicitly ignore content on the connection which + // adds a discard all subscriber that never unsubscribes. For this case, then, the close becomes + // explicit. + subscriber.add(Subscriptions.create(new ExecuteInEventloopAction(channel) { + @Override + public void run() { + if (!connectionInputSubscriberExists(channel)) { + Connection connection = channel.attr(Connection.CONNECTION_ATTRIBUTE_KEY).get(); + if (null != connection) { + connection.closeNow(); + } + } + } + })); + } + + private void newConnectionReuseEvent(Channel channel, final ConnectionReuseEvent event) { + Subscriber> subscriber = event.getSubscriber(); + if (isValidToEmit(subscriber)) { + subscriber.onNext(event.getPooledConnection()); + checkEagerSubscriptionIfConfigured(channel); + } else { + // If pooled connection not sent to the subscriber, release to the pool. + event.getPooledConnection().close(false).subscribe(Actions.empty(), new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Error closing connection.", throwable); + } + }); + } + } + + public static ClientConnectionToChannelBridge addToPipeline(ChannelPipeline pipeline, + boolean isSecure) { + ClientConnectionToChannelBridge toAdd = new ClientConnectionToChannelBridge<>(isSecure); + pipeline.addLast(HANDLER_NAME, toAdd); + return toAdd; + } + + /** + * An event to indicate channel/{@link Connection} reuse. This event should be used for clients that pool + * connections. For every reuse of a connection (connection creation still uses {@link ChannelSubscriberEvent}) + * the corresponding subscriber must be sent via this event. + * + * Every instance of this event resets the older subscriber attached to the connection and connection input. This + * means sending an {@link Subscriber#onCompleted()} to both of those subscribers. It is assumed that the actual + * {@link Subscriber} is similar to {@link SafeSubscriber} which can handle duplicate terminal events. + * + * @param Type read from the connection held by the event. + * @param Type written to the connection held by the event. + */ + public static final class ConnectionReuseEvent implements ConnectionInputSubscriberResetEvent { + + private final Subscriber> subscriber; + private final PooledConnection pooledConnection; + + public ConnectionReuseEvent(Subscriber> subscriber, + PooledConnection pooledConnection) { + this.subscriber = subscriber; + this.pooledConnection = pooledConnection; + } + + public Subscriber> getSubscriber() { + return subscriber; + } + + public PooledConnection getPooledConnection() { + return pooledConnection; + } + } + + /** + * An event to indicate release of a {@link PooledConnection}. + */ + public static final class PooledConnectionReleaseEvent { + + public static final PooledConnectionReleaseEvent INSTANCE = new PooledConnectionReleaseEvent(); + + private PooledConnectionReleaseEvent() { + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientState.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientState.java new file mode 100644 index 0000000..0a243b6 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ClientState.java @@ -0,0 +1,505 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.RxNetty; +import io.reactivex.netty.channel.ChannelSubscriberEvent; +import io.reactivex.netty.channel.ConnectionCreationFailedEvent; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.channel.WriteTransformer; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.ssl.DefaultSslCodec; +import io.reactivex.netty.ssl.SslCodec; +import io.reactivex.netty.util.LoggingHandlerFactory; +import rx.Observable; +import rx.exceptions.Exceptions; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import static io.reactivex.netty.HandlerNames.*; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + + +/** + * A collection of state that a client holds. This supports the copy-on-write semantics of clients. + * + * @param The type of objects written to the client owning this state. + * @param The type of objects read from the client owning this state. + */ +public class ClientState { + + private final Observable hostStream; + private final ConnectionProviderFactory factory; + private final DetachedChannelPipeline detachedPipeline; + private final Map, Object> options; + private final boolean isSecure; + private final EventLoopGroup eventLoopGroup; + private final Class channelClass; + private final ChannelProviderFactory channelProviderFactory; + + protected ClientState(Observable hostStream, ConnectionProviderFactory factory, + DetachedChannelPipeline detachedPipeline, EventLoopGroup eventLoopGroup, + Class channelClass) { + this.eventLoopGroup = eventLoopGroup; + this.channelClass = channelClass; + options = new LinkedHashMap<>(); /// Same as netty bootstrap, order matters. + this.hostStream = hostStream; + this.factory = factory; + this.detachedPipeline = detachedPipeline; + isSecure = false; + channelProviderFactory = new ChannelProviderFactory() { + @Override + public ChannelProvider newProvider(Host host, EventSource eventSource, + EventPublisher publisher, ClientEventListener clientPublisher) { + return new ChannelProvider() { + @Override + public Observable newChannel(Observable input) { + return input; + } + }; + } + }; + } + + protected ClientState(ClientState toCopy, ChannelOption option, Object value) { + options = new LinkedHashMap<>(toCopy.options); // Since, we are adding an option, copy it. + options.put(option, value); + detachedPipeline = toCopy.detachedPipeline; + hostStream = toCopy.hostStream; + factory = toCopy.factory; + eventLoopGroup = toCopy.eventLoopGroup; + channelClass = toCopy.channelClass; + isSecure = toCopy.isSecure; + channelProviderFactory = toCopy.channelProviderFactory; + } + + protected ClientState(ClientState toCopy, DetachedChannelPipeline newPipeline, boolean secure) { + final ClientState toCopyCast = toCopy.cast(); + options = toCopy.options; + hostStream = toCopy.hostStream; + factory = toCopyCast.factory; + eventLoopGroup = toCopy.eventLoopGroup; + channelClass = toCopy.channelClass; + detachedPipeline = newPipeline; + isSecure = secure; + channelProviderFactory = toCopyCast.channelProviderFactory; + } + + protected ClientState(ClientState toCopy, ChannelProviderFactory newFactory) { + final ClientState toCopyCast = toCopy.cast(); + options = toCopy.options; + hostStream = toCopy.hostStream; + factory = toCopyCast.factory; + eventLoopGroup = toCopy.eventLoopGroup; + channelClass = toCopy.channelClass; + detachedPipeline = toCopy.detachedPipeline; + channelProviderFactory = newFactory; + isSecure = toCopy.isSecure; + } + + protected ClientState(ClientState toCopy, SslCodec sslCodec) { + this(toCopy, toCopy.detachedPipeline.copy(new TailHandlerFactory(true)).configure(sslCodec), true); + } + + public ClientState channelOption(ChannelOption option, T value) { + return new ClientState<>(this, option, value); + } + + public ClientState addChannelHandlerFirst(String name, Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addFirst(name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addFirst(group, name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerLast(String name, Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addLast(name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addLast(group, name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addBefore(baseName, name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addBefore(group, baseName, name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addAfter(baseName, name, handlerFactory); + return copy; + } + + public ClientState addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory) { + ClientState copy = copy(); + copy.detachedPipeline.addAfter(group, baseName, name, handlerFactory); + return copy; + } + + public ClientState pipelineConfigurator(Action1 pipelineConfigurator) { + ClientState copy = copy(); + copy.detachedPipeline.configure(pipelineConfigurator); + return copy; + } + + public ClientState enableWireLogging(final LogLevel wireLoggingLevel) { + return enableWireLogging(LoggingHandler.class.getName(), wireLoggingLevel); + } + + public ClientState enableWireLogging(String name, final LogLevel wireLoggingLevel) { + return addChannelHandlerFirst(WireLogging.getName(), + LoggingHandlerFactory.getFactory(name, wireLoggingLevel)); + } + + public static ClientState create(ConnectionProviderFactory factory, + Observable hostStream) { + return create(newChannelPipeline(new TailHandlerFactory(false)), factory, hostStream); + } + + public static ClientState create(ConnectionProviderFactory factory, + Observable hostStream, + EventLoopGroup eventLoopGroup, + Class channelClass) { + return new ClientState<>(hostStream, factory, newChannelPipeline(new TailHandlerFactory(false)), eventLoopGroup, + channelClass); + } + + public static ClientState create(DetachedChannelPipeline detachedPipeline, + ConnectionProviderFactory factory, + Observable hostStream) { + return create(detachedPipeline, factory, hostStream, defaultEventloopGroup(), defaultSocketChannelClass()); + } + + public static ClientState create(DetachedChannelPipeline detachedPipeline, + ConnectionProviderFactory factory, + Observable hostStream, + EventLoopGroup eventLoopGroup, + Class channelClass) { + return new ClientState<>(hostStream, factory, detachedPipeline, eventLoopGroup, channelClass); + } + + private static DetachedChannelPipeline newChannelPipeline(TailHandlerFactory thf) { + return new DetachedChannelPipeline(thf) + .addLast(WriteTransformer.getName(), new Func0() { + @Override + public ChannelHandler call() { + return new WriteTransformer(); + } + }); + } + + public Bootstrap newBootstrap(final EventPublisher eventPublisher, final ClientEventListener eventListener) { + final Bootstrap nettyBootstrap = new Bootstrap().group(eventLoopGroup) + .channel(channelClass) + .option(ChannelOption.AUTO_READ, false);// by default do not read content unless asked. + + for (Entry, Object> optionEntry : options.entrySet()) { + // Type is just for safety for user of ClientState, internally in Bootstrap, types are thrown on the floor. + @SuppressWarnings("unchecked") + ChannelOption key = (ChannelOption) optionEntry.getKey(); + nettyBootstrap.option(key, optionEntry.getValue()); + } + + nettyBootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(ClientChannelActiveBufferingHandler.getName(), + new ChannelActivityBufferingHandler(eventPublisher, eventListener)); + } + }); + return nettyBootstrap; + } + + public DetachedChannelPipeline unsafeDetachedPipeline() { + return detachedPipeline; + } + + public Map, Object> unsafeChannelOptions() { + return options; + } + + public ClientState channelProviderFactory(ChannelProviderFactory factory) { + return new ClientState<>(this, factory); + } + + public ClientState secure(Func1 sslEngineFactory) { + return secure(new DefaultSslCodec(sslEngineFactory)); + } + + public ClientState secure(SSLEngine sslEngine) { + return secure(new DefaultSslCodec(sslEngine)); + } + + public ClientState secure(SslCodec sslCodec) { + return new ClientState<>(this, sslCodec); + } + + public ClientState unsafeSecure() { + return secure(new DefaultSslCodec(new Func1() { + @Override + public SSLEngine call(ByteBufAllocator allocator) { + try { + return SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build() + .newEngine(allocator); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + })); + } + + private ClientState copy() { + TailHandlerFactory newTail = new TailHandlerFactory(isSecure); + return new ClientState<>(this, detachedPipeline.copy(newTail), isSecure); + } + + public ConnectionProviderFactory getFactory() { + return factory; + } + + public Observable getHostStream() { + return hostStream; + } + + public ChannelProviderFactory getChannelProviderFactory() { + return channelProviderFactory; + } + + @SuppressWarnings("unchecked") + private ClientState cast() { + return (ClientState) this; + } + + protected static class TailHandlerFactory implements Action1 { + + private final boolean isSecure; + + public TailHandlerFactory(boolean isSecure) { + this.isSecure = isSecure; + } + + @Override + public void call(ChannelPipeline pipeline) { + ClientConnectionToChannelBridge.addToPipeline(pipeline, isSecure); + } + } + + public static EventLoopGroup defaultEventloopGroup() { + return RxNetty.getRxEventLoopProvider().globalClientEventLoop(true); + } + + public static Class defaultSocketChannelClass() { + return RxNetty.isUsingNativeTransport() ? EpollSocketChannel.class : NioSocketChannel.class; + } + + /** + * Clients construct the pipeline, outside of the {@link ChannelInitializer} through {@link ChannelProvider}. + * Thus channel registration and activation events may be lost due to a race condition when the channel is active + * before the pipeline is configured. + * This handler buffers, the channel events till the time, a subscriber appears for channel establishment. + */ + private static class ChannelActivityBufferingHandler extends ChannelDuplexHandler { + + private enum State { + Initialized, + Registered, + Active, + Inactive, + ChannelSubscribed + } + + private State state = State.Initialized; + + /** + * Unregistered state will hide the active/inactive state, hence this is a different flag. + */ + private boolean unregistered; + private long connectStartTimeNanos; + private final EventPublisher eventPublisher; + private final ClientEventListener eventListener; + + private ChannelActivityBufferingHandler(EventPublisher eventPublisher, ClientEventListener eventListener) { + this.eventPublisher = eventPublisher; + this.eventListener = eventListener; + } + + @SuppressWarnings("unchecked") + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + + connectStartTimeNanos = Clock.newStartTimeNanos(); + + if (eventPublisher.publishingEnabled()) { + eventListener.onConnectStart(); + promise.addListener(new ChannelFutureListener() { + @SuppressWarnings("unchecked") + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (eventPublisher.publishingEnabled()) { + long endTimeNanos = Clock.onEndNanos(connectStartTimeNanos); + if (!future.isSuccess()) { + eventListener.onConnectFailed(endTimeNanos, NANOSECONDS, future.cause()); + } else { + eventListener.onConnectSuccess(endTimeNanos, NANOSECONDS); + } + } + } + }); + } + + super.connect(ctx, remoteAddress, localAddress, promise); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + if (State.ChannelSubscribed == state) { + super.channelRegistered(ctx); + } else { + state = State.Registered; + } + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + if (State.ChannelSubscribed == state) { + super.channelUnregistered(ctx); + } else { + unregistered = true; + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + if (State.ChannelSubscribed == state) { + super.channelActive(ctx); + } else { + state = State.Active; + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (State.ChannelSubscribed == state) { + super.channelInactive(ctx); + } else { + state = State.Inactive; + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ChannelSubscriberEvent) { + final State existingState = state; + state = State.ChannelSubscribed; + super.userEventTriggered(ctx, evt); + final ChannelPipeline pipeline = ctx.channel().pipeline(); + switch (existingState) { + case Initialized: + break; + case Registered: + pipeline.fireChannelRegistered(); + break; + case Active: + pipeline.fireChannelRegistered(); + pipeline.fireChannelActive(); + break; + case Inactive: + pipeline.fireChannelRegistered(); + pipeline.fireChannelActive(); + pipeline.fireChannelInactive(); + break; + case ChannelSubscribed: + // Duplicate event, ignore. + break; + } + + if (unregistered) { + pipeline.fireChannelUnregistered(); + } + } else if (evt instanceof ConnectionCreationFailedEvent) { + ConnectionCreationFailedEvent failedEvent = (ConnectionCreationFailedEvent) evt; + onConnectFailedEvent(failedEvent); + super.userEventTriggered(ctx, evt); + } else { + super.userEventTriggered(ctx, evt); + } + } + + @SuppressWarnings("unchecked") + private void onConnectFailedEvent(ConnectionCreationFailedEvent event) { + if (eventPublisher.publishingEnabled()) { + eventListener.onConnectFailed(connectStartTimeNanos, NANOSECONDS, event.getThrowable()); + } + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProvider.java new file mode 100644 index 0000000..0c0d6e4 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client; + +import io.reactivex.netty.channel.Connection; +import rx.Observable; + +/** + * A contract to control how connections are established from a client. + * + * @param The type of objects written on the connections created by this provider. + * @param The type of objects read from the connections created by this provider. + */ +public interface ConnectionProvider { + + /** + * Returns an {@code Observable} that emits a single connection every time it is subscribed. + * + * @return An {@code Observable} that emits a single connection every time it is subscribed. + */ + Observable> newConnectionRequest(); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProviderFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProviderFactory.java new file mode 100644 index 0000000..335085a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client; + +import rx.Observable; + +public interface ConnectionProviderFactory { + + ConnectionProvider newProvider(Observable> hosts); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionRequest.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionRequest.java new file mode 100644 index 0000000..595da5f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/ConnectionRequest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import io.reactivex.netty.channel.Connection; +import rx.Observable; + +/** + * A connection request that is used to create connections for different protocols. + * + *

Mutations

+ * + * All mutations to this request that creates a brand new instance. + * + *

Inititating connections

+ * + * A new connection is initiated every time {@link ConnectionRequest#subscribe()} is called and is the only way of + * creating connections. + * + * @param The type of the objects that are written to the connection created by this request. + * @param The type of objects that are read from the connection created by this request. + */ +public abstract class ConnectionRequest extends Observable> { + + protected ConnectionRequest(OnSubscribe> f) { + super(f); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/Host.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/Host.java new file mode 100644 index 0000000..afae0e3 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/Host.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client; + +import rx.Observable; + +import java.net.SocketAddress; + +public final class Host { + + private final SocketAddress host; + private final Observable closeNotifier; + + public Host(SocketAddress host) { + this(host, Observable.never()); + } + + public Host(SocketAddress host, Observable closeNotifier) { + this.host = host; + this.closeNotifier = closeNotifier; + } + + public SocketAddress getHost() { + return host; + } + + public Observable getCloseNotifier() { + return closeNotifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Host)) { + return false; + } + + Host host1 = (Host) o; + + if (host != null? !host.equals(host1.host) : host1.host != null) { + return false; + } + return closeNotifier != null? closeNotifier.equals(host1.closeNotifier) : host1.closeNotifier == null; + + } + + @Override + public int hashCode() { + int result = host != null? host.hashCode() : 0; + result = 31 * result + (closeNotifier != null? closeNotifier.hashCode() : 0); + return result; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/HostConnector.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/HostConnector.java new file mode 100644 index 0000000..e95d5e5 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/HostConnector.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import rx.Subscription; + +public class HostConnector implements EventSource { + + private final Host host; + private final ConnectionProvider connectionProvider; + @SuppressWarnings("rawtypes") + private final EventSource eventSource; + private final EventPublisher publisher; + private final ClientEventListener clientPublisher; + + public HostConnector(Host host, ConnectionProvider connectionProvider, + EventSource eventSource, EventPublisher publisher, + ClientEventListener clientPublisher) { + this.host = host; + this.connectionProvider = connectionProvider; + this.eventSource = eventSource; + this.publisher = publisher; + this.clientPublisher = clientPublisher; + } + + public HostConnector(HostConnector source, ConnectionProvider connectionProvider) { + this.connectionProvider = connectionProvider; + host = source.host; + eventSource = source.eventSource; + clientPublisher = source.clientPublisher; + publisher = source.publisher; + } + + public Host getHost() { + return host; + } + + public ConnectionProvider getConnectionProvider() { + return connectionProvider; + } + + public ClientEventListener getClientPublisher() { + return clientPublisher; + } + + public EventPublisher getEventPublisher() { + return publisher; + } + + @Override + @SuppressWarnings("unchecked") + public Subscription subscribe(ClientEventListener listener) { + return eventSource.subscribe(listener); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostConnector)) { + return false; + } + + HostConnector that = (HostConnector) o; + + if (host != null? !host.equals(that.host) : that.host != null) { + return false; + } + if (connectionProvider != null? !connectionProvider.equals(that.connectionProvider) : + that.connectionProvider != null) { + return false; + } + if (eventSource != null? !eventSource.equals(that.eventSource) : that.eventSource != null) { + return false; + } + if (publisher != null? !publisher.equals(that.publisher) : that.publisher != null) { + return false; + } + return clientPublisher != null? clientPublisher.equals(that.clientPublisher) : that.clientPublisher == null; + + } + + @Override + public int hashCode() { + int result = host != null? host.hashCode() : 0; + result = 31 * result + (connectionProvider != null? connectionProvider.hashCode() : 0); + result = 31 * result + (eventSource != null? eventSource.hashCode() : 0); + result = 31 * result + (publisher != null? publisher.hashCode() : 0); + result = 31 * result + (clientPublisher != null? clientPublisher.hashCode() : 0); + return result; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/events/ClientEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/events/ClientEventListener.java new file mode 100644 index 0000000..0325c60 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/events/ClientEventListener.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.events; + +import io.reactivex.netty.channel.events.ConnectionEventListener; + +import java.util.concurrent.TimeUnit; + +public class ClientEventListener extends ConnectionEventListener { + /** + * Event whenever a new connection attempt is made. + */ + @SuppressWarnings("unused") + public void onConnectStart() {} + + /** + * Event whenever a new connection is successfully established. + * + * @param duration Duration between connect start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onConnectSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever a connect attempt failed. + * + * @param duration Duration between connect start and failure. + * @param timeUnit Timeunit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + /** + * Event whenever a connection release to the pool is initiated (by closing the connection) + */ + @SuppressWarnings("unused") + public void onPoolReleaseStart() {} + + /** + * Event whenever a connection is successfully released to the pool. + * + * @param duration Duration between release start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever a connection release to pool fails. + * + * @param duration Duration between release start and failure. + * @param timeUnit Timeunit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + /** + * Event whenever an idle connection is removed/evicted from the pool. + */ + @SuppressWarnings("unused") + public void onPooledConnectionEviction() {} + + /** + * Event whenever a connection is reused from the pool. + */ + @SuppressWarnings("unused") + public void onPooledConnectionReuse() {} + + /** + * Event whenever an acquire from the pool is initiated. + */ + @SuppressWarnings("unused") + public void onPoolAcquireStart() {} + + /** + * Event whenever an acquire from the pool is successful. + * + * @param duration Duration between acquire start and completion. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event whenever an acquire from the pool failed. + * + * @param duration Duration between acquire start and failure. + * @param timeUnit Timeunit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/internal/SingleHostConnectionProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/internal/SingleHostConnectionProvider.java new file mode 100644 index 0000000..01d0086 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/internal/SingleHostConnectionProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.internal; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.HostConnector; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.functions.Action1; + +/** + * A connection provider that only ever fetches a single host from the host stream provided to it. + * + * @param The type of objects written on the connections created by this provider. + * @param The type of objects read from the connections created by this provider. + */ +public class SingleHostConnectionProvider implements ConnectionProvider { + + private static final Logger logger = Logger.getLogger(SingleHostConnectionProvider.class.getName()); + + private volatile ConnectionProvider provider; + + public SingleHostConnectionProvider(Observable> connectors) { + connectors.toSingle() + .subscribe(connector -> provider = connector.getConnectionProvider(), + t -> logger.log(Level.SEVERE, "Failed while fetching a host connector from a scalar host source", t)); + } + + @Override + public Observable> newConnectionRequest() { + return null != provider ? provider.newConnectionRequest() + : Observable.>error(new IllegalStateException("No hosts available.")); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategy.java new file mode 100644 index 0000000..11dd776 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategy.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; +import rx.Observable; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public abstract class AbstractP2CStrategy implements LoadBalancingStrategy { + + @Override + public ConnectionProvider newStrategy(final List> hosts) { + newHostsList(hosts.size()); + return new ConnectionProvider() { + + @Override + public Observable> newConnectionRequest() { + HostHolder selected = null; + if (hosts.isEmpty()) { + noUsableHostsFound(); + return Observable.error(NoHostsAvailableException.EMPTY_INSTANCE); + } else if (hosts.size() == 1) { + HostHolder holder = hosts.get(0); + @SuppressWarnings("unchecked") + L eventListener = (L) holder.getEventListener(); + double weight = getWeight(eventListener); + if (isUnusable(weight)) { + noUsableHostsFound(); + return Observable.error(new NoHostsAvailableException("No usable hosts found.")); + } + selected = holder; + } else { + ThreadLocalRandom rand = ThreadLocalRandom.current(); + for (int i = 0; i < 5; i++) { + int pos = rand.nextInt(hosts.size()); + HostHolder first = hosts.get(pos); + int pos2 = (rand.nextInt(hosts.size() - 1) + pos + 1) % hosts.size(); + HostHolder second = hosts.get(pos2); + + @SuppressWarnings("unchecked") + double w1 = getWeight((L) first.getEventListener()); + @SuppressWarnings("unchecked") + double w2 = getWeight((L) second.getEventListener()); + + if (w1 > w2) { + selected = first; + break; + } else if (w1 < w2) { + selected = second; + break; + } else if (!isUnusable(w1)) { + selected = first; + break; + } + foundTwoUnusableHosts(); + } + if (null == selected) { + noUsableHostsFound(); + return Observable.error(new NoHostsAvailableException("No usable hosts found after 5 tries.")); + } + } + + return selected.getConnector().getConnectionProvider().newConnectionRequest(); + } + }; + } + + protected boolean isUnusable(double weight) { + return weight < 0.0; + } + + @Override + public HostHolder toHolder(HostConnector connector) { + return new HostHolder<>(connector, newListener(connector.getHost())); + } + + protected abstract L newListener(Host host); + + protected abstract double getWeight(L listener); + + protected void noUsableHostsFound() { + // No Op by default + } + + protected void foundTwoUnusableHosts() { + // No Op by default + } + + protected void newHostsList(int size) { + // No Op by default + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostCollector.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostCollector.java new file mode 100644 index 0000000..9170413 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostCollector.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import rx.Single; +import rx.functions.Func1; + +import java.util.List; + +public interface HostCollector { + + Func1, Single>>> newCollector(); + + final class HostUpdate { + + public enum Action{ Add, Remove } + + private final Action action; + private final HostHolder hostHolder; + + public HostUpdate(Action action, HostHolder hostHolder) { + this.action = action; + this.hostHolder = hostHolder; + } + + public Action getAction() { + return action; + } + + public HostHolder getHostHolder() { + return hostHolder; + } + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostHolder.java new file mode 100644 index 0000000..050c060 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/HostHolder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; + +public class HostHolder { + + private final HostConnector connector; + private final ClientEventListener eventListener; + + public HostHolder(HostConnector connector, ClientEventListener eventListener) { + this.connector = connector; + this.eventListener = eventListener; + } + + public HostConnector getConnector() { + return connector; + } + + public ClientEventListener getEventListener() { + return eventListener; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostHolder)) { + return false; + } + + HostHolder that = (HostHolder) o; + + if (connector != null? !connector.equals(that.connector) : that.connector != null) { + return false; + } + return eventListener != null? eventListener.equals(that.eventListener) : that.eventListener == null; + + } + + @Override + public int hashCode() { + int result = connector != null? connector.hashCode() : 0; + result = 31 * result + (eventListener != null? eventListener.hashCode() : 0); + return result; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactory.java new file mode 100644 index 0000000..cf4aa03 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.loadbalancer.HostCollector.HostUpdate; +import io.reactivex.netty.client.loadbalancer.HostCollector.HostUpdate.Action; +import io.reactivex.netty.internal.VoidToAnythingCast; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Single; +import rx.functions.Func1; + +import java.util.List; + +public class LoadBalancerFactory implements ConnectionProviderFactory { + + private static final Logger logger = Logger.getLogger(LoadBalancerFactory.class.getName()); + + private final LoadBalancingStrategy strategy; + private final HostCollector collector; + + private LoadBalancerFactory(LoadBalancingStrategy strategy, HostCollector collector) { + this.strategy = strategy; + this.collector = collector; + } + + @Override + public ConnectionProvider newProvider(Observable> hosts) { + + return new ConnectionProviderImpl(hosts.map(connector -> { + HostHolder newHolder = strategy.toHolder(connector); + connector.subscribe(newHolder.getEventListener()); + return newHolder; + }).flatMap((Func1, Observable>>) holder -> holder.getConnector() + .getHost() + .getCloseNotifier() + .map(new VoidToAnythingCast>()) + .ignoreElements() + .onErrorResumeNext(Observable.>empty()) + .concatWith(Observable.just(new HostUpdate<>(Action.Remove, holder))) + .mergeWith(Observable.just(new HostUpdate<>(Action.Add, holder)))).flatMap(newCollector(collector.newCollector()), 1).distinctUntilChanged()); + } + + public static LoadBalancerFactory create(LoadBalancingStrategy strategy) { + return create(strategy, new NoBufferHostCollector()); + } + + public static LoadBalancerFactory create(LoadBalancingStrategy strategy, + HostCollector collector) { + return new LoadBalancerFactory<>(strategy, collector); + } + + private class ConnectionProviderImpl implements ConnectionProvider { + + private volatile ConnectionProvider currentProvider = () -> + Observable.error(NoHostsAvailableException.EMPTY_INSTANCE); + + public ConnectionProviderImpl(Observable>> hosts) { + hosts.subscribe(hostHolders -> currentProvider = strategy.newStrategy(hostHolders), + throwable -> logger.log(Level.SEVERE, "Error while listening on the host stream. Hosts will not be refreshed.", throwable)); + } + + @Override + public Observable> newConnectionRequest() { + return currentProvider.newConnectionRequest(); + } + } + + private Func1, ? extends Observable>>> + newCollector(final Func1, Single>>> f) { + return (Func1, Observable>>>) holder -> + f.call(holder).toObservable(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancingStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancingStrategy.java new file mode 100644 index 0000000..6aa4d7e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/LoadBalancingStrategy.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.HostConnector; + +import java.util.List; + +public interface LoadBalancingStrategy { + + ConnectionProvider newStrategy(List> hosts); + + HostHolder toHolder(HostConnector connector); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoBufferHostCollector.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoBufferHostCollector.java new file mode 100644 index 0000000..40c3e61 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoBufferHostCollector.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import rx.Single; +import rx.functions.Func1; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link HostCollector} implementation that does not buffer any updates and hence emits a new list for every new + * host received or a host removed. + */ +public class NoBufferHostCollector implements HostCollector { + + private final boolean allowDuplicates; + + public NoBufferHostCollector() { + this(false); + } + + public NoBufferHostCollector(boolean allowDuplicates) { + this.allowDuplicates = allowDuplicates; + } + + @Override + public Func1, Single>>> newCollector() { + return new Func1, Single>>>() { + + private volatile List> currentList = Collections.emptyList(); + + @Override + public Single>> call(HostUpdate update) { + List> newList = null; + + switch (update.getAction()) { + case Add: + if (allowDuplicates || !currentList.contains(update.getHostHolder())) { + newList = new ArrayList<>(currentList); + newList.add(update.getHostHolder()); + } + break; + case Remove: + newList = new ArrayList<>(currentList); + newList.remove(update.getHostHolder()); + break; + } + + if (null != newList) { + currentList = newList; + } + + return Single.just(currentList); + } + }; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoHostsAvailableException.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoHostsAvailableException.java new file mode 100644 index 0000000..34aa530 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/loadbalancer/NoHostsAvailableException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.netty.util.internal.EmptyArrays; + +/** + * Exception raised when there are no eligible hosts available to a load balancer. + */ +public class NoHostsAvailableException extends RuntimeException { + + private static final long serialVersionUID = 7993688893506534768L; + + @SuppressWarnings("ThrowableInstanceNeverThrown") + public static final NoHostsAvailableException EMPTY_INSTANCE = new NoHostsAvailableException(); + + static { + EMPTY_INSTANCE.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + + public NoHostsAvailableException() { + } + + public NoHostsAvailableException(String message) { + super(message); + } + + public NoHostsAvailableException(String message, Throwable cause) { + super(message, cause); + } + + public NoHostsAvailableException(Throwable cause) { + super(cause); + } + + public NoHostsAvailableException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/CompositePoolLimitDeterminationStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/CompositePoolLimitDeterminationStrategy.java new file mode 100644 index 0000000..239f748 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/CompositePoolLimitDeterminationStrategy.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.pool; + +import java.util.concurrent.TimeUnit; + +public class CompositePoolLimitDeterminationStrategy implements PoolLimitDeterminationStrategy { + + private final PoolLimitDeterminationStrategy[] strategies; + + public CompositePoolLimitDeterminationStrategy(PoolLimitDeterminationStrategy... strategies) { + if (null == strategies || strategies.length == 0) { + throw new IllegalArgumentException("Strategies can not be null or empty."); + } + for (PoolLimitDeterminationStrategy strategy : strategies) { + if (null == strategy) { + throw new IllegalArgumentException("No strategy can be null."); + } + } + this.strategies = strategies; + } + + @Override + public boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit) { + for (int i = 0; i < strategies.length; i++) { + PoolLimitDeterminationStrategy strategy = strategies[i]; + if (!strategy.acquireCreationPermit(acquireStartTime, timeUnit)) { + if (i > 0) { + for (int j = i - 1; j >= 0; j--) { + strategies[j].releasePermit(); // release all permits acquired before this failure. + } + } + return false; + } + } + return true; // nothing failed and hence it is OK to create a new connection. + } + + /** + * Returns the minimum number of permits available across all strategies. + * + * @return The minimum number of permits available across all strategies. + */ + @Override + public int getAvailablePermits() { + int minPermits = Integer.MAX_VALUE; + for (PoolLimitDeterminationStrategy strategy : strategies) { + int availablePermits = strategy.getAvailablePermits(); + minPermits = Math.min(minPermits, availablePermits); + } + return minPermits; // If will atleast be one strategy (invariant in constructor) and hence this should be the value provided by that strategy. + } + + @Override + public void releasePermit() { + for (PoolLimitDeterminationStrategy strategy : strategies) { + strategy.releasePermit(); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolder.java new file mode 100644 index 0000000..434a580 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; + +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * An implementation of {@link IdleConnectionsHolder} with a FIFO strategy. + * + * @param Type of object that is written to the client using this holder. + * @param Type of object that is read from the the client using this holder. + */ +public class FIFOIdleConnectionsHolder extends IdleConnectionsHolder { + + private final ConcurrentLinkedQueue> idleConnections; + private final Observable> pollObservable; + private final Observable> peekObservable; + + public FIFOIdleConnectionsHolder() { + idleConnections = new ConcurrentLinkedQueue<>(); + + pollObservable = Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + PooledConnection idleConnection; + while (!subscriber.isUnsubscribed() && (idleConnection = idleConnections.poll()) != null) { + subscriber.onNext(idleConnection); + } + if (!subscriber.isUnsubscribed()) { + subscriber.onCompleted(); + } + } + }); + + peekObservable = Observable.from(idleConnections); + } + + @Override + public Observable> poll() { + return pollObservable; + } + + @Override + public Observable> peek() { + return peekObservable; + } + + @Override + public void add(PooledConnection toAdd) { + idleConnections.add(toAdd); + } + + @Override + public boolean remove(PooledConnection toRemove) { + return idleConnections.remove(toRemove); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/IdleConnectionsHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/IdleConnectionsHolder.java new file mode 100644 index 0000000..35dda3e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/IdleConnectionsHolder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.netty.channel.EventLoop; +import rx.Observable; + +/** + * A holder of idle {@link PooledConnection} used by {@link PooledConnectionProvider} + * + * @param Type of object that is written to the client using this holder. + * @param Type of object that is read from the the client using this holder. + */ +public abstract class IdleConnectionsHolder { + + /** + * Creates a stream of idle connections where every item sent on to the stream is removed from the underlying + * idle connections pool. + * + * @return A stream of idle connections. + */ + public abstract Observable> poll(); + + /** + * Creates a stream of idle connections where every item sent on to the stream is removed from the underlying + * idle connections pool. + * This method will only poll connections if the calling thread is an {@link EventLoop} known to this holder. + * Otherwise, it should return an empty stream. + * + * @return A stream of idle connections. + */ + public Observable> pollThisEventLoopConnections() { + return poll(); /*Override if the holder is aware of eventloops*/ + } + + /** + * Creates a stream of idle connections where every item sent on to the stream is NOT removed from the + * underlying idle connections pool. If the connection is to be removed, {@link #remove(PooledConnection)} must + * be called for that connection. + * + * @return A stream of idle connections. + */ + public abstract Observable> peek(); + + /** + * Adds the passed connection to this holder. + * + * @param toAdd Connection to add. + */ + public abstract void add(PooledConnection toAdd); + + /** + * Removes the passed connection from this holder. + * + * @param toRemove Connection to remove. + */ + public abstract boolean remove(PooledConnection toRemove); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/MaxConnectionsBasedStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/MaxConnectionsBasedStrategy.java new file mode 100644 index 0000000..18ab9a0 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/MaxConnectionsBasedStrategy.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.pool; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link PoolLimitDeterminationStrategy} that limits the pool based on a maximum connections limit. + * This limit can be increased or decreased at runtime. + */ +public class MaxConnectionsBasedStrategy implements PoolLimitDeterminationStrategy { + + public static final int DEFAULT_MAX_CONNECTIONS = 1000; + + private final AtomicInteger limitEnforcer; + private final AtomicInteger maxConnections; + + public MaxConnectionsBasedStrategy() { + this(DEFAULT_MAX_CONNECTIONS); + } + + public MaxConnectionsBasedStrategy(int maxConnections) { + this.maxConnections = new AtomicInteger(maxConnections); + limitEnforcer = new AtomicInteger(); + } + + @Override + public boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit) { + /** + * As opposed to limitEnforcer.incrementAndGet() we follow this model as this does not change the limitEnforcer + * value unless there are enough permits. + * If we were to use incrementAndGet(), in case of overflow (from max allowed limit) we would have to decrement + * the limitEnforcer. This may show temporary overflows in getMaxConnections() which may be disturbing for a + * user. However, even if we use incrementAndGet() the counter corrects itself over time. + * This is just a more semantically correct implementation with similar performance characterstics as + * incrementAndGet() + */ + for (;;) { + final int currentValue = limitEnforcer.get(); + final int newValue = currentValue + 1; + final int maxAllowedConnections = maxConnections.get(); + if (newValue <= maxAllowedConnections) { + if (limitEnforcer.compareAndSet(currentValue, newValue)) { + return true; + } + } else { + return false; + } + } + } + + public int incrementMaxConnections(int incrementBy) { + return maxConnections.addAndGet(incrementBy); + } + + public int decrementMaxConnections(int decrementBy) { + return maxConnections.addAndGet(-1 * decrementBy); + } + + public int getMaxConnections() { + return maxConnections.get(); + } + + @Override + public int getAvailablePermits() { + return maxConnections.get() - limitEnforcer.get(); + } + + @Override + public void releasePermit() { + /** + * As opposed to limitEnforcer.decrementAndGet() we follow this model as this does not change the limitEnforcer + * value unless there are enough permits. + * If we were to use decrementAndGet(), in case of overflow (from max allowed limit) we would have to decrement + * the limitEnforcer. This may show temporary overflows in getMaxConnections() which may be disturbing for a + * user. However, even if we use decrementAndGet() the counter corrects itself over time. + * This is just a more semantically correct implementation with similar performance characterstics as + * decrementAndGet() + */ + for (;;) { + final int currentValue = limitEnforcer.get(); + final int newValue = currentValue - 1; + if (newValue >= 0) { + if (!limitEnforcer.compareAndSet(currentValue, newValue)) { + continue; + } + } + break; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolConfig.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolConfig.java new file mode 100644 index 0000000..926748c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolConfig.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import rx.Observable; + +import java.util.concurrent.TimeUnit; + +/** + * A configuration for connection pooling for a client. + * + * @param Type of object that is written to the client using this pool config. + * @param Type of object that is read from the the client using this pool config. + */ +public class PoolConfig { + + public static final long DEFAULT_MAX_IDLE_TIME_MILLIS = TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS); + + private Observable idleConnCleanupTicker; + private PoolLimitDeterminationStrategy limitDeterminationStrategy; + private IdleConnectionsHolder idleConnectionsHolder; + private long maxIdleTimeMillis; + + public PoolConfig() { + maxIdleTimeMillis = DEFAULT_MAX_IDLE_TIME_MILLIS; + idleConnCleanupTicker = Observable.interval(maxIdleTimeMillis, maxIdleTimeMillis, TimeUnit.MILLISECONDS); + idleConnectionsHolder = new FIFOIdleConnectionsHolder<>(); + limitDeterminationStrategy = UnboundedPoolLimitDeterminationStrategy.INSTANCE; + } + + public long getMaxIdleTimeMillis() { + return maxIdleTimeMillis; + } + + public Observable getIdleConnectionsCleanupTimer() { + return idleConnCleanupTicker; + } + + public PoolLimitDeterminationStrategy getPoolLimitDeterminationStrategy() { + return limitDeterminationStrategy; + } + + public PoolConfig maxConnections(int maxConnections) { + limitDeterminationStrategy = new MaxConnectionsBasedStrategy(maxConnections); + return this; + } + + public PoolConfig maxIdleTimeoutMillis(long maxIdleTimeoutMillis) { + maxIdleTimeMillis = maxIdleTimeoutMillis; + return this; + } + + public PoolConfig limitDeterminationStrategy(PoolLimitDeterminationStrategy strategy) { + limitDeterminationStrategy = strategy; + return this; + } + + public PoolLimitDeterminationStrategy getLimitDeterminationStrategy() { + return limitDeterminationStrategy; + } + + public PoolConfig idleConnectionsHolder(IdleConnectionsHolder holder) { + idleConnectionsHolder = holder; + return this; + } + + public IdleConnectionsHolder getIdleConnectionsHolder() { + return idleConnectionsHolder; + } + + public PoolConfig idleConnectionsCleanupTimer(Observable timer) { + idleConnCleanupTicker = timer; + return this; + } + + public Observable getIdleConnCleanupTicker() { + return idleConnCleanupTicker; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolExhaustedException.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolExhaustedException.java new file mode 100644 index 0000000..b0abdb7 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolExhaustedException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +public class PoolExhaustedException extends Exception { + + private static final long serialVersionUID = -6299997509113653123L; + + public PoolExhaustedException() { + } + + public PoolExhaustedException(Throwable cause) { + super(cause); + } + + public PoolExhaustedException(String message) { + super(message); + } + + public PoolExhaustedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolLimitDeterminationStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolLimitDeterminationStrategy.java new file mode 100644 index 0000000..dfeaa64 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PoolLimitDeterminationStrategy.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.pool; + +import java.util.concurrent.TimeUnit; + +/** + * A strategy to delegate the decision pertaining to connection pool size limits. + */ +public interface PoolLimitDeterminationStrategy { + + /** + * Attempts to acquire a creation permit. + * + * @param acquireStartTime The start time for the acquire process in milliseconds since epoch. + * @param timeUnit The timeunit for the acquire start time. + * + * @return {@code true} if the permit was acquired, {@code false} otherwise. + */ + boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit); + + /** + * Returns the number of creation permits available. + * + * @return The number of creation permits available. + */ + int getAvailablePermits(); + + /** + * Release a previously acquired permit. + */ + void releasePermit(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnection.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnection.java new file mode 100644 index 0000000..020ce68 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnection.java @@ -0,0 +1,373 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.FileRegion; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ClientConnectionToChannelBridge; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observable.Transformer; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Actions; +import rx.functions.Func1; + +/** + * An implementation of {@link Connection} which is pooled and reused. + * + * It is required to call {@link #reuse(Subscriber)} for reusing this connection. + * + * @param Type of object that is read from this connection. + * @param Type of object that is written to this connection. + */ +public class PooledConnection extends Connection { + + private static final Logger logger = Logger.getLogger(PooledConnection.class.getName()); + + public static final AttributeKey DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS = + AttributeKey.valueOf("rxnetty_conn_keep_alive_timeout_millis"); + + private final Owner owner; + private final Connection unpooledDelegate; + + private volatile long lastReturnToPoolTimeMillis; + private volatile boolean releasedAtLeastOnce; + private volatile long maxIdleTimeMillis; + private final Observable releaseObservable; + + private PooledConnection(Owner owner, long maxIdleTimeMillis, Connection unpooledDelegate) { + super(unpooledDelegate); + if (null == owner) { + throw new IllegalArgumentException("Pooled connection owner can not be null"); + } + if (null == unpooledDelegate) { + throw new IllegalArgumentException("Connection delegate can not be null"); + } + + this.owner = owner; + this.unpooledDelegate = unpooledDelegate; + this.maxIdleTimeMillis = maxIdleTimeMillis; + lastReturnToPoolTimeMillis = System.currentTimeMillis(); + releaseObservable = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + if (!isUsable()) { + PooledConnection.this.owner.discard(PooledConnection.this) + .unsafeSubscribe(subscriber); + } else { + Long keepAliveTimeout = unsafeNettyChannel().attr(DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS).get(); + if (null != keepAliveTimeout) { + PooledConnection.this.maxIdleTimeMillis = keepAliveTimeout; + } + markAwarePipeline.reset(); // Reset pipeline state, if changed, on release. + PooledConnection.this.owner.release(PooledConnection.this) + .doOnCompleted(new Action0() { + @Override + public void call() { + releasedAtLeastOnce = true; + lastReturnToPoolTimeMillis = System.currentTimeMillis(); + } + }) + .unsafeSubscribe(subscriber); + } + } + }).onErrorResumeNext(discard()); + } + + private PooledConnection(PooledConnection toCopy, Connection unpooledDelegate) { + super(unpooledDelegate); + owner = toCopy.owner; + this.unpooledDelegate = unpooledDelegate; + lastReturnToPoolTimeMillis = toCopy.lastReturnToPoolTimeMillis; + releasedAtLeastOnce = toCopy.releasedAtLeastOnce; + maxIdleTimeMillis = toCopy.maxIdleTimeMillis; + releaseObservable = toCopy.releaseObservable; + } + + @Override + public Observable write(Observable msgs) { + return unpooledDelegate.write(msgs); + } + + @Override + public Observable write(Observable msgs, Func1 flushSelector) { + return unpooledDelegate.write(msgs, flushSelector); + } + + @Override + public Observable writeAndFlushOnEach(Observable msgs) { + return unpooledDelegate.writeAndFlushOnEach(msgs); + } + + @Override + public Observable writeString(Observable msgs) { + return unpooledDelegate.writeString(msgs); + } + + @Override + public Observable writeString(Observable msgs, + Func1 flushSelector) { + return unpooledDelegate.writeString(msgs, flushSelector); + } + + @Override + public Observable writeStringAndFlushOnEach(Observable msgs) { + return unpooledDelegate.writeStringAndFlushOnEach(msgs); + } + + @Override + public Observable writeBytes(Observable msgs) { + return unpooledDelegate.writeBytes(msgs); + } + + @Override + public Observable writeBytes(Observable msgs, + Func1 flushSelector) { + return unpooledDelegate.writeBytes(msgs, flushSelector); + } + + @Override + public Observable writeBytesAndFlushOnEach(Observable msgs) { + return unpooledDelegate.writeBytesAndFlushOnEach(msgs); + } + + @Override + public Observable writeFileRegion(Observable msgs) { + return unpooledDelegate.writeFileRegion(msgs); + } + + @Override + public Observable writeFileRegion(Observable msgs, + Func1 flushSelector) { + return unpooledDelegate.writeFileRegion(msgs, flushSelector); + } + + @Override + public Observable writeFileRegionAndFlushOnEach(Observable msgs) { + return unpooledDelegate.writeFileRegionAndFlushOnEach(msgs); + } + + @Override + public void flush() { + unpooledDelegate.flush(); + } + + @Override + public Observable close() { + return close(true); + } + + @Override + public Observable close(boolean flush) { + if (flush) { + return releaseObservable.doOnSubscribe(new Action0() { + @Override + public void call() { + unpooledDelegate.flush(); + } + }); + } else { + return releaseObservable; + } + } + + @Override + public void closeNow() { + close().subscribe(Actions.empty(), new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Error closing connection.", throwable); + } + }); + } + + @Override + public Observable closeListener() { + return unpooledDelegate.closeListener(); + } + + @Override + public Connection addChannelHandlerAfter(String baseName, String name, + ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerAfter(baseName, name, handler)); + } + + @Override + public Connection addChannelHandlerAfter(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerAfter(group, baseName, name, + handler)); + } + + @Override + public Connection addChannelHandlerBefore(String baseName, String name, + ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerBefore(baseName, name, handler)); + } + + @Override + public Connection addChannelHandlerBefore(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerBefore(group, baseName, name, + handler)); + } + + @Override + public Connection addChannelHandlerFirst(EventExecutorGroup group, + String name, ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerFirst(group, name, handler)); + } + + @Override + public Connection addChannelHandlerFirst(String name, ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerFirst(name, handler)); + } + + @Override + public Connection addChannelHandlerLast(EventExecutorGroup group, + String name, ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerLast(group, name, handler)); + } + + @Override + public Connection addChannelHandlerLast(String name, ChannelHandler handler) { + return new PooledConnection<>(this, unpooledDelegate.addChannelHandlerLast(name, handler)); + } + + @Override + public Connection pipelineConfigurator(Action1 pipelineConfigurator) { + return new PooledConnection<>(this, unpooledDelegate.pipelineConfigurator(pipelineConfigurator)); + } + + @Override + public Connection transformRead(Transformer transformer) { + return new PooledConnection<>(this, unpooledDelegate.transformRead(transformer)); + } + + @Override + public Connection transformWrite(AllocatingTransformer transformer) { + return new PooledConnection<>(this, unpooledDelegate.transformWrite(transformer)); + } + + /** + * Discards this connection, to be called when this connection will never be used again. + * + * @return {@link Observable} representing the result of the discard, this will typically be resulting in a close + * on the underlying {@link Connection}. + */ + /*package private, externally shouldn't be discardable.*/Observable discard() { + return unpooledDelegate.close(); + } + + /** + * Returns whether this connection is safe to be used at this moment. + * This makes sure that the underlying netty's channel is active as returned by + * {@link Channel#isActive()} and it has not passed the maximum idle time in the pool. + * + * @return {@code true} if the connection is usable. + */ + public boolean isUsable() { + final Channel nettyChannel = unsafeNettyChannel(); + Boolean discardConn = nettyChannel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).get(); + + if (!nettyChannel.isActive() || Boolean.TRUE == discardConn) { + return false; + } + + long nowMillis = System.currentTimeMillis(); + long idleTime = nowMillis - lastReturnToPoolTimeMillis; + return idleTime < maxIdleTimeMillis; + } + + /** + * This method must be called for reusing the connection i.e. for sending this connection to the passed subscriber. + * + * @param connectionSubscriber Subscriber for the pooled connection for reuse. + */ + public void reuse(Subscriber> connectionSubscriber) { + unsafeNettyChannel().pipeline().fireUserEventTriggered(new ConnectionReuseEvent<>(connectionSubscriber, this)); + } + + public static PooledConnection create(Owner owner, long maxIdleTimeMillis, + Connection unpooledDelegate) { + final PooledConnection toReturn = new PooledConnection<>(owner, maxIdleTimeMillis, unpooledDelegate + ); + toReturn.connectCloseToChannelClose(); + return toReturn; + } + + /** + * Returns {@code true} if this connection is reused at least once. + * + * @return {@code true} if this connection is reused at least once. + */ + public boolean isReused() { + return releasedAtLeastOnce; + } + + @Override + public ChannelPipeline getChannelPipeline() { + return markAwarePipeline; // Always return mark aware as, we always have to reset state on release to pool. + } + + /*Visible for testin*/ void setLastReturnToPoolTimeMillis(long lastReturnToPoolTimeMillis) { + this.lastReturnToPoolTimeMillis = lastReturnToPoolTimeMillis; + } + + /** + * A contract for the owner of the {@link PooledConnection} to which any instance of {@link PooledConnection} must + * be returned after use. + */ + public interface Owner { + + /** + * Releases the passed connection back to the owner, for reuse. + * + * @param connection Connection to be released. + * + * @return {@link Observable} representing result of the release. Every subscription to this, releases the + * connection. + */ + Observable release(PooledConnection connection); + + /** + * Discards the passed connection from the pool. This is usually called due to an external event like closing of + * a connection that the pool may not know. + * This operation is idempotent and hence can be called multiple times with no side effects + * + * @param connection The connection to discard. + * + * @return {@link Observable} indicating the result of the discard (which usually results in a close()). + * Every subscription to this {@link Observable} will discard the connection. + */ + Observable discard(PooledConnection connection); + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProvider.java new file mode 100644 index 0000000..bc3bd28 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.pool.PooledConnection.Owner; + +/** + * An implementation of {@link PooledConnectionProvider} that pools connections. + * + * Following are the key parameters: + * + *
    +
  • {@link PoolLimitDeterminationStrategy}: A strategy to determine whether a new physical connection should be + created as part of the user request.
  • +
  • {@link PoolConfig#getIdleConnectionsCleanupTimer()}: The schedule for cleaning up idle connections in the pool.
  • +
  • {@link PoolConfig#getMaxIdleTimeMillis()}: Maximum time a connection can be idle in this pool.
  • +
+ * + *

Usage

+ * + *

Complementing a {@link ConnectionProviderFactory}

+ * + * For employing better host selection strategies, this provider can be used to complement the default + * {@link ConnectionProvider} provided by a {@link HostConnector}. + * + *

Standalone

+ * + * For clients that do not use a pool of hosts can use {@link SingleHostPoolingProviderFactory} that will only ever pick + * a single host but will pool connections. + */ +public abstract class PooledConnectionProvider implements ConnectionProvider , Owner { + + public static PooledConnectionProvider createUnbounded(final HostConnector delegate) { + return create(new PoolConfig(), delegate); + } + + public static PooledConnectionProvider createBounded(int maxConnections, + final HostConnector delegate) { + return create(new PoolConfig().maxConnections(maxConnections), delegate); + } + + public static PooledConnectionProvider create(final PoolConfig config, + final HostConnector delegate) { + return new PooledConnectionProviderImpl<>(config, delegate); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProviderImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProviderImpl.java new file mode 100644 index 0000000..3b02e25 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PooledConnectionProviderImpl.java @@ -0,0 +1,431 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.PooledConnectionReleaseEvent; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventPublisher; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Actions; +import rx.functions.Func1; + +import static io.reactivex.netty.events.EventAttributeKeys.*; +import static java.util.concurrent.TimeUnit.*; + +/** + * An implementation of {@link PooledConnectionProvider} that pools connections. + * + * Following are the key parameters: + * + *
    +
  • {@link PoolLimitDeterminationStrategy}: A strategy to determine whether a new physical connection should be + created as part of the user request.
  • +
  • {@link PoolConfig#getIdleConnectionsCleanupTimer()}: The schedule for cleaning up idle connections in the pool.
  • +
  • {@link PoolConfig#getMaxIdleTimeMillis()}: Maximum time a connection can be idle in this pool.
  • +
+ * + * @param Type of object that is written to the client using this factory. + * @param Type of object that is read from the the client using this factory. + */ +public final class PooledConnectionProviderImpl extends PooledConnectionProvider { + + private static final Logger logger = Logger.getLogger(PooledConnectionProviderImpl.class.getName()); + + private final Subscription idleConnCleanupSubscription; + private final IdleConnectionsHolder idleConnectionsHolder; + + private final PoolLimitDeterminationStrategy limitDeterminationStrategy; + private final long maxIdleTimeMillis; + private final HostConnector hostConnector; + private volatile boolean isShutdown; + + public PooledConnectionProviderImpl(PoolConfig poolConfig, HostConnector hostConnector) { + this.hostConnector = hostConnector; + idleConnectionsHolder = poolConfig.getIdleConnectionsHolder(); + limitDeterminationStrategy = poolConfig.getPoolLimitDeterminationStrategy(); + maxIdleTimeMillis = poolConfig.getMaxIdleTimeMillis(); + // In case, there is no cleanup required, this observable should never give a tick. + idleConnCleanupSubscription = poolConfig.getIdleConnCleanupTicker() + .doOnError(LogErrorAction.INSTANCE) + .retry() // Retry when there is an error in timer. + .concatMap(new IdleConnectionCleanupTask()) + .doOnError(new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Ignoring error cleaning up idle connections.", + throwable); + } + }) + .retry() + .subscribe(); + + hostConnector.getHost() + .getCloseNotifier() + .doOnTerminate(new Action0() { + @Override + public void call() { + isShutdown = true; + idleConnCleanupSubscription.unsubscribe(); + } + }) + .onErrorResumeNext(new Func1>() { + @Override + public Observable call(Throwable throwable) { + logger.log(Level.SEVERE, "Error listening to Host close notifications. Shutting down the pool.", + throwable); + return Observable.empty(); + } + }) + .subscribe(Actions.empty()); + } + + @Override + public Observable> newConnectionRequest() { + return Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + if (isShutdown) { + subscriber.onError(new IllegalStateException("Connection provider is shutdown.")); + } + idleConnectionsHolder.pollThisEventLoopConnections() + .concatWith(connectIfAllowed()) + .filter(new Func1, Boolean>() { + @Override + public Boolean call(PooledConnection c) { + boolean isUsable = c.isUsable(); + if (!isUsable) { + discardNow(c); + } + return isUsable; + } + }) + .take(1) + .lift(new ReuseSubscriberLinker()) + .lift(new ConnectMetricsOperator()) + .unsafeSubscribe(subscriber); + } + }); + } + + @Override + public Observable release(final PooledConnection connection) { + @SuppressWarnings("unchecked") + final PooledConnection c = (PooledConnection) connection; + return Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + if (null == c) { + subscriber.onCompleted(); + } else { + /** + * Executing the release on the eventloop to avoid race-conditions between code cleaning up + * connection in the pipeline and the connecting being released to the pool. + */ + c.unsafeNettyChannel() + .eventLoop() + .submit(new ReleaseTask(c, subscriber)); + } + } + }); + } + + @Override + public Observable discard(final PooledConnection connection) { + return connection.discard().doOnSubscribe(new Action0() { + @Override + public void call() { + EventPublisher eventPublisher = connection.unsafeNettyChannel().attr(EVENT_PUBLISHER).get(); + if (eventPublisher.publishingEnabled()) { + ClientEventListener eventListener = connection.unsafeNettyChannel() + .attr(CLIENT_EVENT_LISTENER).get(); + eventListener.onPooledConnectionEviction(); + } + limitDeterminationStrategy.releasePermit();/*Since, an idle connection took a permit*/ + } + }); + } + + private Observable> connectIfAllowed() { + return Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + final long startTimeNanos = Clock.newStartTimeNanos(); + if (limitDeterminationStrategy.acquireCreationPermit(startTimeNanos, NANOSECONDS)) { + Observable> newConnObsv = hostConnector.getConnectionProvider() + .newConnectionRequest(); + newConnObsv.map(new Func1, PooledConnection>() { + @Override + public PooledConnection call(Connection connection) { + return PooledConnection.create(PooledConnectionProviderImpl.this, + maxIdleTimeMillis, connection); + } + }).doOnError(new Action1() { + @Override + public void call(Throwable throwable) { + limitDeterminationStrategy.releasePermit(); /*Before connect we acquired.*/ + } + }).unsafeSubscribe(subscriber); + } else { + idleConnectionsHolder.poll() + .switchIfEmpty(Observable.>error( + new PoolExhaustedException("Client connection pool exhausted."))) + .unsafeSubscribe(subscriber); + } + } + }); + } + + private void discardNow(PooledConnection toDiscard) { + discard(toDiscard).subscribe(Actions.empty(), new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Error discarding connection.", throwable); + } + }); + } + + private static class LogErrorAction implements Action1 { + + public static final LogErrorAction INSTANCE = new LogErrorAction(); + + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Error from idle connection cleanup timer. This will be retried.", throwable); + } + } + + private class IdleConnectionCleanupTask implements Func1> { + @Override + public Observable call(Long aLong) { + return idleConnectionsHolder.peek() + .map((Func1, Void>) connection -> { + if (!connection.isUsable()) { + idleConnectionsHolder.remove(connection); + discardNow(connection); + } + return null; + }).ignoreElements(); + } + } + + private class ReleaseTask implements Runnable { + + private final PooledConnection connection; + private final Subscriber subscriber; + private final long releaseStartTimeNanos; + private final EventPublisher eventPublisher; + private final ClientEventListener eventListener; + + private ReleaseTask(PooledConnection connection, Subscriber subscriber) { + this.connection = connection; + this.subscriber = subscriber; + releaseStartTimeNanos = Clock.newStartTimeNanos(); + eventPublisher = connection.unsafeNettyChannel().attr(EVENT_PUBLISHER).get(); + eventListener = connection.unsafeNettyChannel().attr(CLIENT_EVENT_LISTENER).get(); + } + + @Override + public void run() { + try { + connection.unsafeNettyChannel().pipeline().fireUserEventTriggered(PooledConnectionReleaseEvent.INSTANCE); + if (eventPublisher.publishingEnabled()) { + eventListener.onPoolReleaseStart(); + } + if (isShutdown || !connection.isUsable()) { + discardNow(connection); + } else { + idleConnectionsHolder.add(connection); + } + + if (eventPublisher.publishingEnabled()) { + eventListener.onPoolReleaseSuccess(Clock.onEndNanos(releaseStartTimeNanos), NANOSECONDS); + } + subscriber.onCompleted(); + } catch (Throwable throwable) { + if (eventPublisher.publishingEnabled()) { + eventListener.onPoolReleaseFailed(Clock.onEndNanos(releaseStartTimeNanos), NANOSECONDS, throwable); + } + subscriber.onError(throwable); + } + } + } + + private class ConnectMetricsOperator implements Operator, PooledConnection> { + + @Override + public Subscriber> call(final Subscriber> o) { + final long startTimeNanos = Clock.newStartTimeNanos(); + + return new Subscriber<>(o) { + + private volatile boolean publishingEnabled; + private volatile ClientEventListener eventListener; + + @Override + public void onCompleted() { + if (publishingEnabled) { + eventListener.onPoolAcquireStart(); + eventListener.onPoolAcquireSuccess(Clock.onEndNanos(startTimeNanos), NANOSECONDS); + } + o.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (publishingEnabled) { + /*Error means no connection was received, as it always every gets at most one connection*/ + eventListener.onPoolAcquireStart(); + eventListener.onPoolAcquireFailed(Clock.onEndNanos(startTimeNanos), NANOSECONDS, e); + } + o.onError(e); + } + + @Override + public void onNext(PooledConnection c) { + EventPublisher eventPublisher = c.unsafeNettyChannel().attr(EVENT_PUBLISHER).get(); + if (eventPublisher.publishingEnabled()) { + publishingEnabled = true; + eventListener = c.unsafeNettyChannel().attr(CLIENT_EVENT_LISTENER).get(); + } + o.onNext(c); + } + }; + } + } + + private class ReuseSubscriberLinker implements Operator, PooledConnection> { + + private ScalarAsyncSubscriber onReuseSubscriber; + + @Override + public Subscriber> call(final Subscriber> o) { + return new Subscriber<>(o) { + + @Override + public void onCompleted() { + /*This subscriber is not invoked by different threads, so don't need sychronization*/ + if (null != onReuseSubscriber) { + onReuseSubscriber.onCompleted(); + } else { + o.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + /*This subscriber is not invoked by different threads, so don't need sychronization*/ + if (null != onReuseSubscriber) { + onReuseSubscriber.onError(e); + } else { + o.onError(e); + } + } + + @Override + public void onNext(PooledConnection c) { + if (c.isReused()) { + EventPublisher eventPublisher = c.unsafeNettyChannel().attr(EVENT_PUBLISHER).get(); + if (eventPublisher.publishingEnabled()) { + ClientEventListener eventListener = c.unsafeNettyChannel() + .attr(CLIENT_EVENT_LISTENER).get(); + eventListener.onPooledConnectionReuse(); + } + onReuseSubscriber = new ScalarAsyncSubscriber<>(o); + c.reuse(onReuseSubscriber); /*Reuse will on next to the subscriber*/ + } else { + o.onNext(c); + } + } + }; + } + + } + + private static class ScalarAsyncSubscriber extends Subscriber> { + + private boolean terminated; /*Guarded by this*/ + private Throwable error; /*Guarded by this*/ + private boolean onNextArrived; /*Guarded by this*/ + private final Subscriber> delegate; + + private ScalarAsyncSubscriber(Subscriber> delegate) { + this.delegate = delegate; + } + + @Override + public void onCompleted() { + boolean _onNextArrived; + + synchronized (this) { + _onNextArrived = onNextArrived; + } + + terminated = true; + + if (_onNextArrived) { + delegate.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + boolean _onNextArrived; + + synchronized (this) { + _onNextArrived = onNextArrived; + } + terminated = true; + error = e; + + if (_onNextArrived) { + delegate.onError(e); + } + } + + @Override + public void onNext(PooledConnection conn) { + boolean _terminated; + Throwable _error; + synchronized (this) { + onNextArrived = true; + _terminated = terminated; + _error = error; + delegate.onNext(conn); + } + + + if (_terminated) { + if (null != error) { + delegate.onError(_error); + } else { + delegate.onCompleted(); + } + } + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolder.java new file mode 100644 index 0000000..b175334 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolder.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.FastThreadLocal; +import io.reactivex.netty.client.ClientConnectionToChannelBridge; +import io.reactivex.netty.threads.PreferCurrentEventLoopGroup; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Actions; +import rx.functions.Func0; + +import java.util.ArrayList; + +/** + * An {@link IdleConnectionsHolder} implementation that can identify if the calling thread is an {@link EventLoop} in + * the provided {@link PreferCurrentEventLoopGroup} and prefers a connection registered with the calling + * {@link EventLoop}. + * + * If the calling thread is not an {@link EventLoop} in the provided {@link PreferCurrentEventLoopGroup} then + * {@link #poll()} and {@link #peek()} will iterate over connections from all {@link EventLoop}s however + * {@link #add(PooledConnection)} will attempt to find the {@link EventLoop} of the added {@link PooledConnection}. If + * the {@link EventLoop} of the connection does not belong to the provided {@link PreferCurrentEventLoopGroup} then the + * connection will be discarded. + * + * @param Type of object that is written to the client using this holder. + * @param Type of object that is read from the the client using this holder. + */ +public class PreferCurrentEventLoopHolder extends IdleConnectionsHolder { + + private static final Logger logger = Logger.getLogger(PreferCurrentEventLoopHolder.class.getName()); + + private final FastThreadLocal> perElHolder = new FastThreadLocal<>(); + private final ArrayList> allElHolders; + private final Observable> pollObservable; + private final Observable> peekObservable; + + PreferCurrentEventLoopHolder(PreferCurrentEventLoopGroup eventLoopGroup) { + this(eventLoopGroup, new FIFOIdleConnectionsHolderFactory()); + } + + PreferCurrentEventLoopHolder(PreferCurrentEventLoopGroup eventLoopGroup, + final IdleConnectionsHolderFactory holderFactory) { + final ArrayList> _allElHolders = new ArrayList<>(); + allElHolders = _allElHolders; + for (final EventExecutor child : eventLoopGroup) { + final IdleConnectionsHolder newHolder = holderFactory.call(); + allElHolders.add(newHolder); + child.submit(new Runnable() { + @Override + public void run() { + perElHolder.set(newHolder); + } + }); + } + + Observable> pollOverAllHolders = Observable.empty(); + Observable> peekOverAllHolders = Observable.empty(); + + for (IdleConnectionsHolder anElHolder : allElHolders) { + pollOverAllHolders = pollOverAllHolders.concatWith(anElHolder.poll()); + peekOverAllHolders = peekOverAllHolders.concatWith(anElHolder.peek()); + } + + pollObservable = pollOverAllHolders; + peekObservable = peekOverAllHolders; + } + + @Override + public Observable> poll() { + return pollObservable; + } + + @Override + public Observable> pollThisEventLoopConnections() { + + return Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + final IdleConnectionsHolder holderForThisEL = perElHolder.get(); + if (null == holderForThisEL) { + /*Caller is not an eventloop*/ + PreferCurrentEventLoopHolder.super.pollThisEventLoopConnections().unsafeSubscribe(subscriber); + } else { + holderForThisEL.poll().unsafeSubscribe(subscriber); + } + } + }); + } + + @Override + public Observable> peek() { + return peekObservable; + } + + @Override + public void add(final PooledConnection toAdd) { + final IdleConnectionsHolder holderForThisEL = perElHolder.get(); + if (null != holderForThisEL) { + holderForThisEL.add(toAdd); + } else { + /* + * This should not happen as the code generally adds the connection from within an eventloop. + * By executing the add on the eventloop, the owner eventloop is correctly discovered for this eventloop. + */ + toAdd.unsafeNettyChannel().eventLoop().execute(new Runnable() { + @Override + public void run() { + IdleConnectionsHolder holderForThisEl = perElHolder.get(); + if (null == holderForThisEl) { + logger.log(Level.SEVERE, "Unrecognized eventloop: " + Thread.currentThread().getName() + + ". Returned connection can not be added to the pool. Closing the connection."); + toAdd.unsafeNettyChannel().attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); + toAdd.close().subscribe(Actions.empty(), new Action1() { + @Override + public void call(Throwable throwable) { + logger.log(Level.SEVERE, "Failed to discard connection.", throwable); + } + }); + } else { + holderForThisEl.add(toAdd); + } + } + }); + } + } + + @Override + public boolean remove(PooledConnection toRemove) { + for (IdleConnectionsHolder anElHolder : allElHolders) { + if (anElHolder.remove(toRemove)) { + return true; + } + } + return false; + } + + public interface IdleConnectionsHolderFactory extends Func0> { + } + + private static class FIFOIdleConnectionsHolderFactory implements IdleConnectionsHolderFactory { + + @Override + public IdleConnectionsHolder call() { + return new FIFOIdleConnectionsHolder<>(); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/SingleHostPoolingProviderFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/SingleHostPoolingProviderFactory.java new file mode 100644 index 0000000..3d8d27b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/SingleHostPoolingProviderFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.pool; + +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.internal.SingleHostConnectionProvider; +import rx.Observable; +import rx.functions.Func1; + +/** + * A {@link ConnectionProviderFactory} that must only be used for a client that operates on a single host. + */ +public class SingleHostPoolingProviderFactory implements ConnectionProviderFactory { + + private final PoolConfig config; + + private SingleHostPoolingProviderFactory(PoolConfig config) { + this.config = config; + } + + @Override + public ConnectionProvider newProvider(Observable> hosts) { + return new SingleHostConnectionProvider<>(hosts.map(new Func1, HostConnector>() { + @Override + public HostConnector call(HostConnector hc) { + return new HostConnector<>(hc, PooledConnectionProvider.create(config, hc)); + } + })); + } + + public static SingleHostPoolingProviderFactory createUnbounded() { + return create(new PoolConfig()); + } + + public static SingleHostPoolingProviderFactory createBounded(int maxConnections) { + return create(new PoolConfig().maxConnections(maxConnections)); + } + + public static SingleHostPoolingProviderFactory create(final PoolConfig config) { + return new SingleHostPoolingProviderFactory<>(config); + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/UnboundedPoolLimitDeterminationStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/UnboundedPoolLimitDeterminationStrategy.java new file mode 100644 index 0000000..0b9bfec --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/client/pool/UnboundedPoolLimitDeterminationStrategy.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import java.util.concurrent.TimeUnit; + +public class UnboundedPoolLimitDeterminationStrategy implements PoolLimitDeterminationStrategy { + + public static final PoolLimitDeterminationStrategy INSTANCE = new UnboundedPoolLimitDeterminationStrategy(); + + private UnboundedPoolLimitDeterminationStrategy() { } + + @Override + public boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit) { + return true; + } + + @Override + public int getAvailablePermits() { + return Integer.MAX_VALUE; + } + + @Override + public void releasePermit() { + // No Op, no limit. + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/Clock.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/Clock.java new file mode 100644 index 0000000..4c29fc0 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/Clock.java @@ -0,0 +1,140 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.events; + +import io.reactivex.netty.RxNetty; + +import java.util.concurrent.TimeUnit; + +/** + * A simple utility to wrap start and end times of a call. + * + *

Thread Safety

+ * + * This class is NOT threadsafe. + *

Memory overhead

+ * + * One of the major concerns in publishing events is the object allocation overhead and having a Clock instance + * can attribute to such overheads. This is the reason why this class also provides static convenience methods to mark + * start and end of times to reduce some boiler plate code. + */ +public class Clock { + + /** + * The value returned by all static methods in this class, viz., + *
    +
  • {@link #newStartTime(TimeUnit)}
  • +
  • {@link #newStartTimeNanos()}
  • +
  • {@link #onEndNanos(long)}
  • +
  • {@link #onEnd(long, TimeUnit)}
  • +
+ * after calling {@link RxNetty#disableEventPublishing()} + */ + public static final long SYSTEM_TIME_DISABLED_TIME = -1; + + private final long startTimeNanos = System.nanoTime(); + private long endTimeNanos = -1; + private long durationNanos = -1; + + /** + * Stops this clock. This method is idempotent, so, after invoking this method, the duration of the clock is + * immutable. Hence, you can call this method multiple times with no side-effects. + * + * @return The duration in nanoseconds for which the clock was running. + */ + public long stop() { + if (-1 != endTimeNanos) { + endTimeNanos = System.nanoTime(); + durationNanos = endTimeNanos - startTimeNanos; + } + return durationNanos; + } + + public long getStartTimeNanos() { + return startTimeNanos; + } + + public long getStartTime(TimeUnit targetUnit) { + return targetUnit.convert(startTimeNanos, TimeUnit.NANOSECONDS); + } + + /** + * Returns the duration for which this clock was running in nanoseconds. + * + * @return The duration for which this clock was running in nanoseconds. + * + * @throws IllegalStateException If the clock is not yet stopped. + */ + public long getDurationInNanos() { + if (isRunning()) { + throw new IllegalStateException("The clock is not yet stopped."); + } + return durationNanos; + } + + /** + * Returns the duration for which this clock was running in the given timeunit. + * + * @return The duration for which this clock was running in the given timeunit. + * + * @throws IllegalStateException If the clock is not yet stopped. + */ + public long getDuration(TimeUnit targetUnit) { + if (isRunning()) { + throw new IllegalStateException("The clock is not yet stopped."); + } + return targetUnit.convert(durationNanos, TimeUnit.NANOSECONDS); + } + + public boolean isRunning() { + return -1 != durationNanos; + } + + public static long newStartTimeNanos() { + return RxNetty.isEventPublishingDisabled() ? SYSTEM_TIME_DISABLED_TIME : System.nanoTime(); + } + + public static long newStartTime(TimeUnit timeUnit) { + if (RxNetty.isEventPublishingDisabled() ) { + return SYSTEM_TIME_DISABLED_TIME; + } + + if (TimeUnit.NANOSECONDS == timeUnit) { + return newStartTimeNanos(); + } + return timeUnit.convert(newStartTimeNanos(), TimeUnit.NANOSECONDS); + } + + public static long onEnd(long startTime, TimeUnit timeUnit) { + if (RxNetty.isEventPublishingDisabled() ) { + return SYSTEM_TIME_DISABLED_TIME; + } + if (TimeUnit.NANOSECONDS == timeUnit) { + return onEndNanos(startTime); + } + long startTimeNanos = TimeUnit.NANOSECONDS.convert(startTime, timeUnit); + return timeUnit.convert(onEndNanos(startTimeNanos), TimeUnit.NANOSECONDS); + } + + public static long onEndNanos(long startTimeNanos) { + if (RxNetty.isEventPublishingDisabled() ) { + return SYSTEM_TIME_DISABLED_TIME; + } + return System.nanoTime() - startTimeNanos; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/EventAttributeKeys.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventAttributeKeys.java new file mode 100644 index 0000000..0bbec12 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventAttributeKeys.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.events; + +import io.netty.util.AttributeKey; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.client.events.ClientEventListener; + +/** + * A set of {@link AttributeKey} that are used by the event infrastructure. + */ +public final class EventAttributeKeys { + + private EventAttributeKeys() { + } + + public static final AttributeKey EVENT_PUBLISHER = + AttributeKey.valueOf("rxnetty_client_event_publisher"); + public static final AttributeKey CLIENT_EVENT_LISTENER = + AttributeKey.valueOf("rxnetty_client_event_listener"); + public static final AttributeKey CONNECTION_EVENT_LISTENER = + AttributeKey.valueOf("rxnetty_client_conn_event_listener"); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/EventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventListener.java new file mode 100644 index 0000000..2e9c1bf --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventListener.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import java.util.concurrent.TimeUnit; + +/** + * A listener to subscribe to events published by an {@link EventSource} + */ +public interface EventListener { + + /** + * Marks the end of all event callbacks. No methods on this listener will ever be called once this method is called. + */ + void onCompleted(); + + /** + * Typically specific instances on {@link EventListener} will provide events that are fired by RxNetty, however, + * there may be cases, where a user would want to emit custom events. eg: Creating a custom protocol on top of an + * existing protocol like TCP, would perhaps require some additional events. In such a case, this callback can be + * utilized. + * + * @param event Event published. + * + * @see #onCustomEvent(Object, long, TimeUnit) + * @see #onCustomEvent(Object, Throwable) + * @see #onCustomEvent(Object, long, TimeUnit, Throwable) + */ + void onCustomEvent(Object event); + + /** + * Typically specific instances on {@link EventListener} will provide events that are fired by RxNetty, however, + * there may be cases, where a user would want to emit custom events. eg: Creating a custom protocol on top of an + * existing protocol like TCP, would perhaps require some additional events. In such a case, this callback can be + * utilized. + * + * One should use this overload as opposed to {@link #onCustomEvent(Object)} if the custom event need not be created + * per invocation but has to be associated with a duration. This is a simple optimization to reduce event creation + * overhead. + * + * @param event Event published. + * @param duration Duration associated with this event. The semantics of this duration is totally upto the published + * event. + * @param timeUnit Timeunit for the duration. + * + * @see #onCustomEvent(Object, long, TimeUnit) + * @see #onCustomEvent(Object, Throwable) + * @see #onCustomEvent(Object, long, TimeUnit, Throwable) + */ + void onCustomEvent(Object event, long duration, TimeUnit timeUnit); + + /** + * Typically specific instances on {@link EventListener} will provide events that are fired by RxNetty, however, + * there may be cases, where a user would want to emit custom events. eg: Creating a custom protocol on top of an + * existing protocol like TCP, would perhaps require some additional events. In such a case, this callback can be + * utilized. + * + * One should use this overload as opposed to {@link #onCustomEvent(Object)} if the custom event need not be created + * per invocation but has to be associated with an error. This is a simple optimization to reduce event creation + * overhead. + * + * @param event Event published. + * + * @see #onCustomEvent(Object, long, TimeUnit) + * @see #onCustomEvent(Object, Throwable) + * @see #onCustomEvent(Object, long, TimeUnit, Throwable) + */ + void onCustomEvent(Object event, Throwable throwable); + + /** + * Typically specific instances on {@link EventListener} will provide events that are fired by RxNetty, however, + * there may be cases, where a user would want to emit custom events. eg: Creating a custom protocol on top of an + * existing protocol like TCP, would perhaps require some additional events. In such a case, this callback can be + * utilized. + * + * One should use this overload as opposed to {@link #onCustomEvent(Object)} if the custom event need not be created + * per invocation but has to be associated with a duration and an error. This is a simple optimization to reduce + * event creation overhead. + * + * @param event Event published. + * @param duration Duration associated with this event. The semantics of this duration is totally upto the published + * event. + * @param timeUnit Timeunit for the duration. + * @param throwable Error associated with the event. + * + * @see #onCustomEvent(Object, long, TimeUnit) + * @see #onCustomEvent(Object, Throwable) + * @see #onCustomEvent(Object, long, TimeUnit, Throwable) + */ + void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/EventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventPublisher.java new file mode 100644 index 0000000..358d1f8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventPublisher.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +/** + * A contract for any publisher of events. + */ +public interface EventPublisher { + + /** + * Returns {@code true} if event publishing is enabled. This is primarily used to short-circuit event publishing + * if the publishing is not enabled. Event publishing will be disabled if there are no active listeners or has + * been explicitly disabled using {@link io.reactivex.netty.RxNetty#disableEventPublishing()} + * + * @return {@code true} if event publishing is enabled. + */ + boolean publishingEnabled(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/EventSource.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventSource.java new file mode 100644 index 0000000..a07c074 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/EventSource.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import rx.Subscription; + +/** + * An event source to which {@link EventListener}s can subscribe to receive events. + */ +public interface EventSource { + + /** + * Subscribes the passed {@code listener} for events published by this source. + * + * @param listener Listener for events published by this source. + * + * @return Subscription, from which one can unsubscribe to stop receiving events. + */ + Subscription subscribe(T listener); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenerInvocationException.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenerInvocationException.java new file mode 100644 index 0000000..fbbda24 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenerInvocationException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ListenerInvocationException extends RuntimeException { + + private Map exceptions; + private String message; + + private static final long serialVersionUID = -4381062024201397997L; + + @SuppressWarnings("rawtypes") + protected ListenerInvocationException() { + super("Metric event listener invocation failed."); + exceptions = new HashMap<>(); + message = super.getMessage(); + } + + protected void addException(EventListener listener, Throwable error) { + exceptions.put(listener, error); + } + + protected void finish() { + exceptions = Collections.unmodifiableMap(exceptions); + StringBuilder msgBuilder = new StringBuilder(getMessage()).append(". Errors: \n"); + for (Map.Entry exceptionEntry : exceptions.entrySet()) { + msgBuilder.append("Listener: "); + msgBuilder.append(exceptionEntry.getKey().getClass().getSimpleName()); + msgBuilder.append("\n Error:"); + ByteArrayOutputStream stackTraceStream = new ByteArrayOutputStream(); + exceptionEntry.getValue().printStackTrace(new PrintStream(stackTraceStream)); + msgBuilder.append(stackTraceStream.toString()); + } + message = msgBuilder.toString(); + } + + public Map getExceptions() { + return exceptions; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenersHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenersHolder.java new file mode 100644 index 0000000..d4c2cd5 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/ListenersHolder.java @@ -0,0 +1,406 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import io.reactivex.netty.RxNetty; +import java.util.logging.Level; +import java.util.logging.Logger; +import rx.Subscription; +import rx.exceptions.Exceptions; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.functions.Action5; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; + +/** + * A holder for storing {@link EventListener} providing utility methods for any {@link EventSource} implementation that + * requires storing and invoking listeners. + * + * @param Type of listener to store. + */ +public final class ListenersHolder implements EventSource, EventPublisher { + + private static final Logger logger = Logger.getLogger(ListenersHolder.class.getName()); + + private final CopyOnWriteArraySet> listeners; + + public ListenersHolder() { + listeners = new CopyOnWriteArraySet<>(); + } + + public ListenersHolder(ListenersHolder toCopy) { + + listeners = new CopyOnWriteArraySet<>(toCopy.listeners); + + for (final ListenerHolder holder : listeners) { + // Add the subscription to the existing holder, so that on unsubscribe, it is also removed from this list. + holder.subscription.add(Subscriptions.create(new Action0() { + @Override + public void call() { + listeners.remove(holder); + } + })); + } + } + + @Override + public Subscription subscribe(final T listener) { + final CompositeSubscription cs = new CompositeSubscription(); + + ListenerHolder.configureRemoval(cs, listener, listeners); + + final ListenerHolder holder = new ListenerHolder<>(listener, cs); + listeners.add(holder); + return cs; + } + + @Override + public boolean publishingEnabled() { + return !RxNetty.isEventPublishingDisabled() && !listeners.isEmpty(); + } + + public void dispose() { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + try { + listener.onCompleted(); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + + if (null != exception) { + exception.finish(); + throw exception; + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + */ + public void invokeListeners(Action1 invocationAction) { + ListenerInvocationException exception = null; + for (final ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param duration Duration. + * @param timeUnit Time unit for the duration. + */ + public void invokeListeners(Action3 invocationAction, long duration, TimeUnit timeUnit) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, duration, timeUnit); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param duration Duration. + * @param timeUnit Time unit for the duration. + * @param throwable An error. + */ + public void invokeListeners(Action4 invocationAction, long duration, + TimeUnit timeUnit, Throwable throwable) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, duration, timeUnit, throwable); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param duration Duration. + * @param timeUnit Time unit for the duration. + * @param arg Any arbitrary argument + */ + public void invokeListeners(Action4 invocationAction, long duration, + TimeUnit timeUnit, A arg) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, duration, timeUnit, arg); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param duration Duration. + * @param timeUnit Time unit for the duration. + * @param throwable An error. + * @param arg Any arbitrary argument + */ + public void invokeListeners(Action5 invocationAction, long duration, + TimeUnit timeUnit, Throwable throwable, A arg) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, duration, timeUnit, throwable, arg); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param arg Any arbitrary argument + */ + public void invokeListeners(Action2 invocationAction, A arg) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, arg); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + /** + * Invoke listeners with an action expressed by the passed {@code invocationAction}. This method does the necessary + * validations required for invoking a listener and also guards against a listener throwing exceptions on invocation. + * + * @param invocationAction The action to perform on all listeners. + * @param throwable An error. + * @param arg Any arbitrary argument + */ + public void invokeListeners(Action3 invocationAction, Throwable throwable, A arg) { + ListenerInvocationException exception = null; + for (ListenerHolder listener : listeners) { + if (!listener.subscription.isUnsubscribed()) { + try { + invocationAction.call(listener.delegate, throwable, arg); + } catch (Throwable e) { + exception = handleListenerError(exception, listener, e); + } + } + } + + if (null != exception) { + exception.finish(); + /*Do not bubble event notification errors to the caller, event notifications are best effort.*/ + logger.log(Level.SEVERE, "Error occured while invoking event listeners.", exception); + } + } + + private ListenerInvocationException handleListenerError(ListenerInvocationException exception, + ListenerHolder listener, Throwable e) { + Exceptions.throwIfFatal(e); + if (null == exception) { + exception = new ListenerInvocationException(); + } + exception.addException(listener.delegate, e); + return exception; + } + + public ListenersHolder copy() { + return new ListenersHolder<>(this); + } + + /*Visible for testing*/Collection getAllListeners() { + final Collection toReturn = new ArrayList<>(); + for (ListenerHolder listener : listeners) { + toReturn.add(listener.delegate); + } + return toReturn; + } + + /*Visible for testing*/CopyOnWriteArraySet> getActualListenersList() { + return listeners; + } + + public void subscribeAllTo(EventSource lazySource) { + for (ListenerHolder listener : listeners) { + listener.subscription.add(lazySource.subscribe(listener.delegate)); + } + } + + private static class ListenerHolder implements EventListener { + + private static final CompositeSubscription EMPTY_SUB_FOR_REMOVAL = new CompositeSubscription(); + + private final T delegate; + private final CompositeSubscription subscription; + + public ListenerHolder(T delegate, CompositeSubscription subscription) { + this.delegate = delegate; + this.subscription = subscription; + } + + @Override + public void onCompleted() { + if (!subscription.isUnsubscribed()) { + try { + delegate.onCompleted(); + } finally { + subscription.unsubscribe(); + } + } + } + + @Override + public void onCustomEvent(Object event) { } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { } + + public static ListenerHolder forRemoval(X listenerToRemove) { + return new ListenerHolder<>(listenerToRemove, EMPTY_SUB_FOR_REMOVAL); + } + + public static void configureRemoval(CompositeSubscription cs, + final X listenerToRemove, + final CopyOnWriteArraySet> removeFrom) { + cs.add(Subscriptions.create(new Action0() { + @Override + public void call() { + /** + * Why do we add {@link ListenerHolder} but remove {@link X}? + * Since {@link ListenerHolder} requires the associated {@link Subscription}, and then + * {@link Subscription} will require the {@link ListenerHolder}, there will be a circular dependency. + * + * Instead, by having {@link ListenerHolder} implement equals/hashcode to only look for the + * enclosing {@link X} instance, it is possible to add {@link ListenerHolder} but remove {@link X} + */ + removeFrom.remove(forRemoval(listenerToRemove)); + } + })); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ListenerHolder)) { + return false; + } + + @SuppressWarnings("rawtypes") + ListenerHolder that = (ListenerHolder) o; + + return delegate.equals(that.delegate); + + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/events/internal/SafeEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/events/internal/SafeEventListener.java new file mode 100644 index 0000000..b48b89d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/events/internal/SafeEventListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events.internal; + +import io.reactivex.netty.events.EventListener; + +/** + * A marker interface to indicate that the {@link EventListener} is safe to be invoked multiple times but the wrapped + * listener will only be invoked once. + */ +public interface SafeEventListener extends EventListener { +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/internal/ExecuteInEventloopAction.java b/netty-http-rx/src/main/java/io/reactivex/netty/internal/ExecuteInEventloopAction.java new file mode 100644 index 0000000..26a5aea --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/internal/ExecuteInEventloopAction.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.internal; + +import io.netty.channel.Channel; +import rx.functions.Action0; + +public abstract class ExecuteInEventloopAction implements Action0, Runnable { + + private final Channel channel; + + protected ExecuteInEventloopAction(Channel channel) { + this.channel = channel; + } + + @Override + public void call() { + if (channel.eventLoop().inEventLoop()) { + run(); + } else { + channel.eventLoop().execute(this); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/internal/InternalReadTimeoutHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/internal/InternalReadTimeoutHandler.java new file mode 100644 index 0000000..83bc01f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/internal/InternalReadTimeoutHandler.java @@ -0,0 +1,247 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.internal; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.PooledConnectionReleaseEvent; +import java.util.logging.Level; +import java.util.logging.Logger; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * A copy of netty's {@link ReadTimeoutHandler}. This is required because {@link ReadTimeoutHandler} does not allow + * reuse in the same pipeline, which is required for connection pooling. + * See issue https://github.com/ReactiveX/RxNetty/issues/344 + */ +public class InternalReadTimeoutHandler extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(InternalReadTimeoutHandler.class.getName()); + + private static final long MIN_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(1); + + private final long timeoutNanos; + + private volatile ScheduledFuture timeout; + private volatile long lastReadTime; + + private enum State { + Created, + Active, + Paused, + Destroyed + } + + private volatile State state = State.Created; + + private boolean closed; + + /** + * Creates a new instance. + * + * @param timeout + * read timeout + * @param unit + * the {@link TimeUnit} of {@code timeout} + */ + public InternalReadTimeoutHandler(long timeout, TimeUnit unit) { + if (unit == null) { + throw new NullPointerException("unit"); + } + + if (timeout <= 0) { + timeoutNanos = 0; + } else { + timeoutNanos = Math.max(unit.toNanos(timeout), MIN_TIMEOUT_NANOS); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isActive() && ctx.channel().isRegistered()) { + // channelActive() event has been fired already, which means this.channelActive() will + // not be invoked. We have to scheduleAfresh here instead. + scheduleAfresh(ctx); + } + + // channelActive() event has not been fired yet. this.channelActive() will be invoked + // and initialization will occur there. + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + destroy(); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + // Initialize early if channel is active already. + if (ctx.channel().isActive()) { + scheduleAfresh(ctx); + } + super.channelRegistered(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // This method will be invoked only if this handler was added + // before channelActive() event is fired. If a user adds this handler + // after the channelActive() event, scheduleAfresh() will be called by beforeAdd(). + scheduleAfresh(ctx); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + destroy(); + super.channelInactive(ctx); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + lastReadTime = System.nanoTime(); + ctx.fireChannelRead(msg); + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (State.Paused == state) { + // Add the timeout handler when write is complete. + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (State.Paused == state) { + /* + * Multiple writes can all add a listener, till it is active again (on write success), so it is + * required to only schedule next when the state is actually paused. + */ + scheduleAfresh(ctx); + } + } + }); + } + + super.write(ctx, msg, promise); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof PooledConnectionReleaseEvent) { + cancelTimeoutSchedule(ctx); + } + super.userEventTriggered(ctx, evt); + } + + private void cancelTimeoutSchedule(ChannelHandlerContext ctx) { + assert ctx.channel().eventLoop().inEventLoop(); /*should only be called from the owner eventloop*/ + if (State.Active == state) { + state = State.Paused; + timeout.cancel(false); + } + } + + private void scheduleAfresh(ChannelHandlerContext ctx) { + // Avoid the case where destroy() is called before scheduling timeouts. + // See: https://github.com/netty/netty/issues/143 + switch (state) { + case Created: + break; + case Active: + return; + case Paused: + break; + case Destroyed: + logger.log(Level.WARNING, "Not scheduling next read timeout task as the channel handler is removed."); + return; + } + + state = State.Active; + + lastReadTime = System.nanoTime(); + if (timeoutNanos > 0) { + timeout = _scheduleNextTask(ctx, new ReadTimeoutTask(ctx), timeoutNanos); + } + } + + private ScheduledFuture _scheduleNextTask(ChannelHandlerContext ctx, ReadTimeoutTask task, long timeoutNanos) { + try { + return ctx.executor().schedule(task, timeoutNanos, TimeUnit.NANOSECONDS); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to schedule read timeout task. Read timeout will not work on channel: " + + ctx.channel(), e); + throw e; + } + } + + private void destroy() { + state = State.Destroyed; + + if (timeout != null) { + timeout.cancel(false); + timeout = null; + } + } + + /** + * Is called when a read timeout was detected. + */ + protected void readTimedOut(ChannelHandlerContext ctx) throws Exception { + if (!closed) { + ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE); + ctx.close(); + closed = true; + } + } + + private final class ReadTimeoutTask implements Runnable { + + private final ChannelHandlerContext ctx; + + ReadTimeoutTask(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public void run() { + if (!ctx.channel().isOpen()) { + return; + } + + long currentTime = System.nanoTime(); + long nextDelay = timeoutNanos - (currentTime - lastReadTime); + if (nextDelay <= 0) { + // Read timed out - set a new timeout and notify the callback. + timeout = ctx.executor().schedule(this, timeoutNanos, TimeUnit.NANOSECONDS); + try { + readTimedOut(ctx); + } catch (Throwable t) { + ctx.fireExceptionCaught(t); + } + } else { + // Read occurred before the timeout - set a new timeout with shorter delay. + timeout = ctx.executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS); + } + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/internal/VoidToAnythingCast.java b/netty-http-rx/src/main/java/io/reactivex/netty/internal/VoidToAnythingCast.java new file mode 100644 index 0000000..0faa569 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/internal/VoidToAnythingCast.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.internal; + +import rx.Observable; +import rx.functions.Func1; + +/** + * A function to be used in place of {@link Observable#cast(Class)} to support nested generics. + * + * @param Target type. + */ +public class VoidToAnythingCast implements Func1 { + + @Override + public T call(Void aVoid) { + return null; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/CookiesHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/CookiesHolder.java new file mode 100644 index 0000000..8fdcbca --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/CookiesHolder.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http; + +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.util.AsciiString; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +/** + * A holder of cookies parsed from the Http headers. + */ +public class CookiesHolder { + + private final HttpHeaders nettyHeaders; + private final AsciiString cookiesHeaderName; + private final boolean isClientDecoder; + private Map> allCookies; + private boolean cookiesParsed; + + private CookiesHolder(HttpHeaders nettyHeaders, AsciiString cookiesHeaderName, boolean isClientDecoder) { + this.nettyHeaders = nettyHeaders; + this.cookiesHeaderName = cookiesHeaderName; + this.isClientDecoder = isClientDecoder; + allCookies = Collections.emptyMap(); + } + + public Map> getAllCookies() { + return _parseIfNeededAndGet(); + } + + public static CookiesHolder newClientResponseHolder(HttpHeaders headers) { + return new CookiesHolder(headers, SET_COOKIE, true); + } + + public static CookiesHolder newServerRequestHolder(HttpHeaders headers) { + return new CookiesHolder(headers, COOKIE, false); + } + + private synchronized Map> _parseIfNeededAndGet() { + if (cookiesParsed) { // This method is synchronized, a memory barrier for this variable to be refreshed. + return allCookies; + } + List allCookieHeaders = nettyHeaders.getAll(cookiesHeaderName); + Map> cookies = new HashMap<>(); + for (String aCookieHeader : allCookieHeaders) { + Set decode; + if (isClientDecoder) { + Cookie decoded = ClientCookieDecoder.STRICT.decode(aCookieHeader); + Set existingCookiesOfName = cookies.get(decoded.name()); + if (null == existingCookiesOfName) { + existingCookiesOfName = new HashSet<>(); + cookies.put(decoded.name(), existingCookiesOfName); + } + existingCookiesOfName.add(decoded); + } else { + decode = ServerCookieDecoder.STRICT.decode(aCookieHeader); + for (Cookie cookie : decode) { + Set existingCookiesOfName = cookies.get(cookie.name()); + if (null == existingCookiesOfName) { + existingCookiesOfName = new HashSet<>(); + cookies.put(cookie.name(), existingCookiesOfName); + } + existingCookiesOfName.add(cookie); + } + } + } + allCookies = Collections.unmodifiableMap(cookies); + cookiesParsed = true; + return allCookies; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/HttpHandlerNames.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/HttpHandlerNames.java new file mode 100644 index 0000000..2cb5e11 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/HttpHandlerNames.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http; + +/** + * A list of all handler names added by for HTTP. This is just to ensure consistency in naming. + */ +public enum HttpHandlerNames { + + HttpClientCodec("http-client-codec"), + HttpServerDecoder("http-server-request-decoder"), + HttpServerEncoder("http-server-response-encoder"), + WsServerDecoder("ws-server-request-decoder"), + WsServerEncoder("ws-server-response-encoder"), + WsServerUpgradeHandler("ws-server-upgrade-handler"), + WsClientDecoder("ws-client-request-decoder"), + WsClientEncoder("ws-client-response-encoder"), + WsClientUpgradeHandler("ws-client-upgrade-handler"), + SseClientCodec("sse-client-codec"), + SseServerCodec("sse-server-codec"), + ; + + private final String name; + + HttpHandlerNames(String name) { + this.name = qualify(name); + } + + public String getName() { + return name; + } + + private static String qualify(String name) { + return "_rx_netty_" + name; + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/TrailingHeaders.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/TrailingHeaders.java new file mode 100644 index 0000000..bdf59ec --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/TrailingHeaders.java @@ -0,0 +1,116 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http; + +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.LastHttpContent; + +import java.util.List; + +/** + * A mutable stateful entity containing trailing headers. + * + * This class is not thread safe + */ +public class TrailingHeaders { + + private final LastHttpContent lastHttpContent; + + public TrailingHeaders() { + lastHttpContent = LastHttpContent.EMPTY_LAST_CONTENT; + } + + public TrailingHeaders(LastHttpContent lastHttpContent) { + this.lastHttpContent = lastHttpContent; + } + + /** + * Adds an HTTP trailing header with the passed {@code name} and {@code value} to this request. + * + * @param name Name of the header. + * @param value Value for the header. + * + * @return {@code this}. + */ + public TrailingHeaders addHeader(CharSequence name, Object value) { + lastHttpContent.trailingHeaders().add(name, value); + return this; + } + + /** + * Adds an HTTP trailing header with the passed {@code name} and {@code values} to this request. + * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this}. + */ + public TrailingHeaders addHeader(CharSequence name, Iterable values) { + lastHttpContent.trailingHeaders().add(name, values); + return this; + } + + /** + * Overwrites the current value, if any, of the passed trailing header to the passed value for this request. + * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this}. + */ + public TrailingHeaders setHeader(CharSequence name, Object value) { + lastHttpContent.trailingHeaders().set(name, value); + return this; + } + + /** + * Overwrites the current value, if any, of the passed trailing header to the passed values for this request. + * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this}. + */ + public TrailingHeaders setHeader(CharSequence name, Iterable values) { + lastHttpContent.trailingHeaders().set(name, values); + return this; + } + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * + * @return The first header value or {@code null} if there is no such header + */ + public String getHeader(CharSequence name) { + return lastHttpContent.trailingHeaders().get(name); + } + + /** + * Returns the values of headers with the specified name + * + * @param name The name of the headers to search + * + * @return A {@link List} of header values which will be empty if no values are found + */ + public List getAllHeaderValues(CharSequence name) { + return lastHttpContent.trailingHeaders().getAll(name); + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClient.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClient.java new file mode 100644 index 0000000..9f875d0 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClient.java @@ -0,0 +1,363 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.ssl.SslCodec; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +/** + * An HTTP client for executing HTTP requests. + * + *

Immutability

+ * An instance of this client is immutable and all mutations produce a new client instance. For this reason it is + * recommended that the mutations are done during client creation and not during request processing to avoid repeated + * object creation overhead. + * + * @param The type of the content of request. + * @param The type of the content of response. + */ +public abstract class HttpClient extends InterceptingHttpClient { + + /** + * Creates a new client instances, inheriting all configurations from this client and adding the passed read timeout + * for all requests created by the newly created client instance. + * + * @param timeOut Read timeout duration. + * @param timeUnit Timeunit for the timeout. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient readTimeOut(int timeOut, TimeUnit timeUnit); + + /** + * Creates a new client instances, inheriting all configurations from this client and adding a + * {@link ChannelOption} for the connections created by the newly created client instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient channelOption(ChannelOption option, T value); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerFirst(String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerLast(String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(String, String, ChannelHandler)} + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory); + + /** + * Creates a new client instances, inheriting all configurations from this client and using the passed + * action to configure all the connections created by the newly created client instance. + * + * @param pipelineConfigurator Action to configure {@link ChannelPipeline}. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient pipelineConfigurator(Action1 pipelineConfigurator); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslEngineFactory} for all secured connections created by the newly created client instance. + * + * If the {@link SSLEngine} instance can be statically, created, {@link #secure(SSLEngine)} can be used. + * + * @param sslEngineFactory Factory for all secured connections created by the newly created client instance. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient secure(Func1 sslEngineFactory); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslEngine} for all secured connections created by the newly created client instance. + * + * If the {@link SSLEngine} instance can not be statically, created, {@link #secure(Func1)} )} can be used. + * + * @param sslEngine {@link SSLEngine} for all secured connections created by the newly created client instance. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient secure(SSLEngine sslEngine); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslCodec} for all secured connections created by the newly created client instance. + * + * This is required only when the {@link SslHandler} used by {@link SslCodec} is to be modified before adding to + * the {@link ChannelPipeline}. For most of the cases, {@link #secure(Func1)} or {@link #secure(SSLEngine)} will be + * enough. + * + * @param sslCodec {@link SslCodec} for all secured connections created by the newly created client instance. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient secure(SslCodec sslCodec); + + /** + * Creates a new client instance, inheriting all configurations from this client and using a trust-all + * {@link TrustManagerFactory}for all secured connections created by the newly created client instance. + * + * This is only for testing and should not be used for real production clients. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient unsafeSecure(); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link LoggingHandler} + * + * @return A new {@link HttpClient} instance. + * + * @deprecated Use {@link #enableWireLogging(String, LogLevel)} instead. + */ + @Deprecated + public abstract HttpClient enableWireLogging(LogLevel wireLoggingLevel); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param name Name of the logger that can be used to control the logging dynamically. + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link LoggingHandler} + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient enableWireLogging(String name, LogLevel wireLoggingLevel); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code providerFactory}. + * + * @param providerFactory Channel provider factory. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient channelProvider(ChannelProviderFactory providerFactory); + + /** + * Creates a new HTTP client instance with the passed host and port for the target server. + * + * @param host Hostname for the target server. + * @param port Port for the target server. + * + * @return A new {@code HttpClient} instance. + */ + public static HttpClient newClient(String host, int port) { + return newClient(new InetSocketAddress(host, port)); + } + + /** + * Creates a new HTTP client instance with the passed address of the target server. + * + * @param serverAddress Socket address for the target server. + * + * @return A new {@code HttpClient} instance. + */ + public static HttpClient newClient(SocketAddress serverAddress) { + return HttpClientImpl.create(serverAddress); + } + + /** + * Creates a new HTTP client instance using the supplied connection provider. + * + * @param providerFactory {@link ConnectionProviderFactory} for the client. + * @param hostStream Stream of hosts for the client. + * + * @return A new {@code HttpClient} instance. + */ + public static HttpClient newClient(ConnectionProviderFactory providerFactory, + Observable hostStream) { + return HttpClientImpl.create(providerFactory, hostStream); + } + + /** + * Creates a new client instances, inheriting all configurations from this client and following the passed number of + * max HTTP redirects. + * + * @param maxRedirects Maximum number of redirects to follow for any request. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient followRedirects(int maxRedirects); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling/disabling redirects + * for all requests created by the newly created client instance. + * + * @param follow {@code true} to follow redirects. {@code false} to disable any redirects. + * + * @return A new {@link HttpClient} instance. + */ + public abstract HttpClient followRedirects(boolean follow); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientImpl.java new file mode 100644 index 0000000..79b3280 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientImpl.java @@ -0,0 +1,312 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LogLevel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.protocol.http.HttpHandlerNames; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventPublisher; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import io.reactivex.netty.protocol.http.client.internal.HttpChannelProviderFactory; +import io.reactivex.netty.protocol.http.client.internal.HttpClientRequestImpl; +import io.reactivex.netty.protocol.http.client.internal.HttpClientToConnectionBridge; +import io.reactivex.netty.protocol.http.client.internal.Redirector; +import io.reactivex.netty.protocol.http.ws.client.Ws7To13UpgradeHandler; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import io.reactivex.netty.protocol.tcp.client.TcpClientImpl; +import io.reactivex.netty.ssl.SslCodec; +import rx.Observable; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +import static io.reactivex.netty.protocol.http.client.internal.HttpClientRequestImpl.*; + +public final class HttpClientImpl extends HttpClient { + + private final TcpClient> client; + private final int maxRedirects; + private final HttpClientEventPublisher clientEventPublisher; + private final RequestProvider requestProvider; + + private HttpClientImpl(final TcpClient> client, final int maxRedirects, + HttpClientEventPublisher clientEventPublisher) { + this.client = client; + this.maxRedirects = maxRedirects; + this.clientEventPublisher = clientEventPublisher; + requestProvider = new RequestProvider() { + @Override + public HttpClientRequest createRequest(HttpVersion version, HttpMethod method, String uri) { + return HttpClientRequestImpl.create(version, method, uri, client, maxRedirects); + } + }; + } + + @Override + public HttpClientRequest createGet(String uri) { + return createRequest(HttpMethod.GET, uri); + } + + @Override + public HttpClientRequest createPost(String uri) { + return createRequest(HttpMethod.POST, uri); + } + + @Override + public HttpClientRequest createPut(String uri) { + return createRequest(HttpMethod.PUT, uri); + } + + @Override + public HttpClientRequest createDelete(String uri) { + return createRequest(HttpMethod.DELETE, uri); + } + + @Override + public HttpClientRequest createHead(String uri) { + return createRequest(HttpMethod.HEAD, uri); + } + + @Override + public HttpClientRequest createOptions(String uri) { + return createRequest(HttpMethod.OPTIONS, uri); + } + + @Override + public HttpClientRequest createPatch(String uri) { + return createRequest(HttpMethod.PATCH, uri); + } + + @Override + public HttpClientRequest createTrace(String uri) { + return createRequest(HttpMethod.TRACE, uri); + } + + @Override + public HttpClientRequest createConnect(String uri) { + return createRequest(HttpMethod.CONNECT, uri); + } + + @Override + public HttpClientRequest createRequest(HttpMethod method, String uri) { + return createRequest(HttpVersion.HTTP_1_1, method, uri); + } + + @Override + public HttpClientRequest createRequest(HttpVersion version, HttpMethod method, String uri) { + return requestProvider.createRequest(version, method, uri); + } + + @Override + public HttpClientInterceptorChain intercept() { + return new HttpClientInterceptorChainImpl<>(requestProvider, clientEventPublisher); + } + + @Override + public HttpClientImpl readTimeOut(int timeOut, TimeUnit timeUnit) { + return _copy(client.readTimeOut(timeOut, timeUnit), maxRedirects); + } + + @Override + public HttpClientImpl followRedirects(int maxRedirects) { + return _copy(client, maxRedirects); + } + + @Override + public HttpClientImpl followRedirects(boolean follow) { + return _copy(client, follow ? Redirector.DEFAULT_MAX_REDIRECTS : NO_REDIRECTS); + } + + @Override + public HttpClientImpl channelOption(ChannelOption option, T value) { + return _copy(client.channelOption(option, value), maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerFirst(String name, Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerFirst(name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerFirst(group, name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerLast(String name, Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerLast(name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerLast(group, name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerBefore(baseName, name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerBefore(group, baseName, name, + handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerAfter(baseName, name, handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpClientImpl.castClient(client.addChannelHandlerAfter(group, baseName, name, + handlerFactory)), + maxRedirects); + } + + @Override + public HttpClientImpl pipelineConfigurator(Action1 pipelineConfigurator) { + return _copy(HttpClientImpl.castClient(client.pipelineConfigurator(pipelineConfigurator)), + maxRedirects); + } + + @Override + public HttpClientImpl secure(Func1 sslEngineFactory) { + return _copy(client.secure(sslEngineFactory), maxRedirects); + } + + @Override + public HttpClientImpl secure(SSLEngine sslEngine) { + return _copy(client.secure(sslEngine), maxRedirects); + } + + @Override + public HttpClientImpl secure(SslCodec sslCodec) { + return _copy(client.secure(sslCodec), maxRedirects); + } + + @Override + public HttpClientImpl unsafeSecure() { + return _copy(client.unsafeSecure(), maxRedirects); + } + + @Override + @Deprecated + public HttpClientImpl enableWireLogging(LogLevel wireLoggingLevel) { + return _copy(client.enableWireLogging(wireLoggingLevel), maxRedirects); + } + + @Override + public HttpClient enableWireLogging(String name, LogLevel wireLoggingLevel) { + return _copy(client.enableWireLogging(name, wireLoggingLevel), maxRedirects); + } + + @Override + public HttpClientImpl channelProvider(ChannelProviderFactory providerFactory) { + return _copy(client.channelProvider(new HttpChannelProviderFactory(clientEventPublisher, providerFactory)), + maxRedirects); + } + + @Override + public Subscription subscribe(HttpClientEventsListener listener) { + return clientEventPublisher.subscribe(listener); + } + + public static HttpClient create(final ConnectionProviderFactory providerFactory, + Observable hostStream) { + ConnectionProviderFactory cpf = new ConnectionProviderFactory() { + @Override + public ConnectionProvider newProvider(Observable> hosts) { + return providerFactory.newProvider(hosts.map( + new Func1, HostConnector>() { + @Override + public HostConnector call(HostConnector hc) { + HttpClientEventPublisher hcep = new HttpClientEventPublisher(); + hc.subscribe(hcep); + return new HostConnector<>(hc.getHost(), hc.getConnectionProvider(), hcep, hcep, hcep); + } + })); + } + }; + return _newClient(TcpClientImpl.create(cpf, hostStream)); + } + + public static HttpClient create(SocketAddress socketAddress) { + return _newClient(TcpClientImpl.create(socketAddress)); + } + + private static HttpClient _newClient(TcpClient tcpClient) { + + HttpClientEventPublisher clientEventPublisher = new HttpClientEventPublisher(); + + TcpClient> client = + tcpClient.>pipelineConfigurator(new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + pipeline.addLast(HttpHandlerNames.HttpClientCodec.getName(), new HttpClientCodec()); + pipeline.addLast(new HttpClientToConnectionBridge<>()); + pipeline.addLast(HttpHandlerNames.WsClientUpgradeHandler.getName(), + new Ws7To13UpgradeHandler()); + } + }).channelProvider(new HttpChannelProviderFactory(clientEventPublisher)); + + client.subscribe(clientEventPublisher); + + return new HttpClientImpl<>(client, NO_REDIRECTS, clientEventPublisher); + } + + @SuppressWarnings("unchecked") + private static TcpClient> castClient(TcpClient rawTypes) { + return (TcpClient>) rawTypes; + } + + private HttpClientImpl _copy(TcpClient> newClient, int maxRedirects) { + return new HttpClientImpl<>(newClient, maxRedirects, clientEventPublisher); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChain.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChain.java new file mode 100644 index 0000000..418fddb --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChain.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +/** + * Interceptor chain for {@link HttpClient}, obtained via {@link HttpClient#intercept()}.

+ * + * Multiple interceptors can be added to this chain by using the various {@code next*()} methods available, before + * calling {@link #finish()} that returns a new {@link HttpClient} which inherits all the configuration from the parent + * client (from which this chain was created) and adds these interceptors. + * + *

Order of execution

+ * + * Interceptors are executed in the order in which they are added. + * + * @param The type of the content of request. + * @param The type of the content of response. + */ +public interface HttpClientInterceptorChain { + + /** + * Adds a simple interceptor that does not change the type of objects read/written to a connection. + * + * @param interceptor Interceptor to add. + * + * @return {@code this} + */ + HttpClientInterceptorChain next(Interceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects read from the connections created by the client provided by + * this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + HttpClientInterceptorChain nextWithReadTransform(TransformingInterceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects written to the connections created by the client provided by + * this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + HttpClientInterceptorChain nextWithWriteTransform(TransformingInterceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects read and written to the connections created by the client + * provided by this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + HttpClientInterceptorChain nextWithTransform(TransformingInterceptor interceptor); + + /** + * Finish the addition of interceptors and create a new client instance. + * + * @return New client instance which inherits all the configuration from the parent client + * (from which this chain was created) and adds these interceptors. + */ + InterceptingHttpClient finish(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChainImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChainImpl.java new file mode 100644 index 0000000..c5b3b61 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientInterceptorChainImpl.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.reactivex.netty.protocol.http.client.events.HttpClientEventPublisher; + +final class HttpClientInterceptorChainImpl implements HttpClientInterceptorChain { + + private final RequestProvider rp; + private final HttpClientEventPublisher cep; + + HttpClientInterceptorChainImpl(RequestProvider rp, HttpClientEventPublisher cep) { + this.rp = rp; + this.cep = cep; + } + + @Override + public HttpClientInterceptorChain next(Interceptor i) { + return new HttpClientInterceptorChainImpl<>(i.intercept(rp), cep); + } + + @Override + public HttpClientInterceptorChain nextWithReadTransform(TransformingInterceptor i) { + return new HttpClientInterceptorChainImpl<>(i.intercept(rp), cep); + } + + @Override + public HttpClientInterceptorChain nextWithWriteTransform(TransformingInterceptor i) { + return new HttpClientInterceptorChainImpl<>(i.intercept(rp), cep); + } + + @Override + public HttpClientInterceptorChain nextWithTransform(TransformingInterceptor i) { + return new HttpClientInterceptorChainImpl<>(i.intercept(rp), cep); + } + + @Override + public InterceptingHttpClient finish() { + return new InterceptingHttpClientImpl<>(rp, cep); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientRequest.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientRequest.java new file mode 100644 index 0000000..655ed45 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientRequest.java @@ -0,0 +1,575 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client; + +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.protocol.http.TrailingHeaders; +import io.reactivex.netty.protocol.http.ws.client.WebSocketRequest; +import rx.Observable; +import rx.annotations.Experimental; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * An HTTP request. An instance of a request can only be created from an associated {@link HttpClient} and can be + * modified after creation. + * + *

Request URIs

+ * + * While creating a request, the user should provide a URI to be used for the request. The URI can be relative or + * absolute. If the URI is relative (missing host and port information), the target host and port are inferred from the + * {@link HttpClient} that created the request. If the URI is absolute, the host and port are used from the URI. + * + *

Mutations

+ * + * All mutations to this request creates a brand new instance. + + *

Trailing headers

+ * + * One can write HTTP trailing headers by using + * + *

Executing request

+ * + * The request is executed every time {@link HttpClientRequest}, or {@link Observable} returned by + * {@code write*Content} is subscribed and is the only way of executing the request. + * + * @param The type of objects read from the request content. + * @param The type of objects read from the response content. + */ +public abstract class HttpClientRequest extends Observable> { + + protected HttpClientRequest(OnSubscribe> onSubscribe) { + super(onSubscribe); + } + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeContent(Observable contentSource); + + /** + * Uses the passed {@link Observable} as the source of content for this request. Every item is written and flushed + * immediately. + * + * @param contentSource Content source for the request. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeContentAndFlushOnEach(Observable contentSource); + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeContent(Observable contentSource, + Func1 flushSelector); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeStringContent(Observable contentSource); + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeStringContent(Observable contentSource, + Func1 flushSelector); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeStringContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeStringContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeBytesContent(Observable contentSource); + + /** + * Uses the passed {@link Observable} as the source of content for this request. + * + * @param contentSource Content source for the request. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable> writeBytesContent(Observable contentSource, + Func1 flushSelector); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeBytesContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + * @param contentSource Content source for the request. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. All pending + * writes are flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + @Experimental + public abstract Observable> writeBytesContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * Enables read timeout for the response of the newly created and returned request. + * + * @param timeOut Read timeout duration. + * @param timeUnit Read timeout time unit. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest readTimeOut(int timeOut, TimeUnit timeUnit); + + /** + * Enables following HTTP redirects for the newly created and returned request. + * + * @param maxRedirects Maximum number of redirects allowed. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest followRedirects(int maxRedirects); + + /** + * Enables/disables following HTTP redirects for the newly created and returned request. + * + * @param follow {@code true} for enabling redirects, {@code false} to disable. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest followRedirects(boolean follow); + + /** + * Updates the HTTP method of the request and creates a new {@link HttpClientRequest} instance. + * + * @param method New HTTP method to use. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setMethod(HttpMethod method); + + /** + * Updates the URI of the request and creates a new {@link HttpClientRequest} instance. + * + * @param newUri New URI to use. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setUri(String newUri); + + /** + * Adds an HTTP header with the passed {@code name} and {@code value} to this request. + * + * @param name Name of the header. + * @param value Value for the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addHeader(CharSequence name, Object value); + + /** + * Adds the HTTP headers from the passed {@code headers} to this request. + * + * @param headers Map of the headers. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addHeaders(Map> headers); + + /** + * Adds the passed {@code cookie} to this request. + * + * @param cookie Cookie to add. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addCookie(Cookie cookie); + + /** + * Adds the passed header as a date value to this request. The date is formatted using netty's {@link + * HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the HTTP specifications into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addDateHeader(CharSequence name, Date value); + + /** + * Adds multiple date values for the passed header name to this request. The date values are formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the HTTP specifications into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values for the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addDateHeader(CharSequence name, Iterable values); + + /** + * Adds an HTTP header with the passed {@code name} and {@code values} to this request. + * + * @param name Name of the header. + * @param values Values for the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest addHeaderValues(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed date value for this request. The date is + * formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date + * as per the HTTP specifications into + * the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setDateHeader(CharSequence name, Date value); + + /** + * Overwrites the current value, if any, of the passed header to the passed value for this request. + * + * @param name Name of the header. + * @param value Value of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setHeader(CharSequence name, Object value); + + /** + * Overwrites the current values, if any, of the passed headers for this request. + * + * @param headers Map of the headers. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setHeaders(Map> headers); + + /** + * Overwrites the current value, if any, of the passed header to the passed date values for this request. The date + * is formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the + * date as per the HTTP specifications + * into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setDateHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed values for this request. + * + * @param name Name of the header. + * @param values Values of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setHeaderValues(CharSequence name, Iterable values); + + /** + * Removes the passed header from this request. + * + * @param name Name of the header. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest removeHeader(CharSequence name); + + /** + * Sets HTTP Connection header to the appropriate value for HTTP keep-alive. This delegates to {@link + * HttpHeaders#setKeepAlive(HttpMessage, boolean)} + * + * @param keepAlive {@code true} to enable keep alive. + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setKeepAlive(boolean keepAlive); + + /** + * Sets the HTTP transfer encoding to chunked for this request. This delegates to {@link + * HttpHeaders#setTransferEncodingChunked(HttpMessage)} + * + * @return A new instance of the {@link HttpClientRequest} sharing all existing state from this request. + */ + public abstract HttpClientRequest setTransferEncodingChunked(); + + /** + * Creates a new {@code HttpClientRequest} instance modifying the content type using the passed {@code transformer}. + * + * @param transformer Transformer to transform the content stream. + * + * @param New type of the content. + * + * @return A new instance of {@link HttpClientRequest} with the transformed content stream. + */ + public abstract HttpClientRequest transformContent(AllocatingTransformer transformer); + + /** + * Creates a new {@code HttpClientRequest} instance modifying the content type of the response using the + * passed {@code transformer}. + * + * @param transformer Transformer to transform the content stream. + * + * @param New type of the content. + * + * @return A new instance of {@link HttpClientRequest} with the transformed response content stream. + */ + public abstract HttpClientRequest transformResponseContent(Transformer transformer); + + /** + * Creates a new {@link WebSocketRequest}, inheriting all configurations from this request, that will request an + * upgrade to websockets from the server. + * + * @return A new {@link WebSocketRequest}. + */ + public abstract WebSocketRequest requestWebSocketUpgrade(); + + /** + * Checks whether a header with the passed name exists for this request. + * + * @param name Header name. + * + * @return {@code true} if the header exists. + */ + public abstract boolean containsHeader(CharSequence name); + + /** + * Checks whether a header with the passed name and value exists for this request. + * + * @param name Header name. + * @param value Value to check. + * @param caseInsensitiveValueMatch If the value has to be matched ignoring case. + * + * @return {@code true} if the header with the passed value exists. + */ + public abstract boolean containsHeaderWithValue(CharSequence name, CharSequence value, + boolean caseInsensitiveValueMatch); + + /** + * Fetches the value of a header, if exists, for this request. + * + * @param name Name of the header. + * + * @return The value of the header, if it exists, {@code null} otherwise. If there are multiple values for this + * header, the first value is returned. + */ + public abstract String getHeader(CharSequence name); + + /** + * Fetches all values of a header, if exists, for this request. + * + * @param name Name of the header. + * + * @return All values of the header, if it exists, {@code null} otherwise. + */ + public abstract List getAllHeaders(CharSequence name); + + /** + * Returns an iterator over the header entries. Multiple values for the same header appear as separate entries in + * the returned iterator. + * + * @return An iterator over the header entries + */ + public abstract Iterator> headerIterator(); + + /** + * Returns a new {@link Set} that contains the names of all headers in this request. Note that modifying the + * returned {@link Set} will not affect the state of this response. + */ + public abstract Set getHeaderNames(); + + /** + * Returns the HTTP version of this request. + * + * @return The HTTP version of this request. + */ + public abstract HttpVersion getHttpVersion(); + + /** + * Returns the HTTP method for this request. + * + * @return The HTTP method for this request. + */ + public abstract HttpMethod getMethod(); + + /** + * Returns the URI for this request. The returned URI does not contain the scheme, host and port portion of + * the URI. + * + * @return The URI for this request. + */ + public abstract String getUri(); + +} + diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientResponse.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientResponse.java new file mode 100644 index 0000000..821f373 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpClientResponse.java @@ -0,0 +1,410 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ContentSource; +import io.reactivex.netty.protocol.http.internal.HttpMessageFormatter; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import rx.Observable; +import rx.Observable.Transformer; +import rx.Subscriber; + +import java.text.ParseException; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * HTTP response for {@link HttpClient} + * + *

Thread safety

+ * + * This object is not thread-safe and must not be used by multiple threads. + * + *

Mutability

+ * + * Headers and trailing headers can be mutated for this response. + */ +public abstract class HttpClientResponse { + + /** + * Returns the HTTP version for this response. + * + * @return The HTTP version for this response. + */ + public abstract HttpVersion getHttpVersion(); + + /** + * Returns the HTTP status for this response. + * + * @return The HTTP status for this response. + */ + public abstract HttpResponseStatus getStatus(); + + /** + * Returns an immutable map of cookie names and cookies contained in this response. + * + * @return An immutable map of cookie names and cookies contained in this response. + */ + public abstract Map> getCookies(); + + /** + * Checks if there is a header with the passed name in this response. + * + * @param name Name of the header. + * + * @return {@code true} if there is a header with the passed name in this response. + */ + public abstract boolean containsHeader(CharSequence name); + + /** + * Checks if there is a header with the passed name and value in this response. + * + * @param name Name of the header. + * @param value Value of the header. + * @param ignoreCaseValue {@code true} then the value comparision is done ignoring case. + * + * @return {@code true} if there is a header with the passed name and value in this response. + */ + public abstract boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue); + + /** + * Returns an iterator over the header entries. Multiple values for the same header appear as separate entries in + * the returned iterator. + * + * @return An iterator over the header entries + */ + public abstract Iterator> headerIterator(); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @return The first header value or {@code null} if there is no such header + */ + public abstract String getHeader(CharSequence name); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return The first header value or {@code defaultValue} if there is no such header + */ + public abstract String getHeader(CharSequence name, String defaultValue); + + /** + * Returns the values of headers with the specified name + * + * @param name The name of the headers to search + * + * @return A {@link List} of header values which will be empty if no values are found + */ + public abstract List getAllHeaderValues(CharSequence name); + + /** + * Returns the length of the content. + * + * @return the content length + * + * @throws NumberFormatException if the message does not have the {@code "Content-Length"} header or its value is + * not a number. + */ + public abstract long getContentLength(); + + /** + * Returns the length of the content. + * + * @param defaultValue Default value if the message does not have a {@code "Content-Length"} header or its value is + * not a number + * + * @return the content length or {@code defaultValue} if this message does not have the {@code "Content-Length"} + * header or its value is not a number + */ + public abstract long getContentLength(long defaultValue); + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * + * @return the header value + * + * @throws ParseException if there is no such header or the header value is not a formatted date + */ + public abstract long getDateHeader(CharSequence name) throws ParseException; + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * @param defaultValue Default value if there is no header with this name. + * + * @return the header value or {@code defaultValue} if there is no header with this name. + */ + public abstract long getDateHeader(CharSequence name, long defaultValue); + + /** + * Returns the value of the {@code "Host"} header. + */ + public abstract String getHostHeader(); + + /** + * Returns the value of the {@code "Host"} header. + * + * @param defaultValue Default if the header does not exist. + * + * @return The value of the {@code "Host"} header or {@code defaultValue} if there is no such header. + */ + public abstract String getHost(String defaultValue); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * + * @return the header value + * + * @throws NumberFormatException if there is no such header or the header value is not a number + */ + public abstract int getIntHeader(CharSequence name); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return the header value or the {@code defaultValue} if there is no such header or the header value is not a + * number + */ + public abstract int getIntHeader(CharSequence name, int defaultValue); + + /** + * Returns {@code true} if and only if this response has the content-length header set. + */ + public abstract boolean isContentLengthSet(); + + /** + * Returns {@code true} if and only if the connection can remain open and thus 'kept alive'. This methods respects + * the value of the {@code "Connection"} header first and then the return value of + * {@link HttpVersion#isKeepAliveDefault()}. + */ + public abstract boolean isKeepAlive(); + + /** + * Checks to see if the transfer encoding of this response is chunked + * + * @return True if transfer encoding is chunked, otherwise false + */ + public abstract boolean isTransferEncodingChunked(); + + /** + * Returns a new {@link Set} that contains the names of all headers in this response. Note that modifying the + * returned {@link Set} will not affect the state of this response. + */ + public abstract Set getHeaderNames(); + + /** + * Adds an HTTP header with the passed {@code name} and {@code value} to this response. + * + * @param name Name of the header. + * @param value Value for the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse addHeader(CharSequence name, Object value); + + /** + * Adds the passed {@code cookie} to this response. + * + * @param cookie Cookie to add. + * + * @return {@code this} + */ + public abstract HttpClientResponse addCookie(Cookie cookie); + + /** + * Adds the passed header as a date value to this response. The date is formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse addDateHeader(CharSequence name, Date value); + + /** + * Adds multiple date values for the passed header name to this response. The date values are formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse addDateHeader(CharSequence name, Iterable values); + + /** + * Adds an HTTP header with the passed {@code name} and {@code values} to this response. + * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse addHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed date value for this response. The date is + * formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date + * as per the HTTP specifications into + * the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse setDateHeader(CharSequence name, Date value); + + /** + * Overwrites the current value, if any, of the passed header to the passed value for this response. + * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse setHeader(CharSequence name, Object value); + + /** + * Overwrites the current value, if any, of the passed header to the passed date values for this response. The date + * is formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the + * date as per the HTTP specifications + * into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse setDateHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed values for this response. + * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse setHeader(CharSequence name, Iterable values); + + /** + * Removes the passed header from this response. + * + * @param name Name of the header. + * + * @return {@code this} + */ + public abstract HttpClientResponse removeHeader(CharSequence name); + + /** + * Returns the content as a stream of Server sent events. + * There can only be one {@link Subscriber} to the returned {@link Observable}, any subsequent subscriptions will + * get an error. + * + * @return Stream of content as {@link ServerSentEvent} messages. + */ + public abstract ContentSource getContentAsServerSentEvents(); + + /** + * Returns the content as a stream. There can only be one {@link Subscriber} to the returned {@link Observable}, any + * subsequent subscriptions will get an error. + * + * @return Stream of content. + */ + public abstract ContentSource getContent(); + + /** + * Marks the content to be discarded. This means that the content can not be read from this response from now. + * + * @return An {@link Observable}, subscription to which will discard the content. This {@code Observable} will + * error/complete when the content errors/completes and unsubscription from here will unsubscribe from the content. + */ + public abstract Observable discardContent(); + + /** + * Transforms the type of objects read from the content of this response, using the supplied {@code transformer}. + * + * @return A new instance of {@code HttpClientResponse} with transformed content. + */ + public abstract HttpClientResponse transformContent(Transformer transformer); + + /** + * Returns the underlying channel on which this response was received. + * + * @return The underlying channel on which this response was received. + */ + public abstract Channel unsafeNettyChannel(); + + /** + * Returns the underlying connection on which this response was received. + * + * @return The underlying connection on which this response was received. + */ + public abstract Connection unsafeConnection(); + + public String toString() { + return HttpMessageFormatter.formatResponse(getHttpVersion(), getStatus(), headerIterator()); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpRedirectException.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpRedirectException.java new file mode 100644 index 0000000..4dcec7a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/HttpRedirectException.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client; + +/** + * An exception signifying a failed HTTP redirects. Every exception has an encapsulated {@link Reason} retrievable via + * {@link #getReason()} + */ +public class HttpRedirectException extends RuntimeException { + + private static final long serialVersionUID = 612647744832660373L; + private final Reason reason; + + public enum Reason { + RedirectLoop, + TooManyRedirects, + InvalidRedirect + } + + public HttpRedirectException(Reason reason) { + this.reason = reason; + } + + public HttpRedirectException(Reason reason, Throwable cause) { + super(getMsgWithReason(reason), cause); + this.reason = reason; + } + + public HttpRedirectException(Reason reason, String message) { + super(getMsgWithReason(reason, message)); + this.reason = reason; + } + + public HttpRedirectException(Reason reason, String message, Throwable cause) { + super(getMsgWithReason(reason, message), cause); + this.reason = reason; + } + + public Reason getReason() { + return reason; + } + + private static String getMsgWithReason(Reason reason) { + return "Redirect failed. Reason: " + reason; + } + + private static String getMsgWithReason(Reason reason, String message) { + return getMsgWithReason(reason) + ". Error: " + message; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClient.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClient.java new file mode 100644 index 0000000..94b3308 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClient.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; + +public abstract class InterceptingHttpClient implements EventSource { + + /** + * Creates a GET request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createGet(String uri); + + /** + * Creates a POST request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createPost(String uri); + + /** + * Creates a PUT request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createPut(String uri); + + /** + * Creates a DELETE request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createDelete(String uri); + + /** + * Creates a HEAD request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createHead(String uri); + + /** + * Creates an OPTIONS request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createOptions(String uri); + + /** + * Creates a PATCH request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createPatch(String uri); + + /** + * Creates a TRACE request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createTrace(String uri); + + /** + * Creates a CONNECT request for the passed URI. + * + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createConnect(String uri); + + /** + * Creates a request for the passed HTTP method and URI. + * + * @param method Http Method. + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createRequest(HttpMethod method, String uri); + + /** + * Creates a request for the passed HTTP version, method and URI. + * + * @param version HTTP version + * @param method Http Method. + * @param uri The URI for the request. The URI should be absolute and should not contain the scheme, host and port. + * + * @return New {@link HttpClientRequest}. + */ + public abstract HttpClientRequest createRequest(HttpVersion version, HttpMethod method, String uri); + + /** + * Starts the process of adding interceptors to this client. Interceptors help in achieving various usecases of + * instrumenting and transforming connections. + * + * @return A new interceptor chain to add the various interceptors. + */ + public abstract HttpClientInterceptorChain intercept(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClientImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClientImpl.java new file mode 100644 index 0000000..5967e3f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/InterceptingHttpClientImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventPublisher; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import rx.Subscription; + +class InterceptingHttpClientImpl extends InterceptingHttpClient { + + private final RequestProvider requestProvider; + private final HttpClientEventPublisher cep; + + public InterceptingHttpClientImpl(RequestProvider requestProvider, HttpClientEventPublisher cep) { + this.requestProvider = requestProvider; + this.cep = cep; + } + + @Override + public HttpClientRequest createGet(String uri) { + return createRequest(HttpMethod.GET, uri); + } + + @Override + public HttpClientRequest createPost(String uri) { + return createRequest(HttpMethod.POST, uri); + } + + @Override + public HttpClientRequest createPut(String uri) { + return createRequest(HttpMethod.PUT, uri); + } + + @Override + public HttpClientRequest createDelete(String uri) { + return createRequest(HttpMethod.DELETE, uri); + } + + @Override + public HttpClientRequest createHead(String uri) { + return createRequest(HttpMethod.HEAD, uri); + } + + @Override + public HttpClientRequest createOptions(String uri) { + return createRequest(HttpMethod.OPTIONS, uri); + } + + @Override + public HttpClientRequest createPatch(String uri) { + return createRequest(HttpMethod.PATCH, uri); + } + + @Override + public HttpClientRequest createTrace(String uri) { + return createRequest(HttpMethod.TRACE, uri); + } + + @Override + public HttpClientRequest createConnect(String uri) { + return createRequest(HttpMethod.CONNECT, uri); + } + + @Override + public HttpClientRequest createRequest(HttpMethod method, String uri) { + return createRequest(HttpVersion.HTTP_1_1, method, uri); + } + + @Override + public HttpClientRequest createRequest(HttpVersion version, HttpMethod method, String uri) { + return requestProvider.createRequest(version, method, uri); + } + + @Override + public HttpClientInterceptorChain intercept() { + return new HttpClientInterceptorChainImpl<>(requestProvider, cep); + } + + @Override + public Subscription subscribe(HttpClientEventsListener listener) { + return cep.subscribe(listener); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/Interceptor.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/Interceptor.java new file mode 100644 index 0000000..89d0004 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/Interceptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +/** + * An interceptor that preserves the type of client request and response content. + * + * @param The type of the content of request. + * @param The type of the content of response. + */ +public interface Interceptor { + + /** + * Intercepts and optionally changes the passed {@code RequestProvider}. + * + * @param provider Provider to intercept. + * + * @return Provider to use after this transformation. + */ + RequestProvider intercept(RequestProvider provider); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/RequestProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/RequestProvider.java new file mode 100644 index 0000000..89c8721 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/RequestProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; + +/** + * An abstraction that creates new instance of {@link HttpClientRequest}. + * + * @param The type of the content of request. + * @param The type of the content of response. + */ +public interface RequestProvider { + + /** + * Creates a new {@link HttpClientRequest} with the provided {@code version}, {@code method} and {@code uri} + * + * @param version HTTP version. + * @param method HTTP method. + * @param uri URI. + * + * @return A new instance of {@code HttpClientRequest} + */ + HttpClientRequest createRequest(HttpVersion version, HttpMethod method, String uri); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/TransformingInterceptor.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/TransformingInterceptor.java new file mode 100644 index 0000000..052afef --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/TransformingInterceptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +/** + * An interceptor that preserves the type of client request and response content. + * + * @param The type of the content of request. + * @param The type of the content of response. + */ +public interface TransformingInterceptor { + + /** + * Intercepts and changes the passed {@code RequestProvider}. + * + * @param provider Provider to intercept. + * + * @return Provider to use after this transformation. + */ + RequestProvider intercept(RequestProvider provider); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisher.java new file mode 100644 index 0000000..a5c33e9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisher.java @@ -0,0 +1,343 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.events; + +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.events.ListenersHolder; +import io.reactivex.netty.events.internal.SafeEventListener; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.subscriptions.CompositeSubscription; + +import java.util.concurrent.TimeUnit; + +public final class HttpClientEventPublisher extends HttpClientEventsListener + implements EventSource, EventPublisher { + + private static final Action1 REQUEST_SUBMIT_ACTION = + new Action1() { + @Override + public void call(HttpClientEventsListener listener) { + listener.onRequestSubmitted(); + } + }; + + private static final Action1 REQUEST_WRITE_START_ACTION = + new Action1() { + @Override + public void call(HttpClientEventsListener listener) { + listener.onRequestWriteStart(); + } + }; + + private static final Action3 REQUEST_WRITE_COMPLETE_ACTION = + new Action3() { + @Override + public void call(HttpClientEventsListener listener, Long duration, TimeUnit timeUnit) { + listener.onRequestWriteComplete(duration, timeUnit); + } + }; + + private static final Action4 REQUEST_WRITE_FAILED_ACTION = + new Action4() { + @Override + public void call(HttpClientEventsListener listener, Long duration, TimeUnit timeUnit, Throwable t) { + listener.onRequestWriteFailed(duration, timeUnit, t); + } + }; + + private static final Action4 RESP_HEADER_RECIEVED_ACTION = + new Action4() { + @Override + public void call(HttpClientEventsListener listener, Long duration, TimeUnit timeUnit, + Integer responseCode) { + listener.onResponseHeadersReceived(responseCode, duration, timeUnit); + } + }; + + private static final Action1 RESP_CONTENT_RECIEVED_ACTION = + new Action1() { + @Override + public void call(HttpClientEventsListener listener) { + listener.onResponseContentReceived(); + } + }; + + private static final Action3 RESP_RECIEVE_COMPLETE_ACTION = + new Action3() { + @Override + public void call(HttpClientEventsListener listener, Long duration, TimeUnit timeUnit) { + listener.onResponseReceiveComplete(duration, timeUnit); + } + }; + + private static final Action2 RESP_FAILED_ACTION = + new Action2() { + @Override + public void call(HttpClientEventsListener listener, Throwable t) { + listener.onResponseFailed(t); + } + }; + + private static final Action3 PROCESSING_COMPLETE_ACTION = + new Action3() { + @Override + public void call(HttpClientEventsListener listener, Long duration, TimeUnit timeUnit) { + listener.onRequestProcessingComplete(duration, timeUnit); + } + }; + + private final ListenersHolder listeners; + private final TcpClientEventPublisher tcpDelegate; + + public HttpClientEventPublisher() { + listeners = new ListenersHolder<>(); + tcpDelegate = new TcpClientEventPublisher(); + } + + private HttpClientEventPublisher(ListenersHolder l, TcpClientEventPublisher tcpDelegate) { + listeners = new ListenersHolder<>(l); + this.tcpDelegate = tcpDelegate; + } + + @Override + public void onRequestSubmitted() { + listeners.invokeListeners(REQUEST_SUBMIT_ACTION); + } + + @Override + public void onRequestWriteStart() { + listeners.invokeListeners(REQUEST_WRITE_START_ACTION); + } + + @Override + public void onRequestWriteComplete(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(REQUEST_WRITE_COMPLETE_ACTION, duration, timeUnit); + } + + @Override + public void onRequestWriteFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(REQUEST_WRITE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onResponseHeadersReceived(final int responseCode, long duration, TimeUnit timeUnit) { + listeners.invokeListeners(RESP_HEADER_RECIEVED_ACTION, duration, timeUnit, responseCode); + } + + @Override + public void onResponseContentReceived() { + listeners.invokeListeners(RESP_CONTENT_RECIEVED_ACTION); + } + + @Override + public void onResponseReceiveComplete(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(RESP_RECIEVE_COMPLETE_ACTION, duration, timeUnit); + } + + @Override + public void onResponseFailed(final Throwable throwable) { + listeners.invokeListeners(RESP_FAILED_ACTION, throwable); + } + + @Override + public void onRequestProcessingComplete(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(PROCESSING_COMPLETE_ACTION, duration, timeUnit); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectStart() { + tcpDelegate.onConnectStart(); + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectSuccess(duration, timeUnit); + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onConnectFailed(duration, timeUnit, throwable); + } + + @Override + public void onPoolReleaseStart() { + tcpDelegate.onPoolReleaseStart(); + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onPoolReleaseSuccess(duration, timeUnit); + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onPoolReleaseFailed(duration, timeUnit, throwable); + } + + @Override + public void onPooledConnectionEviction() { + tcpDelegate.onPooledConnectionEviction(); + } + + @Override + public void onPooledConnectionReuse() { + tcpDelegate.onPooledConnectionReuse(); + } + + @Override + public void onPoolAcquireStart() { + tcpDelegate.onPoolAcquireStart(); + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onPoolAcquireSuccess(duration, timeUnit); + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onPoolAcquireFailed(duration, timeUnit, throwable); + } + + @Override + public void onByteRead(long bytesRead) { + tcpDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + tcpDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onFlushStart() { + tcpDelegate.onFlushStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + tcpDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onWriteStart() { + tcpDelegate.onWriteStart(); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseStart() { + tcpDelegate.onConnectionCloseStart(); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event) { + tcpDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + tcpDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + tcpDelegate.onCustomEvent(event, throwable); + } + + @Override + public boolean publishingEnabled() { + return listeners.publishingEnabled(); + } + + @Override + public Subscription subscribe(HttpClientEventsListener listener) { + if (!SafeEventListener.class.isAssignableFrom(listener.getClass())) { + listener = new SafeHttpClientEventsListener(listener); + } + + CompositeSubscription cs = new CompositeSubscription(); + cs.add(listeners.subscribe(listener)); + + TcpClientEventListener tcpListener = listener; + if (listener instanceof SafeHttpClientEventsListener) { + tcpListener = ((SafeHttpClientEventsListener) listener).unwrap(); + } + + cs.add(tcpDelegate.subscribe(tcpListener)); + return cs; + } + + public EventSource asTcpEventSource() { + return new EventSource() { + @Override + public Subscription subscribe(TcpClientEventListener listener) { + if (listener instanceof HttpClientEventsListener) { + return HttpClientEventPublisher.this.subscribe((HttpClientEventsListener) listener); + } + return tcpDelegate.subscribe(listener); + } + }; + } + + public HttpClientEventPublisher copy() { + return new HttpClientEventPublisher(listeners.copy(), tcpDelegate.copy()); + } + + /*Visible for testing*/ListenersHolder getListeners() { + return listeners; + } + + /*Visible for testing*/TcpClientEventListener getTcpDelegate() { + return tcpDelegate; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListener.java new file mode 100644 index 0000000..a5ad0a9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListener.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.events; + +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; + +import java.util.concurrent.TimeUnit; + +/** + * A listener for all events published by {@link HttpClient} + */ +public abstract class HttpClientEventsListener extends TcpClientEventListener { + + /** + * Event when a new request is submitted for the client. + */ + public void onRequestSubmitted() {} + + /** + * Event when the write of request started. + */ + public void onRequestWriteStart() {} + + /** + * Event when a request write is completed. + * + * @param duration Time taken from the start of write to completion. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onRequestWriteComplete(long duration, TimeUnit timeUnit) {} + + /** + * Event when a request write failed. + * + * @param duration Time taken from the start of write to failure. + * @param timeUnit Time unit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onRequestWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + /** + * Event when the response headers are received. + * + * @param responseCode The HTTP response code. + * @param duration The time between the request write completion and response header recieve. + * @param timeUnit Timeunit for the duration. + */ + @SuppressWarnings("unused") + public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) {} + + /** + * Event whenever an HTTP response content is received (an HTTP response can have multiple content chunks, in which + * case this event will be fired as many times for the same response). + */ + public void onResponseContentReceived() {} + + /** + * Event when the response receive is completed. + * + * @param duration Time taken between receiving the response headers and completion of response. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onResponseReceiveComplete(long duration, TimeUnit timeUnit) {} + + /** + * Event when the response failed (either it did not arrive or not arrived completely) + * + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onResponseFailed(Throwable throwable) {} + + /** + * Event when the entire request processing (request header write to response failed/complete) is completed. + * + * @param duration Time taken from start of write of request to response receive completion. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onRequestProcessingComplete(long duration, TimeUnit timeUnit) {} +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/SafeHttpClientEventsListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/SafeHttpClientEventsListener.java new file mode 100644 index 0000000..09814fa --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/events/SafeHttpClientEventsListener.java @@ -0,0 +1,306 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.events; + +import io.reactivex.netty.events.internal.SafeEventListener; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +final class SafeHttpClientEventsListener extends HttpClientEventsListener implements SafeEventListener { + + private final HttpClientEventsListener delegate; + + private final AtomicBoolean completed = new AtomicBoolean(); + + public SafeHttpClientEventsListener(HttpClientEventsListener delegate) { + this.delegate = delegate; + } + + @Override + public void onCompleted() { + if (completed.compareAndSet(false, true)) { + delegate.onCompleted(); + } + } + + @Override + public void onRequestSubmitted() { + if (!completed.get()) { + delegate.onRequestSubmitted(); + } + } + + @Override + public void onRequestWriteStart() { + if (!completed.get()) { + delegate.onRequestWriteStart(); + } + } + + @Override + public void onRequestWriteComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onRequestWriteComplete(duration, timeUnit); + } + } + + @Override + public void onRequestWriteFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onRequestWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onResponseHeadersReceived(responseCode, duration, timeUnit); + } + } + + @Override + public void onResponseContentReceived() { + if (!completed.get()) { + delegate.onResponseContentReceived(); + } + } + + @Override + public void onResponseReceiveComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onResponseReceiveComplete(duration, timeUnit); + } + } + + @Override + public void onResponseFailed(Throwable throwable) { + if (!completed.get()) { + delegate.onResponseFailed(throwable); + } + } + + @Override + public void onRequestProcessingComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onRequestProcessingComplete(duration, timeUnit); + } + } + + @Override + public void onConnectStart() { + if (!completed.get()) { + delegate.onConnectStart(); + } + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onConnectFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onPoolReleaseStart() { + if (!completed.get()) { + delegate.onPoolReleaseStart(); + } + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onPoolReleaseSuccess(duration, timeUnit); + } + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onPoolReleaseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onPooledConnectionEviction() { + if (!completed.get()) { + delegate.onPooledConnectionEviction(); + } + } + + @Override + public void onPooledConnectionReuse() { + if (!completed.get()) { + delegate.onPooledConnectionReuse(); + } + } + + @Override + public void onPoolAcquireStart() { + if (!completed.get()) { + delegate.onPoolAcquireStart(); + } + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onPoolAcquireSuccess(duration, timeUnit); + } + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onPoolAcquireFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onByteRead(long bytesRead) { + if (!completed.get()) { + delegate.onByteRead(bytesRead); + } + } + + @Override + public void onByteWritten(long bytesWritten) { + if (!completed.get()) { + delegate.onByteWritten(bytesWritten); + } + } + + @Override + public void onFlushStart() { + if (!completed.get()) { + delegate.onFlushStart(); + } + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onFlushComplete(duration, timeUnit); + } + } + + @Override + public void onWriteStart() { + if (!completed.get()) { + delegate.onWriteStart(); + } + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onWriteSuccess(duration, timeUnit); + } + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onConnectionCloseStart() { + if (!completed.get()) { + delegate.onConnectionCloseStart(); + } + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionCloseSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event) { + if (!completed.get()) { + delegate.onCustomEvent(event); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit, throwable); + } + } + + public HttpClientEventsListener unwrap() { + return delegate; + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, throwable); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SafeHttpClientEventsListener)) { + return false; + } + + SafeHttpClientEventsListener that = (SafeHttpClientEventsListener) o; + + return !(delegate != null? !delegate.equals(that.delegate) : that.delegate != null); + + } + + @Override + public int hashCode() { + return delegate != null? delegate.hashCode() : 0; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProvider.java new file mode 100644 index 0000000..7d3d9c8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventPublisher; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import rx.Observable; +import rx.functions.Func1; + +public class HttpChannelProvider implements ChannelProvider { + + public static final AttributeKey HTTP_CLIENT_EVENT_LISTENER = + AttributeKey.valueOf("rxnetty_http_client_event_listener"); + + private final HttpClientEventPublisher hostEventPublisher; + private final ChannelProvider delegate; + + public HttpChannelProvider(HttpClientEventPublisher hostEventPublisher, ChannelProvider delegate) { + this.hostEventPublisher = hostEventPublisher; + this.delegate = delegate; + } + + @Override + public Observable newChannel(Observable input) { + if (null != delegate) { + input = delegate.newChannel(input); + } + return input.map(new Func1() { + @Override + public Channel call(Channel channel) { + channel.attr(HTTP_CLIENT_EVENT_LISTENER).set(hostEventPublisher); + return channel; + } + }); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProviderFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProviderFactory.java new file mode 100644 index 0000000..05ab3a9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpChannelProviderFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client.internal; + +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventPublisher; + +public class HttpChannelProviderFactory implements ChannelProviderFactory { + + private final HttpClientEventPublisher clientEventPublisher; + private final ChannelProviderFactory delegate; + + public HttpChannelProviderFactory(HttpClientEventPublisher clientEventPublisher) { + this(clientEventPublisher, null); + } + + public HttpChannelProviderFactory(HttpClientEventPublisher clientEventPublisher, ChannelProviderFactory delegate) { + this.clientEventPublisher = clientEventPublisher; + this.delegate = delegate instanceof HttpChannelProviderFactory + ? ((HttpChannelProviderFactory) delegate).delegate : delegate; + } + + @Override + public ChannelProvider newProvider(Host host, EventSource eventSource, + EventPublisher publisher, ClientEventListener clientPublisher) { + final HttpClientEventPublisher hostPublisher = new HttpClientEventPublisher(); + ChannelProvider delegate = null; + if (null != this.delegate) { + delegate = this.delegate.newProvider(host, eventSource, publisher, clientPublisher); + } + hostPublisher.subscribe(clientEventPublisher); + eventSource.subscribe(hostPublisher); + return new HttpChannelProvider(hostPublisher, delegate); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImpl.java new file mode 100644 index 0000000..b087550 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImpl.java @@ -0,0 +1,542 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.channel.AppendTransformerEvent; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.internal.VoidToAnythingCast; +import io.reactivex.netty.protocol.http.TrailingHeaders; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import io.reactivex.netty.protocol.http.internal.OperatorTrailer; +import io.reactivex.netty.protocol.http.ws.client.internal.WebSocketRequestImpl; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.*; + +public final class HttpClientRequestImpl extends HttpClientRequest { + + public static final int NO_REDIRECTS = -1; + + private final List immutableTransformers; + private final List immutableResponseTransformers; + private final RawRequest rawRequest; + private final TcpClient> client; + private final Func1 flushOnEachSelector = new Func1() { + @Override + public Boolean call(I next) { + return true; + } + }; + + private HttpClientRequestImpl(final RawRequest rawRequest, final TcpClient> client, + List immutableTransformers, + List immutableResponseTransformers) { + super(new OnSubscribeFuncImpl<>(client, rawRequest, immutableResponseTransformers, immutableTransformers)); + this.rawRequest = rawRequest; + this.client = client; + this.immutableTransformers = immutableTransformers; + this.immutableResponseTransformers = immutableResponseTransformers; + } + + @Override + public Observable> writeContent(Observable contentSource) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, false); + } + + @Override + public Observable> writeContentAndFlushOnEach(Observable contentSource) { + return writeContent(contentSource, flushOnEachSelector); + } + + @Override + public Observable> writeStringContent(Observable contentSource) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, false); + } + + @Override + public Observable> writeBytesContent(Observable contentSource) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, false); + } + + @Override + public Observable> writeContent(Observable contentSource, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, flushSelector, false); + } + + @Override + public Observable> writeStringContent(Observable contentSource, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, flushSelector, false); + } + + @Override + public Observable> writeBytesContent(Observable contentSource, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(rawObservable, flushSelector, false); + } + + @Override + public Observable> writeContent(Observable contentSource, + final Func0 trailerFactory, + final Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), true); + } + + @Override + public Observable> writeStringContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), true); + } + + @Override + public Observable> writeBytesContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), true); + } + + @Override + public Observable> writeContent(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), flushSelector, + true); + } + + @Override + public Observable> writeStringContent( + Observable contentSource, Func0 trailerFactory, Func2 trailerMutator, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), flushSelector, + true); + } + + @Override + public Observable> writeBytesContent( + Observable contentSource, Func0 trailerFactory, Func2 trailerMutator, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return _writeContentRaw(OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), flushSelector, + true); + } + + @Override + public HttpClientRequestImpl readTimeOut(int timeOut, TimeUnit timeUnit) { + return _copy(client.readTimeOut(timeOut, timeUnit)); + } + + @Override + public HttpClientRequestImpl followRedirects(int maxRedirects) { + final Redirector redirector = new Redirector<>(maxRedirects, client); + HttpClientRequestImpl toReturn = _copy(client, rawRequest.followRedirect(redirector)); + redirector.setOriginalRequest(toReturn.rawRequest); + return toReturn; + } + + @Override + public HttpClientRequestImpl followRedirects(boolean follow) { + return follow ? followRedirects(Redirector.DEFAULT_MAX_REDIRECTS) : followRedirects(NO_REDIRECTS); + } + + @Override + public HttpClientRequestImpl setMethod(HttpMethod method) { + return _copy(client, rawRequest.setMethod(method)); + } + + @Override + public HttpClientRequestImpl setUri(String newUri) { + return _copy(client, rawRequest.setUri(newUri)); + } + + @Override + public HttpClientRequestImpl addHeader(CharSequence name, Object value) { + return _copy(client, rawRequest.addHeader(name, value)); + } + + @Override + public HttpClientRequest addHeaders(Map> headers) { + return _copy(client, rawRequest.addHeaders(headers)); + } + + @Override + public HttpClientRequestImpl addCookie(Cookie cookie) { + return _copy(client, rawRequest.addCookie(cookie)); + } + + @Override + public HttpClientRequestImpl addDateHeader(CharSequence name, Date value) { + return _copy(client, rawRequest.addDateHeader(name, value)); + } + + @Override + public HttpClientRequestImpl addDateHeader(CharSequence name, Iterable values) { + return _copy(client, rawRequest.addDateHeader(name, values)); + } + + @Override + public HttpClientRequestImpl addHeaderValues(CharSequence name, Iterable values) { + return _copy(client, rawRequest.addHeaderValues(name, values)); + } + + @Override + public HttpClientRequestImpl setDateHeader(CharSequence name, Date value) { + return _copy(client, rawRequest.setDateHeader(name, value)); + } + + @Override + public HttpClientRequestImpl setHeader(CharSequence name, Object value) { + return _copy(client, rawRequest.setHeader(name, value)); + } + + @Override + public HttpClientRequest setHeaders(Map> headers) { + return _copy(client, rawRequest.setHeaders(headers)); + } + + @Override + public HttpClientRequestImpl setDateHeader(CharSequence name, Iterable values) { + return _copy(client, rawRequest.setDateHeader(name, values)); + } + + @Override + public HttpClientRequestImpl setHeaderValues(CharSequence name, Iterable values) { + return _copy(client, rawRequest.setHeaderValues(name, values)); + } + + @Override + public HttpClientRequestImpl removeHeader(CharSequence name) { + return _copy(client, rawRequest.removeHeader(name)); + } + + @Override + public HttpClientRequestImpl setKeepAlive(boolean keepAlive) { + return _copy(client, rawRequest.setKeepAlive(keepAlive)); + } + + @Override + public HttpClientRequestImpl setTransferEncodingChunked() { + return _copy(client, rawRequest.setTransferEncodingChunked()); + } + + @Override + public HttpClientRequestImpl transformContent(AllocatingTransformer transformer) { + final List newTransformers = new ArrayList<>(immutableTransformers); + @SuppressWarnings("unchecked") + AppendTransformerEvent e = new AppendTransformerEvent(transformer); + newTransformers.add(e); + @SuppressWarnings("unchecked") + RawRequest cast = (RawRequest) this.rawRequest; + return new HttpClientRequestImpl<>(cast, client, newTransformers, immutableResponseTransformers); + } + + @Override + public HttpClientRequestImpl transformResponseContent(Transformer transformer) { + final List newTransformers = new ArrayList<>(immutableResponseTransformers); + newTransformers.add(transformer); + @SuppressWarnings("unchecked") + RawRequest cast = (RawRequest) this.rawRequest; + TcpClient rawClient = client; + @SuppressWarnings("unchecked") + TcpClient> _client = (TcpClient>)rawClient; + return new HttpClientRequestImpl<>(cast, _client, immutableTransformers, newTransformers); + } + + @Override + public WebSocketRequestImpl requestWebSocketUpgrade() { + return WebSocketRequestImpl.createNew(this); + } + + @Override + public boolean containsHeader(CharSequence name) { + return rawRequest.getHeaders().headers().contains(name); + } + + @Override + public boolean containsHeaderWithValue(CharSequence name, CharSequence value, boolean caseInsensitiveValueMatch) { + return rawRequest.getHeaders().headers().contains(name, value, caseInsensitiveValueMatch); + } + + @Override + public String getHeader(CharSequence name) { + return rawRequest.getHeaders().headers().get(name); + } + + @Override + public List getAllHeaders(CharSequence name) { + return rawRequest.getHeaders().headers().getAll(name); + } + + @Override + public Iterator> headerIterator() { + return rawRequest.getHeaders().headers().iteratorCharSequence(); + } + + @Override + public Set getHeaderNames() { + return rawRequest.getHeaders().headers().names(); + } + + @Override + public HttpVersion getHttpVersion() { + return rawRequest.getHeaders().protocolVersion(); + } + + @Override + public HttpMethod getMethod() { + return rawRequest.getHeaders().method(); + } + + @Override + public String getUri() { + return rawRequest.getHeaders().uri(); + } + + public static HttpClientRequestImpl create(final HttpVersion version, final HttpMethod httpMethod, + final String uri, + final TcpClient> client, + int maxRedirects) { + Redirector redirector = NO_REDIRECTS == maxRedirects + ? null + : new Redirector(maxRedirects, client + ); + + final RawRequest rawRequest = RawRequest.create(version, httpMethod, uri, redirector); + + if (null != redirector) { + redirector.setOriginalRequest(rawRequest); + } + + return create(rawRequest, client); + } + + public static HttpClientRequestImpl create(final HttpVersion version, final HttpMethod httpMethod, + final String uri, + final TcpClient> client) { + return create(version, httpMethod, uri, client, NO_REDIRECTS); + } + + public static HttpClientRequestImpl create(final RawRequest rawRequest, + final TcpClient> client) { + return new HttpClientRequestImpl<>(rawRequest, client, Collections.emptyList(), + Collections.emptyList()); + } + + public TcpClient> getClient() { + return client; + } + + @SuppressWarnings("unchecked") + private HttpClientRequestImpl _copy(TcpClient> c) { + return _copy(c, (RawRequest)rawRequest); + } + + @SuppressWarnings("unchecked") + private HttpClientRequestImpl _copy(TcpClient> c, + RawRequest rawRequest) { + return new HttpClientRequestImpl<>(rawRequest, c, immutableTransformers, immutableResponseTransformers); + } + + @SuppressWarnings("rawtypes") + private Observable> _writeContentRaw(Observable rawContent, boolean hasTrailers) { + return _writeContentRaw(rawContent, null, hasTrailers); + } + + @SuppressWarnings("rawtypes") + private Observable> _writeContentRaw(Observable rawContent, + Func1 flushSelector, boolean hasTrailers) { + final RawRequest r = RawRequest.create(rawRequest.getHeaders(), rawContent, flushSelector, hasTrailers, + rawRequest.getRedirector()); + return new HttpClientRequestImpl<>(r, client, immutableTransformers, immutableResponseTransformers); + } + + public RawRequest unsafeRawRequest() { + return rawRequest; + } + + private static class OnSubscribeFuncImpl implements OnSubscribe> { + @SuppressWarnings("rawtypes") + private final Observable source; + private final TcpClient> client; + + public OnSubscribeFuncImpl(final TcpClient> client, RawRequest rawRequest, + List responseTransformers, + List requestTransformers) { + this.client = client; + ConnToResponseFunc connToResponseFunc = new ConnToResponseFunc<>(rawRequest, responseTransformers, + requestTransformers); + Observable> source = this.client.createConnectionRequest() + .take(1) + .switchMap(connToResponseFunc); + + if (null != rawRequest.getRedirector()) { + source = source.switchMap(rawRequest.getRedirector()); + } + + this.source = source; + } + + @Override + @SuppressWarnings("unchecked") + public void call(Subscriber> subscriber) { + @SuppressWarnings("rawtypes") + final Subscriber rawSub = subscriber; + source.unsafeSubscribe(rawSub); + } + + } + + private static class ConnToResponseFunc + implements Func1, ?>, Observable>> { + + private final RawRequest rawRequest; + private List responseTransformers; + private List requestTransformers; + + public ConnToResponseFunc(RawRequest rawRequest, List responseTransformers, + List requestTransformers) { + this.rawRequest = rawRequest; + this.responseTransformers = responseTransformers; + this.requestTransformers = requestTransformers; + } + + @Override + public Observable> call(final Connection, ?> conn) { + for (AppendTransformerEvent requestTransformer : requestTransformers) { + conn.unsafeNettyChannel().pipeline().fireUserEventTriggered(requestTransformer); + } + + final Observable> input = conn.getInput(); + + final HttpClientEventsListener eventsListener = + conn.unsafeNettyChannel().attr(HttpChannelProvider.HTTP_CLIENT_EVENT_LISTENER).get(); + final EventPublisher eventPublisher = + conn.unsafeNettyChannel().attr(EventAttributeKeys.EVENT_PUBLISHER).get(); + + return writeRequest(conn).lift(new RequestWriteMetricsOperator(eventsListener, eventPublisher)) + .map(new VoidToAnythingCast>()) + .ignoreElements() + .concatWith(input.take(1)) + .map(new Func1, HttpClientResponse>() { + @SuppressWarnings("unchecked") + @Override + public HttpClientResponse call(HttpClientResponse r) { + HttpClientResponse rp = HttpClientResponseImpl.newInstance(r, conn); + for (Transformer transformer : responseTransformers) { + rp = rp.transformContent(transformer); + } + return (HttpClientResponse) rp; + } + }); + } + + @SuppressWarnings("unchecked") + protected Observable writeRequest(Connection, ?> conn) { + return conn.write(rawRequest.asObservable(conn)); + } + } + + private static class RequestWriteMetricsOperator implements Operator { + + private final EventPublisher eventPublisher; + private final HttpClientEventsListener eventsListener; + + public RequestWriteMetricsOperator(HttpClientEventsListener eventsListener, EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + this.eventsListener = eventsListener; + } + + @Override + public Subscriber call(final Subscriber o) { + final long startTimeNanos = eventPublisher.publishingEnabled() ? Clock.newStartTimeNanos() : -1; + if (eventPublisher.publishingEnabled()) { + eventsListener.onRequestSubmitted(); + } + return new Subscriber(o) { + @Override + public void onCompleted() { + if (eventPublisher.publishingEnabled()) { + eventsListener.onRequestWriteComplete(Clock.onEndNanos(startTimeNanos), NANOSECONDS); + } + o.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (eventPublisher.publishingEnabled()) { + eventsListener.onRequestWriteFailed(Clock.onEndNanos(startTimeNanos), NANOSECONDS, e); + } + o.onError(e); + } + + @Override + public void onNext(Void aVoid) { + o.onNext(aVoid); + } + }; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientResponseImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientResponseImpl.java new file mode 100644 index 0000000..eca0992 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientResponseImpl.java @@ -0,0 +1,353 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.util.ReferenceCountUtil; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ContentSource; +import io.reactivex.netty.protocol.http.CookiesHolder; +import io.reactivex.netty.protocol.http.HttpHandlerNames; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.internal.HttpContentSubscriberEvent; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.sse.client.ServerSentEventDecoder; +import rx.Observable; +import rx.Observable.Transformer; +import rx.Subscriber; +import rx.functions.Func1; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; + +public final class HttpClientResponseImpl extends HttpClientResponse { + + private static final Logger logger = Logger.getLogger(HttpClientResponseImpl.class.getName()); + + public static final String KEEP_ALIVE_HEADER_NAME = "Keep-Alive"; + private static final Pattern PATTERN_COMMA = Pattern.compile(","); + private static final Pattern PATTERN_EQUALS = Pattern.compile("="); + public static final String KEEP_ALIVE_TIMEOUT_HEADER_ATTR = "timeout"; + + private final HttpResponse nettyResponse; + private final Connection connection; + private final CookiesHolder cookiesHolder; + private final ContentSource contentSource; + + private HttpClientResponseImpl(HttpResponse nettyResponse) { + this(nettyResponse, UnusableConnection.create()); + } + + private HttpClientResponseImpl(HttpResponse nettyResponse, Connection connection) { + this.nettyResponse = nettyResponse; + this.connection = connection; + cookiesHolder = CookiesHolder.newClientResponseHolder(nettyResponse.headers()); + contentSource = new ContentSource<>(unsafeNettyChannel(), new ContentSourceSubscriptionFactory()); + } + + private HttpClientResponseImpl(HttpClientResponseImpl toCopy, ContentSource newSource) { + nettyResponse = toCopy.nettyResponse; + connection = toCopy.connection; + cookiesHolder = toCopy.cookiesHolder; + contentSource = newSource; + } + + @Override + public HttpVersion getHttpVersion() { + return nettyResponse.protocolVersion(); + } + + @Override + public HttpResponseStatus getStatus() { + return nettyResponse.status(); + } + + @Override + public Map> getCookies() { + return cookiesHolder.getAllCookies(); + } + + @Override + public boolean containsHeader(CharSequence name) { + return nettyResponse.headers().contains(name); + } + + @Override + public boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue) { + return nettyResponse.headers().contains(name, value, ignoreCaseValue); + } + + @Override + public Iterator> headerIterator() { + return nettyResponse.headers().iteratorCharSequence(); + } + + @Override + public String getHeader(CharSequence name) { + return nettyResponse.headers().get(name); + } + + @Override + public String getHeader(CharSequence name, String defaultValue) { + return nettyResponse.headers().get(name, defaultValue); + } + + @Override + public List getAllHeaderValues(CharSequence name) { + return nettyResponse.headers().getAll(name); + } + + @Override + public long getContentLength() { + return HttpUtil.getContentLength(nettyResponse); + } + + @Override + public long getContentLength(long defaultValue) { + return HttpUtil.getContentLength(nettyResponse, defaultValue); + } + + @Override + public long getDateHeader(CharSequence name) { + return nettyResponse.headers().getTimeMillis(name); + } + + @Override + public long getDateHeader(CharSequence name, long defaultValue) { + return nettyResponse.headers().getTimeMillis(name, defaultValue); + } + + @Override + public String getHostHeader() { + return nettyResponse.headers().get(HOST); + } + + @Override + public String getHost(String defaultValue) { + return nettyResponse.headers().get(HOST, defaultValue); + } + + @Override + public int getIntHeader(CharSequence name) { + return nettyResponse.headers().getInt(name); + } + + @Override + public int getIntHeader(CharSequence name, int defaultValue) { + return nettyResponse.headers().getInt(name, defaultValue); + } + + @Override + public boolean isContentLengthSet() { + return HttpUtil.isContentLengthSet(nettyResponse); + } + + @Override + public boolean isKeepAlive() { + return HttpUtil.isKeepAlive(nettyResponse); + } + + @Override + public boolean isTransferEncodingChunked() { + return HttpUtil.isTransferEncodingChunked(nettyResponse); + } + + @Override + public Set getHeaderNames() { + return nettyResponse.headers().names(); + } + + @Override + public HttpClientResponse addHeader(CharSequence name, Object value) { + nettyResponse.headers().add(name, value); + return this; + } + + @Override + public HttpClientResponse addCookie(Cookie cookie) { + nettyResponse.headers().add(SET_COOKIE, ClientCookieEncoder.STRICT.encode(cookie)); + return this; + } + + @Override + public HttpClientResponse addDateHeader(CharSequence name, Date value) { + nettyResponse.headers().set(name, value); + return this; + } + + @Override + public HttpClientResponse addDateHeader(CharSequence name, Iterable values) { + for (Date value : values) { + nettyResponse.headers().add(name, value); + } + return this; + } + + @Override + public HttpClientResponse addHeader(CharSequence name, Iterable values) { + nettyResponse.headers().add(name, values); + return this; + } + + @Override + public HttpClientResponse setDateHeader(CharSequence name, Date value) { + nettyResponse.headers().set(name, value); + return this; + } + + @Override + public HttpClientResponse setHeader(CharSequence name, Object value) { + nettyResponse.headers().set(name, value); + return this; + } + + @Override + public HttpClientResponse setDateHeader(CharSequence name, Iterable values) { + for (Date value : values) { + nettyResponse.headers().set(name, value); + } + return this; + } + + @Override + public HttpClientResponse setHeader(CharSequence name, Iterable values) { + nettyResponse.headers().set(name, values); + return this; + } + + @Override + public HttpClientResponse removeHeader(CharSequence name) { + nettyResponse.headers().remove(name); + return this; + } + + @Override + public ContentSource getContentAsServerSentEvents() { + if (containsHeader(CONTENT_TYPE) && getHeader(CONTENT_TYPE).startsWith("text/event-stream")) { + ChannelPipeline pipeline = unsafeNettyChannel().pipeline(); + ChannelHandlerContext decoderCtx = pipeline.context(HttpHandlerNames.HttpClientCodec.getName()); + if (null != decoderCtx) { + pipeline.addAfter(decoderCtx.name(), HttpHandlerNames.SseClientCodec.getName(), + new ServerSentEventDecoder()); + } + return new ContentSource<>(unsafeNettyChannel(), new ContentSourceSubscriptionFactory()); + } + + return new ContentSource<>(new IllegalStateException("Response is not a server sent event response.")); + } + + @Override + public ContentSource getContent() { + return contentSource; + } + + @Override + public Observable discardContent() { + return getContent().map(new Func1() { + @Override + public Void call(T t) { + ReferenceCountUtil.release(t); + return null; + } + }).ignoreElements(); + } + + @Override + public HttpClientResponse transformContent(Transformer transformer) { + return new HttpClientResponseImpl<>(this, contentSource.transform(transformer)); + } + + @Override + public Channel unsafeNettyChannel() { + return unsafeConnection().unsafeNettyChannel(); + } + + @Override + public Connection unsafeConnection() { + return connection; + } + + /** + * Parses the timeout value from the HTTP keep alive header (with name {@link #KEEP_ALIVE_HEADER_NAME}) as described in + * this spec + * + * @return The keep alive timeout or {@code null} if this response does not define the appropriate header value. + */ + public Long getKeepAliveTimeoutSeconds() { + String keepAliveHeader = nettyResponse.headers().get(KEEP_ALIVE_HEADER_NAME); + if (null != keepAliveHeader && !keepAliveHeader.isEmpty()) { + String[] pairs = PATTERN_COMMA.split(keepAliveHeader); + if (pairs != null) { + for (String pair: pairs) { + String[] nameValue = PATTERN_EQUALS.split(pair.trim()); + if (nameValue != null && nameValue.length >= 2) { + String name = nameValue[0].trim().toLowerCase(); + String value = nameValue[1].trim(); + if (KEEP_ALIVE_TIMEOUT_HEADER_ATTR.equals(name)) { + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + logger.log(Level.INFO, "Invalid HTTP keep alive timeout value. Keep alive header: " + + keepAliveHeader + ", timeout attribute value: " + nameValue[1], e); + return null; + } + } + } + } + } + } + return null; + } + + /*Visible for the client bridge*/static HttpClientResponseImpl unsafeCreate(HttpResponse nettyResponse) { + return new HttpClientResponseImpl<>(nettyResponse); + } + + public static HttpClientResponse newInstance(HttpClientResponse unsafeInstance, + Connection connection) { + HttpClientResponseImpl cast = (HttpClientResponseImpl) unsafeInstance; + return new HttpClientResponseImpl<>(cast.nettyResponse, connection); + } + + public static HttpClientResponse newInstance(HttpResponse nettyResponse, Connection connection) { + return new HttpClientResponseImpl<>(nettyResponse, connection); + } + + private static class ContentSourceSubscriptionFactory implements Func1, Object> { + @Override + public Object call(Subscriber subscriber) { + return new HttpContentSubscriberEvent<>(subscriber); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientToConnectionBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientToConnectionBridge.java new file mode 100644 index 0000000..ecc72cb --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/HttpClientToConnectionBridge.java @@ -0,0 +1,193 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.AttributeKey; +import io.reactivex.netty.client.ClientConnectionToChannelBridge; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.PooledConnectionReleaseEvent; +import io.reactivex.netty.client.pool.PooledConnection; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import static java.util.concurrent.TimeUnit.*; + +public class HttpClientToConnectionBridge extends AbstractHttpConnectionBridge { + + /** + * This attribute stores the value of any dynamic idle timeout value sent via an HTTP keep alive header. + * This follows the proposal specified here: http://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html + * The attribute can be extracted from an HTTP response header using the helper method + * {@link HttpClientResponseImpl#getKeepAliveTimeoutSeconds()} + */ + public static final AttributeKey KEEP_ALIVE_TIMEOUT_MILLIS_ATTR = + PooledConnection.DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS; + + private HttpClientEventsListener eventsListener; + private EventPublisher eventPublisher; + private String hostHeader; + private long requestWriteCompletionTimeNanos; + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + eventsListener = ctx.channel().attr(HttpChannelProvider.HTTP_CLIENT_EVENT_LISTENER).get(); + eventPublisher = ctx.channel().attr(EventAttributeKeys.EVENT_PUBLISHER).get(); + super.handlerAdded(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + SocketAddress remoteAddr = ctx.channel().remoteAddress(); + if (remoteAddr instanceof InetSocketAddress) { + InetSocketAddress inetSock = (InetSocketAddress) remoteAddr; + String hostString = inetSock.getHostString(); // Don't use hostname that does a DNS lookup. + hostHeader = hostString + ':' + inetSock.getPort(); + } + super.channelActive(ctx); + } + + @Override + protected void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos) { + /*Reset on every request write, we do not currently support pipelining, otherwise, this should be stored in a + queue.*/ + requestWriteCompletionTimeNanos = -1; + if (null != hostHeader) { + if (!httpMsg.headers().contains(HttpHeaderNames.HOST)) { + httpMsg.headers().set(HttpHeaderNames.HOST, hostHeader); + } + } + if (eventPublisher.publishingEnabled()) { + eventsListener.onRequestWriteStart(); + } + } + + @Override + protected void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, + final long headerWriteStartTimeNanos) { + if (eventPublisher.publishingEnabled()) { + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (eventPublisher.publishingEnabled()) { + requestWriteCompletionTimeNanos = Clock.newStartTimeNanos(); + if (future.isSuccess()) { + eventsListener.onRequestWriteComplete(Clock.onEndNanos(headerWriteStartTimeNanos), + NANOSECONDS); + } else { + eventsListener.onRequestWriteFailed(Clock.onEndNanos(headerWriteStartTimeNanos), + NANOSECONDS, future.cause()); + } + } + } + }); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ConnectionReuseEvent) { + resetSubscriptionState(connectionInputSubscriber); + connectionInputSubscriber = null; + } else if (PooledConnectionReleaseEvent.INSTANCE == evt) { + onPooledConnectionRelease(connectionInputSubscriber); + } + super.userEventTriggered(ctx, evt); + } + + @Override + protected void onClosedBeforeReceiveComplete(Channel channel) { + if (channel.isActive()) { + /* + * If the close is triggerred by the user, the channel will be active. + * If the response, isn't complete, then the connection can not be used. + */ + channel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); + } + } + + @Override + protected boolean isInboundHeader(Object nextItem) { + return nextItem instanceof HttpResponse; + } + + @Override + protected boolean isOutboundHeader(Object nextItem) { + return nextItem instanceof HttpRequest; + } + + @Override + protected Object newHttpObject(Object nextItem, Channel channel) { + final HttpResponse nettyResponse = (HttpResponse) nextItem; + + if (eventPublisher.publishingEnabled()) { + long duration = -1; + if (requestWriteCompletionTimeNanos != -1) { + duration = Clock.onEndNanos(requestWriteCompletionTimeNanos); + } + eventsListener.onResponseHeadersReceived(nettyResponse.status().code(), duration, NANOSECONDS); + } + + final HttpClientResponseImpl rxResponse = HttpClientResponseImpl.unsafeCreate(nettyResponse); + Long keepAliveTimeoutSeconds = rxResponse.getKeepAliveTimeoutSeconds(); + if (null != keepAliveTimeoutSeconds) { + channel.attr(KEEP_ALIVE_TIMEOUT_MILLIS_ATTR).set(keepAliveTimeoutSeconds * 1000); + } + + if (!rxResponse.isKeepAlive()) { + channel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); /*Discard connection when done with this response.*/ + } + + return rxResponse; + } + + @Override + protected void onContentReceived() { + if (eventPublisher.publishingEnabled()) { + eventsListener.onResponseContentReceived(); + } + } + + @Override + protected void onContentReceiveComplete(long receiveStartTimeNanos) { + connectionInputSubscriber.onCompleted(); /*Unsubscribe from the input and hence close/release connection*/ + if (eventPublisher.publishingEnabled()) { + long headerWriteStart = getHeaderWriteStartTimeNanos(); + eventsListener.onResponseReceiveComplete(Clock.onEndNanos(receiveStartTimeNanos), NANOSECONDS); + eventsListener.onRequestProcessingComplete(Clock.onEndNanos(headerWriteStart), NANOSECONDS); + } + } + + private void onPooledConnectionRelease(ConnectionInputSubscriber connectionInputSubscriber) { + onChannelClose(connectionInputSubscriber); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/RawRequest.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/RawRequest.java new file mode 100644 index 0000000..1cf1ab3 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/RawRequest.java @@ -0,0 +1,244 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.FlushSelectorOperator; +import rx.Observable; +import rx.functions.Func1; + +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; + +public final class RawRequest { + + private final Redirector redirector; + private final HttpRequest headers; + @SuppressWarnings("rawtypes") + private final Observable content; + private final Func1 flushSelector; + private final boolean hasTrailers; + + @SuppressWarnings("rawtypes") + private RawRequest(HttpRequest headers, Observable content, Func1 flushSelector, boolean hasTrailers, + Redirector redirector) { + this.headers = headers; + this.content = content; + this.flushSelector = flushSelector; + this.hasTrailers = hasTrailers; + this.redirector = redirector; + } + + public RawRequest addHeader(CharSequence name, Object value) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().add(name, value); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest addHeaders(Map> headers) { + HttpRequest headersCopy = _copyHeaders(); + for (Entry> header : headers.entrySet()) { + headersCopy.headers().add(header.getKey(), header.getValue()); + } + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest addHeaderValues(CharSequence name, Iterable values) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().add(name, values); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest addCookie(Cookie cookie) { + String cookieHeader = ClientCookieEncoder.STRICT.encode(cookie); + return addHeader(HttpHeaderNames.COOKIE, cookieHeader); + } + + public RawRequest addDateHeader(CharSequence name, Date value) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().add(name, value); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest addDateHeader(CharSequence name, Iterable values) { + HttpRequest headersCopy = _copyHeaders(); + for (Date value : values) { + headersCopy.headers().add(name, value); + } + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setDateHeader(CharSequence name, Date value) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().set(name, value); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setHeader(CharSequence name, Object value) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().set(name, value); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setHeaders(Map> headers) { + HttpRequest headersCopy = _copyHeaders(); + for (Entry> header : headers.entrySet()) { + headersCopy.headers().set(header.getKey(), header.getValue()); + } + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setHeaderValues(CharSequence name, Iterable values) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().set(name, values); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setDateHeader(CharSequence name, Iterable values) { + HttpRequest headersCopy = _copyHeaders(); + boolean addNow = false; + for (Date value : values) { + if (addNow) { + headersCopy.headers().add(name, value); + } else { + headersCopy.headers().set(name, value); + addNow = true; + } + } + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setKeepAlive(boolean keepAlive) { + HttpRequest headersCopy = _copyHeaders(); + HttpUtil.setKeepAlive(headersCopy, keepAlive); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setTransferEncodingChunked() { + HttpRequest headersCopy = _copyHeaders(); + HttpUtil.setTransferEncodingChunked(headersCopy, true); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest removeHeader(CharSequence name) { + HttpRequest headersCopy = _copyHeaders(); + headersCopy.headers().remove(name); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setMethod(HttpMethod method) { + HttpRequest headersCopy = _copyHeaders(headers.uri(), method); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest setUri(String uri) { + HttpRequest headersCopy = _copyHeaders(uri, headers.method()); + return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector); + } + + public RawRequest followRedirect(Redirector redirectHandler) { + return new RawRequest<>(headers, content, flushSelector, hasTrailers, redirectHandler); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public Observable asObservable(Connection connection) { + HttpRequest headers = this.headers; + if (null == content) { + headers = _copyHeaders(); + headers.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0); + } + + Observable toReturn = Observable.just(headers); + + if (null != content) { + if (null == flushSelector) { + toReturn = toReturn.concatWith(content); + } else { + toReturn = toReturn.concatWith(content.lift(new FlushSelectorOperator(flushSelector, connection))); + } + } + + if (!hasTrailers) { + toReturn = toReturn.concatWith(Observable.just(LastHttpContent.EMPTY_LAST_CONTENT)); + } + + return toReturn; + } + + private HttpRequest _copyHeaders() { + return _copyHeaders(headers.uri(), headers.method()); + } + + private HttpRequest _copyHeaders(String uri, HttpMethod method) { + final HttpRequest newHeaders = new DefaultHttpRequest(headers.protocolVersion(), method, uri); + // TODO: May be we can optimize this by not copying + for (Entry header : headers.headers()) { + newHeaders.headers().set(header.getKey(), header.getValue()); + } + return newHeaders; + } + + public static RawRequest create(HttpVersion version, HttpMethod httpMethod, String uri, + Redirector redirectHandler) { + final HttpRequest headers = new DefaultHttpRequest(version, httpMethod, uri); + return create(headers, null, null, false, redirectHandler); + } + + @SuppressWarnings("rawtypes") + public static RawRequest create(HttpRequest headers, Observable content, boolean hasTrailers, + Redirector redirectHandler) { + return create(headers, content, null, hasTrailers, redirectHandler); + } + + @SuppressWarnings("rawtypes") + public static RawRequest create(HttpRequest headers, Observable content, + Func1 flushSelector, boolean hasTrailers, + Redirector redirectHandler) { + return new RawRequest<>(headers, content, flushSelector, hasTrailers, redirectHandler); + } + + public HttpRequest getHeaders() { + return headers; + } + + @SuppressWarnings("rawtypes") + public Observable getContent() { + return content; + } + + public Func1 getFlushSelector() { + return flushSelector; + } + + public boolean hasTrailers() { + return hasTrailers; + } + + public Redirector getRedirector() { + return redirector; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/Redirector.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/Redirector.java new file mode 100644 index 0000000..8e7974c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/Redirector.java @@ -0,0 +1,201 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.internal.VoidToAnythingCast; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.client.HttpRedirectException; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import rx.Observable; +import rx.functions.Func1; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.reactivex.netty.protocol.http.client.HttpRedirectException.Reason.*; + +public class Redirector implements Func1, Observable>> { + + public static final int DEFAULT_MAX_REDIRECTS = 5; + + private static final Logger logger = Logger.getLogger(Redirector.class.getName()); + + private static final int[] REDIRECTABLE_STATUS_CODES = {301, 302, 303, 307, 308}; + + static { + Arrays.sort(REDIRECTABLE_STATUS_CODES); // Required as we do binary search. This is a safety net in case the + // array is modified (code change) & is not sorted. + } + + private final List visitedLocations; // Is never updated concurrently as redirects are sequential. + private final int maxHops; + private final AtomicInteger redirectCount; // Can be shared across multiple event loops, so needs to be thread-safe. + private volatile HttpResponseStatus lastRedirectStatus; + private final TcpClient> client; + + private RawRequest originalRequest; + + public Redirector(int maxHops, TcpClient> client) { + this.maxHops = maxHops; + this.client = client; + visitedLocations = new ArrayList<>(); + redirectCount = new AtomicInteger(); + } + + public Redirector(TcpClient> client) { + this(DEFAULT_MAX_REDIRECTS, client); + } + + public void setOriginalRequest(RawRequest originalRequest) { + if (null != this.originalRequest) { + throw new IllegalStateException("Original request is already set."); + } + this.originalRequest = originalRequest; + visitedLocations.add(originalRequest.getHeaders().uri()); + } + + @Override + public Observable> call(HttpClientResponse response) { + + Observable> toReturn; + + if (null == originalRequest) { + toReturn = Observable.error(new IllegalStateException("Raw request not available to the redirector.")); + } else if (requiresRedirect(response)) { + String location = extractRedirectLocation(response); + + if (location == null) { + toReturn = Observable.error(new HttpRedirectException(InvalidRedirect, "No redirect location found.")); + } else if (visitedLocations.contains(location)) { + // this forms a loop + toReturn = Observable.error(new HttpRedirectException(RedirectLoop, + "Redirection contains a loop. Last requested location: " + + location)); + } else if (redirectCount.get() >= maxHops) { + toReturn = Observable.error(new HttpRedirectException(TooManyRedirects, + "Too many redirects. Max redirects: " + maxHops)); + } else { + URI redirectUri; + + try { + redirectUri = new URI(location); + + lastRedirectStatus = response.getStatus(); + + redirectCount.incrementAndGet(); + + toReturn = createRedirectRequest(originalRequest, redirectUri, lastRedirectStatus.code()); + } catch (Exception e) { + toReturn = Observable.error(new HttpRedirectException(InvalidRedirect, + "Location is not a valid URI. Provided location: " + + location, e)); + } + } + + } else { + return Observable.just(response); + } + + return response.discardContent() + .map(new VoidToAnythingCast>()) + .ignoreElements() + .concatWith(toReturn); + + + } + + public boolean requiresRedirect(HttpClientResponse response) { + int statusCode = response.getStatus().code(); + boolean requiresRedirect = false; + // This class only supports relative redirects as an HttpClient is always tied to a host:port combo and hence + // can not do an absolute redirect. + if (Arrays.binarySearch(REDIRECTABLE_STATUS_CODES, statusCode) >= 0) { + String location = extractRedirectLocation(response); + // Only process relative URIs: Issue https://github.com/ReactiveX/RxNetty/issues/270 + requiresRedirect = null == location || !location.startsWith("http"); + } + + if (requiresRedirect && statusCode != HttpResponseStatus.SEE_OTHER.code()) { + HttpMethod originalMethod = originalRequest.getHeaders().method(); + // If the Method is not HEAD/GET do not auto redirect + requiresRedirect = originalMethod == HttpMethod.GET || originalMethod == HttpMethod.HEAD; + } + + return requiresRedirect; + } + + protected String extractRedirectLocation(HttpClientResponse redirectedResponse) { + return redirectedResponse.getHeader(HttpHeaderNames.LOCATION); + } + + protected HttpClientRequest createRedirectRequest(RawRequest original, URI redirectLocation, + int redirectStatus) { + + String redirectUri = getNettyRequestUri(redirectLocation, original.getHeaders().uri(), redirectStatus); + + RawRequest redirectRequest = original.setUri(redirectUri); + + if (redirectStatus == 303) { + // according to HTTP spec, 303 mandates the change of request type to GET + // If it is a get, then the content is not to be sent. + redirectRequest = RawRequest.create(redirectRequest.getHeaders().protocolVersion(), HttpMethod.GET, + redirectUri, this); + } + + return HttpClientRequestImpl.create(redirectRequest, client); + } + + protected static String getNettyRequestUri(URI uri, String originalUriString, int redirectStatus) { + StringBuilder sb = new StringBuilder(); + if (uri.getRawPath() != null) { + sb.append(uri.getRawPath()); + } + if (uri.getRawQuery() != null) { + sb.append('?').append(uri.getRawQuery()); + } + if (uri.getRawFragment() != null) { + sb.append('#').append(uri.getRawFragment()); + } else if(redirectStatus >= 300) { + // http://tools.ietf.org/html/rfc7231#section-7.1.2 suggests that the URI fragment should be carried over to + // the redirect location if not exists in the redirect location. + // Issue: https://github.com/ReactiveX/RxNetty/issues/271 + try { + URI originalUri = new URI(originalUriString); + if (originalUri.getRawFragment() != null) { + sb.append('#').append(originalUri.getRawFragment()); + } + } catch (URISyntaxException e) { + logger.log(Level.WARNING, "Error parsing original request URI during redirect. " + + "This means that the path fragment if any in the original request will not be inherited " + + "by the redirect.", e); + } + } + return sb.toString(); + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/UnusableConnection.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/UnusableConnection.java new file mode 100644 index 0000000..9a105bf --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/internal/UnusableConnection.java @@ -0,0 +1,194 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.FileRegion; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.events.EventPublisher; +import rx.Observable; +import rx.Observable.Transformer; +import rx.functions.Action1; +import rx.functions.Func1; + +final class UnusableConnection extends Connection { + + protected UnusableConnection(Channel nettyChannel, + ConnectionEventListener eventListener, + EventPublisher eventPublisher) { + super(nettyChannel); + } + + @Override + public Observable write(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable write(Observable msgs, Func1 flushSelector) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeAndFlushOnEach(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeString(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeString(Observable msgs, Func1 flushSelector) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeStringAndFlushOnEach(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeBytes(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeBytes(Observable msgs, Func1 flushSelector) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeBytesAndFlushOnEach(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeFileRegion(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeFileRegion(Observable msgs, Func1 flushSelector) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable writeFileRegionAndFlushOnEach(Observable msgs) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public void flush() { + throw new IllegalStateException("Connection is not usable."); + } + + @Override + public Observable close() { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public Observable close(boolean flush) { + return Observable.error(new IllegalStateException("Connection is not usable.")); + } + + @Override + public void closeNow() { + throw new IllegalStateException("Connection is not usable."); + } + + @Override + public Observable closeListener() { + throw new IllegalStateException("Connection is not usable."); + } + + public static Connection create() { + return new UnusableConnection<>(new EmbeddedChannel(), null, null); + } + + @Override + public Connection addChannelHandlerFirst(String name, ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerFirst(EventExecutorGroup group, String name, + ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerLast(String name, ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerLast(EventExecutorGroup group, String name, + ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerBefore(String baseName, String name, ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerAfter(String baseName, String name, ChannelHandler handler) { + return cast(); + } + + @Override + public Connection addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + ChannelHandler handler) { + return cast(); + } + + @Override + public Connection pipelineConfigurator(Action1 pipelineConfigurator) { + return cast(); + } + + @Override + public Connection transformRead(Transformer transformer) { + throw new IllegalStateException("Connection is not usable."); + } + + @Override + public Connection transformWrite(AllocatingTransformer transformer) { + throw new IllegalStateException("Connection is not usable."); + } + + @SuppressWarnings("unchecked") + private Connection cast() { + return (Connection) this; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/loadbalancer/EWMABasedP2CStrategy.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/loadbalancer/EWMABasedP2CStrategy.java new file mode 100644 index 0000000..e2ce4dc --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/client/loadbalancer/EWMABasedP2CStrategy.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client.loadbalancer; + +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.client.loadbalancer.AbstractP2CStrategy; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import io.reactivex.netty.protocol.http.client.loadbalancer.EWMABasedP2CStrategy.HttpClientListenerImpl; + +import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class EWMABasedP2CStrategy extends AbstractP2CStrategy { + private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; + private final double tauUp; + private final double tauDown; + private double penaltyOnConnectionFailure; + private double penaltyOn503; + + public EWMABasedP2CStrategy(double tauUp, double tauDown, double penaltyOnConnectionFailure, + double penaltyOn503) { + this.tauUp = tauUp; + this.tauDown = tauDown; + this.penaltyOnConnectionFailure = penaltyOnConnectionFailure; + this.penaltyOn503 = penaltyOn503; + } + + public EWMABasedP2CStrategy() { + this(NANOSECONDS.convert(1, SECONDS), NANOSECONDS.convert(15, SECONDS), 2, 5); + } + + @Override + protected HttpClientListenerImpl newListener(Host host) { + return new HttpClientListenerImpl(); + } + + @Override + protected double getWeight(ClientEventListener listener) { + return ((HttpClientListenerImpl) listener).getWeight(); + } + + public class HttpClientListenerImpl extends HttpClientEventsListener { + private final long epoch = System.nanoTime(); + private long stamp = epoch; // last timestamp in nanos we observed an rtt + private int pending = 0; // instantaneous rate + private double cost = 0.0; // ewma of rtt, sensitive to peaks. + + public double getWeight() { + observe(0.0); + if (cost == 0.0 && pending != 0) { + return STARTUP_PENALTY + pending; + } else { + return cost * (pending+1); + } + } + + @Override + public synchronized void onRequestWriteComplete(long duration, TimeUnit timeUnit) { + pending += 1; + } + + @Override + public synchronized void onResponseReceiveComplete(long duration, TimeUnit timeUnit) { + pending -= 1; + observe(NANOSECONDS.convert(duration, timeUnit)); + } + + @Override + public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { + if (responseCode == 503) { + observe(TimeUnit.NANOSECONDS.convert(duration, timeUnit) * penaltyOn503); + } + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + observe(TimeUnit.NANOSECONDS.convert(duration, timeUnit) * penaltyOnConnectionFailure); + } + + private void observe(double rtt) { + long t = System.nanoTime(); + long td = Math.max(t - stamp, 0L); + if (rtt > cost) { + double w = Math.exp(-td / tauUp); + cost = cost * w + rtt * (1.0 - w); + } else { + double w = Math.exp(-td / tauDown); + cost = cost * w + rtt * (1.0 - w); + } + stamp = t; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridge.java new file mode 100644 index 0000000..d6a4278 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridge.java @@ -0,0 +1,555 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.RecyclableArrayList; +import io.reactivex.netty.channel.AppendTransformerEvent; +import io.reactivex.netty.channel.ConnectionInputSubscriberEvent; +import io.reactivex.netty.channel.ConnectionInputSubscriberReplaceEvent; +import io.reactivex.netty.channel.SubscriberToChannelFutureBridge; +import io.reactivex.netty.channel.WriteTransformations; +import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge.State.Stage; +import rx.Producer; +import rx.Subscriber; +import rx.functions.Action0; +import rx.subscriptions.Subscriptions; + +import java.nio.channels.ClosedChannelException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; + +public abstract class AbstractHttpConnectionBridge extends ChannelDuplexHandler { + + private static final Logger logger = Logger.getLogger(AbstractHttpConnectionBridge.class.getName()); + + public static final AttributeKey CONNECTION_UPGRADED = + AttributeKey.valueOf("rxnetty_http_upgraded_connection"); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED = + new IllegalStateException("Only one subscriber allowed for HTTP content."); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException LAZY_CONTENT_INPUT_SUB = + new IllegalStateException("Channel is set to auto-read but the subscription was lazy."); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final IllegalStateException CONTENT_ARRIVED_WITH_NO_SUB = + new IllegalStateException("HTTP Content received but no subscriber was registered."); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + + static { + ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + LAZY_CONTENT_INPUT_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + CONTENT_ARRIVED_WITH_NO_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + + protected ConnectionInputSubscriber connectionInputSubscriber; + private final UnsafeEmptySubscriber emptyContentSubscriber; + private final WriteTransformations transformations; + private long headerWriteStartTimeNanos; + + protected AbstractHttpConnectionBridge() { + emptyContentSubscriber = new UnsafeEmptySubscriber<>("Error while waiting for HTTP content."); + transformations = new WriteTransformations(); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + + boolean skipNextHandlers = false; + if (isOutboundHeader(msg)) { + /*Reset on every header write, when we support pipelining, this should be a queue.*/ + headerWriteStartTimeNanos = Clock.newStartTimeNanos(); + HttpMessage httpMsg = (HttpMessage) msg; + if (!HttpUtil.isContentLengthSet(httpMsg) && !HttpVersion.HTTP_1_0.equals(httpMsg.protocolVersion())) { + // If there is no content length we need to specify the transfer encoding as chunked as we always + // send data in multiple HttpContent. + // On the other hand, if someone wants to not have chunked encoding, adding content-length will work + // as expected. + httpMsg.headers().set(TRANSFER_ENCODING, CHUNKED); + } + + beforeOutboundHeaderWrite(httpMsg, promise, headerWriteStartTimeNanos); + + } else if (msg instanceof LastHttpContent) { + onOutboundLastContentWrite((LastHttpContent) msg, promise, headerWriteStartTimeNanos); + } else if (transformations.acceptMessage(msg)) { + RecyclableArrayList out = RecyclableArrayList.newInstance(); + try { + transformations.transform(msg, ctx.alloc(), out); + } finally { + final int sizeMinusOne = out.size() - 1; + if (sizeMinusOne == 0) { + ctx.write(out.get(0), promise); + } else if (sizeMinusOne > 0) { + ChannelPromise voidPromise = ctx.voidPromise(); + boolean isVoidPromise = promise == voidPromise; + for (int i = 0; i < sizeMinusOne; i ++) { + ChannelPromise p; + if (isVoidPromise) { + p = voidPromise; + } else { + p = ctx.newPromise(); + } + ctx.write(out.get(i), p); + } + ctx.write(out.get(sizeMinusOne), promise); + } + out.recycle(); + skipNextHandlers = true; + } + } + + if (!skipNextHandlers) { + super.write(ctx, msg, promise); + } + } + + protected abstract void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos); + + protected abstract void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, + long headerWriteStartTimeNanos); + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception { + + Object eventToPropagateFurther = evt; + Boolean connUpgradedAttr = ctx.channel().attr(CONNECTION_UPGRADED).get(); + boolean connUpgraded = null != connUpgradedAttr ? connUpgradedAttr : false; + + if (evt instanceof ConnectionInputSubscriberEvent) { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + ConnectionInputSubscriberEvent orig = (ConnectionInputSubscriberEvent) evt; + + if (!connUpgraded) { + /*Local copy to refer from the channel close listener. As the instance level copy can change*/ + @SuppressWarnings("unchecked") + final ConnectionInputSubscriber _connectionInputSubscriber = newConnectionInputSubscriber(orig, + ctx.channel()); + + connectionInputSubscriber = _connectionInputSubscriber; + + final SubscriberToChannelFutureBridge l = new SubscriberToChannelFutureBridge() { + + @Override + protected void doOnSuccess(ChannelFuture future) { + onChannelClose(_connectionInputSubscriber); + } + + @Override + protected void doOnFailure(ChannelFuture future, Throwable cause) { + onChannelClose(_connectionInputSubscriber); + } + }; + + l.bridge(ctx.channel().closeFuture(), _connectionInputSubscriber); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + ConnectionInputSubscriberEvent newEvent = new ConnectionInputSubscriberEvent(_connectionInputSubscriber + ); + eventToPropagateFurther = newEvent; + } else { + if (null != connectionInputSubscriber) { + connectionInputSubscriber.state.stage = Stage.Upgraded; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + ConnectionInputSubscriberReplaceEvent replaceEvt = new ConnectionInputSubscriberReplaceEvent<>(orig); + eventToPropagateFurther = replaceEvt; + } + } else if (evt instanceof HttpContentSubscriberEvent) { + newHttpContentSubscriber(evt, connectionInputSubscriber); + } else if (evt instanceof AppendTransformerEvent) { + transformations.appendTransformer(((AppendTransformerEvent)evt).getTransformer()); + } else if (evt instanceof ConnectionReuseEvent) { + transformations.resetTransformations(); + } + + super.userEventTriggered(ctx, eventToPropagateFurther); + } + + protected ConnectionInputSubscriber newConnectionInputSubscriber(ConnectionInputSubscriberEvent orig, + Channel channel) { + ConnectionInputSubscriber toReturn = new ConnectionInputSubscriber(orig.getSubscriber(), channel); + toReturn.state.headerSub.add(Subscriptions.create(toReturn)); + return toReturn; + } + + protected final void onChannelClose(ConnectionInputSubscriber connectionInputSubscriber) { + /* + * If any of the subscribers(header or content) are still subscribed and the channel is closed, it is an + * error. If they are unsubscribed, this will be a no-op. + */ + connectionInputSubscriber.onError(CLOSED_CHANNEL_EXCEPTION); + } + + protected void onClosedBeforeReceiveComplete(Channel channel) { + // No Op. Override to add behavior + } + + protected void resetSubscriptionState(final ConnectionInputSubscriber connectionInputSubscriber) { + connectionInputSubscriber.resetSubscribers(); + } + + protected abstract boolean isInboundHeader(Object nextItem); + + protected abstract boolean isOutboundHeader(Object nextItem); + + protected abstract Object newHttpObject(Object nextItem, Channel channel); + + protected abstract void onContentReceived(); + + protected abstract void onContentReceiveComplete(long receiveStartTimeNanos); + + protected void onNewContentSubscriber(ConnectionInputSubscriber inputSubscriber, Subscriber newSub) { + // No Op. + } + + protected long getHeaderWriteStartTimeNanos() { + return headerWriteStartTimeNanos; + } + + private void processNextItemInEventloop(Object nextItem, ConnectionInputSubscriber connectionInputSubscriber) { + final State state = connectionInputSubscriber.state; + final Channel channel = connectionInputSubscriber.channel; + + if (isInboundHeader(nextItem)) { + state.headerReceived(); + Object newHttpObject = newHttpObject(nextItem, channel); + connectionInputSubscriber.nextHeader(newHttpObject); + /*Why not complete the header sub? It may be listening to multiple responses (pipelining)*/ + checkEagerSubscriptionIfConfigured(channel, state); + + final HttpObject httpObject = (HttpObject) nextItem; + if (httpObject.decoderResult().isFailure()) { + connectionInputSubscriber.onError(httpObject.decoderResult().cause()); + channel.close();// Netty rejects all data after decode failure, so closing connection + // Issue: https://github.com/netty/netty/issues/3362 + } + } + + if (nextItem instanceof HttpContent) { + onContentReceived(); + ByteBuf content = ((ByteBufHolder) nextItem).content(); + if (nextItem instanceof LastHttpContent) { + /* + * Since, LastHttpContent is always received, even if the pipeline does not emit ByteBuf, if + * ByteBuf with the LastHttpContent is empty, only trailing headers are emitted. Otherwise, + * the content type should be a ByteBuf. + */ + if (content.isReadable()) { + connectionInputSubscriber.nextContent(content); + } else { + /*Since, the content buffer, was not sent, release it*/ + ReferenceCountUtil.release(content); + } + state.contentComplete(); + connectionInputSubscriber.contentComplete(); + onContentReceiveComplete(state.headerReceivedTimeNanos); + } else { + connectionInputSubscriber.nextContent(content); + } + } else if(!isInboundHeader(nextItem)){ + connectionInputSubscriber.nextContent(nextItem); + } + } + + private void newHttpContentSubscriber(final Object evt, final ConnectionInputSubscriber inputSubscriber) { + @SuppressWarnings("unchecked") + HttpContentSubscriberEvent contentSubscriberEvent = (HttpContentSubscriberEvent) evt; + Subscriber newSub = contentSubscriberEvent.getSubscriber(); + Throwable errorToRaise = null; + + if (null == inputSubscriber) { + errorToRaise = new NullPointerException("Null Connection input subscriber."); + } else { + final State state = inputSubscriber.state; + + if (state.raiseErrorOnInputSubscription()) { + errorToRaise = state.raiseErrorOnInputSubscription; + } else if (isValidToEmit(state.contentSub)) { + /*Allow only one concurrent input subscriber but allow concatenated subscribers*/ + if (!newSub.isUnsubscribed()) { + errorToRaise = ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED; + } + } else if (state.stage == Stage.HeaderReceived) { + inputSubscriber.setupContentSubscriber(newSub); + onNewContentSubscriber(inputSubscriber, newSub); + } else { + errorToRaise = new IllegalStateException("Content subscription received without request start."); + } + } + + if (null != errorToRaise && isValidToEmit(newSub)) { + newSub.onError(errorToRaise); + } + } + + private void checkEagerSubscriptionIfConfigured(Channel channel, final State state) { + if (channel.config().isAutoRead()) { + if (null == state.contentSub) { + // If the channel is set to auto-read and there is no eager subscription then, we should raise errors + // when a subscriber arrives. + state.raiseErrorOnInputSubscription = LAZY_CONTENT_INPUT_SUB; + state.contentSub = emptyContentSubscriber; + } + } + } + + private static boolean isValidToEmit(Subscriber subscriber) { + return null != subscriber && !subscriber.isUnsubscribed(); + } + + /** + * All state for this handler. At any point we need to invoke any method outside of this handler, this state should + * be stored in a local variable and used after the external call finishes. Failure to do so will cause race + * conditions in us using different state before and after the method call specifically if the external call ends + * up generating a user generated event and triggering {@link #userEventTriggered(ChannelHandlerContext, Object)} + * which in turn changes this state. + * + * Issue: https://github.com/Netflix/RxNetty/issues/129 + */ + protected static final class State { + + /*Visible for testing*/enum Stage { + /*Strictly in the order in which the transitions would happen*/ + Created, + HeaderReceived, + ContentComplete, + Upgraded + } + + protected IllegalStateException raiseErrorOnInputSubscription; + @SuppressWarnings("rawtypes") private Subscriber headerSub; + @SuppressWarnings("rawtypes") private Subscriber contentSub; + private long headerReceivedTimeNanos; + + private volatile Stage stage = Stage.Created; + + /*Visible for testing*/void headerReceived() { + headerReceivedTimeNanos = Clock.newStartTimeNanos(); + stage = Stage.HeaderReceived; + } + + private void contentComplete() { + stage = Stage.ContentComplete; + } + + public boolean raiseErrorOnInputSubscription() { + return null != raiseErrorOnInputSubscription; + } + + public boolean startButNotCompleted() { + return stage == Stage.HeaderReceived; + } + + public boolean receiveStarted() { + return stage.ordinal() > Stage.Created.ordinal(); + } + + /*Visible for testing*/Subscriber getHeaderSub() { + return headerSub; + } + + /*Visible for testing*/Subscriber getContentSub() { + return contentSub; + } + } + + protected class ConnectionInputSubscriber extends Subscriber implements Action0, Runnable { + + private final Channel channel; + private final State state; + private Producer producer; + + @SuppressWarnings("rawtypes") + private ConnectionInputSubscriber(Subscriber subscriber, Channel channel) { + state = new State(); + this.channel = channel; + state.headerSub = subscriber; + } + + @Override + public void onCompleted() { + // This means channel input has completed + if (state.startButNotCompleted()) { + onError(CLOSED_CHANNEL_EXCEPTION); + } else { + completeAllSubs(); + } + } + + @Override + public void onError(Throwable e) { + // This means channel input has got an error & hence no other notifications will arrive. + errorAllSubs(e); + + if (state.startButNotCompleted()) { + onClosedBeforeReceiveComplete(channel); + } + } + + @Override + public void onNext(final Object next) { + if (channel.eventLoop().inEventLoop()) { + processNextItemInEventloop(next, this); + } else { + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + processNextItemInEventloop(next, ConnectionInputSubscriber.this); + } + }); + } + } + + @Override + public void setProducer(Producer producer) { + this.producer = producer; + state.headerSub.setProducer(producer); /*Content & trailer producers are set on subscription*/ + } + + public Channel getChannel() { + return channel; + } + + public void resetSubscribers() { + completeAllSubs(); + } + + private void completeAllSubs() { + if (isValidToEmit(state.headerSub)) { + state.headerSub.onCompleted(); + } + if (isValidToEmit(state.contentSub)) { + state.contentSub.onCompleted(); + } + } + + private void errorAllSubs(Throwable throwable) { + if (isValidToEmit(state.headerSub)) { + state.headerSub.onError(throwable); + } + if (isValidToEmit(state.contentSub)) { + state.contentSub.onError(throwable); + } + } + + @SuppressWarnings("unchecked") + private void nextContent(final Object nextObject) { + if (isValidToEmit(state.contentSub)) { + state.contentSub.onNext(nextObject); + } else { + contentArrivedWhenSubscriberNotValid(); + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Data received on channel, but no subscriber registered. Discarding data. Message class: " + + nextObject.getClass().getName() + ", channel: " + channel); + } + ReferenceCountUtil.release(nextObject); + } + } + + @SuppressWarnings("unchecked") + private void nextHeader(final Object nextObject) { + if (isValidToEmit(state.headerSub)) { + state.headerSub.onNext(nextObject); + } + } + + private void setupContentSubscriber(Subscriber newSub) { + + assert channel.eventLoop().inEventLoop(); + + state.contentSub = newSub; + state.contentSub.add(Subscriptions.create(this)); + state.contentSub.setProducer(producer); /*Content demand matches upstream demand*/ + } + + public void contentComplete() { + assert channel.eventLoop().inEventLoop(); + + if (isValidToEmit(state.contentSub)) { + state.contentSub.onCompleted(); + } else { + contentArrivedWhenSubscriberNotValid(); + } + } + + private void contentArrivedWhenSubscriberNotValid() { + if (null == state.contentSub) { + /* + * Cases when auto-read is off and there is lazy subscription, due to mismatched request demands on the + * subscriber, it may so happen that we get content without a subscriber, in such cases, we should raise + * an error. + */ + state.raiseErrorOnInputSubscription = CONTENT_ARRIVED_WITH_NO_SUB; + } + } + + /*Visible for testing*/State getState() { + return state; + } + + @Override + public void run() { + if (state.contentSub != null) { + if (state.contentSub.isUnsubscribed()) { + // Content sub exists and unsubscribed, so unsubscribe from input. + unsubscribe(); + } else if (state.headerSub.isUnsubscribed() && !state.receiveStarted()) { + // Header sub unsubscribed before request started, unsubscribe from input. + unsubscribe(); + } + } else if (state.headerSub.isUnsubscribed() && !state.receiveStarted()) { + // Header sub unsubscribed before request started, unsubscribe from input. + unsubscribe(); + } + } + + @Override + public void call() { + if (channel.eventLoop().inEventLoop()) { + run(); + } else { + channel.eventLoop().execute(this); + } + } + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpContentSubscriberEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpContentSubscriberEvent.java new file mode 100644 index 0000000..7f2a93b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpContentSubscriberEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.internal; + +import rx.Subscriber; + +public class HttpContentSubscriberEvent { + + private final Subscriber subscriber; + + public HttpContentSubscriberEvent(Subscriber subscriber) { + this.subscriber = subscriber; + } + + public Subscriber getSubscriber() { + return subscriber; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpMessageFormatter.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpMessageFormatter.java new file mode 100644 index 0000000..a733546 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/HttpMessageFormatter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.internal; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import java.util.Iterator; +import java.util.Map.Entry; + +public final class HttpMessageFormatter { + + private HttpMessageFormatter() { + } + + public static String formatRequest(HttpVersion version,HttpMethod method, String uri, + Iterator> headers) { + StringBuilder builder = new StringBuilder(); + builder.append(method) + .append(' ') + .append(uri) + .append(' ') + .append(version.text()) + .append('\n'); + + printHeaders(headers, builder); + + return builder.toString(); + } + + public static String formatResponse(HttpVersion version, HttpResponseStatus status, + Iterator> headers) { + StringBuilder builder = new StringBuilder(); + builder.append(version.text()) + .append(' ') + .append(status.code()) + .append(' ') + .append(status.reasonPhrase()) + .append('\n'); + + printHeaders(headers, builder); + + return builder.toString(); + } + + private static void printHeaders(Iterator> headers, StringBuilder builder) { + while (headers.hasNext()) { + Entry next = headers.next(); + builder.append(next.getKey()) + .append(": ") + .append(next.getValue()) + .append('\n'); + } + + builder.append('\n'); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/OperatorTrailer.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/OperatorTrailer.java new file mode 100644 index 0000000..095ac5a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/OperatorTrailer.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.internal; + +import io.reactivex.netty.protocol.http.TrailingHeaders; +import rx.Observable; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; +import rx.functions.Func0; +import rx.functions.Func2; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class OperatorTrailer implements Operator { + + private final Func0 trailerFactory; + private final Func2 trailerMutator; + + public OperatorTrailer(Func0 trailerFactory, Func2 trailerMutator) { + this.trailerFactory = trailerFactory; + this.trailerMutator = trailerMutator; + } + + @Override + public Object call(Object child) { + final Subscriber subscriber = (Subscriber) child; + return new Subscriber(subscriber) { + + private T trailer = trailerFactory.call(); + + @SuppressWarnings("unchecked") + @Override + public void onCompleted() { + subscriber.onNext(trailer); + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(Object i) { + try { + trailer = (T) trailerMutator.call(trailer, i); + subscriber.onNext(i); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(OnErrorThrowable.addValueAsLastCause(e, i)); + } + } + }; + } + + @SuppressWarnings("unchecked") + public static Observable liftFrom(Observable source, + Func0 trailerFactory, Func2 trailerMutator) { + return source.lift(new OperatorTrailer<>(trailerFactory, trailerMutator)); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/UnsafeEmptySubscriber.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/UnsafeEmptySubscriber.java new file mode 100644 index 0000000..b4d7db7 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/internal/UnsafeEmptySubscriber.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.internal; + +import io.netty.util.ReferenceCountUtil; +import rx.Subscriber; +import rx.observers.SafeSubscriber; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A subscriber that can be reused if and only if not wrapped in a {@link SafeSubscriber}. + */ +final class UnsafeEmptySubscriber extends Subscriber { + + private static final Logger logger = Logger.getLogger(UnsafeEmptySubscriber.class.getName()); + + private final String msg; + + protected UnsafeEmptySubscriber(String msg) { + this.msg = msg; + } + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + logger.log(Level.SEVERE, msg, e); + } + + @Override + public void onNext(T o) { + ReferenceCountUtil.release(o); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ContentWriterImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ContentWriterImpl.java new file mode 100644 index 0000000..bd9a18c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ContentWriterImpl.java @@ -0,0 +1,218 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.reactivex.netty.channel.ChannelOperations; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.FlushSelectorOperator; +import io.reactivex.netty.protocol.http.TrailingHeaders; +import io.reactivex.netty.protocol.http.internal.OperatorTrailer; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; + +final class ContentWriterImpl extends ResponseContentWriter { + + @SuppressWarnings("rawtypes") + private final Connection connection; + + @SuppressWarnings("rawtypes") + private final Observable headersObservable; + @SuppressWarnings("rawtypes") + private final Observable contentObservable; + + private final HttpResponse headers; + + private final Func1 flushOnEachSelector = new Func1() { + @Override + public Boolean call(C w) { + return true; + } + }; + + ContentWriterImpl(@SuppressWarnings("rawtypes") final Connection connection, final HttpResponse headers) { + super(new OnSubscribe() { + @SuppressWarnings("unchecked") + @Override + public void call(Subscriber subscriber) { + /*We are never sending content as the subscription is to the headers only writer.*/ + if (!HttpUtil.isTransferEncodingChunked(headers)) { + headers.headers().set(CONTENT_LENGTH, 0); + } + connection.write(Observable.just(headers)).unsafeSubscribe(subscriber); + } + }); + this.connection = connection; + this.headers = headers; + headersObservable = Observable.just(headers); + contentObservable = null; + } + + private ContentWriterImpl(final ContentWriterImpl parent, + @SuppressWarnings("rawtypes") final Observable content, final boolean appendTrailer) { + super(new OnSubscribe() { + @SuppressWarnings("unchecked") + @Override + public void call(Subscriber subscriber) { + parent.connection.write(getHttpStream(parent, content, appendTrailer)) + .unsafeSubscribe(subscriber); + } + }); + connection = parent.connection; + headers = parent.headers; + headersObservable = parent.headersObservable; + if (null == parent.contentObservable) { + contentObservable = content; + } else { + @SuppressWarnings({"rawtypes", "unchecked"}) + Observable rawMerged = parent.contentObservable.mergeWith(content); + contentObservable = rawMerged; + } + } + + @Override + public ResponseContentWriter write(Observable msgs) { + return new ContentWriterImpl<>(this, msgs, true); + } + + @Override + public Observable write(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return new ContentWriterImpl<>(this, OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), + false); + } + + @Override + public Observable write(Observable contentSource, Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return write(contentSource.lift(new FlushSelectorOperator<>(flushSelector, connection)), + trailerFactory, trailerMutator); + } + + @Override + public ResponseContentWriter write(Observable msgs, final Func1 flushSelector) { + return new ContentWriterImpl<>(this, msgs.lift(new FlushSelectorOperator<>(flushSelector, connection)), + true); + } + + @Override + public ResponseContentWriter writeAndFlushOnEach(Observable msgs) { + return write(msgs, flushOnEachSelector); + } + + @Override + public ResponseContentWriter writeString(Observable msgs) { + return new ContentWriterImpl<>(this, msgs, true); + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return new ContentWriterImpl<>(this, OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), + false); + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource.lift(new FlushSelectorOperator<>(flushSelector, connection)); + return new ContentWriterImpl<>(this, OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), + false); + } + + @Override + public ResponseContentWriter writeString(Observable msgs, Func1 flushSelector) { + return new ContentWriterImpl<>(this, msgs.lift(new FlushSelectorOperator<>(flushSelector, connection)), + true); + } + + @Override + public ResponseContentWriter writeStringAndFlushOnEach(Observable msgs) { + return writeString(msgs, ChannelOperations.FLUSH_ON_EACH_STRING); + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs) { + return new ContentWriterImpl<>(this, msgs, true); + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource; + return new ContentWriterImpl<>(this, OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), + false); + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + @SuppressWarnings("rawtypes") + Observable rawObservable = contentSource.lift(new FlushSelectorOperator<>(flushSelector, connection)); + return new ContentWriterImpl<>(this, OperatorTrailer.liftFrom(rawObservable, trailerFactory, trailerMutator), + false); + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs, Func1 flushSelector) { + return new ContentWriterImpl<>(this, msgs.lift(new FlushSelectorOperator<>(flushSelector, connection)), + true); + } + + @Override + public ResponseContentWriter writeBytesAndFlushOnEach(Observable msgs) { + return writeBytes(msgs, ChannelOperations.FLUSH_ON_EACH_BYTES); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static Observable getHttpStream(final ContentWriterImpl parent, Observable content, boolean appendTrailer) { + Observable httpStream = parent.headersObservable; + if (null != parent.contentObservable) { + httpStream = httpStream.concatWith(parent.contentObservable.mergeWith(content)); + } else { + httpStream = httpStream.concatWith(content); + } + + if (appendTrailer) { + httpStream = httpStream.concatWith(Observable.just(LastHttpContent.EMPTY_LAST_CONTENT)); + } + + return httpStream; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/FailedContentWriter.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/FailedContentWriter.java new file mode 100644 index 0000000..c367107 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/FailedContentWriter.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.reactivex.netty.protocol.http.TrailingHeaders; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +class FailedContentWriter extends ResponseContentWriter { + + FailedContentWriter() { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.onError(new IllegalStateException("HTTP headers are already sent.")); + } + }); + } + + @Override + public ResponseContentWriter write(Observable msgs) { + return this; + } + + @Override + public Observable write(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + return this; + } + + @Override + public Observable write(Observable contentSource, Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter write(Observable msgs, Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter writeAndFlushOnEach(Observable msgs) { + return this; + } + + @Override + public ResponseContentWriter writeString(Observable msgs) { + return this; + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + return this; + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter writeString(Observable msgs, Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter writeStringAndFlushOnEach(Observable msgs) { + return this; + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs) { + return this; + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + return this; + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs, Func1 flushSelector) { + return this; + } + + @Override + public ResponseContentWriter writeBytesAndFlushOnEach(Observable msgs) { + return this; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpConnectionHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpConnectionHandler.java new file mode 100644 index 0000000..45327a3 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpConnectionHandler.java @@ -0,0 +1,207 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventPublisher; +import io.reactivex.netty.protocol.tcp.server.ConnectionHandler; +import rx.Observable; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.functions.Func1; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.reactivex.netty.events.Clock.*; +import static java.util.concurrent.TimeUnit.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpConnectionHandler implements ConnectionHandler, Object> { + + private static final Logger logger = Logger.getLogger(HttpConnectionHandler.class.getName()); + + private final RequestHandler requestHandler; + private final HttpServerEventPublisher eventPublisher; + private final boolean sendHttp10ResponseFor10Request; + + public HttpConnectionHandler(RequestHandler requestHandler, HttpServerEventPublisher eventPublisher, + boolean sendHttp10ResponseFor10Request) { + this.requestHandler = requestHandler; + this.eventPublisher = eventPublisher; + this.sendHttp10ResponseFor10Request = sendHttp10ResponseFor10Request; + } + + @Override + public Observable handle(final Connection, Object> c) { + return c.getInput() + .nest() + .concatMap(new Func1>, Observable>() { + @Override + public Observable call(Observable> reqSource) { + return reqSource.take(1) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpServerRequest req) { + final long startNanos = eventPublisher.publishingEnabled() + ? Clock.newStartTimeNanos() + : -1; + + if (eventPublisher.publishingEnabled()) { + eventPublisher.onNewRequestReceived(); + } + + final HttpServerResponse response = newResponse(req, c); + return handleRequest(req, startNanos, response, c); + } + }); + } + }) + .repeat() + .ambWith(c.closeListener()); + } + + @SuppressWarnings("unchecked") + private Observable handleRequest(HttpServerRequest request, final long startTimeNanos, + final HttpServerResponse response, + final Connection, Object> c) { + Observable requestHandlingResult = null; + try { + + if (request.decoderResult().isSuccess()) { + requestHandlingResult = requestHandler.handle(request, response); + } + + if(null == requestHandlingResult) { + /*If decoding failed an appropriate response status would have been set. + Otherwise, overwrite the status to 500*/ + if (response.getStatus().equals(OK)) { + response.setStatus(INTERNAL_SERVER_ERROR); + } + requestHandlingResult = response.write(Observable.empty()); + } + + } catch (Throwable throwable) { + logger.log(Level.SEVERE, "Unexpected error while invoking HTTP user handler.", throwable); + /*If the headers are already written, then this will produce an error Observable.*/ + requestHandlingResult = response.setStatus(INTERNAL_SERVER_ERROR) + .write(Observable.empty()); + } + + if (eventPublisher.publishingEnabled()) { + requestHandlingResult = requestHandlingResult.lift(new Operator() { + @Override + public Subscriber call(final Subscriber o) { + + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestHandlingStart(onEndNanos(startTimeNanos), NANOSECONDS); + } + + return new Subscriber(o) { + @Override + public void onCompleted() { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestHandlingSuccess(onEndNanos(startTimeNanos), + NANOSECONDS); + } + o.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestHandlingFailed(onEndNanos(startTimeNanos), + NANOSECONDS, e); + } + logger.log(Level.SEVERE, "Unexpected error processing a request.", e); + o.onError(e); + } + + @Override + public void onNext(Void aVoid) { + // No Op, its a void + } + }; + } + }); + } + + return requestHandlingResult.onErrorResumeNext(new Func1>() { + @Override + public Observable call(Throwable throwable) { + logger.log(Level.SEVERE, "Unexpected error while processing request.", throwable); + return response.setStatus(INTERNAL_SERVER_ERROR) + .dispose() + .concatWith(c.close()) + .onErrorResumeNext(Observable.empty());// Ignore errors on cleanup + } + }).concatWith(request.dispose()/*Dispose request at the end of processing to discard content if not read*/ + ).concatWith(response.dispose()/*Dispose response at the end of processing to cleanup*/); + + } + + private HttpServerResponse newResponse(HttpServerRequest request, + final Connection, Object> c) { + + /* + * Server should send the highest version it is compatible with. + * http://tools.ietf.org/html/rfc2145#section-2.3 + * + * unless overriden explicitly. + */ + final HttpVersion version = sendHttp10ResponseFor10Request ? request.getHttpVersion() + : HttpVersion.HTTP_1_1; + + HttpResponse responseHeaders; + if (request.decoderResult().isFailure()) { + // As per the spec, we should send 414/431 for URI too long and headers too long, but we do not have + // enough info to decide which kind of failure has caused this error here. + responseHeaders = new DefaultHttpResponse(version, REQUEST_HEADER_FIELDS_TOO_LARGE); + responseHeaders.headers() + .set(CONNECTION, HttpHeaderValues.CLOSE) + .set(CONTENT_LENGTH, 0); + } else { + responseHeaders = new DefaultHttpResponse(version, OK); + } + HttpServerResponse response = HttpServerResponseImpl.create(request, c, responseHeaders); + setConnectionHeader(request, response); + return response; + } + + private void setConnectionHeader(HttpServerRequest request, HttpServerResponse response) { + if (request.isKeepAlive()) { + if (!request.getHttpVersion().isKeepAliveDefault()) { + // Avoid sending keep-alive header if keep alive is default. + // Issue: https://github.com/Netflix/RxNetty/issues/167 + // This optimizes data transferred on the wire. + + // Add keep alive header as per: + // - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection + response.setHeader(CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + } else { + response.setHeader(CONNECTION, HttpHeaderValues.CLOSE); + } + } + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServer.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServer.java new file mode 100644 index 0000000..71e0102 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServer.java @@ -0,0 +1,445 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventsListener; +import io.reactivex.netty.protocol.tcp.server.TcpServer; +import io.reactivex.netty.ssl.SslCodec; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +/** + * An HTTP server. + * + * @param The type of objects received as content from a request to this server. + * @param The type of objects written as content from a response from this server. + */ +public abstract class HttpServer implements EventSource { + + /** + * Creates a new server instance, inheriting all configurations from this server and adding a {@link ChannelOption} + * for the server socket created by the newly created server instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer channelOption(ChannelOption option, T value); + + /** + * Creates a new server instance, inheriting all configurations from this server and adding a {@link ChannelOption} + * for the client socket created by the newly created server instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer clientChannelOption(ChannelOption option, T value); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this + * server. The specified handler is added at the first position of the pipeline as specified by {@link + * ChannelPipeline#addFirst(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerFirst(String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added at the first position of the pipeline as specified by {@link + * ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group The {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param name The name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerLast(String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added at the last position of the pipeline as specified by {@link + * ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added before an existing handler with the passed {@code baseName} in the pipeline as + * specified by {@link ChannelPipeline#addBefore(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added before an existing handler with the passed {@code baseName} in the pipeline as + * specified by {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added after an existing handler with the passed {@code baseName} in the pipeline as + * specified by {@link ChannelPipeline#addAfter(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The + * specified handler is added after an existing handler with the passed {@code baseName} in the pipeline as + * specified by {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, + Func0 handlerFactory); + + /** + * Creates a new client instances, inheriting all configurations from this client and using the passed action to + * configure all the connections created by the newly created client instance. + * + * @param pipelineConfigurator Action to configure {@link ChannelPipeline}. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer pipelineConfigurator(Action1 pipelineConfigurator); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslEngineFactory} for all secured connections accepted by the newly created server instance. + * + * If the {@link SSLEngine} instance can be statically, created, {@link #secure(SSLEngine)} can be used. + * + * @param sslEngineFactory Factory for all secured connections created by the newly created server instance. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer secure(Func1 sslEngineFactory); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslEngine} for all secured connections accepted by the newly created server instance. + * + * If the {@link SSLEngine} instance can not be statically, created, {@link #secure(Func1)} )} can be used. + * + * @param sslEngine {@link SSLEngine} for all secured connections created by the newly created server instance. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer secure(SSLEngine sslEngine); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslCodec} for all secured connections accepted by the newly created server instance. + * + * This is required only when the {@link SslHandler} used by {@link SslCodec} is to be modified before adding to + * the {@link ChannelPipeline}. For most of the cases, {@link #secure(Func1)} or {@link #secure(SSLEngine)} will be + * enough. + * + * @param sslCodec {@link SslCodec} for all secured connections created by the newly created server instance. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer secure(SslCodec sslCodec); + + /** + * Creates a new server instances, inheriting all configurations from this server and using a self-signed + * certificate for all secured connections accepted by the newly created server instance. + * + * This is only for testing and should not be used for real production servers. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer unsafeSecure(); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done + * if logging is enabled at this level for {@link io.netty.handler.logging.LoggingHandler} + * + * @return A new {@link HttpServer} instance. + * + * @deprecated Use {@link #enableWireLogging(String, LogLevel)} instead. + */ + @Deprecated + public abstract HttpServer enableWireLogging(LogLevel wireLoggingLevel); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param name Name of the logger that can be used to control the logging dynamically. + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done + * if logging is enabled at this level for {@link io.netty.handler.logging.LoggingHandler} + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer enableWireLogging(String name, LogLevel wireLoggingLevel); + + /** + * According to the specification, an HTTP server must + * send the highest HTTP version response, it is compatible with. Since, all implementations of this server are + * expected to be HTTP 1.1 compatible, they should always send the response for 1.1. However, in some cases, if + * desired, this behavior can be overridden to send 1.0 response for 1.0 request. If so desired, the behavior can + * be enabled/ disabled by this method. + * + * @param sendHttp10ResponseFor10Request If {@code true} then sends 1.0 version response for 1.0 request. + * + * @return A new {@link HttpServer} instance. + */ + public abstract HttpServer sendHttp10ResponseFor10Request(boolean sendHttp10ResponseFor10Request); + + /** + * Returns the port at which this server is running. + * + * For servers using ephemeral ports, this would return the actual port used, only after the server is started. + * + * @return The port at which this server is running. + */ + public abstract int getServerPort(); + + /** + * Returns the address at which this server is running. + * + * @return The address at which this server is running. + */ + public abstract SocketAddress getServerAddress(); + + /** + * Starts this server. + * + * @param requestHandler Connection handler that will handle any new client connections to this server. + * + * @return This server. + */ + public abstract HttpServer start(RequestHandler requestHandler); + + /** + * Shutdown this server and waits till the server socket is closed. + */ + public abstract void shutdown(); + + /** + * Waits for the shutdown of this server. + * + * This does not actually shutdown the server. It just waits for some other action to shutdown. + */ + public abstract void awaitShutdown(); + + /** + * Waits for the shutdown of this server, waiting a maximum of the passed duration. + * + * This does not actually shutdown the server. It just waits for some other action to shutdown. + * + * @param duration Duration to wait for shutdown. + * @param timeUnit Timeunit for the duration to wait for shutdown. + */ + public abstract void awaitShutdown(long duration, TimeUnit timeUnit); + + /** + * Creates a new server using an ephemeral port. The port used can be found by {@link #getServerPort()} + * + * @return A new {@link HttpServer} + */ + public static HttpServer newServer() { + return _newServer(TcpServer.newServer(0)); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(int port) { + return _newServer(TcpServer.newServer(port)); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @param eventLoopGroup Eventloop group to be used for server as well as client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(int port, EventLoopGroup eventLoopGroup, + Class channelClass) { + return _newServer(TcpServer.newServer(port, eventLoopGroup, eventLoopGroup, channelClass)); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @param serverGroup Eventloop group to be used for server sockets. + * @param clientGroup Eventloop group to be used for client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(int port, EventLoopGroup serverGroup, + EventLoopGroup clientGroup, + Class channelClass) { + return _newServer(TcpServer.newServer(port, serverGroup, clientGroup, channelClass)); + } + + /** + * Creates a new server using the passed port. + * + * @param socketAddress Socket address for the server. + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(SocketAddress socketAddress) { + return _newServer(TcpServer.newServer(socketAddress)); + } + + /** + * Creates a new server using the passed port. + * + * @param socketAddress Socket address for the server. + * @param eventLoopGroup Eventloop group to be used for server as well as client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(SocketAddress socketAddress, EventLoopGroup eventLoopGroup, + Class channelClass) { + return _newServer(TcpServer.newServer(socketAddress, eventLoopGroup, eventLoopGroup, channelClass)); + } + + /** + * Creates a new server using the passed port. + * + * @param socketAddress Socket address for the server. + * @param serverGroup Eventloop group to be used for server sockets. + * @param clientGroup Eventloop group to be used for client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link HttpServer} + */ + public static HttpServer newServer(SocketAddress socketAddress, EventLoopGroup serverGroup, + EventLoopGroup clientGroup, + Class channelClass) { + return _newServer(TcpServer.newServer(socketAddress, serverGroup, clientGroup, channelClass)); + } + + private static HttpServer _newServer(TcpServer tcpServer) { + return HttpServerImpl.create(tcpServer); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerImpl.java new file mode 100644 index 0000000..ac28e71 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerImpl.java @@ -0,0 +1,228 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.protocol.http.HttpHandlerNames; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventPublisher; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventsListener; +import io.reactivex.netty.protocol.http.ws.server.Ws7To13UpgradeHandler; +import io.reactivex.netty.protocol.tcp.server.TcpServer; +import io.reactivex.netty.ssl.SslCodec; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.subscriptions.CompositeSubscription; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public final class HttpServerImpl extends HttpServer { + + private final TcpServer, Object> server; + private final HttpServerEventPublisher eventPublisher; + private boolean sendHttp10ResponseFor10Request; + + private HttpServerImpl(TcpServer, Object> server, HttpServerEventPublisher eventPublisher) { + this.server = server; + this.eventPublisher = eventPublisher; + } + + @Override + public HttpServer channelOption(ChannelOption option, T value) { + return _copy(server.channelOption(option, value), eventPublisher); + } + + @Override + public HttpServer clientChannelOption(ChannelOption option, T value) { + return _copy(server.clientChannelOption(option, value), eventPublisher); + } + + @Override + public HttpServer addChannelHandlerFirst(String name, Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerFirst(name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerFirst(group, name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerLast(String name, Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerLast(name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerLast(group, name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerBefore(baseName, name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerBefore(group, baseName, name, + handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerAfter(baseName, name, handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return _copy(HttpServerImpl.castServer(server.addChannelHandlerAfter(group, baseName, name, + handlerFactory)), + eventPublisher); + } + + @Override + public HttpServer pipelineConfigurator(Action1 pipelineConfigurator) { + return _copy(HttpServerImpl.castServer(server.pipelineConfigurator(pipelineConfigurator)), + eventPublisher); + } + + @Override + public HttpServer secure(Func1 sslEngineFactory) { + return _copy(server.secure(sslEngineFactory), eventPublisher); + } + + @Override + public HttpServer secure(SSLEngine sslEngine) { + return _copy(server.secure(sslEngine), eventPublisher); + } + + @Override + public HttpServer secure(SslCodec sslCodec) { + return _copy(server.secure(sslCodec), eventPublisher); + } + + @Override + public HttpServer unsafeSecure() { + return _copy(server.unsafeSecure(), eventPublisher); + } + + @Override + @Deprecated + public HttpServer enableWireLogging(LogLevel wireLoggingLevel) { + return _copy(server.enableWireLogging(wireLoggingLevel), eventPublisher); + } + + @Override + public HttpServer enableWireLogging(String name, LogLevel wireLoggingLevel) { + return _copy(server.enableWireLogging(name, wireLoggingLevel), eventPublisher); + } + + @Override + public HttpServer sendHttp10ResponseFor10Request(boolean sendHttp10ResponseFor10Request) { + HttpServerImpl toReturn = _copy(server, eventPublisher); + toReturn.sendHttp10ResponseFor10Request = sendHttp10ResponseFor10Request; + return toReturn; + } + + @Override + public int getServerPort() { + return server.getServerPort(); + } + + @Override + public SocketAddress getServerAddress() { + return server.getServerAddress(); + } + + @Override + public HttpServer start(RequestHandler requestHandler) { + server.start(new HttpConnectionHandler<>(requestHandler, eventPublisher, sendHttp10ResponseFor10Request)); + return this; + } + + @Override + public void shutdown() { + server.shutdown(); + } + + @Override + public void awaitShutdown() { + server.awaitShutdown(); + } + + @Override + public void awaitShutdown(long duration, TimeUnit timeUnit) { + server.awaitShutdown(duration, timeUnit); + } + + static HttpServer create(final TcpServer tcpServer) { + final HttpServerEventPublisher eventPublisher = new HttpServerEventPublisher(tcpServer.getEventPublisher()); + return new HttpServerImpl<>( + tcpServer., Object>pipelineConfigurator(new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + pipeline.addLast(HttpHandlerNames.HttpServerEncoder.getName(), new HttpResponseEncoder()); + pipeline.addLast(HttpHandlerNames.HttpServerDecoder.getName(), new HttpRequestDecoder()); + pipeline.addLast(HttpHandlerNames.WsServerUpgradeHandler.getName(), new Ws7To13UpgradeHandler()); + pipeline.addLast(new HttpServerToConnectionBridge<>(eventPublisher)); + } + }), eventPublisher); + } + + @SuppressWarnings("unchecked") + private static TcpServer, Object> castServer(TcpServer rawTypes) { + return (TcpServer, Object>)rawTypes; + } + + private static HttpServerImpl _copy(TcpServer, Object> newServer, + HttpServerEventPublisher oldEventPublisher) { + return new HttpServerImpl<>(newServer, oldEventPublisher.copy(newServer.getEventPublisher())); + } + + @Override + public Subscription subscribe(HttpServerEventsListener listener) { + final CompositeSubscription cs = new CompositeSubscription(); + cs.add(server.subscribe(listener)); + cs.add(eventPublisher.subscribe(listener)); + return cs; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerInterceptorChain.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerInterceptorChain.java new file mode 100644 index 0000000..3181011 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerInterceptorChain.java @@ -0,0 +1,245 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import rx.annotations.Beta; + +/** + * A utility to create an interceptor chain to be used with a {@link HttpServer} to modify behavior of requests + * processed by that server. + * + *

What are interceptors?

+ * + * Interceptors can be used to achieve use-cases that involve instrumentation related to any behavior of the requests, + * they can even be used to short-circuit the rest of the chain or to provide canned responses. In order to achieve such + * widely different use-cases, an interceptor is modelled as a simple function that takes one {@link RequestHandler} + * and returns another {@link RequestHandler} instance. With this low level abstraction, any use-case pertaining to + * connection instrumentation can be achieved. + * + * An interceptor chain always starts with an interceptor and ends with a {@link RequestHandler} and any number of + * other interceptors can exist between the start and end.

+ * + * A chain can be created by using the various {@code start*()} methods available in this class, eg: + * {@link #start()}, {@link #startRaw()}.

+ * + * After starting a chain, any number of other interceptors can be added by using the various {@code next*()} methods + * available in this class, eg: {@link #next(Interceptor)}, {@link #nextWithTransform(TransformingInterceptor)}, + * {@link #nextWithRequestTransform(TransformingInterceptor)} and + * {@link #nextWithResponseTransform(TransformingInterceptor)}

+ * + * After adding the required interceptors, by providing a {@link RequestHandler} via the + * {@link #end(RequestHandler)} method, the chain can be ended and the returned {@link RequestHandler} can be used + * with any {@link HttpServer}

+ * + * So, a typical interaction with this class would look like:

+ * + * {@code + * HttpServer.newServer().start(HttpServerInterceptorChain.start(first).next(second).next(third).end(handler)) + * } + * + *

Simple Interceptor

+ * + * For interceptors that do not change the types of objects read or written to the underlying request/response, the + * interface {@link Interceptor} defines the interceptor contract. + * + *

Modifying the type of data read/written to the request/response

+ * + * Sometimes, it is required to change the type of objects read from a request or written to a response handled by + * a {@link HttpServer}. For such cases, the interface {@link TransformingInterceptor} + * defines the interceptor contract. Since, this included 4 generic arguments to the interceptor, this is not the base + * type for all interceptors and should be used only when the types of the request/response are actually to be + * changed. + * + * + * The above diagram depicts the execution order of interceptors. The first request handler (internal) is created by + * this class and as is returned by {@link #end(RequestHandler)} method by providing a {@link RequestHandler} that + * does the actual processing of the connection. {@link HttpServer} with which this interceptor chain is used, will + * invoke the internal request handler provided by this class.

+ * + * The interceptors are invoked in the order that they are added to this chain. + * + * @param The type of objects received as content from a request to this server. + * @param The type of objects written as content from a response from this server. + * @param The type of objects received as content from a request to this server after applying these interceptors. + * @param The type of objects written as content from a response from this server after applying these + * interceptors. + */ +@Beta +public final class HttpServerInterceptorChain { + + private final TransformingInterceptor interceptor; + + private HttpServerInterceptorChain(TransformingInterceptor interceptor) { + this.interceptor = interceptor; + } + + /** + * Add the next interceptor to this chain. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors currently existing and the passed interceptor added to the + * end. + */ + public HttpServerInterceptorChain next(final Interceptor next) { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects read from the request accepted by + * the associated {@link HttpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors currently existing and the passed interceptor added to the + * end. + */ + public HttpServerInterceptorChain nextWithRequestTransform(final TransformingInterceptor next) { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects written to the response sent by + * the associated {@link HttpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public HttpServerInterceptorChain nextWithResponseTransform(final TransformingInterceptor next) { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects read read from the request and written + * to the response sent by the associated {@link HttpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public HttpServerInterceptorChain nextWithTransform(final TransformingInterceptor next) { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Terminates this chain with the passed {@link RequestHandler} and returns a {@link RequestHandler} to be + * used by a {@link HttpServer} + * + * @param handler Request handler to use. + * + * @return A request handler that wires the interceptor chain, to be used with {@link HttpServer} instead of + * directly using the passed {@code handler} + */ + public RequestHandler end(RequestHandler handler) { + return interceptor.intercept(handler); + } + + /** + * Starts a new interceptor chain. + * + * @return A new interceptor chain. + */ + public static HttpServerInterceptorChain start() { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return handler; + } + }); + } + + /** + * Starts a new interceptor chain with {@link ByteBuf} read and written from request and to responses. + * + * @return A new interceptor chain. + */ + public static HttpServerInterceptorChain startRaw() { + return new HttpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public RequestHandler intercept(RequestHandler handler) { + return handler; + } + }); + } + + /** + * An interceptor that preserves the type of content of request and response. + * + * @param The type of objects received as content from a request to this server. + * @param The type of objects written as content from a response from this server. + */ + public interface Interceptor { + + /** + * Intercepts and optionally changes the passed {@code RequestHandler}. + * + * @param handler Handler to intercept. + * + * @return Handler to use after this transformation. + */ + RequestHandler intercept(RequestHandler handler); + + } + + /** + * An interceptor that changes the type of content of request and response. + * + * @param The type of objects received as content from a request to this server. + * @param The type of objects written as content from a response from this server. + * @param The type of objects received as content from a request to this server after applying this + * interceptor. + * @param The type of objects written as content from a response from this server after applying this + * interceptor. + */ + public interface TransformingInterceptor { + + /** + * Intercepts and changes the passed {@code RequestHandler}. + * + * @param handler Handler to intercept. + * + * @return Handler to use after this transformation. + */ + RequestHandler intercept(RequestHandler handler); + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequest.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequest.java new file mode 100644 index 0000000..b550f49 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequest.java @@ -0,0 +1,441 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.ContentSource; +import io.reactivex.netty.protocol.http.internal.HttpMessageFormatter; +import rx.Observable; +import rx.Observable.Transformer; +import rx.Subscriber; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * HTTP server request + * + *

Thread safety

+ * + * This object is not thread-safe and must not be used by multiple threads. + * + *

Mutability

+ * + * Headers and trailing headers can be mutated for this response. + */ +public abstract class HttpServerRequest { + + /** + * Returns the HTTP method for this request. + * + * @return The HTTP method for this request. + */ + public abstract HttpMethod getHttpMethod(); + + /** + * Returns the HTTP version for this request. + * + * @return The HTTP version for this request. + */ + public abstract HttpVersion getHttpVersion(); + + /** + * Returns the raw URI for the request, including path and query parameters. The URI is not decoded. + * + * @return The raw URI for the request. + */ + public abstract String getUri(); + + /** + * Returns the decoded URI path for this request. + * + * @return The decoded URI path for this request. + */ + public abstract String getDecodedPath(); + + /** + * Returns the query string for this request. The query string is not decoded. + * + * @return The query string for this request. + */ + public abstract String getRawQueryString(); + + /** + * Returns an immutable map of cookie names and cookies contained in this request. + * + * @return An immutable map of cookie names and cookies contained in this request. + */ + public abstract Map> getCookies(); + + /** + * Returns an immutable map of query parameter names and values contained in this request. The names and values for + * the query parameters will be decoded. + * + * @return An immutable map of query parameter names and values contained in this request. + */ + public abstract Map> getQueryParameters(); + + /** + * Checks if there is a header with the passed name in this request. + * + * @param name Name of the header. + * + * @return {@code true} if there is a header with the passed name in this request. + */ + public abstract boolean containsHeader(CharSequence name); + + /** + * Checks if there is a header with the passed name and value in this request. + * + * @param name Name of the header. + * @param value Value of the header. + * @param ignoreCaseValue {@code true} then the value comparision is done ignoring case. + * + * @return {@code true} if there is a header with the passed name and value in this request. + */ + public abstract boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue); + + /** + * Returns an iterator over the header entries. Multiple values for the same header appear as separate entries in + * the returned iterator. + * + * @return An iterator over the header entries + */ + public abstract Iterator> headerIterator(); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @return The first header value or {@code null} if there is no such header + */ + public abstract String getHeader(CharSequence name); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return The first header value or {@code defaultValue} if there is no such header + */ + public abstract String getHeader(CharSequence name, String defaultValue); + + /** + * Returns the values of headers with the specified name + * + * @param name The name of the headers to search + * + * @return A {@link List} of header values which will be empty if no values are found + */ + public abstract List getAllHeaderValues(CharSequence name); + + /** + * Returns the length of the content. + * + * @return the content length + * + * @throws NumberFormatException if the message does not have the {@code "Content-Length"} header or its value is + * not a number. + */ + public abstract long getContentLength(); + + /** + * Returns the length of the content. + * + * @param defaultValue Default value if the message does not have a {@code "Content-Length"} header or its value is + * not a number + * + * @return the content length or {@code defaultValue} if this message does not have the {@code "Content-Length"} + * header or its value is not a number + */ + public abstract long getContentLength(long defaultValue); + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * + * @return the header value + */ + public abstract long getDateHeader(CharSequence name); + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * @param defaultValue Default value if there is no header with this name. + * + * @return the header value or {@code defaultValue} if there is no header with this name. + */ + public abstract long getDateHeader(CharSequence name, long defaultValue); + + /** + * Returns the value of the {@code "Host"} header. + */ + public abstract String getHostHeader(); + + /** + * Returns the value of the {@code "Host"} header. + * + * @param defaultValue Default if the header does not exist. + * + * @return The value of the {@code "Host"} header or {@code defaultValue} if there is no such header. + */ + public abstract String getHostHeader(String defaultValue); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * + * @return the header value + * + * @throws NumberFormatException if there is no such header or the header value is not a number + */ + public abstract int getIntHeader(CharSequence name); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return the header value or the {@code defaultValue} if there is no such header or the header value is not a + * number + */ + public abstract int getIntHeader(CharSequence name, int defaultValue); + + /** + * Returns {@code true} if and only if this request contains the {@code "Expect: 100-continue"} header. + */ + public abstract boolean is100ContinueExpected(); + + /** + * Returns {@code true} if and only if this request has the content-length header set. + */ + public abstract boolean isContentLengthSet(); + + /** + * Returns {@code true} if and only if the connection can remain open and thus 'kept alive'. This methods respects + * the value of the {@code "Connection"} header first and then the return value of + * {@link HttpVersion#isKeepAliveDefault()}. + */ + public abstract boolean isKeepAlive(); + + /** + * Checks to see if the transfer encoding of this request is chunked + * + * @return True if transfer encoding is chunked, otherwise false + */ + public abstract boolean isTransferEncodingChunked(); + + /** + * Returns a new {@link Set} that contains the names of all headers in this request. Note that modifying the + * returned {@link Set} will not affect the state of this request. + */ + public abstract Set getHeaderNames(); + + /** + * Adds an HTTP header with the passed {@code name} and {@code value} to this request. + * + * @param name Name of the header. + * @param value Value for the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest addHeader(CharSequence name, Object value); + + /** + * Adds the passed {@code cookie} to this request. + * + * @param cookie Cookie to add. + * + * @return {@code this} + */ + public abstract HttpServerRequest addCookie(Cookie cookie); + + /** + * Adds the passed header as a date value to this request. The date is formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest addDateHeader(CharSequence name, Date value); + + /** + * Adds multiple date values for the passed header name to this request. The date values are formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest addDateHeader(CharSequence name, Iterable values); + + /** + * Adds an HTTP header with the passed {@code name} and {@code values} to this request. + * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest addHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed date value for this request. The date is + * formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date + * as per the HTTP specifications into + * the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest setDateHeader(CharSequence name, Date value); + + /** + * Overwrites the current value, if any, of the passed header to the passed value for this request. + * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest setHeader(CharSequence name, Object value); + + /** + * Overwrites the current value, if any, of the passed header to the passed date values for this request. The date + * is formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the + * date as per the HTTP specifications + * into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest setDateHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed values for this request. + * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest setHeader(CharSequence name, Iterable values); + + /** + * Removes the passed header from this request. + * + * @param name Name of the header. + * + * @return {@code this} + */ + public abstract HttpServerRequest removeHeader(CharSequence name); + + /** + * Returns the content as a stream. There can only be one {@link Subscriber} to the returned {@link Observable}, any + * subsequent subscriptions will get an error. + * + * @return Stream of content. + */ + public abstract ContentSource getContent(); + + /** + * Subscribes to the content and discards. + * + * @return An {@link Observable}, subscription to which will discard the content. This {@code Observable} will + * error/complete when the content errors/completes and unsubscription from here will unsubscribe from the content. + */ + public abstract Observable discardContent(); + + /** + * Disposes this request. If the content is not yet subscribed, will subscribe and discard the same. + * + * @return An {@link Observable}, subscription to which will dispose this request. If the content is not yet + * subscribed then this is the same as {@link #discardContent()}. + */ + public abstract Observable dispose(); + + /** + * Checks to see if upgrade to websocket protocol is requested by this HTTP request. + * + * @return {@code true} if upgrade to websocket is requested. + */ + public abstract boolean isWebSocketUpgradeRequested(); + + /** + * Creates a new {@code HttpServerRequest} instance modifying the content type using the passed {@code transformer}. + * + * @param transformer Transformer to transform the content stream. + * + * @param New type of the content. + * + * @return A new instance of {@link HttpServerRequest} with the transformed content stream. + */ + public abstract HttpServerRequest transformContent(Transformer transformer); + + /** + * Package private method to get the decoder result from netty. + * + * @return Decoder result. + */ + abstract DecoderResult decoderResult(); + + public String toString() { + return HttpMessageFormatter.formatRequest(getHttpVersion(), getHttpMethod(), getUri(), headerIterator()); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequestImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequestImpl.java new file mode 100644 index 0000000..0e5ede1 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerRequestImpl.java @@ -0,0 +1,307 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.channel.Channel; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.util.ReferenceCountUtil; +import io.reactivex.netty.channel.ContentSource; +import io.reactivex.netty.protocol.http.CookiesHolder; +import io.reactivex.netty.protocol.http.internal.HttpContentSubscriberEvent; +import io.reactivex.netty.protocol.http.ws.server.WebSocketHandshaker; +import rx.Observable; +import rx.Observable.Transformer; +import rx.Subscriber; +import rx.functions.Func1; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; + +class HttpServerRequestImpl extends HttpServerRequest { + + private final Channel nettyChannel; + private final HttpRequest nettyRequest; + private final CookiesHolder cookiesHolder; + private final UriInfoHolder uriInfoHolder; + private final ContentSource contentSource; + + HttpServerRequestImpl(HttpRequest nettyRequest, Channel nettyChannel) { + this.nettyRequest = nettyRequest; + this.nettyChannel = nettyChannel; + uriInfoHolder = new UriInfoHolder(this.nettyRequest.uri()); + cookiesHolder = CookiesHolder.newServerRequestHolder(nettyRequest.headers()); + contentSource = new ContentSource<>(nettyChannel, new Func1, Object>() { + @Override + public Object call(Subscriber subscriber) { + return new HttpContentSubscriberEvent<>(subscriber); + } + }); + } + + private HttpServerRequestImpl(HttpRequest nettyRequest, Channel nettyChannel, ContentSource contentSource) { + this.nettyRequest = nettyRequest; + this.nettyChannel = nettyChannel; + uriInfoHolder = new UriInfoHolder(this.nettyRequest.uri()); + cookiesHolder = CookiesHolder.newServerRequestHolder(nettyRequest.headers()); + this.contentSource = contentSource; + } + + @Override + public HttpMethod getHttpMethod() { + return nettyRequest.method(); + } + + @Override + public HttpVersion getHttpVersion() { + return nettyRequest.protocolVersion(); + } + + @Override + public String getUri() { + return uriInfoHolder.getRawUriString(); + } + + @Override + public String getDecodedPath() { + return uriInfoHolder.getPath(); + } + + @Override + public String getRawQueryString() { + return uriInfoHolder.getQueryString(); + } + + @Override + public Map> getCookies() { + return cookiesHolder.getAllCookies(); + } + + @Override + public Map> getQueryParameters() { + return uriInfoHolder.getQueryParameters(); + } + + @Override + public boolean containsHeader(CharSequence name) { + return nettyRequest.headers().contains(name); + } + + @Override + public boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue) { + return nettyRequest.headers().contains(name, value, ignoreCaseValue); + } + + @Override + public Iterator> headerIterator() { + return nettyRequest.headers().iteratorCharSequence(); + } + + @Override + public String getHeader(CharSequence name) { + return nettyRequest.headers().get(name); + } + + @Override + public String getHeader(CharSequence name, String defaultValue) { + return nettyRequest.headers().get(name, defaultValue); + } + + @Override + public List getAllHeaderValues(CharSequence name) { + return nettyRequest.headers().getAll(name); + } + + @Override + public long getContentLength() { + return HttpUtil.getContentLength(nettyRequest); + } + + @Override + public long getContentLength(long defaultValue) { + return HttpUtil.getContentLength(nettyRequest, defaultValue); + } + + @Override + public long getDateHeader(CharSequence name) { + return nettyRequest.headers().getTimeMillis(name); + } + + @Override + public long getDateHeader(CharSequence name, long defaultValue) { + return nettyRequest.headers().getTimeMillis(name, defaultValue); + } + + @Override + public String getHostHeader() { + return nettyRequest.headers().get(HOST); + } + + @Override + public String getHostHeader(String defaultValue) { + return nettyRequest.headers().get(HOST, defaultValue); + } + + @Override + public int getIntHeader(CharSequence name) { + return nettyRequest.headers().getInt(name); + } + + @Override + public int getIntHeader(CharSequence name, int defaultValue) { + return nettyRequest.headers().getInt(name, defaultValue); + } + + @Override + public boolean is100ContinueExpected() { + return HttpUtil.is100ContinueExpected(nettyRequest); + } + + @Override + public boolean isContentLengthSet() { + return HttpUtil.isContentLengthSet(nettyRequest); + } + + @Override + public boolean isKeepAlive() { + return HttpUtil.isKeepAlive(nettyRequest); + } + + @Override + public boolean isTransferEncodingChunked() { + return HttpUtil.isTransferEncodingChunked(nettyRequest); + } + + @Override + public Set getHeaderNames() { + return nettyRequest.headers().names(); + } + + @Override + public HttpServerRequest addHeader(CharSequence name, Object value) { + nettyRequest.headers().add(name, value); + return this; + } + + @Override + public HttpServerRequest addCookie(Cookie cookie) { + nettyRequest.headers().add(COOKIE, + ClientCookieEncoder.STRICT.encode(cookie) /*Since this is a request object, cookies are + as if coming from a client*/); + return this; + + } + + @Override + public HttpServerRequest addDateHeader(CharSequence name, Date value) { + nettyRequest.headers().add(name, value); + return this; + } + + @Override + public HttpServerRequest addDateHeader(CharSequence name, Iterable values) { + for (Date value : values) { + nettyRequest.headers().add(name, value); + } + return this; + } + + @Override + public HttpServerRequest addHeader(CharSequence name, Iterable values) { + nettyRequest.headers().add(name, values); + return this; + } + + @Override + public HttpServerRequest setDateHeader(CharSequence name, Date value) { + nettyRequest.headers().set(name, value); + return this; + } + + @Override + public HttpServerRequest setHeader(CharSequence name, Object value) { + nettyRequest.headers().set(name, value); + return this; + } + + @Override + public HttpServerRequest setDateHeader(CharSequence name, Iterable values) { + for (Date value : values) { + nettyRequest.headers().set(name, value); + } + return this; + } + + @Override + public HttpServerRequest setHeader(CharSequence name, Iterable values) { + nettyRequest.headers().add(name, values); + return this; + } + + @Override + public HttpServerRequest removeHeader(CharSequence name) { + nettyRequest.headers().remove(name); + return this; + } + + @Override + public ContentSource getContent() { + return contentSource; + } + + @Override + public Observable discardContent() { + return getContent().map(new Func1() { + @Override + public Void call(T t) { + ReferenceCountUtil.release(t); + return null; + } + }).ignoreElements(); + } + + @Override + public Observable dispose() { + return discardContent().onErrorResumeNext(Observable.empty()); + } + + @Override + public boolean isWebSocketUpgradeRequested() { + return WebSocketHandshaker.isUpgradeRequested(this); + } + + @Override + public HttpServerRequest transformContent(Transformer transformer) { + return new HttpServerRequestImpl<>(nettyRequest, nettyChannel, contentSource.transform(transformer)); + } + + @Override + DecoderResult decoderResult() { + return nettyRequest.decoderResult(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponse.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponse.java new file mode 100644 index 0000000..164b88e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponse.java @@ -0,0 +1,388 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.cookie.Cookie; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.ws.server.WebSocketHandler; +import io.reactivex.netty.protocol.http.ws.server.WebSocketHandshaker; +import rx.Observable; +import rx.annotations.Experimental; + +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * An HTTP server response. + * + *

Thread safety

+ * + * This object is not thread safe and should not be accessed from multiple threads. + * + * @param The type of objects written as the content of the response. + */ +public abstract class HttpServerResponse extends ResponseContentWriter { + + protected HttpServerResponse(OnSubscribe f) { + super(f); + } + + /** + * Returns the status of this response. If the status is not explicitly set, the default value is + * {@link HttpResponseStatus#OK} + * + * @return The status of this response. + */ + public abstract HttpResponseStatus getStatus(); + + /** + * Checks if there is a header with the passed name in this response. + * + * @param name Name of the header. + * + * @return {@code true} if there is a header with the passed name in this response. + */ + public abstract boolean containsHeader(CharSequence name); + + /** + * Checks if there is a header with the passed name and value in this response. + * + * @param name Name of the header. + * @param value Value of the header. + * @param ignoreCaseValue {@code true} then the value comparision is done ignoring case. + * + * @return {@code true} if there is a header with the passed name and value in this response. + */ + public abstract boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @return The first header value or {@code null} if there is no such header + */ + public abstract String getHeader(CharSequence name); + + /** + * Returns the value of a header with the specified name. If there are more than one values for the specified name, + * the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return The first header value or {@code defaultValue} if there is no such header + */ + public abstract String getHeader(CharSequence name, String defaultValue); + + /** + * Returns the values of headers with the specified name + * + * @param name The name of the headers to search + * + * @return A {@link List} of header values which will be empty if no values are found + */ + public abstract List getAllHeaderValues(CharSequence name); + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * + * @return the header value + * + * @throws ParseException if there is no such header or the header value is not a formatted date + */ + public abstract long getDateHeader(CharSequence name) throws ParseException; + + /** + * Returns the date header value with the specified header name. If there are more than one header value for the + * specified header name, the first value is returned. + * The value is parsed as per the + * HTTP specifications using the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name The name of the header to search + * @param defaultValue Default value if there is no header with this name. + * + * @return the header value or {@code defaultValue} if there is no header with this name. + */ + public abstract long getDateHeader(CharSequence name, long defaultValue); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * + * @return the header value + * + * @throws NumberFormatException if there is no such header or the header value is not a number + */ + public abstract int getIntHeader(CharSequence name); + + /** + * Returns the integer header value with the specified header name. If there are more than one header value for + * the specified header name, the first value is returned. + * + * @param name The name of the header to search + * @param defaultValue Default if the header does not exist. + * + * @return the header value or the {@code defaultValue} if there is no such header or the header value is not a + * number + */ + public abstract int getIntHeader(CharSequence name, int defaultValue); + + /** + * Returns a new {@link Set} that contains the names of all headers in this response. Note that modifying the + * returned {@link Set} will not affect the state of this response. + */ + public abstract Set getHeaderNames(); + + /** + * Adds an HTTP header with the passed {@code name} and {@code value} to this response. + * + * @param name Name of the header. + * @param value Value for the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse addHeader(CharSequence name, Object value); + + /** + * Adds the passed {@code cookie} to this response. + * + * @param cookie Cookie to add. + * + * @return {@code this} + */ + public abstract HttpServerResponse addCookie(Cookie cookie); + + /** + * Adds the passed header as a date value to this response. The date is formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse addDateHeader(CharSequence name, Date value); + + /** + * Adds multiple date values for the passed header name to this response. The date values are formatted using netty's + * {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date as per the + * HTTP specifications into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse addDateHeader(CharSequence name, Iterable values); + + /** + * Adds an HTTP header with the passed {@code name} and {@code values} to this response. + * + * @param name Name of the header. + * @param values Values for the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse addHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed date value for this response. The date is + * formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the date + * as per the HTTP specifications into + * the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse setDateHeader(CharSequence name, Date value); + + /** + * Overwrites the current value, if any, of the passed header to the passed value for this response. + * + * @param name Name of the header. + * @param value Value of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse setHeader(CharSequence name, Object value); + + /** + * Overwrites the current value, if any, of the passed header to the passed date values for this response. The date + * is formatted using netty's {@link HttpHeaders#addDateHeader(HttpMessage, CharSequence, Date)} which formats the + * date as per the HTTP specifications + * into the format: + * + *
"E, dd MMM yyyy HH:mm:ss z"
+ * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse setDateHeader(CharSequence name, Iterable values); + + /** + * Overwrites the current value, if any, of the passed header to the passed values for this response. + * + * @param name Name of the header. + * @param values Values of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse setHeader(CharSequence name, Iterable values); + + /** + * Removes the passed header from this response. + * + * @param name Name of the header. + * + * @return {@code this} + */ + public abstract HttpServerResponse removeHeader(CharSequence name); + + /** + * Sets the status for the response. + * + * @param status Status to set. + * + * @return {@code this} + */ + public abstract HttpServerResponse setStatus(HttpResponseStatus status); + + /** + * Sets the HTTP transfer encoding to chunked for this response. This delegates to + * {@link HttpHeaders#setTransferEncodingChunked(HttpMessage)} + * + * @return {@code this} + */ + public abstract HttpServerResponse setTransferEncodingChunked(); + + /** + * This is a performance optimization to not flush the channel on every response send. + * + *

When NOT to use

+ * This can be used + * only when the processing for a server is not asynchronous, in which case, one would have to flush the responses + * written explicitly (done on completion of the {@link Observable} written). Something like this: + * +
+     resp.sendHeaders()
+         .writeStringAndFlushOnEach(Observable.interval(1, TimeUnit.SECONDS))
+                                              .map(aLong -$gt; "Interval =>" + aLong)
+                                   )
+     
+ * + *

When to use

+ * + * This can be used when the response is written synchronously from a {@link RequestHandler}, something like: + * +
+     response.writeString(Observable.just("Hello world");
+     
+ * + * When set, this will make the channel to be flushed only when all the requests available on the channel are + * read. Thus, making it possible to do a gathering write for all pipelined requests on a connection. This reduces + * the number of system calls and is helpful in "Hello World" benchmarks. + */ + public abstract HttpServerResponse flushOnlyOnReadComplete(); + + /** + * Sends the headers for this response when the returned {@code Observable} is subscribed. Alternatively, one can + * continue to write contents using the returned {@link ResponseContentWriter} + * + * @return {@link ResponseContentWriter} which can be subscribed to only send the headers or to write payload. + */ + public abstract ResponseContentWriter sendHeaders(); + + /** + * Converts this response to enable writing {@link ServerSentEvent}s. + * + * @return This response with writing of {@link ServerSentEvent} enabled. + */ + @Experimental + public abstract HttpServerResponse transformToServerSentEvents(); + + /** + * Creates a new {@code HttpServerResponse} instance modifying the content type using the passed {@code transformer}. + * + * @param transformer Transformer to transform the content stream. + * + * @param New type of the content. + * + * @return A new instance of {@link HttpServerResponse} with the transformed content stream. + */ + public abstract HttpServerResponse transformContent(AllocatingTransformer transformer); + + /** + * Accepts the upgrade to websockets, if requested and after sending a successful handshake response, + * invokes the passed handler to handle the websocket connection. + * + * If any changes to this response are required for the handshake, they should be done before invoking this method. + * + * @return {@link WebSocketHandshaker} for sending a handshake to the client. Subscription to the handshaker, will + * send the handshake. + */ + public abstract WebSocketHandshaker acceptWebSocketUpgrade(WebSocketHandler handler); + + /** + * Disposes this response. If the response is not yet set then this will attempt to send an error response if the + * connection is still open. + * + * @return An {@link Observable}, subscription to which will dispose this response. + */ + public abstract Observable dispose(); + + /** + * Returns the underlying channel on which this response was received. + * + * @return The underlying channel on which this response was received. + */ + public abstract Channel unsafeNettyChannel(); + + /** + * Returns the underlying connection on which this response was received. + * + * @return The underlying connection on which this response was received. + */ + public abstract Connection unsafeConnection(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponseImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponseImpl.java new file mode 100644 index 0000000..050cf46 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerResponseImpl.java @@ -0,0 +1,432 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.reactivex.netty.channel.AllocatingTransformer; +import io.reactivex.netty.channel.ChannelOperations; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.MarkAwarePipeline; +import io.reactivex.netty.protocol.http.HttpHandlerNames; +import io.reactivex.netty.protocol.http.TrailingHeaders; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.sse.server.ServerSentEventEncoder; +import io.reactivex.netty.protocol.http.ws.server.WebSocketHandler; +import io.reactivex.netty.protocol.http.ws.server.WebSocketHandshaker; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +public final class HttpServerResponseImpl extends HttpServerResponse { + + private final State state; + + private HttpServerResponseImpl(final State state) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + state.sendHeaders().unsafeSubscribe(subscriber); + } + }); + this.state = state; + } + + @Override + public HttpResponseStatus getStatus() { + return state.headers.status(); + } + + @Override + public boolean containsHeader(CharSequence name) { + return state.headers.headers().contains(name); + } + + @Override + public boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue) { + return state.headers.headers().contains(name, value, ignoreCaseValue); + } + + @Override + public String getHeader(CharSequence name) { + return state.headers.headers().get(name); + } + + @Override + public String getHeader(CharSequence name, String defaultValue) { + return state.headers.headers().get(name, defaultValue); + } + + @Override + public List getAllHeaderValues(CharSequence name) { + return state.headers.headers().getAll(name); + } + + @Override + public long getDateHeader(CharSequence name) { + return state.headers.headers().getTimeMillis(name); + } + + @Override + public long getDateHeader(CharSequence name, long defaultValue) { + return state.headers.headers().getTimeMillis(name, defaultValue); + } + + @Override + public int getIntHeader(CharSequence name) { + return state.headers.headers().getInt(name); + } + + @Override + public int getIntHeader(CharSequence name, int defaultValue) { + return state.headers.headers().getInt(name, defaultValue); + } + + @Override + public Set getHeaderNames() { + return state.headers.headers().names(); + } + + @Override + public HttpServerResponse addHeader(CharSequence name, Object value) { + if (state.allowUpdate()) { + state.headers.headers().add(name, value); + } + return this; + } + + @Override + public HttpServerResponse addCookie(Cookie cookie) { + if (state.allowUpdate()) { + state.headers.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); + } + return this; + } + + @Override + public HttpServerResponse addDateHeader(CharSequence name, Date value) { + if (state.allowUpdate()) { + state.headers.headers().add(name, value); + } + return this; + } + + @Override + public HttpServerResponse addDateHeader(CharSequence name, Iterable values) { + if (state.allowUpdate()) { + for (Date value : values) { + state.headers.headers().add(name, value); + } + } + return this; + } + + @Override + public HttpServerResponse addHeader(CharSequence name, Iterable values) { + if (state.allowUpdate()) { + state.headers.headers().add(name, values); + } + return this; + } + + @Override + public HttpServerResponse setDateHeader(CharSequence name, Date value) { + if (state.allowUpdate()) { + state.headers.headers().set(name, value); + } + return this; + } + + @Override + public HttpServerResponse setHeader(CharSequence name, Object value) { + if (state.allowUpdate()) { + state.headers.headers().set(name, value); + } + return this; + } + + @Override + public HttpServerResponse setDateHeader(CharSequence name, Iterable values) { + if (state.allowUpdate()) { + for (Date value : values) { + state.headers.headers().set(name, value); + } + } + return this; + } + + @Override + public HttpServerResponse setHeader(CharSequence name, Iterable values) { + if (state.allowUpdate()) { + state.headers.headers().set(name, values); + } + return this; + } + + @Override + public HttpServerResponse removeHeader(CharSequence name) { + if (state.allowUpdate()) { + state.headers.headers().remove(name); + } + return this; + } + + @Override + public HttpServerResponse setStatus(HttpResponseStatus status) { + if (state.allowUpdate()) { + state.headers.setStatus(status); + } + return this; + } + + @Override + public HttpServerResponse setTransferEncodingChunked() { + if (state.allowUpdate()) { + HttpUtil.setTransferEncodingChunked(state.headers, true); + } + return this; + } + + @Override + public HttpServerResponse flushOnlyOnReadComplete() { + // Does not need to be guarded by allowUpdate() as flush semantics can be changed anytime. + state.connection.unsafeNettyChannel().attr(ChannelOperations.FLUSH_ONLY_ON_READ_COMPLETE).set(true); + return this; + } + + @Override + public ResponseContentWriter sendHeaders() { + return state.sendHeaders(); + } + + @Override + public HttpServerResponse transformToServerSentEvents() { + markAwarePipeline().addAfter(HttpHandlerNames.HttpServerEncoder.getName(), + HttpHandlerNames.SseServerCodec.getName(), + new ServerSentEventEncoder()); + return _cast(); + } + + @Override + public HttpServerResponse transformContent(AllocatingTransformer transformer) { + @SuppressWarnings("unchecked") + Connection transformedC = state.connection.transformWrite(transformer); + return new HttpServerResponseImpl<>(new State(state, transformedC)); + } + + @Override + public WebSocketHandshaker acceptWebSocketUpgrade(WebSocketHandler handler) { + return WebSocketHandshaker.isUpgradeRequested(state.request) + ? WebSocketHandshaker.newHandshaker(state.request, this, handler) + : WebSocketHandshaker.newErrorHandshaker(new IllegalStateException("WebSocket upgrade was not requested.")); + } + + @Override + public Observable dispose() { + return Observable.defer(new Func0>() { + @Override + public Observable call() { + return (state.allowUpdate() ? write(Observable.empty()) : Observable.empty()) + .doOnSubscribe(new Action0() { + @Override + public void call() { + state.connection + .getResettableChannelPipeline() + .reset(); + } + }); + } + }); + } + + @Override + public Channel unsafeNettyChannel() { + return state.connection.unsafeNettyChannel(); + } + + @Override + public Connection unsafeConnection() { + return state.connection; + } + + @Override + public ResponseContentWriter write(Observable msgs) { + return state.sendHeaders().write(msgs); + } + + @Override + public Observable write(Observable contentSource, Func0 trailerFactory, + Func2 trailerMutator) { + return state.sendHeaders().write(contentSource, trailerFactory, trailerMutator); + } + + @Override + public Observable write(Observable contentSource, Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return state.sendHeaders().write(contentSource, trailerFactory, trailerMutator, flushSelector); + } + + @Override + public ResponseContentWriter write(Observable msgs, Func1 flushSelector) { + return state.sendHeaders().write(msgs, flushSelector); + } + + @Override + public ResponseContentWriter writeAndFlushOnEach(Observable msgs) { + return state.sendHeaders().writeAndFlushOnEach(msgs); + } + + @Override + public ResponseContentWriter writeString(Observable msgs) { + return state.sendHeaders().writeString(msgs); + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + return state.sendHeaders().writeString(contentSource, trailerFactory, trailerMutator); + } + + @Override + public Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return state.sendHeaders().writeString(contentSource, trailerFactory, trailerMutator, flushSelector); + } + + @Override + public ResponseContentWriter writeString(Observable msgs, Func1 flushSelector) { + return state.sendHeaders().writeString(msgs, flushSelector); + } + + @Override + public ResponseContentWriter writeStringAndFlushOnEach(Observable msgs) { + return state.sendHeaders().writeStringAndFlushOnEach(msgs); + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs) { + return state.sendHeaders().writeBytes(msgs); + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator) { + return state.sendHeaders().writeBytes(contentSource, trailerFactory, trailerMutator); + } + + @Override + public Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector) { + return state.sendHeaders().writeBytes(contentSource, trailerFactory, trailerMutator, flushSelector); + } + + @Override + public ResponseContentWriter writeBytes(Observable msgs, Func1 flushSelector) { + return state.sendHeaders().writeBytes(msgs, flushSelector); + } + + @Override + public ResponseContentWriter writeBytesAndFlushOnEach(Observable msgs) { + return state.sendHeaders().writeBytesAndFlushOnEach(msgs); + } + + public static HttpServerResponse create(HttpServerRequest request, + @SuppressWarnings("rawtypes") Connection connection, + HttpResponse headers) { + final State newState = new State<>(headers, connection, request); + return new HttpServerResponseImpl<>(newState); + } + + @SuppressWarnings("unchecked") + private HttpServerResponse _cast() { + return (HttpServerResponse) this; + } + + private MarkAwarePipeline markAwarePipeline() { + return state.connection.getResettableChannelPipeline().markIfNotYetMarked(); + } + + private static class State { + + private final HttpResponse headers; + + @SuppressWarnings("rawtypes") + private final Connection connection; + private final HttpServerRequest request; + /*This links the headers sent dynamic state from one response to a child response + (created via a mutation method). If it is a simple boolean, then a copy of state will just lead to a copy by + value and not reference.*/ + private final HeaderSentStateHolder sentStateHolder; + + private State(HttpResponse headers, @SuppressWarnings("rawtypes") Connection connection, HttpServerRequest request) { + this.headers = headers; + this.connection = connection; + this.request = request; + this.sentStateHolder = new HeaderSentStateHolder(); + } + + public State(State state, Connection connection) { + this.headers = state.headers; + this.request = state.request; + this.sentStateHolder = state.sentStateHolder; + this.connection = connection; + } + + private boolean allowUpdate() { + return !sentStateHolder.headersSent; + } + + public ResponseContentWriter sendHeaders() { + if (allowUpdate()) { + sentStateHolder.headersSent = true; + return new ContentWriterImpl<>(connection, headers); + } + + return new FailedContentWriter<>(); + } + + } + + private static final class HeaderSentStateHolder implements Func0 { + + private boolean headersSent = false; + + @Override + public Object call() { + return headersSent; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridge.java new file mode 100644 index 0000000..0ba820a --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridge.java @@ -0,0 +1,180 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.reactivex.netty.channel.ChannelOperations; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; +import io.reactivex.netty.protocol.http.internal.HttpContentSubscriberEvent; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventPublisher; +import rx.functions.Action0; +import rx.subscriptions.Subscriptions; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.concurrent.TimeUnit.*; + +public class HttpServerToConnectionBridge extends AbstractHttpConnectionBridge { + + private static final Logger logger = Logger.getLogger(HttpServerToConnectionBridge.class.getName()); + + private volatile boolean activeContentSubscriberExists; + + private final Object contentSubGuard = new Object(); + private Queue> pendingContentSubs; /*Guarded by contentSubGuard*/ + private final HttpServerEventPublisher eventPublisher; + private int lastSeenResponseCode; + + public HttpServerToConnectionBridge(HttpServerEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + protected void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, final long startTimeNanos) { + HttpResponse response = (HttpResponse) httpMsg; + if (eventPublisher.publishingEnabled()) { + eventPublisher.onResponseWriteStart(); + } + lastSeenResponseCode = response.status().code(); + } + + @Override + protected void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, + final long headerWriteStartTimeNanos) { + final int _responseCode = lastSeenResponseCode; + + if (eventPublisher.publishingEnabled()) { + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (eventPublisher.publishingEnabled()) { + long endNanos = Clock.onEndNanos(headerWriteStartTimeNanos); + if (future.isSuccess()) { + eventPublisher.onResponseWriteSuccess(endNanos, NANOSECONDS, _responseCode); + } else { + eventPublisher.onResponseWriteFailed(endNanos, NANOSECONDS, future.cause()); + } + } + } + }); + } + } + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof HttpContentSubscriberEvent) { + + final HttpContentSubscriberEvent subscriberEvent = (HttpContentSubscriberEvent) evt; + subscriberEvent.getSubscriber().add(Subscriptions.create(new Action0() { + @Override + public void call() { + HttpContentSubscriberEvent nextSub = null; + synchronized (contentSubGuard) { + if (null != pendingContentSubs) { + nextSub = pendingContentSubs.poll(); + } + } + + activeContentSubscriberExists = null != nextSub; + if (null != nextSub) { + fireContentSubscriberEvent(ctx, nextSub); + } + } + })); + + if (activeContentSubscriberExists) { + synchronized (contentSubGuard) { + if (null == pendingContentSubs) { + pendingContentSubs = new ArrayDeque<>(); /*Guarded by contentSubGuard*/ + } + pendingContentSubs.add(subscriberEvent); + } + return; + } + + activeContentSubscriberExists = true; + } + + // TODO: Handle trailers + super.userEventTriggered(ctx, evt); + } + + @Override + protected boolean isInboundHeader(Object nextItem) { + return nextItem instanceof HttpRequest; + } + + @Override + protected boolean isOutboundHeader(Object nextItem) { + return nextItem instanceof HttpResponse; + } + + @Override + protected Object newHttpObject(Object nextItem, Channel channel) { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestHeadersReceived(); + } + return new HttpServerRequestImpl<>((HttpRequest) nextItem, channel); + } + + @Override + protected void onContentReceived() { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestContentReceived(); + } + } + + @Override + protected void onContentReceiveComplete(long receiveStartTimeNanos) { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onRequestReceiveComplete(Clock.onEndNanos(receiveStartTimeNanos), NANOSECONDS); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + super.channelReadComplete(ctx); + Boolean shouldFlush = ctx.channel().attr(ChannelOperations.FLUSH_ONLY_ON_READ_COMPLETE).get(); + if (null != shouldFlush && shouldFlush) { + ctx.flush(); /*This is a no-op if there is nothing to flush but supports HttpServerResponse.flushOnlyOnReadComplete()*/ + } + } + + private void fireContentSubscriberEvent(ChannelHandlerContext ctx, HttpContentSubscriberEvent event) { + try { + super.userEventTriggered(ctx, event); + } catch (Exception e) { + try { + exceptionCaught(ctx, e); + } catch (Exception e1) { + logger.log(Level.SEVERE, "Exception while handling error in handler.", e1); + } + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/RequestHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/RequestHandler.java new file mode 100644 index 0000000..1404d5e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/RequestHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import rx.Observable; + +/** + * A handler for an {@link HttpServerRequest} to produce a {@link HttpServerResponse} + * + * @param The type of objects received as content from the request. + * @param The type of objects written as content from the response. + */ +public interface RequestHandler { + + /** + * Provides a request and response pair to process. + * + * @param request Http request to process. + * @param response Http response to populate after processing the request. + * + * @return An {@link Observable} that represents the processing of the request. Subscribing to this should start + * the request processing and unsubscribing should cancel the processing. + */ + Observable handle(HttpServerRequest request, HttpServerResponse response); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ResponseContentWriter.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ResponseContentWriter.java new file mode 100644 index 0000000..5f8f73c --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/ResponseContentWriter.java @@ -0,0 +1,334 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.reactivex.netty.protocol.http.TrailingHeaders; +import rx.Observable; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; + +/** + * A facility to optionally write content to the response. + * + *

Thread safety

+ * + * This object is not thread-safe and can not be accessed from multiple threads. + */ +public abstract class ResponseContentWriter extends Observable { + + ResponseContentWriter(OnSubscribe f) { + super(f); + } + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + public abstract ResponseContentWriter write(Observable msgs); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable write(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable write(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. + * Channel is flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + public abstract ResponseContentWriter write(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + public abstract ResponseContentWriter writeAndFlushOnEach(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + public abstract ResponseContentWriter writeString(Observable msgs); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable writeString(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. + * Channel is flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + public abstract ResponseContentWriter writeString(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + public abstract ResponseContentWriter writeStringAndFlushOnEach(Observable msgs); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param msgs Stream of messages to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + public abstract ResponseContentWriter writeBytes(Observable msgs); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + *

Flush

+ * + * The writes are flushed when the passed stream completes. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator); + + /** + * Uses the passed {@link Observable} as the source of content for this request. This method provides a way to + * write trailing headers. + * + * A new instance of {@link TrailingHeaders} will be created using the passed {@code trailerFactory} and the passed + * {@code trailerMutator} will be invoked for every item emitted from the content source, giving a chance to modify + * the trailing headers instance. + * + *

Multiple invocations

+ * + * This method can not be invoked multiple times for the same response as on completion of the passed + * source, it writes the trailing headers and trailing headers can only be written once for an HTTP response. + * So, any subsequent invocation of this method will always emit an error when subscribed. + * + * @param contentSource Content source for the response. + * @param trailerFactory A factory function to create a new {@link TrailingHeaders} per subscription of the content. + * @param trailerMutator A function to mutate the trailing header on each item emitted from the content source. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An new instance of {@link Observable} which can be subscribed to execute the request. + */ + public abstract Observable writeBytes(Observable contentSource, + Func0 trailerFactory, + Func2 trailerMutator, + Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. + * Channel is flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + public abstract ResponseContentWriter writeBytes(Observable msgs, Func1 flushSelector); + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this and all writes done prior to the flush. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + public abstract ResponseContentWriter writeBytesAndFlushOnEach(Observable msgs); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/UriInfoHolder.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/UriInfoHolder.java new file mode 100644 index 0000000..6ae17d1 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/UriInfoHolder.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.handler.codec.http.QueryStringDecoder; + +import java.util.List; +import java.util.Map; + +public class UriInfoHolder { + + private final String uri; + private final String queryString; + private final QueryStringDecoder decoder; + + public UriInfoHolder(String uri) { + this.uri = uri; + + // java.net.URI doesn't support a relaxed mode and fails for many URIs that get used + // in practice + int indexOfStartOfQP = uri.indexOf('?'); + if (-1 != indexOfStartOfQP && uri.length() >= indexOfStartOfQP) { + queryString = uri.substring(indexOfStartOfQP + 1); + } else { + queryString = ""; + } + + decoder = new QueryStringDecoder(getPath(uri)); + } + + // If it is a relative URI then just pass it to the decoder. Otherwise we need to remove + // everything before the path. This method assumes the first '/' after the scheme is the + // start of the path. + private static String getPath(String uri) { + int offset = 0; + if (uri.startsWith("http://")) { + offset = "http://".length(); + } else if (uri.startsWith("https://")) { + offset = "https://".length(); + } + + if (offset == 0) { + return uri; + } else { + int firstSlash = uri.indexOf('/', offset); + return -1 != firstSlash? uri.substring(firstSlash) : uri; + } + } + + public String getRawUriString() { + return uri; + } + + public synchronized String getPath() { + return decoder.path(); + } + + public String getQueryString() { + return queryString; + } + + public synchronized Map> getQueryParameters() { + return decoder.parameters(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisher.java new file mode 100644 index 0000000..408d0f3 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisher.java @@ -0,0 +1,304 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server.events; + +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.events.ListenersHolder; +import io.reactivex.netty.events.internal.SafeEventListener; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventListener; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.subscriptions.CompositeSubscription; + +import java.util.concurrent.TimeUnit; + +public final class HttpServerEventPublisher extends HttpServerEventsListener + implements EventSource, EventPublisher { + + private static final Action1 NEW_REQUEST_ACTION = new Action1() { + @Override + public void call(HttpServerEventsListener l) { + l.onNewRequestReceived(); + } + }; + + private static final Action3 HANDLE_START_ACTION = + new Action3() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit) { + l.onRequestHandlingStart(duration, timeUnit); + } + }; + + private static final Action3 HANDLE_SUCCESS_ACTION = + new Action3() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit) { + l.onRequestHandlingSuccess(duration, timeUnit); + } + }; + + private static final Action4 HANDLE_FAILED_ACTION = + new Action4() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onRequestHandlingFailed(duration, timeUnit, t); + } + }; + + private static final Action1 HEADER_RECIEVED_ACTION = new Action1() { + @Override + public void call(HttpServerEventsListener l) { + l.onRequestHeadersReceived(); + } + }; + + private static final Action1 CONTENT_RECIEVED_ACTION = new Action1() { + @Override + public void call(HttpServerEventsListener l) { + l.onRequestContentReceived(); + } + }; + + private static final Action3 REQ_RECV_COMPLETE_ACTION = + new Action3() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit) { + l.onRequestReceiveComplete(duration, timeUnit); + } + }; + + private static final Action1 RESP_WRITE_START_ACTION = + new Action1() { + @Override + public void call(HttpServerEventsListener l) { + l.onResponseWriteStart(); + } + }; + + private static final Action4 RESP_WRITE_SUCCESS_ACTION = + new Action4() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit, Integer respCode) { + l.onResponseWriteSuccess(duration, timeUnit, respCode); + } + }; + + private static final Action4 RESP_WRITE_FAILED_ACTION = + new Action4() { + @Override + public void call(HttpServerEventsListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onResponseWriteFailed(duration, timeUnit, t); + } + }; + + private final ListenersHolder listeners; + private final TcpServerEventPublisher tcpDelegate; + + public HttpServerEventPublisher(TcpServerEventPublisher tcpDelegate) { + listeners = new ListenersHolder<>(); + this.tcpDelegate = tcpDelegate; + } + + public HttpServerEventPublisher(TcpServerEventPublisher tcpDelegate, ListenersHolder l) { + this.tcpDelegate = tcpDelegate; + listeners = l; + } + + @Override + public void onNewRequestReceived() { + listeners.invokeListeners(NEW_REQUEST_ACTION); + } + + @Override + public void onRequestHandlingStart(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(HANDLE_START_ACTION, duration, timeUnit); + } + + @Override + public void onRequestHandlingSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(HANDLE_SUCCESS_ACTION, duration, timeUnit); + } + + @Override + public void onRequestHandlingFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(HANDLE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onRequestHeadersReceived() { + listeners.invokeListeners(HEADER_RECIEVED_ACTION); + } + + @Override + public void onRequestContentReceived() { + listeners.invokeListeners(CONTENT_RECIEVED_ACTION); + } + + @Override + public void onRequestReceiveComplete(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(REQ_RECV_COMPLETE_ACTION, duration, timeUnit); + } + + @Override + public void onResponseWriteStart() { + listeners.invokeListeners(RESP_WRITE_START_ACTION); + } + + @Override + public void onResponseWriteSuccess(final long duration, final TimeUnit timeUnit, final int responseCode) { + listeners.invokeListeners(RESP_WRITE_SUCCESS_ACTION, duration, timeUnit, responseCode); + } + + @Override + public void onResponseWriteFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(RESP_WRITE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseStart() { + tcpDelegate.onConnectionCloseStart(); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteStart() { + tcpDelegate.onWriteStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + tcpDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onFlushStart() { + tcpDelegate.onFlushStart(); + } + + @Override + public void onByteRead(long bytesRead) { + tcpDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + tcpDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onConnectionHandlingFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionHandlingSuccess(duration, timeUnit); + } + + @Override + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionHandlingStart(duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event) { + tcpDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + tcpDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + tcpDelegate.onCustomEvent(event, throwable); + } + + @Override + public void onNewClientConnected() { + tcpDelegate.onNewClientConnected(); + } + + @Override + public boolean publishingEnabled() { + return listeners.publishingEnabled(); + } + + @Override + public Subscription subscribe(HttpServerEventsListener listener) { + if (!SafeEventListener.class.isAssignableFrom(listener.getClass())) { + listener = new SafeHttpServerEventsListener(listener); + } + + CompositeSubscription cs = new CompositeSubscription(); + cs.add(listeners.subscribe(listener)); + + TcpServerEventListener tcpListener = listener; + + if (listener instanceof SafeHttpServerEventsListener) { + tcpListener = ((SafeHttpServerEventsListener) listener).unwrap(); + } + + cs.add(tcpDelegate.subscribe(tcpListener)); + + return cs; + } + + public HttpServerEventPublisher copy(TcpServerEventPublisher newTcpDelegate) { + return new HttpServerEventPublisher(newTcpDelegate, listeners.copy()); + } + + /*Visible for testing*/ListenersHolder getListeners() { + return listeners; + } + + /*Visible for testing*/TcpServerEventPublisher getTcpDelegate() { + return tcpDelegate; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListener.java new file mode 100644 index 0000000..72de587 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListener.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server.events; + +import io.reactivex.netty.protocol.http.server.HttpServer; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventListener; + +import java.util.concurrent.TimeUnit; + +/** + * A listener for all events published by {@link HttpServer} + */ +public abstract class HttpServerEventsListener extends TcpServerEventListener { + + /** + * Event whenever a new request is received by the server. + */ + public void onNewRequestReceived() {} + + /** + * When request handling started. + * + * @param duration Time between the receiving request and start of processing. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onRequestHandlingStart(long duration, TimeUnit timeUnit) { } + + /** + * When request handling completes successfully. + * + * @param duration Time between the request processing start and completion. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onRequestHandlingSuccess(long duration, TimeUnit timeUnit) {} + + /** + * When request handling completes with an error. + * + * @param duration Time between the request processing start and failure. + * @param timeUnit Time unit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onRequestHandlingFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} + + /** + * Whenever request headers are received. + */ + public void onRequestHeadersReceived() {} + + /** + * Event whenever an HTTP request content is received (an HTTP request can have multiple content chunks, in which + * case this event will be fired as many times for the same request). + */ + public void onRequestContentReceived() {} + + /** + * Event when the request receive is completed. + * + * @param duration Time taken between receiving the request headers and completion of request. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onRequestReceiveComplete(long duration, TimeUnit timeUnit) {} + + /** + * Event when the response write starts. + */ + @SuppressWarnings("unused") + public void onResponseWriteStart() {} + + /** + * Event when the response write is completed successfully. + * + * @param duration Time taken between write start and completion. + * @param timeUnit Time unit for the duration. + * @param responseCode HTTP response code for the response. + */ + @SuppressWarnings("unused") + public void onResponseWriteSuccess(long duration, TimeUnit timeUnit, int responseCode) {} + + /** + * Event when the response write is completed with an error. + * + * @param duration Time taken between write start and completion. + * @param timeUnit Time unit for the duration. + * @param throwable Error that caused the failure. + */ + @SuppressWarnings("unused") + public void onResponseWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) {} +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/SafeHttpServerEventsListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/SafeHttpServerEventsListener.java new file mode 100644 index 0000000..7fa50da --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/server/events/SafeHttpServerEventsListener.java @@ -0,0 +1,263 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server.events; + +import io.reactivex.netty.events.internal.SafeEventListener; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +final class SafeHttpServerEventsListener extends HttpServerEventsListener implements SafeEventListener { + + private final AtomicBoolean completed = new AtomicBoolean(); + private final HttpServerEventsListener delegate; + + public SafeHttpServerEventsListener(HttpServerEventsListener delegate) { + this.delegate = delegate; + } + + @Override + public void onCompleted() { + if (completed.compareAndSet(false, true)) { + delegate.onCompleted(); + } + } + + @Override + public void onNewRequestReceived() { + if (!completed.get()) { + delegate.onNewRequestReceived(); + } + } + + @Override + public void onRequestHandlingStart(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onRequestHandlingStart(duration, timeUnit); + } + } + + @Override + public void onRequestHandlingSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onRequestHandlingSuccess(duration, timeUnit); + } + } + + @Override + public void onRequestHandlingFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onRequestHandlingFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onRequestHeadersReceived() { + if (!completed.get()) { + delegate.onRequestHeadersReceived(); + } + } + + @Override + public void onRequestContentReceived() { + if (!completed.get()) { + delegate.onRequestContentReceived(); + } + } + + @Override + public void onRequestReceiveComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onRequestReceiveComplete(duration, timeUnit); + } + } + + @Override + public void onResponseWriteStart() { + if (!completed.get()) { + delegate.onResponseWriteStart(); + } + } + + @Override + public void onResponseWriteSuccess(long duration, TimeUnit timeUnit, int responseCode) { + if (!completed.get()) { + delegate.onResponseWriteSuccess(duration, timeUnit, responseCode); + } + } + + @Override + public void onResponseWriteFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onResponseWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onNewClientConnected() { + if (!completed.get()) { + delegate.onNewClientConnected(); + } + } + + @Override + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionHandlingStart(duration, timeUnit); + } + } + + @Override + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionHandlingSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionHandlingFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onByteRead(long bytesRead) { + if (!completed.get()) { + delegate.onByteRead(bytesRead); + } + } + + @Override + public void onByteWritten(long bytesWritten) { + if (!completed.get()) { + delegate.onByteWritten(bytesWritten); + } + } + + @Override + public void onFlushStart() { + if (!completed.get()) { + delegate.onFlushStart(); + } + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onFlushComplete(duration, timeUnit); + } + } + + @Override + public void onWriteStart() { + if (!completed.get()) { + delegate.onWriteStart(); + } + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onWriteSuccess(duration, timeUnit); + } + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onConnectionCloseStart() { + if (!completed.get()) { + delegate.onConnectionCloseStart(); + } + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionCloseSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event) { + if (!completed.get()) { + delegate.onCustomEvent(event); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, throwable); + } + } + + public HttpServerEventsListener unwrap() { + return delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SafeHttpServerEventsListener)) { + return false; + } + + SafeHttpServerEventsListener that = (SafeHttpServerEventsListener) o; + + return !(delegate != null? !delegate.equals(that.delegate) : that.delegate != null); + + } + + @Override + public int hashCode() { + return delegate != null? delegate.hashCode() : 0; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/ServerSentEvent.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/ServerSentEvent.java new file mode 100644 index 0000000..84879d9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/ServerSentEvent.java @@ -0,0 +1,329 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.sse; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.Unpooled; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An object representing a server-sent-event following the SSE specifications + * + * A server sent event is composed of the following: + * + *
    +
  • Event id: This is the last event id seen on the stream this event was received. This can be null, if no id is received.
  • +
  • Event type: The last seen event type seen on the stream this event was received. This can be null, if no type is received.
  • +
  • Data: This is the actual event data.
  • +
+ * + *

Type

+ * + * A {@link ServerSentEvent} is of the type {@link Type#Data} unless it is explicitly passed on creation. + * + *

Memory management

+ * + * This is an implementation of {@link ByteBufHolder} so it is required to be explicitly released by calling + * {@link #release()} when this instance is no longer required. + */ +public class ServerSentEvent implements ByteBufHolder { + + private static final Logger logger = Logger.getLogger(ServerSentEvent.class.getName()); + + private static Charset sseEncodingCharset; + + static { + try { + sseEncodingCharset = StandardCharsets.UTF_8; + } catch (Exception e) { + logger.log(Level.SEVERE, "UTF-8 charset not available. Since SSE only contains UTF-8 data, we can not read SSE data."); + sseEncodingCharset = null; + } + } + + public enum Type { + Data, + Id, + EventType + } + + private final Type type; + /*This is required to make sure we allocate ByteBuf only inside an eventloop, else the ByteBuf pool will grow in the + * owner thread*/ + private final String dataAsString; + private final ByteBuf data; + private final ByteBuf eventId; + private final ByteBuf eventType; + + public ServerSentEvent(Type type, ByteBuf data) { + this(type, null, null, data); + } + + public ServerSentEvent(ByteBuf data) { + this(Type.Data, data); + } + + public ServerSentEvent(ByteBuf eventId, ByteBuf eventType, ByteBuf data) { + this(Type.Data, eventId, eventType, data); + } + + protected ServerSentEvent(Type type, ByteBuf eventId, ByteBuf eventType, ByteBuf data) { + dataAsString = null; + this.data = data; + this.type = type; + this.eventId = eventId; + this.eventType = eventType; + } + + private ServerSentEvent(String data) { + dataAsString = data; + this.data = null; + type = Type.Data; + eventId = null; + eventType = null; + } + + /** + * The type of this event. For events which contain an event Id or event type along with data, the type is still + * {@link Type#Data}. The type will be {@link Type#Id} or {@link Type#EventType} only if the event just contains the + * event type or event id and no data. + * + * @return Type of this event. + */ + public Type getType() { + return type; + } + + public boolean hasEventId() { + return null != eventId; + } + + public boolean hasEventType() { + return null != eventType; + } + + public ByteBuf getEventId() { + return eventId; + } + + public String getEventIdAsString() { + return eventId.toString(getSseCharset()); + } + + public ByteBuf getEventType() { + return eventType; + } + + public String getEventTypeAsString() { + return eventType.toString(getSseCharset()); + } + + public boolean hasDataAsString() { + return null != dataAsString; + } + + public String contentAsString() { + return null != dataAsString ? dataAsString : data.toString(getSseCharset()); + } + + @Override + public ByteBuf content() { + return null != data ? data : Unpooled.buffer().writeBytes(dataAsString.getBytes(getSseCharset())); + } + + @Override + public ByteBufHolder copy() { + if (hasDataAsString()) { + return new ServerSentEvent(dataAsString); + } else { + return new ServerSentEvent(type, null != eventId? eventId.copy() : null, + null != eventType? eventType.copy() : null, data.copy()); + } + } + + @Override + public ByteBufHolder duplicate() { + if (hasDataAsString()) { + return new ServerSentEvent(dataAsString); + } else { + return new ServerSentEvent(type, null != eventId ? eventId.duplicate() : null, + null != eventType ? eventType.duplicate() : null, data.duplicate()); + } + } + + @Override + public ByteBufHolder retainedDuplicate() { + return duplicate().retain(); + } + + @Override + public ByteBufHolder replace(ByteBuf content) { + return new ServerSentEvent(content); + } + + @Override + public int refCnt() { + return hasDataAsString() ? 1 : data.refCnt(); // Ref count is consistent across data, eventId and eventType + } + + @Override + public ByteBufHolder retain() { + if(hasEventId()) { + eventId.retain(); + } + if(hasEventType()) { + eventType.retain(); + } + if (!hasDataAsString()) { + data.retain(); + } + return this; + } + + @Override + public ByteBufHolder retain(int increment) { + if(hasEventId()) { + eventId.retain(increment); + } + if(hasEventType()) { + eventType.retain(increment); + } + if (!hasDataAsString()) { + data.retain(increment); + } + return this; + } + + @Override + public ByteBufHolder touch() { + return touch(null); + } + + @Override + public ByteBufHolder touch(Object hint) { + if (!hasDataAsString()) { + data.touch(hint); + } + return this; + } + + @Override + public boolean release() { + return data.release(1); + } + + @Override + public boolean release(int decrement) { + if(hasEventId()) { + eventId.release(decrement); + } + if(hasEventType()) { + eventType.release(decrement); + } + return data.release(decrement); + } + + /** + * Creates a {@link ServerSentEvent} instance with an event id. + * + * @param eventId Id for the event. + * @param data Data for the event. + * + * @return The {@link ServerSentEvent} instance. + */ + public static ServerSentEvent withEventId(ByteBuf eventId, ByteBuf data) { + return new ServerSentEvent(eventId, null, data); + } + + /** + * Creates a {@link ServerSentEvent} instance with an event type. + * + * @param eventType Type for the event. + * @param data Data for the event. + * + * @return The {@link ServerSentEvent} instance. + */ + public static ServerSentEvent withEventType(ByteBuf eventType, ByteBuf data) { + return new ServerSentEvent(null, eventType, data); + } + + /** + * Creates a {@link ServerSentEvent} instance with an event id and type. + * + * @param eventType Type for the event. + * @param eventId Id for the event. + * @param data Data for the event. + * + * @return The {@link ServerSentEvent} instance. + */ + public static ServerSentEvent withEventIdAndType(ByteBuf eventId, ByteBuf eventType, ByteBuf data) { + return new ServerSentEvent(eventId, eventType, data); + } + + /** + * Creates a {@link ServerSentEvent} instance with data. + * + * @param data Data for the event. + * + * @return The {@link ServerSentEvent} instance. + */ + public static ServerSentEvent withData(ByteBuf data) { + return new ServerSentEvent(data); + } + + /** + * Creates a {@link ServerSentEvent} instance with data. + * + * @param data Data for the event. + * + * @return The {@link ServerSentEvent} instance. + */ + public static ServerSentEvent withData(String data) { + return new ServerSentEvent(data); + } + + protected Charset getSseCharset() { + return null == sseEncodingCharset ? Charset.forName("UTF-8") : sseEncodingCharset; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (hasEventId()) { + sb.append("id: "); + sb.append(getEventIdAsString()); + sb.append('\n'); + } + + if (hasEventType()) { + sb.append("event: "); + sb.append(getEventTypeAsString()); + sb.append('\n'); + } + + sb.append("data: "); + sb.append(contentAsString()); + sb.append('\n'); + + return sb.toString(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoder.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoder.java new file mode 100644 index 0000000..dd8ba8b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoder.java @@ -0,0 +1,355 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.sse.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.ByteProcessor; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent.Type; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A decoder to decode Server sent events into {@link ServerSentEvent} + */ +public class ServerSentEventDecoder extends MessageToMessageDecoder { + + private static final Logger logger = Logger.getLogger(ServerSentEventDecoder.class.getName()); + + public static final int DEFAULT_MAX_FIELD_LENGTH = 100; + + private static final char[] EVENT_ID_FIELD_NAME = "event".toCharArray(); + private static final char[] DATA_FIELD_NAME = "data".toCharArray(); + private static final char[] ID_FIELD_NAME = "id".toCharArray(); + + protected static final ByteProcessor SKIP_TILL_LINE_DELIMITER_PROCESSOR = new ByteProcessor() { + @Override + public boolean process(byte value) throws Exception { + return !isLineDelimiter((char) value); + } + }; + + protected static final ByteProcessor SKIP_LINE_DELIMITERS_AND_SPACES_PROCESSOR = new ByteProcessor() { + @Override + public boolean process(byte value) throws Exception { + return isLineDelimiter((char) value) || (char) value == ' '; + } + }; + + protected static final ByteProcessor SKIP_COLON_AND_WHITE_SPACE_PROCESSOR = new ByteProcessor() { + @Override + public boolean process(byte value) throws Exception { + char valueChar = (char) value; + return valueChar == ':' || valueChar == ' '; + } + }; + + protected static final ByteProcessor SCAN_COLON_PROCESSOR = new ByteProcessor() { + @Override + public boolean process(byte value) throws Exception { + return (char) value != ':'; + } + }; + + protected static final ByteProcessor SCAN_EOL_PROCESSOR = value -> !isLineDelimiter((char) value); + + private static Charset sseEncodingCharset; + + static { + try { + sseEncodingCharset = StandardCharsets.UTF_8; + } catch (Exception e) { + logger.log(Level.SEVERE, "UTF-8 charset not available. Since SSE only contains UTF-8 data, we can not read SSE data."); + sseEncodingCharset = null; + } + } + + private enum State { + SkipColonAndWhiteSpaces,// Skip colon and all whitespaces after reading field name. + SkipLineDelimitersAndSpaces,// Skip all line delimiters after field value end. + DiscardTillEOL,// On recieving an illegal/unidentified field, ignore everything till EOL. + ReadFieldName, // Read till a colon to get the name of the field. + ReadFieldValue // Read value till the line delimiter. + } + + /** + * Release of these buffers happens in the following ways: + * + * 1) If this was a data buffer, it is released when ServerSentEvent is released. + * 2) If this was an eventId buffer, it is released when next Id arrives or when the connection + * is closed. + * 3) If this was an eventType buffer, it is released when next type arrives or when the connection + * is closed. + */ + private ByteBuf lastEventId; + private ByteBuf lastEventType; + private ByteBuf incompleteData; // Can be field value of name, according to the current state. + + private Type currentFieldType; + + private State state = State.ReadFieldName; + + @Override + protected void decode(ChannelHandlerContext ctx, HttpContent httpContent, List out) { + + final ByteBuf in = httpContent.content(); + + if (null == sseEncodingCharset) { + throw new IllegalArgumentException("Can not read SSE data as UTF-8 charset is not available."); + } + + while (in.isReadable()) { + + final int readerIndexAtStart = in.readerIndex(); + + switch (state) { + case SkipColonAndWhiteSpaces: + if (skipColonAndWhiteSpaces(in)) { + state = State.ReadFieldValue; + } + break; + case SkipLineDelimitersAndSpaces: + if (skipLineDelimiters(in)) { + state = State.ReadFieldName; + } + break; + case DiscardTillEOL: + if(skipTillEOL(in)) { + state = State.SkipLineDelimitersAndSpaces; + } + break; + case ReadFieldName: + final int indexOfColon = scanAndFindColon(in); + + if (-1 == indexOfColon) { // No colon found + // Accumulate data into the field name buffer. + if (null == incompleteData) { + incompleteData = ctx.alloc().buffer(); + } + // accumulate into incomplete data buffer to be used when the full data arrives. + incompleteData.writeBytes(in); + } else { + int fieldNameLengthInTheCurrentBuffer = indexOfColon - readerIndexAtStart; + + ByteBuf fieldNameBuffer; + if (null != incompleteData) { + // Read the remaining data into the temporary buffer + in.readBytes(incompleteData, fieldNameLengthInTheCurrentBuffer); + fieldNameBuffer = incompleteData; + incompleteData = null; + } else { + // Consume the data from the input buffer. + fieldNameBuffer = ctx.alloc().buffer(fieldNameLengthInTheCurrentBuffer, + fieldNameLengthInTheCurrentBuffer); + in.readBytes(fieldNameBuffer, fieldNameLengthInTheCurrentBuffer); + } + + state = State.SkipColonAndWhiteSpaces; // We have read the field name, next we should skip colon & WS. + try { + currentFieldType = readCurrentFieldTypeFromBuffer(fieldNameBuffer); + } finally { + if (null == currentFieldType) { + state = State.DiscardTillEOL; // Ignore this event completely. + } + fieldNameBuffer.release(); + } + } + break; + case ReadFieldValue: + + final int endOfLineStartIndex = scanAndFindEndOfLine(in); + + + if (-1 == endOfLineStartIndex) { // End of line not found, accumulate data into a temporary buffer. + if (null == incompleteData) { + incompleteData = ctx.alloc().buffer(in.readableBytes()); + } + // accumulate into incomplete data buffer to be used when the full data arrives. + incompleteData.writeBytes(in); + } else { // Read the data till end of line into the value buffer. + final int bytesAvailableInThisIteration = endOfLineStartIndex - readerIndexAtStart; + if (null == incompleteData) { + incompleteData = ctx.alloc().buffer(bytesAvailableInThisIteration, + bytesAvailableInThisIteration); + } + incompleteData.writeBytes(in, bytesAvailableInThisIteration); + + switch (currentFieldType) { + case Data: + if (incompleteData.isReadable()) { + out.add(ServerSentEvent.withEventIdAndType(lastEventId, lastEventType, + incompleteData)); + } else { + incompleteData.release(); + } + break; + case Id: + if (incompleteData.isReadable()) { + lastEventId = incompleteData; + } else { + incompleteData.release(); + lastEventId = null; + } + break; + case EventType: + if (incompleteData.isReadable()) { + lastEventType = incompleteData; + } else { + incompleteData.release(); + lastEventType = null; + } + break; + } + /* + * Since all data is read, reset the incomplete data to null. Release of this buffer happens in + * the following ways + * 1) If this was a data buffer, it is released when ServerSentEvent is released. + * 2) If this was an eventId buffer, it is released when next Id arrives or when the connection + * is closed. + * 3) If this was an eventType buffer, it is released when next type arrives or when the connection + * is closed. + */ + incompleteData = null; + state = State.SkipLineDelimitersAndSpaces; // Skip line delimiters after reading a field value completely. + } + break; + } + } + + + if (httpContent instanceof LastHttpContent) { + ctx.fireChannelRead(httpContent); // Since the content is already consumed above (by the SSEDecoder), this is just + // as sending just trailing headers. This is critical to mark the end of stream. + } + } + + private static ServerSentEvent.Type readCurrentFieldTypeFromBuffer(final ByteBuf fieldNameBuffer) { + /* + * This code tries to eliminate the need of creating a string from the ByteBuf as the field names are very + * constrained. The algorithm is as follows: + * + * -- Scan the bytes in the buffer. + * -- Ignore an leading whitespaces + * -- If the first byte matches the expected field names then use the matching field name char array to verify + * the rest of the field name. + * -- If the first byte does not match, reject the field name. + * -- After the first byte, exact match the rest of the field name with the expected field name, byte by byte. + * -- If the name does not exactly match the expected value, then reject the field name. + */ + ServerSentEvent.Type toReturn = ServerSentEvent.Type.Data; + skipLineDelimiters(fieldNameBuffer); + int readableBytes = fieldNameBuffer.readableBytes(); + final int readerIndexAtStart = fieldNameBuffer.readerIndex(); + char[] fieldNameToVerify = DATA_FIELD_NAME; + boolean verified = false; + int actualFieldNameIndexToCheck = 0; // Starts with 1 as the first char is validated by equality. + for (int i = readerIndexAtStart; i < readerIndexAtStart + readableBytes; i++) { + final char charAtI = (char) fieldNameBuffer.getByte(i); + + if (i == readerIndexAtStart) { + switch (charAtI) { // See which among the known field names this buffer belongs. + case 'e': + fieldNameToVerify = EVENT_ID_FIELD_NAME; + toReturn = ServerSentEvent.Type.EventType; + break; + case 'd': + fieldNameToVerify = DATA_FIELD_NAME; + toReturn = ServerSentEvent.Type.Data; + break; + case 'i': + fieldNameToVerify = ID_FIELD_NAME; + toReturn = ServerSentEvent.Type.Id; + break; + default: + return null; + } + } else { + if (++actualFieldNameIndexToCheck >= fieldNameToVerify.length || charAtI != fieldNameToVerify[actualFieldNameIndexToCheck]) { + // If the character does not match or the buffer is bigger than the expected name, then discard. + verified = false; + break; + } else { + // Verified till now. If all characters are matching then this stays as verified, else changed to false. + verified = true; + } + } + } + + if (verified) { + return toReturn; + } else { + return null; + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + if (null != lastEventId) { + lastEventId.release(); + } + if (null != lastEventType) { + lastEventType.release(); + } + if (null != incompleteData) { + incompleteData.release(); + } + } + + protected static int scanAndFindColon(ByteBuf byteBuf) { + return byteBuf.forEachByte(SCAN_COLON_PROCESSOR); + } + + protected static int scanAndFindEndOfLine(ByteBuf byteBuf) { + return byteBuf.forEachByte(SCAN_EOL_PROCESSOR); + } + + protected static boolean skipLineDelimiters(ByteBuf byteBuf) { + return skipTillMatching(byteBuf, SKIP_LINE_DELIMITERS_AND_SPACES_PROCESSOR); + } + + protected static boolean skipColonAndWhiteSpaces(ByteBuf byteBuf) { + return skipTillMatching(byteBuf, SKIP_COLON_AND_WHITE_SPACE_PROCESSOR); + } + + private static boolean skipTillEOL(ByteBuf in) { + return skipTillMatching(in, SKIP_TILL_LINE_DELIMITER_PROCESSOR); + } + + protected static boolean skipTillMatching(ByteBuf byteBuf, ByteProcessor processor) { + final int lastIndexProcessed = byteBuf.forEachByte(processor); + if (-1 == lastIndexProcessed) { + byteBuf.readerIndex(byteBuf.readerIndex() + byteBuf.readableBytes()); // If all the remaining bytes are to be ignored, discard the buffer. + } else { + byteBuf.readerIndex(lastIndexProcessed); + } + + return -1 != lastIndexProcessed; + } + + protected static boolean isLineDelimiter(char c) { + return c == '\r' || c == '\n'; + } +} \ No newline at end of file diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoder.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoder.java new file mode 100644 index 0000000..71dd99e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoder.java @@ -0,0 +1,123 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.sse.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.util.ByteProcessor; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; + +/** + * An encoder to handle {@link io.reactivex.netty.protocol.http.sse.ServerSentEvent} encoding for an HTTP server. + * + * This encoder will encode any {@link io.reactivex.netty.protocol.http.sse.ServerSentEvent} to {@link ByteBuf} and also set the appropriate HTTP Response + * headers required for SSE + */ +@ChannelHandler.Sharable +public class ServerSentEventEncoder extends ChannelOutboundHandlerAdapter { + + private static final byte[] EVENT_PREFIX_BYTES = "event: ".getBytes(); + private static final byte[] NEW_LINE_AS_BYTES = "\n".getBytes(); + private static final byte[] ID_PREFIX_AS_BYTES = "id: ".getBytes(); + private static final byte[] DATA_PREFIX_AS_BYTES = "data: ".getBytes(); + private final boolean splitSseData; + + public ServerSentEventEncoder() { + this(false); + } + + /** + * Splits the SSE data on new line and create multiple "data" events if {@code splitSseData} is {@code true} + * + * @param splitSseData {@code true} if the SSE data is to be splitted on new line to create multiple "data" events. + */ + public ServerSentEventEncoder(boolean splitSseData) { + this.splitSseData = splitSseData; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + + Object msgToWriteFurther = msg; + + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + /*Set the content-type for SSE*/ + response.headers().set(CONTENT_TYPE, "text/event-stream"); + } else if (msg instanceof ServerSentEvent) { + + final ServerSentEvent serverSentEvent = (ServerSentEvent) msg; + + final ByteBuf out = ctx.alloc().buffer(); + msgToWriteFurther = out; + + if (serverSentEvent.hasEventType()) { // Write event type, if available + out.writeBytes(EVENT_PREFIX_BYTES); + out.writeBytes(serverSentEvent.getEventType()); + out.writeBytes(NEW_LINE_AS_BYTES); + } + + if (serverSentEvent.hasEventId()) { // Write event id, if available + out.writeBytes(ID_PREFIX_AS_BYTES); + out.writeBytes(serverSentEvent.getEventId()); + out.writeBytes(NEW_LINE_AS_BYTES); + } + + final ByteBuf content; + if (serverSentEvent.hasDataAsString()) { + /*Allocate ByteBuf only in the eventloop*/ + content = ctx.alloc().buffer().writeBytes(serverSentEvent.contentAsString().getBytes()); + } else { + content = serverSentEvent.content(); + } + + if (splitSseData) { + while (content.isReadable()) { // Scan the buffer and split on new line into multiple data lines. + final int readerIndexAtStart = content.readerIndex(); + int newLineIndex = content.forEachByte(new ByteProcessor() { + @Override + public boolean process(byte value) throws Exception { + return (char) value != '\n'; + } + }); + if (-1 == newLineIndex) { // No new line, write the buffer as is. + out.writeBytes(DATA_PREFIX_AS_BYTES); + out.writeBytes(content); + out.writeBytes(NEW_LINE_AS_BYTES); + } else { // Write the buffer till the new line and then iterate this loop + out.writeBytes(DATA_PREFIX_AS_BYTES); + out.writeBytes(content, newLineIndex - readerIndexAtStart); + content.readerIndex(content.readerIndex() + 1); + out.writeBytes(NEW_LINE_AS_BYTES); + } + } + } else { // write the buffer with data prefix and new line post fix. + out.writeBytes(DATA_PREFIX_AS_BYTES); + out.writeBytes(content); + out.writeBytes(NEW_LINE_AS_BYTES); + } + } + + ctx.write(msgToWriteFurther, promise); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/util/HttpContentStringLineDecoder.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/util/HttpContentStringLineDecoder.java new file mode 100644 index 0000000..9891747 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/util/HttpContentStringLineDecoder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.util; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.LastHttpContent; +import io.reactivex.netty.util.LineReader; +import io.reactivex.netty.util.StringLineDecoder; + +import java.util.List; + +/** + * A handler just like {@link StringLineDecoder} but works on {@link HttpContent}. This handler will decode the HTTP + * content as lines, separated by a new line. + */ +public class HttpContentStringLineDecoder extends MessageToMessageDecoder { + + private final LineReader reader = new LineReader(); + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + reader.dispose(); + super.handlerRemoved(ctx); + } + + @Override + protected void decode(ChannelHandlerContext ctx, HttpContent msg, List out) throws Exception { + if (msg instanceof LastHttpContent) { + reader.decodeLast(msg.content(), out, ctx.alloc()); + out.add(LastHttpContent.EMPTY_LAST_CONTENT); + } else { + reader.decode(msg.content(), out, ctx.alloc()); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/WebSocketConnection.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/WebSocketConnection.java new file mode 100644 index 0000000..dba1ebf --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/WebSocketConnection.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws; + +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.reactivex.netty.channel.Connection; +import rx.Observable; +import rx.annotations.Beta; +import rx.functions.Func1; + +/** + * A WebSocket connection which is used to read/write {@link WebSocketFrame}s. + */ +public final class WebSocketConnection { + + private final Connection delegate; + + public WebSocketConnection(Connection delegate) { + this.delegate = delegate; + } + + /** + * Returns the input stream for this connection, until a {@link CloseWebSocketFrame} is received. The terminal + * {@link CloseWebSocketFrame} is included in the returned stream. + * + * @return The input stream for this connection. + */ + public Observable getInput() { + return getInput(true); + } + + /** + * Returns the input stream for this connection. If {@code untilCloseFrame} is {@code true} then the returned stream + * completes after receiving (and emitting) a {@link CloseWebSocketFrame}, otherwise, it completes with an error + * when the underlying channel is closed. + * + * @return The input stream for this connection. + */ + @Beta + public Observable getInput(boolean untilCloseFrame) { + Observable rawInput = delegate.getInput(); + + if (untilCloseFrame) { + return rawInput.takeUntil(new Func1() { + @Override + public Boolean call(WebSocketFrame webSocketFrame) { + return webSocketFrame instanceof CloseWebSocketFrame; + } + }); + } else { + return rawInput; + } + } + + /** + * Writes a stream of frames on this connection. The writes are flushed on completion of the stream, if other flush + * strategies are required, one must use {@link #write(Observable, Func1)} or + * {@link #writeAndFlushOnEach(Observable)} + * + * @param msgs Stream of frames to write. + * + * @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable} + * will replay the write on the channel. + */ + public Observable write(Observable msgs) { + return delegate.write(msgs); + } + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before + * subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those + * writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is + * flushed, iff this function returns, {@code true}. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the + * {@code flushSelector} returns {@code true} + */ + public Observable write(Observable msgs, Func1 flushSelector) { + return delegate.write(msgs, flushSelector); + } + + /** + * On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel + * and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the + * returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this + * write does not, the returned {@link Observable} will not fail. + * + * @param msgs Message stream to write. + * + * @return An {@link Observable} representing the result of this write. Every + * subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every + * write. + */ + public Observable writeAndFlushOnEach(Observable msgs) { + return delegate.writeAndFlushOnEach(msgs); + } + + /** + * Flushes all writes, if any, before calling the flush. + */ + public void flush() { + delegate.flush(); + } + + /** + * Flushes any pending writes and closes the connection. Same as calling {@code close(true)} + * + * @return {@link Observable} representing the result of close. + */ + public Observable close() { + return delegate.close(); + } + + /** + * Closes this channel after flushing all pending writes. + * + * @return {@link Observable} representing the result of close and flush. + */ + public Observable close(boolean flush) { + return delegate.close(flush); + } + + /** + * Returns an {@link Observable} that completes when this connection is closed. + * + * @return An {@link Observable} that completes when this connection is closed. + */ + public Observable closeListener() { + return delegate.closeListener(); + } + + /** + * Closes the connection immediately. Same as calling {@link #close()} and subscribing to the returned + * {@code Observable} + */ + public void closeNow() { + delegate.closeNow(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnection.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnection.java new file mode 100644 index 0000000..b0d5f27 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnection.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.ws.client; + +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import rx.Observable; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.annotations.Experimental; +import rx.functions.Action0; +import rx.functions.Actions; +import rx.functions.Func1; + +/** + * An operator to cache a {@link WebSocketConnection} until it closes, upon which the source that re-creates an HTTP + * upgrade request to get a fresh {@link WebSocketConnection} is subscribed, to refresh the stale connection in the + * cache. + * + * A typical usage example for this operator is: + * +
+ {@code
+     HttpClient.newClient(socketAddress)
+               .createGet("/ws")
+               .requestWebSocketUpgrade()
+               .map(WebSocketResponse::getWebSocketConnection)
+               .nest()
+               .lift(new OperatorCacheSingleWebsocketConnection())
+ }
+ 
+ * + * Since multiple subscriptions to {@link WebSocketResponse#getWebSocketConnection()} do not re-run the original HTTP + * upgrade request, this operator expects the source {@code Observable} to be passed to it, so that on close of the + * cached {@link WebSocketConnection}, it can re-subscribe to the original HTTP request and create a fresh connection. + * This is the reason the above code uses {@link Observable#nest()} to get a reference to the source {@code Observable}. + * + *

Cache liveness guarantees

+ * + * Although, this operator will make sure that when the cached connection has terminated, the next refresh will + * re-subscribe to the source, there is no guarantee that a dead connection is never emitted from this operator as it + * completely depends on the timing of when the connection terminates and when a new subscription arrives. The two + * events can be concurrent and hence unpredictable. + */ +@Experimental +public class OperatorCacheSingleWebsocketConnection + implements Operator>> { + + private boolean subscribedToSource; /*Guarded by this*/ + private Observable cachedSource; /*Guarded by this*/ + + @Override + public Subscriber>> + call(final Subscriber subscriber) { + + return new Subscriber>>(subscriber) { + + private volatile boolean anItemEmitted; + + @Override + public void onCompleted() { + if (!anItemEmitted) { + subscriber.onError(new IllegalStateException("No Observable emitted from source.")); + } + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(Observable> source) { + anItemEmitted = true; + + /** + * The idea below is for using a single cache {@code Observable} so that the cache operator can cache + * the generated connection. However, when the cached connection is terminated, a new cached source + * must be generated to be used for subsequent subscriptions. + * As the only way to re-run the original HTTP upgrade request, to obtain a fresh connection, is to + * subscribe to the {@code Observable>}, that is the reason the below + * code uses a {@code flatmap} to transform {@code Observable>} to an + * {@code Observable} and still keeping the ability to re-subscribe to the original + * {@code Observable>}. + */ + final Observable _cachedSource; + final Observable o = source.flatMap( + new Func1, Observable>() { + @Override + public Observable call(Observable connSource) { + /*This is for flatmap to subscribe to the nested {@code Observable}*/ + return connSource; + } + }).map(new Func1() { + @Override + public WebSocketConnection call(WebSocketConnection connection) { + Observable lifecycle = connection.closeListener(); + lifecycle = lifecycle.onErrorResumeNext(Observable.empty()) + .doAfterTerminate(new Action0() { + @Override + public void call() { + synchronized (OperatorCacheSingleWebsocketConnection.this) { + // refresh the source on next subscribe + subscribedToSource = false; + } + } + }); + subscriber.add(lifecycle.subscribe(Actions.empty())); + return connection; + } + }).cache(); + + synchronized (OperatorCacheSingleWebsocketConnection.this) { + if (!subscribedToSource) { + subscribedToSource = true; + /*From here on, all subscriptions will use the newly created cached source which on first + subscription will re-run the original HTTP upgrade request and get a fresh WS connection*/ + cachedSource = o; + } + + _cachedSource = cachedSource; + } + + _cachedSource.unsafeSubscribe(subscriber); + } + }; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketRequest.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketRequest.java new file mode 100644 index 0000000..9dbb4e3 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.client; + +import rx.Observable; + +/** + * A WebSocket upgrade HTTP request that will generate a {@link WebSocketResponse} + * + * @param The type of the content received in the HTTP response, in case, the upgrade was rejected by the server. + */ +public abstract class WebSocketRequest extends Observable> { + + protected WebSocketRequest(OnSubscribe> f) { + super(f); + } + + /** + * Specify any sub protocols that are to be requested to the server as specified by the + * specifications + * + * @param subProtocols Sub protocols to request. + * + * @return A new instance of {@link WebSocketRequest} with the sub protocols requested. + */ + public abstract WebSocketRequest requestSubProtocols(String... subProtocols); + + /** + * By default, the websocket request made is for the latest version in the specifications, however, if an earlier + * version is required, it can be updated by this method. + * + * @param version WebSocket version. + * + * @return A new instance of {@link WebSocketRequest} with the version requested. + */ + public abstract WebSocketRequest version(int version); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketResponse.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketResponse.java new file mode 100644 index 0000000..bce6dd8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/WebSocketResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.client; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import rx.Observable; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; + +public abstract class WebSocketResponse extends HttpClientResponse { + + public abstract Observable getWebSocketConnection(); + + public String getAcceptedSubProtocol() { + return getHeader(SEC_WEBSOCKET_PROTOCOL); + } + + public boolean isUpgraded() { + return getStatus().equals(HttpResponseStatus.SWITCHING_PROTOCOLS) + && containsHeader(CONNECTION, HttpHeaderValues.UPGRADE, true) + && containsHeader(HttpHeaderNames.UPGRADE, WEBSOCKET, true) + && containsHeader(SEC_WEBSOCKET_ACCEPT); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/Ws7To13UpgradeHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/Ws7To13UpgradeHandler.java new file mode 100644 index 0000000..7ef1f9d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/Ws7To13UpgradeHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.client; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder; +import io.netty.handler.codec.http.websocketx.WebSocket13FrameEncoder; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.util.CharsetUtil; +import io.reactivex.netty.protocol.http.ws.internal.WsUtils; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; +import static io.netty.handler.codec.http.HttpHeaderValues.*; +import static io.reactivex.netty.protocol.http.HttpHandlerNames.*; + +/** + * A channel handler to appropriately setup WebSocket upgrade requests and verify upgrade responses. + * It also updates the pipeline post a successful upgrade. + * + * The handshake code here is taken from {@link WebSocketClientHandshaker13} and not used directly because the APIs + * do not suit our needs. + */ +public class Ws7To13UpgradeHandler extends ChannelDuplexHandler { + + private String expectedChallengeResponseString; + private boolean upgraded; + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpRequest) { + final HttpRequest request = (HttpRequest) msg; + if (request.headers().contains(UPGRADE, WEBSOCKET, false)) { + /* + * We can safely modify the request here as this request is exclusively for WS upgrades and the following + * headers are added for ALL upgrade requests. Since, the handler is single-threaded, these updates do not + * step on each other. + */ + // Get 16 bit nonce and base 64 encode it + byte[] nonce = WsUtils.randomBytes(16); + String key = WsUtils.base64(nonce); + request.headers().set(SEC_WEBSOCKET_KEY, key); + String acceptSeed = key + WebSocketClientHandshaker13.MAGIC_GUID; + byte[] sha1 = WsUtils.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); + expectedChallengeResponseString = WsUtils.base64(sha1); + String hostHeader = request.headers().get(HOST); + if (null != hostHeader) { + request.headers().set(SEC_WEBSOCKET_ORIGIN, "http://" + hostHeader); + } + final ChannelHandlerContext clientCodecCtx = ctx.pipeline().context(HttpClientCodec.getName()); + if (null == clientCodecCtx) { + promise.tryFailure(new IllegalStateException( + "Http client codec not found, can not upgrade to WebSockets.")); + return; + } + + final HttpClientCodec codec = (HttpClientCodec) clientCodecCtx.handler(); + + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + ChannelPipeline p = future.channel().pipeline(); + // Remove the encoder part of the codec as the user may start writing frames after this method returns. + p.addAfter(clientCodecCtx.name(), WsClientEncoder.getName(), + new WebSocket13FrameEncoder(true/*Clients must set this to true*/)); + } + } + }); + } + } + super.write(ctx, msg, promise); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (isUpgradeResponse(msg)) { + final HttpResponse response = (HttpResponse) msg; + /*Other verifications are done by WebSocketResponse itself.*/ + String accept = response.headers().get(SEC_WEBSOCKET_ACCEPT); + if (accept == null || !accept.equals(expectedChallengeResponseString)) { + throw new WebSocketHandshakeException(String.format( + "Invalid challenge. Actual: %s. Expected: %s", accept, expectedChallengeResponseString)); + } + + final ChannelPipeline pipeline = ctx.pipeline(); + ChannelHandlerContext codecCtx = pipeline.context(HttpClientCodec.getName()); + + if (null == codecCtx) { + throw new IllegalStateException("Http codec not found, can not upgrade to WebSocket."); + } + + pipeline.addAfter(codecCtx.name(), WsClientDecoder.getName(), + new WebSocket13FrameDecoder(false/*Clients must set this to false*/, false, + 65555));//TODO: Fix me + pipeline.remove(HttpClientCodec.class); + upgraded = true; + } + + if (upgraded && msg instanceof HttpContent) { + /*Ignore Content once upgraded. The content should not come typically since an Upgrade accept response is + empty. The only HttpContent that would come is an empty LastHttpContent that netty generates.*/ + ((HttpContent)msg).release(); + return; + } + + super.channelRead(ctx, msg); + } + + private static boolean isUpgradeResponse(Object msg) { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + HttpHeaders headers = response.headers(); + return response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS) + && headers.contains(CONNECTION, HttpHeaderValues.UPGRADE, true) + && headers.contains(UPGRADE, WEBSOCKET, true) + && headers.contains(SEC_WEBSOCKET_ACCEPT); + } + return false; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketRequestImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketRequestImpl.java new file mode 100644 index 0000000..0e88d95 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketRequestImpl.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.client.internal; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.client.internal.HttpClientRequestImpl; +import io.reactivex.netty.protocol.http.client.internal.RawRequest; +import io.reactivex.netty.protocol.http.ws.client.WebSocketRequest; +import io.reactivex.netty.protocol.http.ws.client.WebSocketResponse; +import rx.Subscriber; +import rx.functions.Func1; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; + +public final class WebSocketRequestImpl extends WebSocketRequest { + + private final String[] subProtocolsRequested; + private final WebSocketVersion version; + private final HttpClientRequest httpRequest; + + private WebSocketRequestImpl(final HttpClientRequest httpRequest) { + this(httpRequest, null, WebSocketVersion.V13); + } + + private WebSocketRequestImpl(final HttpClientRequest httpRequest, String[] subProtocolsRequested, + WebSocketVersion version) { + super(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + httpRequest.map(new Func1, WebSocketResponseImpl>() { + @Override + public WebSocketResponseImpl call(HttpClientResponse response) { + return new WebSocketResponseImpl<>(response); + } + }).unsafeSubscribe(subscriber); + } + }); + this.httpRequest = httpRequest; + this.subProtocolsRequested = subProtocolsRequested; + this.version = version; + } + + public String[] getSubProtocolsRequested() { + return subProtocolsRequested; + } + + @Override + public WebSocketRequestImpl requestSubProtocols(String... subProtocols) { + return new WebSocketRequestImpl<>(httpRequest.setHeader(SEC_WEBSOCKET_PROTOCOL, + expectedSubProtocol(subProtocols)), subProtocols, + version); + } + + @Override + public WebSocketRequestImpl version(int version) { + WebSocketVersion webSocketVersion; + + switch (version) { + case 7: + webSocketVersion = WebSocketVersion.V07; + break; + case 8: + webSocketVersion = WebSocketVersion.V08; + break; + case 13: + webSocketVersion = WebSocketVersion.V13; + break; + default: + webSocketVersion = WebSocketVersion.UNKNOWN; + break; + } + return new WebSocketRequestImpl<>(httpRequest.setHeader(SEC_WEBSOCKET_VERSION, version), + subProtocolsRequested, webSocketVersion); + } + + public static WebSocketRequestImpl createNew(final HttpClientRequestImpl httpRequest) { + /*This makes a copy of the request so that we can safely make modifications to the underlying headers.*/ + @SuppressWarnings("unchecked") + final HttpClientRequestImpl upgradeRequest = + (HttpClientRequestImpl) httpRequest.addHeader(HttpHeaderNames.UPGRADE, WEBSOCKET); + RawRequest rawRequest = upgradeRequest.unsafeRawRequest(); + HttpRequest headers = rawRequest.getHeaders(); + headers.headers().add(CONNECTION, HttpHeaderValues.UPGRADE); + headers.headers().add(SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue()); + + return new WebSocketRequestImpl<>(upgradeRequest); + } + + private static String expectedSubProtocol(String[] subProtocols) { + if (null == subProtocols || subProtocols.length == 0) { + return null; + } + + if (subProtocols.length == 1) { + return subProtocols[0]; + } + + StringBuilder builder = new StringBuilder(); + for (String subProtocol : subProtocols) { + if (builder.length() != 0) { + builder.append(','); + } + builder.append(subProtocol); + } + return builder.toString(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketResponseImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketResponseImpl.java new file mode 100644 index 0000000..5f6d2e1 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/client/internal/WebSocketResponseImpl.java @@ -0,0 +1,268 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.client.internal; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ContentSource; +import io.reactivex.netty.client.ClientConnectionToChannelBridge; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import io.reactivex.netty.protocol.http.ws.client.WebSocketResponse; +import rx.Observable; +import rx.Observable.Transformer; + +import java.text.ParseException; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public final class WebSocketResponseImpl extends WebSocketResponse { + + private final HttpClientResponse delegate; + private final WebSocketConnection wsConnection; + private final Channel channel; + + public WebSocketResponseImpl(HttpClientResponse delegate) { + this.delegate = delegate; + @SuppressWarnings("unchecked") + Connection cast = + (Connection) delegate.unsafeConnection(); + channel = cast.unsafeNettyChannel(); + wsConnection = new WebSocketConnection(cast); + } + + @Override + public Observable getWebSocketConnection() { + if (isUpgraded()) { + /*Do not pool connection once upgraded to WS. A closing handshake closes the channel*/ + channel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); + channel.attr(AbstractHttpConnectionBridge.CONNECTION_UPGRADED).set(true); + return Observable.just(wsConnection); + } else { + return Observable.error(new IllegalStateException("WebSocket upgrade rejected by the server.")); + } + } + + @Override + public HttpVersion getHttpVersion() { + return delegate.getHttpVersion(); + } + + @Override + public HttpResponseStatus getStatus() { + return delegate.getStatus(); + } + + @Override + public Map> getCookies() { + return delegate.getCookies(); + } + + @Override + public boolean containsHeader(CharSequence name) { + return delegate.containsHeader(name); + } + + @Override + public boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue) { + return delegate.containsHeader(name, value, ignoreCaseValue); + } + + @Override + public Iterator> headerIterator() { + return delegate.headerIterator(); + } + + @Override + public String getHeader(CharSequence name) { + return delegate.getHeader(name); + } + + @Override + public String getHeader(CharSequence name, String defaultValue) { + return delegate.getHeader(name, defaultValue); + } + + @Override + public List getAllHeaderValues(CharSequence name) { + return delegate.getAllHeaderValues(name); + } + + @Override + public long getContentLength() { + return delegate.getContentLength(); + } + + @Override + public long getContentLength(long defaultValue) { + return delegate.getContentLength(defaultValue); + } + + @Override + public long getDateHeader(CharSequence name) throws ParseException { + return delegate.getDateHeader(name); + } + + @Override + public long getDateHeader(CharSequence name, long defaultValue) { + return delegate.getDateHeader(name, defaultValue); + } + + @Override + public String getHostHeader() { + return delegate.getHostHeader(); + } + + @Override + public String getHost(String defaultValue) { + return delegate.getHost(defaultValue); + } + + @Override + public int getIntHeader(CharSequence name) { + return delegate.getIntHeader(name); + } + + @Override + public int getIntHeader(CharSequence name, int defaultValue) { + return delegate.getIntHeader(name, defaultValue); + } + + @Override + public boolean isContentLengthSet() { + return delegate.isContentLengthSet(); + } + + @Override + public boolean isKeepAlive() { + return delegate.isKeepAlive(); + } + + @Override + public boolean isTransferEncodingChunked() { + return delegate.isTransferEncodingChunked(); + } + + @Override + public Set getHeaderNames() { + return delegate.getHeaderNames(); + } + + @Override + public HttpClientResponse addHeader(CharSequence name, + Object value) { + return delegate.addHeader(name, value); + } + + @Override + public HttpClientResponse addCookie( + Cookie cookie) { + return delegate.addCookie(cookie); + } + + @Override + public HttpClientResponse addDateHeader(CharSequence name, + Date value) { + return delegate.addDateHeader(name, value); + } + + @Override + public HttpClientResponse addDateHeader(CharSequence name, + Iterable values) { + return delegate.addDateHeader(name, values); + } + + @Override + public HttpClientResponse addHeader(CharSequence name, + Iterable values) { + return delegate.addHeader(name, values); + } + + @Override + public HttpClientResponse setDateHeader(CharSequence name, + Date value) { + return delegate.setDateHeader(name, value); + } + + @Override + public HttpClientResponse setHeader(CharSequence name, + Object value) { + return delegate.setHeader(name, value); + } + + @Override + public HttpClientResponse setDateHeader(CharSequence name, + Iterable values) { + return delegate.setDateHeader(name, values); + } + + @Override + public HttpClientResponse setHeader(CharSequence name, + Iterable values) { + return delegate.setHeader(name, values); + } + + @Override + public HttpClientResponse removeHeader(CharSequence name) { + return delegate.removeHeader(name); + } + + @Override + public ContentSource getContentAsServerSentEvents() { + return delegate.getContentAsServerSentEvents(); + } + + @Override + public ContentSource getContent() { + return delegate.getContent(); + } + + @Override + public Observable discardContent() { + return delegate.discardContent(); + } + + @Override + public HttpClientResponse transformContent(Transformer transformer) { + return delegate.transformContent(transformer); + } + + @Override + public Channel unsafeNettyChannel() { + return delegate.unsafeNettyChannel(); + } + + @Override + public Connection unsafeConnection() { + return delegate.unsafeConnection(); + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/internal/WsUtils.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/internal/WsUtils.java new file mode 100644 index 0000000..6719277 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/internal/WsUtils.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.base64.Base64; +import io.netty.util.CharsetUtil; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * This is a copy of relevant methods from WebSocketUtil in netty as that class is not public. + */ +public final class WsUtils { + + private WsUtils() { + } + + /** + * Performs a SHA-1 hash on the specified data + * + * @param data The data to hash + * @return The hashed data + */ + public static byte[] sha1(byte[] data) { + try { + //Attempt to get a MessageDigest that uses SHA1 + MessageDigest md = MessageDigest.getInstance("SHA1"); + //Hash the data + return md.digest(data); + } catch (NoSuchAlgorithmException e) { + //Alright, you might have an old system. + throw new InternalError("SHA-1 is not supported on this platform - Outdated?"); + } + } + + /** + * Performs base64 encoding on the specified data + * + * @param data The data to encode + * @return An encoded string containing the data + */ + public static String base64(byte[] data) { + ByteBuf encodedData = Unpooled.wrappedBuffer(data); + ByteBuf encoded = Base64.encode(encodedData); + String encodedString = encoded.toString(CharsetUtil.UTF_8); + encoded.release(); + return encodedString; + } + + /** + * Creates an arbitrary number of random bytes + * + * @param size the number of random bytes to create + * @return An array of random bytes + */ + public static byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + + for (int index = 0; index < size; index++) { + bytes[index] = (byte) randomNumber(0, 255); + } + + return bytes; + } + + /** + * Generates a pseudo-random number + * + * @param minimum The minimum allowable value + * @param maximum The maximum allowable value + * @return A pseudo-random number + */ + public static int randomNumber(int minimum, int maximum) { + return (int) (Math.random() * maximum + minimum); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/V7to13Handshaker.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/V7to13Handshaker.java new file mode 100644 index 0000000..e42006d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/V7to13Handshaker.java @@ -0,0 +1,162 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.server; + +import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.ws.server.Ws7To13UpgradeHandler.WebSocket7To13UpgradeAcceptedEvent; +import rx.Subscriber; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; + +/** + * Implementation of {@link WebSocketHandshaker} for web socket spec versions 7.0 to 13.0 (includes final RFC) + */ +final class V7to13Handshaker extends WebSocketHandshaker { + + private final State state; + private final HttpServerRequest request; + private final WebSocketHandler handler; + + private V7to13Handshaker(final State state, final HttpServerRequest request, final WebSocketHandler handler) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + state.upgradeResponse.unsafeNettyChannel() + .pipeline() + .fireUserEventTriggered(new WebSocket7To13UpgradeAcceptedEvent(subscriber, handler, + state, request)); + } + }); + this.state = state; + this.request = request; + this.handler = handler; + } + + @Override + public WebSocketHandshaker subprotocol(String... subprotocols) { + return new V7to13Handshaker(new State(state, subprotocols), request, handler); + } + + @Override + public WebSocketHandshaker allowExtensions(boolean allowExtensions) { + return new V7to13Handshaker(new State(state, allowExtensions), request, handler); + } + + @Override + public WebSocketHandshaker location(String webSocketLocation) { + return new V7to13Handshaker(new State(state, webSocketLocation), request, handler); + } + + @Override + public WebSocketHandshaker maxFramePayloadLength(int maxFramePayloadLength) { + return new V7to13Handshaker(new State(state, maxFramePayloadLength), request, handler); + } + + static V7to13Handshaker createNew(WebSocketVersion version, HttpServerRequest request, + HttpServerResponse upgradeResponse, WebSocketHandler handler) { + return new V7to13Handshaker(new State(version, request, upgradeResponse), request, handler); + } + + /*package-private, used by upgrade handler*/static final class State { + + private final WebSocketVersion version; + private final HttpServerResponse upgradeResponse; + private final String[] supportedSubProtocols; + private final String locationForV00; + private final boolean allowExtensions; + private final int maxFramePayloadLength; + private final String secWSkey; + private final String requestSubProtocols; + + private State(WebSocketVersion version, HttpServerRequest request, HttpServerResponse upgradeResponse) { + this(getKey(request), getRequestedProtocols(request), version, upgradeResponse, null, null, + DEFAULT_ALLOW_EXTENSIONS, DEFAULT_MAX_FRAME_PAYLOAD_LENGTH); + } + + private State(State current, String... subprotocols) { + this(current.secWSkey, current.requestSubProtocols, current.version, current.upgradeResponse, + subprotocols, current.locationForV00, current.allowExtensions, current.maxFramePayloadLength); + } + + private State(State current, int maxFramePayloadLength) { + this(current.secWSkey, current.requestSubProtocols, current.version, current.upgradeResponse, + current.supportedSubProtocols, current.locationForV00, current.allowExtensions, + maxFramePayloadLength); + } + + private State(State current, boolean allowExtensions) { + this(current.secWSkey, current.requestSubProtocols, current.version, current.upgradeResponse, + current.supportedSubProtocols, current.locationForV00, allowExtensions, + current.maxFramePayloadLength); + } + + private State(String secWSkey, String requestSubProtocols, WebSocketVersion version, + HttpServerResponse upgradeResponse, String[] supportedSubProtocols, String locationForV00, + boolean allowExtensions, int maxFramePayloadLength) { + this.secWSkey = secWSkey; + this.requestSubProtocols = requestSubProtocols; + this.version = version; + this.upgradeResponse = upgradeResponse; + this.supportedSubProtocols = supportedSubProtocols; + this.locationForV00 = locationForV00; + this.allowExtensions = allowExtensions; + this.maxFramePayloadLength = maxFramePayloadLength; + } + + private static String getRequestedProtocols(HttpServerRequest request) { + return request.getHeader(SEC_WEBSOCKET_PROTOCOL); + } + + private static String getKey(HttpServerRequest request) { + return request.getHeader(SEC_WEBSOCKET_KEY); + } + + public WebSocketVersion getVersion() { + return version; + } + + public HttpServerResponse getUpgradeResponse() { + return upgradeResponse; + } + + public String[] getSupportedSubProtocols() { + return supportedSubProtocols; + } + + public String getLocationForV00() { + return locationForV00; + } + + public boolean isAllowExtensions() { + return allowExtensions; + } + + public int getMaxFramePayloadLength() { + return maxFramePayloadLength; + } + + public String getSecWSkey() { + return secWSkey; + } + + public String getRequestSubProtocols() { + return requestSubProtocols; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandler.java new file mode 100644 index 0000000..5d42baa --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.server; + +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import rx.Observable; + +/** + * A handler for {@link WebSocketConnection} upon a successful upgrade from an HTTP request. + */ +public interface WebSocketHandler { + + /** + * Processes the passed connection. + * + * @param wsConnection Connection to process. + * + * @return {@code Observable} representing the processing termination. + */ + Observable handle(WebSocketConnection wsConnection); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandlers.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandlers.java new file mode 100644 index 0000000..1ee9e3e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandlers.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.server; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import rx.Observable; + +/** + * A utility to provide convenience {@link RequestHandler} implementations for Web Sockets. + */ +public final class WebSocketHandlers { + + private WebSocketHandlers() { + } + + /** + * Returns a {@link RequestHandler} that accepts all WebSocket upgrade requests by delegating it to the passed + * handler but sends an HTTP 404 response for all other requests. + * + * @param handler Web Socket handler for all web socket upgrade requests. + * + * @return request handler. + */ + public static RequestHandler acceptAllUpgrades(final WebSocketHandler handler) { + return new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + if (request.isWebSocketUpgradeRequested()) { + return response.acceptWebSocketUpgrade(handler); + } + + return response.setStatus(HttpResponseStatus.NOT_FOUND) + .write(Observable.empty()); + } + }; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandshaker.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandshaker.java new file mode 100644 index 0000000..afe5ef5 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/WebSocketHandshaker.java @@ -0,0 +1,152 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.server; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.netty.util.internal.StringUtil; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import rx.Observable; +import rx.Subscriber; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; +/** + * The websocket handshaker for sending handshake response back to the client. + * + * The defaults chosen by the handshaker can be altered by using the various methods here. + */ +public abstract class WebSocketHandshaker extends Observable { + + public static final int DEFAULT_MAX_FRAME_PAYLOAD_LENGTH = 65536; + public static final boolean DEFAULT_ALLOW_EXTENSIONS = true; + + protected WebSocketHandshaker(OnSubscribe f) { + super(f); + } + + public abstract WebSocketHandshaker subprotocol(String... subprotocols); + + public abstract WebSocketHandshaker allowExtensions(boolean allowExtensions); + + public abstract WebSocketHandshaker location(String webSocketLocation); + + public abstract WebSocketHandshaker maxFramePayloadLength(int maxFramePayloadLength); + + public static WebSocketHandshaker newHandshaker(HttpServerRequest request, + HttpServerResponse upgradeResponse, WebSocketHandler handler) { + final WebSocketVersion wsVersion = getWsVersion(request); + return V7to13Handshaker.createNew(wsVersion, request, upgradeResponse, handler); + } + + public static WebSocketHandshaker newErrorHandshaker(Throwable error) { + return new ErrorWebSocketHandshaker(error); + } + + /** + * This is copied from {@link WebSocketServerHandshaker} + * + * Selects the first matching supported sub protocol + * + * @param requestedSubprotocols CSV of protocols to be supported. e.g. "chat, superchat" + * @return First matching supported sub protocol. Null if not found. + */ + protected static String selectSubprotocol(String requestedSubprotocols, String[] supportedSubProtocols) { + if (requestedSubprotocols == null || supportedSubProtocols.length == 0) { + return null; + } + + String[] requestedSubprotocolArray = requestedSubprotocols.split(","); + + for (String p: requestedSubprotocolArray) { + String requestedSubprotocol = p.trim(); + + for (String supportedSubprotocol: supportedSubProtocols) { + if (WebSocketServerHandshaker.SUB_PROTOCOL_WILDCARD.equals(supportedSubprotocol) + || requestedSubprotocol.equals(supportedSubprotocol)) { + return requestedSubprotocol; + } + } + } + + // No match found + return null; + } + + public static boolean isUpgradeRequested(HttpServerRequest upgradeRequest) { + return null != upgradeRequest && upgradeRequest.containsHeader(HttpHeaderNames.UPGRADE) + && WEBSOCKET.contentEqualsIgnoreCase(upgradeRequest.getHeader(HttpHeaderNames.UPGRADE)); + } + + public static boolean isUpgradeRequested(HttpRequest upgradeRequest) { + return null != upgradeRequest && upgradeRequest.headers().contains(HttpHeaderNames.UPGRADE) + && WEBSOCKET.contentEqualsIgnoreCase(upgradeRequest.headers() + .get(HttpHeaderNames.UPGRADE)); + } + + private static WebSocketVersion getWsVersion(HttpServerRequest request) { + String version = request.getHeader(SEC_WEBSOCKET_VERSION); + + switch (version) { + case "0": + return WebSocketVersion.V00; + case "7": + return WebSocketVersion.V07; + case "8": + return WebSocketVersion.V08; + case "13": + return WebSocketVersion.V13; + default: + return WebSocketVersion.UNKNOWN; + } + } + + private static class ErrorWebSocketHandshaker extends WebSocketHandshaker { + + public ErrorWebSocketHandshaker(final Throwable error) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.onError(error); + } + }); + } + + @Override + public WebSocketHandshaker subprotocol(String... subprotocols) { + return this; + } + + @Override + public WebSocketHandshaker allowExtensions(boolean allowExtensions) { + return this; + } + + @Override + public WebSocketHandshaker location(String webSocketLocation) { + return this; + } + + @Override + public WebSocketHandshaker maxFramePayloadLength(int maxFramePayloadLength) { + return this; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/Ws7To13UpgradeHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/Ws7To13UpgradeHandler.java new file mode 100644 index 0000000..4779be2 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/http/ws/server/Ws7To13UpgradeHandler.java @@ -0,0 +1,198 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.ws.server; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder; +import io.netty.handler.codec.http.websocketx.WebSocket13FrameEncoder; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrameDecoder; +import io.netty.handler.codec.http.websocketx.WebSocketFrameEncoder; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker07; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker08; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker13; +import io.netty.util.CharsetUtil; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.MarkAwarePipeline; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import io.reactivex.netty.protocol.http.ws.internal.WsUtils; +import io.reactivex.netty.protocol.http.ws.server.V7to13Handshaker.State; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func0; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; +import static io.reactivex.netty.protocol.http.HttpHandlerNames.*; + +/** + * A websocket upgrade handler for upgrading to WebSocket versions 7 to 13. This handler listens for + * {@link WebSocket7To13UpgradeAcceptedEvent} and upon recieving such an event, it sets up the + * {@link WebSocketConnection} to hand it over to the associated {@link WebSocketHandler} + */ +public final class Ws7To13UpgradeHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + + if (evt instanceof WebSocket7To13UpgradeAcceptedEvent) { + + final WebSocket7To13UpgradeAcceptedEvent wsUpEvt = (WebSocket7To13UpgradeAcceptedEvent) evt; + + final State state = wsUpEvt.state; + final Subscriber subscriber = wsUpEvt.acceptUpgradeSubscriber; + + String errorIfAny = configureResponseForWs(state); + + if (null != errorIfAny) { + subscriber.onError(new IllegalStateException(errorIfAny)); + return; + } + + final MarkAwarePipeline pipeline = state.getUpgradeResponse().unsafeConnection() + .getResettableChannelPipeline(); + + @SuppressWarnings("unchecked") + final Connection wsConn = + (Connection) wsUpEvt.state.getUpgradeResponse().unsafeConnection(); + + wsUpEvt.request.discardContent() + .onErrorResumeNext(Observable.empty()) // In case, the request content was read, ignore. + .concatWith(state.getUpgradeResponse().setTransferEncodingChunked().sendHeaders()) + .doOnCompleted(new Action0() { + @Override + public void call() { + /*We are no more talking HTTP*/ + pipeline.remove(HttpServerEncoder.getName()); + pipeline.remove(HttpServerDecoder.getName()); + pipeline.channel().attr(AbstractHttpConnectionBridge.CONNECTION_UPGRADED).set(true); + } + }) + .concatWith(Observable.defer(new Func0>() { + @Override + public Observable call() { + return wsUpEvt.handler.handle(new WebSocketConnection(wsConn)); + } + })) + .concatWith(Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber sub) { + /* + * In this case, the client did not send a close frame but the server end processing + * is over, so we should send a close frame to indicate closure from server. + */ + if (wsConn.unsafeNettyChannel().isOpen()) { + wsConn.write(Observable.just(new CloseWebSocketFrame())) + .concatWith(wsConn.close()) + .unsafeSubscribe(sub); + } + } + })) + .unsafeSubscribe(subscriber); /*Unsafe as the subscriber is coming from the user.*/ + + } + + ctx.fireUserEventTriggered(evt); + } + + private static String configureResponseForWs(State state) { + + String acceptGuid; + + switch (state.getVersion()) { + case V07: + acceptGuid = WebSocketServerHandshaker07.WEBSOCKET_07_ACCEPT_GUID; + break; + case V08: + acceptGuid = WebSocketServerHandshaker08.WEBSOCKET_08_ACCEPT_GUID; + break; + case V13: + acceptGuid = WebSocketServerHandshaker13.WEBSOCKET_13_ACCEPT_GUID; + break; + default: + return "Unsupported web socket version: " + state.getVersion(); + } + + WebSocketFrameEncoder wsEncoder = new WebSocket13FrameEncoder(false /*servers should set this to false.*/); + WebSocketFrameDecoder wsDecoder = new WebSocket13FrameDecoder(true/*servers should set this to true.*/, + state.isAllowExtensions(), + state.getMaxFramePayloadLength(), true); + + final HttpServerResponse upgradeResponse = state.getUpgradeResponse(); + final MarkAwarePipeline pipeline = upgradeResponse.unsafeConnection().getResettableChannelPipeline(); + ChannelHandlerContext httpDecoderCtx = pipeline.context(HttpServerDecoder.getName()); + if (null == httpDecoderCtx) { + return "No HTTP decoder found, can not upgrade to WebSocket."; + } + ChannelHandlerContext httpEncoderCtx = pipeline.context(HttpServerEncoder.getName()); + if (null == httpEncoderCtx) { + return "No HTTP encoder found, can not upgrade to WebSocket."; + } + + pipeline.addAfter(httpDecoderCtx.name(), WsServerDecoder.getName(), wsDecoder); + pipeline.addBefore(httpEncoderCtx.name(), WsServerEncoder.getName(), wsEncoder); + + updateHandshakeHeaders(state, acceptGuid, upgradeResponse); + + return null; + } + + private static void updateHandshakeHeaders(State state, String acceptGuid, HttpServerResponse upgradeResponse) { + String acceptSeed = state.getSecWSkey() + acceptGuid; + byte[] sha1 = WsUtils.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); + String accept = WsUtils.base64(sha1); + + upgradeResponse.addHeader(SEC_WEBSOCKET_ACCEPT, accept); + upgradeResponse.setStatus(HttpResponseStatus.SWITCHING_PROTOCOLS); + upgradeResponse.addHeader(HttpHeaderNames.UPGRADE, WEBSOCKET); + upgradeResponse.addHeader(CONNECTION, HttpHeaderValues.UPGRADE); + + if (state.getRequestSubProtocols() != null) { + String selectedSubprotocol = WebSocketHandshaker.selectSubprotocol(state.getRequestSubProtocols(), + state.getSupportedSubProtocols()); + if (selectedSubprotocol != null) { + state.getUpgradeResponse().addHeader(SEC_WEBSOCKET_PROTOCOL, selectedSubprotocol); + } + } + } + + public static class WebSocket7To13UpgradeAcceptedEvent { + + private final Subscriber acceptUpgradeSubscriber; + private final WebSocketHandler handler; + private final State state; + private final HttpServerRequest request; + + WebSocket7To13UpgradeAcceptedEvent(Subscriber acceptUpgradeSubscriber, WebSocketHandler handler, + State state, HttpServerRequest request) { + this.acceptUpgradeSubscriber = acceptUpgradeSubscriber; + this.handler = handler; + this.state = state; + this.request = request; + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/TcpHandlerNames.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/TcpHandlerNames.java new file mode 100644 index 0000000..e18f765 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/TcpHandlerNames.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp; + +/** + * A list of all handler names added for TCP. This is just to ensure consistency in naming. + */ +public enum TcpHandlerNames { + + ClientReadTimeoutHandler("client-read-timeout-handler"), + ; + + private final String name; + + TcpHandlerNames(String name) { + this.name = qualify(name); + } + + public String getName() { + return name; + } + + private static String qualify(String name) { + return "_rx_netty_" + name; + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/ConnectionRequestImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/ConnectionRequestImpl.java new file mode 100644 index 0000000..7fdc6b4 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/ConnectionRequestImpl.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionRequest; +import rx.Subscriber; + +final class ConnectionRequestImpl extends ConnectionRequest { + + ConnectionRequestImpl(final ConnectionProvider cp) { + super(new OnSubscribe>() { + @Override + public void call(final Subscriber> subscriber) { + cp.newConnectionRequest().unsafeSubscribe(subscriber); + } + }); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClient.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClient.java new file mode 100644 index 0000000..1541655 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClient.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.client.ConnectionRequest; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; + +public abstract class InterceptingTcpClient implements EventSource { + + /** + * Creates a new {@link ConnectionRequest} which should be subscribed to actually connect to the target server. + * + * @return A new {@link ConnectionRequest} which either can be subscribed directly or altered in various ways + * before subscription. + */ + public abstract ConnectionRequest createConnectionRequest(); + + /** + * Starts the process of adding interceptors to this client. Interceptors help in achieving various usecases of + * instrumenting and transforming connections. + * + * @return A new interceptor chain to add the various interceptors. + */ + public abstract TcpClientInterceptorChain intercept(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClientImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClientImpl.java new file mode 100644 index 0000000..a5786e2 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/InterceptingTcpClientImpl.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionRequest; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; +import rx.Subscription; + +public class InterceptingTcpClientImpl extends InterceptingTcpClient { + + private final ConnectionProvider cp; + private final TcpClientEventPublisher eventPublisher; + private final ConnectionRequest connectionRequest; + + public InterceptingTcpClientImpl(ConnectionProvider cp, TcpClientEventPublisher ep) { + this.cp = cp; + this.eventPublisher = ep; + connectionRequest = new ConnectionRequestImpl<>(this.cp); + + } + + @Override + public ConnectionRequest createConnectionRequest() { + return connectionRequest; + } + + @Override + public TcpClientInterceptorChain intercept() { + return new TcpClientInterceptorChainImpl<>(cp, eventPublisher); + } + + @Override + public Subscription subscribe(TcpClientEventListener listener) { + return eventPublisher.subscribe(listener); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/Interceptor.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/Interceptor.java new file mode 100644 index 0000000..93e42e4 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/Interceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.client.ConnectionProvider; + +/** + * An interceptor that preserves the type of objects read and written to the connection. + * + * @param Type of objects read from the connection handled by this interceptor. + * @param Type of objects written to the connection handled by this interceptor. + */ +public interface Interceptor { + + /** + * Intercepts and optionally changes the passed {@code ConnectionProvider}. + * + * @param provider Provider to intercept. + * + * @return Provider to use after this transformation. + */ + ConnectionProvider intercept(ConnectionProvider provider); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClient.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClient.java new file mode 100644 index 0000000..5908ac7 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClient.java @@ -0,0 +1,361 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.ssl.SslCodec; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +/** + * A TCP client for creating TCP connections. + * + *

Immutability

+ * An instance of this client is immutable and all mutations produce a new client instance. For this reason it is + * recommended that the mutations are done during client creation and not during connection creation to avoid repeated + * object creation overhead. + * + * @param The type of objects written to this client. + * @param The type of objects read from this client. + */ +public abstract class TcpClient extends InterceptingTcpClient { + + /** + * Creates a new client instances, inheriting all configurations from this client and adding a + * {@link ChannelOption} for the connections created by the newly created client instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient channelOption(ChannelOption option, T value); + + /** + * Creates a new client instances, inheriting all configurations from this client and enables read timeout for all + * the connection created by this client. + * + * @param timeOut Read timeout duration. + * @param timeUnit Read timeout time unit. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient readTimeOut(int timeOut, TimeUnit timeUnit); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerFirst(String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerLast(String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this client. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory); + + /** + * Creates a new client instances, inheriting all configurations from this client and using the passed + * action to configure all the connections created by the newly created client instance. + * + * @param pipelineConfigurator Action to configure {@link ChannelPipeline}. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient pipelineConfigurator(Action1 pipelineConfigurator); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link LoggingHandler} + * + * @return A new {@link TcpClient} instance. + * + * @deprecated Use {@link #enableWireLogging(String, LogLevel)} instead. + */ + @Deprecated + public abstract TcpClient enableWireLogging(LogLevel wireLoggingLevel); + + /** + * Creates a new client instances, inheriting all configurations from this client and enabling wire logging at the + * passed level for the newly created client instance. + * + * @param name Name of the logger that can be used to control the logging dynamically. + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link LoggingHandler} + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient enableWireLogging(String name, LogLevel wireLoggingLevel); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslEngineFactory} for all secured connections created by the newly created client instance. + * + * If the {@link SSLEngine} instance can be statically, created, {@link #secure(SSLEngine)} can be used. + * + * @param sslEngineFactory Factory for all secured connections created by the newly created client instance. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient secure(Func1 sslEngineFactory); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslEngine} for all secured connections created by the newly created client instance. + * + * If the {@link SSLEngine} instance can not be statically, created, {@link #secure(Func1)} )} can be used. + * + * @param sslEngine {@link SSLEngine} for all secured connections created by the newly created client instance. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient secure(SSLEngine sslEngine); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code sslCodec} for all secured connections created by the newly created client instance. + * + * This is required only when the {@link SslHandler} used by {@link SslCodec} is to be modified before adding to + * the {@link ChannelPipeline}. For most of the cases, {@link #secure(Func1)} or {@link #secure(SSLEngine)} will be + * enough. + * + * @param sslCodec {@link SslCodec} for all secured connections created by the newly created client instance. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient secure(SslCodec sslCodec); + + /** + * Creates a new client instance, inheriting all configurations from this client and using a trust-all + * {@link TrustManagerFactory}for all secured connections created by the newly created client instance. + * + * This is only for testing and should not be used for real production clients. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient unsafeSecure(); + + /** + * Creates a new client instance, inheriting all configurations from this client and using the passed + * {@code providerFactory}. + * + * @param providerFactory Channel provider factory. + * + * @return A new {@link TcpClient} instance. + */ + public abstract TcpClient channelProvider(ChannelProviderFactory providerFactory); + + /** + * Creates a new TCP client instance with the passed address of the target server. + * + * @param host Hostname for the target server. + * @param port Port for the target server. + * + * @return A new {@code TcpClient} instance. + */ + public static TcpClient newClient(String host, int port) { + return newClient(new InetSocketAddress(host, port)); + } + + /** + * Creates a new TCP client instance with the passed address of the target server. + * + * @param serverAddress Socket address for the target server. + * + * @return A new {@code TcpClient} instance. + */ + public static TcpClient newClient(SocketAddress serverAddress) { + return TcpClientImpl.create(serverAddress); + } + + /** + * Creates a new TCP client instance with the passed address of the target server. + * + * @param serverAddress Socket address for the target server. + * @param eventLoopGroup Eventloop group for the client. + * @param channelClass Channel class for the client. + * + * @return A new {@code TcpClient} instance. + */ + public static TcpClient newClient(SocketAddress serverAddress, EventLoopGroup eventLoopGroup, + Class channelClass) { + return TcpClientImpl.create(serverAddress, eventLoopGroup, channelClass); + } + + /** + * Creates a new TCP client instance using the supplied connection provider. + * + * @param providerFactory {@link ConnectionProviderFactory} for the client. + * @param hostStream Stream of hosts for the client. + * + * @return A new {@code TcpClient} instance. + */ + public static TcpClient newClient(ConnectionProviderFactory providerFactory, + Observable hostStream) { + return TcpClientImpl.create(providerFactory, hostStream); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientImpl.java new file mode 100644 index 0000000..e73e5be --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientImpl.java @@ -0,0 +1,361 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.handler.logging.LogLevel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.HandlerNames; +import io.reactivex.netty.channel.ChannelSubscriberEvent; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.ClientState; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.ConnectionRequest; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.internal.SingleHostConnectionProvider; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.internal.InternalReadTimeoutHandler; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; +import io.reactivex.netty.protocol.tcp.client.internal.TcpChannelProviderFactory; +import io.reactivex.netty.ssl.SslCodec; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +public final class TcpClientImpl extends TcpClient { + + private final ClientState state; + private final TcpClientEventPublisher eventPublisher; + private final InterceptingTcpClient interceptingTcpClient; + private ConnectionRequestImpl requestSetLazily; + + private TcpClientImpl(ClientState state, TcpClientEventPublisher eventPublisher, + InterceptingTcpClient interceptingTcpClient) { + this.state = state; + this.eventPublisher = eventPublisher; + this.interceptingTcpClient = interceptingTcpClient; + } + + @Override + public ConnectionRequest createConnectionRequest() { + return requestSetLazily; + } + + @Override + public TcpClient channelOption(ChannelOption option, T value) { + return copy(state.channelOption(option, value), eventPublisher); + } + + @Override + public TcpClient readTimeOut(final int timeOut, final TimeUnit timeUnit) { + return addChannelHandlerFirst(HandlerNames.ClientReadTimeoutHandler.getName(), new Func0() { + @Override + public ChannelHandler call() { + return new InternalReadTimeoutHandler(timeOut, timeUnit); + } + }); + } + + @Override + public TcpClient addChannelHandlerFirst(String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerFirst(name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerFirst(group, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerLast(String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerLast(name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerLast(group, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerBefore(baseName, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerBefore(group, baseName, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerAfter(baseName, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerAfter(group, baseName, name, handlerFactory), eventPublisher); + } + + @Override + public TcpClient pipelineConfigurator(Action1 pipelineConfigurator) { + return copy(state.pipelineConfigurator(pipelineConfigurator), eventPublisher); + } + + @Override + @Deprecated + public TcpClient enableWireLogging(LogLevel wireLoggingLevel) { + return copy(state.enableWireLogging(wireLoggingLevel), eventPublisher); + } + + @Override + public TcpClient enableWireLogging(String name, LogLevel wireLoggingLevel) { + return copy(state.enableWireLogging(name, wireLoggingLevel), eventPublisher); + } + + @Override + public TcpClient secure(Func1 sslEngineFactory) { + return copy(state.secure(sslEngineFactory), eventPublisher); + } + + @Override + public TcpClient secure(SSLEngine sslEngine) { + return copy(state.secure(sslEngine), eventPublisher); + } + + @Override + public TcpClient secure(SslCodec sslCodec) { + return copy(state.secure(sslCodec), eventPublisher); + } + + @Override + public TcpClient unsafeSecure() { + return copy(state.unsafeSecure(), eventPublisher); + } + + @Override + public TcpClient channelProvider(ChannelProviderFactory providerFactory) { + return copy(state.channelProviderFactory(providerFactory), eventPublisher); + } + + @Override + public Subscription subscribe(TcpClientEventListener listener) { + return interceptingTcpClient.subscribe(listener); + } + + @Override + public TcpClientInterceptorChain intercept() { + return interceptingTcpClient.intercept(); + } + + /*Visible for testing*/ ClientState getClientState() { + return state; + } + + public static TcpClientImpl create(SocketAddress socketAddress) { + return create(socketAddress, ClientState.defaultEventloopGroup(), ClientState.defaultSocketChannelClass()); + } + + public static TcpClientImpl create(SocketAddress socketAddress, EventLoopGroup eventLoopGroup, + Class channelClass) { + final Host host = new Host(socketAddress); + ConnectionProviderFactory factory = new ConnectionProviderFactory() { + @Override + public ConnectionProvider newProvider(Observable> hosts) { + return new SingleHostConnectionProvider<>(hosts); + } + }; + Observable hostStream = Observable.just(host); + ClientState state = ClientState.create(factory, hostStream, eventLoopGroup, channelClass); + final TcpClientEventPublisher eventPublisher = new TcpClientEventPublisher(); + return _create(state, eventPublisher); + } + + public static TcpClientImpl create(ConnectionProviderFactory factory, + Observable hostStream) { + ClientState state = ClientState.create(factory, hostStream); + final TcpClientEventPublisher eventPublisher = new TcpClientEventPublisher(); + return _create(state, eventPublisher); + } + + private static TcpClientImpl copy(final ClientState state, + TcpClientEventPublisher eventPublisher) { + return _create(state, eventPublisher); + } + + /*Visible for testing*/ static TcpClientImpl _create(ClientState state, + TcpClientEventPublisher eventPublisher) { + DetachedChannelPipeline channelPipeline = state.unsafeDetachedPipeline(); + state = state.channelProviderFactory(new TcpChannelProviderFactory(channelPipeline, + state.getChannelProviderFactory())); + + HostConnectorFactory hostConnectorFactory = new HostConnectorFactory<>(state, eventPublisher); + + ConnectionProvider cp = state.getFactory() + .newProvider(state.getHostStream().map(hostConnectorFactory)); + + InterceptingTcpClient interceptingTcpClient = new InterceptingTcpClientImpl<>(cp, eventPublisher); + TcpClientImpl client = new TcpClientImpl<>(state, eventPublisher, interceptingTcpClient); + client.requestSetLazily = new ConnectionRequestImpl<>(cp); + return client; + } + + private static class HostConnectorFactory implements Func1> { + + private final ChannelProviderFactory channelProviderFactory; + private final TcpClientEventPublisher clientEventPublisher; + private final ClientState state; + + public HostConnectorFactory(ClientState state, TcpClientEventPublisher clientEventPublisher) { + this.state = state; + channelProviderFactory = state.getChannelProviderFactory(); + this.clientEventPublisher = clientEventPublisher; + } + + @Override + public HostConnector call(final Host host) { + TcpClientEventPublisher hostEventPublisher = new TcpClientEventPublisher(); + @SuppressWarnings({"unchecked", "rawtypes"}) + EventSource eventSource = hostEventPublisher; + hostEventPublisher.subscribe(clientEventPublisher); + @SuppressWarnings("unchecked") + ChannelProvider channelProvider = channelProviderFactory.newProvider(host, eventSource, hostEventPublisher, + hostEventPublisher); + return new HostConnector<>(host, new TerminalConnectionProvider<>(hostEventPublisher, host, + channelProvider, state), + hostEventPublisher, hostEventPublisher, hostEventPublisher); + } + } + + private static class TerminalConnectionProvider implements ConnectionProvider { + + private final Host host; + private final Bootstrap bootstrap; + private final ChannelProvider channelProvider; + + public TerminalConnectionProvider(TcpClientEventPublisher hostEventPublisher, + Host host, ChannelProvider channelProvider, ClientState state) { + this.host = host; + this.channelProvider = channelProvider; + bootstrap = state.newBootstrap(hostEventPublisher, hostEventPublisher); + } + + @Override + public Observable> newConnectionRequest() { + return channelProvider.newChannel(Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber s) { + final ChannelFuture cf = bootstrap.connect(host.getHost()); + s.add(Subscriptions.create(new Action0() { + @Override + public void call() { + if (null != cf && !cf.isDone()) { + cf.cancel(false); + } + } + })); + cf.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(final ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + s.onError(future.cause()); + } else { + s.onNext(cf.channel()); + s.onCompleted(); + } + } + }); + } + })).switchMap(new Func1>() { + @Override + public Observable call(final Channel channel) { + + /* + * If channel is unregistered, all handlers are removed and hence the event will not flow through + * to the handler for the subscriber to be notified. + * So, here the channel is directly passed through the chain if the channel isn't registered. + */ + if (channel.eventLoop().inEventLoop()) { + if (channel.isRegistered()) { + return Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + channel.pipeline().fireUserEventTriggered(new ChannelSubscriberEvent<>(subscriber)); + } + }); + } else { + return Observable.just(channel); + } + } else { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (channel.isRegistered()) { + channel.pipeline() + .fireUserEventTriggered(new ChannelSubscriberEvent<>(subscriber)); + } else { + subscriber.onNext(channel); + subscriber.onCompleted(); + } + } + }); + } + }); + } + } + }).map(new Func1>() { + @Override + public Connection call(Channel channel) { + return ConnectionImpl.fromChannel(channel); + } + }); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChain.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChain.java new file mode 100644 index 0000000..1790336 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChain.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +/** + * Interceptor chain for {@link TcpClient}, obtained via {@link TcpClient#intercept()}.

+ * + * Multiple interceptors can be added to this chain by using the various {@code next*()} methods available, before + * calling {@link #finish()} that returns a new {@link TcpClient} which inherits all the configuration from the parent + * client (from which this chain was created) and adds these interceptors. + * + *

Order of execution

+ * + * Interceptors are executed in the order in which they are added. + * + * @param The type of objects written to the client created by this chain. + * @param The type of objects read from the client created by this chain. + */ +public interface TcpClientInterceptorChain { + + /** + * Adds a simple interceptor that does not change the type of objects read/written to a connection. + * + * @param interceptor Interceptor to add. + * + * @return {@code this} + */ + TcpClientInterceptorChain next(Interceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects read from the connections created by the client provided by + * this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + TcpClientInterceptorChain nextWithReadTransform(TransformingInterceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects written to the connections created by the client provided by + * this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + TcpClientInterceptorChain nextWithWriteTransform(TransformingInterceptor interceptor); + + /** + * Adds an interceptor that changes the type of objects read and written to the connections created by the client + * provided by this chain. + * + * @param interceptor Interceptor to add. + * + * @return A new chain instance. + */ + TcpClientInterceptorChain nextWithTransform(TransformingInterceptor interceptor); + + /** + * Finish the addition of interceptors and create a new client instance. + * + * @return New client instance which inherits all the configuration from the parent client + * (from which this chain was created) and adds these interceptors. + */ + InterceptingTcpClient finish(); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChainImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChainImpl.java new file mode 100644 index 0000000..8fcd7d8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TcpClientInterceptorChainImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; + +public class TcpClientInterceptorChainImpl implements TcpClientInterceptorChain { + + private ConnectionProvider connectionProvider; + private final TcpClientEventPublisher eventPublisher; + + public TcpClientInterceptorChainImpl(ConnectionProvider cp, TcpClientEventPublisher ep) { + connectionProvider = cp; + this.eventPublisher = ep; + } + + @Override + public TcpClientInterceptorChain next(Interceptor i) { + connectionProvider = i.intercept(connectionProvider); + return this; + } + + @Override + public TcpClientInterceptorChain nextWithReadTransform(TransformingInterceptor i) { + return new TcpClientInterceptorChainImpl<>(i.intercept(connectionProvider), eventPublisher); + } + + @Override + public TcpClientInterceptorChain nextWithWriteTransform(TransformingInterceptor i) { + return new TcpClientInterceptorChainImpl<>(i.intercept(connectionProvider), eventPublisher); + } + + @Override + public TcpClientInterceptorChain nextWithTransform(TransformingInterceptor i) { + return new TcpClientInterceptorChainImpl<>(i.intercept(connectionProvider), eventPublisher); + } + + @Override + public InterceptingTcpClient finish() { + return new InterceptingTcpClientImpl<>(connectionProvider, eventPublisher); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TransformingInterceptor.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TransformingInterceptor.java new file mode 100644 index 0000000..47967fa --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/TransformingInterceptor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.client.ConnectionProvider; + +/** + * An interceptor that changes the type of objects read and written to the connection. + * + * @param Type of objects read from the connection before applying this interceptor. + * @param Type of objects written to the connection before applying this interceptor. + * @param Type of objects read from the connection after applying this interceptor. + * @param Type of objects written to the connection after applying this interceptor. + */ +public interface TransformingInterceptor { + + /** + * Intercepts and changes the passed {@code ConnectionProvider}. + * + * @param provider Provider to intercept. + * + * @return Provider to use after this transformation. + */ + ConnectionProvider intercept(ConnectionProvider provider); + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/SafeTcpClientEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/SafeTcpClientEventListener.java new file mode 100644 index 0000000..5b7e94e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/SafeTcpClientEventListener.java @@ -0,0 +1,237 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client.events; + +import io.reactivex.netty.events.internal.SafeEventListener; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +final class SafeTcpClientEventListener extends TcpClientEventListener implements SafeEventListener { + + private final TcpClientEventListener delegate; + private final AtomicBoolean completed = new AtomicBoolean(); + + public SafeTcpClientEventListener(TcpClientEventListener delegate) { + this.delegate = delegate; + } + + @Override + public void onCompleted() { + if (completed.compareAndSet(false, true)) { + delegate.onCompleted(); + } + } + + @Override + public void onConnectStart() { + if (!completed.get()) { + delegate.onConnectStart(); + } + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onConnectFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onPoolReleaseStart() { + if (!completed.get()) { + delegate.onPoolReleaseStart(); + } + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onPoolReleaseSuccess(duration, timeUnit); + } + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onPoolReleaseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onPooledConnectionEviction() { + if (!completed.get()) { + delegate.onPooledConnectionEviction(); + } + } + + @Override + public void onPooledConnectionReuse() { + if (!completed.get()) { + delegate.onPooledConnectionReuse(); + } + } + + @Override + public void onPoolAcquireStart() { + if (!completed.get()) { + delegate.onPoolAcquireStart(); + } + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onPoolAcquireSuccess(duration, timeUnit); + } + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onPoolAcquireFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onByteRead(long bytesRead) { + if (!completed.get()) { + delegate.onByteRead(bytesRead); + } + } + + @Override + public void onByteWritten(long bytesWritten) { + if (!completed.get()) { + delegate.onByteWritten(bytesWritten); + } + } + + @Override + public void onFlushStart() { + if (!completed.get()) { + delegate.onFlushStart(); + } + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onFlushComplete(duration, timeUnit); + } + } + + @Override + public void onWriteStart() { + if (!completed.get()) { + delegate.onWriteStart(); + } + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onWriteSuccess(duration, timeUnit); + } + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onConnectionCloseStart() { + if (!completed.get()) { + delegate.onConnectionCloseStart(); + } + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionCloseSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event) { + if (!completed.get()) { + delegate.onCustomEvent(event); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, throwable); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SafeTcpClientEventListener)) { + return false; + } + + SafeTcpClientEventListener that = (SafeTcpClientEventListener) o; + + return !(delegate != null? !delegate.equals(that.delegate) : that.delegate != null); + + } + + @Override + public int hashCode() { + return delegate != null? delegate.hashCode() : 0; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventListener.java new file mode 100644 index 0000000..0392b0f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client.events; + +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.protocol.tcp.client.TcpClient; + +/** + * A listener for all events published by {@link TcpClient} + */ +public abstract class TcpClientEventListener extends ClientEventListener { + +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisher.java new file mode 100644 index 0000000..390c56d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisher.java @@ -0,0 +1,280 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client.events; + +import io.reactivex.netty.channel.events.ConnectionEventPublisher; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.events.ListenersHolder; +import io.reactivex.netty.events.internal.SafeEventListener; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.subscriptions.CompositeSubscription; + +import java.util.concurrent.TimeUnit; + +public final class TcpClientEventPublisher extends TcpClientEventListener + implements EventSource, EventPublisher { + + public static final Action1 CONN_START_ACTION = new Action1() { + @Override + public void call(TcpClientEventListener l) { + l.onConnectStart(); + } + }; + + public static final Action3 CONN_SUCCESS_ACTION = + new Action3() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit) { + l.onConnectSuccess(duration, timeUnit); + } + }; + + public static final Action4 CONN_FAILED_ACTION = + new Action4() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onConnectFailed(duration, timeUnit, t); + } + }; + + public static final Action1 EVICTION_ACTION = new Action1() { + @Override + public void call(TcpClientEventListener l) { + l.onPooledConnectionEviction(); + } + }; + + public static final Action1 REUSE_ACTION = new Action1() { + @Override + public void call(TcpClientEventListener l) { + l.onPooledConnectionReuse(); + } + }; + + public static final Action1 ACQUIRE_START_ACTION = new Action1() { + @Override + public void call(TcpClientEventListener l) { + l.onPoolAcquireStart(); + } + }; + + public static final Action3 ACQUIRE_SUCCESS_ACTION = + new Action3() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit) { + l.onPoolAcquireSuccess(duration, timeUnit); + } + }; + + public static final Action4 ACQUIRE_FAILED_ACTION = + new Action4() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onPoolAcquireFailed(duration, timeUnit, t); + } + }; + + public static final Action1 RELEASE_START_ACTION = new Action1() { + @Override + public void call(TcpClientEventListener l) { + l.onPoolReleaseStart(); + } + }; + + public static final Action3 RELEASE_SUCCESS_ACTION = + new Action3() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit) { + l.onPoolReleaseSuccess(duration, timeUnit); + } + }; + + public static final Action4 RELEASE_FAILED_ACTION = + new Action4() { + @Override + public void call(TcpClientEventListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onPoolReleaseFailed(duration, timeUnit, t); + } + }; + + private final ListenersHolder listeners; + private final ConnectionEventPublisher connDelegate; + + public TcpClientEventPublisher() { + listeners = new ListenersHolder<>(); + connDelegate = new ConnectionEventPublisher<>(); + } + + public TcpClientEventPublisher(TcpClientEventPublisher toCopy) { + listeners = toCopy.listeners.copy(); + connDelegate = toCopy.connDelegate.copy(); + } + + @Override + public void onConnectStart() { + listeners.invokeListeners(CONN_START_ACTION); + } + + @Override + public void onConnectSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(CONN_SUCCESS_ACTION, duration, timeUnit); + } + + @Override + public void onConnectFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(CONN_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onPoolReleaseStart() { + listeners.invokeListeners(RELEASE_START_ACTION); + } + + @Override + public void onPoolReleaseSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(RELEASE_SUCCESS_ACTION, duration, timeUnit); + } + + @Override + public void onPoolReleaseFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(RELEASE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onPooledConnectionEviction() { + listeners.invokeListeners(EVICTION_ACTION); + } + + @Override + public void onPooledConnectionReuse() { + listeners.invokeListeners(REUSE_ACTION); + } + + @Override + public void onPoolAcquireStart() { + listeners.invokeListeners(ACQUIRE_START_ACTION); + } + + @Override + public void onPoolAcquireSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(ACQUIRE_SUCCESS_ACTION, duration, timeUnit); + } + + @Override + public void onPoolAcquireFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(ACQUIRE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onByteRead(long bytesRead) { + connDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + connDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onFlushStart() { + connDelegate.onFlushStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + connDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onWriteStart() { + connDelegate.onWriteStart(); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseStart() { + connDelegate.onConnectionCloseStart(); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event) { + connDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + connDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + connDelegate.onCustomEvent(event, throwable); + } + + @Override + public Subscription subscribe(TcpClientEventListener listener) { + if (!SafeEventListener.class.isAssignableFrom(listener.getClass())) { + listener = new SafeTcpClientEventListener(listener); + } + + CompositeSubscription cs = new CompositeSubscription(); + cs.add(listeners.subscribe(listener)); + cs.add(connDelegate.subscribe(listener)); + return cs; + } + + @Override + public boolean publishingEnabled() { + return listeners.publishingEnabled(); + } + + public TcpClientEventPublisher copy() { + return new TcpClientEventPublisher(this); + } + + /*Visible for testing*/ ListenersHolder getListeners() { + return listeners; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProvider.java new file mode 100644 index 0000000..b79b74b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client.internal; + +import io.netty.channel.Channel; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import rx.Observable; +import rx.functions.Func1; + +public class TcpChannelProvider implements ChannelProvider { + + private final DetachedChannelPipeline channelPipeline; + private final ChannelProvider delegate; + private final EventPublisher publisher; + private final ClientEventListener hostEventPublisher; + + public TcpChannelProvider(DetachedChannelPipeline channelPipeline, ChannelProvider delegate, + EventPublisher publisher, ClientEventListener hostEventPublisher) { + this.channelPipeline = channelPipeline; + this.delegate = delegate; + this.publisher = publisher; + this.hostEventPublisher = hostEventPublisher; + } + + @Override + public Observable newChannel(Observable input) { + return delegate.newChannel(input) + .map(new Func1() { + @Override + public Channel call(Channel channel) { + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(publisher); + channel.attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).set(hostEventPublisher); + channel.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).set(hostEventPublisher); + channelPipeline.addToChannel(channel); + return channel; + } + }); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProviderFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProviderFactory.java new file mode 100644 index 0000000..c66ecf9 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/client/internal/TcpChannelProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client.internal; + +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; + +public class TcpChannelProviderFactory implements ChannelProviderFactory { + + private final DetachedChannelPipeline channelPipeline; + private final ChannelProviderFactory delegate; + + public TcpChannelProviderFactory(DetachedChannelPipeline channelPipeline, ChannelProviderFactory delegate) { + this.channelPipeline = channelPipeline; + this.delegate = delegate instanceof TcpChannelProviderFactory ? ((TcpChannelProviderFactory) delegate).delegate + : delegate; + } + + @Override + public ChannelProvider newProvider(Host host, EventSource hostEventSource, + EventPublisher publisher, ClientEventListener hostEventPublisher) { + ChannelProvider delegate = this.delegate.newProvider(host, hostEventSource, publisher, hostEventPublisher); + return new TcpChannelProvider(channelPipeline, delegate, publisher, hostEventPublisher); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/ConnectionHandler.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/ConnectionHandler.java new file mode 100644 index 0000000..89c0daf --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/ConnectionHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server; + +import io.reactivex.netty.channel.Connection; +import rx.Observable; + +/** + * A connection handler invoked for every new connection is established by {@link TcpServer} + * + * @param The type of the object that is read from a new connection. + * @param The type of objects that are written to a new connection. + */ +public interface ConnectionHandler { + + /** + * Invoked whenever a new connection is established. + * + * @param newConnection Newly established connection. + * + * @return An {@link Observable}, unsubscribe from which should cancel the handling. + */ + Observable handle(Connection newConnection); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServer.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServer.java new file mode 100644 index 0000000..e59cd10 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServer.java @@ -0,0 +1,442 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventListener; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import io.reactivex.netty.ssl.SslCodec; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +/** + * A TCP server. + * + *

Immutability

+ * An instance of this server is immutable and all mutations produce a new server instance. + * + * @param The type of objects read from this server. + * @param The type of objects written to this server. + */ +public abstract class TcpServer implements EventSource { + + /** + * Creates a new server instance, inheriting all configurations from this server and adding a + * {@link ChannelOption} for the server socket created by the newly created server instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer channelOption(ChannelOption option, T value); + + /** + * Creates a new server instance, inheriting all configurations from this server and adding a + * {@link ChannelOption} for the client socket created by the newly created server instance. + * + * @param option Option to add. + * @param value Value for the option. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer clientChannelOption(ChannelOption option, T value); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. + * The specified handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} + * will be more convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerFirst(String name, Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added at the first position of the pipeline as specified by + * {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param group The {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name The name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be + * more convenient. + * + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerLast(String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added at the last position of the pipeline as specified by + * {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param baseName the name of the existing handler + * @param name Name of the handler. + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory); + + /** + * Adds a {@link ChannelHandler} to {@link ChannelPipeline} for all connections created by this server. The specified + * handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by + * {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)} + * + * For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be more + * convenient. + * + * @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} + * methods + * @param baseName the name of the existing handler + * @param name the name of the handler to append + * @param handlerFactory Factory to create handler instance to add. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * action to configure all the connections created by the newly created server instance. + * + * @param pipelineConfigurator Action to configure {@link ChannelPipeline}. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer pipelineConfigurator(Action1 pipelineConfigurator); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslEngineFactory} for all secured connections accepted by the newly created server instance. + * + * If the {@link SSLEngine} instance can be statically, created, {@link #secure(SSLEngine)} can be used. + * + * @param sslEngineFactory Factory for all secured connections created by the newly created server instance. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer secure(Func1 sslEngineFactory); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslEngine} for all secured connections accepted by the newly created server instance. + * + * If the {@link SSLEngine} instance can not be statically, created, {@link #secure(Func1)} )} can be used. + * + * @param sslEngine {@link SSLEngine} for all secured connections created by the newly created server instance. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer secure(SSLEngine sslEngine); + + /** + * Creates a new server instances, inheriting all configurations from this server and using the passed + * {@code sslCodec} for all secured connections accepted by the newly created server instance. + * + * This is required only when the {@link SslHandler} used by {@link SslCodec} is to be modified before adding to + * the {@link ChannelPipeline}. For most of the cases, {@link #secure(Func1)} or {@link #secure(SSLEngine)} will be + * enough. + * + * @param sslCodec {@link SslCodec} for all secured connections created by the newly created server instance. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer secure(SslCodec sslCodec); + + /** + * Creates a new server instances, inheriting all configurations from this server and using a self-signed + * certificate for all secured connections accepted by the newly created server instance. + * + * This is only for testing and should not be used for real production servers. + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer unsafeSecure(); + + /** + * Creates a new server instances, inheriting all configurations from this server and enabling wire logging at the + * passed level for the newly created server instance. + * + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link io.netty.handler.logging.LoggingHandler} + * + * @return A new {@link TcpServer} instance. + * + * @deprecated Use {@link #enableWireLogging(String, LogLevel)} instead. + */ + @Deprecated + public abstract TcpServer enableWireLogging(LogLevel wireLoggingLevel); + + /** + * Creates a new server instances, inheriting all configurations from this server and enabling wire logging at the + * passed level for the newly created server instance. + * + * @param name Name of the logger that can be used to control the logging dynamically. + * @param wireLoggingLevel Logging level at which the wire logs will be logged. The wire logging will only be done if + * logging is enabled at this level for {@link io.netty.handler.logging.LoggingHandler} + * + * @return A new {@link TcpServer} instance. + */ + public abstract TcpServer enableWireLogging(String name, LogLevel wireLoggingLevel); + + /** + * Returns the port at which this server is running. + * + * For servers using ephemeral ports, this would return the actual port used, only after the server is started. + * + * @return The port at which this server is running. + */ + public abstract int getServerPort(); + + /** + * Returns the address at which this server is running. + * + * @return The address at which this server is running. + */ + public abstract SocketAddress getServerAddress(); + + /** + * Starts this server. + * + * @param connectionHandler Connection handler that will handle any new server connections to this server. + * + * @return This server. + */ + public abstract TcpServer start(ConnectionHandler connectionHandler); + + /** + * Shutdown this server and waits till the server socket is closed. + */ + public abstract void shutdown(); + + /** + * Waits for the shutdown of this server. + * + * This does not actually shutdown the server. It just waits for some other action to shutdown. + */ + public abstract void awaitShutdown(); + + /** + * Waits for the shutdown of this server, waiting a maximum of the passed duration. + * + * This does not actually shutdown the server. It just waits for some other action to shutdown. + * + * @param duration Duration to wait for shutdown. + * @param timeUnit Timeunit for the duration to wait for shutdown. + */ + public abstract void awaitShutdown(long duration, TimeUnit timeUnit); + + /** + * Returns the event publisher for this server. + * + * @return The event publisher for this server. + */ + public abstract TcpServerEventPublisher getEventPublisher(); + + /** + * Creates a new server using an ephemeral port. The port used can be queried after starting this server, using + * {@link #getServerPort()} + * + * @return A new {@link TcpServer} + */ + public static TcpServer newServer() { + return newServer(0); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(int port) { + return new TcpServerImpl<>(new InetSocketAddress(port)); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @param eventLoopGroup Eventloop group to be used for server as well as client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(int port, EventLoopGroup eventLoopGroup, + Class channelClass) { + return newServer(port, eventLoopGroup, eventLoopGroup, channelClass); + } + + /** + * Creates a new server using the passed port. + * + * @param port Port for the server. {@code 0} to use ephemeral port. + * @param acceptGroup Eventloop group to be used for server sockets. + * @param clientGroup Eventloop group to be used for client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(int port, EventLoopGroup acceptGroup, + EventLoopGroup clientGroup, + Class channelClass) { + return newServer(new InetSocketAddress(port), acceptGroup, clientGroup, channelClass); + } + + /** + * Creates a new server using the passed address. + * + * @param socketAddress Socket address for the server. + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(SocketAddress socketAddress) { + return new TcpServerImpl<>(socketAddress); + } + + /** + * Creates a new server using the passed address. + * + * @param socketAddress Socket address for the server. + * @param eventLoopGroup Eventloop group to be used for server as well as client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(SocketAddress socketAddress, EventLoopGroup eventLoopGroup, + Class channelClass) { + return new TcpServerImpl<>(socketAddress, eventLoopGroup, eventLoopGroup, channelClass); + } + + /** + * Creates a new server using the passed address. + * + * @param socketAddress Socket address for the server. + * @param acceptGroup Eventloop group to be used for server sockets. + * @param clientGroup Eventloop group to be used for client sockets. + * @param channelClass The class to be used for server channel. + * + * @return A new {@link TcpServer} + */ + public static TcpServer newServer(SocketAddress socketAddress, EventLoopGroup acceptGroup, + EventLoopGroup clientGroup, + Class channelClass) { + return new TcpServerImpl<>(socketAddress, acceptGroup, clientGroup, channelClass); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerConnectionToChannelBridge.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerConnectionToChannelBridge.java new file mode 100644 index 0000000..88a7eed --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerConnectionToChannelBridge.java @@ -0,0 +1,157 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.reactivex.netty.channel.AbstractConnectionToChannelBridge; +import io.reactivex.netty.channel.ChannelSubscriberEvent; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.channel.EmitConnectionEvent; +import io.reactivex.netty.events.Clock; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func1; + +import java.nio.channels.ClosedChannelException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.concurrent.TimeUnit.*; + +/** + * An implementation of {@link AbstractConnectionToChannelBridge} for servers. + * + * @param The type of objects read from the server using this bridge. + * @param The type of objects written to this server using this bridge. + */ +public class TcpServerConnectionToChannelBridge extends AbstractConnectionToChannelBridge { + + private static final Logger logger = Logger.getLogger(TcpServerConnectionToChannelBridge.class.getName()); + private static final String HANDLER_NAME = "server-conn-channel-bridge"; + + private final ConnectionHandler connectionHandler; + private final TcpServerEventPublisher eventPublisher; + private final boolean isSecure; + private final ChannelSubscriberEvent channelSubscriberEvent; + + private TcpServerConnectionToChannelBridge(ConnectionHandler connectionHandler, + TcpServerEventPublisher eventPublisher, boolean isSecure) { + super(HANDLER_NAME, eventPublisher, eventPublisher); + this.connectionHandler = connectionHandler; + this.eventPublisher = eventPublisher; + this.isSecure = isSecure; + channelSubscriberEvent = new ChannelSubscriberEvent<>(new NewChannelSubscriber()); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + userEventTriggered(ctx, channelSubscriberEvent); + if (!isSecure) {/*When secure, the event is triggered post SSL handshake via the SslCodec*/ + userEventTriggered(ctx, EmitConnectionEvent.INSTANCE); + } + super.channelRegistered(ctx); + } + + public static TcpServerConnectionToChannelBridge addToPipeline(ChannelPipeline pipeline, + ConnectionHandler connectionHandler, + TcpServerEventPublisher eventPublisher, + boolean isSecure) { + TcpServerConnectionToChannelBridge toAdd = new TcpServerConnectionToChannelBridge<>(connectionHandler, + eventPublisher, isSecure); + pipeline.addLast(HANDLER_NAME, toAdd); + return toAdd; + } + + private final class NewChannelSubscriber extends Subscriber { + + @Override + public void onCompleted() { + // No Op. + } + + @Override + public void onError(Throwable e) { + logger.log(Level.SEVERE, "Error while listening for new client connections.", e); + } + + @Override + public void onNext(final Channel channel) { + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(eventPublisher); + channel.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).set(eventPublisher); + + final Connection connection = ConnectionImpl.fromChannel(channel); + final long startTimeNanos = eventPublisher.publishingEnabled() ? Clock.newStartTimeNanos() : -1; + if (eventPublisher.publishingEnabled()) { + eventPublisher.onNewClientConnected(); + } + Observable handledObservable; + try { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onConnectionHandlingStart(Clock.onEndNanos(startTimeNanos), NANOSECONDS); + } + handledObservable = connectionHandler.handle(connection); + } catch (Throwable throwable) { + handledObservable = Observable.error(throwable); + } + + if (null == handledObservable) { + logger.log(Level.SEVERE, "Connection handler returned null."); + handledObservable = Observable.empty(); + } + + handledObservable + .onErrorResumeNext( + new Func1>() { + @Override + public Observable call(Throwable throwable) { + if (throwable instanceof ClosedChannelException) { + return Observable.empty(); + } else { + /*Since, this is always reading input for new requests, it will always get a + closed channel exception on connection close from client. No point in logging + that error.*/ + if (eventPublisher.publishingEnabled()) { + eventPublisher.onConnectionHandlingFailed(Clock.onEndNanos(startTimeNanos), + NANOSECONDS, + throwable); + } + logger.log(Level.SEVERE, "Error processing connection.", throwable); + return connection.close(); + } + } + }) + .ambWith(connection.closeListener()) + .concatWith(connection.close()) + .doOnCompleted(new Action0() { + @Override + public void call() { + if (eventPublisher.publishingEnabled()) { + eventPublisher.onConnectionHandlingSuccess(Clock.onEndNanos(startTimeNanos), + NANOSECONDS); + } + } + }) + .subscribe(); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerImpl.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerImpl.java new file mode 100644 index 0000000..362b952 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerImpl.java @@ -0,0 +1,281 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventListener; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import io.reactivex.netty.server.ServerState; +import io.reactivex.netty.ssl.SslCodec; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class TcpServerImpl extends TcpServer { + + private static final Logger logger = Logger.getLogger(TcpServerImpl.class.getName()); + + protected enum ServerStatus {Created, Starting, Started, Shutdown} + + private final ServerState state; + private ChannelFuture bindFuture; + protected final AtomicReference serverStateRef; + + public TcpServerImpl(SocketAddress socketAddress) { + state = TcpServerState.create(socketAddress); + serverStateRef = new AtomicReference<>(ServerStatus.Created); + } + + public TcpServerImpl(SocketAddress socketAddress, EventLoopGroup parent, EventLoopGroup child, + Class channelClass) { + state = TcpServerState.create(socketAddress, parent, child, channelClass); + serverStateRef = new AtomicReference<>(ServerStatus.Created); + } + + private TcpServerImpl(ServerState state) { + this.state = state; + serverStateRef = new AtomicReference<>(ServerStatus.Created); + } + + @Override + public TcpServer channelOption(ChannelOption option, T value) { + return copy(state.channelOption(option, value)); + } + + @Override + public TcpServer clientChannelOption(ChannelOption option, T value) { + return copy(state.clientChannelOption(option, value)); + } + + @Override + public TcpServer addChannelHandlerFirst(String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerFirst(name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerFirst(group, name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerLast(String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerLast(name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerLast(group, name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerBefore(String baseName, String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerBefore(baseName, name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerBefore(group, baseName, name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerAfter(String baseName, String name, Func0 handlerFactory) { + return copy(state.addChannelHandlerAfter(baseName, name, handlerFactory)); + } + + @Override + public TcpServer addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name, + Func0 handlerFactory) { + return copy(state.addChannelHandlerAfter(group, baseName, name, handlerFactory)); + } + + @Override + public TcpServer pipelineConfigurator(Action1 pipelineConfigurator) { + return copy(state.pipelineConfigurator(pipelineConfigurator)); + } + + @Override + public TcpServer secure(Func1 sslEngineFactory) { + return copy(((TcpServerState)state).secure(sslEngineFactory)); + } + + @Override + public TcpServer secure(SSLEngine sslEngine) { + return copy(((TcpServerState)state).secure(sslEngine)); + } + + @Override + public TcpServer secure(SslCodec sslCodec) { + return copy(((TcpServerState)state).secure(sslCodec)); + } + + @Override + public TcpServer unsafeSecure() { + return copy(((TcpServerState)state).unsafeSecure()); + } + + @Override + @Deprecated + public TcpServer enableWireLogging(LogLevel wireLoggingLevel) { + return copy(state.enableWireLogging(wireLoggingLevel)); + } + + @Override + public TcpServer enableWireLogging(String name, LogLevel wireLoggingLevel) { + return copy(state.enableWireLogging(name, wireLoggingLevel)); + } + + @Override + public int getServerPort() { + + final SocketAddress localAddress = getServerAddress(); + if (localAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) localAddress).getPort(); + } else { + return 0; + } + } + + @Override + public SocketAddress getServerAddress() { + SocketAddress localAddress; + if (null != bindFuture && bindFuture.isDone()) { + localAddress = bindFuture.channel().localAddress(); + } else { + localAddress = state.getServerAddress(); + } + return localAddress; + } + + @Override + public TcpServer start(final ConnectionHandler connectionHandler) { + if (!serverStateRef.compareAndSet(ServerStatus.Created, ServerStatus.Starting)) { + throw new IllegalStateException("Server already started"); + } + try { + Action1 handlerFactory = new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + TcpServerState tcpState = (TcpServerState) state; + TcpServerConnectionToChannelBridge.addToPipeline(pipeline, connectionHandler, + tcpState.getEventPublisher(), tcpState.isSecure()); + } + }; + final TcpServerState newState = (TcpServerState) state.pipelineConfigurator(handlerFactory); + bindFuture = newState.getBootstrap().bind(newState.getServerAddress()).sync(); + if (!bindFuture.isSuccess()) { + throw new RuntimeException(bindFuture.cause()); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + serverStateRef.set(ServerStatus.Started); // It will come here only if this was the thread that transitioned to Starting + + logger.info("Rx server started at port: " + getServerPort()); + + return this; + } + + @Override + public void shutdown() { + if (!serverStateRef.compareAndSet(ServerStatus.Started, ServerStatus.Shutdown)) { + throw new IllegalStateException("The server is already shutdown."); + } else { + try { + bindFuture.channel().close().sync(); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Interrupted while waiting for the server socket to close.", e); + } + } + } + + @Override + public void awaitShutdown() { + ServerStatus status = serverStateRef.get(); + switch (status) { + case Created: + case Starting: + throw new IllegalStateException("Server not started yet."); + case Started: + try { + bindFuture.channel().closeFuture().await(); + } catch (InterruptedException e) { + Thread.interrupted(); // Reset the interrupted status + logger.log(Level.SEVERE, "Interrupted while waiting for the server socket to close.", e); + } + break; + case Shutdown: + // Nothing to do as it is already shutdown. + break; + } + } + + @Override + public void awaitShutdown(long duration, TimeUnit timeUnit) { + ServerStatus status = serverStateRef.get(); + switch (status) { + case Created: + case Starting: + throw new IllegalStateException("Server not started yet."); + case Started: + try { + bindFuture.channel().closeFuture().await(duration, timeUnit); + } catch (InterruptedException e) { + Thread.interrupted(); // Reset the interrupted status + logger.log(Level.SEVERE, "Interrupted while waiting for the server socket to close.", e); + } + break; + case Shutdown: + // Nothing to do as it is already shutdown. + break; + } + } + + @Override + public TcpServerEventPublisher getEventPublisher() { + return ((TcpServerState)state).getEventPublisher(); + } + + @Override + public Subscription subscribe(TcpServerEventListener listener) { + return ((TcpServerState)state).getEventPublisher().subscribe(listener); + } + + private static TcpServer copy(ServerState newState) { + return new TcpServerImpl<>(newState); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerInterceptorChain.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerInterceptorChain.java new file mode 100644 index 0000000..e89157f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerInterceptorChain.java @@ -0,0 +1,257 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.channel.Connection; +import rx.annotations.Beta; + +/** + * A utility to create an interceptor chain to be used with a {@link TcpServer} to modify behavior of connections + * accepted by that server. + * + *

What are interceptors?

+ * + * Interceptors can be used to achieve use-cases that involve instrumentation related to any behavior of the connection, + * they can even be used to short-circuit the rest of the chain or to provide canned responses. In order to achieve such + * widely different use-cases, an interceptor is modelled as a simple function that takes one {@link ConnectionHandler} + * and returns another {@link ConnectionHandler} instance. With this low level abstraction, any use-case pertaining to + * connection instrumentation can be achieved. + * + * + * An interceptor chain always starts with an interceptor and ends with a {@link ConnectionHandler} and any number of + * other interceptors can exist between the start and end.

+ * + * A chain can be created by using the various {@code start*()} methods available in this class, eg: + * {@link #start(Interceptor)}, {@link #startRaw(Interceptor)}.

+ * + * After starting a chain, any number of other interceptors can be added by using the various {@code next*()} methods + * available in this class, eg: {@link #next(Interceptor)}, {@link #nextWithTransform(TransformingInterceptor)}, + * {@link #nextWithReadTransform(TransformingInterceptor)} and {@link #nextWithWriteTransform(TransformingInterceptor)}

+ * + * After adding the required interceptors, by providing a {@link ConnectionHandler} via the + * {@link #end(ConnectionHandler)} method, the chain can be ended and the returned {@link ConnectionHandler} can be used + * with any {@link TcpServer}

+ * + * So, a typical interaction with this class would look like:

+ * + * {@code + * TcpServer.newServer().start(TcpServerInterceptorChain.start(first).next(second).next(third).end(handler)) + * } + * + *

Simple Interceptor

+ * + * For interceptors that do not change the types of objects read or written to the underlying connection, the interface + * {@link Interceptor} defines the interceptor contract. + * + *

Modifying the type of data read/written to the {@link Connection}

+ * + * Sometimes, it is required to change the type of objects read or written to a {@link Connection} instance handled by + * a {@link TcpServer}. For such cases, the interface {@link TransformingInterceptor} defines + * the interceptor contract. Since, this included 4 generic arguments to the interceptor, this is not the base type for + * all interceptors and should be used only when the types of the {@link Connection} are actually to be changed. + * + * + * The above diagram depicts the execution order of interceptors. The first connection handler (internal) is created by + * this class and as is returned by {@link #end(ConnectionHandler)} method by providing a {@link ConnectionHandler} that + * does the actual processing of the connection. {@link TcpServer} with which this interceptor chain is used, will + * invoke the internal connection handler provided by this class.

+ * + * The interceptors are invoked in the order that they are added to this chain. + * + * @param The type of objects read from a connection to {@link TcpServer} with which this interceptor chain will be + * used. + * @param The type of objects written to a connection to {@link TcpServer} with which this interceptor chain will be + * used. + * @param The type of objects read from a connection to {@link TcpServer} after applying this interceptor chain. + * @param The type of objects written to a connection to {@link TcpServer} after applying this interceptor chain. + */ +@Beta +public final class TcpServerInterceptorChain { + + private final TransformingInterceptor interceptor; + + private TcpServerInterceptorChain(TransformingInterceptor interceptor) { + this.interceptor = interceptor; + } + + /** + * Add the next interceptor to this chain. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public TcpServerInterceptorChain next(final Interceptor next) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects read from the connections processed by + * the associated {@link TcpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public TcpServerInterceptorChain nextWithReadTransform(final TransformingInterceptor next) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects written to the connections processed by + * the associated {@link TcpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public TcpServerInterceptorChain nextWithWriteTransform(final TransformingInterceptor next) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Add the next interceptor to this chain, which changes the type of objects read and written from/to the + * connections processed by the associated {@link TcpServer}. + * + * @param next Next interceptor to add. + * + * @return A new interceptor chain with the interceptors current existing and the passed interceptor added to the + * end. + */ + public TcpServerInterceptorChain nextWithTransform(final TransformingInterceptor next) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return interceptor.intercept(next.intercept(handler)); + } + }); + } + + /** + * Terminates this chain with the passed {@link ConnectionHandler} and returns a {@link ConnectionHandler} to be + * used by a {@link TcpServer} + * + * @param handler Connection handler to use. + * + * @return A connection handler that wires the interceptor chain, to be used with {@link TcpServer} instead of + * directly using the passed {@code handler} + */ + public ConnectionHandler end(ConnectionHandler handler) { + return interceptor.intercept(handler); + } + + /** + * One of the methods to start creating the interceptor chain. The other start methods can be used for starting with + * interceptors that modify the type of Objects read/written from/to the connections processed by the associated + * {@link TcpServer}. + * + * @param start The starting interceptor for this chain. + * + * @param The type of objects read from a connection to {@link TcpServer} with which this interceptor chain will + * be used. + * @param The type of objects written to a connection to {@link TcpServer} with which this interceptor chain + * will be used. + * + * @return A new interceptor chain. + */ + public static TcpServerInterceptorChain start(final Interceptor start) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return start.intercept(handler); + } + }); + } + + /** + * One of the methods to start creating the interceptor chain. The other start methods can be used for starting with + * interceptors that modify the type of Objects read/written from/to the connections processed by the associated + * {@link TcpServer}. + * + * @param start The starting interceptor for this chain. + * + * @return A new interceptor chain. + */ + public static TcpServerInterceptorChain startRaw(final Interceptor start) { + return new TcpServerInterceptorChain<>(new TransformingInterceptor() { + @Override + public ConnectionHandler intercept(ConnectionHandler handler) { + return start.intercept(handler); + } + }); + } + + /** + * An interceptor that preserves the type of objects read and written to the connection. + * + * @param Type of objects read from the connection handled by this interceptor. + * @param Type of objects written to the connection handled by this interceptor. + */ + public interface Interceptor { + + /** + * Intercepts and optionally changes the passed {@code ConnectionHandler}. + * + * @param handler Handler to intercept. + * + * @return Handler to use after this transformation. + */ + ConnectionHandler intercept(ConnectionHandler handler); + + } + + /** + * An interceptor that changes the type of objects read and written to the connection. + * + * @param Type of objects read from the connection before applying this interceptor. + * @param Type of objects written to the connection before applying this interceptor. + * @param Type of objects read from the connection after applying this interceptor. + * @param Type of objects written to the connection after applying this interceptor. + */ + public interface TransformingInterceptor { + + /** + * Intercepts and changes the passed {@code ConnectionHandler}. + * + * @param handler Handler to intercept. + * + * @return Handler to use after this transformation. + */ + ConnectionHandler intercept(ConnectionHandler handler); + + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerState.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerState.java new file mode 100644 index 0000000..64f00a1 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/TcpServerState.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.reactivex.netty.RxNetty; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import io.reactivex.netty.server.ServerState; +import io.reactivex.netty.ssl.DefaultSslCodec; +import io.reactivex.netty.ssl.SslCodec; +import rx.exceptions.Exceptions; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; + +/** + * A collection of state that {@link TcpServer} holds. + * This supports the copy-on-write semantics of {@link TcpServer} + * + * @param The type of objects read from the server owning this state. + * @param The type of objects written to the server owning this state. + */ +public class TcpServerState extends ServerState { + + private final TcpServerEventPublisher eventPublisher; + private final boolean secure; + + protected TcpServerState(SocketAddress socketAddress, EventLoopGroup parent, + EventLoopGroup child, + Class channelClass) { + super(socketAddress, parent, child, channelClass); + secure = false; + eventPublisher = new TcpServerEventPublisher(); + } + + protected TcpServerState(TcpServerState toCopy, SslCodec sslCodec) { + super(toCopy, toCopy.detachedPipeline.configure(sslCodec)); + secure = true; + eventPublisher = toCopy.eventPublisher.copy(); + } + + protected TcpServerState(TcpServerState toCopy, SocketAddress socketAddress) { + super(toCopy, socketAddress); + secure = toCopy.secure; + eventPublisher = toCopy.eventPublisher.copy(); + } + + protected TcpServerState(TcpServerState toCopy, ServerBootstrap clone) { + super(toCopy, clone); + secure = toCopy.secure; + eventPublisher = toCopy.eventPublisher.copy(); + } + + protected TcpServerState(TcpServerState toCopy, DetachedChannelPipeline newPipeline) { + super(toCopy, newPipeline); + secure = toCopy.secure; + eventPublisher = toCopy.eventPublisher.copy(); + } + + public TcpServerState secure(Func1 sslEngineFactory) { + return secure(new DefaultSslCodec(sslEngineFactory)); + } + + public TcpServerState secure(SSLEngine sslEngine) { + return secure(new DefaultSslCodec(sslEngine)); + } + + public TcpServerState secure(SslCodec sslCodec) { + return new TcpServerState<>(this, sslCodec); + } + + public TcpServerState unsafeSecure() { + return secure(new DefaultSslCodec(new Func1() { + @Override + public SSLEngine call(ByteBufAllocator allocator) { + SelfSignedCertificate ssc; + try { + ssc = new SelfSignedCertificate(); + return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .build() + .newEngine(allocator); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + })); + } + + @Override + protected ServerState copyBootstrapOnly() { + return new TcpServerState<>(this, bootstrap.clone()); + } + + @Override + protected ServerState copy() { + return new TcpServerState<>(this, detachedPipeline.copy()); + } + + @Override + protected ServerState copy(SocketAddress newSocketAddress) { + return new TcpServerState<>(this, socketAddress); + } + + public boolean isSecure() { + return secure; + } + + public TcpServerEventPublisher getEventPublisher() { + return eventPublisher; + } + + /*package private. Should not leak as it is mutable*/ ServerBootstrap getBootstrap() { + return bootstrap; + } + + public static TcpServerState create(SocketAddress socketAddress) { + return create(socketAddress, RxNetty.getRxEventLoopProvider().globalServerEventLoop(true), + RxNetty.isUsingNativeTransport() ? EpollServerSocketChannel.class : NioServerSocketChannel.class); + } + + public static TcpServerState create(SocketAddress socketAddress, EventLoopGroup group, + Class channelClass) { + return create(socketAddress, group, group, channelClass); + } + + public static TcpServerState create(SocketAddress socketAddress, EventLoopGroup parent, + EventLoopGroup child, + Class channelClass) { + return new TcpServerState<>(socketAddress, parent, child, channelClass); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/SafeTcpServerEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/SafeTcpServerEventListener.java new file mode 100644 index 0000000..7315981 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/SafeTcpServerEventListener.java @@ -0,0 +1,187 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server.events; + +import io.reactivex.netty.events.internal.SafeEventListener; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +final class SafeTcpServerEventListener extends TcpServerEventListener implements SafeEventListener { + + private final TcpServerEventListener delegate; + private final AtomicBoolean completed = new AtomicBoolean(); + + public SafeTcpServerEventListener(TcpServerEventListener delegate) { + this.delegate = delegate; + } + + @Override + public void onCompleted() { + if (completed.compareAndSet(false, true)) { + delegate.onCompleted(); + } + } + + @Override + public void onNewClientConnected() { + if (!completed.get()) { + delegate.onNewClientConnected(); + } + } + + @Override + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionHandlingStart(duration, timeUnit); + } + } + + @Override + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionHandlingSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionHandlingFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onByteRead(long bytesRead) { + if (!completed.get()) { + delegate.onByteRead(bytesRead); + } + } + + @Override + public void onByteWritten(long bytesWritten) { + if (!completed.get()) { + delegate.onByteWritten(bytesWritten); + } + } + + @Override + public void onFlushStart() { + if (!completed.get()) { + delegate.onFlushStart(); + } + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onFlushComplete(duration, timeUnit); + } + } + + @Override + public void onWriteStart() { + if (!completed.get()) { + delegate.onWriteStart(); + } + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onWriteSuccess(duration, timeUnit); + } + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onWriteFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onConnectionCloseStart() { + if (!completed.get()) { + delegate.onConnectionCloseStart(); + } + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onConnectionCloseSuccess(duration, timeUnit); + } + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + if (!completed.get()) { + delegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event) { + if (!completed.get()) { + delegate.onCustomEvent(event); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, duration, timeUnit, throwable); + } + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + if (!completed.get()) { + delegate.onCustomEvent(event, throwable); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SafeTcpServerEventListener)) { + return false; + } + + SafeTcpServerEventListener that = (SafeTcpServerEventListener) o; + + return !(delegate != null? !delegate.equals(that.delegate) : that.delegate != null); + + } + + @Override + public int hashCode() { + return delegate != null? delegate.hashCode() : 0; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventListener.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventListener.java new file mode 100644 index 0000000..b3b6b37 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventListener.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server.events; + +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.protocol.tcp.server.TcpServer; + +import java.util.concurrent.TimeUnit; + +/** + * A listener for all events published by {@link TcpServer} + */ +public abstract class TcpServerEventListener extends ConnectionEventListener { + + /** + * Event whenever a new client connection is accepted. + */ + public void onNewClientConnected() { } + + /** + * Event when any connection handling starts. + * + * @param duration Time between a client connection is accepted to when it is handled by the connection handler. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { } + + /** + * Event when any connection handling is successfully completed. + * + * @param duration Time taken for connection handling. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) {} + + /** + * Event when any connection handling completes with an error. + * + * @param duration Time taken for connection handling. + * @param timeUnit Time unit for the duration. + */ + @SuppressWarnings("unused") + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, Throwable throwable) { } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisher.java b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisher.java new file mode 100644 index 0000000..a2f1526 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisher.java @@ -0,0 +1,199 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server.events; + +import io.reactivex.netty.channel.events.ConnectionEventPublisher; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.events.ListenersHolder; +import io.reactivex.netty.events.internal.SafeEventListener; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.subscriptions.CompositeSubscription; + +import java.util.concurrent.TimeUnit; + +public final class TcpServerEventPublisher extends TcpServerEventListener + implements EventSource, EventPublisher { + + private static final Action1 NEW_CLIENT_ACTION = new Action1() { + @Override + public void call(TcpServerEventListener l) { + l.onNewClientConnected(); + } + }; + + private static final Action3 HANDLE_START_ACTION = + new Action3() { + @Override + public void call(TcpServerEventListener l, Long duration, TimeUnit timeUnit) { + l.onConnectionHandlingStart(duration, timeUnit); + } + }; + + private static final Action3 HANDLE_SUCCESS_ACTION = + new Action3() { + @Override + public void call(TcpServerEventListener l, Long duration, TimeUnit timeUnit) { + l.onConnectionHandlingSuccess(duration, timeUnit); + } + }; + + private static final Action4 HANDLE_FAILED_ACTION = + new Action4() { + @Override + public void call(TcpServerEventListener l, Long duration, TimeUnit timeUnit, Throwable t) { + l.onConnectionHandlingFailed(duration, timeUnit, t); + } + }; + + private final ListenersHolder listeners; + private final ConnectionEventPublisher connDelegate; + + public TcpServerEventPublisher() { + listeners = new ListenersHolder<>(); + connDelegate = new ConnectionEventPublisher<>(); + } + + private TcpServerEventPublisher(TcpServerEventPublisher toCopy) { + listeners = toCopy.listeners.copy(); + connDelegate = toCopy.connDelegate.copy(); + } + + @Override + public void onNewClientConnected() { + listeners.invokeListeners(NEW_CLIENT_ACTION); + } + + @Override + public void onConnectionHandlingStart(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(HANDLE_START_ACTION, duration, timeUnit); + } + + @Override + public void onConnectionHandlingSuccess(final long duration, final TimeUnit timeUnit) { + listeners.invokeListeners(HANDLE_SUCCESS_ACTION, duration, timeUnit); + } + + @Override + public void onConnectionHandlingFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) { + listeners.invokeListeners(HANDLE_FAILED_ACTION, duration, timeUnit, throwable); + } + + @Override + public void onByteRead(long bytesRead) { + connDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + connDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onFlushStart() { + connDelegate.onFlushStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + connDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onWriteStart() { + connDelegate.onWriteStart(); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + connDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseStart() { + connDelegate.onConnectionCloseStart(); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + connDelegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event) { + connDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + connDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + connDelegate.onCustomEvent(event, throwable); + } + + @Override + public boolean publishingEnabled() { + return listeners.publishingEnabled(); + } + + @Override + public Subscription subscribe(TcpServerEventListener listener) { + if (!SafeEventListener.class.isAssignableFrom(listener.getClass())) { + listener = new SafeTcpServerEventListener(listener); + } + + CompositeSubscription cs = new CompositeSubscription(); + cs.add(listeners.subscribe(listener)); + cs.add(connDelegate.subscribe(listener)); + return cs; + } + + public TcpServerEventPublisher copy() { + return new TcpServerEventPublisher(this); + } + + /*Visible for testing*/ListenersHolder getListeners() { + return listeners; + } + + /*Visible for testing*/ConnectionEventPublisher getConnDelegate() { + return connDelegate; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/server/ServerState.java b/netty-http-rx/src/main/java/io/reactivex/netty/server/ServerState.java new file mode 100644 index 0000000..f83612e --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/server/ServerState.java @@ -0,0 +1,194 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.HandlerNames; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.channel.WriteTransformer; +import rx.functions.Action1; +import rx.functions.Func0; + +import java.net.SocketAddress; + +/** + * A collection of state that a server holds. + * + * @param The type of objects read from the server owning this state. + * @param The type of objects written to the server owning this state. + */ +public abstract class ServerState { + + protected final SocketAddress socketAddress; + protected final ServerBootstrap bootstrap; + protected final DetachedChannelPipeline detachedPipeline; + + protected ServerState(SocketAddress socketAddress, EventLoopGroup parent, EventLoopGroup child, + Class channelClass) { + this.socketAddress = socketAddress; + bootstrap = new ServerBootstrap(); + bootstrap.childOption(ChannelOption.AUTO_READ, false); // by default do not read content unless asked. + bootstrap.group(parent, child); + bootstrap.channel(channelClass); + detachedPipeline = new DetachedChannelPipeline(); + detachedPipeline.addLast(HandlerNames.WriteTransformer.getName(), new Func0() { + @Override + public ChannelHandler call() { + return new WriteTransformer(); + } + }); + bootstrap.childHandler(detachedPipeline.getChannelInitializer()); + } + + protected ServerState(ServerState toCopy, final ServerBootstrap newBootstrap) { + socketAddress = toCopy.socketAddress; + bootstrap = newBootstrap; + detachedPipeline = toCopy.detachedPipeline; + bootstrap.childHandler(detachedPipeline.getChannelInitializer()); + } + + protected ServerState(ServerState toCopy, final DetachedChannelPipeline newPipeline) { + final ServerState toCopyCast = toCopy.cast(); + socketAddress = toCopy.socketAddress; + bootstrap = toCopyCast.bootstrap.clone(); + detachedPipeline = newPipeline; + bootstrap.childHandler(detachedPipeline.getChannelInitializer()); + } + + protected ServerState(ServerState toCopy, final SocketAddress socketAddress) { + this.socketAddress = socketAddress; + bootstrap = toCopy.bootstrap.clone(); + detachedPipeline = toCopy.detachedPipeline; + bootstrap.childHandler(detachedPipeline.getChannelInitializer()); + } + + public ServerState channelOption(ChannelOption option, T value) { + ServerState copy = copyBootstrapOnly(); + copy.bootstrap.option(option, value); + return copy; + } + + public ServerState clientChannelOption(ChannelOption option, T value) { + ServerState copy = copyBootstrapOnly(); + copy.bootstrap.childOption(option, value); + return copy; + } + + public ServerState addChannelHandlerFirst(String name, Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addFirst(name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerFirst(EventExecutorGroup group, String name, + Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addFirst(group, name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerLast(String name, Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addLast(name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerLast(EventExecutorGroup group, String name, + Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addLast(group, name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerBefore(String baseName, String name, + Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addBefore(baseName, name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerBefore(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addBefore(group, baseName, name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerAfter(String baseName, String name, + Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addAfter(baseName, name, handlerFactory); + return copy; + } + + public ServerState addChannelHandlerAfter(EventExecutorGroup group, String baseName, + String name, Func0 handlerFactory) { + ServerState copy = copy(); + copy.detachedPipeline.addAfter(group, baseName, name, handlerFactory); + return copy; + } + + public ServerState pipelineConfigurator(Action1 pipelineConfigurator) { + ServerState copy = copy(); + copy.detachedPipeline.configure(pipelineConfigurator); + return copy; + } + + public ServerState enableWireLogging(final LogLevel wireLoggingLevel) { + return enableWireLogging(LoggingHandler.class.getName(), wireLoggingLevel); + } + + public ServerState enableWireLogging(final String name, final LogLevel wireLoggingLevel) { + return addChannelHandlerFirst(HandlerNames.WireLogging.getName(), new Func0() { + @Override + public ChannelHandler call() { + return new LoggingHandler(name, wireLoggingLevel); + } + }); + } + + public ServerState serverAddress(SocketAddress socketAddress) { + return copy(socketAddress); + } + + public SocketAddress getServerAddress() { + return socketAddress; + } + + /*package private. Should not leak as it is mutable*/ ServerBootstrap getBootstrap() { + return bootstrap; + } + + protected abstract ServerState copyBootstrapOnly(); + + protected abstract ServerState copy(); + + protected abstract ServerState copy(SocketAddress newSocketAddress); + + @SuppressWarnings("unchecked") + private ServerState cast() { + return (ServerState) this; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/ssl/DefaultSslCodec.java b/netty-http-rx/src/main/java/io/reactivex/netty/ssl/DefaultSslCodec.java new file mode 100644 index 0000000..4b6fd5b --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/ssl/DefaultSslCodec.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.ssl; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.ssl.SslHandler; +import rx.functions.Func1; + +import javax.net.ssl.SSLEngine; + +/** + * Default implementation of {@link SslCodec} that uses a statically created {@link SSLEngine} or a provided factory to + * create one dynamically. + * + * No custom configurations are applied to the created {@link SslHandler} unless done so explicity by overriding + * {@link #configureHandler(SslHandler)} + */ +public class DefaultSslCodec extends SslCodec { + + private final Func1 engineFactory; + + public DefaultSslCodec(Func1 engineFactory) { + this.engineFactory = engineFactory; + } + + public DefaultSslCodec(final SSLEngine sslEngine) { + this(new Func1() { + @Override + public SSLEngine call(ByteBufAllocator allocator) { + return sslEngine; + } + }); + } + + @Override + protected SslHandler newSslHandler(ChannelPipeline pipeline) { + SslHandler toReturn = new SslHandler(engineFactory.call(pipeline.channel().alloc())); + configureHandler(toReturn); + return toReturn; + } + + /** + * An optional method that can be overridden to add any custom configurations to the {@link SslHandler} returned + * by {@link #newSslHandler(ChannelPipeline)} + * + * @param handler Handler to configure. + */ + protected void configureHandler(@SuppressWarnings("unused")SslHandler handler) { + // No Op .. + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/ssl/SslCodec.java b/netty-http-rx/src/main/java/io/reactivex/netty/ssl/SslCodec.java new file mode 100644 index 0000000..a4b1483 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/ssl/SslCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.ssl; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.reactivex.netty.channel.ConnectionCreationFailedEvent; +import io.reactivex.netty.channel.EmitConnectionEvent; +import rx.functions.Action1; + +import static io.reactivex.netty.HandlerNames.*; + +/** + * A codec to use when enabling SSL/TLS on SSL + * · TLS and StartTLS support for a TCP client/server. + * + * This codec requires an {@link SslHandler} instance and adds the necessary infrastructure required for the + * TCP client/server to work. + */ +public abstract class SslCodec implements Action1 { + + protected SslCodec() { + } + + @Override + public final void call(final ChannelPipeline pipeline) { + final SslHandler sslHandler = newSslHandler(pipeline); + ChannelHandler wireLogging = pipeline.get(WireLogging.getName()); + if (null != wireLogging) { + /*So that, all activity on the channel is printed including SSL.*/ + pipeline.addAfter(WireLogging.getName(), SslHandler.getName(), sslHandler); + } else { + pipeline.addFirst(SslHandler.getName(), sslHandler); + } + + pipeline.addAfter(SslHandler.getName(), SslConnectionEmissionHandler.getName(), new SslConnEmissionHandler()); + } + + protected abstract SslHandler newSslHandler(ChannelPipeline pipeline); + + private static final class SslConnEmissionHandler extends ChannelDuplexHandler { + + private boolean handshakeDone; + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.read(); // Read till handshake is over, else handshake will never be done, without reading from channel. + super.channelActive(ctx); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + if (!handshakeDone) { + ctx.read(); /*Read till handshake over.*/ + } + super.channelReadComplete(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + handshakeDone = true; + SslHandshakeCompletionEvent handshakeCompletionEvent = (SslHandshakeCompletionEvent) evt; + if (handshakeCompletionEvent.isSuccess()) { + ctx.fireUserEventTriggered(EmitConnectionEvent.INSTANCE); + } else { + ctx.fireUserEventTriggered(new ConnectionCreationFailedEvent(handshakeCompletionEvent.cause())); + } + } + super.userEventTriggered(ctx, evt); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroup.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroup.java new file mode 100644 index 0000000..8bc0ea0 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroup.java @@ -0,0 +1,215 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.threads; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ScheduledFuture; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * This {@link EventLoopGroup} can be used for clients that favors use of the "current" {@link EventLoop} for the + * outbound connection. A current {@link EventLoop} is determined by checking if the thread calling {@link #next()} is + * an eventloop instance belonging to this group. If so, the same instance is returned from {@link #next()} otherwise, + * the call to {@link #next()} is delegated to the actual {@link EventLoopGroup} passed to this instance. + * + * This is generally useful for applications that process a received request by calling some other downstream + * applications. If, during processing of these requests, there is no new thread introduced, then the {@link EventLoop} + * processing the received request will also execute the outbound request to another application. + * + * The above, although being subtle has benefits around removing queuing while writing data to any channel in the entire + * request processing. + */ +public class PreferCurrentEventLoopGroup implements EventLoopGroup { + + private final FastThreadLocal self = new FastThreadLocal<>(); + private final EventLoopGroup delegate; + + public PreferCurrentEventLoopGroup(EventLoopGroup delegate) { + this.delegate = delegate; + for (final EventExecutor child : delegate) { + child.submit(new Runnable() { + @Override + public void run() { + if (child instanceof EventLoop) { + self.set((EventLoop) child); + } + } + }); // Since this is an optimization, there is no need for us to wait for this task to finish. + } + } + + @Override + public EventLoop next() { + final EventLoop thisEventLoop = self.get(); + return null != thisEventLoop ? thisEventLoop : delegate.next(); + } + + @Override + public ChannelFuture register(Channel channel) { + return next().register(channel); + } + + @Override + public ChannelFuture register(ChannelPromise promise) { + return next().register(promise); + } + + @Deprecated + @Override + public ChannelFuture register(Channel channel, + ChannelPromise promise) { + return next().register(channel, promise); + } + + @Override + public boolean isShuttingDown() { + return delegate.isShuttingDown(); + } + + @Override + public Future shutdownGracefully() { + return delegate.shutdownGracefully(); + } + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, + TimeUnit unit) { + return delegate.shutdownGracefully(quietPeriod, timeout, unit); + } + + @Override + public Future terminationFuture() { + return delegate.terminationFuture(); + } + + @Override + @Deprecated + public void shutdown() { + delegate.shutdown(); + } + + @Override + @Deprecated + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + @Deprecated + public Iterator iterator() { + return delegate.iterator(); + } + + @Override + public Future submit(Runnable task) { + return next().submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return next().submit(task, result); + } + + @Override + public Future submit(Callable task) { + return next().submit(task); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, + TimeUnit unit) { + return next().schedule(command, delay, unit); + } + + @Override + public ScheduledFuture schedule(Callable callable, + long delay, TimeUnit unit) { + return next().schedule(callable, delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, + long period, + TimeUnit unit) { + return next().scheduleAtFixedRate(command, initialDelay, period, unit); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, + long delay, + TimeUnit unit) { + return next().scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public List> invokeAll( + Collection> tasks) throws InterruptedException { + return next().invokeAll(tasks); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + return next().invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + return next().invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return next().invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + next().execute(command); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxDefaultThreadFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxDefaultThreadFactory.java new file mode 100644 index 0000000..4a173a8 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxDefaultThreadFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.threads; + +import io.netty.util.concurrent.DefaultThreadFactory; + +public class RxDefaultThreadFactory extends DefaultThreadFactory { + + public RxDefaultThreadFactory(String threadNamePrefix) { + super(threadNamePrefix, true); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxEventLoopProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxEventLoopProvider.java new file mode 100644 index 0000000..9490264 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxEventLoopProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.threads; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; + +/** + * A provider for netty's {@link EventLoopGroup} to be used for RxNetty's clients and servers when they are not + * provided explicitly. + */ +public abstract class RxEventLoopProvider { + + /** + * The {@link EventLoopGroup} to be used by all client instances if it is not explicitly provided in the client. + * + * @return The {@link EventLoopGroup} to be used for all clients. + */ + public abstract EventLoopGroup globalClientEventLoop(); + + /** + * The {@link EventLoopGroup} to be used by all server instances if it is not explicitly provided. + * + * @return The {@link EventLoopGroup} to be used for all servers. + */ + public abstract EventLoopGroup globalServerEventLoop(); + + /** + * The {@link EventLoopGroup} to be used by all server instances as a parent eventloop group + * (First argument to this method: {@link ServerBootstrap#group(EventLoopGroup, EventLoopGroup)}), + * if it is not explicitly provided. + * + * @return The {@link EventLoopGroup} to be used for all servers. + */ + public abstract EventLoopGroup globalServerParentEventLoop(); + + /** + * The {@link EventLoopGroup} to be used by all client instances if it is not explicitly provided. + * + * @param nativeTransport {@code true} If the eventloop for native transport is to be returned (if configured) + * + * @return The {@link EventLoopGroup} to be used for all client. If {@code nativeTransport} was {@code true} then + * return the {@link EventLoopGroup} for native transport. + */ + public abstract EventLoopGroup globalClientEventLoop(boolean nativeTransport); + + /** + * The {@link EventLoopGroup} to be used by all server instances if it is not explicitly provided. + * + * @param nativeTransport {@code true} If the eventloop for native transport is to be returned (if configured) + * + * @return The {@link EventLoopGroup} to be used for all servers. If {@code nativeTransport} was {@code true} then + * return the {@link EventLoopGroup} for native transport. + */ + public abstract EventLoopGroup globalServerEventLoop(boolean nativeTransport); + + /** + * The {@link EventLoopGroup} to be used by all server instances as a parent eventloop group + * (First argument to this method: {@link ServerBootstrap#group(EventLoopGroup, EventLoopGroup)}), + * if it is not explicitly provided. + * + * @param nativeTransport {@code true} If the eventloop for native transport is to be returned (if configured) + * + * @return The {@link EventLoopGroup} to be used for all servers. If {@code nativeTransport} was {@code true} then + * return the {@link EventLoopGroup} for native transport. + */ + public abstract EventLoopGroup globalServerParentEventLoop(boolean nativeTransport); +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaEventloopScheduler.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaEventloopScheduler.java new file mode 100644 index 0000000..98be1aa --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaEventloopScheduler.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.threads; + +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.Future; +import rx.Scheduler; +import rx.Subscription; +import rx.annotations.Beta; +import rx.functions.Action0; +import rx.internal.schedulers.EventLoopsScheduler; +import rx.internal.schedulers.ScheduledAction; +import rx.internal.util.SubscriptionList; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; + +import java.util.concurrent.TimeUnit; + +/** + * A scheduler that uses a provided {@link EventLoopGroup} instance to schedule tasks. This should typically be used as + * a computation scheduler or any other scheduler that do not schedule blocking tasks. + */ +@Beta +public class RxJavaEventloopScheduler extends Scheduler { + + private final EventLoopGroup eventLoopGroup; + + public RxJavaEventloopScheduler(EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + } + + @Override + public Worker createWorker() { + final EventLoop eventLoop = eventLoopGroup.next(); + return new EventloopWorker(eventLoop); + } + + /** + * This code is more or less copied from RxJava's {@link EventLoopsScheduler} worker code. + **/ + /*Visible for testing*/static class EventloopWorker extends Worker { + + /** + * Why are there two subscription holders? + * + * The serial subscriptions are used for non-delayed schedules which are always executed (and hence removed) + * in order. Since SubscriptionList holds the subs as a linked list, removals are optimal for serial removes. + * OTOH, delayed schedules are executed (and hence removed) out of order and hence a CompositeSubscription, + * that stores the subs in a hash structure is more optimal for removals. + */ + private final SubscriptionList serial; + private final CompositeSubscription timed; + private final SubscriptionList both; + private final EventLoop eventLoop; + + public EventloopWorker(EventLoop eventLoop) { + this.eventLoop = eventLoop; + serial = new SubscriptionList(); + timed = new CompositeSubscription(); + both = new SubscriptionList(serial, timed); + } + + @Override + public Subscription schedule(final Action0 action) { + return schedule(action, 0, TimeUnit.DAYS); + } + + @Override + public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit) { + + if (isUnsubscribed()) { + return Subscriptions.unsubscribed(); + } + + final ScheduledAction sa; + + if (delayTime <= 0) { + sa = new ScheduledAction(action, serial); + serial.add(sa); + } else { + sa = new ScheduledAction(action, timed); + timed.add(sa); + } + + final Future result = eventLoop.schedule(sa, delayTime, unit); + Subscription cancelFuture = Subscriptions.create(new Action0() { + @Override + public void call() { + result.cancel(false); + } + }); + sa.add(cancelFuture); /*An unsubscribe of the returned sub should cancel the future*/ + return sa; + } + + @Override + public void unsubscribe() { + both.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return both.isUnsubscribed(); + } + + public boolean hasScheduledSubscriptions() { + return serial.hasSubscriptions(); + } + + public boolean hasDelayScheduledSubscriptions() { + return timed.hasSubscriptions(); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaNettyBasedSchedulersHook.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaNettyBasedSchedulersHook.java new file mode 100644 index 0000000..36a533d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/RxJavaNettyBasedSchedulersHook.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.threads; + +import io.netty.channel.EventLoopGroup; +import rx.Scheduler; +import rx.annotations.Beta; +import rx.plugins.RxJavaSchedulersHook; +import rx.schedulers.Schedulers; + +/** + * A scheduler hook for RxJava, to override the computation scheduler, as retrieved via {@link Schedulers#computation()}, + * with a scheduler to use netty's {@link EventLoopGroup}. The computation scheduler implementation is as provided as an + * {@link RxJavaEventloopScheduler} instance.

+ * + * This is to be used as

+ {@code + RxJavaPlugins.getInstance().registerSchedulersHook(hook); + } + at startup. + */ +@Beta +public class RxJavaNettyBasedSchedulersHook extends RxJavaSchedulersHook { + + private final RxJavaEventloopScheduler computationScheduler; + + public RxJavaNettyBasedSchedulersHook(RxJavaEventloopScheduler computationScheduler) { + this.computationScheduler = computationScheduler; + } + + @Override + public Scheduler getComputationScheduler() { + return computationScheduler; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/threads/SingleNioLoopProvider.java b/netty-http-rx/src/main/java/io/reactivex/netty/threads/SingleNioLoopProvider.java new file mode 100644 index 0000000..55d7e1f --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/threads/SingleNioLoopProvider.java @@ -0,0 +1,147 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.threads; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.reactivex.netty.RxNetty; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * An implementation of {@link RxEventLoopProvider} that returns the same {@link EventLoopGroup} instance for both + * client and server. + */ +public class SingleNioLoopProvider extends RxEventLoopProvider { + + private final EventLoopGroup eventLoop; + private final EventLoopGroup clientEventLoop; + private final EventLoopGroup parentEventLoop; + private final AtomicReference nativeEventLoop; + private final AtomicReference nativeClientEventLoop; + private final AtomicReference nativeParentEventLoop; + private final int parentEventLoopCount; + private final int childEventLoopCount; + + public SingleNioLoopProvider() { + this(Runtime.getRuntime().availableProcessors()); + } + + public SingleNioLoopProvider(int threadCount) { + eventLoop = new NioEventLoopGroup(threadCount, new RxDefaultThreadFactory("rxnetty-nio-eventloop")); + clientEventLoop = new PreferCurrentEventLoopGroup(eventLoop); + parentEventLoop = eventLoop; + parentEventLoopCount = childEventLoopCount = threadCount; + nativeEventLoop = new AtomicReference<>(); + nativeClientEventLoop = new AtomicReference<>(); + nativeParentEventLoop = nativeEventLoop; + } + + public SingleNioLoopProvider(int parentEventLoopCount, int childEventLoopCount) { + this.parentEventLoopCount = parentEventLoopCount; + this.childEventLoopCount = childEventLoopCount; + parentEventLoop = new NioEventLoopGroup(parentEventLoopCount, + new RxDefaultThreadFactory("rxnetty-nio-eventloop")); + eventLoop = new NioEventLoopGroup(childEventLoopCount, new RxDefaultThreadFactory("rxnetty-nio-eventloop")); + clientEventLoop = new PreferCurrentEventLoopGroup(eventLoop); + nativeParentEventLoop = new AtomicReference<>(); + nativeEventLoop = new AtomicReference<>(); + nativeClientEventLoop = new AtomicReference<>(); + } + + @Override + public EventLoopGroup globalClientEventLoop() { + return clientEventLoop; + } + + @Override + public EventLoopGroup globalServerEventLoop() { + return eventLoop; + } + + @Override + public EventLoopGroup globalServerParentEventLoop() { + return parentEventLoop; + } + + @Override + public EventLoopGroup globalClientEventLoop(boolean nativeTransport) { + if (nativeTransport && RxNetty.isUsingNativeTransport()) { + return getNativeClientEventLoop(); + } + return globalClientEventLoop(); + } + + @Override + public EventLoopGroup globalServerEventLoop(boolean nativeTransport) { + if (nativeTransport && RxNetty.isUsingNativeTransport()) { + return getNativeEventLoop(); + } + return globalServerEventLoop(); + } + + @Override + public EventLoopGroup globalServerParentEventLoop(boolean nativeTransport) { + if (nativeTransport && RxNetty.isUsingNativeTransport()) { + return getNativeParentEventLoop(); + } + return globalServerParentEventLoop(); + } + + private EventLoopGroup getNativeParentEventLoop() { + if (nativeParentEventLoop == nativeEventLoop) { // Means using same event loop for acceptor and worker pool. + return getNativeEventLoop(); + } + + EventLoopGroup eventLoopGroup = nativeParentEventLoop.get(); + if (null == eventLoopGroup) { + EventLoopGroup newEventLoopGroup = new EpollEventLoopGroup(parentEventLoopCount, + new RxDefaultThreadFactory( "rxnetty-epoll-eventloop")); + if (!nativeParentEventLoop.compareAndSet(null, newEventLoopGroup)) { + newEventLoopGroup.shutdownGracefully(); + } + } + return nativeParentEventLoop.get(); + } + + private EventLoopGroup getNativeEventLoop() { + EventLoopGroup eventLoopGroup = nativeEventLoop.get(); + if (null == eventLoopGroup) { + EventLoopGroup newEventLoopGroup = new EpollEventLoopGroup(childEventLoopCount, + new RxDefaultThreadFactory( "rxnetty-epoll-eventloop")); + if (!nativeEventLoop.compareAndSet(null, newEventLoopGroup)) { + newEventLoopGroup.shutdownGracefully(); + } + } + return nativeEventLoop.get(); + } + + private EventLoopGroup getNativeClientEventLoop() { + EventLoopGroup eventLoopGroup = nativeClientEventLoop.get(); + if (null == eventLoopGroup) { + EventLoopGroup newEventLoopGroup = new EpollEventLoopGroup(childEventLoopCount, + new RxDefaultThreadFactory( "rxnetty-epoll-eventloop")); + newEventLoopGroup = new PreferCurrentEventLoopGroup(newEventLoopGroup); + if (!nativeClientEventLoop.compareAndSet(null, newEventLoopGroup)) { + newEventLoopGroup.shutdownGracefully(); + } + } + return nativeClientEventLoop.get(); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/util/CollectBytes.java b/netty-http-rx/src/main/java/io/reactivex/netty/util/CollectBytes.java new file mode 100644 index 0000000..ee2fb22 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/util/CollectBytes.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import rx.Observable; +import rx.Observable.Transformer; +import rx.exceptions.OnErrorThrowable; +import rx.functions.Action2; +import rx.functions.Func0; + +/** + * An {@link Observable.Transformer} to collect a stream of {@link ByteBuf ByteBufs} into a single + * ByteBuf. On success the receiver must release the returned ByteBuf. + * On failure this will release all received ByteBufs. + *

+ * This Transformer should not be used with {@link io.reactivex.netty.channel.ContentSource#autoRelease()} + * as this will release the underlying collected ByteBufs before the collection is complete. + */ +public class CollectBytes implements Transformer { + + private final int maxBytes; + + /** + * Collect all emitted ByteBufs into a single ByteBuf. This will return at most + * {@link Integer#MAX_VALUE} + * bytes. This is the upper limit of {@link ByteBuf#readableBytes()}. If more than + * Integer#MAX_VALUE bytes are received a {@link TooMuchDataException} will be emitted. + * {@link TooMuchDataException#getCause()} + * will contain an + * {@link OnErrorThrowable.OnNextValue} with the bytes accumulated before the exception + * was thrown. + */ + public static CollectBytes all() { + return upTo(Integer.MAX_VALUE); + } + + /** + * Collect all emitted ByteBufs into a single ByteBuf until maxBytes have + * been collected. If more than maxBytes are received this will unsubscribe from + * the upstream Observable and will emit a + * {@link TooMuchDataException}. {@link TooMuchDataException#getCause()} + * will contain an + * {@link OnErrorThrowable.OnNextValue} with the bytes accumulated before the exception + * was thrown. + * @param maxBytes the maximum number of bytes to read + * @throws IllegalArgumentException when maxBytes is negative + */ + public static CollectBytes upTo(int maxBytes) { + return new CollectBytes(maxBytes); + } + + private CollectBytes(int maxBytes) { + if (maxBytes < 0) { + throw new IllegalArgumentException("maxBytes must not be negative"); + } + this.maxBytes = maxBytes; + } + + @Override + public Observable call(Observable upstream) { + return upstream + .collect( + new Func0() { + @Override + public CompositeByteBuf call() { + return Unpooled.compositeBuffer(); + } + }, + new Action2() { + @Override + public void call(CompositeByteBuf collector, ByteBuf buf) { + long newLength = collector.readableBytes() + buf.readableBytes(); + if (newLength <= maxBytes) { + collector.addComponent(true, buf); + } else { + collector.release(); + buf.release(); + throw new TooMuchDataException("More than " + maxBytes + "B received"); + } + } + } + ) + .cast(ByteBuf.class); + } + + public static class TooMuchDataException extends RuntimeException { + public TooMuchDataException(String message) { + super(message); + } + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/util/LineReader.java b/netty-http-rx/src/main/java/io/reactivex/netty/util/LineReader.java new file mode 100644 index 0000000..0819e3d --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/util/LineReader.java @@ -0,0 +1,135 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; +import io.netty.util.ByteProcessor; + +import java.nio.charset.Charset; +import java.util.List; + +/** + * A utility class to be used with a channel handler to parse {@code ByteBuffer}s that contain strings terminated with + * a new line. This reader supports non-blocking incremental reads split across random places in the line.

+ * + * This is not thread-safe and must only be called from a {@link ChannelHandler}. + */ +public class LineReader { + + public static final int DEFAULT_INITIAL_CAPACITY = 256; + + private static final ByteProcessor LINE_END_FINDER = new ByteProcessor() { + public static final char LF = 10; + + @Override + public boolean process(byte value) throws Exception { + char nextByte = (char) value; + return LF != nextByte; + } + }; + private ByteBuf incompleteBuffer; + private final int maxLineLength; + private final Charset encoding; + + public LineReader() { + this(Integer.MAX_VALUE, Charset.defaultCharset()); + } + + public LineReader(int maxLineLength, Charset encoding) { + this.maxLineLength = maxLineLength; + this.encoding = encoding; + } + + /** + * Reads the {@code in} buffer as much as it can and adds all the read lines into the {@code out} list.

+ * If there is any outstanding data that is not read, it is stored into a temporary buffer and the next decode will + * prepend this data to the newly read data.

+ * + * {@link #dispose()} must be called when the associated {@link ChannelHandler} is removed from the pipeline. + * + * @param in Buffer to decode. + * @param out List to add the read lines to. + * @param allocator Allocator to allocate new buffers, if required. + */ + public void decode(ByteBuf in, List out, ByteBufAllocator allocator) { + while (in.isReadable()) { + final int startIndex = in.readerIndex(); + + int lastReadIndex = in.forEachByte(LINE_END_FINDER); + + if (-1 == lastReadIndex) { + // Buffer end without line termination + if (null == incompleteBuffer) { + incompleteBuffer = allocator.buffer(DEFAULT_INITIAL_CAPACITY, maxLineLength); + } + + /*Add to the incomplete buffer*/ + incompleteBuffer.ensureWritable(in.readableBytes()); + incompleteBuffer.writeBytes(in); + } else { + ByteBuf lineBuf = in.readSlice(lastReadIndex - startIndex); + String line; + if (null != incompleteBuffer) { + line = incompleteBuffer.toString(encoding) + lineBuf.toString(encoding); + incompleteBuffer.release(); + incompleteBuffer = null; + } else { + line = lineBuf.toString(encoding); + } + out.add(line); + in.skipBytes(1); // Skip new line character. + } + } + } + + /** + * Same as {@link #decode(ByteBuf, List, ByteBufAllocator)} but it also produces the left-over buffer, even in + * absence of a line termination. + * + * {@link #dispose()} must be called when the associated {@link ChannelHandler} is removed from the pipeline. + * + * @param in Buffer to decode. + * @param out List to add the read lines to. + * @param allocator Allocator to allocate new buffers, if required. + */ + public void decodeLast(ByteBuf in, List out, ByteBufAllocator allocator) { + decode(in, out, allocator); + if (null != incompleteBuffer && incompleteBuffer.isReadable()) { + out.add(incompleteBuffer.toString(encoding)); + } + } + + /** + * Disposes any half-read data buffers. + */ + public void dispose() { + if (null != incompleteBuffer) { + incompleteBuffer.release(); + } + } + + public static boolean isLineDelimiter(char c) { + return c == '\r' || c == '\n'; + } + + /*Visible for testing*/ ByteBuf getIncompleteBuffer() { + return incompleteBuffer; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/util/LoggingHandlerFactory.java b/netty-http-rx/src/main/java/io/reactivex/netty/util/LoggingHandlerFactory.java new file mode 100644 index 0000000..946a916 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/util/LoggingHandlerFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.util; + +import io.netty.channel.ChannelHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import rx.functions.Func0; + +import java.util.EnumMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link LoggingHandler} is a shaerable handler and hence need not be created for all channels. This factory + * manages a static map of log level to instance which can be used directly instead of creating a new factory per + * client. + */ +public class LoggingHandlerFactory implements Func0 { + + private static final ConcurrentMap> factories = + new ConcurrentHashMap<>(); + + private final LoggingHandler loggingHandler; + + private LoggingHandlerFactory(String name, LogLevel wireLoggingLevel) { + loggingHandler = new LoggingHandler(name, wireLoggingLevel); + } + + public static LoggingHandler get(String name, LogLevel logLevel) { + return getFactory(name, logLevel).loggingHandler; + } + + public static LoggingHandlerFactory getFactory(String name, LogLevel logLevel) { + EnumMap f = factories.get(name); + if (null == f) { + f = newEnumMap(name); + EnumMap existing = factories.putIfAbsent(name, f); + if (null != existing) { + f = existing; + } + } + return f.get(logLevel); + } + + @Override + public ChannelHandler call() { + return loggingHandler;/*logging handler is shareable.*/ + } + + private static EnumMap newEnumMap(String name) { + EnumMap toReturn = new EnumMap<>(LogLevel.class); + for (LogLevel logLevel : LogLevel.values()) { + toReturn.put(logLevel, new LoggingHandlerFactory(name, logLevel)); + } + return toReturn; + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/util/StringLineDecoder.java b/netty-http-rx/src/main/java/io/reactivex/netty/util/StringLineDecoder.java new file mode 100644 index 0000000..9f9f2b6 --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/util/StringLineDecoder.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +/** + * A decoder that breaks an incoming {@link ByteBuf}s into a list of strings delimited by a new line. + */ +public class StringLineDecoder extends ByteToMessageDecoder { + + private final LineReader lineReader = new LineReader(); + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + lineReader.dispose(); + super.handlerRemoved0(ctx); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + lineReader.decode(in, out, ctx.alloc()); + } +} diff --git a/netty-http-rx/src/main/java/io/reactivex/netty/util/UnicastBufferingSubject.java b/netty-http-rx/src/main/java/io/reactivex/netty/util/UnicastBufferingSubject.java new file mode 100644 index 0000000..19f5afa --- /dev/null +++ b/netty-http-rx/src/main/java/io/reactivex/netty/util/UnicastBufferingSubject.java @@ -0,0 +1,286 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.util; + +import rx.Subscriber; +import rx.annotations.Experimental; +import rx.exceptions.Exceptions; +import rx.exceptions.MissingBackpressureException; +import rx.functions.Action0; +import rx.internal.util.BackpressureDrainManager; +import rx.internal.util.BackpressureDrainManager.BackpressureQueueCallback; +import rx.subjects.Subject; +import rx.subscriptions.Subscriptions; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +/** + * An {@code Observable} that only supports a single active subscriber and buffers messages on back pressure and when + * there is no active subscription. + * + *

Add new messages

+ * + * New messages can be added to this subject via + *
    +
  • {@link #onNext(Object)}: This throws an error if there is a buffer overflow.
  • +
  • {@link #offerNext(Object)}: This returns {@code false} if there is a buffer overflow.
  • +
+ * + *

Backpressure

+ * + * This subject supports backpressure from the only concurrent subscriber it can have at any time. The buffer limits + * that are specified while creating the subject is the maximum buffer that is allowed during backpressure. + * + * @param The type of objects accepted by this subject. + */ +@Experimental +public class UnicastBufferingSubject extends Subject { + + private final State state; + + protected UnicastBufferingSubject(OnSubscribe onSubscribe, State state) { + super(onSubscribe); + this.state = state; + } + + public static UnicastBufferingSubject create(long bufferSize) { + final State state = new State<>(bufferSize); + return new UnicastBufferingSubject<>(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + state.registerSubscriber(subscriber); + } + }, state); + } + + public boolean isTerminated() { + synchronized (state) { + if (null != state.producer) { + return state.producer.isTerminated(); + } else { + return state.terminatedBeforeSubscribe; + } + } + } + + @Override + public boolean hasObservers() { + return null != state.subscriber; + } + + @Override + public void onCompleted() { + BackpressureDrainManager p = null; + /* If a subscriber is active, send the completion to the subscriber, else store it to be delivered to the + * buffered producer post subscription.*/ + synchronized (state) { + if (null != state.producer) { + p = state.producer; + } else { + state.terminatedBeforeSubscribe = true; + state.errorBeforeSubscribe = null; + } + } + + /*Send callbacks outside the sync block*/ + if (null != p) { + p.terminateAndDrain(); + } + } + + @Override + public void onError(Throwable e) { + BackpressureDrainManager p = null; + /* If a subscriber is active, send the completion to the subscriber, else store it to be delivered to the + * buffered producer post subscription.*/ + synchronized (state) { + if (null != state.producer) { + p = state.producer; + } else { + state.terminatedBeforeSubscribe = true; + state.errorBeforeSubscribe = e; + } + } + + /*Send callbacks outside the sync block*/ + if (null != p) { + p.terminateAndDrain(e); + } + } + + @Override + public void onNext(T t) { + try { + addNext(t); + } catch (MissingBackpressureException e) { + throw Exceptions.propagate(e); + } + } + + private void addNext(T next) throws MissingBackpressureException { + if (isTerminated()) { + throw new IllegalStateException("Observable is already completed."); + } + + /*Check for overflow*/ + while (true) { + final long currentSize = state.currentSize.get(); + final long newSize = currentSize + 1; + if (newSize > state.maxBufferedCount) { + throw new MissingBackpressureException("Max buffer limit exceeded. Current size: " + currentSize); + } + + if (state.currentSize.compareAndSet(currentSize, newSize)) { + break; + } + } + + state.nexts.add(next); + + BackpressureDrainManager p = null; + /*Drain the producer, if a subscriber is active.*/ + synchronized (state) { + if (null != state.producer) { + p = state.producer; + } + } + if (null != p) { + p.drain(); + } + } + + /** + * Offers the passed item to this subject. Same as {@link #onNext(Object)} just that this method does not throw an + * exception in case of buffer overflow, instead returns a {@code false}. + * + * @param next Next item to offer. + * + * @return {@code true} if the item was accepted, {@code false} if the subject is already terminated or the buffer + * is full. + */ + public boolean offerNext(T next) { + try { + addNext(next); + return true; + } catch (MissingBackpressureException e) { + return false; + } + } + + private static final class State { + + private final ConcurrentLinkedQueue nexts; + + private final BackpressureQueueCallbackImpl queueCallback; + private final AtomicLong currentSize = new AtomicLong(); + + private final long maxBufferedCount; + + private volatile Subscriber subscriber; + private volatile BackpressureDrainManager producer; + private volatile Throwable errorBeforeSubscribe; + private volatile boolean terminatedBeforeSubscribe; + + private State(long maxBufferedCount) { + this.maxBufferedCount = maxBufferedCount; + nexts = new ConcurrentLinkedQueue<>(); + queueCallback = new BackpressureQueueCallbackImpl(); + } + + public void registerSubscriber(final Subscriber subscriber) { + + boolean _shdSubscribe = false; + boolean _terminated = false; + Throwable _terminalError = null; + BackpressureDrainManager p = null; + synchronized (this) { + if (null == this.subscriber) { + this.subscriber = subscriber; + _shdSubscribe = true; + _terminated = terminatedBeforeSubscribe; + _terminalError = errorBeforeSubscribe; + p = new BackpressureDrainManager(queueCallback); + producer = p; + } + } + + if (_shdSubscribe) { + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + synchronized (State.this) { + State.this.subscriber = null; + State.this.producer = null; + /** + * Why not clear the terminate-before-subscribe state? + * It can be so that there are multiple subscribers and the first subscriber did not + * completely consume the events and hence on unsubscribe clears the terminal state. + * The new subscriber will never get the terminal state in this case. + */ + } + } + })); + subscriber.setProducer(p); + if (_terminated) { + p.terminateAndDrain(_terminalError); + } + } else { + subscriber.onError(new IllegalStateException("Only one subscriber is allowed.")); + } + } + + /** + * Shared {@link BackpressureQueueCallback} for all producers (subscribers) as there is no state in this + * callback. + */ + private class BackpressureQueueCallbackImpl implements BackpressureQueueCallback { + + @Override + public Object peek() { + return nexts.peek(); + } + + @Override + public Object poll() { + T poll = nexts.poll(); + if (null != poll) { + currentSize.decrementAndGet(); + } + return poll; + } + + @Override + public boolean accept(Object next) { + @SuppressWarnings("unchecked") + T t = (T) next; + subscriber.onNext(t); + return false; + } + + @Override + public void complete(Throwable exception) { + if (null == exception) { + subscriber.onCompleted(); + } else { + subscriber.onError(exception); + } + } + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridgeTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridgeTest.java new file mode 100644 index 0000000..e0d0628 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/AbstractConnectionToChannelBridgeTest.java @@ -0,0 +1,310 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.channel.BackpressureManagingHandler.RequestReadIfRequiredEvent; +import io.reactivex.netty.channel.events.ConnectionEventListener; +import io.reactivex.netty.test.util.MockEventPublisher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.observers.TestSubscriber; + +import static io.reactivex.netty.test.util.MockEventPublisher.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class AbstractConnectionToChannelBridgeTest { + + @Rule + public final ConnectionHandlerRule connectionHandlerRule = new ConnectionHandlerRule(); + + @Test(timeout = 60000) + public void testChannelActive() throws Exception { + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(false); + + connectionHandlerRule.activateConnectionAndAssert(subscriber); + + assertThat("Duplicate channel active event sent a notification", subscriber.getOnNextEvents(), hasSize(1)); + connectionHandlerRule.handler.channelActive(connectionHandlerRule.ctx); // duplicate event should not trigger onNext. + /*One item from activation*/ + assertThat("Duplicate channel active event sent a notification", subscriber.getOnNextEvents(), hasSize(1)); + } + + @Test(timeout = 60000) + public void testEagerContentSubscriptionFail() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(true); // should mandate eager content subscription + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(false); + + connectionHandlerRule.activateConnectionAndAssert(subscriber); + + ConnectionInputSubscriber inputSubscriber = connectionHandlerRule.enableConnectionInputSubscriber(); + + subscriber.assertTerminalEvent(); + assertThat("Unexpected first notification kind.", inputSubscriber.getOnErrorEvents(), hasSize(1)); + + } + + @Test(timeout = 60000) + public void testEagerContentSubscriptionPass() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(true); // should mandate eager content subscription + + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + + connectionHandlerRule.activateConnectionAndAssert(subscriber); // eagerly subscribes to input. + ConnectionInputSubscriber inputSubscriber = subscriber.getInputSubscriber(); + + assertThat("Unexpected notifications count after channel active.", inputSubscriber.getOnNextEvents(), + hasSize(0)); + inputSubscriber.assertNoErrors(); + assertThat("Input subscriber is unsubscribed.", inputSubscriber.isUnsubscribed(), is(false)); + } + + @Test(timeout = 60000) + public void testLazyContentSubscription() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(false); + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(false); //lazy input sub. + connectionHandlerRule.activateConnectionAndAssert(subscriber); + ConnectionInputSubscriber inputSubscriber = connectionHandlerRule.enableConnectionInputSubscriber(); + + inputSubscriber.assertNoErrors(); + assertThat("Unexpected on next events after channel active.", inputSubscriber.getOnNextEvents(), + hasSize(0)); + assertThat("Unexpected on completed events after channel active.", inputSubscriber.getOnCompletedEvents(), + hasSize(0)); + assertThat("Input subscriber is unsubscribed.", inputSubscriber.isUnsubscribed(), is(false)); + + connectionHandlerRule.startRead(); + connectionHandlerRule.testSendInputMsgs(inputSubscriber, "hello1"); + } + + @Test(timeout = 60000) + public void testInputCompleteOnChannelUnregister() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(false); + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + connectionHandlerRule.activateConnectionAndAssert(subscriber); + ConnectionInputSubscriber inputSubscriber = subscriber.getInputSubscriber(); // since sub is eager. + connectionHandlerRule.startRead(); + connectionHandlerRule.testSendInputMsgs(inputSubscriber, "hello1"); + + + assertThat("Unexpected notifications count after channel active.", inputSubscriber.getOnNextEvents(), + hasSize(1)); + inputSubscriber.unsubscribe(); // else channel close will generate error if subscribed + connectionHandlerRule.handler.channelUnregistered(connectionHandlerRule.ctx); + inputSubscriber.assertNoErrors(); + assertThat("Unexpected notifications count after channel active.", inputSubscriber.getOnNextEvents(), + hasSize(1)); + } + + @Test(timeout = 60000) + public void testMultipleInputSubscriptions() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(false); + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + connectionHandlerRule.activateConnectionAndAssert(subscriber); // one subscription + + ConnectionInputSubscriber inputSubscriber = connectionHandlerRule.enableConnectionInputSubscriber(); + + inputSubscriber.assertTerminalEvent(); + + assertThat("Unexpected on next events for second subscriber.", inputSubscriber.getOnNextEvents(), hasSize(0)); + assertThat("Unexpected notification type for second subscriber.", inputSubscriber.getOnErrorEvents(), + hasSize(1)); + } + + @Test(timeout = 60000) + public void testInputSubscriptionReset() throws Exception { + connectionHandlerRule.channel.config().setAutoRead(false); + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + connectionHandlerRule.activateConnectionAndAssert(subscriber); // one subscription + + ConnectionInputSubscriber inputSubscriber = connectionHandlerRule.enableConnectionInputSubscriber(); + inputSubscriber.assertTerminalEvent(); + assertThat("Unexpected on next events for second subscriber.", inputSubscriber.getOnNextEvents(), hasSize(0)); + + connectionHandlerRule.handler.userEventTriggered(connectionHandlerRule.ctx, + new ConnectionInputSubscriberResetEvent() { + }); + + inputSubscriber = connectionHandlerRule.enableConnectionInputSubscriber(); + assertThat("Unexpected on next count for input subscriber post reset.", inputSubscriber.getOnNextEvents(), + hasSize(0)); + assertThat("Unexpected on error count for input subscriber post reset.", inputSubscriber.getOnErrorEvents(), + hasSize(0)); + assertThat("Unexpected on completed count for input subscriber post reset.", + inputSubscriber.getOnCompletedEvents(), hasSize(0)); + } + + @Test(timeout = 60000) + public void testErrorBeforeConnectionActive() throws Exception { + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + final NullPointerException exception = new NullPointerException(); + connectionHandlerRule.handler.exceptionCaught(connectionHandlerRule.ctx, exception); + + subscriber.assertTerminalEvent(); + + assertThat("Unexpected on next notifications count post exception.", subscriber.getOnNextEvents(), hasSize(0)); + assertThat("Unexpected notification type post exception.", subscriber.getOnErrorEvents(), hasSize(1)); + } + + @Test(timeout = 60000) + public void testErrorPostInputSubscribe() throws Exception { + ConnectionSubscriber subscriber = connectionHandlerRule.enableConnectionSubscriberAndAssert(true); + connectionHandlerRule.activateConnectionAndAssert(subscriber); + ConnectionInputSubscriber inputSubscriber = subscriber.getInputSubscriber(); // since sub is eager. + + assertThat("Unexpected on next notifications count pre exception.", inputSubscriber.getOnNextEvents(), hasSize(0)); + assertThat("Unexpected on error notifications count pre exception.", inputSubscriber.getOnErrorEvents(), hasSize(0)); + assertThat("Unexpected on completed notifications count pre exception.", inputSubscriber.getOnCompletedEvents(), hasSize(0)); + final NullPointerException exception = new NullPointerException(); + connectionHandlerRule.handler.exceptionCaught(connectionHandlerRule.ctx, exception); + + inputSubscriber.assertTerminalEvent(); + + assertThat("Unexpected on next notifications count post exception.", inputSubscriber.getOnNextEvents(), hasSize(0)); + assertThat("Unexpected on error notifications count post exception.", inputSubscriber.getOnErrorEvents(), + hasSize(1)); + } + + public static class ConnectionHandlerRule extends ExternalResource { + + private Channel channel; + private ChannelHandlerContext ctx; + private AbstractConnectionToChannelBridge handler; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + channel = new EmbeddedChannel(new ChannelDuplexHandler()); + ctx = channel.pipeline().firstContext(); + handler = new AbstractConnectionToChannelBridge("foo", + new ConnectionEventListener() { }, + disabled()) { }; + base.evaluate(); + } + }; + } + + public void startRead() throws Exception { + handler.userEventTriggered(ctx, new RequestReadIfRequiredEvent() { + @Override + protected boolean shouldReadMore(ChannelHandlerContext ctx) { + return true; + } + }); + } + + public ConnectionSubscriber enableConnectionSubscriberAndAssert(boolean eagerSubToInput) throws Exception { + ConnectionSubscriber toReturn = new ConnectionSubscriber(eagerSubToInput, this); + handler.userEventTriggered(ctx, new ChannelSubscriberEvent<>(toReturn)); + assertThat("Unexpected on next notifications count before channel active.", toReturn.getOnNextEvents(), + hasSize(0)); + assertThat("Unexpected on error notifications count before channel active.", toReturn.getOnErrorEvents(), + hasSize(0)); + assertThat("Unexpected on complete notifications count before channel active.", toReturn.getOnCompletedEvents(), hasSize(0)); + return toReturn; + } + + public ConnectionInputSubscriber enableConnectionInputSubscriber() + throws Exception { + ConnectionInputSubscriber toReturn = new ConnectionInputSubscriber(); + handler.userEventTriggered(ctx, new ConnectionInputSubscriberEvent<>(toReturn)); + return toReturn; + } + + public void activateConnectionAndAssert(ConnectionSubscriber subscriber) throws Exception { + handler.userEventTriggered(ctx, EmitConnectionEvent.INSTANCE); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("No connections received.", subscriber.getOnNextEvents(), is(not(empty()))); + assertThat("Unexpected channel in new connection.", subscriber.getOnNextEvents().get(0), + is(channel)); + + } + + public void testSendInputMsgs(ConnectionInputSubscriber inputSubscriber, String... msgs) throws Exception { + + for (String msg: msgs) { + handler.channelRead(ctx, msg); + } + + assertThat("Unexpected notifications count after read.", inputSubscriber.getOnNextEvents(), + hasSize(msgs.length)); + assertThat("Unexpected notifications count after read.", inputSubscriber.getOnNextEvents(), + contains(msgs)); + + assertThat("Input subscriber is unsubscribed after read.", inputSubscriber.isUnsubscribed(), + is(false)); + } + } + + public static class ConnectionSubscriber extends TestSubscriber { + + private final boolean subscribeToInput; + private final ConnectionHandlerRule rule; + private ConnectionInputSubscriber inputSubscriber; + + public ConnectionSubscriber(boolean subscribeToInput, ConnectionHandlerRule rule) { + this.subscribeToInput = subscribeToInput; + this.rule = rule; + } + + @Override + public void onNext(Channel channel) { + super.onNext(channel); + try { + if (subscribeToInput) { + inputSubscriber = rule.enableConnectionInputSubscriber(); + } + } catch (Exception e) { + onError(e); + } + } + + public ConnectionInputSubscriber getInputSubscriber() { + return inputSubscriber; + } + } + + public static class ConnectionInputSubscriber extends TestSubscriber { + + private final long requestAtStart; + + public ConnectionInputSubscriber() { + this(Long.MAX_VALUE); + } + + public ConnectionInputSubscriber(long requestAtStart) { + this.requestAtStart = requestAtStart; + } + + @Override + public void onStart() { + request(requestAtStart); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/BackpressureManagingHandlerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/BackpressureManagingHandlerTest.java new file mode 100644 index 0000000..d750469 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/BackpressureManagingHandlerTest.java @@ -0,0 +1,458 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.channel.BackpressureManagingHandler.BytesWriteInterceptor; +import io.reactivex.netty.channel.BackpressureManagingHandler.RequestReadIfRequiredEvent; +import io.reactivex.netty.channel.BackpressureManagingHandler.State; +import io.reactivex.netty.test.util.InboundRequestFeeder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import rx.Observable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class BackpressureManagingHandlerTest { + + @Rule + public final HandlerRule handlerRule = new HandlerRule(); + + @Test(timeout = 60000) + public void testExactDemandAndSupply() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + handlerRule.feedMessagesForRead(msg1, msg2); /*Exact supply*/ + + handlerRule.setMaxMessagesPerRead(2); /*Send all msgs in one iteration*/ + handlerRule.requestMessages(2); /*Exact demand*/ + + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand is met (requested 2 and got 2) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + } + + @Test(timeout = 60000) + public void testExactDemandAndSupplyMultiRequests() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + handlerRule.feedMessagesForRead(msg1, msg2); /*Exact supply*/ + + handlerRule.setMaxMessagesPerRead(2); /*Send all msgs in one iteration*/ + handlerRule.requestMessages(2); /*Exact demand*/ + + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand is met (requested 2 and got 2) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + handlerRule.resetReadCount(); + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + + handlerRule.handler.reset(); + + final String msg3 = "hello3"; + handlerRule.feedMessagesForRead(msg3); + + /*No demand, no read fired*/ + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + handlerRule.assertMessagesReceived(); + + handlerRule.requestMessages(1); + + /*Read on demand*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg3); + + /*Since, the demand is met (requested 3 and got 3) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Reading)); + } + + @Test(timeout = 60000) + public void testMoreDemand() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + handlerRule.feedMessagesForRead(msg1, msg2); /*less supply*/ + + handlerRule.setMaxMessagesPerRead(2); /*Send all msgs in one iteration*/ + handlerRule.requestMessages(4); /*More demand*/ + + /*One read for start and one when the supply completed but demand exists.*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(2)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand is not met (requested 4 but got 2) , stay in read requested.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.ReadRequested)); + } + + @Test(timeout = 60000) + public void testMoreSupply() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + final String msg3 = "hello3"; + handlerRule.feedMessagesForRead(msg1, msg2, msg3); /*more supply*/ + + handlerRule.setMaxMessagesPerRead(3); /*Send all msgs in one iteration*/ + handlerRule.requestMessages(2); /*less demand*/ + + /*One read for start.*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand was met (requested 2 and got 2) , but the supply was more (3), we should be buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(1)); + assertThat("Unexpected buffer contents.", handlerRule.handler.getBuffer(), contains((Object) msg3)); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + } + + @Test(timeout = 60000) + public void testBufferDrainSingleIteration() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + final String msg3 = "hello3"; + handlerRule.feedMessagesForRead(msg1, msg2, msg3); /*more supply*/ + + handlerRule.setMaxMessagesPerRead(3); /*Send all msgs in one iteration & cause buffer*/ + handlerRule.requestMessages(2); /*less demand*/ + + /*One read for start.*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand was met (requested 2 and got 2) , but the supply was more (3), we should be buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(1)); + assertThat("Unexpected buffer contents.", handlerRule.handler.getBuffer(), contains((Object) msg3)); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + + handlerRule.resetReadCount(); + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + + handlerRule.handler.reset(); + + handlerRule.requestMessages(1); /*Should come from the buffer.*/ + + assertThat("Unexpected read requested when expected to be fed from buffer.", + handlerRule.getReadRequestedCount(), is(0)); + handlerRule.assertMessagesReceived(msg3); + + /*Since, the demand is now met (requested 3 and got 3) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), is(nullValue())); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + } + + @Test(timeout = 60000) + public void testBufferDrainMultiIteration() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + final String msg3 = "hello3"; + final String msg4 = "hello4"; + handlerRule.feedMessagesForRead(msg1, msg2, msg3, msg4); /*more supply*/ + + handlerRule.setMaxMessagesPerRead(4); /*Send all msgs in one iteration & cause buffer*/ + handlerRule.requestMessages(2); /*less demand*/ + + /*One read for start.*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand was met (requested 2 and got 2) , but the supply was more (4), we should be buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(2)); + assertThat("Unexpected buffer contents.", handlerRule.handler.getBuffer(), contains((Object) msg3, msg4)); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + + /*Reset read state before next read*/ + handlerRule.resetReadCount(); + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + handlerRule.handler.reset(); + + handlerRule.requestMessages(1); /*Should come from the buffer.*/ + + assertThat("Unexpected read requested when expected to be fed from buffer.", + handlerRule.getReadRequestedCount(), is(0)); + handlerRule.assertMessagesReceived(msg3); + + /*Since, the demand is now met (requested 3 and got 3) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + /*Buffer does not change till it has data*/ + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(2)); + /*Buffer reader index changes till it has data*/ + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(1)); + + /*Reset read state before next read*/ + handlerRule.resetReadCount(); + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + handlerRule.handler.reset(); + + handlerRule.requestMessages(1); /*Should come from the buffer.*/ + + assertThat("Unexpected read requested when expected to be fed from buffer.", + handlerRule.getReadRequestedCount(), is(0)); + handlerRule.assertMessagesReceived(msg4); + + /*Since, the demand is now met (requested 4 and got 4) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), is(nullValue())); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + } + + @Test(timeout = 60000) + public void testBufferDrainWithMoreDemand() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final String msg1 = "hello1"; + final String msg2 = "hello2"; + final String msg3 = "hello3"; + handlerRule.feedMessagesForRead(msg1, msg2, msg3); /*more supply*/ + + handlerRule.setMaxMessagesPerRead(3); /*Send all msgs in one iteration & cause buffer*/ + handlerRule.requestMessages(2); /*less demand*/ + + /*One read for start.*/ + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1, msg2); + + /*Since, the demand was met (requested 2 and got 2) , but the supply was more (3), we should be buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(1)); + assertThat("Unexpected buffer contents.", handlerRule.handler.getBuffer(), contains((Object) msg3)); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + + handlerRule.resetReadCount(); + assertThat("Unexpected read requested count post reset.", handlerRule.getReadRequestedCount(), is(0)); + + handlerRule.handler.reset(); + + handlerRule.requestMessages(2); /*Should come from the buffer.*/ + + /*Since demand can not be fulfilled by the buffer, a read should be requested.*/ + assertThat("Unexpected read requested.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg3); + + /*Since, the demand is now met (requested 3 and got 3) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.ReadRequested)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), is(nullValue())); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + } + + @Test(timeout = 60000) + public void testBufferDrainOnRemove() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + final ByteBuf msg1 = Unpooled.buffer().writeBytes("hello1".getBytes()); + final ByteBuf msg2 = Unpooled.buffer().writeBytes("hello2".getBytes()); + handlerRule.feedMessagesForRead(msg1, msg2); /*More supply then demand*/ + + handlerRule.setMaxMessagesPerRead(2); /*Send all msgs in one iteration and cause buffer*/ + handlerRule.requestMessages(1); /*Less demand*/ + + assertThat("Unexpected read requested count.", handlerRule.getReadRequestedCount(), is(1)); + handlerRule.assertMessagesReceived(msg1); + + /*Since, the demand is met (requested 1 and got 1) , we move to buffering.*/ + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), hasSize(1)); + assertThat("Unexpected buffer contents.", handlerRule.handler.getBuffer(), contains((Object) msg2)); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + + handlerRule.channel.close(); // Should remove handler. + handlerRule.channel.runPendingTasks(); + + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Stopped)); + assertThat("Unexpected buffer size.", handlerRule.handler.getBuffer(), is(nullValue())); + assertThat("Unexpected buffer read index.", handlerRule.handler.getCurrentBufferIndex(), is(0)); + assertThat("Buffered item not released.", msg2.refCnt(), is(0)); + } + + @Test(timeout = 60000) + public void testDiscardReadWhenStopped() throws Exception { + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Buffering)); + + handlerRule.channel.close(); // Should remove handler. + handlerRule.channel.runPendingTasks(); + + assertThat("Unexpected handler state.", handlerRule.handler.getCurrentState(), is(State.Stopped)); + + final ByteBuf msg = Unpooled.buffer().writeBytes("Hello".getBytes()); + handlerRule.handler.channelRead(Mockito.mock(ChannelHandlerContext.class), msg); + + assertThat("Message not released when stopped.", msg.refCnt(), is(0)); + } + + @Test(timeout = 60000) + public void testWriteWithBufferingHandler() throws Exception { + BufferingHandler bufferingHandler = new BufferingHandler(); + handlerRule.channel.pipeline() + .addBefore(BytesWriteInterceptor.WRITE_INSPECTOR_HANDLER_NAME, "buffering-handler", + bufferingHandler); + + final String[] dataToWrite = {"Hello1", "Hello2"}; + + handlerRule.channel.writeAndFlush(Observable.from(dataToWrite));/*Using Observable.from() to enable backpressure.*/ + + assertThat("Messages written to the channel, inspite of buffering", handlerRule.channel.outboundMessages(), + is(empty())); + + /*Inspite of the messages, not reaching the channel, the extra demand should be generated and the buffering + handler should contain all messages.*/ + assertThat("Unexpected buffer size in buffering handler.", bufferingHandler.buffer, hasSize(2)); + } + + public static class HandlerRule extends ExternalResource { + + private MockBackpressureManagingHandler handler; + private EmbeddedChannel channel; + private InboundRequestFeeder inboundRequestFeeder; + private FixedRecvByteBufAllocator recvByteBufAllocator; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + inboundRequestFeeder = new InboundRequestFeeder(); + channel = new EmbeddedChannel(new LoggingHandler()); + String bpName = "backpressure-manager"; + channel.pipeline().addFirst(bpName, + handler = new MockBackpressureManagingHandler(bpName)); + channel.pipeline().addBefore(bpName, "primitive-converter", new WriteTransformer()); + channel.pipeline().addFirst(inboundRequestFeeder); + channel.config().setAutoRead(false); + recvByteBufAllocator = new FixedRecvByteBufAllocator(1024); + channel.config().setRecvByteBufAllocator(recvByteBufAllocator); + base.evaluate(); + } + }; + } + + public void setMaxMessagesPerRead(int maxMessagesPerRead) { + recvByteBufAllocator.maxMessagesPerRead(maxMessagesPerRead); + } + + public void assertMessagesReceived(Object... expected) { + final List msgsReceived = handler.getMsgsReceived(); + + if (null != expected && expected.length > 0) { + assertThat("Unexpected messages received count.", msgsReceived, hasSize(expected.length)); + assertThat("Unexpected messages received.", msgsReceived, contains(expected)); + } else { + assertThat("Unexpected messages received.", msgsReceived, is(empty())); + } + } + + public int resetReadCount() { + return inboundRequestFeeder.resetReadRequested(); + } + + public int getReadRequestedCount() { + return inboundRequestFeeder.getReadRequestedCount(); + } + + public void requestMessages(long requested) throws Exception { + handler.incrementRequested(requested); + channel.pipeline().fireUserEventTriggered(new RequestReadIfRequiredEvent() { + @Override + protected boolean shouldReadMore(ChannelHandlerContext ctx) { + return true; + } + }); + channel.runPendingTasks(); + } + + public void feedMessagesForRead(Object... msgs) { + inboundRequestFeeder.addToTheFeed(msgs); + } + } + + private static class MockBackpressureManagingHandler extends BackpressureManagingHandler { + + private final List msgsReceived = new ArrayList<>(); + private final AtomicLong requested = new AtomicLong(); + + protected MockBackpressureManagingHandler(String thisHandlerName) { + super(thisHandlerName); + } + + @Override + protected void newMessage(ChannelHandlerContext ctx, Object msg) { + requested.decrementAndGet(); + msgsReceived.add(msg); + } + + @Override + protected boolean shouldReadMore(ChannelHandlerContext ctx) { + return requested.get() > 0; + } + + public List getMsgsReceived() { + return msgsReceived; + } + + public void reset() { + msgsReceived.clear(); + requested.set(0); + } + + public void incrementRequested(long requested) { + this.requested.addAndGet(requested); + } + } + + private static class BufferingHandler extends ChannelOutboundHandlerAdapter { + + private final List buffer = new ArrayList<>(); + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + buffer.add(msg); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/BytesWriteInterceptorTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/BytesWriteInterceptorTest.java new file mode 100644 index 0000000..a1fdcfe --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/BytesWriteInterceptorTest.java @@ -0,0 +1,425 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.concurrent.AbstractScheduledEventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.internal.ObjectUtil; +import io.reactivex.netty.channel.BackpressureManagingHandler.BytesWriteInterceptor; +import io.reactivex.netty.channel.BackpressureManagingHandler.WriteStreamSubscriber; +import io.reactivex.netty.test.util.MockProducer; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Scheduler; +import rx.functions.Action0; +import rx.schedulers.Schedulers; + +import java.nio.charset.Charset; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static io.reactivex.netty.channel.BackpressureManagingHandler.BytesWriteInterceptor.MAX_PER_SUBSCRIBER_REQUEST; +import static io.reactivex.netty.channel.BytesWriteInterceptorTest.InspectorRule.defaultRequestN; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static rx.Observable.just; + +public class BytesWriteInterceptorTest { + + @Rule + public final InspectorRule inspectorRule = new InspectorRule(); + + @Test(timeout = 60000) + public void testAddSubscriber() throws Exception { + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + + assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), hasSize(1)); + assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1)); + + sub1.unsubscribe(); + inspectorRule.channel.runPendingTasks(); + assertThat("Subscriber not removed post unsubscribe", inspectorRule.interceptor.getSubscribers(), is(empty())); + } + + @Test(timeout = 60000) + public void testRequestMore() throws Exception { + + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer mockProducer = inspectorRule.setupSubscriberAndValidate(sub1, 1); + assertThat("Unexpected items requested from producer.", mockProducer.getRequested(), is(defaultRequestN())); + + inspectorRule.sendMessages(1); + + assertThat("Channel not writable post write.", inspectorRule.channel.isWritable(), is(true)); + assertThat("Unexpected items requested.", mockProducer.getRequested(), is(defaultRequestN())); + } + + @Test(timeout = 60000) + public void testRequestMorePostFlush() throws Exception { + + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer mockProducer = inspectorRule.setupSubscriberAndValidate(sub1, 1); + assertThat("Unexpected items requested from producer.", mockProducer.getRequested(), is(defaultRequestN())); + + inspectorRule.channel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(1, 2)); /*Make sure that the channel is not writable on writing.*/ + + String msg = "Hello"; + inspectorRule.channel.write(msg); + + assertThat("Channel still writable.", inspectorRule.channel.isWritable(), is(false)); + assertThat("More items requested when channel is not writable.", mockProducer.getRequested(), + is(defaultRequestN())); + + inspectorRule.channel.flush(); + + assertThat("Channel not writable post flush.", inspectorRule.channel.isWritable(), is(true)); + assertThat("Unexpected items requested.", mockProducer.getRequested(), is(defaultRequestN())); + } + + @Test(timeout = 60000) + public void testMultiSubscribers() throws Exception { + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1); + + WriteStreamSubscriber sub2 = inspectorRule.newSubscriber(); + MockProducer producer2 = inspectorRule.setupSubscriberAndValidate(sub2, 2); + + inspectorRule.sendMessages(1); + + assertThat("Channel not writable post write.", inspectorRule.channel.isWritable(), is(true)); + assertThat("Unexpected items requested from first subscriber.", producer1.getRequested(), + is(defaultRequestN())); + assertThat("Unexpected items requested from second subscriber.", producer2.getRequested(), + is(defaultRequestN() / 2)); + } + + @Test(timeout = 10000) + public void testOneLongWriteAndManySmallWrites() throws Exception { + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1); + assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(defaultRequestN())); + inspectorRule.setupNewSubscriberAndComplete(2, true); + inspectorRule.setupNewSubscriberAndComplete(2, true); + + inspectorRule.sendMessages(sub1, 33); + assertThat("Unexpected items requested.", producer1.getRequested(), is(97L)); + } + + @Ignore + @Test(timeout = 100000) + public void testWritesInOrderFromDifferentThreads() throws Exception { + final WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + + // Set the current thread to be the thread of the event loop + inspectorRule.setEventLoopThread(); + + // Send 1000 messages from two different threads + int msgCount = 1000; + Scheduler.Worker worker = Schedulers.computation().createWorker(); + for (int i = 1; i < msgCount; i+=2) { + sub1.onNext(String.valueOf(i)); + + // Send from other thread + inspectorRule.sendFromOtherThread(sub1, worker, String.valueOf(i+1)); + } + + // In lack of a way of running all pending tasks on computation scheduler + Thread.sleep(1000); + + // Ensure messages are in order + Queue written = inspectorRule.getWrittenMessages(); + for (int i = 1; i <= msgCount; i++) { + Object msg = written.poll(); + String strMsg = ((ByteBuf) msg).toString(Charset.defaultCharset()); + assertThat("Not in order ", strMsg, is(String.valueOf(i))); + } + } + + @Test(timeout = 10000) + public void testBatchedSubscriberRemoves() { + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1); + assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(defaultRequestN())); + for (int i=1; i < 5; i++) { + inspectorRule.setupNewSubscriberAndComplete(i+1, false); + } + + inspectorRule.channel.runPendingTasks(); + + inspectorRule.sendMessages(sub1, 35); + assertThat("Unexpected items requested.", producer1.getRequested(), is(95L)); + } + + @Test(timeout = 10000) + public void testMinRequestN() throws Exception { + for (int i=1; i < 66; i++) { + inspectorRule.setupNewSubscriberAndComplete(i, false); + } + WriteStreamSubscriber sub1 = inspectorRule.newSubscriber(); + MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 66); + assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(1L)); + + inspectorRule.channel.runPendingTasks(); + inspectorRule.sendMessages(sub1, 35); + assertThat("Unexpected items requested.", producer1.getRequested(), greaterThan(1L)); + } + + public static class InspectorRule extends ExternalResource { + + private BytesWriteInterceptor interceptor; + private EmbeddedChannel channel; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + interceptor = new BytesWriteInterceptor("foo"); + channel = new TestEmbeddedChannel(new WriteTransformer(), interceptor); + base.evaluate(); + } + }; + } + + WriteStreamSubscriber newSubscriber() { + return interceptor.newSubscriber(channel.pipeline().lastContext(), channel.newPromise()); + } + + private MockProducer setupSubscriberAndValidate(WriteStreamSubscriber sub, int expectedSubCount) { + MockProducer mockProducer = setupSubscriber(sub); + assertThat("Subscriber not added.", interceptor.getSubscribers(), hasSize(expectedSubCount)); + assertThat("Subscriber not added.", interceptor.getSubscribers().get(expectedSubCount - 1), equalTo(sub)); + return mockProducer; + } + + private static MockProducer setupSubscriber(WriteStreamSubscriber sub) { + sub.onStart(); + MockProducer mockProducer = new MockProducer(); + sub.setProducer(mockProducer); + return mockProducer; + } + + public static Long defaultRequestN() { + return Long.valueOf(MAX_PER_SUBSCRIBER_REQUEST); + } + + public void sendMessages(WriteStreamSubscriber subscriber, int msgCount) { + for(int i=0; i < msgCount; i++) { + subscriber.onNext("Hello"); + channel.write("Hello"); + } + channel.flush(); + } + + public void sendMessages(int msgCount) { + for(int i=0; i < msgCount; i++) { + channel.write("Hello"); + } + channel.flush(); + } + + public void setupNewSubscriberAndComplete(int expectedSubCount, boolean runPendingTasks) { + WriteStreamSubscriber sub2 = newSubscriber(); + MockProducer producer2 = setupSubscriberAndValidate(sub2, expectedSubCount); + assertThat("Unexpected items requested from producer.", producer2.getRequested(), + lessThanOrEqualTo(Math.max(1, defaultRequestN()/expectedSubCount))); + sub2.onCompleted(); + sub2.unsubscribe(); + if (runPendingTasks) { + channel.runPendingTasks(); + } + } + + public Queue getWrittenMessages() { + channel.runPendingTasks(); + channel.flush(); + return channel.outboundMessages(); + } + + public void setEventLoopThread() { + ChannelPromise deregisterPromise = channel.newPromise(); + channel.deregister(deregisterPromise); + channel.runPendingTasks(); + assertThat("failed to deregister", deregisterPromise.isDone() && deregisterPromise.isSuccess()); + + ThreadAwareEmbeddedEventLoop loop = new ThreadAwareEmbeddedEventLoop(Thread.currentThread()); + ChannelFuture registerPromise = loop.register(channel); + assertThat("failed to register", registerPromise.isDone() && registerPromise.isSuccess()); + } + + private void sendFromOtherThread(final WriteStreamSubscriber subscriber, Scheduler.Worker worker, final Object msg) throws InterruptedException { + final CountDownLatch countDown = new CountDownLatch(1); + worker.schedule(new Action0() { + @Override + public void call() { + subscriber.onNext(msg); + countDown.countDown(); + } + }); + countDown.await(); + } + } + + /** + * A custom EmbeddedChannel allowing a special EventLoop, so that we can simulate calls not coming from the event loop. + */ + private static class TestEmbeddedChannel extends EmbeddedChannel { + + public TestEmbeddedChannel(WriteTransformer writeTransformer, BytesWriteInterceptor interceptor) { + super(writeTransformer, interceptor); + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return loop instanceof ThreadAwareEmbeddedEventLoop || super.isCompatible(loop); + } + + @Override + public void runPendingTasks() { + if (super.eventLoop() instanceof ThreadAwareEmbeddedEventLoop) { + ThreadAwareEmbeddedEventLoop loop = (ThreadAwareEmbeddedEventLoop) super.eventLoop(); + loop.runTasks(); + } else { + super.runPendingTasks(); + } + } + } + + /** + * Need an embedded event loop that considers a single thread to be "on the loop" in order to have writes from + * outside the event loop. + * Due to final modifier of EmbeddedEventLoop there was some copying needed. + */ + private static class ThreadAwareEmbeddedEventLoop extends AbstractScheduledEventExecutor implements EventLoop { + + private final Queue tasks = new ArrayDeque(2); + private final Thread loopThread; + + public ThreadAwareEmbeddedEventLoop(Thread loopThread) { + this.loopThread = loopThread; + } + + @Override + public EventLoopGroup parent() { + return (EventLoopGroup) super.parent(); + } + + @Override + public EventLoop next() { + return (EventLoop) super.next(); + } + + @Override + public void execute(Runnable command) { + if (command == null) { + throw new NullPointerException("command"); + } + tasks.add(command); + } + + void runTasks() { + for (;;) { + Runnable task = tasks.poll(); + if (task == null) { + break; + } + + task.run(); + } + } + + @Override + protected void cancelScheduledTasks() { + super.cancelScheduledTasks(); + } + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public Future terminationFuture() { + throw new UnsupportedOperationException(); + } + + @Override + @Deprecated + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShuttingDown() { + return false; + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return false; + } + + @Override + public ChannelFuture register(Channel channel) { + return register(new DefaultChannelPromise(channel, this)); + } + + @Override + public ChannelFuture register(ChannelPromise promise) { + ObjectUtil.checkNotNull(promise, "promise"); + promise.channel().unsafe().register(this, promise); + return promise; + } + + @Deprecated + @Override + public ChannelFuture register(Channel channel, ChannelPromise promise) { + channel.unsafe().register(this, promise); + return promise; + } + + @Override + public boolean inEventLoop() { + return Thread.currentThread() == loopThread; + } + + @Override + public boolean inEventLoop(Thread thread) { + return thread == loopThread; + } + } + +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/ConnectionImplTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ConnectionImplTest.java new file mode 100644 index 0000000..1a48e72 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ConnectionImplTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.FileRegion; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.test.util.FlushSelector; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import rx.Observable; + +public class ConnectionImplTest { + + @Rule + public final ConnRule connRule = new ConnRule(); + + @Test(timeout = 60000) + public void testWrite() throws Exception { + + Observable toWrite = Observable.empty(); + connRule.connection.write(toWrite); + + Mockito.verify(connRule.channelOperations).write(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteWithFlushSelector() throws Exception { + Observable toWrite = Observable.empty(); + FlushSelector flushSelector = new FlushSelector<>(1); + connRule.connection.write(toWrite, flushSelector); + + Mockito.verify(connRule.channelOperations).write(toWrite, flushSelector); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteAndFlushOnEach() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeAndFlushOnEach(toWrite); + + Mockito.verify(connRule.channelOperations).writeAndFlushOnEach(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteString() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeString(toWrite); + + Mockito.verify(connRule.channelOperations).writeString(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + + } + + @Test(timeout = 60000) + public void testWriteStringWithFlushSelector() throws Exception { + Observable toWrite = Observable.empty(); + FlushSelector flushSelector = new FlushSelector<>(1); + connRule.connection.writeString(toWrite, flushSelector); + + Mockito.verify(connRule.channelOperations).writeString(toWrite, flushSelector); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteStringAndFlushOnEach() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeStringAndFlushOnEach(toWrite); + + Mockito.verify(connRule.channelOperations).writeStringAndFlushOnEach(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteBytes() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeBytes(toWrite); + + Mockito.verify(connRule.channelOperations).writeBytes(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + + } + + @Test(timeout = 60000) + public void testWriteBytesWithFlushSelector() throws Exception { + Observable toWrite = Observable.empty(); + FlushSelector flushSelector = new FlushSelector<>(1); + connRule.connection.writeBytes(toWrite, flushSelector); + + Mockito.verify(connRule.channelOperations).writeBytes(toWrite, flushSelector); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteBytesAndFlushOnEach() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeBytesAndFlushOnEach(toWrite); + + Mockito.verify(connRule.channelOperations).writeBytesAndFlushOnEach(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteFileRegion() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeFileRegion(toWrite); + + Mockito.verify(connRule.channelOperations).writeFileRegion(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + + } + + @Test(timeout = 60000) + public void testWriteFileRegionWithFlushSelector() throws Exception { + Observable toWrite = Observable.empty(); + FlushSelector flushSelector = new FlushSelector<>(1); + connRule.connection.writeFileRegion(toWrite, flushSelector); + + Mockito.verify(connRule.channelOperations).writeFileRegion(toWrite, flushSelector); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testWriteFileRegionAndFlushOnEach() throws Exception { + Observable toWrite = Observable.empty(); + connRule.connection.writeFileRegionAndFlushOnEach(toWrite); + + Mockito.verify(connRule.channelOperations).writeFileRegionAndFlushOnEach(toWrite); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testFlush() throws Exception { + connRule.connection.flush(); + + Mockito.verify(connRule.channelOperations).flush(); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testClose() throws Exception { + connRule.connection.close(); + + Mockito.verify(connRule.channelOperations).close(); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + @Test(timeout = 60000) + public void testCloseWithoutFlush() throws Exception { + connRule.connection.close(false); + + Mockito.verify(connRule.channelOperations).close(false); + Mockito.verifyNoMoreInteractions(connRule.channelOperations); + } + + public static class ConnRule extends ExternalResource { + + private ChannelOperations channelOperations; + private Connection connection; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); + + @SuppressWarnings("unchecked") + ChannelOperations channelOperations = Mockito.mock(ChannelOperations.class); + + ConnRule.this.channelOperations = channelOperations; + connection = ConnectionImpl.create(channel, ConnRule.this.channelOperations); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceRule.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceRule.java new file mode 100644 index 0000000..3f47969 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceRule.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; + +public class ContentSourceRule extends ExternalResource implements Func1, Object> { + + private ByteBuf data; + private EmbeddedChannel channel; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + data = Unpooled.buffer().writeBytes("Hello".getBytes()); + channel = new EmbeddedChannel(new ChannelDuplexHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SourceEvent) { + SourceEvent sourceEvent = (SourceEvent) evt; + sourceEvent.subscriber.onNext(data); + sourceEvent.subscriber.onCompleted(); + } + super.userEventTriggered(ctx, evt); + } + }); + base.evaluate(); + } + }; + } + + public EmbeddedChannel getChannel() { + return channel; + } + + public ByteBuf getData() { + return data; + } + + public TestSubscriber subscribe(Observable source, int expectedRefCnt) { + TestSubscriber subscriber = new TestSubscriber<>(); + source.subscribe(subscriber); + subscriber.awaitTerminalEvent(); + subscriber.assertValue(data); + + ByteBuf data = subscriber.getOnNextEvents().get(0); + + assertThat("Unexpected ref count of data", data.refCnt(), is(expectedRefCnt)); + return subscriber; + } + + @Override + public Object call(Subscriber subscriber) { + return new SourceEvent(subscriber); + } + + public static class SourceEvent { + + private final Subscriber subscriber; + + public SourceEvent(Subscriber subscriber) { + this.subscriber = subscriber; + } + + public Subscriber getSubscriber() { + return subscriber; + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceTest.java new file mode 100644 index 0000000..68c1c1e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ContentSourceTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import org.junit.Rule; +import org.junit.Test; +import rx.observers.TestSubscriber; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; + +public class ContentSourceTest { + + @Rule + public final ContentSourceRule sourceRule = new ContentSourceRule(); + + @Test(timeout = 60000) + public void testNoAutoRelease() throws Exception { + ContentSource source = new ContentSource<>(sourceRule.getChannel(), sourceRule); + sourceRule.subscribe(source, 1); + } + + @Test(timeout = 60000) + public void testAutoRelease() throws Exception { + ContentSource source = new ContentSource<>(sourceRule.getChannel(), sourceRule); + sourceRule.subscribe(source.autoRelease(), 0); + } + + @Test(timeout = 60000) + public void testReplayable() throws Exception { + DisposableContentSource disposable = new ContentSource<>(sourceRule.getChannel(), sourceRule).replayable(); + sourceRule.subscribe(disposable.autoRelease(), 1); + sourceRule.subscribe(disposable.autoRelease(), 1); + + assertThat("Unexpected ref count before dispose.", sourceRule.getData().refCnt(), is(1)); + + disposable.dispose(); + + assertThat("Unexpected ref count after dispose.", sourceRule.getData().refCnt(), is(0)); + } + + @Test(timeout = 60000) + public void testSubscribePostDispose() throws Exception { + DisposableContentSource disposable = new ContentSource<>(sourceRule.getChannel(), sourceRule).replayable(); + sourceRule.subscribe(disposable.autoRelease(), 1); + disposable.dispose(); + + TestSubscriber subscriber = new TestSubscriber<>(); + disposable.subscribe(subscriber); + subscriber.awaitTerminalEvent(); + subscriber.assertError(IllegalStateException.class); + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/DefaultChannelOperationsTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/DefaultChannelOperationsTest.java new file mode 100644 index 0000000..d72883e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/DefaultChannelOperationsTest.java @@ -0,0 +1,393 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultFileRegion; +import io.netty.channel.FileRegion; +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.test.util.FlushSelector; +import io.reactivex.netty.test.util.MockEventPublisher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.functions.Action1; +import rx.observers.TestSubscriber; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import static io.reactivex.netty.test.util.MockEventPublisher.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class DefaultChannelOperationsTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Rule + public final ChannelOpRule channelOpRule = new ChannelOpRule(); + + @Test(timeout = 60000) + public void testWrite() throws Exception { + final String msg = "Hello"; + Observable writeO = channelOpRule.channelOperations.write(ChannelOpRule.bbJust(msg)); + + _testWrite(writeO, msg); + } + + @Test(timeout = 60000) + public void testWriteWithFlushSelector() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.write(ChannelOpRule.bbJust(msg1, msg2), + new FlushSelector(1)); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteAndFlushOnEach() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.writeAndFlushOnEach(ChannelOpRule.bbJust(msg1, msg2)); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteString() throws Exception { + final String msg = "Hello"; + Observable writeO = channelOpRule.channelOperations.writeString(Observable.just(msg)); + + _testWrite(writeO, msg); + } + + @Test(timeout = 60000) + public void testWriteStringWithFlushSelector() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.writeString(Observable.just(msg1, msg2), + new FlushSelector(1)); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteStringAndFlushOnEach() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.writeStringAndFlushOnEach(Observable.just(msg1, msg2)); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteBytes() throws Exception { + final String msg = "Hello"; + Observable writeO = channelOpRule.channelOperations.writeBytes(Observable.just(msg.getBytes())); + + _testWrite(writeO, msg); + } + + @Test(timeout = 60000) + public void testWriteBytesWithFlushSelector() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.writeBytes(Observable.just(msg1.getBytes(), + msg2.getBytes()), + new FlushSelector(1)); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteBytesAndFlushOnEach() throws Exception { + final String msg1 = "Hello1"; + final String msg2 = "Hello2"; + + Observable writeO = channelOpRule.channelOperations.writeBytesAndFlushOnEach( + Observable.just(msg1.getBytes(), + msg2.getBytes())); + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testWriteFileRegion() throws Exception { + FileRegion msg = new DefaultFileRegion(folder.newFile("msg.txt"), 0, 0); + + Observable writeO = channelOpRule.channelOperations.writeFileRegion(Observable.just(msg)); + + _testWrite(writeO, msg); + } + + @Test(timeout = 60000) + public void testWriteFileRegionWithFlushSelector() throws Exception { + FileRegion msg1 = new DefaultFileRegion(folder.newFile("msg1.txt"), 0, 0); + FileRegion msg2 = new DefaultFileRegion(folder.newFile("msg2.txt"), 0, 0); + + Observable writeO = channelOpRule.channelOperations.writeFileRegion(Observable.just(msg1, msg2), + new FlushSelector(1)); + + _testWithFlushSelector(writeO, msg1, msg2); + + } + + @Test(timeout = 60000) + public void testWriteFileRegionAndFlushOnEach() throws Exception { + FileRegion msg1 = new DefaultFileRegion(folder.newFile("msg1.txt"), 0, 0); + FileRegion msg2 = new DefaultFileRegion(folder.newFile("msg2.txt"), 0, 0); + + Observable writeO = channelOpRule.channelOperations + .writeFileRegionAndFlushOnEach(Observable.just(msg1, msg2)); + + _testWithFlushSelector(writeO, msg1, msg2); + } + + @Test(timeout = 60000) + public void testFlush() throws Exception { + String msg = "Hello"; + channelOpRule.channel.write(Unpooled.buffer().writeBytes(msg.getBytes())); + + channelOpRule.channelOperations.flush(); + + channelOpRule.verifyOutboundMessages(msg); + } + + @Test(timeout = 60000) + public void testCloseWithFlush() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + channelOpRule.channelOperations.close().subscribe(subscriber); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Channel not closed.", channelOpRule.channel.isOpen(), is(false)); + } + + @Test(timeout = 60000) + public void testCloseWithoutFlush() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + channelOpRule.channel.write("Hello"); + + channelOpRule.channelOperations.close(false).subscribe(subscriber); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + + channelOpRule.verifyOutboundMessages(); + assertThat("Channel not closed.", channelOpRule.channel.isOpen(), is(false)); + } + + private void _testWithFlushSelector(Observable writeObservable, Object expected1, Object expected2) { + final TestSubscriber writeSub = new TestSubscriber<>(); + + writeObservable.subscribe(writeSub); + + assertThat("Unexpected write subscribers on the channel.", channelOpRule.writeObservableSubscribers, + hasSize(1)); + + ChannelOpRule.TestWriteSubscriber testSubscriber = channelOpRule.writeObservableSubscribers.remove(0); + + channelOpRule.verifyOutboundMessages(expected1); + channelOpRule.channel.outboundMessages().clear(); + + testSubscriber.requestMore(1); + channelOpRule.verifyOutboundMessages(expected2); + + testSubscriber.awaitTerminalEvent(); + + testSubscriber.finishOverarchingWritePromiseIfAllPromisesFinished(); + + writeSub.assertNoErrors(); + writeSub.assertTerminalEvent(); + } + + private void _testWrite(Observable writeObservable, Object expected) { + final TestSubscriber writeSub = new TestSubscriber<>(); + + writeObservable.subscribe(writeSub); + + assertThat("Unexpected write subscribers on the channel.", channelOpRule.writeObservableSubscribers, + hasSize(1)); + + ChannelOpRule.TestWriteSubscriber testSubscriber = channelOpRule.writeObservableSubscribers.remove(0); + testSubscriber.finishOverarchingWritePromiseIfAllPromisesFinished(); + testSubscriber.awaitTerminalEvent(); + + writeSub.assertNoErrors(); + writeSub.assertTerminalEvent(); + + channelOpRule.verifyOutboundMessages(expected); + } + + @Test(timeout = 60000) + public void testCloseListener() throws Exception { + Observable closeListener = channelOpRule.channelOperations.closeListener(); + TestSubscriber subscriber = new TestSubscriber<>(); + closeListener.subscribe(subscriber); + + subscriber.assertNoTerminalEvent(); + + subscriber.unsubscribe(); + + subscriber.assertNoTerminalEvent(); + + channelOpRule.channel.close().sync(); + + subscriber.assertNoTerminalEvent(); + } + + public static class ChannelOpRule extends ExternalResource { + + private DefaultChannelOperations channelOperations; + private EmbeddedChannel channel; + private List writeObservableSubscribers; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + writeObservableSubscribers = new ArrayList<>(); + /*Since, the appropriate handler is not added to the pipeline that handles O<> writes.*/ + channel = new EmbeddedChannel(new HandleObservableWrite(writeObservableSubscribers)); + channelOperations = new DefaultChannelOperations<>(channel, null, disabled()); + base.evaluate(); + } + }; + } + + public static Observable bbJust(String... items) { + List bbItems = new ArrayList<>(); + + for (String item : items) { + bbItems.add(Unpooled.buffer().writeBytes(item.getBytes())); + } + + return Observable.from(bbItems); + } + + public void verifyOutboundMessages(Object... msgs) { + + boolean stringConversionRequired = msgs != null && msgs.length != 0 && msgs[0] instanceof String; + + final List outMsgsToTest = new ArrayList<>(); + + for (Object next : channel.outboundMessages()) { + if (stringConversionRequired) { + if (next instanceof ByteBuf) { + outMsgsToTest.add(((ByteBuf) next).toString(Charset.defaultCharset())); + } + } else { + outMsgsToTest.add(next); + } + } + + if (null == msgs || msgs.length == 0) { + assertThat("Unexpected messages written on the channel.", outMsgsToTest, is(empty())); + } else { + assertThat("Unexpected messages written on the channel.", outMsgsToTest, contains(msgs)); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static class HandleObservableWrite extends ChannelDuplexHandler { + + private final List writeObservableSubscribers; + + public HandleObservableWrite(List writeObservableSubscribers) { + this.writeObservableSubscribers = writeObservableSubscribers; + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + if (msg instanceof Observable) { + Observable msgO = (Observable) msg; + final TestWriteSubscriber testSubscriber = new TestWriteSubscriber(promise); + msgO.doOnNext(new Action1() { + @Override + public void call(Object o) { + final ChannelPromise channelPromise = ctx.newPromise(); + testSubscriber.allPromises.add(channelPromise); + + if (o instanceof String) { + o = Unpooled.buffer().writeBytes(((String) o).getBytes()); + } else if (o instanceof byte[]) { + o = Unpooled.buffer().writeBytes((byte[]) o); + } + ctx.write(o, channelPromise); + } + }).doOnError(new Action1() { + @Override + public void call(Throwable throwable) { + ctx.fireExceptionCaught(throwable); + } + }).subscribe(testSubscriber); + + writeObservableSubscribers.add(testSubscriber); + } else { + super.write(ctx, msg, promise); + } + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static class TestWriteSubscriber extends TestSubscriber { + + private final List allPromises = new ArrayList<>(); + private final ChannelPromise overarchingPromise; + + public TestWriteSubscriber(ChannelPromise promise) { + overarchingPromise = promise; + } + + public void finishOverarchingWritePromiseIfAllPromisesFinished() { + for (ChannelPromise aPromise : allPromises) { + if (aPromise.isDone()) { + if (!aPromise.isSuccess()) { + overarchingPromise.tryFailure(aPromise.cause()); + return; + } + } else { + overarchingPromise.tryFailure(new IllegalStateException("A write promise did not complete.")); + return; + } + } + + overarchingPromise.trySuccess(); + } + + @Override + public void onStart() { + request(1); + } + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/DetachedChannelPipelineTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/DetachedChannelPipelineTest.java new file mode 100644 index 0000000..fda3d1b --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/DetachedChannelPipelineTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.reactivex.netty.channel.DetachedChannelPipeline.HandlerHolder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.functions.Action1; +import rx.functions.Func0; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class DetachedChannelPipelineTest { + + private static final EventLoopGroup MULTI_GRP = new NioEventLoopGroup(); + + public static final Func0 HANDLER_FACTORY = new Func0() { + @Override + public ChannelHandler call() { + return new ChannelDuplexHandler(); + } + }; + private static final HandlerHolder HANDLER_1_NO_NAME = new HandlerHolder(HANDLER_FACTORY); + private static final HandlerHolder HANDLER_1 = new HandlerHolder("handler-1", HANDLER_FACTORY); + private static final HandlerHolder HANDLER_1_GRP = + new HandlerHolder("handler-1", HANDLER_FACTORY, new NioEventLoopGroup()); + private static final HandlerHolder HANDLER_1_GRP_NO_NAME = + new HandlerHolder(null, HANDLER_FACTORY, MULTI_GRP); + + private static final HandlerHolder HANDLER_2_NO_NAME = new HandlerHolder(HANDLER_FACTORY); + private static final HandlerHolder HANDLER_2 = new HandlerHolder("handler-2", HANDLER_FACTORY); + private static final HandlerHolder HANDLER_2_GRP = + new HandlerHolder("handler-2", HANDLER_FACTORY, new NioEventLoopGroup()); + private static final HandlerHolder HANDLER_2_GRP_NO_NAME = + new HandlerHolder(null, HANDLER_FACTORY, MULTI_GRP); + + @Rule + public final PipelineRule pipelineRule = new PipelineRule(); + + @Test(timeout = 60000) + public void testCopy() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1.getNameIfConfigured(), HANDLER_1.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addLast(HANDLER_2.getNameIfConfigured(), HANDLER_2.getHandlerFactoryIfConfigured()); + + DetachedChannelPipeline copy = pipelineRule.pipeline.copy(); + assertThat("Copy did not create a new instance.", copy, not(pipelineRule.pipeline)); + + assertThat("Unexpected handlers count in the copy.", copy.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers in the copy.", copy.getHoldersInOrder(), + contains(pipelineRule.pipeline.getHoldersInOrder().toArray())); + } + + @Test(timeout = 60000) + public void testAddFirst() throws Exception { + pipelineRule.pipeline.addFirst(HANDLER_1.getNameIfConfigured(), HANDLER_1.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addFirst(HANDLER_2.getNameIfConfigured(), HANDLER_2.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_2, HANDLER_1)); + } + + @Test(timeout = 60000) + public void testAddFirstWithGroup() throws Exception { + pipelineRule.pipeline.addFirst(HANDLER_1_GRP.getGroupIfConfigured(), HANDLER_1_GRP.getNameIfConfigured(), + HANDLER_1_GRP.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addFirst(HANDLER_2_GRP.getGroupIfConfigured(), HANDLER_2_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_2_GRP, HANDLER_1_GRP)); + } + + @Test(timeout = 60000) + public void testAddLast() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1.getNameIfConfigured(), HANDLER_1.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addLast(HANDLER_2.getNameIfConfigured(), HANDLER_2.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1, HANDLER_2)); + } + + @Test(timeout = 60000) + public void testAddLastWithGroup() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1_GRP.getGroupIfConfigured(), HANDLER_1_GRP.getNameIfConfigured(), + HANDLER_1_GRP.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addLast(HANDLER_2_GRP.getGroupIfConfigured(), HANDLER_2_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_GRP, HANDLER_2_GRP)); + } + + @Test(timeout = 60000) + public void testAddBefore() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1.getGroupIfConfigured(), + HANDLER_1.getNameIfConfigured(), HANDLER_1.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addBefore(HANDLER_1.getNameIfConfigured(), + HANDLER_2.getNameIfConfigured(), + HANDLER_2.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_2, HANDLER_1)); + } + + @Test(timeout = 60000) + public void testAddBeforeWithGroup() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1_GRP.getGroupIfConfigured(), + HANDLER_1_GRP.getNameIfConfigured(), HANDLER_1_GRP.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addBefore(HANDLER_2_GRP.getGroupIfConfigured(), + HANDLER_1_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_2_GRP, HANDLER_1_GRP)); + } + + @Test(timeout = 60000) + public void testAddAfter() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1.getGroupIfConfigured(), + HANDLER_1.getNameIfConfigured(), HANDLER_1.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addAfter(HANDLER_1.getNameIfConfigured(), + HANDLER_2.getNameIfConfigured(), + HANDLER_2.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1, HANDLER_2)); + } + + @Test(timeout = 60000) + public void testAddAfterWithGroup() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1_GRP.getGroupIfConfigured(), + HANDLER_1_GRP.getNameIfConfigured(), HANDLER_1_GRP.getHandlerFactoryIfConfigured()); + pipelineRule.pipeline.addAfter(HANDLER_2_GRP.getGroupIfConfigured(), HANDLER_1_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getNameIfConfigured(), + HANDLER_2_GRP.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_GRP, HANDLER_2_GRP)); + } + + @Test(timeout = 60000) + public void testAddFirstMulti() throws Exception { + pipelineRule.pipeline.addFirst(HANDLER_1_NO_NAME.getHandlerFactoryIfConfigured(), HANDLER_2_NO_NAME.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_NO_NAME, HANDLER_2_NO_NAME)); + } + + @Test(timeout = 60000) + public void testAddFirstMultiWithGroup() throws Exception { + pipelineRule.pipeline.addFirst(MULTI_GRP, HANDLER_1_GRP_NO_NAME.getHandlerFactoryIfConfigured(), + HANDLER_2_GRP_NO_NAME.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_GRP_NO_NAME, HANDLER_2_GRP_NO_NAME)); + } + + @Test(timeout = 60000) + public void testAddLastMulti() throws Exception { + pipelineRule.pipeline.addLast(HANDLER_1_NO_NAME.getHandlerFactoryIfConfigured(), + HANDLER_2_NO_NAME.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_NO_NAME, HANDLER_2_NO_NAME)); + } + + @Test(timeout = 60000) + public void testAddLastMultiWithGroup() throws Exception { + pipelineRule.pipeline.addLast(MULTI_GRP, HANDLER_1_GRP_NO_NAME.getHandlerFactoryIfConfigured(), + HANDLER_2_GRP_NO_NAME.getHandlerFactoryIfConfigured()); + + assertThat("Unexpected handlers count.", pipelineRule.pipeline.getHoldersInOrder(), hasSize(2)); + assertThat("Unexpected handlers.", pipelineRule.pipeline.getHoldersInOrder(), + contains(HANDLER_1_GRP_NO_NAME, HANDLER_2_GRP_NO_NAME)); + } + + public static class PipelineRule extends ExternalResource { + + private DetachedChannelPipeline pipeline; + private ChannelDuplexHandler tail; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + tail = new ChannelDuplexHandler(); + pipeline = new DetachedChannelPipeline(new Action1() { + @Override + public void call(ChannelPipeline pipeline1) { + pipeline1.addLast(tail); + } + }); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/ReadProducerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ReadProducerTest.java new file mode 100644 index 0000000..eb6fe55 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/ReadProducerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.channel.AbstractConnectionToChannelBridge.ReadProducer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Subscriber; +import rx.exceptions.MissingBackpressureException; +import rx.observers.Subscribers; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class ReadProducerTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ProducerRule producerRule = new ProducerRule(); + + @Test(timeout = 60000) + public void testTurnOffBackpressureAtStart() throws Exception { + producerRule.producer.request(Long.MAX_VALUE); + + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + + producerRule.producer.sendOnNext("Hello"); + + /*Backpressure turned off, don't decrement*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + } + + @Test(timeout = 60000) + public void testTurnOffBackpressureLater() throws Exception { + producerRule.producer.request(1); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(1L)); + + producerRule.producer.request(Long.MAX_VALUE); /*now turn off*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + + producerRule.producer.sendOnNext("Hello"); + + /*Backpressure turned off, don't decrement*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + } + + @Test(timeout = 60000) + public void testBackpressure() throws Exception { + producerRule.producer.request(1); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(1L)); + + producerRule.producer.sendOnNext("Hello"); + + /*Backpressure on, so decrement*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(0L)); + } + + @Test(timeout = 60000) + public void testRequestedOverflow() throws Exception { + producerRule.producer.request(1); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(1L)); + + producerRule.producer.request(Long.MAX_VALUE - 1); /*Adding overflows & turn off backpressure*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + + producerRule.producer.sendOnNext("Hello"); + + /*Backpressure turned off, don't decrement*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(Long.MAX_VALUE)); + } + + @Test(timeout = 60000) + public void testSupplyMoreThanDemand() throws Exception { + thrown.expectCause(isA(MissingBackpressureException.class)); + + producerRule.producer.request(1); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(1L)); + + producerRule.producer.sendOnNext("Hello"); + + /*Backpressure on, so decrement*/ + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(0L)); + + producerRule.producer.sendOnNext("Hello"); /*overflow*/ + } + + @Test(timeout = 60000) + public void testShouldReadMoreOnLessDemand() throws Exception { + producerRule.producer.request(0); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(0L)); + assertThat("Unexpected read more status.", + producerRule.producer.shouldReadMore(producerRule.channel.pipeline().firstContext()), is(false)); + } + + @Test(timeout = 60000) + public void testShouldReadMoreOnUnsubscribe() throws Exception { + producerRule.producer.request(1); + assertThat("Unexpected requested count.", producerRule.producer.getRequested(), is(1L)); + assertThat("Unexpected read more status.", + producerRule.producer.shouldReadMore(producerRule.channel.pipeline().firstContext()), is(true)); + + producerRule.subscriber.unsubscribe(); + + assertThat("Unexpected read more status.", + producerRule.producer.shouldReadMore(producerRule.channel.pipeline().firstContext()), is(false)); + } + + public static class ProducerRule extends ExternalResource { + + private ReadProducer producer; + private EmbeddedChannel channel; + private Subscriber subscriber; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + channel = new EmbeddedChannel(new LoggingHandler()); + subscriber = Subscribers.empty(); + producer = new ReadProducer<>(subscriber, channel); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridgeTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridgeTest.java new file mode 100644 index 0000000..960f46e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/SubscriberToChannelFutureBridgeTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelFuture; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import rx.observers.TestSubscriber; + +public class SubscriberToChannelFutureBridgeTest { + + @Rule + public final BridgeRule rule = new BridgeRule(); + + @Test(timeout = 60000) + public void testBridge() throws Exception { + + rule.subscriber.unsubscribe(); + + Mockito.verify(rule.future).addListener(rule.bridge); + Mockito.verify(rule.future).removeListener(rule.bridge); + } + + @Test(timeout = 60000) + public void testSuccess() throws Exception { + rule.completeFuture(); + rule.subscriber.assertTerminalEvent(); + rule.subscriber.assertNoErrors(); + } + + @Test(timeout = 60000) + public void testError() throws Exception { + rule.failFuture(); + rule.subscriber.assertTerminalEvent(); + rule.subscriber.assertError(IllegalStateException.class); + } + + public static class BridgeRule extends ExternalResource { + + public TestSubscriber subscriber; + public SubscriberToChannelFutureBridge bridge; + private ChannelFuture future; + private volatile boolean futureTerminated; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + subscriber = new TestSubscriber<>(); + bridge = new SubscriberToChannelFutureBridge() { + + @Override + protected void doOnSuccess(ChannelFuture future) { + subscriber.onCompleted(); + } + + @Override + protected void doOnFailure(ChannelFuture future, Throwable cause) { + subscriber.onError(cause); + } + }; + + future = Mockito.mock(ChannelFuture.class); + + bridge.bridge(future, subscriber); + base.evaluate(); + } + }; + } + + public void completeFuture() throws Exception { + if (futureTerminated) { + throw new IllegalStateException("Channel future is already terminated"); + } + futureTerminated = true; + Mockito.when(future.isSuccess()).thenReturn(true); + bridge.operationComplete(future); + } + + public void failFuture() throws Exception { + if (futureTerminated) { + throw new IllegalStateException("Channel future is already terminated"); + } + futureTerminated = true; + Mockito.when(future.isSuccess()).thenReturn(false); + Mockito.when(future.cause()).thenReturn(new IllegalStateException("Force terminate")); + bridge.operationComplete(future); + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteStreamSubscriberTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteStreamSubscriberTest.java new file mode 100644 index 0000000..1a71a57 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteStreamSubscriberTest.java @@ -0,0 +1,285 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.channel.BackpressureManagingHandler.BytesWriteInterceptor; +import io.reactivex.netty.channel.BackpressureManagingHandler.WriteStreamSubscriber; +import io.reactivex.netty.test.util.MockProducer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.IOException; +import java.util.Queue; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class WriteStreamSubscriberTest { + + @Rule + public final SubscriberRule subscriberRule = new SubscriberRule(); + + @Test(timeout = 60000) + public void testOnStart() throws Exception { + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + subscriberRule.start(); + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(subscriberRule.defaultRequestN())); + } + + @Test(timeout = 60000) + public void testUnsubscribeOnPromiseCancel() throws Exception { + subscriberRule.start(); + + assertThat("Subsriber isn't subscribed.", subscriberRule.subscriber.isUnsubscribed(), is(false)); + + subscriberRule.channelPromise.cancel(false); + + assertThat("Promise not cancelled.", subscriberRule.channelPromise.isCancelled(), is(true)); + + assertThat("Subsriber isn't unsubscribed.", subscriberRule.subscriber.isUnsubscribed(), is(true)); + } + + @Test(timeout = 60000) + public void testWriteCompleteBeforeStream() throws Exception { + subscriberRule.start(); + + String msg1 = "msg1"; + subscriberRule.writeAndFlushMessages(msg1); + subscriberRule.assertMessagesWritten(msg1); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + + subscriberRule.subscriber.onCompleted(); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(true)); + assertThat("Unexpected promise result.", subscriberRule.channelPromise.isSuccess(), is(true)); + } + + @Test(timeout = 60000) + public void testWriteCompleteAfterStream() throws Exception { + subscriberRule.start(); + + String msg1 = "msg1"; + subscriberRule.writeMessages(msg1); + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + subscriberRule.subscriber.onCompleted(); + /*Complete when write completes.*/ + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + + subscriberRule.channel.flush(); /*Completes write*/ + + subscriberRule.assertMessagesWritten(msg1); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(true)); + assertThat("Unexpected promise result.", subscriberRule.channelPromise.isSuccess(), is(true)); + } + + @Test(timeout = 60000) + public void testMultiWrite() throws Exception { + subscriberRule.start(); + + String msg1 = "msg1"; + String msg2 = "msg2"; + subscriberRule.writeMessages(msg1, msg2); + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + subscriberRule.subscriber.onCompleted(); + /*Complete when write completes.*/ + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + + subscriberRule.channel.flush(); /*Completes write*/ + + subscriberRule.assertMessagesWritten(msg1, msg2); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(true)); + assertThat("Unexpected promise result.", subscriberRule.channelPromise.isSuccess(), is(true)); + } + + @Test(timeout = 60000) + public void testWriteFailed() throws Exception { + subscriberRule.start(); + + String msg1 = "msg1"; + subscriberRule.writeMessages(msg1); + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(false)); + + subscriberRule.channel.close(); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(true)); + assertThat("Unexpected promise result.", subscriberRule.channelPromise.isSuccess(), is(false)); + } + + @Test(timeout = 60000) + public void testStreamError() throws Exception { + subscriberRule.start(); + + subscriberRule.sendMessagesAndAssert(1); + + subscriberRule.subscriber.onError(new IOException()); + + assertThat("Unexpected promise completion state.", subscriberRule.channelPromise.isDone(), is(true)); + assertThat("Unexpected promise result.", subscriberRule.channelPromise.isSuccess(), is(false)); + } + + @Test(timeout = 60000) + public void testRequestMoreNotRequired() throws Exception { + subscriberRule.init(4); + subscriberRule.start(); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(subscriberRule.defaultRequestN())); + + subscriberRule.sendMessagesAndAssert(2); // Pending: 4 - 2 : low water mark: 4/2 + subscriberRule.subscriber.requestMoreIfNeeded(subscriberRule.defaultRequestN); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(subscriberRule.defaultRequestN())); + } + + @Test(timeout = 60000) + public void testRequestMoreRequired() throws Exception { + subscriberRule.init(4); + subscriberRule.start(); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(subscriberRule.defaultRequestN())); + + subscriberRule.sendMessagesAndAssert(3); // Pending: 4 - 3 : low water mark: 4/2 + subscriberRule.subscriber.requestMoreIfNeeded(subscriberRule.defaultRequestN); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(7L));// request: 4 + 4 - (4 - 3) + } + + @Test(timeout = 60000) + public void testLowerMaxBufferSize() throws Exception { + subscriberRule.init(4); + subscriberRule.start(); + + subscriberRule.subscriber.requestMoreIfNeeded(2); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(subscriberRule.defaultRequestN())); + } + + @Test(timeout = 60000) + public void testLowerMaxBufferSizeAndThenMore() throws Exception { + subscriberRule.init(8); + subscriberRule.start(); + + subscriberRule.subscriber.requestMoreIfNeeded(6); + subscriberRule.sendMessagesAndAssert(6); // Pending: 8 - 6 : low water mark: 6/2 + subscriberRule.subscriber.requestMoreIfNeeded(6); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(12L)); // requestN: 8 + 6 - (8 - 6) + } + + @Test(timeout = 60000) + public void testHigherMaxBufferSize() throws Exception { + subscriberRule.init(4); + subscriberRule.start(); + + subscriberRule.subscriber.requestMoreIfNeeded(6); + + assertThat("Unexpected request made to the producer.", subscriberRule.mockProducer.getRequested(), + is(6L)); // requestN: 4 + 6 - 4 + } + + public static class SubscriberRule extends ExternalResource { + + private WriteStreamSubscriber subscriber; + private ChannelPromise channelPromise; + private EmbeddedChannel channel; + private MockProducer mockProducer; + private int defaultRequestN; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + init(BytesWriteInterceptor.MAX_PER_SUBSCRIBER_REQUEST); + base.evaluate(); + } + }; + } + + protected void init(int defaultRequestN) { + this.defaultRequestN = defaultRequestN; + channel = new EmbeddedChannel(new LoggingHandler()); + channelPromise = channel.newPromise(); + ChannelHandlerContext ctx = channel.pipeline().firstContext(); + subscriber = new WriteStreamSubscriber(ctx, channelPromise, defaultRequestN().intValue()); + mockProducer = new MockProducer(); + } + + public void start() { + subscriber.onStart(); /*So that setProducer does not request Long.MAX_VALUE*/ + subscriber.setProducer(mockProducer); + + mockProducer.assertBackpressureRequested(); + mockProducer.assertIllegalRequest(); + } + + public void writeAndFlushMessages(Object... msgs) { + writeMessages(msgs); + channel.flush(); + } + + public void writeMessages(Object... msgs) { + for (Object msg : msgs) { + subscriber.onNext(msg); + } + } + + public void assertMessagesWritten(Object... msgs) { + Queue outboundMessages = channel.outboundMessages(); + + if (null == msgs || msgs.length == 0) { + assertThat("Unexpected number of messages written on the channel.", outboundMessages, is(empty())); + return; + } + + assertThat("Unexpected number of messages written on the channel.", outboundMessages, hasSize(msgs.length)); + assertThat("Unexpected messages written on the channel.", outboundMessages, contains(msgs)); + } + + protected void sendMessagesAndAssert(int count) { + String[] msgs = new String[count]; + for (int i = 0; i < count; i++) { + msgs[i] = "msg" + i; + } + writeAndFlushMessages(msgs); + assertThat("Unexpected promise completion state.", channelPromise.isDone(), is(false)); + assertMessagesWritten(msgs); + } + + public Long defaultRequestN() { + return Long.valueOf(defaultRequestN); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteTransformerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteTransformerTest.java new file mode 100644 index 0000000..f454a8b --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/WriteTransformerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class WriteTransformerTest { + + @Rule + public final ConverterRule converterRule = new ConverterRule(); + + @Test(timeout = 60000) + public void testWriteString() throws Exception { + String msg = "Hello"; + converterRule.channel.writeAndFlush(msg); + + ByteBuf written = converterRule.readNextOutboundBuffer(); + assertThat("Unexpected content of buffer written.", written.toString(Charset.defaultCharset()), equalTo(msg)); + } + + @Test(timeout = 60000) + public void testWriteByteArray() throws Exception { + byte[] msg = "Hello".getBytes(); + converterRule.channel.writeAndFlush(msg); + ByteBuf writtenMsg = converterRule.readNextOutboundBuffer(); + byte[] asBytes = new byte[msg.length]; + writtenMsg.readBytes(asBytes); + + assertThat("Unexpected content of buffer written.", asBytes, equalTo(msg)); + } + + @Test(timeout = 60000) + public void testTransformerSingle() throws Exception { + converterRule.appendTransformer(new AllocatingTransformer() { + @Override + public List transform(Integer toTransform, ByteBufAllocator allocator) { + return Arrays.asList(allocator.buffer().writeInt(toTransform), + allocator.buffer().writeInt(++toTransform)); + } + }); + + converterRule.channel.writeAndFlush(1); + ByteBuf written = converterRule.readNextOutboundBuffer(2); + + assertThat("Unexpected message written on the channel", written.readInt(), is(1)); + + written = converterRule.readNextOutboundBuffer(); + assertThat("Unexpected message written on the channel", written.readInt(), is(2)); + } + + @Test(timeout = 60000) + public void testTransformerChained() throws Exception { + converterRule.appendTransformer(new AllocatingTransformer() { + @Override + public List transform(Integer toTransform, ByteBufAllocator allocator) { + return Arrays.asList(allocator.buffer().writeInt(toTransform), + allocator.buffer().writeInt(++toTransform)); + } + }); + + converterRule.appendTransformer(new AllocatingTransformer() { + @Override + public List transform(Long toTransform, ByteBufAllocator allocator) { + int i = toTransform.intValue(); + return Arrays.asList(i, ++i); + } + }); + + converterRule.channel.writeAndFlush(1L); + ByteBuf written = converterRule.readNextOutboundBuffer(4); + + assertThat("Unexpected message written on the channel", written.readInt(), is(1)); + + written = converterRule.readNextOutboundBuffer(3); + assertThat("Unexpected message written on the channel", written.readInt(), is(2)); + + written = converterRule.readNextOutboundBuffer(2); + assertThat("Unexpected message written on the channel", written.readInt(), is(2)); + + written = converterRule.readNextOutboundBuffer(1); + assertThat("Unexpected message written on the channel", written.readInt(), is(3)); + } + + public static class ConverterRule extends ExternalResource { + + private WriteTransformer converter; + private EmbeddedChannel channel; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + converter = new WriteTransformer(); + channel = new EmbeddedChannel(converter); + base.evaluate(); + } + }; + } + + public ByteBuf readNextOutboundBuffer() { + return readNextOutboundBuffer(1); + } + + public ByteBuf readNextOutboundBuffer(int expectedWrittenMessages) { + assertThat("Unexpected outbound messages size.", channel.outboundMessages(), + hasSize(expectedWrittenMessages)); + Object writtenMsg = channel.readOutbound(); + assertThat("Unexpected message type written on the channel.", writtenMsg, is(instanceOf(ByteBuf.class))); + return (ByteBuf) writtenMsg; + } + + public void appendTransformer(AllocatingTransformer transformer) { + @SuppressWarnings({"rawtypes", "unchecked"}) + AppendTransformerEvent event = new AppendTransformerEvent(transformer); + channel.pipeline().fireUserEventTriggered(event); + + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/channel/events/ConnectionEventPublisherTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/channel/events/ConnectionEventPublisherTest.java new file mode 100644 index 0000000..aa2c6f2 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/channel/events/ConnectionEventPublisherTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.channel.events; + +import io.reactivex.netty.test.util.MockConnectionEventListener; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class ConnectionEventPublisherTest { + + @Rule + public final PublisherRule rule = new PublisherRule(); + + @Test(timeout = 60000) + public void testOnByteRead() throws Exception { + rule.publisher.onByteRead(1); + rule.listener.assertMethodsCalled(Event.BytesRead); + + assertThat("Listener not called with bytes read.", rule.listener.getBytesRead(), is(1L)); + } + + @Test(timeout = 60000) + public void testOnByteWritten() throws Exception { + rule.publisher.onByteWritten(1); + rule.listener.assertMethodsCalled(Event.BytesWritten); + + assertThat("Listener not called with bytes written.", rule.listener.getBytesWritten(), is(1L)); + } + + @Test(timeout = 60000) + public void testOnFlushStart() throws Exception { + rule.publisher.onFlushStart(); + rule.listener.assertMethodsCalled(Event.FlushStart); + } + + @Test(timeout = 60000) + public void testOnFlushSuccess() throws Exception { + rule.publisher.onFlushComplete(1, TimeUnit.MILLISECONDS); + rule.listener.assertMethodsCalled(Event.FlushSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnWriteStart() throws Exception { + rule.publisher.onWriteStart(); + rule.listener.assertMethodsCalled(Event.WriteStart); + } + + @Test(timeout = 60000) + public void testOnWriteSuccess() throws Exception { + rule.publisher.onWriteSuccess(1, TimeUnit.MILLISECONDS); + rule.listener.assertMethodsCalled(Event.WriteSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnWriteFailed() throws Exception { + final Throwable expected = new NullPointerException("Deliberate"); + rule.publisher.onWriteFailed(1, TimeUnit.MILLISECONDS, expected); + rule.listener.assertMethodsCalled(Event.WriteFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseStart() throws Exception { + rule.publisher.onConnectionCloseStart(); + rule.listener.assertMethodsCalled(Event.CloseStart); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseSuccess() throws Exception { + rule.publisher.onConnectionCloseSuccess(1, TimeUnit.MILLISECONDS); + rule.listener.assertMethodsCalled(Event.CloseSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseFailed() throws Exception { + final Throwable expected = new NullPointerException("Deliberate"); + rule.publisher.onConnectionCloseFailed(1, TimeUnit.MILLISECONDS, expected); + rule.listener.assertMethodsCalled(Event.CloseFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testCustomEvent() throws Exception { + Object event = "Hello"; + rule.publisher.onCustomEvent(event); + rule.listener.assertMethodsCalled(Event.CustomEvent); + + assertThat("Listener not called with event.", rule.listener.getCustomEvent(), is(event)); + } + + @Test(timeout = 60000) + public void testCustomEventWithError() throws Exception { + final Throwable expected = new NullPointerException("Deliberate"); + Object event = "Hello"; + rule.publisher.onCustomEvent(event, expected); + rule.listener.assertMethodsCalled(Event.CustomEventWithError); + + assertThat("Listener not called with event.", rule.listener.getCustomEvent(), is(event)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testCustomEventWithDuration() throws Exception { + Object event = "Hello"; + rule.publisher.onCustomEvent(event, 1, TimeUnit.MILLISECONDS); + rule.listener.assertMethodsCalled(Event.CustomEventWithDuration); + + assertThat("Listener not called with event.", rule.listener.getCustomEvent(), is(event)); + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testCustomEventWithDurationAndError() throws Exception { + final Throwable expected = new NullPointerException("Deliberate"); + Object event = "Hello"; + rule.publisher.onCustomEvent(event, 1, TimeUnit.MILLISECONDS, expected); + rule.listener.assertMethodsCalled(Event.CustomEventWithDurationAndError); + + assertThat("Listener not called with event.", rule.listener.getCustomEvent(), is(event)); + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + assertThat("Publishing not enabled.", rule.publisher.publishingEnabled(), is(true)); + } + + @Test(timeout = 60000) + public void testCopy() throws Exception { + ConnectionEventPublisher copy = rule.publisher.copy(); + + assertThat("Publisher not copied.", copy, is(not(sameInstance(rule.publisher)))); + assertThat("Listeners not copied.", copy.getListeners(), is(not(sameInstance(rule.publisher.getListeners())))); + } + + public static class PublisherRule extends ExternalResource { + + private MockConnectionEventListener listener; + private ConnectionEventPublisher publisher; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + listener = new MockConnectionEventListener(); + publisher = new ConnectionEventPublisher<>(); + publisher.subscribe(listener); + base.evaluate(); + } + }; + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/ClientStateTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/ClientStateTest.java new file mode 100644 index 0000000..3a98b82 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/ClientStateTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.HandlerNames; +import io.reactivex.netty.channel.DetachedChannelPipeline; +import io.reactivex.netty.test.util.embedded.EmbeddedConnectionProvider; +import io.reactivex.netty.util.LoggingHandlerFactory; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Matchers; +import org.mockito.Mockito; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func0; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class ClientStateTest { + + @Rule + public final ClientStateRule clientStateRule = new ClientStateRule(); + + @Test(timeout = 60000) + public void testAddChannelHandlerFirst() throws Exception { + String handlerName = "test_handler"; + Func0 handlerFactory = clientStateRule.newHandler(); + + ClientState newState = clientStateRule.clientState + .addChannelHandlerFirst(handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addFirst(handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerFirstWithEventExecGroup() throws Exception { + String handlerName = "test_handler"; + Func0 handlerFactory = clientStateRule.newHandler(); + NioEventLoopGroup executor = new NioEventLoopGroup(); + + ClientState newState = clientStateRule.clientState + .addChannelHandlerFirst(executor, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addFirst(executor, handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerLast() throws Exception { + String handlerName = "test_handler"; + Func0 handlerFactory = clientStateRule.newHandler(); + + ClientState newState = clientStateRule.clientState + .addChannelHandlerLast(handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addLast(handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerLastWithEventExecGroup() throws Exception { + String handlerName = "test_handler"; + Func0 handlerFactory = clientStateRule.newHandler(); + NioEventLoopGroup executor = new NioEventLoopGroup(); + + ClientState newState = clientStateRule.clientState + .addChannelHandlerLast(executor, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addLast(executor, handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerBefore() throws Exception { + String handlerName = "test_handler"; + String baseHandlerName = "test_handler_base"; + Func0 handlerFactory = clientStateRule.newHandler(); + + ClientState newState = clientStateRule.clientState + .addChannelHandlerBefore(baseHandlerName, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addBefore(baseHandlerName, handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerBeforeWithEventExecGroup() throws Exception { + String handlerName = "test_handler"; + String baseHandlerName = "test_handler_base"; + Func0 handlerFactory = clientStateRule.newHandler(); + NioEventLoopGroup executor = new NioEventLoopGroup(); + ClientState newState = clientStateRule.clientState + .addChannelHandlerBefore(executor, baseHandlerName, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addBefore(executor, baseHandlerName, handlerName, + handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerAfter() throws Exception { + String handlerName = "test_handler"; + String baseHandlerName = "test_handler_base"; + Func0 handlerFactory = clientStateRule.newHandler(); + ClientState newState = clientStateRule.clientState + .addChannelHandlerAfter(baseHandlerName, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addAfter(baseHandlerName, handlerName, handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + + } + + @Test(timeout = 60000) + public void testAddChannelHandlerAfterWithEventExecGroup() throws Exception { + String handlerName = "test_handler"; + String baseHandlerName = "test_handler_base"; + Func0 handlerFactory = clientStateRule.newHandler(); + NioEventLoopGroup executor = new NioEventLoopGroup(); + ClientState newState = clientStateRule.clientState + .addChannelHandlerAfter(executor, baseHandlerName, handlerName, handlerFactory); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).addAfter(executor, baseHandlerName, handlerName, + handlerFactory); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + + } + + @Test(timeout = 60000) + public void testPipelineConfigurator() throws Exception { + final Action1 pipelineConfigurator = new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + } + }; + + ClientState newState = clientStateRule.clientState.pipelineConfigurator(pipelineConfigurator); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()).configure(pipelineConfigurator); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + @Test(timeout = 60000) + public void testEnableWireLogging() throws Exception { + ClientState newState = clientStateRule.clientState.enableWireLogging("", LogLevel.ERROR); + + clientStateRule.verifyMockPipelineAccessPostCopy(); + assertThat("Client state not copied.", clientStateRule.clientState, is(not(newState))); + assertThat("Options copied.", clientStateRule.clientState.unsafeChannelOptions(), + is(newState.unsafeChannelOptions())); + assertThat("Detached pipeline not copied.", clientStateRule.clientState.unsafeDetachedPipeline(), + is(not(newState.unsafeDetachedPipeline()))); + + Mockito.verify(newState.unsafeDetachedPipeline()) + .addFirst(HandlerNames.WireLogging.getName(), LoggingHandlerFactory.getFactory("", LogLevel.ERROR)); + + Mockito.verifyNoMoreInteractions(newState.unsafeDetachedPipeline()); + Mockito.verifyNoMoreInteractions(clientStateRule.mockPipeline); + } + + public static class ClientStateRule extends ExternalResource { + + private ClientState clientState; + private DetachedChannelPipeline mockPipeline; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + mockPipeline = Mockito.mock(DetachedChannelPipeline.class, Mockito.RETURNS_MOCKS); + EmbeddedConnectionProvider ecp = new EmbeddedConnectionProvider<>(); + clientState = ClientState.create(mockPipeline, ecp.asFactory(), Observable.empty()) + .enableWireLogging(LogLevel.ERROR); + base.evaluate(); + } + }; + } + + public Func0 newHandler() { + return new Func0() { + @Override + public ChannelHandler call() { + return new TestableChannelHandler(); + } + }; + } + + public void verifyMockPipelineAccessPostCopy() { + Mockito.verify(mockPipeline).copy(Matchers.>anyObject()); + } + + public ClientState updateState(ClientState newState) { + final ClientState current = clientState; + clientState = newState; + return current; + } + + public static class TestableChannelHandler extends ChannelDuplexHandler { + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/SslClientTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/SslClientTest.java new file mode 100644 index 0000000..ae04239 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/SslClientTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client; + +import org.junit.Test; + +public class SslClientTest { + + @Test(timeout = 60000) + public void testReleaseOnSslFailure() throws Exception { + //TODO: Fix me +/* + HttpServer server = + HttpServer.newServer() + .start(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, + HttpServerResponse response) { + return Observable.empty(); + } + }); + final SocketAddress serverAddress = new InetSocketAddress("127.0.0.1", server.getServerPort()); + + MockPoolLimitDeterminationStrategy strategy = new MockPoolLimitDeterminationStrategy(1); + + // The connect fails because the server does not support SSL. + TestSubscriber subscriber = new TestSubscriber<>(); + final PoolConfig config = new PoolConfig<>(); + config.limitDeterminationStrategy(strategy); + + ConnectionProvider connectionProvider = PooledConnectionProvider.create(config, serverAddress); + + HttpClient.newClient(connectionProvider) + .unsafeSecure() + .createGet("/") + .flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse response) { + return response.getContent(); + } + }) + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), is(instanceOf(SSLException.class))); + + Assert.assertEquals("Unexpected acquire counts.", 1, strategy.getAcquireCount()); + Assert.assertEquals("Unexpected release counts.", 1, strategy.getReleaseCount()); + Assert.assertEquals("Unexpected available permits.", 1, strategy.getAvailablePermits()); +*/ + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategyTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategyTest.java new file mode 100644 index 0000000..6ff4c18 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/AbstractP2CStrategyTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.test.util.MockEventPublisher; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class AbstractP2CStrategyTest { + + public static final int UNUSABLE_HOST_WEIGHT = -1; + @Rule + public final StrategyRule rule = new StrategyRule(); + + @Test + public void testNoHosts() { + ConnectionProvider cp = rule.strategy.newStrategy( + Collections.>emptyList()); + TestSubscriber> sub = new TestSubscriber<>(); + cp.newConnectionRequest().subscribe(sub); + + sub.awaitTerminalEvent(); + sub.assertError(NoHostsAvailableException.class); + Assert.assertEquals("Unexpected number of hosts in the pool.", 0, rule.strategy.hostsInPool); + Assert.assertEquals("Unexpected number of no usable hosts count.", 1, rule.strategy.allUnusable); + } + + @Test + public void testSingleUnusableHost() { + ConnectionProvider cp = rule.strategy.newStrategy(rule.newHostStream(UNUSABLE_HOST_WEIGHT)); + TestSubscriber> sub = new TestSubscriber<>(); + cp.newConnectionRequest().subscribe(sub); + + sub.awaitTerminalEvent(); + sub.assertError(NoHostsAvailableException.class); + + Assert.assertEquals("Unexpected number of hosts in the pool.", 1, rule.strategy.hostsInPool); + Assert.assertEquals("Unexpected number of Unusable hosts found count.", 0, rule.strategy.twoUnusableHosts); + Assert.assertEquals("Unexpected number of no usable hosts count.", 1, rule.strategy.allUnusable); + } + + @Test + public void testMultipleUnusableHost() { + ConnectionProvider cp = rule.strategy.newStrategy(rule.newHostStream(UNUSABLE_HOST_WEIGHT, + UNUSABLE_HOST_WEIGHT)); + TestSubscriber> sub = new TestSubscriber<>(); + cp.newConnectionRequest().subscribe(sub); + + sub.awaitTerminalEvent(); + sub.assertError(NoHostsAvailableException.class); + + Assert.assertEquals("Unexpected number of hosts in the pool.", 2, rule.strategy.hostsInPool); + Assert.assertEquals("Unexpected number of Unusable hosts found count.", 5, rule.strategy.twoUnusableHosts); + Assert.assertEquals("Unexpected number of no usable hosts count.", 1, rule.strategy.allUnusable); + } + + @Test + public void testUsableAndUnusable() { + ConnectionProvider cp = rule.strategy.newStrategy(rule.newHostStream(10, UNUSABLE_HOST_WEIGHT)); + TestSubscriber> sub = new TestSubscriber<>(); + cp.newConnectionRequest().subscribe(sub); + + sub.awaitTerminalEvent(); + sub.assertNoErrors(); + + Assert.assertEquals("Unexpected number of hosts in the pool.", 2, rule.strategy.hostsInPool); + Assert.assertEquals("Unexpected number of Unusable hosts found count.", 0, rule.strategy.twoUnusableHosts); + Assert.assertEquals("Unexpected number of no usable hosts count.", 0,rule.strategy.allUnusable); + } + + public static class StrategyRule extends ExternalResource { + + private MockP2CStrategy strategy; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + strategy = new MockP2CStrategy(); + base.evaluate(); + } + }; + } + + public List> newHostStream(int... weights) { + List> toReturn = new ArrayList<>(); + for (int weight : weights) { + ConnectionProvider dummy = new ConnectionProvider() { + @Override + public Observable> newConnectionRequest() { + return Observable.empty(); + } + }; + Host h = new Host(new InetSocketAddress(0)); + EventPublisher publisher = MockEventPublisher.disabled(); + HostConnector connector = new HostConnector<>(h, dummy, null, publisher, null); + toReturn.add(new HostHolder<>(connector, new ClientListenerImpl(weight))); + } + return toReturn; + } + + private static class ClientListenerImpl extends ClientEventListener { + + private volatile double weight; + + public ClientListenerImpl(double weight) { + this.weight = weight; + } + + public double getWeight() { + return weight; + } + } + + private static class MockP2CStrategy extends AbstractP2CStrategy { + + private volatile int allUnusable; + private volatile int hostsInPool; + private volatile int twoUnusableHosts; + + @Override + protected ClientListenerImpl newListener(Host host) { + return new ClientListenerImpl(0); + } + + @Override + protected double getWeight(ClientListenerImpl listener) { + return listener.getWeight(); + } + + @Override + protected void noUsableHostsFound() { + allUnusable++; + } + + @Override + protected void foundTwoUnusableHosts() { + twoUnusableHosts++; + } + + @Override + protected void newHostsList(int size) { + hostsInPool += size; + } + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactoryTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactoryTest.java new file mode 100644 index 0000000..322bef6 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/loadbalancer/LoadBalancerFactoryTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.client.loadbalancer; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import rx.Observable; +import rx.functions.Func1; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class LoadBalancerFactoryTest { + + @Rule + public final LBFactoryRule rule = new LBFactoryRule(); + + @Test(timeout = 60000) + public void testHostRemove() throws Exception { + + TestSubscriber>> testSubscriber = rule.newHostsListListener(); + + rule.initProvider(); + Host host = rule.emitHost(); + testSubscriber.assertNoTerminalEvent(); + + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected number of hosts received", testSubscriber.getOnNextEvents().get(0), hasSize(1)); + assertThat("Unexpected host received", testSubscriber.getOnNextEvents().get(0).get(0).getConnector().getHost(), + is(host)); + + rule.completeHost(host); + + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(2)); + assertThat("Unexpected number of hosts received", testSubscriber.getOnNextEvents().get(1), + is(Matchers.>empty())); + } + + @Test(timeout = 60000) + public void testDuplicateHost() throws Exception { + TestSubscriber>> testSubscriber = rule.newHostsListListener(); + + rule.initProvider(); + Host host = rule.emitHost(); + testSubscriber.assertNoTerminalEvent(); + + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected number of hosts received", testSubscriber.getOnNextEvents().get(0), hasSize(1)); + assertThat("Unexpected host received", testSubscriber.getOnNextEvents().get(0).get(0).getConnector().getHost(), + is(host)); + + rule.emitHost(host); + testSubscriber.assertNoTerminalEvent(); + + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected number of hosts received", testSubscriber.getOnNextEvents().get(0), hasSize(1)); + assertThat("Unexpected host received", testSubscriber.getOnNextEvents().get(0).get(0).getConnector().getHost(), + is(host)); + } + + @Test(timeout = 60000) + public void testHostSourceEmitsError() throws Exception { + TestSubscriber>> testSubscriber = rule.newHostsListListener(); + + rule.initProvider(); + rule.emitHost(); + testSubscriber.assertNoTerminalEvent(); + + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(1)); + + rule.hostStream.onError(new NullPointerException("Deliberate exception")); + + testSubscriber.assertNoTerminalEvent(); + assertThat("Unexpected number of hosts lists received", testSubscriber.getOnNextEvents(), hasSize(1)); + } + + public static class LBFactoryRule extends ExternalResource { + + private PublishSubject>> lists = PublishSubject.create(); + private PublishSubject hostStream = PublishSubject.create(); + private List emittedHosts = new ArrayList<>(); + private ConnectionProvider connectionProviderMock; + private EventSource eventSourceMock; + private EventPublisher eventPublisherMock; + private ClientEventListener eventListenerMock; + private LoadBalancerFactory factory; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + @SuppressWarnings("unchecked") + ConnectionProvider m = + (ConnectionProvider) Mockito.mock(ConnectionProvider.class); + connectionProviderMock = m; + + @SuppressWarnings("unchecked") + EventSource es = (EventSource)Mockito.mock(EventSource.class); + eventSourceMock = es; + + eventPublisherMock = Mockito.mock(EventPublisher.class); + eventListenerMock = Mockito.mock(ClientEventListener.class); + + factory = LoadBalancerFactory.create(new LoadBalancingStrategy() { + @Override + public ConnectionProvider newStrategy(List> hosts) { + lists.onNext(hosts); + return new ConnectionProvider() { + @Override + public Observable> newConnectionRequest() { + return Observable.empty(); + } + }; + } + + @Override + public HostHolder toHolder(HostConnector connector) { + return new HostHolder<>(connector, eventListenerMock); + } + }); + base.evaluate(); + } + }; + } + + public ConnectionProvider initProvider() { + return factory.newProvider(hostStream.map(new Func1>() { + @Override + public HostConnector call(Host host) { + return new HostConnector<>(host, connectionProviderMock, eventSourceMock, eventPublisherMock, + eventListenerMock); + } + })); + } + + public TestSubscriber>> newHostsListListener() { + TestSubscriber>> testSubscriber = new TestSubscriber<>(); + lists.subscribe(testSubscriber); + return testSubscriber; + } + + public Host emitHost() { + Host host = new Host(new InetSocketAddress(0), PublishSubject.create()); + emittedHosts.add(host); + hostStream.onNext(host); + return host; + } + + public void emitHost(Host host) { + emittedHosts.add(host); + hostStream.onNext(host); + } + + public void completeHost(int index) { + completeHost(emittedHosts.get(index)); + } + + public void completeHost(Host host) { + ((PublishSubject)host.getCloseNotifier()).onCompleted(); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolderTest.java new file mode 100644 index 0000000..0352d2c --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/FIFOIdleConnectionsHolderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import rx.observers.TestSubscriber; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class FIFOIdleConnectionsHolderTest { + + @Rule + public final HolderRule holderRule = new HolderRule(); + + @Test(timeout = 60000) + public void testPoll() throws Exception { + holderRule.pollNow(); + + @SuppressWarnings("unchecked") + final PooledConnection mock = Mockito.mock(PooledConnection.class); + Mockito.when(mock.isUsable()).thenReturn(true); + + PooledConnection added = holderRule.addAConnection(); + + holderRule.pollNow(added); + + holderRule.pollNow(); // Poll removes the item. + } + + @Test(timeout = 60000) + public void testPeek() throws Exception { + holderRule.peekNow(); + + @SuppressWarnings("unchecked") + final PooledConnection mock = Mockito.mock(PooledConnection.class); + Mockito.when(mock.isUsable()).thenReturn(true); + + PooledConnection added = holderRule.addAConnection(); + + holderRule.peekNow(added); + + holderRule.peekNow(added); // Peek does not removes the item. + } + + @Test(timeout = 60000) + public void testAdd() throws Exception { + PooledConnection added = holderRule.addAConnection(); + + holderRule.peekNow(added); + + PooledConnection added2 = holderRule.addAConnection(); + + holderRule.peekNow(added, added2); // Get both items in the same order. + } + + @Test(timeout = 60000) + public void testRemove() throws Exception { + PooledConnection added = holderRule.addAConnection(); + PooledConnection added2 = holderRule.addAConnection(); + + holderRule.peekNow(added, added2); // Get both items in the same order. + + holderRule.holder.remove(added); + + holderRule.peekNow(added2); // one item is removed + } + + public static class HolderRule extends ExternalResource { + + private FIFOIdleConnectionsHolder holder; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + holder = new FIFOIdleConnectionsHolder<>(); + base.evaluate(); + } + }; + } + + @SafeVarargs + public final TestSubscriber> pollNow(PooledConnection... expected) { + TestSubscriber> subscriber = new TestSubscriber<>(); + holder.poll().subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertTerminalEvent(); + + assertThat("Unexpected connections received from the holder.", subscriber.getOnNextEvents(), + hasSize(expected.length)); + + if (expected.length > 0) { + assertThat("Unexpected connections received from the holder.", subscriber.getOnNextEvents(), + contains(expected)); + } + + return subscriber; + } + + @SafeVarargs + public final TestSubscriber> peekNow(PooledConnection... expected) { + return peekNow(holder, expected); + } + + @SafeVarargs + public static TestSubscriber> peekNow( + IdleConnectionsHolder holder, + PooledConnection... expected) { + TestSubscriber> subscriber = new TestSubscriber<>(); + holder.peek().subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertTerminalEvent(); + + assertThat("Unexpected connections received from the holder.", subscriber.getOnNextEvents(), + hasSize(expected.length)); + + if (expected.length > 0) { + assertThat("Unexpected connections received from the holder.", subscriber.getOnNextEvents(), + contains(expected)); + } + + return subscriber; + } + + public PooledConnection addAConnection() { + + @SuppressWarnings("unchecked") + PooledConnection mock = Mockito.mock(PooledConnection.class); + Mockito.when(mock.isUsable()).thenReturn(true); + + holder.add(mock); + + return mock; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PoolLimitStrategyTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PoolLimitStrategyTest.java new file mode 100644 index 0000000..e988f6f --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PoolLimitStrategyTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class PoolLimitStrategyTest { + + @Test(timeout = 60000) + public void testMaxConnectionLimit() throws Exception { + + MaxConnectionsBasedStrategy strategy = new MaxConnectionsBasedStrategy(3); + long startTime = System.currentTimeMillis(); + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available permits.", 2, strategy.getAvailablePermits()); + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available permits.", 1, strategy.getAvailablePermits()); + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available permits.", 0, strategy.getAvailablePermits()); + + Assert.assertFalse("Invalid permit acquire success.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + + strategy.releasePermit(); + + Assert.assertEquals("Unexpected available permits.", 1, strategy.getAvailablePermits()); + Assert.assertTrue("Permit not available after release.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testCompositeStrategy() throws Exception { + long startTime = System.currentTimeMillis(); + MaxConnectionsBasedStrategy global = new MaxConnectionsBasedStrategy(1); + MaxConnectionsBasedStrategy local = new MaxConnectionsBasedStrategy(2); + CompositePoolLimitDeterminationStrategy strategy = + new CompositePoolLimitDeterminationStrategy(local, global); + + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available global permits.", 0, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 1, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 0, strategy.getAvailablePermits()); // Should be min. of all strategies + + Assert.assertFalse("Invalid permit acquire success.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + + Assert.assertEquals("Unexpected available global permits.", 0, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 1, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 0, strategy.getAvailablePermits()); // Should be min. of all strategies + + strategy.releasePermit(); + + Assert.assertEquals("Unexpected available global permits.", 1, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 2, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 1, strategy.getAvailablePermits()); // Should be min. of all strategies + + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available global permits.", 0, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 1, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 0, strategy.getAvailablePermits()); // Should be min. of all strategies + } + + @Test(timeout = 60000) + public void testFirstStrategyHasMorePermits() throws Exception { + long startTime = System.currentTimeMillis(); + MaxConnectionsBasedStrategy global = new MaxConnectionsBasedStrategy(2); + MaxConnectionsBasedStrategy local = new MaxConnectionsBasedStrategy(1); + CompositePoolLimitDeterminationStrategy strategy = new CompositePoolLimitDeterminationStrategy(local, global); + + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available global permits.", 1, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 0, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 0, strategy.getAvailablePermits()); // Should be min. of all strategies + + Assert.assertFalse("Invalid permit acquire success.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + + strategy.releasePermit(); + + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available global permits.", 1, global.getAvailablePermits()); + Assert.assertEquals("Unexpected available local permits.", 0, local.getAvailablePermits()); + Assert.assertEquals("Unexpected available composite permits.", 0, strategy.getAvailablePermits()); // Should be min. of all strategies + } + + @Test(timeout = 60000) + public void testIncrementDecrementMaxConnections() throws Exception { + long startTime = System.currentTimeMillis(); + MaxConnectionsBasedStrategy strategy = new MaxConnectionsBasedStrategy(1); + Assert.assertTrue("Invalid permit acquire failure.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + Assert.assertEquals("Unexpected available permits.", 0, strategy.getAvailablePermits()); + + Assert.assertFalse("Invalid permit acquire success.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + + strategy.incrementMaxConnections(1); + + Assert.assertEquals("Unexpected available permits.", 1, strategy.getAvailablePermits()); + Assert.assertTrue("Permit not available after release.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + + strategy.releasePermit(); + strategy.decrementMaxConnections(1); + + Assert.assertEquals("Unexpected available permits.", 0, strategy.getAvailablePermits()); + Assert.assertFalse("Invalid permit acquire success.", strategy.acquireCreationPermit(startTime, TimeUnit.MILLISECONDS)); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PooledConnectionProviderImplTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PooledConnectionProviderImplTest.java new file mode 100644 index 0000000..0d7bab7 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PooledConnectionProviderImplTest.java @@ -0,0 +1,382 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.client.ClientConnectionToChannelBridge; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.HostConnector; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.test.util.MockEventPublisher; +import io.reactivex.netty.test.util.TrackableMetricEventsListener; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.schedulers.TestScheduler; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.InetSocketAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static io.reactivex.netty.client.pool.MaxConnectionsBasedStrategy.*; +import static java.lang.annotation.ElementType.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class PooledConnectionProviderImplTest { + + @Rule + public ExpectedException thrown= ExpectedException.none(); + + @Rule + public final PooledFactoryRule pooledFactoryRule = new PooledFactoryRule(); + + @Test(timeout = 6000000) + public void testConnect() throws Exception { + pooledFactoryRule.getAConnection(); + pooledFactoryRule.assertNoIdleConnection(); + } + + @MaxConnections(1) + @Test(timeout = 60000) + public void testReuse() throws Exception { + final PooledConnection conn1 = pooledFactoryRule.getAConnection(); + + pooledFactoryRule.returnToIdle(conn1); + + PooledConnection conn2 = pooledFactoryRule.getAConnection(); + + assertThat("Connection not reused.", conn2, is(conn1)); + } + + @Test(timeout = 60000) + public void testRelease() throws Exception { + _testRelease(); + } + + @Test(timeout = 60000) + public void testDiscard() throws Exception { + final Connection connection = pooledFactoryRule.getAConnection(); + assertThat("Connection is null.", connection, notNullValue()); + + pooledFactoryRule.assertNoIdleConnection(); + + /*This attribute will discard on close*/ + connection.unsafeNettyChannel().attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); + + /* Close will discard */ + pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ + + pooledFactoryRule.assertNoIdleConnection(); + } + + @Test(timeout = 60000) + public void testExpired() throws Exception { + final PooledConnection connection = pooledFactoryRule.getAConnection(); + assertThat("Connection is null.", connection, notNullValue()); + + pooledFactoryRule.assertNoIdleConnection(); + + /*This attribute will discard on close*/ + connection.setLastReturnToPoolTimeMillis(System.currentTimeMillis() - 30000); + + /* Close will discard */ + pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ + + pooledFactoryRule.assertNoIdleConnection(); + } + + @Test(timeout = 60000000) + public void testIdleConnectionCleanup() throws Exception { + PooledConnection idleConnection = _testRelease(); + + /*Force discard by next idle connection reap*/ + idleConnection.unsafeNettyChannel().attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); + + pooledFactoryRule.testScheduler.advanceTimeBy(1, TimeUnit.MINUTES); + + pooledFactoryRule.assertNoIdleConnection(); + } + + @MaxConnections(1) + @Test(timeout = 60000) + public void testPoolExhaustion() throws Exception { + thrown.expectCause(isA(PoolExhaustedException.class)); + + pooledFactoryRule.getAConnection(); + + pooledFactoryRule.getProvider().newConnectionRequest().toBlocking().single(); + } + + @Test(timeout = 60000) + public void testConnectFailed() throws Exception { + PooledConnectionProvider factory; + PoolConfig config = new PoolConfig<>(); + config.idleConnectionsHolder(pooledFactoryRule.holder); + + MockEventPublisher publisher = MockEventPublisher.disabled(); + ClientEventListener listener = new ClientEventListener(); + EmbeddedConnectionProvider connectionProvider = new EmbeddedConnectionProvider(publisher, true, listener); + Host host = new Host(new InetSocketAddress("127.0.0.1", 0)); + HostConnector connector = new HostConnector<>(host, connectionProvider, + publisher, publisher, listener); + + factory = new PooledConnectionProviderImpl<>(config, connector); + + TestSubscriber subscriber = new TestSubscriber<>(); + factory.newConnectionRequest().subscribe(subscriber); + + subscriber.assertTerminalEvent(); + + assertThat("Error not returned to connect.", subscriber.getOnCompletedEvents(), is(empty())); + assertThat("Error not returned to connect.", subscriber.getOnNextEvents(), is(empty())); + + } + + @Test(timeout = 60000) + public void testMetricEventCallback() throws Throwable { + TrackableMetricEventsListener eventsListener = new TrackableMetricEventsListener(); + + pooledFactoryRule.init(DEFAULT_MAX_CONNECTIONS, MockEventPublisher.enabled(), + MockEventPublisher.enabled(), eventsListener); + final PooledConnection connection = pooledFactoryRule.getAConnection(); + + assertThat("Unexpected acquire attempted count.", eventsListener.getAcquireAttemptedCount(), + is(1L)); + assertThat("Unexpected acquire succedded count.", eventsListener.getAcquireSucceededCount(), + is(1L)); + assertThat("Unexpected acquire failed count.", eventsListener.getAcquireFailedCount(), + is(0L)); + + pooledFactoryRule.returnToIdle(connection); + + assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), + is(1L)); + assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), + is(1L)); + assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); + + final PooledConnection reusedConn = pooledFactoryRule.getAConnection(); + + Assert.assertEquals("Reused connection not same as original.", connection, reusedConn); + + assertThat("Unexpected acquire attempted count.", eventsListener.getAcquireAttemptedCount(), + is(2L)); + assertThat("Unexpected acquire succedded count.", eventsListener.getAcquireSucceededCount(), + is(2L)); + assertThat("Unexpected acquire failed count.", eventsListener.getAcquireFailedCount(), + is(0L)); + assertThat("Unexpected reuse count.", eventsListener.getReuseCount(), is(1L)); + + pooledFactoryRule.closeAndAwait(reusedConn); + + assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), is(2L)); + assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), is(2L)); + assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); + + pooledFactoryRule.provider.discard(reusedConn).toBlocking().lastOrDefault(null); + + assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), is(2L)); + assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), is(2L)); + assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); + assertThat("Unexpected connection eviction count.", eventsListener.getEvictionCount(), is(2L)); + } + + private PooledConnection _testRelease() throws Exception { + final Connection connection = pooledFactoryRule.getAConnection(); + + pooledFactoryRule.assertNoIdleConnection(); + + /* Close will release */ + pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ + + PooledConnection connIdle = + pooledFactoryRule.holder.peek().defaultIfEmpty(null).toBlocking().single(); + + assertThat("Release did not add to idle.", connIdle, not(nullValue())); + + return connIdle; + } + + public static class PooledFactoryRule extends ExternalResource { + + private PooledConnectionProvider provider; + private TestScheduler testScheduler; + private FIFOIdleConnectionsHolder holder; + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + MaxConnections maxConnections1 = description.getAnnotation(MaxConnections.class); + ClientEventListener listener = new ClientEventListener(); + final MockEventPublisher publisher = MockEventPublisher.disabled(); + int maxConnections = null == maxConnections1? DEFAULT_MAX_CONNECTIONS + : maxConnections1.value(); + init(maxConnections, publisher, publisher, listener); + base.evaluate(); + } + }; + } + + protected void init(int maxConnections, EventSource eventSource, + EventPublisher publisher, ClientEventListener clientListener) { + testScheduler = Schedulers.test(); + Observable idleConnCleaner = Observable.timer(1, TimeUnit.MINUTES, testScheduler); + holder = new FIFOIdleConnectionsHolder<>(); + PoolConfig config = new PoolConfig<>(); + config.idleConnectionsCleanupTimer(idleConnCleaner) + .maxConnections(maxConnections) + .idleConnectionsHolder(holder); + Host host = new Host(new InetSocketAddress("127.0.0.1", 0)); + ConnectionProvider cp = new EmbeddedConnectionProvider(publisher, clientListener); + HostConnector connector = new HostConnector<>(host, cp, eventSource, publisher, + clientListener); + provider = new PooledConnectionProviderImpl<>(config, connector); + } + + public PooledConnectionProvider getProvider() { + return provider; + } + + public PooledConnection getAConnection(Observable> connectionObservable) + throws InterruptedException, ExecutionException, TimeoutException { + TestSubscriber> connSub = new TestSubscriber<>(); + connectionObservable.subscribe(connSub); + + connSub.awaitTerminalEvent(); + connSub.assertNoErrors(); + assertThat("Unexpected connections returned on connect.", connSub.getOnNextEvents(), hasSize(1)); + + Connection connection = connSub.getOnNextEvents().get(0); + assertThat("Connection is null.", connection, notNullValue()); + + return (PooledConnection) connection; + } + + public PooledConnection getAConnection() + throws InterruptedException, ExecutionException, TimeoutException { + return getAConnection(getProvider().newConnectionRequest()); + } + + public void closeAndAwait(Connection toClose) throws Exception { + EmbeddedChannel embeddedChannel= (EmbeddedChannel) toClose.unsafeNettyChannel(); + + final TestSubscriber testSubscriber = new TestSubscriber<>(); + + toClose.close().subscribe(testSubscriber); + + embeddedChannel.runPendingTasks(); + + testSubscriber.awaitTerminalEvent(); + + testSubscriber.assertNoErrors(); + } + + public void assertNoIdleConnection() { + final TestSubscriber> subscriber = new TestSubscriber<>(); + holder.peek().subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Idle connection available", subscriber.getOnNextEvents(), is(empty())); + } + + public void returnToIdle(PooledConnection conn1) { + conn1.closeNow(); + EmbeddedChannel embeddedChannel= (EmbeddedChannel) conn1.unsafeNettyChannel(); + embeddedChannel.runPendingTasks(); + + TestSubscriber> subscriber = new TestSubscriber<>(); + holder.peek().take(1).subscribe(subscriber); + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected number of idle connections post release.", subscriber.getOnNextEvents(), hasSize(1)); + assertThat("Connection not returned to idle holder on close.", + subscriber.getOnNextEvents().get(0), is(conn1)); + } + + } + + private static class EmbeddedConnectionProvider implements ConnectionProvider { + + private final EventPublisher publisher; + private final boolean failConnect; + private final ClientEventListener clientListener; + + public EmbeddedConnectionProvider(EventPublisher publisher, boolean failConnect, + ClientEventListener clientListener) { + this.publisher = publisher; + this.failConnect = failConnect; + this.clientListener = clientListener; + } + + public EmbeddedConnectionProvider(EventPublisher publisher, ClientEventListener clientListener) { + this(publisher, false, clientListener); + } + + @Override + public Observable> newConnectionRequest() { + if (failConnect) { + return Observable.error(new IllegalStateException("Deliberate connect failure")); + } + + return Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> s) { + EmbeddedChannel c = new EmbeddedChannel(new LoggingHandler()); + c.attr(EventAttributeKeys.EVENT_PUBLISHER).set(publisher); + if (publisher.publishingEnabled()) { + c.attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).set(clientListener); + c.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).set(clientListener); + } + ClientConnectionToChannelBridge.addToPipeline(c.pipeline(), false); + s.onNext(ConnectionImpl.fromChannel(c)); + s.onCompleted(); + } + }); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(METHOD) + public @interface MaxConnections { + int value() default DEFAULT_MAX_CONNECTIONS; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolderTest.java new file mode 100644 index 0000000..348e7b5 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/client/pool/PreferCurrentEventLoopHolderTest.java @@ -0,0 +1,226 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.client.pool; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.Future; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.client.pool.PooledConnection.Owner; +import io.reactivex.netty.client.pool.PreferCurrentEventLoopHolder.IdleConnectionsHolderFactory; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.test.util.MockEventPublisher; +import io.reactivex.netty.threads.PreferCurrentEventLoopGroup; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.schedulers.Schedulers; + +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class PreferCurrentEventLoopHolderTest { + + @Rule + public final PreferCurrentELHolderRule preferCurrentELHolderRule = new PreferCurrentELHolderRule(); + + @Test(timeout = 60000) + public void testPollOutOfEventloop() throws Exception { + PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + + + PooledConnection connection = preferCurrentELHolderRule.holder.poll() + .defaultIfEmpty(null).toBlocking() + .single(); + + assertThat("Unexpected connection.", connection, is(connection1)); + } + + @Test(timeout = 60000) + public void testPollInEventloop() throws Exception { + final PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + + preferCurrentELHolderRule.runFromChannelEventLoop(new Callable() { + @Override + public Void call() throws Exception { + PooledConnection connection = preferCurrentELHolderRule.holder.poll() + .defaultIfEmpty(null) + .toBlocking() + .single(); + assertThat("Connection available in the eventloop.", connection, is(connection1)); + return null; + } + }).get(1, TimeUnit.MINUTES); + } + + @Test + public void testPollThisEventLoopConnectionsInEl() throws Exception { + final PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + preferCurrentELHolderRule.runFromChannelEventLoop(new Callable() { + @Override + public Void call() throws Exception { + PooledConnection connection = + preferCurrentELHolderRule.holder.pollThisEventLoopConnections() + .defaultIfEmpty(null) + .toBlocking().single(); + assertThat("Connection available in the eventloop.", connection, is(connection1)); + return null; + } + }).get(1, TimeUnit.MINUTES); + } + + @Test(timeout = 60000) + public void testPollRemovesItem() throws Exception { + PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + PooledConnection connection = preferCurrentELHolderRule.holder.poll() + .defaultIfEmpty(null) + .toBlocking().single(); + + assertThat("Connection not available with poll.", connection, is(connection1)); + + connection = preferCurrentELHolderRule.holder.poll() + .defaultIfEmpty(null) + .toBlocking().single(); + + assertThat("Connection available after poll.", connection, is(nullValue())); + } + + @Test(timeout = 60000) + public void testPeek() throws Exception { + PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + PooledConnection connection = preferCurrentELHolderRule.holder.peek() + .defaultIfEmpty(null) + .toBlocking().single(); + + assertThat("Connection not available with peek.", connection, is(connection1)); + + connection = preferCurrentELHolderRule.holder.peek().defaultIfEmpty(null) + .toBlocking().single(); + + assertThat("Connection not available after peek.", connection, not(nullValue())); + assertThat("Unexpected connection on peek.", connection, is(connection1)); + } + + @Test(timeout = 60000) + public void testRemove() throws Exception { + PooledConnection connection1 = preferCurrentELHolderRule.addConnection(); + PooledConnection connection = preferCurrentELHolderRule.holder.peek() + .defaultIfEmpty(null) + .toBlocking().single(); + + assertThat("Connection not available with peek.", connection, is(connection1)); + + preferCurrentELHolderRule.holder.remove(connection1); + + connection = preferCurrentELHolderRule.holder.peek().defaultIfEmpty(null).toBlocking().single(); + + assertThat("Connection not removed.", connection, is(nullValue())); + } + + public static class PreferCurrentELHolderRule extends ExternalResource implements Owner { + + private PreferCurrentEventLoopHolder holder; + private EventPublisher eventPublisher; + private PoolConfig poolConfig; + private ConcurrentLinkedQueue> discarded; + private ConcurrentLinkedQueue> released; + private ExecutorService eventLoopThread; + private EmbeddedChannel channel; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + eventLoopThread = Executors.newFixedThreadPool(1); + channel = new EmbeddedChannel(new LoggingHandler()); + PreferCurrentEventLoopGroup eventLoopGroup = new PreferCurrentEventLoopGroup(channel.eventLoop()); + eventPublisher = MockEventPublisher.disabled(); + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(eventPublisher); + holder = new PreferCurrentEventLoopHolder<>(eventLoopGroup, + new IdleConnectionsHolderFactoryImpl()); + poolConfig = new PoolConfig<>(); + poolConfig.maxIdleTimeoutMillis(TimeUnit.DAYS.toMillis(1)) + .idleConnectionsCleanupTimer(Observable.timer(1, TimeUnit.DAYS, Schedulers.test())) + .limitDeterminationStrategy(new MaxConnectionsBasedStrategy(1)) + .idleConnectionsHolder(holder); + discarded = new ConcurrentLinkedQueue<>(); + released = new ConcurrentLinkedQueue<>(); + base.evaluate(); + } + }; + } + + public PooledConnection addConnection() throws Exception { + Connection connection = ConnectionImpl.fromChannel(channel); + PooledConnection pooledConnection = PooledConnection.create(this, + poolConfig.getMaxIdleTimeMillis(), + connection); + holder.add(pooledConnection); + + runAllPendingTasksOnChannel(); + + return pooledConnection; + } + + @Override + public Observable release(PooledConnection connection) { + released.add(connection); + return Observable.empty(); + } + + @Override + public Observable discard(PooledConnection connection) { + discarded.add(connection); + return Observable.empty(); + } + + public Future runFromChannelEventLoop(Callable runnable) throws Exception { + Future toReturn = channel.eventLoop().submit(runnable); + runAllPendingTasksOnChannel(); + return toReturn; + } + + public void runAllPendingTasksOnChannel() throws Exception { + eventLoopThread.submit(new Runnable() { + @Override + public void run() { + channel.runPendingTasks(); + } + }).get(1, TimeUnit.MINUTES); + } + + private static class IdleConnectionsHolderFactoryImpl implements IdleConnectionsHolderFactory { + + @Override + public IdleConnectionsHolder call() { + return new FIFOIdleConnectionsHolder<>(); + } + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderRule.java b/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderRule.java new file mode 100644 index 0000000..4286a00 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderRule.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import io.reactivex.netty.test.util.MockEventListener; +import org.hamcrest.Matchers; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Subscription; + +import java.util.Collection; + +import static org.hamcrest.MatcherAssert.*; + +public class ListenersHolderRule extends ExternalResource { + + private ListenersHolder holder; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + holder = new ListenersHolder<>(); + base.evaluate(); + } + }; + } + + public ListenerWithSub addAListener() { + final MockEventListener listener = new MockEventListener(); + Subscription subscription = holder.subscribe(listener); + assertListenerAdded(listener); + + return new ListenerWithSub(listener, subscription); + } + + public void assertListenerAdded(MockEventListener... listeners) { + Collection allListeners = holder.getAllListeners(); + assertThat("Unexpected listeners count in the holder.", allListeners, Matchers.hasSize(listeners.length)); + assertThat("Listener not added to the holder.", allListeners, Matchers.contains(listeners)); + } + + public ListenersHolder getHolder() { + return holder; + } + + public static class ListenerWithSub { + + final MockEventListener listener; + final Subscription subscription; + + public ListenerWithSub(MockEventListener listener, Subscription subscription) { + this.subscription = subscription; + this.listener = listener; + } + + public MockEventListener getListener() { + return listener; + } + + public Subscription getSubscription() { + return subscription; + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderTest.java new file mode 100644 index 0000000..9cc697b --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/events/ListenersHolderTest.java @@ -0,0 +1,564 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.events; + +import io.reactivex.netty.events.ListenersHolderRule.ListenerWithSub; +import io.reactivex.netty.test.util.MockEventListener; +import org.junit.Rule; +import org.junit.Test; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; +import rx.functions.Action4; +import rx.functions.Action5; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class ListenersHolderTest { + + @Rule + public final ListenersHolderRule holderRule = new ListenersHolderRule(); + + @Test(timeout = 60000) + public void testSubscribe() throws Exception { + + ListenerWithSub l = holderRule.addAListener(); + + holderRule.assertListenerAdded(l.listener); + + l.subscription.unsubscribe(); + + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), is(empty())); + } + + @Test(timeout = 60000) + public void testMultipleListeners() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + final MockEventListener listener2 = new MockEventListener(); + Subscription subscription1 = holderRule.getHolder().subscribe(listener1); + Subscription subscription2 = holderRule.getHolder().subscribe(listener2); + + holderRule.assertListenerAdded(listener1, listener2); + + subscription1.unsubscribe(); + + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), hasSize(1)); + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), + not(contains(listener1))); + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), + contains(listener2)); + + subscription2.unsubscribe(); + + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), is(empty())); + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + + assertThat("Publishing enabled with no listeners", holderRule.getHolder().publishingEnabled(), is(false)); + + ListenerWithSub l1 = holderRule.addAListener(); + + assertThat("Publishing disabled with a listener", holderRule.getHolder().publishingEnabled(), is(true)); + + l1.subscription.unsubscribe(); + + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), is(empty())); + + assertThat("Publishing enabled post listener unsubscribe", holderRule.getHolder().publishingEnabled(), is(false)); + } + + @Test(timeout = 60000) + public void testDispose() throws Exception { + ListenerWithSub l = holderRule.addAListener(); + holderRule.getHolder().dispose(); + + assertThat("On complete not called on dispose.", l.listener.getOnCompletedCount(), is(1)); + assertThat("Listener not unsubscribed on dispose.", l.subscription.isUnsubscribed(), is(true)); + + assertThat("Listener not removed on dispose.", holderRule.getHolder().getAllListeners(), + not(contains(l.listener))); + } + + @Test(timeout = 60000) + public void testDisposeWithExceptions() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + final MockEventListener listener2 = new MockEventListener(); + Subscription subscription1 = holderRule.getHolder().subscribe(listener1); + Subscription subscription2 = holderRule.getHolder().subscribe(listener2); + + assertThat("Listeners not added.", holderRule.getHolder().getAllListeners(), hasSize(2)); + assertThat("Listeners not added.", holderRule.getHolder().getAllListeners(), contains(listener1, listener2)); + + try { + holderRule.getHolder().dispose(); + throw new AssertionError("Error not thrown on dispose."); + } catch (Exception e) { + // Expected. + } + + assertThat("First listener not completed.", listener1.getOnCompletedCount(), is(1)); + assertThat("Second listener not completed.", listener2.getOnCompletedCount(), is(1)); + + assertThat("First listener not unsubscribed.", subscription1.isUnsubscribed(), is(true)); + assertThat("Second listener not unsubscribed.", subscription2.isUnsubscribed(), is(true)); + + assertThat("Listeners not removed post dispose.", holderRule.getHolder().getAllListeners(), is(empty())); + } + + @Test(timeout = 60000) + public void testInvokeListeners() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + holderRule.getHolder().invokeListeners(new Action1() { + @Override + public void call(MockEventListener mockEventListener) { + mockEventListener.anEvent(); + } + }); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMulti() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + holderRule.getHolder().invokeListeners(new Action1() { + @Override + public void call(MockEventListener mockEventListener) { + mockEventListener.anEvent(); + } + }); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked.", listener2.getEventInvocationCount(), is(1)); + } + + @Test(timeout = 60000) + public void testInvokeListenersRaiseException() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + holderRule.getHolder().subscribe(listener2); + + assertThat("Listeners not added.", holderRule.getHolder().getAllListeners(), hasSize(2)); + assertThat("Listeners not added.", holderRule.getHolder().getAllListeners(), contains(listener1, listener2)); + + holderRule.getHolder().invokeListeners(new Action1() { + @Override + public void call(MockEventListener mockEventListener) { + mockEventListener.anEvent(); + } + }); + + assertThat("First listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDuration() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + holderRule.getHolder().invokeListeners(new Action3() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit) { + mockEventListener.anEventWithDuration(duration, timeUnit); + } + }, 1, TimeUnit.MICROSECONDS); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMultiWithDuration() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + holderRule.getHolder().invokeListeners(new Action3() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit) { + mockEventListener.anEventWithDuration(duration, timeUnit); + } + }, 1, TimeUnit.MICROSECONDS); + + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDurationRaiseException() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + holderRule.getHolder().invokeListeners(new Action3() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit) { + mockEventListener.anEventWithDuration(duration, timeUnit); + } + }, 1, TimeUnit.MICROSECONDS); + + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDurationAndError() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + final Throwable expected = new NullPointerException(); + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, Throwable t) { + mockEventListener.anEventWithDurationAndError(duration, timeUnit, t); + } + }, 1, TimeUnit.MICROSECONDS, expected); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with error.", listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMultiWithDurationAndError() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final Throwable expected = new NullPointerException(); + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, Throwable t) { + mockEventListener.anEventWithDurationAndError(duration, timeUnit, t); + } + }, 1, TimeUnit.MICROSECONDS, expected); + + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with error.", listener1.getRecievedError(), is(expected)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Second listener not invoked with error.", listener2.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDurationAndErrorRaiseException() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final Throwable expected = new NullPointerException(); + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, Throwable t) { + mockEventListener.anEventWithDurationAndError(duration, timeUnit, t); + } + }, 1, TimeUnit.MICROSECONDS, expected); + + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with error.", listener1.getRecievedError(), is(expected)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Second listener not invoked with error.", listener2.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDurationAndArg() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, String arg) { + mockEventListener.anEventWithDurationAndArg(duration, timeUnit, arg); + } + }, 1, TimeUnit.MICROSECONDS, arg); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with argument.", listener.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMultiWithDurationAndArg() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, String arg) { + mockEventListener.anEventWithDurationAndArg(duration, timeUnit, arg); + } + }, 1, TimeUnit.MICROSECONDS, arg); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with argument.", listener1.getArg(), is(arg)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Second listener not invoked with argument.", listener2.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMultiWithDurationErrorAndArg() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final Object event = "doom"; + final Throwable expected = new NullPointerException(); + holderRule.getHolder().invokeListeners(new Action5() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, Throwable throwable, + Object event) { + mockEventListener.onCustomEvent(event, duration, timeUnit, throwable); + } + }, 1, TimeUnit.MICROSECONDS, expected, event); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with error.", listener1.getRecievedError(), is(expected)); + assertThat("Listener not invoked with argument.", listener1.getCustomEvent(), is(event)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Second listener not invoked with error.", listener2.getRecievedError(), is(expected)); + assertThat("Second listener not invoked with argument.", listener2.getCustomEvent(), is(event)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithDurationArgRaiseException() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action4() { + @Override + public void call(MockEventListener mockEventListener, Long duration, TimeUnit timeUnit, String arg) { + mockEventListener.anEventWithDurationAndArg(duration, timeUnit, arg); + } + }, 1, TimeUnit.MICROSECONDS, arg); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with duration.", listener1.getDuration(), is(1L)); + assertThat("Listener not invoked with time unit.", listener1.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Listener not invoked with argument.", listener1.getArg(), is(arg)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with duration.", listener2.getDuration(), is(1L)); + assertThat("Second listener not invoked with time unit.", listener2.getTimeUnit(), is(TimeUnit.MICROSECONDS)); + assertThat("Second listener not invoked with argument.", listener2.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithArg() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action2() { + @Override + public void call(MockEventListener mockEventListener, String arg) { + mockEventListener.anEventWithArg(arg); + } + }, arg); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with argument.", listener.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithExceptionAndArg() throws Exception { + final MockEventListener listener = new MockEventListener(); + holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + final Object event = "doom"; + final Throwable expected = new NullPointerException(); + holderRule.getHolder().invokeListeners(new Action3() { + @Override + public void call(MockEventListener mockEventListener, Throwable throwable, Object arg) { + mockEventListener.onCustomEvent(arg, throwable); + } + }, expected, event); + + assertThat("Listener not invoked.", listener.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with argument.", listener.getCustomEvent(), equalTo(event)); + assertThat("Listener not invoked with exception.", listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testInvokeListenersMultiWithArg() throws Exception { + final MockEventListener listener1 = new MockEventListener(); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action2() { + @Override + public void call(MockEventListener mockEventListener, String arg) { + mockEventListener.anEventWithArg(arg); + } + }, arg); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with argument.", listener1.getArg(), is(arg)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with argument.", listener2.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testInvokeListenersWithArgAndRaiseException() throws Exception { + final MockEventListener listener1 = new MockEventListener(true); + holderRule.getHolder().subscribe(listener1); + + final MockEventListener listener2 = new MockEventListener(); + holderRule.getHolder().subscribe(listener2); + holderRule.assertListenerAdded(listener1, listener2); + + final String arg = "doom"; + holderRule.getHolder().invokeListeners(new Action2() { + @Override + public void call(MockEventListener mockEventListener, String arg) { + mockEventListener.anEventWithArg(arg); + } + }, arg); + + assertThat("Listener not invoked.", listener1.getEventInvocationCount(), is(1)); + assertThat("Listener not invoked with argument.", listener1.getArg(), is(arg)); + + assertThat("Second listener not invoked.", listener2.getEventInvocationCount(), is(1)); + assertThat("Second listener not invoked with argument.", listener2.getArg(), is(arg)); + } + + @Test(timeout = 60000) + public void testDuplicateListeners() throws Exception { + ListenerWithSub l = holderRule.addAListener(); + + holderRule.assertListenerAdded(l.listener); + + holderRule.getHolder().subscribe(l.listener); + + assertThat("Duplicate listener added.", holderRule.getHolder().getActualListenersList(), hasSize(1)); + + l.subscription.unsubscribe(); + + assertThat("Listener not removed on unsubscribe.", holderRule.getHolder().getAllListeners(), is(empty())); + } + + @Test(timeout = 60000) + public void testCopy() throws Exception { + final MockEventListener listener = new MockEventListener(); + Subscription subscription = holderRule.getHolder().subscribe(listener); + holderRule.assertListenerAdded(listener); + + final ListenersHolder copy = holderRule.getHolder().copy(); + + assertThat("Holder not copied", copy, is(not(holderRule.getHolder()))); + assertThat("Listeners list not copied", copy.getActualListenersList(), + not(sameInstance(holderRule.getHolder().getActualListenersList()))); + + final Collection allListenersCopied = copy.getAllListeners(); + + assertThat("Registered listeners not copied", allListenersCopied, contains(listener)); + + subscription.unsubscribe(); + + assertThat("Not removed from copy on unsubscribe.", copy.getAllListeners(), not(contains(listener))); + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/CookiesHolderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/CookiesHolderTest.java new file mode 100644 index 0000000..793e9ac --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/CookiesHolderTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http; + +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.junit.Test; + +import java.util.Map; +import java.util.Set; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class CookiesHolderTest { + + @Test(timeout = 60000) + public void testClientResponseHolder() throws Exception { + DefaultHttpResponse headers = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + String cookie1Name = "PREF"; + String cookie1Value = "ID=a95756377b78e75e:FF=0:TM=1392709628:LM=1392709628:S=a5mOVvTB7DBkexgi"; + String cookie1Header = cookie1Name + '=' + cookie1Value + + "; expires=Thu, 18-Feb-2016 07:47:08 GMT;"; + headers.headers().add(SET_COOKIE, cookie1Header); + + CookiesHolder holder = CookiesHolder.newClientResponseHolder(headers.headers()); + Map> cookies = holder.getAllCookies(); + + assertThat("Cookies are null.", cookies, is(notNullValue())); + assertThat("Cookies are empty.", cookies.values(), is(not(empty()))); + + Set cookies1 = cookies.get(cookie1Name); + + assertThat("No cookies found with name: " + cookie1Name, cookies1, is(notNullValue())); + assertThat("Unexpected number of cookies found.", cookies1, hasSize(1)); + + Cookie cookieFound = cookies1.iterator().next(); + + assertThat("Unexpected cookie name.", cookieFound.name(), equalTo(cookie1Name)); + assertThat("Unexpected cookie value.", cookieFound.value(), equalTo(cookie1Value)); + } + + @Test(timeout = 60000) + public void testServerRequestHolder() throws Exception { + DefaultHttpRequest headers = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); + String cookie1Name = "PREF"; + String cookie1Value = "ID=a95756377b78e75e:FF=0:TM=1392709628:LM=1392709628:S=a5mOVvTB7DBkexgi"; + Cookie cookie = new DefaultCookie(cookie1Name, cookie1Value); + + headers.headers().add(COOKIE, ClientCookieEncoder.STRICT.encode(cookie)); + + CookiesHolder holder = CookiesHolder.newServerRequestHolder(headers.headers()); + Map> cookies = holder.getAllCookies(); + + assertThat("Cookies are null.", cookies, is(notNullValue())); + assertThat("Cookies are empty.", cookies.values(), is(not(empty()))); + + Set cookies1 = cookies.get(cookie1Name); + assertThat("No cookies found with name: " + cookie1Name, cookies1, is(notNullValue())); + assertThat("Unexpected number of cookies found.", cookies1, hasSize(1)); + + Cookie cookieFound = cookies1.iterator().next(); + + assertThat("Unexpected cookie name.", cookieFound.name(), equalTo(cookie1Name)); + assertThat("Unexpected cookie value.", cookieFound.value(), equalTo(cookie1Value)); + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/EventListenerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/EventListenerTest.java new file mode 100644 index 0000000..e5ea0a3 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/EventListenerTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; +import io.reactivex.netty.protocol.http.server.HttpServer; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.net.SocketAddress; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class EventListenerTest { + + @Rule + public final HttpServerRule rule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testEventListener() throws Exception { + HttpClient client = HttpClient.newClient(rule.serverAddress); + + assertListenerCalled(client); + } + + @Test(timeout = 60000) + public void testEventListenerPostCopy() throws Exception { + HttpClient client = HttpClient.newClient(rule.serverAddress) + .enableWireLogging("test", LogLevel.ERROR); + + assertListenerCalled(client); + } + + @Test(timeout = 60000) + public void testSubscriptionPreCopy() throws Exception { + HttpClient client = HttpClient.newClient(rule.serverAddress); + + MockHttpClientEventsListener listener = subscribe(client); + + client = client.enableWireLogging("test", LogLevel.DEBUG); + + connectAndAssertListenerInvocation(client, listener); + } + + private static void assertListenerCalled(HttpClient client) { + MockHttpClientEventsListener listener = subscribe(client); + connectAndAssertListenerInvocation(client, listener); + } + + private static void connectAndAssertListenerInvocation(HttpClient client, + MockHttpClientEventsListener listener) { + TestSubscriber subscriber = new TestSubscriber<>(); + client.createGet("") + .flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse r) { + return r.getContent(); + } + }) + .take(1) + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("HTTP methods not invoked on the listener.", listener.httpListenerInvoked, is(true)); + assertThat("TCP methods not invoked on the listener.", listener.tcpListenerInvoked, is(true)); + } + + private static MockHttpClientEventsListener subscribe(HttpClient client) { + MockHttpClientEventsListener listener = new MockHttpClientEventsListener(); + client.subscribe(listener); + return listener; + } + + public static class HttpServerRule extends ExternalResource { + + private SocketAddress serverAddress; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + serverAddress = HttpServer.newServer().enableWireLogging("test", LogLevel.ERROR) + .start(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, + HttpServerResponse response) { + return response.writeString(Observable.just("Hello")); + } + }).getServerAddress(); + base.evaluate(); + } + }; + } + } + + private static class MockHttpClientEventsListener extends HttpClientEventsListener { + + private volatile boolean httpListenerInvoked; + private volatile boolean tcpListenerInvoked; + + @Override + public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { + httpListenerInvoked = true; + } + + @Override + public void onByteRead(long bytesRead) { + tcpListenerInvoked = true; + } + } + +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientPoolTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientPoolTest.java new file mode 100644 index 0000000..989949a --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientPoolTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.client.pool.FIFOIdleConnectionsHolder; +import io.reactivex.netty.client.pool.PoolConfig; +import io.reactivex.netty.client.pool.PooledConnection; +import io.reactivex.netty.protocol.http.client.internal.HttpClientResponseImpl; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.observers.TestSubscriber; + +import java.nio.channels.ClosedChannelException; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpClientPoolTest { + + @Rule + public final PooledHttpClientRule clientRule = new PooledHttpClientRule(); + + @Test(timeout = 60000) + public void testBasicAcquireRelease() throws Exception { + + clientRule.assertIdleConnections(0); + + final HttpClientRequest request1 = clientRule.getHttpClient().createGet("/"); + TestSubscriber subscriber = clientRule.sendRequestAndDiscardResponseContent(request1); + + clientRule.assertIdleConnections(0); // No idle connections post connect + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + clientRule.feedResponseAndComplete(); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + + clientRule.getLastCreatedChannel().runPendingTasks(); + + clientRule.assertIdleConnections(1); + } + + @Test(timeout = 60000) + public void testBasicAcquireReleaseWithServerClose() throws Exception { + + clientRule.assertIdleConnections(0); + + final HttpClientRequest request1 = clientRule.getHttpClient().createGet("/"); + TestSubscriber subscriber = clientRule.sendRequestAndDiscardResponseContent(request1); + + clientRule.assertIdleConnections(0); // No idle connections post connect + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + clientRule.getLastCreatedChannel().close().await(); + + subscriber.assertTerminalEvent(); + assertThat("On complete sent instead of onError", subscriber.getOnCompletedEvents(), is(empty())); + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error notification.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(ClosedChannelException.class))); + + clientRule.getLastCreatedChannel().runPendingTasks(); + + clientRule.assertIdleConnections(0); // Since, channel is closed, it should be discarded. + } + + @Test(timeout = 60000) + public void testCloseOnKeepAliveTimeout() throws Exception { + + clientRule.assertIdleConnections(0); + + final HttpClientRequest request1 = clientRule.getHttpClient().createGet("/"); + + TestSubscriber> responseSub = clientRule.sendRequest(request1); + + clientRule.assertIdleConnections(0); // No idle connections post connect + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.headers().set(HttpClientResponseImpl.KEEP_ALIVE_HEADER_NAME, + HttpClientResponseImpl.KEEP_ALIVE_TIMEOUT_HEADER_ATTR + "=0"); + clientRule.feedResponseAndComplete(response); + + HttpClientResponse resp = clientRule.discardResponseContent(responseSub); + Channel nettyChannel = resp.unsafeNettyChannel(); + + clientRule.getLastCreatedChannel().runPendingTasks(); + + // Close is while release, so this should be post running pending tasks + assertThat("Channel not closed.", nettyChannel.isOpen(), is(false)); + clientRule.assertIdleConnections(0); // Since, the channel is closed + } + + @Test(timeout = 60000) + public void testReuse() throws Exception { + clientRule.assertIdleConnections(0); + + Channel channel1 = clientRule.sendRequestAndGetChannel(); + + clientRule.getLastCreatedChannel().runPendingTasks(); + + clientRule.assertIdleConnections(1); + + Channel channel2 = clientRule.sendRequestAndGetChannel(); + + assertThat("Connection was not reused.", channel2, is(channel1)); + } + + public static class PooledHttpClientRule extends HttpClientRule { + + private FIFOIdleConnectionsHolder idleConnHolder; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + idleConnHolder = new FIFOIdleConnectionsHolder<>(); + PoolConfig pConfig = new PoolConfig<>(); + pConfig.idleConnectionsHolder(idleConnHolder); + + setupPooledConnectionFactory(pConfig); // sets the client et al. + + base.evaluate(); + } + }; + } + + public void assertIdleConnections(int expectedCount) { + TestSubscriber> testSub = new TestSubscriber<>(); + idleConnHolder.peek().subscribe(testSub); + + testSub.assertTerminalEvent(); + testSub.assertNoErrors(); + + assertThat("Unexpected number of connections in the holder.", testSub.getOnNextEvents(), + hasSize(expectedCount)); + } + + protected Channel sendRequestAndGetChannel() { + final HttpClientRequest request1 = getHttpClient().createGet("/"); + + TestSubscriber> respSub = sendRequest(request1); + + feedResponseHeaders(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), + getLastCreatedChannelWithFeeder()); + + respSub.awaitTerminalEvent(); + + assertIdleConnections(0); // No idle connections post connect + assertRequestHeadersWritten(HttpMethod.GET, "/"); + + feedResponse(new DefaultLastHttpContent()); + + final HttpClientResponse response = discardResponseContent(respSub); + + return response.unsafeNettyChannel(); + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientRule.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientRule.java new file mode 100644 index 0000000..d6ef598 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientRule.java @@ -0,0 +1,314 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.pool.PoolConfig; +import io.reactivex.netty.client.pool.SingleHostPoolingProviderFactory; +import io.reactivex.netty.test.util.embedded.EmbeddedChannelProvider; +import io.reactivex.netty.test.util.embedded.EmbeddedChannelWithFeeder; +import org.junit.Assert; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.util.List; +import java.util.regex.Pattern; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpClientRule extends ExternalResource { + + private EmbeddedChannelProvider channelProvider; + private HttpClient httpClient; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + channelProvider = new EmbeddedChannelProvider(); + httpClient = HttpClient.newClient(new InetSocketAddress(0)) + .enableWireLogging("test", LogLevel.ERROR) + .channelProvider(channelProvider.asFactory()); + base.evaluate(); + } + }; + } + + public void setupPooledConnectionFactory(final PoolConfig pConfig) { + channelProvider = new EmbeddedChannelProvider(); + httpClient = HttpClient.newClient(SingleHostPoolingProviderFactory.create(pConfig), + Observable.just(new Host(new InetSocketAddress(0)))) + .channelProvider(channelProvider.asFactory()); + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public EmbeddedChannel getLastCreatedChannel() { + return getLastCreatedChannelWithFeeder().getChannel(); + } + + public EmbeddedChannelWithFeeder getLastCreatedChannelWithFeeder() { + List createdChannels = getCreatedChannels(); + return createdChannels.get(createdChannels.size() - 1); + } + + public List getCreatedChannels() { + return channelProvider.getCreatedChannels(); + } + + public TestSubscriber> sendRequest(Observable> request) { + TestSubscriber> testSubscriber = new TestSubscriber<>(); + request.subscribe(testSubscriber); + testSubscriber.assertNoErrors(); + return testSubscriber; + } + + public TestSubscriber sendRequestAndGetContent(Observable> request) { + TestSubscriber testSubscriber = new TestSubscriber<>(); + request.flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse response) { + return response.getContent() + .map(new Func1() { + @Override + public String call(ByteBuf byteBuf) { + return byteBuf.toString(Charset.defaultCharset()); + } + }); + } + }).subscribe(testSubscriber); + testSubscriber.assertNoErrors(); + return testSubscriber; + } + + public TestSubscriber sendRequestAndDiscardResponseContent(HttpClientRequest request) { + TestSubscriber testSubscriber = new TestSubscriber<>(); + + request.flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse clientResponse) { + return clientResponse.discardContent(); + } + }).subscribe(testSubscriber); + testSubscriber.assertNoErrors(); + return testSubscriber; + } + + public TestSubscriber discardResponseContent(HttpClientResponse response) { + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + response.discardContent().subscribe(testSubscriber); + return testSubscriber; + } + + public HttpClientResponse discardResponseContent(TestSubscriber> responseSub) { + + responseSub.awaitTerminalEvent(); + responseSub.assertTerminalEvent(); + responseSub.assertNoErrors(); + + HttpClientResponse resp = responseSub.getOnNextEvents().get(0); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + resp.discardContent().subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertTerminalEvent(); + testSubscriber.assertNoErrors(); + + return resp; + } + + public void feedResponse(HttpContent... content) { + for (HttpContent httpContent : content) { + getLastCreatedChannelWithFeeder().getFeeder().addToTheFeed(httpContent); + } + } + + public void feedResponse(HttpResponse response, HttpContent content) { + getLastCreatedChannelWithFeeder().getFeeder().addToTheFeed(response, content); + } + + public void feedResponseHeaders(HttpResponse response, EmbeddedChannelWithFeeder channelWithFeeder) { + channelWithFeeder.getFeeder().addToTheFeed(response); + } + + public void feedResponseHeaders(HttpResponse response) { + feedResponseHeaders(response, getLastCreatedChannelWithFeeder()); + } + + public void feedResponseAndComplete(String... content) { + feedResponseHeaders(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), + getLastCreatedChannelWithFeeder()); + for (String contentStr : content) { + ByteBuf contentBuf = Unpooled.buffer().writeBytes(contentStr.getBytes()); + feedResponse(new DefaultHttpContent(contentBuf)); + } + + feedResponse(new DefaultLastHttpContent()); + } + + public void feedResponseAndComplete(HttpResponse response, HttpContent content) { + feedResponseAndComplete(response, content, getLastCreatedChannelWithFeeder()); + } + + public void feedResponseAndComplete(HttpResponse response, HttpContent content, + EmbeddedChannelWithFeeder channelWithFeeder) { + channelWithFeeder.getFeeder().addToTheFeed(response, content, new DefaultLastHttpContent()); + } + + public void feedResponseAndComplete(HttpResponse response) { + feedResponseAndComplete(response, getLastCreatedChannelWithFeeder()); + } + + public void feedResponseAndComplete(HttpResponse response, EmbeddedChannelWithFeeder channelWithFeeder) { + channelWithFeeder.getFeeder().addToTheFeed(response, new DefaultLastHttpContent()); + } + + public void assertRequestHeadersWritten(HttpMethod method, String uri) { + + boolean found = false; + Object outbound; + final String expectedFirstLineStart = method.name().toUpperCase() + ' ' + uri; + String data = null; + + while ((outbound = getLastCreatedChannel().readOutbound()) != null) { + if (outbound instanceof ByteBuf) { + ByteBuf bb = (ByteBuf) outbound; + data = bb.toString(Charset.defaultCharset()); + if (data.startsWith(expectedFirstLineStart)) { + found = true; + break; + } + } + } + + assertThat("Unexpected HTTP method & URI for the written request.", data, + startsWith(expectedFirstLineStart)); + + if (!found) { + assertThat("Request not written.", outbound, is(notNullValue())); + } + } + + public void assertContentWritten(String contentStr) { + boolean found = false; + Object outbound; + String data = null; + + while ((outbound = getLastCreatedChannel().readOutbound()) != null) { + if (outbound instanceof ByteBuf) { + ByteBuf bb = (ByteBuf) outbound; + data = bb.toString(Charset.defaultCharset()); + if (data.equalsIgnoreCase(contentStr)) { + found = true; + break; + } + } + } + + assertThat("Unexpected HTTP content.", data, equalToIgnoringCase(contentStr)); + + if (!found) { + assertThat("Content not written.", outbound, is(notNullValue())); + } + } + + public void assertEmptyBodyWithContentLengthZero() { + assertBodyWithContentLength(0, ""); + } + + public void assertBodyWithContentLength(int contentLength, String body) { + Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE); + Object outbound; + String data = ""; + + while ((outbound = getLastCreatedChannel().readOutbound()) != null) { + if (outbound instanceof ByteBuf) { + ByteBuf bb = (ByteBuf) outbound; + data += bb.toString(Charset.defaultCharset()); + } + } + + if (!data.contains("content-length: " + contentLength + "\r\n")) { + Assert.fail("Missing header 'content-length: " + contentLength + "'"); + } + if (data.contains("transfer-encoding: chunked\r\n")) { + Assert.fail("Unexpected header 'transfer-encoding: chunked'"); + } + if (!headerBlock.matcher(data).replaceFirst("").equals(body)) { + Assert.fail("Unexpected body content '" + headerBlock.matcher(data).replaceFirst("") + "'"); + } + } + + public void assertEmptyBodyWithSingleChunk() { + assertChunks(); + } + + public void assertChunks(String... chunks) { + Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE); + Object outbound; + String data = ""; + + while ((outbound = getLastCreatedChannel().readOutbound()) != null) { + if (outbound instanceof ByteBuf) { + ByteBuf bb = (ByteBuf) outbound; + data += bb.toString(Charset.defaultCharset()); + } + } + + if (data.contains("content-length: 0\r\n")) { + Assert.fail("Unexpected header 'content-length: 0'"); + } + if (!data.contains("transfer-encoding: chunked\r\n")) { + Assert.fail("Missing header 'transfer-encoding: chunked'"); + } + String expectedChunkContent = ""; + for (String c : chunks) { + expectedChunkContent += c.getBytes().length + "\r\n"; + expectedChunkContent += c + "\r\n"; + } + expectedChunkContent += "0\r\n\r\n"; + if (!headerBlock.matcher(data).replaceFirst("").equals(expectedChunkContent)) { + Assert.fail("Unexpected body content '" + headerBlock.matcher(data).replaceFirst("") + "'"); + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientTest.java new file mode 100644 index 0000000..633b4fc --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpClientTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.timeout.ReadTimeoutException; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.pool.SingleHostPoolingProviderFactory; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.HttpServerRule; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; +import rx.functions.Func0; +import rx.observers.TestSubscriber; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpClientTest { + + @Rule + public final HttpClientRule clientRule = new HttpClientRule(); + + @Rule + public final HttpServerRule serverRule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testCloseOnResponseComplete() throws Exception { + + HttpClientRequest request = clientRule.getHttpClient().createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndDiscardResponseContent(request); + + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + clientRule.feedResponseAndComplete(nettyResponse); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + assertThat("Channel not closed after response completion.", clientRule.getLastCreatedChannel().isOpen(), is(false)); + } + + @Test(timeout = 60000) + public void testResponseContent() throws Exception { + + HttpClientRequest request = clientRule.getHttpClient().createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndGetContent(request); + + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + final String content = "Hello"; + clientRule.feedResponseAndComplete(content); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + assertThat("Unexpected response content count.", testSubscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected response content.", testSubscriber.getOnNextEvents(), contains(content)); + } + + @Test(timeout = 60000) + public void testResponseContentMultipleChunks() throws Exception { + + HttpClientRequest request = clientRule.getHttpClient().createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndGetContent(request); + + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + final String content1 = "Hello1"; + final String content2 = "Hello2"; + clientRule.feedResponseAndComplete(content1, content2); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + assertThat("Unexpected response content count.", testSubscriber.getOnNextEvents(), hasSize(2)); + assertThat("Unexpected response content.", testSubscriber.getOnNextEvents(), contains(content1, content2)); + } + + @Test(timeout = 60000) + public void testAggregatedContent() throws Exception { + + HttpClientRequest request = clientRule.getHttpClient() + .addChannelHandlerLast("aggregator", new Func0() { + @Override + public ChannelHandler call() { + return new HttpObjectAggregator(1024); + } + }) + .createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndGetContent(request); + + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + final String content1 = "Hello1"; + final String content2 = "Hello2"; + clientRule.feedResponseAndComplete(content1, content2); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + assertThat("Unexpected response content count.", testSubscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected response content.", testSubscriber.getOnNextEvents().get(0), + containsString(content1)); + assertThat("Unexpected response content.", testSubscriber.getOnNextEvents().get(0), + containsString(content2)); + } + + @Test(timeout = 60000) + public void testNoContentSubscribe() throws Exception { + HttpClientRequest request = clientRule.getHttpClient().createGet("/"); + + TestSubscriber> testSubscriber = clientRule.sendRequest(request); + clientRule.assertRequestHeadersWritten(HttpMethod.GET, "/"); + + clientRule.feedResponseHeaders(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)); + + testSubscriber.assertTerminalEvent(); + } + + @Test(timeout = 60000) + public void testPost() throws Exception { + String contentStr = "Hello"; + Observable> request = clientRule.getHttpClient() + .createPost("/") + .writeStringContent(Observable.just(contentStr)); + + TestSubscriber testSubscriber = clientRule.sendRequestAndGetContent(request); + + clientRule.assertRequestHeadersWritten(HttpMethod.POST, "/"); + clientRule.assertContentWritten(contentStr); + + clientRule.feedResponseAndComplete(); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + assertThat("Unexpected response content count.", testSubscriber.getOnNextEvents(), is(empty())); + } + + @Test(timeout = 60000) + public void testReadTimeoutNoPooling() throws Exception { + + startServerThatNeverReplies(); + + HttpClientRequest request = HttpClient.newClient(serverRule.getServerAddress()) + .readTimeOut(1, TimeUnit.SECONDS) + .createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndDiscardResponseContent(request); + + testSubscriber.awaitTerminalEvent(); + + assertThat("On complete invoked, instead of error.", testSubscriber.getOnCompletedEvents(), is(empty())); + assertThat("Unexpected onError count.", testSubscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected exception.", testSubscriber.getOnErrorEvents().get(0), + is(instanceOf(ReadTimeoutException.class))); + } + + @Test(timeout = 60000) + public void testReadTimeoutWithPooling() throws Exception { + + startServerThatNeverReplies(); + + HttpClientRequest request = + HttpClient.newClient(SingleHostPoolingProviderFactory.createUnbounded(), + Observable.just(new Host(serverRule.getServerAddress()))) + .readTimeOut(1, TimeUnit.SECONDS) + .createGet("/"); + + TestSubscriber testSubscriber = clientRule.sendRequestAndDiscardResponseContent(request); + + testSubscriber.awaitTerminalEvent(); + + assertThat("On complete invoked, instead of error.", testSubscriber.getOnCompletedEvents(), is(empty())); + assertThat("Unexpected onError count.", testSubscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected exception.", testSubscriber.getOnErrorEvents().get(0), + is(instanceOf(ReadTimeoutException.class))); + } + + @Test(timeout = 60000) + public void testRequestWithNoContentLengthHeaderOrContentReturnsEmptyBody() { + clientRule.sendRequest(clientRule.getHttpClient().createGet("/")); + clientRule.assertEmptyBodyWithContentLengthZero(); + } + + @Test(timeout = 60000) + public void testRequestWithNoContentLengthHeaderAndContentReturnsContentChunkAndSingleEmptyChunk() { + clientRule.sendRequest(clientRule.getHttpClient().createGet("/") + .writeStringContent(Observable.just("Hello"))); + clientRule.assertChunks("Hello"); + } + + @Test(timeout = 60000) + public void testRequestWithContentLengthReturnsRawBody() { + clientRule.sendRequest(clientRule.getHttpClient().createGet("/") + .setHeader(HttpHeaderNames.CONTENT_LENGTH, 5) + .writeStringContent(Observable.just("Hello"))); + clientRule.assertBodyWithContentLength(5, "Hello"); + } + + @Test(timeout = 60000) + public void testRequestWithZeroContentLengthReturnsEmptyBody() { + clientRule.sendRequest(clientRule.getHttpClient().createGet("/").setHeader(HttpHeaderNames.CONTENT_LENGTH, 0)); + clientRule.assertEmptyBodyWithContentLengthZero(); + } + + @Test(timeout = 60000) + public void testRequestWithOnlyPositiveContentLengthReturnsEmptyBody() { + clientRule.sendRequest(clientRule.getHttpClient().createGet("/").setHeader(HttpHeaderNames.CONTENT_LENGTH, 5)); + clientRule.assertEmptyBodyWithContentLengthZero(); + } + + protected void startServerThatNeverReplies() { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return Observable.never(); + } + }); + } + + @Test(timeout = 60000) + public void testLargeHeaders() throws Exception { + + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpRedirectTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpRedirectTest.java new file mode 100644 index 0000000..508c044 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/HttpRedirectTest.java @@ -0,0 +1,274 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.client.pool.PoolConfig; +import io.reactivex.netty.test.util.embedded.EmbeddedChannelWithFeeder; +import org.junit.Rule; +import org.junit.Test; +import rx.observers.TestSubscriber; + +import java.util.List; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpRedirectTest { + + @Rule + public final HttpClientRule clientRule = new HttpClientRule(); + + @Test(timeout = 60000) + public void testNoLocation() throws Exception { + + final String requestUri = "/"; + + TestSubscriber> subscriber = sendRequest(requestUri); + + assertRequestWritten(requestUri); + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.SEE_OTHER); + clientRule.feedResponseAndComplete(response); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + + } + + @Test(timeout = 60000) + public void testInvalidRedirectLocation() throws Exception { + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(requestUri); + + assertRequestWritten(requestUri); + sendRedirects(" "); // blank is an invalid URI + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + + } + + @Test(timeout = 60000) + public void testTooManyRedirect() throws Throwable { + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(requestUri); + + assertRequestWritten(requestUri); + sendRedirects("/blah", "/blah"); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + } + + @Test(timeout = 60000) + public void testRedirectLoop() throws Throwable { + + final String requestUri = "/blah"; + TestSubscriber> subscriber = sendRequest(requestUri); + + assertRequestWritten(requestUri); + sendRedirects(requestUri); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + } + + @Test(timeout = 60000) + public void testAbsoluteRedirect() throws Throwable { + + final String requestUri = "/blah"; + TestSubscriber> subscriber = sendRequest(requestUri); + + assertRequestWritten(requestUri); + sendRedirects("http://localhost:8888/blah"); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected onNext notifications count.", subscriber.getOnNextEvents(), hasSize(1)); + HttpClientResponse response = subscriber.getOnNextEvents().get(0); + assertThat("Unexpected response.", response, is(notNullValue())); + assertThat("Unexpected response status.", response.getStatus().code(), is(HttpResponseStatus.SEE_OTHER.code())); + } + + @Test(timeout = 60000) + public void testRedirectNoConnPool() throws Throwable { + + final String requestUri = "/"; + + HttpClient client = clientRule.getHttpClient().followRedirects(1); + TestSubscriber> subscriber = sendRequest(client, requestUri); + + assertRequestWritten(requestUri); + sendRedirects("/blah", "/blah"); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + } + + @Test(timeout = 60000) + public void testRedirectWithConnPool() throws Throwable { + PoolConfig pConfig = new PoolConfig().maxConnections(10); + + clientRule.setupPooledConnectionFactory(pConfig); // sets the client et al. + + HttpClient client = clientRule.getHttpClient().followRedirects(1); + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(client, requestUri); + assertRequestWritten(requestUri); + + sendRedirects("/blah", "blah"); + + subscriber.awaitTerminalEvent(); + + assertThat("Unexpected error notifications count.", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Unexpected error.", subscriber.getOnErrorEvents().get(0), + is(instanceOf(HttpRedirectException.class))); + } + + @Test(timeout = 60000) + public void testNoRedirect() { + HttpClient client = clientRule.getHttpClient().followRedirects(false); + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(client, requestUri); + + assertRequestWritten(requestUri); + sendRedirects("/blah2"); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected onNext notifications count.", subscriber.getOnNextEvents(), hasSize(1)); + HttpClientResponse response = subscriber.getOnNextEvents().get(0); + assertThat("Unexpected response.", response, is(notNullValue())); + assertThat("Unexpected response status.", response.getStatus().code(), is(HttpResponseStatus.SEE_OTHER.code())); + } + + @Test(timeout = 60000) + public void testRedirectPost() throws Throwable { + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(HttpMethod.POST, requestUri); + + final HttpResponseStatus responseStatus = HttpResponseStatus.FOUND; + + clientRule.assertRequestHeadersWritten(HttpMethod.POST, requestUri); + sendRedirects(responseStatus, "/blah"); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected onNext notifications count.", subscriber.getOnNextEvents(), hasSize(1)); + HttpClientResponse response = subscriber.getOnNextEvents().get(0); + assertThat("Unexpected response.", response, is(notNullValue())); + assertThat("Unexpected response status.", response.getStatus().code(), is(responseStatus.code())); + } + + @Test(timeout = 60000) + public void testRedirectPostWith303() throws Throwable { + + final String requestUri = "/"; + TestSubscriber> subscriber = sendRequest(HttpMethod.POST, requestUri); + + clientRule.assertRequestHeadersWritten(HttpMethod.POST, requestUri); + sendRedirects(HttpResponseStatus.SEE_OTHER, "/blah"); + + sendResponse(HttpResponseStatus.OK); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected onNext notifications count.", subscriber.getOnNextEvents(), hasSize(1)); + HttpClientResponse response = subscriber.getOnNextEvents().get(0); + assertThat("Unexpected response.", response, is(notNullValue())); + assertThat("Unexpected response status.", response.getStatus().code(), is(HttpResponseStatus.OK.code())); + } + + private static TestSubscriber> sendRequest(HttpClient client, + HttpMethod method, String uri) { + final HttpClientRequest req = client.createRequest(method, uri); + TestSubscriber> subscriber = new TestSubscriber<>(); + req.subscribe(subscriber); + subscriber.assertNoErrors(); + return subscriber; + } + + private void assertRequestWritten(String uri) { + clientRule.assertRequestHeadersWritten(HttpMethod.GET, uri); + } + + private static TestSubscriber> sendRequest(HttpClient client, + String uri) { + return sendRequest(client, HttpMethod.GET, uri); + } + + private TestSubscriber> sendRequest(String uri) { + return sendRequest(clientRule.getHttpClient().followRedirects(1), uri); + } + + private TestSubscriber> sendRequest(HttpMethod method, String uri) { + return sendRequest(clientRule.getHttpClient().followRedirects(1), method, uri); + } + + private void sendRedirects(String... locations) { + sendRedirects(HttpResponseStatus.SEE_OTHER, locations); + } + + private void sendRedirects(HttpResponseStatus redirectStatus, String... locations) { + + for (int i = 0; i < locations.length; i++) { + List createdChannels = clientRule.getCreatedChannels(); + assertThat("Not enough channels created by the embedded factory.", createdChannels, + hasSize(greaterThanOrEqualTo(i + 1))); + String location = locations[i]; + EmbeddedChannelWithFeeder channelWithFeeder = createdChannels.get(i); + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, redirectStatus); + response.headers().set(LOCATION, location); + clientRule.feedResponseAndComplete(response, channelWithFeeder); + } + } + + private void sendResponse(HttpResponseStatus redirectStatus) { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, redirectStatus); + clientRule.feedResponseAndComplete(response); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/RedirectOperatorTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/RedirectOperatorTest.java new file mode 100644 index 0000000..b39e9fb --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/RedirectOperatorTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.client; + +public class RedirectOperatorTest { +/* + + @Test + public void testMaxRedirects() throws Exception { + + Setup setup = new Setup().setup(HttpResponseStatus.TEMPORARY_REDIRECT); + + Assert.assertEquals("Unexpected redirect count.", 2, setup.getHandler().getRedirectsRequested()); + Assert.assertEquals("Unexpected onComplete calls to redirect subscriber.", 0, setup.getSubscriber().getOnCompletes()); + Assert.assertEquals("Unexpected onNext calls to redirect subscriber.", 0, setup.getSubscriber().getOnNexts()); + Assert.assertEquals("Unexpected onError calls to redirect subscriber.", 1, setup.getSubscriber().getOnErrors()); + } + + @Test + public void testRedirect() throws Exception { + + Setup setup = new Setup().setup(HttpResponseStatus.OK); + + Assert.assertEquals("Unexpected redirect count.", 1, setup.getHandler().getRedirectsRequested()); + Assert.assertEquals("Unexpected onComplete calls to redirect subscriber.", 1, setup.getSubscriber().getOnCompletes()); + Assert.assertEquals("Unexpected onNext calls to redirect subscriber.", 1, setup.getSubscriber().getOnNexts()); + Assert.assertEquals("Unexpected onError calls to redirect subscriber.", 0, setup.getSubscriber().getOnErrors()); + } + + private static class TestableRedirectHandler implements RedirectOperator.RedirectHandler { + + private final HttpClientResponse response; + private final int maxHops; + private final AtomicInteger redirectsRequested = new AtomicInteger(); + + public TestableRedirectHandler(int maxHops, HttpResponseStatus redirectResponseStatus) { + this.maxHops = maxHops; + DefaultHttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, redirectResponseStatus); + response = new HttpClientResponse(nettyResponse, UnicastContentSubject.createWithoutNoSubscriptionTimeout()); + } + + public TestableRedirectHandler(int maxHops) { + this(maxHops, HttpResponseStatus.TEMPORARY_REDIRECT); + } + + @Override + public Observable> doRedirect(RedirectionContext context, + HttpClientRequest originalRequest, + HttpClient.HttpClientConfig config) { + redirectsRequested.incrementAndGet(); + return Observable.just(response); + } + + @Override + public boolean requiresRedirect(RedirectionContext context, HttpClientResponse response) { + return response.getStatus() == HttpResponseStatus.TEMPORARY_REDIRECT; + } + + @Override + public void validate(RedirectionContext context, HttpClientResponse redirectResponse) { + if(context.getRedirectCount() >= maxHops) { + throw new HttpRedirectException(HttpRedirectException.Reason.TooManyRedirects, ""); + } + } + + private int getRedirectsRequested() { + return redirectsRequested.get(); + } + } + + private static class UnsafeRedirectSubscriber extends Subscriber> { + private final AtomicInteger onCompletes; + private final CountDownLatch completeLatch; + private final AtomicInteger onErrors; + private final AtomicInteger onNexts; + + public UnsafeRedirectSubscriber() { + onCompletes = new AtomicInteger(); + completeLatch = new CountDownLatch(1); + onErrors = new AtomicInteger(); + onNexts = new AtomicInteger(); + } + + @Override + public void onCompleted() { + onCompletes.incrementAndGet(); + completeLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + onErrors.incrementAndGet(); + completeLatch.countDown(); + } + + @Override + public void onNext(HttpClientResponse response) { + onNexts.incrementAndGet(); + } + + public int getOnCompletes() { + return onCompletes.get(); + } + + public int getOnErrors() { + return onErrors.get(); + } + + public int getOnNexts() { + return onNexts.get(); + } + + public void waitForCompletion(int time, TimeUnit unit) throws InterruptedException { + completeLatch.await(time, unit); + } + } + + private static class Setup { + + private TestableRedirectHandler handler; + private UnsafeRedirectSubscriber subscriber; + + public TestableRedirectHandler getHandler() { + return handler; + } + + public UnsafeRedirectSubscriber getSubscriber() { + return subscriber; + } + + public Setup setup(HttpResponseStatus redirectStatus) throws InterruptedException { + DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); + HttpClientRequest request = new HttpClientRequest(nettyRequest); + DefaultHttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.TEMPORARY_REDIRECT); + final HttpClientResponse response = + new HttpClientResponse(nettyResponse, + UnicastContentSubject .createWithoutNoSubscriptionTimeout()); + handler = new TestableRedirectHandler(2, redirectStatus); + + subscriber = new UnsafeRedirectSubscriber(); + Observable.just(response) + .lift(new RedirectOperator(request, handler)) + .unsafeSubscribe(subscriber); + + subscriber.waitForCompletion(1, TimeUnit.MINUTES); + return this; + } + } +*/ +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisherTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisherTest.java new file mode 100644 index 0000000..d305c15 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventPublisherTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.events; + +import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListenerImpl.HttpEvent; +import io.reactivex.netty.test.util.MockClientEventListener.ClientEvent; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import static java.util.concurrent.TimeUnit.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpClientEventPublisherTest { + + @Rule + public final PublisherRule rule = new PublisherRule(); + + @Test(timeout = 60000) + public void testOnRequestSubmitted() throws Exception { + rule.publisher.onRequestSubmitted(); + rule.listener.assertMethodCalled(HttpEvent.ReqSubmitted); + } + + @Test(timeout = 60000) + public void testOnRequestWriteStart() throws Exception { + rule.publisher.onRequestWriteStart(); + rule.listener.assertMethodCalled(HttpEvent.ReqWriteStart); + } + + @Test(timeout = 60000) + public void testOnRequestWriteComplete() throws Exception { + rule.publisher.onRequestWriteComplete(1, MILLISECONDS); + rule.listener.assertMethodCalled(HttpEvent.ReqWriteSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnRequestWriteFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onRequestWriteFailed(1, MILLISECONDS, expected); + rule.listener.assertMethodCalled(HttpEvent.ReqWriteFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnResponseHeadersReceived() throws Exception { + rule.publisher.onResponseHeadersReceived(200, 1, MILLISECONDS); + rule.listener.assertMethodCalled(HttpEvent.ResHeadersReceived); + + assertThat("Listener not called with response code.", rule.listener.getResponseCode(), is(200)); + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnResponseContentReceived() throws Exception { + rule.publisher.onResponseContentReceived(); + rule.listener.assertMethodCalled(HttpEvent.ResContentReceived); + } + + @Test(timeout = 60000) + public void testOnResponseReceiveComplete() throws Exception { + rule.publisher.onResponseReceiveComplete(1, MILLISECONDS); + rule.listener.assertMethodCalled(HttpEvent.ResReceiveComplete); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnResponseFailed() throws Exception { + final Throwable expected = new NullPointerException(); + rule.publisher.onResponseFailed(expected); + rule.listener.assertMethodCalled(HttpEvent.RespFailed); + + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnRequestProcessingComplete() throws Exception { + rule.publisher.onRequestProcessingComplete(1, MILLISECONDS); + rule.listener.assertMethodCalled(HttpEvent.ProcessingComplete); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseFailed() throws Exception { + final Throwable expected = new NullPointerException(); + rule.publisher.onConnectionCloseFailed(1, MILLISECONDS, expected); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CloseFailed); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectStart() throws Exception { + rule.publisher.onConnectStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ConnectStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectSuccess() throws Exception { + rule.publisher.onConnectSuccess(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ConnectSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectFailed() throws Exception { + rule.publisher.onConnectFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ConnectFailed); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolReleaseStart() throws Exception { + rule.publisher.onPoolReleaseStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ReleaseStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolReleaseSuccess() throws Exception { + rule.publisher.onPoolReleaseSuccess(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ReleaseSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolReleaseFailed() throws Exception { + rule.publisher.onPoolReleaseFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.ReleaseFailed); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPooledConnectionEviction() throws Exception { + rule.publisher.onPooledConnectionEviction(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.Eviction); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPooledConnectionReuse() throws Exception { + rule.publisher.onPooledConnectionReuse(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.Reuse); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolAcquireStart() throws Exception { + rule.publisher.onPoolAcquireStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.AcquireStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolAcquireSuccess() throws Exception { + rule.publisher.onPoolAcquireSuccess(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.AcquireSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnPoolAcquireFailed() throws Exception { + rule.publisher.onPoolAcquireFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().assertMethodsCalled(ClientEvent.AcquireFailed); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnByteRead() throws Exception { + rule.publisher.onByteRead(1); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.BytesRead); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnByteWritten() throws Exception { + rule.publisher.onByteWritten(1); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.BytesWritten); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushStart() throws Exception { + rule.publisher.onFlushStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.FlushStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushSuccess() throws Exception { + rule.publisher.onFlushComplete(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.FlushSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteStart() throws Exception { + rule.publisher.onWriteStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.WriteStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteSuccess() throws Exception { + rule.publisher.onWriteSuccess(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.WriteSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteFailed() throws Exception { + rule.publisher.onWriteFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.WriteFailed); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseStart() throws Exception { + rule.publisher.onConnectionCloseStart(); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CloseStart); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseSuccess() throws Exception { + rule.publisher.onConnectionCloseSuccess(1, MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CloseSuccess); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEvent() throws Exception { + rule.publisher.onCustomEvent("Hello"); + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CustomEvent); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithError() throws Exception { + rule.publisher.onCustomEvent("Hello", new NullPointerException()); + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CustomEventWithError); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDuration() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, MINUTES); + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CustomEventWithDuration); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDurationAndError() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, MINUTES, new NullPointerException()); + rule.listener.getTcpDelegate().assertMethodsCalled(Event.CustomEventWithDurationAndError); // Test for TCP should verify rest + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + assertThat("Publishing not enabled.", rule.publisher.publishingEnabled(), is(true)); + } + + public static class PublisherRule extends ExternalResource { + + private HttpClientEventsListenerImpl listener; + private HttpClientEventPublisher publisher; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + listener = new HttpClientEventsListenerImpl(); + publisher = new HttpClientEventPublisher(); + publisher.subscribe(listener); + base.evaluate(); + } + }; + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListenerImpl.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListenerImpl.java new file mode 100644 index 0000000..71af502 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsListenerImpl.java @@ -0,0 +1,274 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.events; + +import io.reactivex.netty.test.util.MockClientEventListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpClientEventsListenerImpl extends HttpClientEventsListener { + + public enum HttpEvent { + ReqSubmitted, ReqWriteStart, ReqWriteSuccess, ReqWriteFailed, ResHeadersReceived, ResContentReceived, + ResReceiveComplete, RespFailed, ProcessingComplete + } + + private final MockClientEventListener tcpDelegate; + + private int responseCode; + private long duration; + private TimeUnit timeUnit; + private Throwable recievedError; + private final List methodsCalled = new ArrayList<>(); + + public HttpClientEventsListenerImpl() { + tcpDelegate = new MockClientEventListener(); + } + + @Override + public void onRequestSubmitted() { + methodsCalled.add(HttpEvent.ReqSubmitted); + } + + @Override + public void onRequestWriteStart() { + methodsCalled.add(HttpEvent.ReqWriteStart); + } + + @Override + public void onRequestWriteComplete(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.ReqWriteSuccess); + } + + @Override + public void onRequestWriteFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + methodsCalled.add(HttpEvent.ReqWriteFailed); + } + + @Override + public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { + this.responseCode = responseCode; + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.ResHeadersReceived); + } + + @Override + public void onResponseContentReceived() { + methodsCalled.add(HttpEvent.ResContentReceived); + } + + @Override + public void onResponseReceiveComplete(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.ResReceiveComplete); + } + + @Override + public void onResponseFailed(Throwable recievedError) { + this.recievedError = recievedError; + methodsCalled.add(HttpEvent.RespFailed); + } + + @Override + public void onRequestProcessingComplete(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.ProcessingComplete); + } + + public int getResponseCode() { + return responseCode; + } + + public Throwable getRecievedError() { + return recievedError; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public long getDuration() { + return duration; + } + + @Override + public void onConnectStart() { + tcpDelegate.onConnectStart(); + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectSuccess(duration, timeUnit); + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onConnectFailed(duration, timeUnit, throwable); + } + + @Override + public void onPoolReleaseStart() { + tcpDelegate.onPoolReleaseStart(); + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onPoolReleaseSuccess(duration, timeUnit); + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onPoolReleaseFailed(duration, timeUnit, throwable); + } + + @Override + public void onPooledConnectionEviction() { + tcpDelegate.onPooledConnectionEviction(); + } + + @Override + public void onPooledConnectionReuse() { + tcpDelegate.onPooledConnectionReuse(); + } + + @Override + public void onPoolAcquireStart() { + tcpDelegate.onPoolAcquireStart(); + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onPoolAcquireSuccess(duration, timeUnit); + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onPoolAcquireFailed(duration, timeUnit, throwable); + } + + @Override + public void onByteRead(long bytesRead) { + tcpDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + tcpDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onFlushStart() { + tcpDelegate.onFlushStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + tcpDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onWriteStart() { + tcpDelegate.onWriteStart(); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onConnectionCloseStart() { + tcpDelegate.onConnectionCloseStart(); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable throwable) { + tcpDelegate.onConnectionCloseFailed(duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event) { + tcpDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + tcpDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + tcpDelegate.onCustomEvent(event, throwable); + } + + @Override + public void onCompleted() { + tcpDelegate.onCompleted(); + } + + public void assertMethodCalled(HttpEvent... events) { + assertThat("Unexpected methods called count.", methodsCalled, hasSize(events.length)); + assertThat("Unexpected methods called.", methodsCalled, contains(events)); + } + + public MockClientEventListener getTcpDelegate() { + return tcpDelegate; + } + + @Override + public String toString() { + return "HttpClientEventsListenerImpl{" + + "tcpDelegate=" + tcpDelegate + + ", responseCode=" + responseCode + + ", duration=" + duration + + ", timeUnit=" + timeUnit + + ", recievedError=" + recievedError + + ", methodsCalled=" + methodsCalled + + '}'; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsTest.java new file mode 100644 index 0000000..95f7427 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/events/HttpClientEventsTest.java @@ -0,0 +1,66 @@ +package io.reactivex.netty.protocol.http.client.events; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.pool.SingleHostPoolingProviderFactory; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.server.HttpServerRule; +import io.reactivex.netty.test.util.MockClientEventListener.ClientEvent; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; + +public class HttpClientEventsTest { + + @Rule + public final HttpServerRule serverRule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testEventsPublished() throws Exception { + HttpClientEventsListenerImpl listener = sendRequests(false); + + listener.getTcpDelegate().assertMethodCalled(ClientEvent.ConnectStart); + listener.getTcpDelegate().assertMethodCalled(ClientEvent.ConnectSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.WriteStart); + listener.getTcpDelegate().assertMethodCalled(Event.WriteSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.FlushStart); + listener.getTcpDelegate().assertMethodCalled(Event.FlushSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.BytesRead); + } + + @Test(timeout = 60000) + public void testPooledEventsPublished() throws Exception { + HttpClientEventsListenerImpl listener = sendRequests(true); + + listener.getTcpDelegate().assertMethodCalled(ClientEvent.AcquireStart); + listener.getTcpDelegate().assertMethodCalled(ClientEvent.AcquireSuccess); + listener.getTcpDelegate().assertMethodCalled(ClientEvent.ConnectStart); + listener.getTcpDelegate().assertMethodCalled(ClientEvent.ConnectSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.WriteStart); + listener.getTcpDelegate().assertMethodCalled(Event.WriteSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.FlushStart); + listener.getTcpDelegate().assertMethodCalled(Event.FlushSuccess); + listener.getTcpDelegate().assertMethodCalled(Event.BytesRead); + } + + protected HttpClientEventsListenerImpl sendRequests(boolean pool) { + serverRule.startServer(); + HttpClientEventsListenerImpl listener = new HttpClientEventsListenerImpl(); + if (pool) { + SingleHostPoolingProviderFactory provider = + SingleHostPoolingProviderFactory.createBounded(10); + Host host = new Host(serverRule.getServerAddress()); + serverRule.setupClient(HttpClient.newClient(provider, Observable.just(host))); + + } + serverRule.getClient().subscribe(listener); + HttpClientRequest request = serverRule.getClient().createGet("/"); + + HttpClientResponse resp = serverRule.sendRequest(request); + serverRule.assertResponseContent(resp); + return listener; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImplTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImplTest.java new file mode 100644 index 0000000..5048b50 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/client/internal/HttpClientRequestImplTest.java @@ -0,0 +1,940 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.client.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.channel.ConnectionInputSubscriberEvent; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.protocol.http.TrailingHeaders; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; +import io.reactivex.netty.test.util.MockEventPublisher; +import io.reactivex.netty.test.util.FlushSelector; +import io.reactivex.netty.test.util.TcpConnectionRequestMock; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.observers.TestSubscriber; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.concurrent.atomic.AtomicInteger; + +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.*; +import static org.mockito.Matchers.*; + +public class HttpClientRequestImplTest { + + @Rule + public final RequestRule requestRule = new RequestRule(); + + @Test(timeout = 60000) + public void testWriteContent() throws Exception { + Observable content = Observable.just("Hello"); + Observable> newReq = requestRule.request.writeContent(content); + + requestRule.assertContentWrite(content, newReq); + } + + @Test(timeout = 60000) + public void testWriteContentAndFlushOnEach() throws Exception { + Observable content = Observable.just("Hello"); + Observable> newReq = requestRule.request.writeContentAndFlushOnEach(content); + + requestRule.assertContentWriteAndFlushOnEach(content, newReq); + } + + @Test(timeout = 60000) + public void testWriteStringContent() throws Exception { + Observable content = Observable.just("Hello"); + Observable> newReq = requestRule.request.writeStringContent(content); + requestRule.assertContentWrite(content, newReq); + } + + @Test(timeout = 60000) + public void testWriteBytesContent() throws Exception { + Observable content = Observable.just("Hello".getBytes()); + Observable> newReq = requestRule.request.writeBytesContent(content); + requestRule.assertContentWrite(content, newReq); + } + + @Test(timeout = 60000) + public void testWriteContentWithFlushSelector() throws Exception { + Observable content = Observable.just("Hello"); + FlushSelector flushSelector = new FlushSelector<>(5); + Observable> newReq = requestRule.request.writeContent(content, flushSelector); + + requestRule.assertContentWrite(content, newReq, flushSelector); + } + + @Test(timeout = 60000) + public void testWriteStringContentWithFlushSelector() throws Exception { + Observable content = Observable.just("Hello"); + FlushSelector flushSelector = new FlushSelector<>(5); + Observable> newReq = requestRule.request.writeStringContent(content, flushSelector); + + requestRule.assertContentWrite(content, newReq, flushSelector); + } + + @Test(timeout = 60000) + public void testWriteBytesContentWithFlushSelector() throws Exception { + Observable content = Observable.just("Hello".getBytes()); + FlushSelector flushSelector = new FlushSelector<>(5); + Observable> newReq = requestRule.request.writeBytesContent(content, flushSelector); + + requestRule.assertContentWrite(content, newReq, flushSelector); + } + + @Test(timeout = 60000) + public void testWriteContentWithTrailer() throws Exception { + Observable content = Observable.just("Hello"); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeContent(content, tFactory, tMutator); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator); + } + + @Test(timeout = 60000) + public void testWriteStringContentWithTrailer() throws Exception { + Observable content = Observable.just("Hello"); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeStringContent(content, tFactory, + tMutator); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator); + } + + @Test(timeout = 60000) + public void testWriteBytesContentWithTrailer() throws Exception { + Observable content = Observable.just("Hello".getBytes()); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeBytesContent(content, tFactory, + tMutator); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator); + } + + @Test(timeout = 60000) + public void testWriteContentWithTrailerAndSelector() throws Exception { + Observable content = Observable.just("Hello".getBytes()); + FlushSelector selector = new FlushSelector<>(1); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeContent(content, tFactory, tMutator, + selector); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator, 2/*One for content & one for trailer*/); + } + + @Test(timeout = 60000) + public void testWriteStringContentWithTrailerAndSelector() throws Exception { + Observable content = Observable.just("Hello"); + FlushSelector selector = new FlushSelector<>(1); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeStringContent(content, tFactory, + tMutator, selector); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator, 2/*One for content & one for trailer*/); + } + + @Test(timeout = 60000) + public void testWriteBytesContentWithTrailerAndSelector() throws Exception { + Observable content = Observable.just("Hello".getBytes()); + FlushSelector selector = new FlushSelector<>(1); + TestTrailerFactory tFactory = requestRule.newTrailerFactory(); + TestTrailerMutator tMutator = requestRule.newTrailerMutator(); + Observable> newReq = requestRule.request.writeBytesContent(content, tFactory, + tMutator, selector); + + requestRule.assertContentWrite(content, newReq, tFactory, tMutator, 2/*One for content & one for trailer*/); + } + + @Test(timeout = 60000) + public void testAddHeader() throws Exception { + final String headerName = "Foo"; + final String headerVal = "bar"; + + HttpClientRequestImpl newReq = + requestRule.request.addHeader(headerName, headerVal); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal); + } + + @Test(timeout = 60000) + public void testAddCookie() throws Exception { + DefaultCookie cookie = new DefaultCookie("cookie", "cook"); + HttpClientRequestImpl newReq = requestRule.request.addCookie(cookie); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, COOKIE.toString(), ClientCookieEncoder.STRICT.encode(cookie)); + } + + @Test(timeout = 60000) + public void testAddDateHeader() throws Exception { + String headerName = "date"; + Date date = new Date(); + + HttpClientRequestImpl newReq = requestRule.request.addDateHeader(headerName, date); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, date); + } + + @Test(timeout = 60000) + public void testAddDateHeaderMulti() throws Exception { + String headerName = "date"; + Date date1 = new Date(); + Date date2 = new Date(); + + HttpClientRequestImpl newReq = requestRule.request.addDateHeader(headerName, + Arrays.asList(date1, date2)); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, date1, date2); + } + + @Test(timeout = 60000) + public void testAddDateHeaderIncrementally() throws Exception { + String headerName = "foo"; + Date date1 = new Date(); + Date date2 = new Date(); + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, date1); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, date1); + + HttpClientRequestImpl newReq2 = newReq.addHeader(headerName, date2); + + requestRule.assertCopy(newReq, newReq2); + + requestRule.assertHeaderAdded(newReq2, headerName, date1, date2); + } + + @Test(timeout = 60000) + public void testAddHeaderMulti() throws Exception { + String headerName = "foo"; + String val1 = "val1"; + String val2 = "val2"; + + HttpClientRequestImpl newReq = + requestRule.request.addHeaderValues(headerName, Arrays.asList(val1, val2)); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, val1, val2); + } + + @Test(timeout = 60000) + public void testAddHeaderIncrementally() throws Exception { + String headerName = "foo"; + String val1 = "val1"; + String val2 = "val2"; + + HttpClientRequestImpl newReq = + requestRule.request.addHeader(headerName, val1); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, val1); + + HttpClientRequestImpl newReq2 = newReq.addHeader(headerName, val2); + + requestRule.assertCopy(newReq, newReq2); + + requestRule.assertHeaderAdded(newReq2, headerName, val1, val2); + } + + @Test(timeout = 60000) + public void testSetDateHeader() throws Exception { + String headerName = "date"; + Date date1 = new Date(); + + HttpClientRequestImpl addReq = requestRule.request.addDateHeader(headerName, date1); + + requestRule.assertCopy(addReq); + + requestRule.assertHeaderAdded(addReq, headerName, date1); + + Date date2 = new Date(100); + HttpClientRequestImpl setReq = requestRule.request.setDateHeader(headerName, date2); + + requestRule.assertCopy(setReq); + + requestRule.assertHeaderAdded(setReq, headerName, date2); + } + + @Test(timeout = 60000) + public void testSetHeader() throws Exception { + String headerName = "foo"; + String val1 = "bar"; + + HttpClientRequestImpl addReq = requestRule.request.addHeader(headerName, val1); + + requestRule.assertCopy(addReq); + + requestRule.assertHeaderAdded(addReq, headerName, val1); + + String val2 = "bar2"; + HttpClientRequestImpl setReq = requestRule.request.setHeader(headerName, val2); + + requestRule.assertCopy(setReq); + + requestRule.assertHeaderAdded(setReq, headerName, val2); + } + + @Test(timeout = 60000) + public void testSetDateHeaderMulti() throws Exception { + String headerName = "date"; + Date date1 = new Date(); + + HttpClientRequestImpl addReq = requestRule.request.addDateHeader(headerName, date1); + + requestRule.assertCopy(addReq); + + requestRule.assertHeaderAdded(addReq, headerName, date1); + + Date date2 = new Date(100); + Date date3 = new Date(500); + + HttpClientRequestImpl setReq = requestRule.request.setDateHeader(headerName, + Arrays.asList(date2, date3)); + + requestRule.assertCopy(setReq); + + requestRule.assertHeaderAdded(setReq, headerName, date2, date3); + } + + @Test(timeout = 60000) + public void testSetHeaderMulti() throws Exception { + String headerName = "date"; + Date date1 = new Date(); + + HttpClientRequestImpl addReq = requestRule.request.addDateHeader(headerName, date1); + + requestRule.assertCopy(addReq); + + requestRule.assertHeaderAdded(addReq, headerName, date1); + + String val2 = "bar2"; + String val3 = "bar3"; + + HttpClientRequestImpl setReq = requestRule.request.setHeaderValues(headerName, + Arrays.asList(val2, val3)); + + requestRule.assertCopy(setReq); + + requestRule.assertHeaderAdded(setReq, headerName, val2, val3); + } + + @Test(timeout = 60000) + public void testRemoveHeader() throws Exception { + final String headerName = "Foo"; + final String headerVal = "bar"; + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, headerVal); + + requestRule.assertCopy(newReq); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal); + + HttpClientRequestImpl newReq2 = newReq.removeHeader(headerName); + + requestRule.assertCopy(newReq2, newReq); + + HttpRequest newReqHeaders = newReq2.unsafeRawRequest().getHeaders(); + HttpRequest origReqHeaders = newReq.unsafeRawRequest().getHeaders(); + + assertThat("Header not removed.", newReqHeaders.headers().contains(headerName), is(false)); + assertThat("Header removed from original request.", origReqHeaders.headers().contains(headerName), + is(true)); + } + + @Test(timeout = 60000) + public void testSetKeepAlive() throws Exception { + HttpClientRequestImpl newReq = requestRule.request.setKeepAlive(false); + + requestRule.assertHeaderAdded(newReq, CONNECTION.toString(), CLOSE.toString()); + } + + @Test(timeout = 60000) + public void testSetTransferEncodingChunked() throws Exception { + HttpClientRequestImpl newReq = requestRule.request.setTransferEncodingChunked(); + + requestRule.assertHeaderAdded(newReq, TRANSFER_ENCODING.toString(), CHUNKED.toString()); + + } + + @Test(timeout = 60000) + public void testContainsHeader() throws Exception { + final String headerName = "Foo"; + final String headerVal = "bar"; + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, headerVal); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal); + + assertThat("Added header not retrievable.", newReq.containsHeader(headerName), is(true)); + } + + @Test(timeout = 60000) + public void testContainsHeaderWithValue() throws Exception { + final String headerName = "Foo"; + final String headerVal1 = "bar"; + final String headerVal2 = "bar2"; + + HttpClientRequestImpl newReq = requestRule.request + .addHeaderValues(headerName, Arrays.asList(headerVal1, headerVal2)); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal1, headerVal2); + + assertThat("Added header not retrievable.", newReq.containsHeaderWithValue(headerName, headerVal1, false), + is(true)); + } + + @Test(timeout = 60000) + public void testContainsHeaderWithValueCaseInsensitive() throws Exception { + final String headerName = "Foo"; + final String headerVal = "bar"; + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, headerVal); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal); + + assertThat("Added header not retrievable.", newReq.containsHeaderWithValue(headerName, "BaR", true), + is(true)); + } + + @Test(timeout = 60000) + public void testGetHeader() throws Exception { + final String headerName = "Foo"; + final String headerVal = "bar"; + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, headerVal); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal); + + assertThat("Added header not retrievable.", newReq.getHeader(headerName), is(headerVal)); + } + + @Test(timeout = 60000) + public void testGetAllHeaders() throws Exception { + final String headerName = "Foo"; + final String headerVal1 = "bar"; + final String headerVal2 = "bar2"; + + HttpClientRequestImpl newReq = requestRule.request + .addHeaderValues(headerName, Arrays.asList(headerVal1, headerVal2)); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal1, headerVal2); + + assertThat("Added header not retrievable.", newReq.getAllHeaders(headerName), + hasSize(2)); + + assertThat("Added header not retrievable.", newReq.getAllHeaders(headerName), + contains(headerVal1, headerVal2)); + } + + @Test(timeout = 60000) + public void testGetHttpVersion() throws Exception { + assertThat("Unexpected http version", requestRule.request.getHttpVersion(), is(HttpVersion.HTTP_1_1)); + } + + @Test(timeout = 60000) + public void testGetMethod() throws Exception { + assertThat("Unexpected http version", requestRule.request.getMethod(), is(HttpMethod.GET)); + } + + @Test(timeout = 60000) + public void testGetUri() throws Exception { + assertThat("Unexpected http version", requestRule.request.getUri(), is("/")); + } + + @Test(timeout = 60000) + public void testHeaderIterator() throws Exception { + final String headerName = "Foo"; + final String headerVal1 = "bar"; + + HttpClientRequestImpl newReq = + requestRule.request.addHeader(headerName, headerVal1); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal1); + + Iterator> headerIter = newReq.headerIterator(); + List> allHeaders = new ArrayList<>(); + while (headerIter.hasNext()) { + Entry next = headerIter.next(); + allHeaders.add(next); + } + + assertThat("Added header not retrievable.", allHeaders, hasSize(1)); + assertThat("Unexpected header name.", allHeaders.get(0).getKey(), equalTo((CharSequence)headerName)); + assertThat("Unexpected header value.", allHeaders.get(0).getValue(), equalTo((CharSequence)headerVal1)); + } + + @Test(timeout = 60000) + public void testGetHeaderNames() throws Exception { + final String headerName = "Foo"; + final String headerVal1 = "bar"; + + HttpClientRequestImpl newReq = requestRule.request.addHeader(headerName, headerVal1); + + requestRule.assertHeaderAdded(newReq, headerName, headerVal1); + + assertThat("Added header not retrievable.", newReq.getHeaderNames(), hasSize(1)); + assertThat("Unexpected header name.", newReq.getHeaderNames(), contains(headerName)); + } + + @Test(timeout = 60000) + public void testSubscribe() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable> newReq = requestRule.request.writeStringContent(Observable.just("Hello")); + RawRequest rawReq = RequestRule.getRawRequest(newReq); + + newReq.subscribe(subscriber); + + subscriber.assertNoErrors(); + requestRule.channel.flush(); /*Since nobody subscribes to the observable.*/ + + assertThat("Unexpected number of items written on the channel.", requestRule.channel.outboundMessages(), + hasSize(1)); + Object outboundMsg = requestRule.channel.readOutbound(); + assertThat("Unexpected item written on the channel.", outboundMsg, instanceOf(Observable.class)); + + @SuppressWarnings("unchecked") + Observable writtenO = (Observable) outboundMsg; + TestSubscriber writtenOSub = new TestSubscriber<>(); + writtenO.subscribe(writtenOSub); + + writtenOSub.assertTerminalEvent(); + writtenOSub.assertNoErrors(); + + @SuppressWarnings("unchecked") + Observable rawReqO = (Observable) rawReq.asObservable(requestRule.connMock); + + TestSubscriber rawReqOSub = new TestSubscriber<>(); + rawReqO.subscribe(rawReqOSub); + + rawReqOSub.assertTerminalEvent(); + rawReqOSub.assertNoErrors(); + + assertThat("Unexpected items count in Observable written on channel.", writtenOSub.getOnNextEvents(), + hasSize(rawReqOSub.getOnNextEvents().size())); + assertThat("Unexpected items in Observable written on channel.", writtenOSub.getOnNextEvents(), + contains(rawReqOSub.getOnNextEvents().toArray())); + + DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.ACCEPTED); + HttpClientResponse response = HttpClientResponseImpl.newInstance(nettyResponse, requestRule.connMock); + requestRule.addToConnectionInput(response); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected response count received.", subscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected response received.", subscriber.getOnNextEvents().get(0), + instanceOf(HttpClientResponse.class)); + @SuppressWarnings("unchecked") + HttpClientResponse actual = (HttpClientResponse) subscriber.getOnNextEvents().get(0); + + assertThat("Unexpected response received.", actual.getStatus(), is(HttpResponseStatus.ACCEPTED)); + assertThat("Unexpected response received.", actual.getHttpVersion(), is(HttpVersion.HTTP_1_1)); + } + + public static class RequestRule extends ExternalResource { + + private HttpClientRequestImpl request; + private TcpClient> clientMock; + private Connection> connMock; + private EmbeddedChannel channel; + @SuppressWarnings("rawtypes") + private Subscriber cis; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + @SuppressWarnings("unchecked") + TcpClient> clientMock = + (TcpClient>) Mockito.mock(TcpClient.class); + + channel = new EmbeddedChannel(new ChannelDuplexHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ConnectionInputSubscriberEvent) { + @SuppressWarnings({"rawtypes", "unchecked"}) + ConnectionInputSubscriberEvent cise = (ConnectionInputSubscriberEvent) evt; + cis = cise.getSubscriber(); + } + super.userEventTriggered(ctx, evt); + } + }); + + TcpClientEventPublisher eventPublisher = new TcpClientEventPublisher(); + + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(eventPublisher); + channel.attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).set(eventPublisher); + channel.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).set(eventPublisher); + + connMock = ConnectionImpl.fromChannel(channel); + + @SuppressWarnings("unchecked") + final + TcpConnectionRequestMock> connReqMock = + new TcpConnectionRequestMock(Observable.just(connMock)); + + Mockito.when(clientMock.createConnectionRequest()) + .thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return connReqMock; + } + }); + + Answer returnThisMock = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return invocation.getMock(); + } + }; + + Mockito.when(clientMock.addChannelHandlerFirst(anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerFirst(Matchers.anyObject(), + anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerLast(anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerLast(Matchers.anyObject(), + anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerBefore(anyString(), anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerBefore(Matchers.anyObject(), + anyString(), anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerAfter(anyString(), anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.addChannelHandlerAfter(Matchers.anyObject(), + anyString(), anyString(), + Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.pipelineConfigurator(Matchers.>anyObject())) + .thenAnswer(returnThisMock); + + Mockito.when(clientMock.enableWireLogging(anyString(), Matchers.anyObject())) + .thenAnswer(returnThisMock); + + RequestRule.this.clientMock = clientMock; + + request = HttpClientRequestImpl.create(HttpVersion.HTTP_1_1, + HttpMethod.GET, "/", + RequestRule.this.clientMock + ); + base.evaluate(); + } + }; + } + + public void assertCopy(HttpClientRequestImpl newReq) { + assertCopy(request, newReq); + } + + public void assertCopy(HttpClientRequestImpl oldReq, + HttpClientRequestImpl newReq) { + assertThat("Request not copied.", newReq, not(equalTo(oldReq))); + assertThat("Underlying raw request not copied.", newReq.unsafeRawRequest(), + not(equalTo(oldReq.unsafeRawRequest()))); + assertThat("Underlying raw request headers not copied.", newReq.unsafeRawRequest().getHeaders(), + not(equalTo(oldReq.unsafeRawRequest().getHeaders()))); + } + + public void assertHeaderAdded(HttpClientRequestImpl newReq, String headerName, + String... headerVals) { + assertHeaderAdded(request, newReq, headerName, headerVals); + } + + public void assertHeaderAdded(HttpClientRequestImpl oldReq, + HttpClientRequestImpl newReq, String headerName, + String... headerVals) { + + HttpRequest newReqHeaders = newReq.unsafeRawRequest().getHeaders(); + HttpRequest origReqHeaders = oldReq.unsafeRawRequest().getHeaders(); + + assertThat("New header not added.", newReqHeaders.headers().contains(headerName), is(true)); + assertThat("Unexpected header value.", newReqHeaders.headers().getAll(headerName), contains(headerVals)); + assertThat("More than one header added.", newReqHeaders.headers().names(), hasSize(1)); + + assertThat("New header added to original request.", origReqHeaders.headers().names(), is(empty())); + } + + public void assertHeaderAdded(HttpClientRequestImpl newReq, String headerName, + Date... dates) { + SimpleDateFormat sdf = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss z", Locale.ENGLISH); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + + String[] expectedValues = new String[dates.length]; + for (int i = 0; i < dates.length; i++) { + Date date = dates[i]; + expectedValues[i] = sdf.format(date); + } + + assertHeaderAdded(newReq, headerName, expectedValues); + } + + RawRequest assertContentWrite(@SuppressWarnings("rawtypes") Observable contentWritten, + Observable> newReq) { + RawRequest rawRequest = _assertContentWriteContentOnly(contentWritten, newReq); + assertThat("Unexpected flush selector in the created raw request.", rawRequest.getFlushSelector(), + is(nullValue())); + assertThat("Unexpected trailers flag in the created raw request.", rawRequest.hasTrailers(), + is(false)); + return rawRequest; + } + + RawRequest assertContentWriteAndFlushOnEach(@SuppressWarnings("rawtypes") Observable contentWritten, + Observable> newReq) { + + RawRequest rawRequest = _assertContentWriteContentOnly(contentWritten, newReq); + assertThat("Unexpected flush selector in the created raw request.", rawRequest.getFlushSelector(), + is(notNullValue())); + /*Just a way to assert that it is an unconditional flush on each*/ + assertThat("Unexpected flush selector implementation in the created raw request.", + rawRequest.getFlushSelector().call(null), is(true)); + assertThat("Unexpected trailers flag in the created raw request.", rawRequest.hasTrailers(), + is(false)); + return rawRequest; + } + + RawRequest assertContentWrite(@SuppressWarnings("rawtypes") Observable contentWritten, + Observable> newReq, + @SuppressWarnings("rawtypes") Func1 selector) { + + RawRequest rawRequest = _assertContentWriteContentOnly(contentWritten, newReq); + + @SuppressWarnings({"unchecked", "rawtypes"}) + Func1 selectorFound = rawRequest.getFlushSelector(); + assertThat("Unexpected flush selector in the created raw request.", selectorFound, + is(notNullValue())); + assertThat("Unexpected flush selector implementation in the created raw request.", + selectorFound, equalTo(selector)); + assertThat("Unexpected trailers flag in the created raw request.", rawRequest.hasTrailers(), + is(false)); + return rawRequest; + } + + public int assertContentWrite(Observable content, Observable> newReq, + TestTrailerFactory tFactory, TestTrailerMutator tMutator) { + RawRequest rawReq = getRawRequest(newReq); + + final AtomicInteger flushCount = new AtomicInteger(); + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()) { + @Override + public Channel flush() { + flushCount.incrementAndGet(); + return super.flush(); + } + }; + + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(MockEventPublisher.disabled()); + ConnectionImpl conn = ConnectionImpl.fromChannel(channel); + + Observable reqAsO = rawReq.asObservable(conn); + + TestSubscriber writtenContentSub = new TestSubscriber<>(); + content.subscribe(writtenContentSub); + + writtenContentSub.assertTerminalEvent(); + writtenContentSub.assertNoErrors(); + + TestSubscriber reqSubscriber = new TestSubscriber<>(); + reqAsO.subscribe((Observer)reqSubscriber); + + reqSubscriber.awaitTerminalEvent(); + reqSubscriber.assertNoErrors(); + + @SuppressWarnings("unchecked") + List writtenOnNextEvents = (List) writtenContentSub.getOnNextEvents(); + + List reqOnNextEvents = reqSubscriber.getOnNextEvents(); + + assertThat("Unexpected items in raw request as Observable.", reqOnNextEvents, + hasSize(writtenOnNextEvents.size() + 2)); + + assertThat("Unexpected type of first item in raw request Observable.", reqOnNextEvents.get(0), + instanceOf(HttpRequest.class)); + + HttpRequest headers = (HttpRequest) reqOnNextEvents.get(0); + assertThat("Unexpected headers in the created raw request.", headers, + is(request.unsafeRawRequest().getHeaders())); + + assertThat("Unexpected type of last item in raw request Observable.", + reqOnNextEvents.get(reqOnNextEvents.size() - 1), + instanceOf(TrailingHeaders.class)); + + TrailingHeaders trailers = (TrailingHeaders) reqOnNextEvents.get(reqOnNextEvents.size() - 1); + assertThat("Unexpected trailing headers in the created raw request.", trailers, + is(tFactory.lastReturned)); + assertThat("Unexpected trailer mutator invocation count.", tMutator.callCount, + is(writtenOnNextEvents.size())); + + List contentItems = reqOnNextEvents.subList(1, reqOnNextEvents.size() - 1); + + assertThat("Unexpected content items count in raw request as Observable.", contentItems, + hasSize(writtenOnNextEvents.size())); + assertThat("Unexpected content items in raw request as Observable.", contentItems, + contains(writtenOnNextEvents.toArray())); + + return flushCount.get(); + } + + public void assertContentWrite(Observable content, Observable> newReq, + TestTrailerFactory tFactory, TestTrailerMutator tMutator, + int expectedFlushCounts) { + int flushCount = assertContentWrite(content, newReq, tFactory, tMutator); + assertThat("Unexpected flush counts", flushCount, is(expectedFlushCounts)); + } + + private RawRequest _assertContentWriteContentOnly(@SuppressWarnings("rawtypes") Observable contentWritten, + Observable> newReq) { + RawRequest rawRequest = getRawRequest(newReq); + + assertThat("Unexpected headers in the created raw request.", rawRequest.getHeaders(), + is(request.unsafeRawRequest().getHeaders())); + + assertThat("Unexpected content in the created raw request.", rawRequest.getContent(), is(contentWritten)); + return rawRequest; + } + + static RawRequest getRawRequest(Observable> newReq) { + assertThat("Unexpected request.", newReq, instanceOf(HttpClientRequestImpl.class)); + + HttpClientRequestImpl asClientReq = (HttpClientRequestImpl) newReq; + + return asClientReq.unsafeRawRequest(); + } + + public TestTrailerFactory newTrailerFactory() { + return new TestTrailerFactory(); + } + + public TestTrailerMutator newTrailerMutator() { + return new TestTrailerMutator<>(); + } + + @SuppressWarnings("unchecked") + public void addToConnectionInput(Object msg) { + if (null != cis) { + cis.onNext(msg); + } else { + throw new AssertionError("Connection input subscriber not found"); + } + } + } + + public static class TestTrailerFactory implements Func0 { + + private volatile TrailingHeaders lastReturned; + + @Override + public TrailingHeaders call() { + lastReturned = new TrailingHeaders(); + return lastReturned; + } + } + + public static class TestTrailerMutator implements Func2 { + + private volatile int callCount; + + @Override + public TrailingHeaders call(TrailingHeaders trailingHeaders,T content) { + callCount++; + return trailingHeaders; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridgeTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridgeTest.java new file mode 100644 index 0000000..68daef6 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/internal/AbstractHttpConnectionBridgeTest.java @@ -0,0 +1,522 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionInputSubscriberEvent; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge.ConnectionInputSubscriber; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridgeTest.AbstractHttpConnectionBridgeMock.HttpObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Matchers; +import org.mockito.Mockito; +import rx.Producer; +import rx.Subscriber; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.subscriptions.Subscriptions; + +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; + +import static io.netty.handler.codec.http.HttpUtil.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class AbstractHttpConnectionBridgeTest { + + @Rule + public final HandlerRule handlerRule = new HandlerRule(); + + @Test(timeout = 60000) + public void testSetTransferEncoding() throws Exception { + DefaultHttpRequest reqWithNoContentLength = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + handlerRule.channel.writeAndFlush(reqWithNoContentLength); + + assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); + + assertThat("Transfer encoding not set to chunked.", isTransferEncodingChunked(reqWithNoContentLength), + is(true)); + + DefaultHttpRequest reqWithContentLength = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + setContentLength(reqWithContentLength, 100); + + handlerRule.channel.writeAndFlush(reqWithContentLength); + + /*One header from previous write*/ + assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(2)); + + assertThat("Transfer encoding set to chunked when content length was set.", + isTransferEncodingChunked(reqWithContentLength), is(false)); + } + + @Test(timeout = 60000) + public void testWritePrimitives() throws Exception { + handlerRule.channel.writeAndFlush("Hello"); + + assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); + assertThat("Unexpected message written.", handlerRule.channel.readOutbound(), instanceOf(ByteBuf.class)); + + handlerRule.channel.writeAndFlush("Hello".getBytes()); + + assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); + assertThat("Unexpected message written.", handlerRule.channel.readOutbound(), instanceOf(ByteBuf.class)); + } + + @Test(timeout = 60000) + public void testConnInputSubscriberEvent() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + } + + @Test(timeout = 60000) + public void testHttpContentSubscriberEventWithNoContentInputSub() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber)); + + subscriber.assertTerminalEvent(); + + assertThat("Subscriber did not get an error", subscriber.getOnErrorEvents(), hasSize(1)); + assertThat("Subscriber got an unexpected error", subscriber.getOnErrorEvents().get(0), + instanceOf(NullPointerException.class)); + } + + @Test(timeout = 60000) + public void testHttpContentSub() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + handlerRule.simulateHeaderReceive(); /*Simulate header receive, required for content sub.*/ + ProducerAwareSubscriber subscriber = new ProducerAwareSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber)); + + subscriber.assertNoErrors(); + + @SuppressWarnings("unchecked") + Subscriber contentSub = (Subscriber) handlerRule.connInSub.getState().getContentSub(); + + assertThat("Unexpected HTTP Content subscriber found", contentSub, equalTo((Subscriber)subscriber)); + assertThat("Unexpected content subscriber producer.", subscriber.getProducer(), + equalTo(handlerRule.connInputProducerMock)); + + subscriber.unsubscribe(); + + subscriber.assertUnsubscribed(); + + assertThat("Unsubscribing from HTTP content, did not unsubscribe from connection input.", + handlerRule.connInSub.isUnsubscribed(), is(true)); + } + + @Test(timeout = 60000) + public void testContentArrivedBeforeSubscription() throws Exception { + handlerRule.channel.config().setAutoRead(false); + + handlerRule.setupAndAssertConnectionInputSub(); + + handlerRule.connInSub.onNext(new DefaultLastHttpContent());/*Simulating content read on channel*/ + + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + contentSub.assertTerminalEvent(); + + assertThat("Content received on delayed subscription.", contentSub.getOnNextEvents(), is(empty())); + assertThat("Error not received on delayed subscription.", contentSub.getOnErrorEvents(), hasSize(1)); + } + + @Test(timeout = 60000) + public void testLazyContentAndTrailerSubWithAutoReadOn() throws Exception { + handlerRule.channel.config().setAutoRead(true); + + handlerRule.setupAndAssertConnectionInputSub(); + + /*Request sent, no content/trailer sub registered, will cause error on sub.*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + TestSubscriber contentSub = new TestSubscriber<>(); + /*Lazy subscription*/ + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + contentSub.assertTerminalEvent(); + + assertThat("Content received on lazy subscription.", contentSub.getOnNextEvents(), is(empty())); + assertThat("Error not received on lazy subscription.", contentSub.getOnErrorEvents(), hasSize(1)); + } + + @Test(timeout = 60000) + public void testLazyContentAndTrailerSubWithAutoReadOff() throws Exception { + handlerRule.channel.config().setAutoRead(false); + + handlerRule.setupAndAssertConnectionInputSub(); + + /*Request sent, after this it will expect the subscriber to be registered before content arrives..*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + /*Content sent but no subscriber.*/ + handlerRule.connInSub.onNext(new DefaultHttpContent(Unpooled.buffer().writeBytes("Hello".getBytes()))); + + TestSubscriber contentSub = new TestSubscriber<>(); + /*Content already sent, lazy sub now.*/ + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + contentSub.assertTerminalEvent(); + + assertThat("Content received on lazy subscription.", contentSub.getOnNextEvents(), is(empty())); + assertThat("Error not received on lazy subscription.", contentSub.getOnErrorEvents(), hasSize(1)); + + handlerRule.connInSub.onNext(new DefaultLastHttpContent());/*Simulate completion.*/ + } + + @Test(timeout = 60000) + public void testHttpChunked() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + handlerRule.simulateHeaderReceive(); + + /*Eager content subscription*/ + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + /*Headers sent*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + ByteBuf content1 = Unpooled.buffer().writeBytes("Hello".getBytes()); + ByteBuf content2 = Unpooled.buffer().writeBytes("Hello2".getBytes()); + ByteBuf contentLast = Unpooled.buffer().writeBytes("Hello3".getBytes()); + + /*Content 1 sent.*/ + handlerRule.connInSub.onNext(new DefaultHttpContent(content1)); + /*Content 2 sent.*/ + handlerRule.connInSub.onNext(new DefaultHttpContent(content2)); + + DefaultLastHttpContent trailers = new DefaultLastHttpContent(contentLast); + String trailer1Name = "foo"; + String trailer1Value = "bar"; + trailers.trailingHeaders().add(trailer1Name, trailer1Value); + + /*trailers with content*/ + handlerRule.connInSub.onNext(trailers); + + contentSub.assertTerminalEvent(); + contentSub.assertNoErrors(); + + assertThat("Unexpected content chunks.", contentSub.getOnNextEvents(), hasSize(3)); + assertThat("Unexpected content chunks.", contentSub.getOnNextEvents(), contains(content1, content2, + contentLast)); + } + + @Test(timeout = 60000) + public void testClose() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + + /*Eager content subscription*/ + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + /*Headers sent*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + /*Close before response complete*/ + handlerRule.channel.close(); + + handlerRule.headerSub.assertTerminalEvent(); + contentSub.assertTerminalEvent(); + + assertThat("No error to header subscriber on close.", handlerRule.headerSub.getOnErrorEvents(), hasSize(1)); + assertThat("No error to content subscriber on close.", contentSub.getOnErrorEvents(), hasSize(1)); + + assertThat("Close before complete did not get invoked.", + ((AbstractHttpConnectionBridgeMock)handlerRule.handler).closedBeforeReceive, is(true)); + } + + @Test(timeout = 60000) + public void testHeaderUnsubscribeBeforeHeaderReceive() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + + handlerRule.headerSub.unsubscribe(); + + assertThat("Connection input not unsubscribed.", handlerRule.connInSub.isUnsubscribed(), is(true)); + } + + @Test(timeout = 60000) + public void testHeaderUnsubscribeAfterHeaderReceive() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + /*Headers sent*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + handlerRule.headerSub.unsubscribe(); + + assertThat("Connection input unsubscribed post headers.", handlerRule.connInSub.isUnsubscribed(), is(false)); + } + + @Test(timeout = 60000) + public void testConnectionInputCompleteWithNoHeaders() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + handlerRule.simulateHeaderReceive(); + + /*Eager content subscription*/ + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + handlerRule.connInSub.onCompleted(); + + handlerRule.headerSub.assertTerminalEvent(); + /*Since headers started but not content*/ + handlerRule.headerSub.assertError(ClosedChannelException.class); + + contentSub.assertTerminalEvent(); + contentSub.assertError(ClosedChannelException.class); + } + + @Test(timeout = 60000) + public void testConnectionInputCompletePostHeaders() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + + /*Eager content subscription*/ + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + /*Headers sent*/ + handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + handlerRule.headerSub.assertNoErrors(); + assertThat("Header subscriber did not get the headers.", handlerRule.headerSub.getOnNextEvents(), hasSize(1)); + /*Look only for one HTTP message*/ + handlerRule.headerSub.unsubscribe(); + assertThat("Content subscriber unsubscribed post header unsubscribe.", contentSub.isUnsubscribed(), is(false)); + + handlerRule.connInSub.onCompleted(); + + contentSub.assertTerminalEvent(); + assertThat("Content subscriber did not get an error.", contentSub.getOnErrorEvents(), hasSize(1)); + } + + @Test(timeout = 60000) + public void testMultiSubscribers() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + handlerRule.simulateHeaderReceive(); + + /*Eager content subscription*/ + TestSubscriber contentSub = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); + + @SuppressWarnings("unchecked") + Subscriber contentSubFound = (Subscriber) handlerRule.connInSub.getState().getContentSub(); + + assertThat("Unexpected HTTP Content subscriber found", contentSubFound, + equalTo((Subscriber) contentSub)); + + contentSub.assertNoErrors(); + + /*Second active subscription*/ + TestSubscriber contentSub2 = new TestSubscriber<>(); + handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub2)); + + contentSub2.assertTerminalEvent(); + assertThat("Second content subscriber did not get an error.", contentSub2.getOnErrorEvents(), hasSize(1)); + } + + public static class HandlerRule extends ExternalResource { + + private Connection connMock; + private EmbeddedChannel channel; + private AbstractHttpConnectionBridge handler; + private EventCatcher eventCatcher; + private ConnectionInputSubscriber connInSub; + private Producer connInputProducerMock; + private ProducerAwareSubscriber headerSub; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + handler = newAbstractHttpConnectionBridgeMock(); + eventCatcher = new EventCatcher(); + channel = new EmbeddedChannel(handler, eventCatcher); + @SuppressWarnings("unchecked") + Connection connMock = Mockito.mock(Connection.class); + Mockito.when(connMock.unsafeNettyChannel()).thenReturn(channel); + + HandlerRule.this.connMock = connMock; + base.evaluate(); + } + }; + } + + protected AbstractHttpConnectionBridge newAbstractHttpConnectionBridgeMock() { + return new AbstractHttpConnectionBridgeMock(HttpRequest.class); + } + + public EmbeddedChannel getChannel() { + return channel; + } + + public void simulateHeaderReceive() { + connInSub.getState().headerReceived(); + } + + public void setupAndAssertConnectionInputSub() { + headerSub = new ProducerAwareSubscriber<>(); + + @SuppressWarnings({"rawtypes", "unchecked"}) + ConnectionInputSubscriberEvent evt = new ConnectionInputSubscriberEvent(headerSub); + + channel.pipeline().fireUserEventTriggered(evt); + + assertThat("Handler did not pass the event.", eventCatcher.events, hasSize(1)); + assertThat("Handler did not modify the event.", eventCatcher.events, not(contains((Object) evt))); + + Object eventCaught = eventCatcher.events.get(0); + + assertThat("Unexpected propagated event.", eventCaught, instanceOf(ConnectionInputSubscriberEvent.class)); + + @SuppressWarnings({"rawtypes", "unchecked"}) + ConnectionInputSubscriberEvent modEvt = (ConnectionInputSubscriberEvent) eventCaught; + + assertThat("Unexpected propagated event subscriber.", modEvt.getSubscriber(), + instanceOf(ConnectionInputSubscriber.class)); + + @SuppressWarnings("unchecked") + ConnectionInputSubscriber connInSub = (ConnectionInputSubscriber) modEvt.getSubscriber(); + this.connInSub = connInSub; + + assertThat("Channel not set in the subscriber.", connInSub.getChannel(), is(notNullValue())); + assertThat("Unexpected channel set in the subscriber.", connInSub.getChannel(), equalTo((Channel)channel)); + + @SuppressWarnings("unchecked") + Subscriber headerSub = (Subscriber) connInSub.getState().getHeaderSub(); + + assertThat("Unexpected header subscriber.", headerSub, is((Subscriber) this.headerSub)); + + connInputProducerMock = Mockito.mock(Producer.class); + connInSub.setProducer(connInputProducerMock); + + assertThat("Header subscriber producer not set.", this.headerSub.getProducer(), + equalTo(connInputProducerMock)); + + Mockito.verify(connInputProducerMock).request(Matchers.anyLong()); + } + + } + + private static class ProducerAwareSubscriber extends TestSubscriber { + + private Producer producer; + + @Override + public void setProducer(Producer producer) { + this.producer = producer; + super.setProducer(producer); + } + + public Producer getProducer() { + return producer; + } + } + + private static class EventCatcher extends ChannelDuplexHandler { + + private final List events = new ArrayList<>(); + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + events.add(evt); + super.userEventTriggered(ctx, evt); + } + } + + public static class AbstractHttpConnectionBridgeMock extends AbstractHttpConnectionBridge { + + private final Class headerMsgClass; + private volatile boolean closedBeforeReceive; + + public AbstractHttpConnectionBridgeMock(Class headerMsgClass) { + this.headerMsgClass = headerMsgClass; + } + + @Override + protected boolean isInboundHeader(Object nextItem) { + return headerMsgClass.isAssignableFrom(nextItem.getClass()); + } + + @Override + protected boolean isOutboundHeader(Object nextItem) { + return nextItem instanceof HttpRequest; + } + + @Override + protected Object newHttpObject(Object nextItem, Channel channel) { + return new HttpObject(); + } + + @Override + protected void onContentReceived() { + // No Op + } + + @Override + protected void onContentReceiveComplete(long receiveStartTimeNanos) { + // No Op + } + + @Override + protected void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos) { + // No Op + } + + @Override + protected void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, + long headerWriteStartTimeNanos) { + // No Op + } + + @Override + protected void onClosedBeforeReceiveComplete(Channel channel) { + closedBeforeReceive = true; + } + + @Override + protected void onNewContentSubscriber(final ConnectionInputSubscriber inputSubscriber, + Subscriber newSub) { + newSub.add(Subscriptions.create(new Action0() { + @Override + public void call() { + inputSubscriber.unsubscribe(); + } + })); + } + + public static class HttpObject { + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/CookieTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/CookieTest.java new file mode 100644 index 0000000..b1dd532 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/CookieTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.events.EventAttributeKeys; +import io.reactivex.netty.test.util.MockEventPublisher; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; +import java.util.Set; + +public class CookieTest { + + @Test(timeout = 60000) + public void testGetCookie() throws Exception { + DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); + String cookie1Name = "PREF"; + String cookie1Value = "ID=a95756377b78e75e:FF=0:TM=1392709628:LM=1392709628:S=a5mOVvTB7DBkexgi"; + String cookie1Header = cookie1Name + '=' + cookie1Value + + "; expires=Thu, 18-Feb-2016 07:47:08 GMT;"; + nettyRequest.headers().add(HttpHeaderNames.COOKIE, cookie1Header); + + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); + HttpServerRequest request = new HttpServerRequestImpl<>(nettyRequest, channel); + + Map> cookies = request.getCookies(); + Assert.assertEquals("Unexpected number of cookies.", 1, cookies.size()); + Set cookies1 = cookies.get(cookie1Name); + Assert.assertNotNull("No cookie found with name: " + cookie1Name, cookies1); + Assert.assertEquals("Unexpected number of cookies with name: " + cookie1Name, 1, cookies1.size() ); + Cookie cookie = cookies1.iterator().next(); + Assert.assertEquals("Unexpected cookie name.", cookie1Name, cookie.name()); + } + + @Test(timeout = 60000) + public void testSetCookie() throws Exception { + DefaultHttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); + channel.attr(EventAttributeKeys.EVENT_PUBLISHER).set(MockEventPublisher.disabled()); + Connection connection = ConnectionImpl.fromChannel(channel); + HttpServerResponse response = HttpServerResponseImpl.create(null, connection, nettyResponse); + String cookieName = "name"; + String cookieValue = "value"; + response.addCookie(new DefaultCookie(cookieName, cookieValue)); + String cookieHeader = nettyResponse.headers().get(HttpHeaderNames.SET_COOKIE); + Assert.assertNotNull("Cookie header not found.", cookieHeader); + Set decode = ServerCookieDecoder.STRICT.decode(cookieHeader); + Assert.assertNotNull("Decoded cookie not found.", decode); + Assert.assertEquals("Unexpected number of decoded cookie not found.", 1, decode.size()); + Cookie cookie = decode.iterator().next(); + Assert.assertEquals("Unexpected cookie name.", cookieName, cookie.name()); + Assert.assertEquals("Unexpected cookie value.", cookieValue, cookie.value()); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/Http10Test.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/Http10Test.java new file mode 100644 index 0000000..a8e070f --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/Http10Test.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import org.junit.Rule; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class Http10Test { + + @Rule + public final HttpServerRule rule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testHttp1_0Response() throws Exception { + rule.setServer(rule.getServer().sendHttp10ResponseFor10Request(true)); + rule.startServer(); + + final HttpClientRequest request = + rule.getClient().createRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/"); + + final HttpClientResponse response = rule.sendRequest(request); + + assertThat("Unexpected HTTP version.", response.getHttpVersion(), is(HttpVersion.HTTP_1_0)); + assertThat("Unexpected keep-alive value.", response.isKeepAlive(), is(false)); + assertThat("Unexpected transfer encoding.", response.isTransferEncodingChunked(), is(false)); + + rule.assertResponseContent(response); + } + + @Test(timeout = 60000) + public void testHttp1_1Response() throws Exception { + rule.getServer().sendHttp10ResponseFor10Request(true); + rule.startServer(); + + final HttpClientRequest request = + rule.getClient().createRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/"); + + final HttpClientResponse response = rule.sendRequest(request); + + assertThat("Unexpected HTTP version.", response.getHttpVersion(), is(HttpVersion.HTTP_1_1)); + assertThat("Unexpected keep-alive value.", response.isKeepAlive(), is(false)); + assertThat("Unexpected transfer encoding.", response.isTransferEncodingChunked(), is(false)); + + rule.assertResponseContent(response); + } + +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpEndToEndTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpEndToEndTest.java new file mode 100644 index 0000000..110e832 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpEndToEndTest.java @@ -0,0 +1,63 @@ +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Scheduler.Worker; +import rx.Subscriber; +import rx.functions.Action0; +import rx.schedulers.Schedulers; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static io.reactivex.netty.protocol.http.server.HttpServerRule.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpEndToEndTest { + + @Rule + public final HttpServerRule rule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testDelayedWrites() throws Exception { + + final AtomicReference errorFromWriteStreamCompletion = new AtomicReference<>(); + final Worker worker = Schedulers.computation().createWorker(); + rule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.writeString(Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + worker.schedule(new Action0() { + @Override + public void call() { + try { + subscriber.onNext(WELCOME_SERVER_MSG); + subscriber.onCompleted(); + } catch (Exception e) { + errorFromWriteStreamCompletion.set(e); + } + } + }, 1, TimeUnit.MILLISECONDS); + + } + })); + } + }); + + final HttpClientResponse response = rule.sendRequest(rule.getClient().createGet("/")); + + assertThat("Unexpected response code.", response.getStatus(), is(HttpResponseStatus.OK)); + + rule.assertResponseContent(response); + + assertThat("Unexpected exception on server.", errorFromWriteStreamCompletion.get(), is(nullValue())); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRequestUriTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRequestUriTest.java new file mode 100644 index 0000000..b44b7b1 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRequestUriTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.logging.LoggingHandler; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +public class HttpServerRequestUriTest { + + @Test(timeout = 60000) + public void testRequestUri() throws Exception { + String path = "a/b/c"; + String qp1Name = "qp1"; + String qp1Val = "qp1Val"; + String qp2Name = "qp2"; + String qp2Val = "qp2Val"; + String qp2Val2 = "qp2Val222"; + String queryString = qp1Name + '=' + qp1Val + '&' + qp2Name + '=' + qp2Val + '&' + qp2Name + '=' + qp2Val2 ; + String uri = path + '?' + queryString; + DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + HttpServerRequest request = newServerRequest(nettyRequest); + Assert.assertEquals("Unexpected uri string", uri, request.getUri()); + Assert.assertEquals("Unexpected query string", queryString, request.getRawQueryString()); + Assert.assertEquals("Unexpected path string", path, request.getDecodedPath()); + Map> qpsGot = request.getQueryParameters(); + Assert.assertNotNull("Got null query parameters", qpsGot); + Assert.assertEquals("Unexpected number of query parameters", 2, qpsGot.size()); + List qp1Got = qpsGot.get(qp1Name); + Assert.assertNotNull("Got no query parameters with name: " + qp1Name, qp1Got); + Assert.assertEquals("Unexpected number of query parameters with name: " + qp1Name, 1, qp1Got.size()); + Assert.assertEquals("Unexpected query parameter value with name: " + qp1Name, qp1Val, qp1Got.get(0)); + + List qp2Got = qpsGot.get(qp2Name); + Assert.assertNotNull("Got no query parameters with name: " + qp2Name, qp2Got); + Assert.assertEquals("Unexpected number of query parameters with name: " + qp2Name, 2, qp2Got.size()); + Assert.assertEquals("Unexpected query parameter value with name: " + qp2Name, qp2Val, qp2Got.get(0)); + Assert.assertEquals("Unexpected query parameter second value with name: " + qp2Name, qp2Val2, qp2Got.get(1)); + } + + @Test(timeout = 60000) + public void testEmptyQueryString() throws Exception { + String path = "a/b/c"; + String uri = path + '?'; + DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + HttpServerRequest request = newServerRequest(nettyRequest); + Assert.assertEquals("Unexpected uri string", uri, request.getUri()); + Assert.assertEquals("Unexpected query string", "", request.getRawQueryString()); + } + + @Test(timeout = 60000) + public void testAbsentQueryString() throws Exception { + String uri = "a/b/c"; + DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + HttpServerRequest request = newServerRequest(nettyRequest); + Assert.assertEquals("Unexpected uri string", uri, request.getUri()); + Assert.assertEquals("Unexpected query string", "", request.getRawQueryString()); + } + + protected HttpServerRequest newServerRequest(DefaultHttpRequest nettyRequest) { + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler()); + return new HttpServerRequestImpl<>(nettyRequest, channel); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRule.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRule.java new file mode 100644 index 0000000..b8dc794 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerRule.java @@ -0,0 +1,237 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import org.junit.Assert; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.Charset; +import java.util.regex.Pattern; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +public class HttpServerRule extends ExternalResource { + + public static final String WELCOME_SERVER_MSG = "Welcome!"; + + private HttpServer server; + private HttpClient client; + + private String lastResponse = ""; + + @Override + public Statement apply(final Statement base, Description description) { + lastResponse = ""; + return new Statement() { + @Override + public void evaluate() throws Throwable { + server = HttpServer.newServer() + .enableWireLogging("test", LogLevel.INFO) + .addChannelHandlerFirst("raw-message-handler", + RawMessageHandler.factory( + new Action1() { + @Override + public void call(ByteBuf byteBuf) { + lastResponse += byteBuf.toString(Charset.defaultCharset()); + } + } + ) + ); + base.evaluate(); + } + }; + } + + public SocketAddress startServer() { + server.start(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, + HttpServerResponse response) { + return response.setHeader(CONTENT_LENGTH, WELCOME_SERVER_MSG.getBytes().length) + .writeString(Observable.just(WELCOME_SERVER_MSG)); + } + }); + client = HttpClient.newClient("127.0.0.1", server.getServerPort()); + return server.getServerAddress(); + } + + public void startServer(RequestHandler handler) { + server.start(handler); + client = HttpClient.newClient("127.0.0.1", server.getServerPort()); + } + + public void setupClient(HttpClient client) { + this.client = client; + } + + public HttpClientResponse sendRequest(Observable> request) { + TestSubscriber> subscriber = new TestSubscriber<>(); + + request.subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected response count.", subscriber.getOnNextEvents(), hasSize(1)); + return subscriber.getOnNextEvents().get(0); + } + + public void assertResponseContent(HttpClientResponse response) { + TestSubscriber subscriber = new TestSubscriber<>(); + + response.getContent().subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + assertThat("Unexpected content items.", subscriber.getOnNextEvents(), hasSize(1)); + assertThat("Unexpected content.", subscriber.getOnNextEvents().get(0).toString(Charset.defaultCharset()), + equalTo(WELCOME_SERVER_MSG)); + } + + public void assertEmptyBodyWithContentLengthZero() { + assertBodyWithContentLength(0, ""); + } + + public void assertBodyWithContentLength(int contentLength, String body) { + getAndDrainClient(); + Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE); + + if (!lastResponse.contains("content-length: " + contentLength + "\r\n")) { + Assert.fail("Missing header 'content-length: " + contentLength + "'"); + } + if (lastResponse.contains("transfer-encoding: chunked\r\n")) { + Assert.fail("Unexpected header 'transfer-encoding: chunked'"); + } + if (!headerBlock.matcher(lastResponse).replaceFirst("").equals(body)) { + Assert.fail("Unexpected body content '" + headerBlock.matcher(lastResponse).replaceFirst("") + "'"); + } + } + + public void assertEmptyBodyWithSingleChunk() { + assertChunks(); + } + + public void assertChunks(String... chunks) { + getAndDrainClient(); + Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE); + + if (lastResponse.contains("content-length: 0\r\n")) { + Assert.fail("Unexpected header 'content-length: 0'"); + } + if (!lastResponse.contains("transfer-encoding: chunked\r\n")) { + Assert.fail("Missing header 'transfer-encoding: chunked'"); + } + String expectedChunkContent = ""; + for (String c : chunks) { + expectedChunkContent += c.getBytes().length + "\r\n"; + expectedChunkContent += c + "\r\n"; + } + expectedChunkContent += "0\r\n\r\n"; + if (!headerBlock.matcher(lastResponse).replaceFirst("").equals(expectedChunkContent)) { + Assert.fail("Unexpected body content '" + headerBlock.matcher(lastResponse).replaceFirst("") + "'"); + } + } + + public SocketAddress getServerAddress() { + return new InetSocketAddress("127.0.0.1", server.getServerPort()); + } + + public void setServer(HttpServer server) { + this.server = server; + } + + public HttpServer getServer() { + return server; + } + + public HttpClient getClient() { + return client; + } + + + public void getAndDrainClient() { + lastResponse = ""; + TestSubscriber clientDrain = new TestSubscriber<>(); + client.createGet("/") + .flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse clientResponse) { + return clientResponse.discardContent(); + } + }) + .subscribe(clientDrain); + clientDrain.awaitTerminalEvent(); + clientDrain.assertNoErrors(); + } + + + private static class RawMessageHandler extends ChannelDuplexHandler { + + public static Func0 factory(final Action1 onWrite) { + return new Func0() { + @Override + public ChannelHandler call() { + return new RawMessageHandler(onWrite); + } + }; + } + + private final Action1 onWrite; + + public RawMessageHandler(Action1 onWrite) { + this.onWrite = onWrite; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + callback(msg, onWrite); + super.write(ctx, msg, promise); + } + + private void callback(Object msg, Action1 a) { + if (msg instanceof ByteBuf) { + a.call((ByteBuf) msg); + } else if (msg instanceof ByteBufHolder) { + a.call(((ByteBufHolder) msg).content()); + } else { + throw new RuntimeException("Unexpected msg type " + msg.getClass()); + } + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerTest.java new file mode 100644 index 0000000..4aa723f --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; + +public class HttpServerTest { + + @Rule + public final HttpServerRule serverRule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testResponseWithNoContentLengthHeaderOrContentReturnsEmptyBody() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.setStatus(HttpResponseStatus.BAD_REQUEST); + } + }); + + serverRule.assertEmptyBodyWithContentLengthZero(); + } + + @Test(timeout = 60000) + public void testResponseWithNoContentLengthHeaderAndSendHeadersReturnsEmptyBody() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.setStatus(HttpResponseStatus.BAD_REQUEST) + .sendHeaders(); + } + }); + + serverRule.assertEmptyBodyWithContentLengthZero(); + } + + @Test(timeout = 60000) + public void testResponseWithNoContentLengthHeaderAndContentReturnsContentChunkAndSingleEmptyChunk() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.sendHeaders() + .writeString(Observable.just("Hello")); + } + }); + + serverRule.assertChunks("Hello"); + } + + @Test(timeout = 60000) + public void testResponseWithContentLengthReturnsRawBody() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.setStatus(HttpResponseStatus.BAD_REQUEST) + .setHeader(HttpHeaderNames.CONTENT_LENGTH, 5) + .writeString(Observable.just("Hello")); + } + }); + + serverRule.assertBodyWithContentLength(5, "Hello"); + } + + @Test(timeout = 60000) + public void testResponseWithZeroContentLengthReturnsEmptyBody() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.setStatus(HttpResponseStatus.BAD_REQUEST) + .setHeader(HttpHeaderNames.CONTENT_LENGTH, 0); + } + }); + + serverRule.assertEmptyBodyWithContentLengthZero(); + } + + @Test(timeout = 60000) + public void testResponseWithOnlyPositiveContentLengthReturnsEmptyBody() throws Exception { + serverRule.startServer(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + return response.setStatus(HttpResponseStatus.BAD_REQUEST) + .setHeader(HttpHeaderNames.CONTENT_LENGTH, 5); + } + }); + + serverRule.assertEmptyBodyWithContentLengthZero(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridgeTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridgeTest.java new file mode 100644 index 0000000..5e6dc00 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/HttpServerToConnectionBridgeTest.java @@ -0,0 +1,45 @@ +package io.reactivex.netty.protocol.http.server; + +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridgeTest.AbstractHttpConnectionBridgeMock; +import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridgeTest.HandlerRule; +import io.reactivex.netty.protocol.http.internal.HttpContentSubscriberEvent; +import io.reactivex.netty.protocol.http.server.events.HttpServerEventPublisher; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import org.junit.Rule; +import org.junit.Test; +import rx.observers.TestSubscriber; + +import java.nio.channels.ClosedChannelException; + +public class HttpServerToConnectionBridgeTest { + + @Rule + public final HandlerRule handlerRule = new HandlerRule() { + @Override + protected AbstractHttpConnectionBridge newAbstractHttpConnectionBridgeMock() { + return new HttpServerToConnectionBridge<>(new HttpServerEventPublisher(new TcpServerEventPublisher())); + } + }; + + @Test(timeout = 60000) + public void testPendingContentSubscriber() throws Exception { + handlerRule.setupAndAssertConnectionInputSub(); + handlerRule.simulateHeaderReceive(); /*Simulate header receive, required for content sub.*/ + TestSubscriber subscriber = new TestSubscriber<>(); + handlerRule.getChannel().pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber)); + TestSubscriber subscriber1 = new TestSubscriber<>(); + handlerRule.getChannel().pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber1)); + + subscriber.assertNoErrors(); + subscriber1.assertNoErrors(); + subscriber.unsubscribe(); + + subscriber.assertUnsubscribed(); + + handlerRule.getChannel().close().await(); + + subscriber.assertNoErrors(); + subscriber1.assertError(ClosedChannelException.class); + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/PipeliningTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/PipeliningTest.java new file mode 100644 index 0000000..bd9a3b6 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/PipeliningTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server; + +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import static io.netty.handler.codec.http.HttpMethod.*; +import static io.netty.handler.codec.http.HttpVersion.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class PipeliningTest { + + @Rule + public final HttpServerRule serverRule = new HttpServerRule(); + + @Test(timeout = 60000) + public void testPipelining() throws Exception { + + serverRule.startServer(); + + /*Since HTTP client does not yet support pipeling, this example uses a TCP client*/ + TestSubscriber testSubscriber = new TestSubscriber<>(); + TcpClient.newClient(serverRule.getServerAddress()) + .pipelineConfigurator(new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + pipeline.addLast(new HttpClientCodec()); + pipeline.addLast(new HttpObjectAggregator(1024 * 1024)); + } + }) + .createConnectionRequest() + .flatMap(new Func1, Observable>() { + @Override + public Observable call(Connection c) { + DefaultFullHttpRequest request1 = new DefaultFullHttpRequest(HTTP_1_1, GET, "/1"); + DefaultFullHttpRequest request2 = new DefaultFullHttpRequest(HTTP_1_1, GET, "/2"); + return c.write(Observable.just(request1, request2)) + .ignoreElements() + .cast(FullHttpResponse.class) + .concatWith(c.getInput()); + } + } + ) + .map(new Func1() { + @Override + public String call(FullHttpResponse resp) { + return resp.status().toString(); + } + }) + .take(2) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + + testSubscriber.assertNoErrors(); + + assertThat("Unexpected number of responses.", testSubscriber.getOnNextEvents(), hasSize(2)); + assertThat("OK status code not found in response #1.", testSubscriber.getOnNextEvents().get(0), + containsString(HttpResponseStatus.OK.toString())); + assertThat("OK status code not found in response #2.", testSubscriber.getOnNextEvents().get(1), + containsString(HttpResponseStatus.OK.toString())); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisherTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisherTest.java new file mode 100644 index 0000000..4db23c9 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventPublisherTest.java @@ -0,0 +1,284 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server.events; + +import io.reactivex.netty.protocol.http.server.events.HttpServerEventsListenerImpl.HttpEvent; +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventPublisher; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import io.reactivex.netty.test.util.MockTcpServerEventListener.ServerEvent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpServerEventPublisherTest { + + @Rule + public final PublisherRule rule = new PublisherRule(); + + @Test(timeout = 60000) + public void testOnNewRequestReceived() throws Exception { + rule.publisher.onNewRequestReceived(); + + rule.listener.assertMethodsCalled(HttpEvent.ReqRecv); + } + + @Test(timeout = 60000) + public void testOnRequestHandlingStart() throws Exception { + rule.publisher.onRequestHandlingStart(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(HttpEvent.HandlingStart); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnRequestHandlingSuccess() throws Exception { + rule.publisher.onRequestHandlingSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(HttpEvent.HandlingSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnRequestHandlingFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onRequestHandlingFailed(1, TimeUnit.MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(HttpEvent.HandlingFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnRequestHeadersReceived() throws Exception { + rule.publisher.onRequestHeadersReceived(); + rule.listener.assertMethodsCalled(HttpEvent.ReqHdrsReceived); + } + + @Test(timeout = 60000) + public void testOnRequestContentReceived() throws Exception { + rule.publisher.onRequestContentReceived(); + rule.listener.assertMethodsCalled(HttpEvent.ReqContentReceived); + } + + @Test(timeout = 60000) + public void testOnRequestReceiveComplete() throws Exception { + rule.publisher.onRequestReceiveComplete(1, TimeUnit.MILLISECONDS); + rule.listener.assertMethodsCalled(HttpEvent.ReqReceiveComplete); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnResponseWriteStart() throws Exception { + rule.publisher.onResponseWriteStart(); + rule.listener.assertMethodsCalled(HttpEvent.RespWriteStart); + } + + @Test(timeout = 60000) + public void testOnResponseWriteSuccess() throws Exception { + rule.publisher.onResponseWriteSuccess(1, TimeUnit.MILLISECONDS, 200); + rule.listener.assertMethodsCalled(HttpEvent.RespWriteSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with response code.", rule.listener.getResponseCode(), is(200)); + } + + @Test(timeout = 60000) + public void testOnResponseWriteFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onResponseWriteFailed(1, TimeUnit.MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(HttpEvent.RespWriteFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseFailed() throws Exception { + rule.publisher.onConnectionCloseFailed(1, TimeUnit.MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CloseFailed); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseSuccess() throws Exception { + rule.publisher.onConnectionCloseSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CloseSuccess); + } + + @Test(timeout = 60000) + public void testOnConnectionCloseStart() throws Exception { + rule.publisher.onConnectionCloseStart(); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CloseStart); + } + + @Test(timeout = 60000) + public void testOnWriteFailed() throws Exception { + rule.publisher.onWriteFailed(1, TimeUnit.MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.WriteFailed); + } + + @Test(timeout = 60000) + public void testOnWriteSuccess() throws Exception { + rule.publisher.onWriteSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.WriteSuccess); + } + + @Test(timeout = 60000) + public void testOnWriteStart() throws Exception { + rule.publisher.onWriteStart(); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.WriteStart); + } + + @Test(timeout = 60000) + public void testOnFlushSuccess() throws Exception { + rule.publisher.onFlushComplete(1, TimeUnit.MILLISECONDS); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.FlushSuccess); + } + + @Test(timeout = 60000) + public void testOnFlushStart() throws Exception { + rule.publisher.onFlushStart(); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.FlushStart); + } + + @Test(timeout = 60000) + public void testOnByteRead() throws Exception { + rule.publisher.onByteRead(1); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.BytesRead); + } + + @Test(timeout = 60000) + public void testOnByteWritten() throws Exception { + rule.publisher.onByteWritten(1); + + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.BytesWritten); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingFailed() throws Exception { + rule.publisher.onConnectionHandlingFailed(1, TimeUnit.MILLISECONDS, new NullPointerException()); + + rule.listener.getTcpDelegate().assertMethodsCalled(ServerEvent.HandlingFailed); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingSuccess() throws Exception { + rule.publisher.onConnectionHandlingSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(ServerEvent.HandlingSuccess); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingStart() throws Exception { + rule.publisher.onConnectionHandlingStart(1, TimeUnit.MILLISECONDS); + + rule.listener.getTcpDelegate().assertMethodsCalled(ServerEvent.HandlingStart); + } + + @Test(timeout = 60000) + public void testOnNewClientConnected() throws Exception { + rule.publisher.onNewClientConnected(); + + rule.listener.getTcpDelegate().assertMethodsCalled(ServerEvent.NewClient); + } + + @Test(timeout = 60000) + public void testOnCustomEvent() throws Exception { + rule.publisher.onCustomEvent("Hello"); + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CustomEvent); + } + + @Test(timeout = 60000) + public void testOnCustomEventWithError() throws Exception { + rule.publisher.onCustomEvent("Hello", new NullPointerException()); + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CustomEventWithError); + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDuration() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, TimeUnit.MINUTES); + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CustomEventWithDuration); + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDurationAndError() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, TimeUnit.MINUTES, new NullPointerException()); + rule.listener.getTcpDelegate().getConnDelegate().assertMethodsCalled(Event.CustomEventWithDurationAndError); + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + assertThat("Publishing not enabled.", rule.publisher.publishingEnabled(), is(true)); + } + + @Test(timeout = 60000) + public void testCopy() throws Exception { + HttpServerEventPublisher copy = rule.publisher.copy(rule.publisher.getTcpDelegate().copy()); + + assertThat("Publisher not copied.", copy, is(not(sameInstance(rule.publisher)))); + assertThat("Listeners not copied.", copy.getListeners(), is(not(sameInstance(rule.publisher.getListeners())))); + assertThat("Delegate not copied.", copy.getTcpDelegate(), + is(not(sameInstance(rule.publisher.getTcpDelegate())))); + } + + public static class PublisherRule extends ExternalResource { + + private HttpServerEventsListenerImpl listener; + private HttpServerEventPublisher publisher; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + listener = new HttpServerEventsListenerImpl(); + publisher = new HttpServerEventPublisher(new TcpServerEventPublisher()); + publisher.subscribe(listener); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListenerImpl.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListenerImpl.java new file mode 100644 index 0000000..191fb3f --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/server/events/HttpServerEventsListenerImpl.java @@ -0,0 +1,233 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.server.events; + +import io.reactivex.netty.test.util.MockTcpServerEventListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class HttpServerEventsListenerImpl extends HttpServerEventsListener { + + public enum HttpEvent { + ReqRecv, HandlingStart, HandlingSuccess, HandlingFailed, ReqHdrsReceived, ReqContentReceived, + ReqReceiveComplete, RespWriteStart, RespWriteSuccess, RespWriteFailed + } + + private final MockTcpServerEventListener tcpDelegate; + + private int responseCode; + private long duration; + private TimeUnit timeUnit; + private Throwable recievedError; + private final List methodsCalled = new ArrayList<>(); + + public HttpServerEventsListenerImpl() { + tcpDelegate = new MockTcpServerEventListener(); + } + + @Override + public void onNewRequestReceived() { + methodsCalled.add(HttpEvent.ReqRecv); + } + + @Override + public void onRequestHandlingStart(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.HandlingStart); + } + + @Override + public void onRequestHandlingSuccess(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.HandlingSuccess); + } + + @Override + public void onRequestHandlingFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + methodsCalled.add(HttpEvent.HandlingFailed); + } + + @Override + public void onRequestHeadersReceived() { + methodsCalled.add(HttpEvent.ReqHdrsReceived); + } + + @Override + public void onRequestContentReceived() { + methodsCalled.add(HttpEvent.ReqContentReceived); + } + + @Override + public void onRequestReceiveComplete(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(HttpEvent.ReqReceiveComplete); + } + + @Override + public void onResponseWriteStart() { + methodsCalled.add(HttpEvent.RespWriteStart); + } + + @Override + public void onResponseWriteSuccess(long duration, TimeUnit timeUnit, int responseCode) { + this.duration = duration; + this.timeUnit = timeUnit; + this.responseCode = responseCode; + methodsCalled.add(HttpEvent.RespWriteSuccess); + } + + @Override + public void onResponseWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + this.duration = duration; + this.timeUnit = timeUnit; + recievedError = throwable; + methodsCalled.add(HttpEvent.RespWriteFailed); + } + + @Override + public void onByteRead(long bytesRead) { + tcpDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + tcpDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onNewClientConnected() { + tcpDelegate.onNewClientConnected(); + } + + @Override + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionHandlingStart(duration, timeUnit); + } + + @Override + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionHandlingSuccess(duration, timeUnit); + } + + @Override + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + tcpDelegate.onConnectionHandlingFailed(duration, timeUnit, recievedError); + } + + public void assertMethodsCalled(HttpEvent... events) { + assertThat("Unexpected methods called count.", methodsCalled, hasSize(events.length)); + assertThat("Unexpected methods called.", methodsCalled, contains(events)); + } + + public MockTcpServerEventListener getTcpDelegate() { + return tcpDelegate; + } + + public int getResponseCode() { + return responseCode; + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public Throwable getRecievedError() { + return recievedError; + } + + @Override + public void onCompleted() { + tcpDelegate.onCompleted(); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + tcpDelegate.onConnectionCloseFailed(duration, timeUnit, recievedError); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseStart() { + tcpDelegate.onConnectionCloseStart(); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + tcpDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteStart() { + tcpDelegate.onWriteStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + tcpDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onFlushStart() { + tcpDelegate.onFlushStart(); + } + + @Override + public void onCustomEvent(Object event) { + tcpDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + tcpDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + tcpDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + tcpDelegate.onCustomEvent(event, throwable); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/ServerSentEventEndToEndTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/ServerSentEventEndToEndTest.java new file mode 100644 index 0000000..727c8dd --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/ServerSentEventEndToEndTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.sse; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.server.HttpServer; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import rx.Observable; +import rx.functions.Func1; + +import java.util.concurrent.TimeUnit; + +import static rx.Observable.*; + +public class ServerSentEventEndToEndTest { + + private HttpServer sseServer; + + @After + public void tearDown() throws Exception { + if (null != sseServer) { + sseServer.shutdown(); + sseServer.awaitShutdown(1, TimeUnit.MINUTES); + } + } + + @Test(timeout = 60000) + public void testWriteRawString() throws Exception { + startServer(new Func1, Observable>() { + + @Override + public Observable call(HttpServerResponse response) { + return response.writeStringAndFlushOnEach(just("data: interval 1\n")); + } + }); + + receiveAndAssertSingleEvent(); + } + + @Test(timeout = 60000) + public void testWriteRawBytes() throws Exception { + startServer(new Func1, Observable>() { + + @Override + public Observable call(HttpServerResponse response) { + return response.writeBytesAndFlushOnEach(just("data: interval 1\n".getBytes())); + } + }); + + receiveAndAssertSingleEvent(); + } + + @Test(timeout = 60000) + public void testWriteServerSentEvent() throws Exception { + startServer(new Func1, Observable>() { + + @Override + public Observable call(HttpServerResponse response) { + return response.writeAndFlushOnEach(just(ServerSentEvent.withData("interval 1"))); + } + }); + + receiveAndAssertSingleEvent(); + } + + protected void receiveAndAssertSingleEvent() { + ServerSentEvent result = receivesSingleEvent(); + Assert.assertNotNull("Unexpected server sent event received.", result); + Assert.assertEquals("Unexpected event data.", "interval 1", result.contentAsString()); + Assert.assertNull("Unexpected event type.", result.getEventType()); + Assert.assertNull("Unexpected event id.", result.getEventId()); + result.release(); + } + + protected ServerSentEvent receivesSingleEvent() { + return receiveSse().take(1).map(new Func1() { + @Override + public ServerSentEvent call(ServerSentEvent serverSentEvent) { + serverSentEvent.retain(); + return serverSentEvent; + } + }).toBlocking().singleOrDefault(null); + } + + private Observable receiveSse() { + + return HttpClient.newClient("127.0.0.1", sseServer.getServerPort()) + .createGet("/") + .flatMap(new Func1, Observable>() { + @Override + public Observable call(HttpClientResponse resp) { + return resp.getContentAsServerSentEvents(); + } + }); + } + + private void startServer(final Func1, Observable> f) { + sseServer = HttpServer.newServer() + .start(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, + HttpServerResponse response) { + return f.call(response.transformToServerSentEvents()); + } + }); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/SseTestUtil.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/SseTestUtil.java new file mode 100644 index 0000000..fec51b4 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/SseTestUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.http.sse; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.HttpContent; + +import java.nio.charset.Charset; + +import static org.junit.Assert.*; + +public final class SseTestUtil { + + private SseTestUtil() { + } + + public static ServerSentEvent newServerSentEvent(String eventType, String eventId, String data) { + ByteBuf eventTypeBuffer = null != eventType + ? Unpooled.buffer().writeBytes(eventType.getBytes(Charset.forName("UTF-8"))) + : null; + ByteBuf eventIdBuffer = null != eventId + ? Unpooled.buffer().writeBytes(eventId.getBytes(Charset.forName("UTF-8"))) + : null; + + ByteBuf dataBuffer = Unpooled.buffer().writeBytes(data.getBytes(Charset.forName("UTF-8"))); + + return ServerSentEvent.withEventIdAndType(eventIdBuffer, eventTypeBuffer, dataBuffer); + } + + public static String newSseProtocolString(String eventType, String eventId, String... dataElements) { + StringBuilder eventStream = new StringBuilder(); + + if (null != eventType) { + eventStream.append("event: ").append(eventType).append('\n'); + } + + if (null != eventId) { + eventStream.append("id: ").append(eventId).append('\n'); + } + + for (String aData : dataElements) { + eventStream.append("data: ").append(aData).append('\n'); + } + return eventStream.toString(); + } + + public static void assertContentEquals(String message, ByteBuf expected, ByteBuf actual) { + assertEquals(message, + null == expected ? null : expected.toString(Charset.defaultCharset()), + null == actual ? null : actual.toString(Charset.defaultCharset())); + } + + public static HttpContent toHttpContent(String event) { + ByteBuf in = Unpooled.buffer(1024); + in.writeBytes(event.getBytes(Charset.defaultCharset())); + return new DefaultHttpContent(in); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoderTest.java new file mode 100644 index 0000000..73e210e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/client/ServerSentEventDecoderTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.sse.client; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent.Type; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.reactivex.netty.protocol.http.sse.SseTestUtil.*; +import static org.junit.Assert.*; + +public class ServerSentEventDecoderTest { + + private final ServerSentEventDecoder decoder = new ServerSentEventDecoder(); + + private final ChannelHandlerContext ch = new EmbeddedChannel(new LoggingHandler()).pipeline().firstContext(); + + @Test(timeout = 60000) + public void testOneDataLineDecode() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data = "data line"; + + ServerSentEvent expected = newServerSentEvent(eventType, eventId, data); + + doTest(newSseProtocolString(eventType, eventId, data), expected); + } + + @Test(timeout = 60000) + public void testMultipleDataLineDecode() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data1 = "data line"; + String data2 = "data line"; + + ServerSentEvent expected1 = newServerSentEvent(eventType, eventId, data1); + ServerSentEvent expected2 = newServerSentEvent(eventType, eventId, data2); + + doTest(newSseProtocolString(eventType, eventId, data1, data2), expected1, expected2); + } + + @Test(timeout = 60000) + public void testEventWithNoIdDecode() throws Exception { + String eventType = "add"; + String data = "data line"; + + ServerSentEvent expected = newServerSentEvent(eventType, null, data); + + doTest(newSseProtocolString(eventType, null, data), expected); + } + + @Test(timeout = 60000) + public void testEventWithNoEventTypeDecode() throws Exception { + String eventId = "1"; + String data = "data line"; + + ServerSentEvent expected = newServerSentEvent(null, eventId, data); + + doTest(newSseProtocolString(null, eventId, data), expected); + } + + @Test(timeout = 60000) + public void testEventWithDataOnlyDecode() throws Exception { + String data = "data line"; + + ServerSentEvent expected = newServerSentEvent(null, null, data); + + doTest(newSseProtocolString(null, null, data), expected); + } + + @Test(timeout = 60000) + public void testResetEventType() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data1 = "data line"; + String data2 = "data line"; + + ServerSentEvent expected1 = newServerSentEvent(eventType, eventId, data1); + ServerSentEvent expected2 = newServerSentEvent(null, eventId, data2); + + doTest(newSseProtocolString(eventType, eventId, data1) + newSseProtocolString("", null, data2), + expected1, expected2); + } + + @Test(timeout = 60000) + public void testResetEventId() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data1 = "data line"; + String data2 = "data line"; + + ServerSentEvent expected1 = newServerSentEvent(eventType, eventId, data1); + ServerSentEvent expected2 = newServerSentEvent(eventType, null, data2); + + doTest(newSseProtocolString(eventType, eventId, data1) + newSseProtocolString(null, "", data2), + expected1, expected2); + } + + @Test(timeout = 60000) + public void testIncompleteEventId() throws Exception { + List out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("id: 111"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + ServerSentEvent expected = newServerSentEvent(null, "1111", "data line"); + + doTest("1\ndata: data line\n", expected); + + } + + @Test(timeout = 60000) + public void testIncompleteEventType() throws Exception { + List out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("event: ad"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + ServerSentEvent expected = newServerSentEvent("add", null, "data line"); + + doTest("d\ndata: data line\n", expected); + + } + + @Test(timeout = 60000) + public void testIncompleteEventData() throws Exception { + ServerSentEvent expected = newServerSentEvent("add", null, "data line"); + + List out = new ArrayList<>(); + + decoder.decode(ch, toHttpContent("event: add\n"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("data: d"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + doTest("ata line\n", expected); + } + + @Test(timeout = 60000) + public void testIncompleteFieldName() throws Exception { + ServerSentEvent expected = newServerSentEvent("add", null, "data line"); + + List out = new ArrayList<>(); + + decoder.decode(ch, toHttpContent("ev"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("ent: add\n d"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + doTest("ata: data line\n", expected); + } + + @Test(timeout = 60000) + public void testInvalidFieldNameAndNextEvent() throws Exception { + ArrayList out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("event: event type\n"), out); + assertTrue("Output list not empty.", out.isEmpty()); + + decoder.decode(ch, toHttpContent("data: dumb \n"), out); + assertFalse("Event not emitted after invalid field name.", out.isEmpty()); + assertEquals("Unexpected event count after invalid field name.", 1, out.size()); + + } + + @Test(timeout = 60000) + public void testInvalidFieldName() throws Throwable { + ArrayList out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("event: dumb \n"), out); + assertTrue("Event emitted for invalid field name.", out.isEmpty()); + } + + @Test(timeout = 60000) + public void testFieldNameWithSpace() throws Throwable { + ArrayList out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("eve nt: dumb \n"), new ArrayList<>()); + assertTrue("Event emitted for invalid field name.", out.isEmpty()); + } + + @Test(timeout = 60000) + public void testDataInMultipleChunks() throws Exception { + ServerSentEvent expected = newServerSentEvent(null, null, "data line"); + + List out = new ArrayList<>(); + + decoder.decode(ch, toHttpContent("da"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("ta: d"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("ata"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent(" "), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("li"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + decoder.decode(ch, toHttpContent("ne"), out); + assertEquals("Unexpected number of decoded messages.", 0, out.size()); + + doTest("\n", expected); + } + + @Test(timeout = 10000) + public void testLeadingNewLineInFieldName() throws Exception { + List out = new ArrayList<>(); + decoder.decode(ch, toHttpContent("\n data: ad\n"), out); + assertEquals("Unexpected number of decoded messages.", 1, out.size()); + } + + @Test(timeout = 10000) + public void testLeadingSpaceInFieldName() throws Exception { + List out = new ArrayList<>(); + decoder.decode(ch, toHttpContent(" data: ad\n"), out); + assertEquals("Unexpected number of decoded messages.", 1, out.size()); + + ServerSentEvent event = (ServerSentEvent) out.get(0); + assertEquals("Unexpected event type.", Type.Data, event.getType()); + assertEquals("Unexpected event type.", "ad", event.contentAsString()); + } + + private void doTest(String eventText, ServerSentEvent... expected) throws Exception { + List out = new ArrayList<>(); + decoder.decode(ch, toHttpContent(eventText), out); + + assertEquals(expected.length, out.size()); + + for (int i = 0; i < out.size(); i++) { + ServerSentEvent event = (ServerSentEvent) out.get(i); + assertContentEquals("Unexpected SSE data", expected[i].content(), event.content()); + assertContentEquals("Unexpected SSE event type", expected[i].getEventType(), + event.getEventType()); + assertContentEquals("Unexpected SSE event id", expected[i].getEventId(), event.getEventId()); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoderTest.java new file mode 100644 index 0000000..cadc256 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/sse/server/ServerSentEventEncoderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.sse.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.protocol.http.sse.ServerSentEvent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.nio.charset.Charset; + +import static io.reactivex.netty.protocol.http.sse.SseTestUtil.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class ServerSentEventEncoderTest { + + @Rule + public final EncoderRule rule = new EncoderRule(); + + @Test(timeout = 60000) + public void testOneDataLineEncode() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data = "data line"; + ServerSentEvent event = newServerSentEvent(eventType, eventId, data); + String expectedOutput = newSseProtocolString(eventType, eventId, data); + rule.test(expectedOutput, event); + } + + @Test(timeout = 60000) + public void testMultipleDataLineEncode() throws Exception { + ServerSentEventEncoder splitEncoder = new ServerSentEventEncoder(true); + EmbeddedChannel channel = new EmbeddedChannel(splitEncoder); + + String eventType = "add"; + String eventId = "1"; + String data1 = "first line"; + String data2 = "second line"; + String data3 = "third line"; + String data = data1 + '\n' + data2 + '\n' + data3; + ServerSentEvent event = newServerSentEvent(eventType, eventId, data); + String expectedOutput = newSseProtocolString(eventType, eventId, data1, data2, data3); + rule.test(channel, expectedOutput, event); + } + + @Test(timeout = 60000) + public void testNoSplitMode() throws Exception { + String eventType = "add"; + String eventId = "1"; + String data = "first line\nsecond line\nthird line"; + ServerSentEvent event = newServerSentEvent(eventType, eventId, data); + String expectedOutput = newSseProtocolString(eventType, eventId, data); + rule.test(expectedOutput, event); + } + + @Test(timeout = 60000) + public void testEventWithNoIdEncode() throws Exception { + String eventType = "add"; + String data = "data line"; + ServerSentEvent event = newServerSentEvent(eventType, null, data); + String expectedOutput = newSseProtocolString(eventType, null, data); + rule.test(expectedOutput, event); + } + + @Test(timeout = 60000) + public void testEventWithNoEventTypeEncode() throws Exception { + String eventId = "1"; + String data = "data line"; + ServerSentEvent event = newServerSentEvent(null, eventId, data); + String expectedOutput = newSseProtocolString(null, eventId, data); + rule.test(expectedOutput, event); + } + + @Test(timeout = 60000) + public void testEventWithDataOnlyEncode() throws Exception { + String data = "data line"; + ServerSentEvent event = newServerSentEvent(null, null, data); + String expectedOutput = newSseProtocolString(null, null, data); + rule.test(expectedOutput, event); + } + + public static class EncoderRule extends ExternalResource { + + private ServerSentEventEncoder encoder; + private EmbeddedChannel channel; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + encoder = new ServerSentEventEncoder(); + channel = new EmbeddedChannel(encoder); + base.evaluate(); + } + }; + } + + public void test(String expectedOutput, ServerSentEvent... toEncode) { + test(channel, expectedOutput, toEncode); + } + + public void test(EmbeddedChannel channel, String expectedOutput, ServerSentEvent... toEncode) { + + for (ServerSentEvent event : toEncode) { + channel.writeAndFlush(event); + } + + final ByteBuf allOut = Unpooled.buffer(); + ByteBuf anOut; + while ((anOut = channel.readOutbound()) != null) { + allOut.writeBytes(anOut); + } + + assertThat("Unexpected encoder output", allOut.toString(Charset.defaultCharset()), equalTo(expectedOutput)); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnectionTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnectionTest.java new file mode 100644 index 0000000..87b5277 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/client/OperatorCacheSingleWebsocketConnectionTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.ws.client; + +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Func1; +import rx.observers.TestSubscriber; +import rx.subjects.Subject; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class OperatorCacheSingleWebsocketConnectionTest { + + @Rule + public final OpRule opRule = new OpRule(); + + @Test(timeout = 60000) + public void testLifecycleNeverEnds() throws Exception { + opRule.subscribeAndAssertValues(opRule.getSourceWithCache().repeat(2), 1); + } + + @Test(timeout = 60000) + public void testLifecycleCompletesImmediately() throws Exception { + opRule.subscribeAndAssertValues(opRule.getSourceWithCache() + .map(new Func1() { + @Override + public WebSocketConnection call(WebSocketConnection c) { + opRule.terminateFirstConnection(null); + return c; + } + }).repeat(2), + 2);// Since the cached item is immediately invalid, two items will be emitted from source. + } + + @Test(timeout = 60000) + public void testLifecycleErrorsImmediately() throws Exception { + opRule.subscribeAndAssertValues(opRule.getSourceWithCache() + .map(new Func1() { + @Override + public WebSocketConnection call(WebSocketConnection c) { + opRule.terminateFirstConnection(new IllegalStateException()); + return c; + } + }).repeat(2), + 2);// Since the cached item is immediately invalid, two items will be emitted from source. + } + + @Test(timeout = 60000) + public void testSourceEmitsNoItems() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.>>empty() + .lift(new OperatorCacheSingleWebsocketConnection()) + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertError(IllegalStateException.class); + } + + @Test(timeout = 60000) + public void testSourceEmitsError() throws Exception { + TestSubscriber subscriber = new TestSubscriber<>(); + + Observable.>error(new NullPointerException()) + .nest() + .lift(new OperatorCacheSingleWebsocketConnection()) + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertError(NullPointerException.class); + } + + @Test(timeout = 60000) + public void testUnsubscribeFromLifecycle() throws Exception { + + opRule.subscribeAndAssertValues(opRule.getSourceWithCache(), 1); + LifecycleSubject lifecycleSubject = opRule.lifecycles.poll(); + assertThat("No subscribers to lifecycle.", lifecycleSubject.subscribers, hasSize(1)); + assertThat("Lifecycle subscriber not unsubscribed.", lifecycleSubject.subscribers.poll().isUnsubscribed(), + is(true)); + } + + private static class OpRule extends ExternalResource { + + private Observable>> source; + private ConcurrentLinkedQueue lifecycles; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + lifecycles = new ConcurrentLinkedQueue<>(); + source = Observable.create(new OnSubscribe>() { + @Override + public void call(Subscriber> s) { + LifecycleSubject l = new LifecycleSubject(new ConcurrentLinkedQueue>()); + lifecycles.add(l); + WebSocketConnection conn = newConnection(l); + /*Subscriptions to the emitted Observable will always give the same connection but + subscription to the source (this Observable) creates a new connection. This simulates + how the actual websocket connection get works.*/ + s.onNext(Observable.just(conn)); + s.onCompleted(); + + } + }).nest(); + base.evaluate(); + } + }; + } + + public boolean terminateFirstConnection(Throwable error) { + LifecycleSubject poll = lifecycles.poll(); + + if (null != poll) { + if (null == error) { + poll.onCompleted(); + } else { + poll.onError(error); + } + return true; + } + + return false; + } + + public Observable getSourceWithCache() { + return source.lift(new OperatorCacheSingleWebsocketConnection()); + } + + public void subscribeAndAssertValues(Observable source, int distinctItemsCount) { + TestSubscriber subscriber = new TestSubscriber<>(); + + source.subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + List onNextEvents = subscriber.getOnNextEvents(); + Set distinctConns = new HashSet<>(onNextEvents); + assertThat("Unexpected number of distinct connections.", distinctConns, hasSize(distinctItemsCount)); + } + + private WebSocketConnection newConnection(final Observable lifecycle) { + @SuppressWarnings("unchecked") + Connection mock = Mockito.mock(Connection.class); + Mockito.when(mock.closeListener()).thenAnswer(new Answer>() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + return lifecycle; + } + }); + return new WebSocketConnection(mock); + } + } + + private static class LifecycleSubject extends Subject { + + private final ConcurrentLinkedQueue> subscribers; + + protected LifecycleSubject(final ConcurrentLinkedQueue> subscribers) { + super(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscribers.add(subscriber); + } + }); + this.subscribers = subscribers; + } + + @Override + public boolean hasObservers() { + return !subscribers.isEmpty(); + } + + @Override + public void onCompleted() { + for (Subscriber subscriber : subscribers) { + subscriber.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + for (Subscriber subscriber : subscribers) { + subscriber.onError(e); + } + } + + @Override + public void onNext(Void aVoid) { + // No op ... + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/server/WSEagerInputSubscriptionHandlerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/server/WSEagerInputSubscriptionHandlerTest.java new file mode 100644 index 0000000..bdcd8d6 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/http/ws/server/WSEagerInputSubscriptionHandlerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.http.ws.server; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.server.HttpServer; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import io.reactivex.netty.protocol.http.ws.WebSocketConnection; +import io.reactivex.netty.protocol.http.ws.client.WebSocketResponse; +import org.junit.Test; +import rx.Observable; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +public class WSEagerInputSubscriptionHandlerTest { + + @Test(timeout = 60000) + public void testHandlerSubscribesEagerly() throws Exception { + HttpServer server = + HttpServer.newServer() + .start(new RequestHandler() { + @Override + public Observable handle(HttpServerRequest request, + HttpServerResponse response) { + if (request.isWebSocketUpgradeRequested()) { + return response.acceptWebSocketUpgrade( + new WebSocketHandler() { + @Override + public Observable handle(WebSocketConnection wsConnection) { + wsConnection.getInput().subscribe(); + return Observable.never(); + } + }); + } else { + return response.setStatus(HttpResponseStatus.NOT_FOUND); + } + } + }); + + TestSubscriber subscriber = new TestSubscriber<>(); + HttpClient.newClient(server.getServerAddress()) + .createGet("/ws") + .requestWebSocketUpgrade() + .flatMap(new Func1, Observable>() { + @Override + public Observable call(WebSocketResponse wsResp) { + if (wsResp.isUpgraded()) { + return Observable.empty(); + } + return Observable.error(new IllegalStateException("WebSocket upgrade not accepted.")); + } + }) + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/EventListenerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/EventListenerTest.java new file mode 100644 index 0000000..ae1a485 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/EventListenerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.protocol.tcp.server.ConnectionHandler; +import io.reactivex.netty.protocol.tcp.server.TcpServer; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.net.SocketAddress; + +public class EventListenerTest { + + @Rule + public final TcpServerRule rule = new TcpServerRule(); + + @Test(timeout = 60000) + public void testEventListener() throws Exception { + TcpClient client = TcpClient.newClient(rule.serverAddress); + + assertListenerCalled(client); + } + + @Test(timeout = 60000) + public void testEventListenerPostCopy() throws Exception { + TcpClient client = TcpClient.newClient(rule.serverAddress) + .enableWireLogging("test", LogLevel.ERROR); + + assertListenerCalled(client); + } + + @Test(timeout = 60000) + public void testSubscriptionPreCopy() throws Exception { + TcpClient client = TcpClient.newClient(rule.serverAddress); + + MockTcpClientEventListener listener = subscribe(client); + + client = client.enableWireLogging("test", LogLevel.DEBUG); + + connectAndAssertListenerInvocation(client, listener); + } + + private static void assertListenerCalled(TcpClient client) { + MockTcpClientEventListener listener = subscribe(client); + connectAndAssertListenerInvocation(client, listener); + } + + private static void connectAndAssertListenerInvocation(TcpClient client, + MockTcpClientEventListener listener) { + TestSubscriber subscriber = new TestSubscriber<>(); + client.createConnectionRequest().flatMap(new Func1, Observable>() { + @Override + public Observable call(Connection c) { + return c.getInput(); + } + }).take(1).subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + + listener.assertMethodsCalled(Event.BytesRead); + } + + private static MockTcpClientEventListener subscribe(TcpClient client) { + MockTcpClientEventListener listener = new MockTcpClientEventListener(); + client.subscribe(listener); + return listener; + } + + public static class TcpServerRule extends ExternalResource { + + private SocketAddress serverAddress; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + serverAddress = TcpServer.newServer().start(new ConnectionHandler() { + @Override + public Observable handle(Connection newConnection) { + return newConnection.writeString(Observable.just("Hello")); + } + }).getServerAddress(); + base.evaluate(); + } + }; + } + } + +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/MockTcpClientEventListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/MockTcpClientEventListener.java new file mode 100644 index 0000000..f499278 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/MockTcpClientEventListener.java @@ -0,0 +1,192 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.client; + +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; +import io.reactivex.netty.test.util.MockClientEventListener; +import io.reactivex.netty.test.util.MockClientEventListener.ClientEvent; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; + +import java.util.concurrent.TimeUnit; + +public class MockTcpClientEventListener extends TcpClientEventListener { + + private final MockClientEventListener mockDelegate = new MockClientEventListener(); + + public void assertMethodCalled(ClientEvent event) { + mockDelegate.assertMethodCalled(event); + } + + public void assertMethodsCalled(ClientEvent... events) { + mockDelegate.assertMethodsCalled(events); + } + + public void assertMethodCalled(Event event) { + mockDelegate.assertMethodCalled(event); + } + + public void assertMethodsCalled(Event... events) { + mockDelegate.assertMethodsCalled(events); + } + + public long getDuration() { + return mockDelegate.getDuration(); + } + + public Throwable getRecievedError() { + return mockDelegate.getRecievedError(); + } + + public TimeUnit getTimeUnit() { + return mockDelegate.getTimeUnit(); + } + + @Override + public void onByteRead(long bytesRead) { + mockDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + mockDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onCompleted() { + mockDelegate.onCompleted(); + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + mockDelegate.onConnectFailed(duration, timeUnit, recievedError); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + mockDelegate.onConnectionCloseFailed(duration, timeUnit, recievedError); + } + + @Override + public void onConnectionCloseStart() { + mockDelegate.onConnectionCloseStart(); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + mockDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectStart() { + mockDelegate.onConnectStart(); + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + mockDelegate.onConnectSuccess(duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event) { + mockDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + mockDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, + Throwable throwable) { + mockDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + mockDelegate.onCustomEvent(event, throwable); + } + + @Override + public void onFlushStart() { + mockDelegate.onFlushStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + mockDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + mockDelegate.onPoolAcquireFailed(duration, timeUnit, recievedError); + } + + @Override + public void onPoolAcquireStart() { + mockDelegate.onPoolAcquireStart(); + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + mockDelegate.onPoolAcquireSuccess(duration, timeUnit); + } + + @Override + public void onPooledConnectionEviction() { + mockDelegate.onPooledConnectionEviction(); + } + + @Override + public void onPooledConnectionReuse() { + mockDelegate.onPooledConnectionReuse(); + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + mockDelegate.onPoolReleaseFailed(duration, timeUnit, recievedError); + } + + @Override + public void onPoolReleaseStart() { + mockDelegate.onPoolReleaseStart(); + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + mockDelegate.onPoolReleaseSuccess(duration, timeUnit); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + mockDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onWriteStart() { + mockDelegate.onWriteStart(); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + mockDelegate.onWriteSuccess(duration, timeUnit); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/PoolingWithRealChannelTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/PoolingWithRealChannelTest.java new file mode 100644 index 0000000..8555356 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/PoolingWithRealChannelTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.client.pool.PooledConnection; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.observers.AssertableSubscriber; +import org.junit.Test; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.observers.AssertableSubscriber; + +import java.util.ArrayList; +import java.util.List; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static rx.Observable.fromCallable; +import static rx.Observable.just; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static rx.Observable.fromCallable; +import static rx.Observable.just; + +/** + * This tests the code paths which are not invoked for {@link EmbeddedChannel} as it does not schedule any task + * (an EmbeddedChannelEventLopp never returns false for isInEventLoop()) + */ +public class PoolingWithRealChannelTest { + + @Rule + public final TcpClientRule clientRule = new TcpClientRule(); + + /** + * This test validates the async onNext and synchronous onComplete/onError nature of pooling when the connection is + * reused. + */ + //@Test(timeout = 60000) + public void testReuse() throws Exception { + clientRule.startServer(1); + PooledConnection connection = clientRule.connect(); + connection.closeNow(); + + assertThat("Pooled connection is closed.", connection.unsafeNettyChannel().isOpen(), is(true)); + + PooledConnection connection2 = clientRule.connect(); + + assertThat("Connection is not reused.", connection2, is(connection)); + } + + @Test + /** + * + * Load test to prove concurrency issues mainly seen on heavy load. + * + */ + public void testLoad() { + clientRule.startServer(1000); + + MockTcpClientEventListener listener = new MockTcpClientEventListener(); + clientRule.getClient().subscribe(listener); + + + int number_of_iterations = 300; + int numberOfRequests = 10; + + for(int j = 0; j < number_of_iterations; j++) { + + List> results = new ArrayList<>(); + + //Just giving the client some time to recover + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + for (int i = 0; i < numberOfRequests; i++) { + results.add( + fromCallable(new Func0>() { + @Override + public PooledConnection call() { + return clientRule.connectWithCheck(); + } + }) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(PooledConnection connection) { + return connection.writeStringAndFlushOnEach(just("Hello")) + .toCompletable() + .toObservable() + .concatWith(connection.getInput()) + .take(1) + .single() + .map(new Func1() { + @Override + public String call(ByteBuf byteBuf) { + try { + + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(bytes); + String result = new String(bytes); + return result; + } finally { + byteBuf.release(); + } + } + }).doOnError(new Action1() { + @Override + public void call(Throwable throwable) { + Assert.fail("Did not expect exception: " + throwable.getMessage()); + throwable.printStackTrace(); + } + }); + } + })); + } + AssertableSubscriber test = Observable.merge(results).test(); + test.awaitTerminalEvent(); + test.assertNoErrors(); + } + } + + @Test + /** + * + * Load test to prove concurrency issues mainly seen on heavy load. + * + */ + public void assertPermitsAreReleasedWhenMergingObservablesWithExceptions() { + clientRule.startServer(10, true); + + MockTcpClientEventListener listener = new MockTcpClientEventListener(); + clientRule.getClient().subscribe(listener); + + int number_of_iterations = 1; + int numberOfRequests = 3; + + makeRequests(number_of_iterations, numberOfRequests); + + sleep(clientRule.getPoolConfig().getMaxIdleTimeMillis()); + + assertThat("Permits should be 10", clientRule.getPoolConfig().getPoolLimitDeterminationStrategy().getAvailablePermits(), equalTo(10)); + } + + private void sleep(long i) { + try { + Thread.sleep(i); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void makeRequests(int number_of_iterations, int numberOfRequests) { + for (int j = 0; j < number_of_iterations; j++) { + + //List> results = new ArrayList<>(); + + sleep(100); + + List> results = new ArrayList<>(); + + //Just giving the client some time to recover + sleep(100); + + for (int i = 0; i < numberOfRequests; i++) { + results.add( + fromCallable(new Func0>() { + @Override + public PooledConnection call() { + return clientRule.connect(); + } + }) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(PooledConnection connection) { + return connection.writeStringAndFlushOnEach(just("Hello")) + .toCompletable() + .toObservable() + .concatWith(connection.getInput()) + .take(1) + .single() + .map(new Func1() { + @Override + public String call(ByteBuf byteBuf) { + try { + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(bytes); + return new String(bytes); + } finally { + byteBuf.release(); + } + } + }); + } + })); + } + AssertableSubscriber test = Observable.merge(results).test(); + test.awaitTerminalEvent(); + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientImplTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientImplTest.java new file mode 100644 index 0000000..46204ef --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientImplTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.logging.LogLevel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.reactivex.netty.client.ClientState; +import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventPublisher; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import rx.functions.Action1; +import rx.functions.Func0; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TcpClientImplTest { + + @Mock(answer = Answers.RETURNS_MOCKS) + private ClientState state; + + @Test(timeout = 60000) + public void testChannelOption() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + TcpClientImpl newClient = + (TcpClientImpl) client.channelOption(ChannelOption.AUTO_READ, true); + + assertDeepClientCopy(client, newClient); + + verify(state).channelOption(ChannelOption.AUTO_READ, true); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerFirst() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerFirst("handler", factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerFirst("handler", factory); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerFirstWithExecutor() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + EventExecutorGroup group = new NioEventLoopGroup(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerFirst(group, "handler", factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerFirst(group, "handler", factory); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerLast() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerLast("handler", factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerLast("handler", factory); + + } + + @Test(timeout = 60000) + public void testAddChannelHandlerLastWithExecutor() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + EventExecutorGroup group = new NioEventLoopGroup(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerLast(group, "handler", factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerLast(group, "handler", factory); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerBefore() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerBefore("base", "handler", + factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerBefore("base", "handler", factory); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerBeforeWithExecutor() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + EventExecutorGroup group = new NioEventLoopGroup(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerBefore(group, "base", "handler", + factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerBefore(group, "base", "handler", factory); + + } + + @Test(timeout = 60000) + public void testAddChannelHandlerAfter() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerAfter("base", "handler", + factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerAfter("base", "handler", factory); + } + + @Test(timeout = 60000) + public void testAddChannelHandlerAfterWithExecutor() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Func0 factory = newHandlerFactory(); + EventExecutorGroup group = new NioEventLoopGroup(); + TcpClientImpl newClient = + (TcpClientImpl) client.addChannelHandlerAfter(group, "base", "handler", + factory); + + assertDeepClientCopy(client, newClient); + + verify(state).addChannelHandlerAfter(group, "base", "handler", factory); + + } + + @Test(timeout = 60000) + public void testPipelineConfigurator() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + Action1 configurator = new Action1() { + @Override + public void call(ChannelPipeline pipeline) { + } + }; + TcpClientImpl newClient = + (TcpClientImpl) client.pipelineConfigurator(configurator); + + assertDeepClientCopy(client, newClient); + + verify(state).pipelineConfigurator(configurator); + } + + @Test(timeout = 60000) + public void testEnableWireLogging() throws Exception { + TcpClientImpl client = TcpClientImpl._create(state, new TcpClientEventPublisher()); + ClientState state = client.getClientState(); + TcpClientImpl newClient = + (TcpClientImpl) client.enableWireLogging("test", LogLevel.DEBUG); + + assertDeepClientCopy(client, newClient); + + verify(state).enableWireLogging("test", LogLevel.DEBUG); + } + + private static void assertDeepClientCopy(TcpClientImpl client, + TcpClientImpl newClient) { + assertThat("Client was not copied.", newClient, is(not(client))); + assertThat("Client state was not copied.", newClient.getClientState(), + is(not(client.getClientState()))); + } + + private static Func0 newHandlerFactory() { + return new Func0() { + @Override + public ChannelHandler call() { + return new ChannelDuplexHandler(); + } + }; + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientRule.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientRule.java new file mode 100644 index 0000000..36b15f9 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/TcpClientRule.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.pool.PoolConfig; +import io.reactivex.netty.client.pool.PooledConnection; +import io.reactivex.netty.client.pool.SingleHostPoolingProviderFactory; +import io.reactivex.netty.protocol.tcp.server.ConnectionHandler; +import io.reactivex.netty.protocol.tcp.server.TcpServer; +import org.junit.Assert; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.Observer; +import rx.functions.Func0; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +public class TcpClientRule extends ExternalResource { + + private TcpServer server; + private TcpClient client; + private PoolConfig poolConfig; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + server = TcpServer.newServer(); + base.evaluate(); + } + }; + } + + public void startServer(int maxConnections) { + startServer(maxConnections, false); + } + + public void startServer(int maxConnections, final boolean failing) { + server.start(new ConnectionHandler() { + @Override + public Observable handle(Connection newConnection) { + if(failing) { + throw new RuntimeException("exception"); + } else { + return newConnection.writeAndFlushOnEach(newConnection.getInput()); + } + } + }); + createClient(maxConnections); + } + + public void startServer(ConnectionHandler handler, int maxConnections) { + server.start(handler); + createClient(maxConnections); + } + + public PooledConnection connect() { + + TestSubscriber> cSub = new TestSubscriber<>(); + client.createConnectionRequest().subscribe(cSub); + + cSub.awaitTerminalEvent(); + + cSub.assertNoErrors(); + + assertThat("No connection received.", cSub.getOnNextEvents(), hasSize(1)); + + return (PooledConnection) cSub.getOnNextEvents().get(0); + } + + public PooledConnection connectWithCheck() { + + final AtomicBoolean gotOnNext = new AtomicBoolean(false); + + Observable> got_no_connection = client.createConnectionRequest() + .doOnEach(new Observer>() { + @Override + public void onCompleted() { + if(!gotOnNext.get()) { + //A PooledConnection could sometimes send onCompleted before the onNext event occurred. + Assert.fail("Should not get onCompletedBefore onNext"); + } + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Connection byteBufByteBufConnection) { + gotOnNext.set(true); + } + }) + .switchIfEmpty(Observable.defer(new Func0>>() { + @Override + public Observable> call() { + return Observable.empty(); + } + })); + + TestSubscriber> cSub = new TestSubscriber<>(); + got_no_connection.subscribe(cSub); + + cSub.awaitTerminalEvent(); + + cSub.assertNoErrors(); + + assertThat("No connection received.", cSub.getOnNextEvents(), hasSize(1)); + + return (PooledConnection) cSub.getOnNextEvents().get(0); + } + + private void createClient(final int maxConnections) { + InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", server.getServerPort()); + poolConfig = new PoolConfig().maxConnections(maxConnections); + SingleHostPoolingProviderFactory bounded = SingleHostPoolingProviderFactory.create(poolConfig); + client = TcpClient.newClient(bounded, Observable.just(new Host(serverAddr))); + } + + public TcpServer getServer() { + return server; + } + + public TcpClient getClient() { + return client; + } + + public PoolConfig getPoolConfig() { + return poolConfig; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisherTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisherTest.java new file mode 100644 index 0000000..c664c3e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventPublisherTest.java @@ -0,0 +1,267 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.client.events; + +import io.reactivex.netty.protocol.tcp.client.MockTcpClientEventListener; +import io.reactivex.netty.test.util.MockClientEventListener.ClientEvent; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class TcpClientEventPublisherTest { + + @Rule + public PublisherRule rule = new PublisherRule(); + + @Test(timeout = 60000) + public void testOnConnectStart() throws Exception { + rule.publisher.onConnectStart(); + + rule.listener.assertMethodsCalled(ClientEvent.ConnectStart); + } + + @Test(timeout = 60000) + public void testOnConnectSuccess() throws Exception { + rule.publisher.onConnectSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(ClientEvent.ConnectSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnConnectFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onConnectFailed(1, TimeUnit.MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(ClientEvent.ConnectFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnPoolReleaseStart() throws Exception { + rule.publisher.onPoolReleaseStart(); + + rule.listener.assertMethodsCalled(ClientEvent.ReleaseStart); + } + + @Test(timeout = 60000) + public void testOnPoolReleaseSuccess() throws Exception { + rule.publisher.onPoolReleaseSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(ClientEvent.ReleaseSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnPoolReleaseFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onPoolReleaseFailed(1, TimeUnit.MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(ClientEvent.ReleaseFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnPooledConnectionEviction() throws Exception { + rule.publisher.onPooledConnectionEviction(); + + rule.listener.assertMethodsCalled(ClientEvent.Eviction); + } + + @Test(timeout = 60000) + public void testOnPooledConnectionReuse() throws Exception { + rule.publisher.onPooledConnectionReuse(); + + rule.listener.assertMethodsCalled(ClientEvent.Reuse); + } + + @Test(timeout = 60000) + public void testOnPoolAcquireStart() throws Exception { + rule.publisher.onPoolAcquireStart(); + + rule.listener.assertMethodsCalled(ClientEvent.AcquireStart); + } + + @Test(timeout = 60000) + public void testOnPoolAcquireSuccess() throws Exception { + rule.publisher.onPoolAcquireSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(ClientEvent.AcquireSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnPoolAcquireFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onPoolAcquireFailed(1, TimeUnit.MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(ClientEvent.AcquireFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(TimeUnit.MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnByteRead() throws Exception { + rule.publisher.onByteRead(1); + + rule.listener.assertMethodsCalled(Event.BytesRead); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnByteWritten() throws Exception { + rule.publisher.onByteWritten(1); + + rule.listener.assertMethodsCalled(Event.BytesWritten); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushStart() throws Exception { + rule.publisher.onFlushStart(); + + rule.listener.assertMethodsCalled(Event.FlushStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushSuccess() throws Exception { + rule.publisher.onFlushComplete(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(Event.FlushSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteStart() throws Exception { + rule.publisher.onWriteStart(); + + rule.listener.assertMethodsCalled(Event.WriteStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteSuccess() throws Exception { + rule.publisher.onWriteSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(Event.WriteSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteFailed() throws Exception { + rule.publisher.onWriteFailed(1, TimeUnit.MILLISECONDS, new NullPointerException()); + + rule.listener.assertMethodsCalled(Event.WriteFailed); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseStart() throws Exception { + rule.publisher.onConnectionCloseStart(); + + rule.listener.assertMethodsCalled(Event.CloseStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseSuccess() throws Exception { + rule.publisher.onConnectionCloseSuccess(1, TimeUnit.MILLISECONDS); + + rule.listener.assertMethodsCalled(Event.CloseSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseFailed() throws Exception { + rule.publisher.onConnectionCloseFailed(1, TimeUnit.MILLISECONDS, new NullPointerException()); + + rule.listener.assertMethodsCalled(Event.CloseFailed); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + assertThat("Publishing not enabled.", rule.publisher.publishingEnabled(), is(true)); + } + + @Test(timeout = 60000) + public void testCustomEvent() throws Exception { + rule.publisher.onCustomEvent("Hello"); + rule.listener.assertMethodsCalled(Event.CustomEvent); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testCustomEventWithError() throws Exception { + rule.publisher.onCustomEvent("Hello", new NullPointerException()); + rule.listener.assertMethodsCalled(Event.CustomEventWithError); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testCustomEventWithDuration() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, TimeUnit.MINUTES); + rule.listener.assertMethodsCalled(Event.CustomEventWithDuration); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testCustomEventWithDurationAndError() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, TimeUnit.MINUTES, new NullPointerException()); + rule.listener.assertMethodsCalled(Event.CustomEventWithDurationAndError); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testCopy() throws Exception { + TcpClientEventPublisher copy = rule.publisher.copy(); + + assertThat("Publisher not copied.", copy, is(not(sameInstance(rule.publisher)))); + assertThat("Listeners not copied.", copy.getListeners(), is(not(sameInstance(rule.publisher.getListeners())))); + } + + public static class PublisherRule extends ExternalResource { + + private MockTcpClientEventListener listener; + private TcpClientEventPublisher publisher; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + listener = new MockTcpClientEventListener(); + publisher = new TcpClientEventPublisher(); + publisher.subscribe(listener); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventsTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventsTest.java new file mode 100644 index 0000000..186e6c8 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/client/events/TcpClientEventsTest.java @@ -0,0 +1,51 @@ +package io.reactivex.netty.protocol.tcp.client.events; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.client.pool.PooledConnection; +import io.reactivex.netty.protocol.tcp.client.MockTcpClientEventListener; +import io.reactivex.netty.protocol.tcp.client.TcpClientRule; +import io.reactivex.netty.test.util.MockClientEventListener.ClientEvent; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import org.junit.Rule; +import org.junit.Test; +import rx.Observable; +import rx.observers.TestSubscriber; + +public class TcpClientEventsTest { + + @Rule + public final TcpClientRule clientRule = new TcpClientRule(); + + @Test(timeout = 60000) + public void testEventsPublished() throws Exception { + MockTcpClientEventListener listener = sendRequests(); + + listener.assertMethodCalled(ClientEvent.AcquireStart); + listener.assertMethodCalled(ClientEvent.AcquireSuccess); + listener.assertMethodCalled(ClientEvent.ConnectStart); + listener.assertMethodCalled(ClientEvent.ConnectSuccess); + listener.assertMethodCalled(Event.WriteStart); + listener.assertMethodCalled(Event.WriteSuccess); + listener.assertMethodCalled(Event.FlushStart); + listener.assertMethodCalled(Event.FlushSuccess); + listener.assertMethodCalled(Event.BytesRead); + } + + protected MockTcpClientEventListener sendRequests() { + clientRule.startServer(10); + MockTcpClientEventListener listener = new MockTcpClientEventListener(); + clientRule.getClient().subscribe(listener); + PooledConnection connection = clientRule.connect(); + TestSubscriber testSubscriber = new TestSubscriber<>(); + connection.writeStringAndFlushOnEach(Observable.just("Hello")) + .toCompletable() + .toObservable() + .concatWith(connection.getInput()) + .take(1) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + return listener; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/UnexpectedConnectionHandlerErrorsTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/UnexpectedConnectionHandlerErrorsTest.java new file mode 100644 index 0000000..3fd87c0 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/UnexpectedConnectionHandlerErrorsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.protocol.tcp.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelOption; +import io.netty.handler.logging.LogLevel; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.protocol.tcp.client.TcpClient; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.observers.TestSubscriber; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class UnexpectedConnectionHandlerErrorsTest { + + @Rule + public final ErrorRule rule = new ErrorRule(); + + @Test(timeout = 60000) + public void testHandlerReturnsNull() throws Exception { + rule.server.start(new ConnectionHandler() { + @Override + public Observable handle(Connection newConnection) { + return null; + } + }); + + connectAndWaitForClose(); + } + + @Test(timeout = 60000) + public void testHandlerReturnsError() throws Exception { + rule.server.start(new ConnectionHandler() { + @Override + public Observable handle(Connection newConnection) { + return Observable.error(new IllegalStateException("Deliberate Exception")); + } + }); + + connectAndWaitForClose(); + } + + @Test(timeout = 60000) + public void testHandlerThrowsError() throws Exception { + rule.server.start(new ConnectionHandler() { + @Override + public Observable handle(Connection newConnection) { + return Observable.error(new IllegalStateException("Deliberate Exception")); + } + }); + + connectAndWaitForClose(); + } + + private void connectAndWaitForClose() { + Connection connection = rule.connectToServer(); + + TestSubscriber subscriber = new TestSubscriber<>(); + connection.closeListener().subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + } + + public static class ErrorRule extends ExternalResource { + + private TcpServer server; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + server = TcpServer.newServer(0).enableWireLogging("test", LogLevel.ERROR); + base.evaluate(); + } + }; + } + + public Connection connectToServer() { + final TestSubscriber> subscriber = new TestSubscriber<>(); + + TcpClient.newClient("127.0.0.1", server.getServerPort()) + .channelOption(ChannelOption.AUTO_READ, true) /*Else nothing is read from the channel even close*/ + .enableWireLogging("test", LogLevel.ERROR) + .createConnectionRequest() + .subscribe(subscriber); + + subscriber.awaitTerminalEvent(); + subscriber.assertNoErrors(); + assertThat("No connection available.", subscriber.getOnNextEvents(), hasSize(1)); + + return subscriber.getOnNextEvents().get(0); + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisherTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisherTest.java new file mode 100644 index 0000000..f9b8bd1 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/protocol/tcp/server/events/TcpServerEventPublisherTest.java @@ -0,0 +1,204 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.protocol.tcp.server.events; + +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; +import io.reactivex.netty.test.util.MockTcpServerEventListener; +import io.reactivex.netty.test.util.MockTcpServerEventListener.ServerEvent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import static java.util.concurrent.TimeUnit.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class TcpServerEventPublisherTest { + + @Rule + public final PublisherRule rule = new PublisherRule(); + + @Test(timeout = 60000) + public void testOnNewClientConnected() throws Exception { + rule.publisher.onNewClientConnected(); + + rule.listener.assertMethodsCalled(ServerEvent.NewClient); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingStart() throws Exception { + rule.publisher.onConnectionHandlingStart(1, MILLISECONDS); + + rule.listener.assertMethodsCalled(ServerEvent.HandlingStart); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingSuccess() throws Exception { + rule.publisher.onConnectionHandlingSuccess(1, MILLISECONDS); + + rule.listener.assertMethodsCalled(ServerEvent.HandlingSuccess); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + } + + @Test(timeout = 60000) + public void testOnConnectionHandlingFailed() throws Exception { + final Throwable expected = new NullPointerException(); + + rule.publisher.onConnectionHandlingFailed(1, MILLISECONDS, expected); + + rule.listener.assertMethodsCalled(ServerEvent.HandlingFailed); + + assertThat("Listener not called with duration.", rule.listener.getDuration(), is(1L)); + assertThat("Listener not called with time unit.", rule.listener.getTimeUnit(), is(MILLISECONDS)); + assertThat("Listener not called with error.", rule.listener.getRecievedError(), is(expected)); + } + + @Test(timeout = 60000) + public void testOnByteRead() throws Exception { + rule.publisher.onByteRead(1); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.BytesRead); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnByteWritten() throws Exception { + rule.publisher.onByteWritten(1); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.BytesWritten); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushStart() throws Exception { + rule.publisher.onFlushStart(); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.FlushStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnFlushSuccess() throws Exception { + rule.publisher.onFlushComplete(1, MILLISECONDS); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.FlushSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteStart() throws Exception { + rule.publisher.onWriteStart(); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.WriteStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteSuccess() throws Exception { + rule.publisher.onWriteSuccess(1, MILLISECONDS); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.WriteSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnWriteFailed() throws Exception { + rule.publisher.onWriteFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.WriteFailed); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseStart() throws Exception { + rule.publisher.onConnectionCloseStart(); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.CloseStart); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseSuccess() throws Exception { + rule.publisher.onConnectionCloseSuccess(1, MILLISECONDS); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.CloseSuccess); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnConnectionCloseFailed() throws Exception { + rule.publisher.onConnectionCloseFailed(1, MILLISECONDS, new NullPointerException()); + + rule.listener.getConnDelegate().assertMethodsCalled(Event.CloseFailed); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEvent() throws Exception { + rule.publisher.onCustomEvent("Hello"); + rule.listener.getConnDelegate().assertMethodsCalled(Event.CustomEvent); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithError() throws Exception { + rule.publisher.onCustomEvent("Hello", new NullPointerException()); + rule.listener.getConnDelegate().assertMethodsCalled(Event.CustomEventWithError); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDuration() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, MINUTES); + rule.listener.getConnDelegate().assertMethodsCalled(Event.CustomEventWithDuration); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testOnCustomEventWithDurationError() throws Exception { + rule.publisher.onCustomEvent("Hello", 1, MINUTES, new NullPointerException()); + rule.listener.getConnDelegate().assertMethodsCalled(Event.CustomEventWithDurationAndError); // Test for Connection publisher should verify rest + } + + @Test(timeout = 60000) + public void testPublishingEnabled() throws Exception { + assertThat("Publishing not enabled.", rule.publisher.publishingEnabled(), is(true)); + } + + @Test(timeout = 60000) + public void testCopy() throws Exception { + final TcpServerEventPublisher copy = rule.publisher.copy(); + + assertThat("Publisher not copied.", copy, is(not(sameInstance(rule.publisher)))); + assertThat("Listeners not copied.", copy.getListeners(), is(not(sameInstance(rule.publisher.getListeners())))); + assertThat("Listeners not copied.", copy.getConnDelegate(), + is(not(sameInstance(rule.publisher.getConnDelegate())))); + } + + public static class PublisherRule extends ExternalResource { + + private MockTcpServerEventListener listener; + private TcpServerEventPublisher publisher; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + listener = new MockTcpServerEventListener(); + publisher = new TcpServerEventPublisher(); + publisher.subscribe(listener); + base.evaluate(); + } + }; + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/FlushSelector.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/FlushSelector.java new file mode 100644 index 0000000..fa36b49 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/FlushSelector.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import rx.functions.Func1; + +public class FlushSelector implements Func1 { + + private final int flushEvery; + private int count; + + public FlushSelector(int flushEvery) { + this.flushEvery = flushEvery; + } + + @Override + public Boolean call(T o) { + return ++count % flushEvery == 0; + } + + public int getFlushEvery() { + return flushEvery; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/InboundRequestFeeder.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/InboundRequestFeeder.java new file mode 100644 index 0000000..a80c7d6 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/InboundRequestFeeder.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; + +import java.util.Collections; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A handler to be added, typically to an {@link EmbeddedChannel} to simulate read from an actual channel. + * Objects can be added to the buffer of this handler via {@link #addToTheFeed(Object...)} which will be sent to the + * pipeline when requested via {@link #read(ChannelHandlerContext)}. + * This handler abides by the contract of netty's backpressure semantics, to only send the number of messages as + * requested by {@link ChannelConfig#getMaxMessagesPerRead()}, followed by + * {@link ChannelHandlerContext#fireChannelReadComplete()} + */ +@Sharable +public class InboundRequestFeeder extends ChannelOutboundHandlerAdapter { + + private final ConcurrentLinkedQueue feed = new ConcurrentLinkedQueue<>(); + private final AtomicInteger readRequestedCount = new AtomicInteger(); + private int pendingReads; + private boolean sending; + private ChannelHandlerContext ctx; + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ctx.channel().config().setAutoRead(false); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + this.ctx = ctx; + + readRequestedCount.incrementAndGet(); + pendingReads++; + + _sendMessages(ctx); + } + + public void addToTheFeed(Object... msgs) { + if (null != msgs && msgs.length > 0) { + Collections.addAll(feed, msgs); + if (pendingReads > 0) { + _sendMessages(ctx); + } + } + } + + private void _sendMessages(ChannelHandlerContext ctx) { + if (sending) { + return; + } + sending = true; + int sentInThisIteration = 0; + while (true) { + Object next = feed.poll(); + if (null == next) { + break; + } + sentInThisIteration++; + ctx.fireChannelRead(next); + + if (sentInThisIteration >= ctx.channel().config().getMaxMessagesPerRead()) { + sentInThisIteration = 0; + pendingReads--; + ctx.fireChannelReadComplete(); + } + + if (pendingReads <= 0) { + break; + } + } + + sending = false; + } + + public int getReadRequestedCount() { + return readRequestedCount.get(); + } + + public int resetReadRequested() { + return readRequestedCount.getAndSet(0); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockClientEventListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockClientEventListener.java new file mode 100644 index 0000000..c55245e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockClientEventListener.java @@ -0,0 +1,235 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.test.util.MockConnectionEventListener.Event; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class MockClientEventListener extends ClientEventListener { + + public enum ClientEvent { + ConnectStart, ConnectSuccess, ConnectFailed, ReleaseStart, ReleaseSuccess, ReleaseFailed, Eviction, Reuse, + AcquireStart, AcquireSuccess, AcquireFailed + } + + private final List methodsCalled = new ArrayList<>(); + private long duration; + private TimeUnit timeUnit; + private Throwable recievedError; + + private final MockConnectionEventListener delegate = new MockConnectionEventListener(); + + @Override + public void onConnectStart() { + methodsCalled.add(ClientEvent.ConnectStart); + } + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(ClientEvent.ConnectSuccess); + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + methodsCalled.add(ClientEvent.ConnectFailed); + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + } + + @Override + public void onPoolReleaseStart() { + methodsCalled.add(ClientEvent.ReleaseStart); + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + methodsCalled.add(ClientEvent.ReleaseSuccess); + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + methodsCalled.add(ClientEvent.ReleaseFailed); + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + } + + @Override + public void onPooledConnectionEviction() { + methodsCalled.add(ClientEvent.Eviction); + } + + @Override + public void onPooledConnectionReuse() { + methodsCalled.add(ClientEvent.Reuse); + } + + @Override + public void onPoolAcquireStart() { + methodsCalled.add(ClientEvent.AcquireStart); + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + methodsCalled.add(ClientEvent.AcquireSuccess); + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + methodsCalled.add(ClientEvent.AcquireFailed); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + delegate.onConnectionCloseFailed(duration, timeUnit, recievedError); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + delegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseStart() { + delegate.onConnectionCloseStart(); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + delegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + delegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteStart() { + delegate.onWriteStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + delegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onFlushStart() { + delegate.onFlushStart(); + } + + @Override + public void onByteRead(long bytesRead) { + delegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + delegate.onByteWritten(bytesWritten); + } + + @Override + public void onCustomEvent(Object event) { + delegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + delegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + delegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + delegate.onCustomEvent(event, throwable); + } + + @Override + public void onCompleted() { + delegate.onCompleted(); + } + + public void assertMethodCalled(Event event) { + delegate.assertMethodCalled(event); + } + + public void assertMethodsCalled(Event... events) { + delegate.assertMethodsCalled(events); + } + + public void assertMethodCalled(ClientEvent event) { + if (!methodsCalled.contains(event)) { + throw new AssertionError("Method " + event + " not called. Methods called: " + methodsCalled); + } + } + + public void assertMethodsCalled(ClientEvent... events) { + if (methodsCalled.size() != events.length) { + throw new AssertionError("Unexpected methods called count. Methods called: " + methodsCalled.size() + + ". Expected: " + events.length); + } + + if (!methodsCalled.containsAll(Arrays.asList(events))) { + throw new AssertionError("Unexpected methods called count. Methods called: " + methodsCalled + + ". Expected: " + Arrays.toString(events)); + } + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public Throwable getRecievedError() { + return recievedError; + } + + @Override + public String toString() { + return "MockClientEventListener{" + + "methodsCalled=" + methodsCalled + + ", duration=" + duration + + ", timeUnit=" + timeUnit + + ", recievedError=" + recievedError + + ", delegate=" + delegate + + '}'; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockConnectionEventListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockConnectionEventListener.java new file mode 100644 index 0000000..a2ad31e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockConnectionEventListener.java @@ -0,0 +1,200 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util; + +import io.reactivex.netty.channel.events.ConnectionEventListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class MockConnectionEventListener extends ConnectionEventListener { + + public enum Event { + BytesRead, BytesWritten, FlushStart, FlushSuccess, WriteStart, WriteSuccess, WriteFailed, CloseStart, + CloseSuccess, CloseFailed, CustomEvent, CustomEventWithDuration, CustomEventWithDurationAndError, + CustomEventWithError, Complete + } + + private final List methodsCalled = new ArrayList<>(); + private long bytesRead; + private long duration; + private TimeUnit timeUnit; + private long bytesWritten; + private Throwable recievedError; + private Object customeEvent; + + @Override + public void onByteRead(long bytesRead) { + methodsCalled.add(Event.BytesRead); + this.bytesRead = bytesRead; + } + + @Override + public void onByteWritten(long bytesWritten) { + methodsCalled.add(Event.BytesWritten); + this.bytesWritten = bytesWritten; + } + + @Override + public void onFlushStart() { + methodsCalled.add(Event.FlushStart); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + methodsCalled.add(Event.FlushSuccess); + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onWriteStart() { + methodsCalled.add(Event.WriteStart); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + methodsCalled.add(Event.WriteSuccess); + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + methodsCalled.add(Event.WriteFailed); + this.duration = duration; + this.timeUnit = timeUnit; + recievedError = throwable; + } + + @Override + public void onConnectionCloseStart() { + methodsCalled.add(Event.CloseStart); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + methodsCalled.add(Event.CloseSuccess); + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + methodsCalled.add(Event.CloseFailed); + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + } + + @Override + public void onCustomEvent(Object event) { + methodsCalled.add(Event.CustomEvent); + customeEvent = event; + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + methodsCalled.add(Event.CustomEventWithDuration); + customeEvent = event; + this.duration = duration; + this.timeUnit = timeUnit; + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + methodsCalled.add(Event.CustomEventWithDurationAndError); + customeEvent = event; + this.duration = duration; + this.timeUnit = timeUnit; + recievedError = throwable; + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + methodsCalled.add(Event.CustomEventWithError); + customeEvent = event; + recievedError = throwable; + } + + @Override + public void onCompleted() { + methodsCalled.add(Event.Complete); + } + + public void assertMethodCalled(Event event) { + if (!methodsCalled.contains(event)) { + throw new AssertionError("Method " + event + " not called. Methods called: " + methodsCalled); + } + } + + public void assertMethodsCalled(Event... events) { + if (methodsCalled.size() < events.length) { + throw new AssertionError("Unexpected methods called count. Methods called: " + methodsCalled + + ". Expected: " + Arrays.toString(events)); + } + + if (!methodsCalled.containsAll(Arrays.asList(events))) { + throw new AssertionError("Unexpected methods called count. Methods called: " + methodsCalled + + ". Expected: " + Arrays.toString(events)); + } + } + + public List getMethodsCalled() { + return methodsCalled; + } + + public long getBytesRead() { + return bytesRead; + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public long getBytesWritten() { + return bytesWritten; + } + + public Throwable getRecievedError() { + return recievedError; + } + + public Object getCustomEvent() { + return customeEvent; + } + + @Override + public String toString() { + return "MockConnectionEventListener{" + + "methodsCalled=" + methodsCalled + + ", bytesRead=" + bytesRead + + ", duration=" + duration + + ", timeUnit=" + timeUnit + + ", bytesWritten=" + bytesWritten + + ", recievedError=" + recievedError + + ", customeEvent=" + customeEvent + + '}'; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventListener.java new file mode 100644 index 0000000..a28c299 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventListener.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util; + +import io.reactivex.netty.events.EventListener; + +import java.util.concurrent.TimeUnit; + +public class MockEventListener implements EventListener { + + private int onCompletedCount; + private int eventInvocationCount; + private final boolean raiseErrorOnAllInvocations; + private long duration; + private TimeUnit timeUnit; + private Throwable recievedError; + private String arg; + private Object customEvent; + + public MockEventListener() { + this(false); + } + + public MockEventListener(boolean raiseErrorOnAllInvocations) { + this.raiseErrorOnAllInvocations = raiseErrorOnAllInvocations; + } + + public void anEvent() { + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + public void anEventWithArg(String arg) { + eventInvocationCount++; + this.arg = arg; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + public void anEventWithDuration(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + public void anEventWithDurationAndError(long duration, TimeUnit timeUnit, Throwable t) { + this.duration = duration; + this.timeUnit = timeUnit; + recievedError = t; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + public void anEventWithDurationAndArg(long duration, TimeUnit timeUnit, String arg) { + this.duration = duration; + this.timeUnit = timeUnit; + this.arg = arg; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + @Override + public void onCompleted() { + onCompletedCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + @Override + public void onCustomEvent(Object event) { + customEvent = event; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + customEvent = event; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + customEvent = event; + recievedError = throwable; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + this.duration = duration; + this.timeUnit = timeUnit; + recievedError = throwable; + customEvent = event; + eventInvocationCount++; + if (raiseErrorOnAllInvocations) { + throw new IllegalStateException("Deliberate exception."); + } + } + + public int getOnCompletedCount() { + return onCompletedCount; + } + + public int getEventInvocationCount() { + return eventInvocationCount; + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public Throwable getRecievedError() { + return recievedError; + } + + public String getArg() { + return arg; + } + + public Object getCustomEvent() { + return customEvent; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventPublisher.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventPublisher.java new file mode 100644 index 0000000..908e804 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockEventPublisher.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util; + +import io.reactivex.netty.events.EventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import rx.Subscription; +import rx.subscriptions.Subscriptions; + +public class MockEventPublisher implements EventPublisher, EventSource { + + private static final MockEventPublisher DISABLED_EVENT_PUBLISHER = new MockEventPublisher(true); + private static final MockEventPublisher ENABLED_EVENT_PUBLISHER = new MockEventPublisher(false); + + private final boolean disable; + + private MockEventPublisher(boolean disable) { + this.disable = disable; + } + + public static MockEventPublisher disabled() { + @SuppressWarnings("unchecked") + MockEventPublisher t = (MockEventPublisher) DISABLED_EVENT_PUBLISHER; + return t; + } + + public static MockEventPublisher enabled() { + @SuppressWarnings("unchecked") + MockEventPublisher t = (MockEventPublisher) ENABLED_EVENT_PUBLISHER; + return t; + } + + @Override + public boolean publishingEnabled() { + return !disable; + } + + @Override + public Subscription subscribe(T listener) { + return Subscriptions.empty(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockPoolLimitDeterminationStrategy.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockPoolLimitDeterminationStrategy.java new file mode 100644 index 0000000..6570c93 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockPoolLimitDeterminationStrategy.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import io.reactivex.netty.client.pool.MaxConnectionsBasedStrategy; +import io.reactivex.netty.client.pool.PoolLimitDeterminationStrategy; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class MockPoolLimitDeterminationStrategy implements PoolLimitDeterminationStrategy { + + private final MaxConnectionsBasedStrategy delegate; + private final AtomicInteger acquireCount = new AtomicInteger(); + private final AtomicInteger releaseCount = new AtomicInteger(); + + public MockPoolLimitDeterminationStrategy(int maxPermits) { + delegate = new MaxConnectionsBasedStrategy(maxPermits); + } + + @Override + public boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit) { + acquireCount.incrementAndGet(); + return delegate.acquireCreationPermit(acquireStartTime, timeUnit); + } + + @Override + public int getAvailablePermits() { + return delegate.getAvailablePermits(); + } + + @Override + public void releasePermit() { + releaseCount.incrementAndGet(); + delegate.releasePermit(); + } + + public int getAcquireCount() { + return acquireCount.get(); + } + + public int getReleaseCount() { + return releaseCount.get(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockProducer.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockProducer.java new file mode 100644 index 0000000..937289b --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockProducer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import rx.Producer; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class MockProducer implements Producer { + + private final AtomicLong requested = new AtomicLong(); + private final AtomicInteger negativeRequestCount = new AtomicInteger(); + private final AtomicInteger maxValueRequestedCount = new AtomicInteger(); + + @Override + public void request(long n) { + if (Long.MAX_VALUE == n) { + requested.set(Long.MAX_VALUE); + maxValueRequestedCount.incrementAndGet(); + } else if (n <= 0) { + negativeRequestCount.incrementAndGet(); + } + requested.addAndGet(n); + } + + public long getRequested() { + return requested.get(); + } + + public void assertIllegalRequest() { + final int negReqCnt = negativeRequestCount.get(); + if (negReqCnt != 0) { + throw new AssertionError("Negative items requested " + negReqCnt + " times."); + } + } + + public void assertBackpressureRequested() { + final int maxValReqCnt = maxValueRequestedCount.get(); + if (maxValReqCnt != 0) { + throw new AssertionError("Backpressure disabled " + maxValReqCnt + " times."); + } + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockTcpServerEventListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockTcpServerEventListener.java new file mode 100644 index 0000000..fce92ea --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/MockTcpServerEventListener.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import io.reactivex.netty.protocol.tcp.server.events.TcpServerEventListener; +import io.reactivex.netty.test.util.MockConnectionEventListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class MockTcpServerEventListener extends TcpServerEventListener { + + public enum ServerEvent { + NewClient, HandlingStart, HandlingSuccess, HandlingFailed + } + + private final List methodsCalled = new ArrayList<>(); + private long duration; + private TimeUnit timeUnit; + private Throwable recievedError; + + private final MockConnectionEventListener connDelegate; + + public MockTcpServerEventListener() { + connDelegate = new MockConnectionEventListener(); + } + + @Override + public void onNewClientConnected() { + methodsCalled.add(ServerEvent.NewClient); + } + + @Override + public void onConnectionHandlingStart(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(ServerEvent.HandlingStart); + } + + @Override + public void onConnectionHandlingSuccess(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = timeUnit; + methodsCalled.add(ServerEvent.HandlingSuccess); + } + + @Override + public void onConnectionHandlingFailed(long duration, TimeUnit timeUnit, Throwable recievedError) { + this.duration = duration; + this.timeUnit = timeUnit; + this.recievedError = recievedError; + methodsCalled.add(ServerEvent.HandlingFailed); + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public Throwable getRecievedError() { + return recievedError; + } + + @Override + public void onCompleted() { + connDelegate.onCompleted(); + } + + @Override + public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, + Throwable recievedError) { + connDelegate.onConnectionCloseFailed(duration, timeUnit, recievedError); + } + + @Override + public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onConnectionCloseSuccess(duration, timeUnit); + } + + @Override + public void onConnectionCloseStart() { + connDelegate.onConnectionCloseStart(); + } + + @Override + public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onWriteFailed(duration, timeUnit, throwable); + } + + @Override + public void onWriteSuccess(long duration, TimeUnit timeUnit) { + connDelegate.onWriteSuccess(duration, timeUnit); + } + + @Override + public void onWriteStart() { + connDelegate.onWriteStart(); + } + + @Override + public void onFlushComplete(long duration, TimeUnit timeUnit) { + connDelegate.onFlushComplete(duration, timeUnit); + } + + @Override + public void onFlushStart() { + connDelegate.onFlushStart(); + } + + @Override + public void onByteRead(long bytesRead) { + connDelegate.onByteRead(bytesRead); + } + + @Override + public void onByteWritten(long bytesWritten) { + connDelegate.onByteWritten(bytesWritten); + } + + @Override + public void onCustomEvent(Object event) { + connDelegate.onCustomEvent(event); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { + connDelegate.onCustomEvent(event, duration, timeUnit); + } + + @Override + public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { + connDelegate.onCustomEvent(event, duration, timeUnit, throwable); + } + + @Override + public void onCustomEvent(Object event, Throwable throwable) { + connDelegate.onCustomEvent(event, throwable); + } + + public void assertMethodsCalled(ServerEvent... events) { + assertThat("Unexpected methods called count.", methodsCalled, hasSize(events.length)); + assertThat("Unexpected methods called.", methodsCalled, contains(events)); + } + + public MockConnectionEventListener getConnDelegate() { + return connDelegate; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TcpConnectionRequestMock.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TcpConnectionRequestMock.java new file mode 100644 index 0000000..a2c21b1 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TcpConnectionRequestMock.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util; + +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.client.ConnectionRequest; +import rx.Observable; +import rx.Subscriber; + +public class TcpConnectionRequestMock extends ConnectionRequest { + + public TcpConnectionRequestMock(final Observable> connections) { + super(new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + connections.unsafeSubscribe(subscriber); + } + }); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TrackableMetricEventsListener.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TrackableMetricEventsListener.java new file mode 100644 index 0000000..26a60a9 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/TrackableMetricEventsListener.java @@ -0,0 +1,127 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util; + +import io.reactivex.netty.client.events.ClientEventListener; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class TrackableMetricEventsListener extends ClientEventListener { + + private final AtomicLong creationCount = new AtomicLong(); + private final AtomicLong failedCount = new AtomicLong(); + private final AtomicLong reuseCount = new AtomicLong(); + private final AtomicLong evictionCount = new AtomicLong(); + private final AtomicLong acquireAttemptedCount = new AtomicLong(); + private final AtomicLong acquireSucceededCount = new AtomicLong(); + private final AtomicLong acquireFailedCount = new AtomicLong(); + private final AtomicLong releaseAttemptedCount = new AtomicLong(); + private final AtomicLong releaseSucceededCount = new AtomicLong(); + private final AtomicLong releaseFailedCount = new AtomicLong(); + + @Override + public void onConnectSuccess(long duration, TimeUnit timeUnit) { + creationCount.incrementAndGet(); + } + + @Override + public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + failedCount.incrementAndGet(); + } + + @Override + public void onPoolReleaseStart() { + releaseAttemptedCount.incrementAndGet(); + } + + @Override + public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) { + releaseSucceededCount.incrementAndGet(); + } + + @Override + public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + releaseFailedCount.incrementAndGet(); + } + + @Override + public void onPooledConnectionEviction() { + evictionCount.incrementAndGet(); + } + + @Override + public void onPooledConnectionReuse() { + reuseCount.incrementAndGet(); + } + + @Override + public void onPoolAcquireStart() { + acquireAttemptedCount.incrementAndGet(); + } + + @Override + public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) { + acquireSucceededCount.incrementAndGet(); + } + + @Override + public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, Throwable throwable) { + acquireFailedCount.incrementAndGet(); + } + + public long getAcquireAttemptedCount() { + return acquireAttemptedCount.longValue(); + } + + public long getAcquireFailedCount() { + return acquireFailedCount.longValue(); + } + + public long getAcquireSucceededCount() { + return acquireSucceededCount.longValue(); + } + + public long getCreationCount() { + return creationCount.longValue(); + } + + public long getEvictionCount() { + return evictionCount.longValue(); + } + + public long getFailedCount() { + return failedCount.longValue(); + } + + public long getReleaseAttemptedCount() { + return releaseAttemptedCount.longValue(); + } + + public long getReleaseFailedCount() { + return releaseFailedCount.longValue(); + } + + public long getReleaseSucceededCount() { + return releaseSucceededCount.longValue(); + } + + public long getReuseCount() { + return reuseCount.longValue(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelPipelineDelegate.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelPipelineDelegate.java new file mode 100644 index 0000000..27f6cd3 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelPipelineDelegate.java @@ -0,0 +1,438 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util.embedded; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.concurrent.EventExecutorGroup; + +import java.net.SocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicLong; + +class EmbeddedChannelPipelineDelegate implements ChannelPipeline { + + private final ChannelPipeline pipeline; + private final String lastHandlerName; + private final AtomicLong uniqueNameCounter = new AtomicLong(); + + public EmbeddedChannelPipelineDelegate(EmbeddedChannel newChannel) { + pipeline = newChannel.pipeline(); + final ChannelHandler lastHandler = pipeline.last(); + if (null != lastHandler) { + List names = pipeline.names(); + for (String name : names) { + if (pipeline.get(name) == lastHandler) { + lastHandlerName = name; + return; + } + } + lastHandlerName = null; + } else { + lastHandlerName = null; + } + } + + @Override + public ChannelPipeline addFirst(String name, ChannelHandler handler) { + pipeline.addFirst(name, handler); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup group, + String name, ChannelHandler handler) { + pipeline.addFirst(group, name, handler); + return this; + } + + @Override + public ChannelPipeline addLast(String name, ChannelHandler handler) { + if (null != lastHandlerName) { + pipeline.addBefore(lastHandlerName, name, handler); + } else { + pipeline.addLast(name, handler); + } + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup group, + String name, ChannelHandler handler) { + if (null != lastHandlerName) { + pipeline.addBefore(group, lastHandlerName, name, handler); + } else { + pipeline.addLast(group, name, handler); + } + return this; + } + + @Override + public ChannelPipeline addBefore(String baseName, String name, + ChannelHandler handler) { + pipeline.addBefore(baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addBefore(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + pipeline.addBefore(group, baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addAfter(String baseName, String name, + ChannelHandler handler) { + pipeline.addAfter(baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addAfter(EventExecutorGroup group, + String baseName, String name, + ChannelHandler handler) { + pipeline.addAfter(group, baseName, name, handler); + return this; + } + + @Override + public ChannelPipeline addFirst(ChannelHandler... handlers) { + pipeline.addFirst(handlers); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup group, + ChannelHandler... handlers) { + pipeline.addFirst(group, handlers); + return this; + } + + @Override + public ChannelPipeline addLast(ChannelHandler... handlers) { + if (null != lastHandlerName) { + for (ChannelHandler handler : handlers) { + pipeline.addBefore(lastHandlerName, generateUniqueName(), handler); + } + } else { + pipeline.addLast(handlers); + } + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup group, + ChannelHandler... handlers) { + if (null != lastHandlerName) { + for (ChannelHandler handler : handlers) { + pipeline.addBefore(group, lastHandlerName, generateUniqueName(), handler); + } + } else { + pipeline.addLast(group, handlers); + } + return this; + } + + @Override + public ChannelPipeline remove(ChannelHandler handler) { + return pipeline.remove(handler); + } + + @Override + public ChannelHandler remove(String name) { + return pipeline.remove(name); + } + + @Override + public T remove(Class handlerType) { + return pipeline.remove(handlerType); + } + + @Override + public ChannelHandler removeFirst() { + return pipeline.removeFirst(); + } + + @Override + public ChannelHandler removeLast() { + return pipeline.removeLast(); + } + + @Override + public ChannelPipeline replace(ChannelHandler oldHandler, + String newName, ChannelHandler newHandler) { + return pipeline.replace(oldHandler, newName, newHandler); + } + + @Override + public ChannelHandler replace(String oldName, String newName, + ChannelHandler newHandler) { + return pipeline.replace(oldName, newName, newHandler); + } + + @Override + public T replace(Class oldHandlerType, String newName, + ChannelHandler newHandler) { + return pipeline.replace(oldHandlerType, newName, newHandler); + } + + private String generateUniqueName() { + return "ClientEmbeddedConnectionFactoryGeneratedName-" + uniqueNameCounter.incrementAndGet(); + } + + @Override + public ChannelHandler first() { + return pipeline.first(); + } + + @Override + public ChannelHandlerContext firstContext() { + return pipeline.firstContext(); + } + + @Override + public ChannelHandler last() { + return pipeline.last(); + } + + @Override + public ChannelHandlerContext lastContext() { + return pipeline.lastContext(); + } + + @Override + public ChannelHandler get(String name) { + return pipeline.get(name); + } + + @Override + public T get(Class handlerType) { + return pipeline.get(handlerType); + } + + @Override + public ChannelHandlerContext context(ChannelHandler handler) { + return pipeline.context(handler); + } + + @Override + public ChannelHandlerContext context(String name) { + return pipeline.context(name); + } + + @Override + public ChannelHandlerContext context( + Class handlerType) { + return pipeline.context(handlerType); + } + + @Override + public Channel channel() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public List names() { + return pipeline.names(); + } + + @Override + public Map toMap() { + return pipeline.toMap(); + } + + @Override + public ChannelPipeline fireChannelRegistered() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelUnregistered() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelActive() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelInactive() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireExceptionCaught(Throwable cause) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireUserEventTriggered(Object event) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelRead(Object msg) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelReadComplete() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline fireChannelWritabilityChanged() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + SocketAddress localAddress) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture disconnect() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture close() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture deregister() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress, + ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, + SocketAddress localAddress, + ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture disconnect(ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture close(ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture deregister(ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline read() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture write(Object msg) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture write(Object msg, ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPipeline flush() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPromise newPromise() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelProgressivePromise newProgressivePromise() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture newSucceededFuture() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelFuture newFailedFuture(Throwable cause) { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public ChannelPromise voidPromise() { + throw new UnsupportedOperationException("Only pipeline modification operations are allowed"); + } + + @Override + public Iterator> iterator() { + return pipeline.iterator(); + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelProvider.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelProvider.java new file mode 100644 index 0000000..d9d7287 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util.embedded; + +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.client.ChannelProvider; +import io.reactivex.netty.client.ChannelProviderFactory; +import io.reactivex.netty.client.Host; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventPublisher; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.test.util.InboundRequestFeeder; +import rx.Observable; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.functions.Func0; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class EmbeddedChannelProvider implements ChannelProvider { + + private final List createdChannels = new CopyOnWriteArrayList<>(); + private final boolean failConnect; + + public EmbeddedChannelProvider() { + this(false); + } + + public EmbeddedChannelProvider(boolean failConnect) { + this.failConnect = failConnect; + } + + @Override + public Observable newChannel(Observable input) { + if (failConnect) { + return Observable.error(new IOException("Deliberate connection failure")); + } + + return Observable.defer(new Func0>() { + @Override + public Observable call() { + InboundRequestFeeder feeder = new InboundRequestFeeder(); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(feeder); + EmbeddedChannelWithFeeder ecwf = new EmbeddedChannelWithFeeder(embeddedChannel, feeder); + createdChannels.add(ecwf); + return Observable.just(embeddedChannel); + } + }).lift(new Operator() { + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(subscriber) { + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(Channel channel) { + subscriber.onNext(channel); + channel.pipeline().fireChannelActive(); + } + }; + } + }); + } + + public List getCreatedChannels() { + return createdChannels; + } + + public ChannelProviderFactory asFactory() { + return new ChannelProviderFactory() { + @Override + public ChannelProvider newProvider(Host host, EventSource eventSource, + EventPublisher publisher, ClientEventListener clientPublisher) { + return EmbeddedChannelProvider.this; + } + }; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelWithFeeder.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelWithFeeder.java new file mode 100644 index 0000000..9aafad9 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedChannelWithFeeder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.test.util.embedded; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.reactivex.netty.client.events.ClientEventListener; +import io.reactivex.netty.events.EventSource; +import io.reactivex.netty.test.util.MockEventPublisher; +import io.reactivex.netty.test.util.InboundRequestFeeder; + +public class EmbeddedChannelWithFeeder { + + private final EmbeddedChannel channel; + private final InboundRequestFeeder feeder; + private final EventSource tcpEventSource; + + public EmbeddedChannelWithFeeder(EmbeddedChannel channel, InboundRequestFeeder feeder) { + this(channel, feeder, MockEventPublisher.disabled()); + } + + public EmbeddedChannelWithFeeder(EmbeddedChannel channel, InboundRequestFeeder feeder, + EventSource tcpEventSource) { + this.channel = channel; + this.feeder = feeder; + this.tcpEventSource = tcpEventSource; + } + + public EmbeddedChannel getChannel() { + return channel; + } + + public InboundRequestFeeder getFeeder() { + return feeder; + } + + public EventSource getTcpEventSource() { + return tcpEventSource; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedConnectionProvider.java b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedConnectionProvider.java new file mode 100644 index 0000000..62407e2 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/test/util/embedded/EmbeddedConnectionProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.test.util.embedded; + +import io.netty.channel.Channel; +import io.reactivex.netty.channel.Connection; +import io.reactivex.netty.channel.ConnectionImpl; +import io.reactivex.netty.client.ConnectionProvider; +import io.reactivex.netty.client.ConnectionProviderFactory; +import io.reactivex.netty.client.HostConnector; +import rx.Observable; +import rx.functions.Func1; + +public class EmbeddedConnectionProvider implements ConnectionProvider { + + private final EmbeddedChannelProvider channelProvider; + + public EmbeddedConnectionProvider() { + this(new EmbeddedChannelProvider()); + } + + public EmbeddedConnectionProvider(EmbeddedChannelProvider channelProvider) { + this.channelProvider = channelProvider; + } + + @Override + public Observable> newConnectionRequest() { + return channelProvider.newChannel(Observable.empty()) + .map(new Func1>() { + @Override + public Connection call(Channel channel) { + return ConnectionImpl.fromChannel(channel); + } + }); + } + + public EmbeddedChannelProvider getChannelProvider() { + return channelProvider; + } + + public ConnectionProviderFactory asFactory() { + return new ConnectionProviderFactory() { + @Override + public ConnectionProvider newProvider(Observable> hosts) { + return EmbeddedConnectionProvider.this; + } + }; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroupTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroupTest.java new file mode 100644 index 0000000..87816e3 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/threads/PreferCurrentEventLoopGroupTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.reactivex.netty.threads; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class PreferCurrentEventLoopGroupTest { + + @Test(timeout = 60000) + public void testNextInEventloop() throws Exception { + final PreferCurrentEventLoopGroup group = new PreferCurrentEventLoopGroup(new NioEventLoopGroup(4)); + for (EventExecutor child : group) { + Future future = child.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return group.next().inEventLoop(); + } + }); + assertTrue("Current eventloop was not preferred.", future.get(1, TimeUnit.MINUTES)); + } + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/threads/RxJavaEventloopSchedulerTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/threads/RxJavaEventloopSchedulerTest.java new file mode 100644 index 0000000..50dc6ac --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/threads/RxJavaEventloopSchedulerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.threads; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.reactivex.netty.threads.RxJavaEventloopScheduler.EventloopWorker; +import org.junit.Test; +import rx.Observable; +import rx.Subscription; +import rx.functions.Action0; +import rx.observers.TestSubscriber; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class RxJavaEventloopSchedulerTest { + + @Test(timeout = 60000) + public void testScheduleNow() throws Exception { + RxJavaEventloopScheduler scheduler = new RxJavaEventloopScheduler(new NioEventLoopGroup()); + TestSubscriber testSubscriber = new TestSubscriber<>(); + + Observable.range(1, 1) + .observeOn(scheduler) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + testSubscriber.assertValue(1); + } + + @Test(timeout = 60000) + public void testScheduleDelay() throws Exception { + RxJavaEventloopScheduler scheduler = new RxJavaEventloopScheduler(new NioEventLoopGroup()); + TestSubscriber testSubscriber = new TestSubscriber<>(); + + Observable.timer(1, TimeUnit.MILLISECONDS, scheduler) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertNoErrors(); + + testSubscriber.assertValue(0L); + } + + @Test(timeout = 60000) + public void testRemoveNonDelayedTasks() throws Exception { + RxJavaEventloopScheduler scheduler = new RxJavaEventloopScheduler(new NioEventLoopGroup()); + + final EventloopWorker worker = (EventloopWorker) scheduler.createWorker(); + final EventloopWorker worker2 = (EventloopWorker) scheduler.createWorker(); + + assertThat("New worker already has subscriptions.", worker.hasScheduledSubscriptions(), is(false)); + + final AtomicBoolean isScheduledBeforeExecute = new AtomicBoolean(); + final CountDownLatch executed = new CountDownLatch(1); + + Subscription subscription = worker.schedule(new Action0() { + @Override + public void call() { + isScheduledBeforeExecute.set(worker.hasScheduledSubscriptions()); + worker2.schedule(new Action0() { + @Override + public void call() { + executed.countDown(); + } + }); + } + }); + + executed.await(); + + assertThat("No scheduled subscriptions on executing the action.", isScheduledBeforeExecute.get(), is(true)); + assertThat("Action not unsubscribed.", subscription.isUnsubscribed(), is(true)); + + assertThat("Subscription not removed post execution.", worker.hasScheduledSubscriptions(), is(false)); + } + + @Test(timeout = 60000) + public void testRemoveDelayedTasks() throws Exception { + RxJavaEventloopScheduler scheduler = new RxJavaEventloopScheduler(new NioEventLoopGroup()); + + final EventloopWorker worker = (EventloopWorker) scheduler.createWorker(); + final EventloopWorker worker2 = (EventloopWorker) scheduler.createWorker(); + + assertThat("New worker already has subscriptions.", worker.hasDelayScheduledSubscriptions(), is(false)); + + final AtomicBoolean isScheduledBeforeExecute = new AtomicBoolean(); + final CountDownLatch executed = new CountDownLatch(1); + + Subscription subscription = worker.schedule(new Action0() { + @Override + public void call() { + isScheduledBeforeExecute.set(worker.hasDelayScheduledSubscriptions()); + worker2.schedule(new Action0() { + @Override + public void call() { + executed.countDown(); + } + }); + } + }, 1, TimeUnit.MILLISECONDS); + + executed.await(); + + assertThat("No scheduled subscriptions on executing the action.", isScheduledBeforeExecute.get(), is(true)); + assertThat("Action not unsubscribed.", subscription.isUnsubscribed(), is(true)); + + assertThat("Subscription not removed post execution.", worker.hasDelayScheduledSubscriptions(), is(false)); + } + + @Test(timeout = 60000) + public void testUnsubscribeDelayedTasks() throws Exception { + RxJavaEventloopScheduler scheduler = new RxJavaEventloopScheduler(new NioEventLoopGroup()); + + final EventloopWorker worker = (EventloopWorker) scheduler.createWorker(); + + assertThat("New worker already has subscriptions.", worker.hasDelayScheduledSubscriptions(), is(false)); + + Subscription subscription = worker.schedule(new Action0() { + @Override + public void call() { + } + }, 100, TimeUnit.DAYS); + + assertThat("No subscriptions post schedule.", worker.hasDelayScheduledSubscriptions(), is(true)); + + subscription.unsubscribe(); + + assertThat("Subscription not removed post cancellation.", worker.hasDelayScheduledSubscriptions(), is(false)); + } +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/util/CollectBytesTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/util/CollectBytesTest.java new file mode 100644 index 0000000..df3ddad --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/util/CollectBytesTest.java @@ -0,0 +1,211 @@ +package io.reactivex.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.reactivex.netty.util.CollectBytes.TooMuchDataException; +import org.junit.Assert; +import org.junit.Test; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CollectBytesTest { + @Test + public void testCollectOverEmptyObservable() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.empty() + .compose(CollectBytes.all()) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValue(Unpooled.buffer()); + } + + @Test + public void testCollectSingleEvent() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.just(getByteBuf("test")) + .compose(CollectBytes.all()) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValues(getByteBuf("test")); + } + + @Test + public void testCollectManyEvents() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.just( + getByteBuf("t"), + getByteBuf("e"), + getByteBuf("s"), + getByteBuf("t") + ) + .compose(CollectBytes.all()) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValues(getByteBuf("test")); + } + + @Test + public void testWithLimitEqualToBytes() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.just( + getByteBuf("t"), + getByteBuf("e"), + getByteBuf("s"), + getByteBuf("t") + ) + .compose(CollectBytes.upTo(4)) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValues(getByteBuf("test")); + } + + @Test + public void testWithLimitGreaterThanBytes() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.just( + getByteBuf("t"), + getByteBuf("e"), + getByteBuf("s"), + getByteBuf("t") + ) + .compose(CollectBytes.upTo(5)) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValues(getByteBuf("test")); + } + + @Test + public void testCollectWithLimitSmallerThanBytes() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + Observable.just( + getByteBuf("t"), + getByteBuf("e"), + getByteBuf("s"), + getByteBuf("t") + ) + .compose(CollectBytes.upTo(2)) + .subscribe(t); + + t.assertError(TooMuchDataException.class); + t.assertNotCompleted(); + t.assertNoValues(); + } + + @Test + public void testReturnSingleEventWithMoreBytesThanMax() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + toByteBufObservable("test") + .compose(CollectBytes.upTo(0)) + .subscribe(t); + + t.assertError(TooMuchDataException.class); + t.assertNotCompleted(); + t.assertNoValues(); + } + + @Test + public void testReturnMultipleEvents() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + toByteBufObservable("1", "2") + .compose(CollectBytes.upTo(5)) + .subscribe(t); + + t.assertNoErrors(); + t.assertCompleted(); + t.assertValues(getByteBufs("12")); + } + + @Test + public void testReturnEventsOnLimitBoundary() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + toByteBufObservable("12", "34", "56") + .compose(CollectBytes.upTo(4)) + .subscribe(t); + + t.assertError(TooMuchDataException.class); + t.assertNotCompleted(); + t.assertNoValues(); + } + + @Test + public void testReturnMultipleEventsEndingWhenOverMaxBytes() throws Exception { + TestSubscriber t = new TestSubscriber<>(); + toByteBufObservable("first", "second", "third") + .compose(CollectBytes.upTo(7)) + .subscribe(t); + + t.assertError(TooMuchDataException.class); + t.assertNotCompleted(); + t.assertNoValues(); + } + + @Test + public void testUnsubscribeFromUpstream() throws Exception { + final List emittedBufs = new ArrayList<>(); + + toByteBufObservable("first", "second", "third") + .doOnNext(new Action1() { + @Override + public void call(ByteBuf byteBuf) { + emittedBufs.add(byteBuf.toString(Charset.defaultCharset())); + } + }) + .compose(CollectBytes.upTo(7)) + .subscribe(new TestSubscriber<>()); + + Assert.assertEquals(Arrays.asList("first", "second"), emittedBufs); + } + + @Test(expected = IllegalArgumentException.class) + public void testExceptionOnNegativeMaxBytes() throws Exception { + CollectBytes.upTo(-1); + } + + private Observable toByteBufObservable(String... values) { + return Observable.from(values) + .map(new Func1() { + @Override + public ByteBuf call(String s) { + return getByteBuf(s); + } + }); + } + + private ByteBuf getBytes(int length, int value) { + ByteBuf buffer = Unpooled.buffer(length, length); + for (int i = 0; i < length; ++i) { + buffer.writeByte(value); + } + return buffer; + } + + private ByteBuf getByteBuf(String s) { + return ReferenceCountUtil.releaseLater(Unpooled.copiedBuffer(s, Charset.defaultCharset())); + } + + private ByteBuf[] getByteBufs(String... s) { + ByteBuf[] bufs = new ByteBuf[s.length]; + for (int i = 0; i < s.length; ++i) { + bufs[i] = getByteBuf(s[i]); + } + return bufs; + } +} diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/util/LineReaderTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/util/LineReaderTest.java new file mode 100644 index 0000000..d2ef3d3 --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/util/LineReaderTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class LineReaderTest { + + @Rule + public final ReaderRule readerRule = new ReaderRule(); + + @Test(timeout = 60000) + public void testSingleLine() throws Exception { + String msg = "Hello"; + ByteBuf data = Unpooled.buffer().writeBytes((msg + "\n").getBytes()); + List out = new ArrayList<>(); + readerRule.lineReader.decode(data, out, UnpooledByteBufAllocator.DEFAULT); + + assertThat("Unexpected output size.", out, hasSize(1)); + assertThat("Unexpected output message.", out, contains((Object) msg)); + assertThat("Input buffer not consumed.", data.isReadable(), is(false)); + } + + @Test(timeout = 60000) + public void testEmptyInputBuffer() throws Exception { + ByteBuf data = Unpooled.buffer(); + List out = new ArrayList<>(); + readerRule.lineReader.decode(data, out, UnpooledByteBufAllocator.DEFAULT); + + assertThat("Unexpected output size.", out, is(empty())); + assertThat("Input buffer not consumed.", data.isReadable(), is(false)); + } + + @Test(timeout = 60000) + public void testEmptyLine() throws Exception { + ByteBuf data = Unpooled.buffer().writeByte('\n'); + List out = new ArrayList<>(); + readerRule.lineReader.decode(data, out, UnpooledByteBufAllocator.DEFAULT); + + assertThat("Unexpected output size.", out, hasSize(1)); + assertThat("Unexpected output message.", out, contains((Object)"")); + assertThat("Input buffer not consumed.", data.isReadable(), is(false)); + } + + @Test(timeout = 60000) + public void testSplitData() throws Exception { + String msg1 = "Hell"; + String msg2 = "o"; + ByteBuf data1 = Unpooled.buffer().writeBytes((msg1.getBytes())); + ByteBuf data2 = Unpooled.buffer().writeBytes((msg2 + "\n").getBytes()); + List out = new ArrayList<>(); + readerRule.lineReader.decode(data1, out, UnpooledByteBufAllocator.DEFAULT); + assertThat("Unexpected output size post first decode.", out, is(empty())); + assertThat("Input buffer not consumed.", data1.isReadable(), is(false)); + + readerRule.lineReader.decode(data2, out, UnpooledByteBufAllocator.DEFAULT); + assertThat("Unexpected output size post second decode.", out, hasSize(1)); + assertThat("Unexpected output message post second decode.", out, contains((Object) (msg1 + msg2))); + assertThat("Input buffer not consumed.", data2.isReadable(), is(false)); + } + + @Test(timeout = 60000) + public void testUnreadDataDispose() throws Exception { + String msg = "Hell"; + ByteBuf data1 = Unpooled.buffer().writeBytes((msg.getBytes())); + List out = new ArrayList<>(); + readerRule.lineReader.decode(data1, out, UnpooledByteBufAllocator.DEFAULT); + assertThat("Unexpected output size post first decode.", out, is(empty())); + assertThat("Input buffer not consumed.", data1.isReadable(), is(false)); + assertThat("Reader does not have incomplete buffer.", readerRule.lineReader.getIncompleteBuffer(), is(notNullValue())); + assertThat("Reader's incomplete buffer is not readable.", readerRule.lineReader.getIncompleteBuffer() + .isReadable(), + is(true)); + + readerRule.lineReader.dispose(); + + assertThat("Reader does not have incomplete buffer.", readerRule.lineReader.getIncompleteBuffer(), + is(notNullValue())); + assertThat("Reader did not release incomplete buffer.", readerRule.lineReader.getIncompleteBuffer().refCnt(), + is(0)); + } + + public static class ReaderRule extends ExternalResource { + + private LineReader lineReader; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + lineReader = new LineReader(); + base.evaluate(); + } + }; + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/java/io/reactivex/netty/util/UnicastBufferingSubjectTest.java b/netty-http-rx/src/test/java/io/reactivex/netty/util/UnicastBufferingSubjectTest.java new file mode 100644 index 0000000..a5ef79e --- /dev/null +++ b/netty-http-rx/src/test/java/io/reactivex/netty/util/UnicastBufferingSubjectTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.reactivex.netty.util; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.Observable; +import rx.exceptions.MissingBackpressureException; +import rx.observers.TestSubscriber; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class UnicastBufferingSubjectTest { + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final SubjectRule rule = new SubjectRule(); + + @Test(timeout = 60000) + public void testSequentialSubscriptions() throws Exception { + String[] msgs = { "Hello1", "Hello2" }; + + rule.subject.onNext(msgs[0]); + + rule.subscribeAndAssertValues(rule.subject.take(1), msgs[0]); + + rule.subject.onNext(msgs[1]); + + rule.subscribeAndAssertValues(rule.subject.take(1), msgs[1]); + } + + @Test(timeout = 60000) + public void testConcurrentSubscriptions() throws Exception { + TestSubscriber sub1 = new TestSubscriber<>(); + rule.subject.subscribe(sub1); + + sub1.assertNoTerminalEvent(); + + TestSubscriber sub2 = new TestSubscriber<>(); + rule.subject.subscribe(sub2); + + sub2.assertError(IllegalStateException.class); + } + + @Test(timeout = 60000) + public void testBufferCompletion() throws Exception { + rule.subject.onCompleted(); + rule.subscribeAndAssertValues(rule.subject); + } + + @Test(timeout = 60000) + public void testBufferError() throws Exception { + rule.subject.onError(new IllegalStateException()); + + TestSubscriber sub = new TestSubscriber<>(); + rule.subject.subscribe(sub); + + sub.assertTerminalEvent(); + sub.assertError(IllegalStateException.class); + } + + @Test(timeout = 60000) + public void testUnsubscribeBeforeDemandComplete() throws Exception { + String[] msgs = { "Hello1", "Hello2" }; + + rule.subject.onNext(msgs[0]); + rule.subject.onNext(msgs[1]); + rule.subject.onCompleted(); + + rule.subscribeAndAssertValues(rule.subject.take(1), msgs[0]); + + TestSubscriber sub = new TestSubscriber<>(0); + rule.subject.subscribe(sub); + + sub.assertNoTerminalEvent(); + sub.assertNoValues(); + + sub.requestMore(1); + + sub.assertTerminalEvent(); + sub.assertNoErrors(); + sub.assertValue(msgs[1]); + } + + @Test(timeout = 60000) + public void testBufferOverflowWithOffer() throws Exception { + String[] msgs = { "Hello1", "Hello2" }; + UnicastBufferingSubject subject = UnicastBufferingSubject.create(1); + + subject.onNext(msgs[0]); + boolean offered = subject.offerNext(msgs[1]); + + assertThat("Offered passed when over capacity", offered, is(false)); + } + + @Test(timeout = 60000) + public void testBufferOverflowWithOnNext() throws Exception { + expectedException.expectCause(isA(MissingBackpressureException.class)); + + String[] msgs = { "Hello1", "Hello2" }; + UnicastBufferingSubject subject = UnicastBufferingSubject.create(1); + + subject.onNext(msgs[0]); + subject.onNext(msgs[1]); + } + + @Test(timeout = 60000) + public void testOverflowSubscribeAndThenAccept() throws Exception { + String[] msgs = { "Hello1", "Hello2" }; + UnicastBufferingSubject subject = UnicastBufferingSubject.create(1); + + subject.onNext(msgs[0]); + boolean offered = subject.offerNext(msgs[1]); + + assertThat("Offered passed when over capacity", offered, is(false)); + + TestSubscriber subscriber = new TestSubscriber<>(); + subject.subscribe(subscriber); + + subscriber.assertNoTerminalEvent(); + subscriber.assertValue(msgs[0]); + + subject.onNext(msgs[1]); + subject.onCompleted(); + + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + subscriber.assertValues(msgs); + } + + @Test(timeout = 60000) + public void testErrorPostSubscribe() throws Exception { + String[] msgs = { "Hello1", "Hello2" }; + + rule.subject.onNext(msgs[0]); + + TestSubscriber subscriber = new TestSubscriber<>(); + rule.subject.subscribe(subscriber); + + subscriber.assertNoTerminalEvent(); + subscriber.assertValue(msgs[0]); + + rule.subject.onError(new IllegalStateException()); + + subscriber.assertTerminalEvent(); + subscriber.assertError(IllegalStateException.class); + } + + private static class SubjectRule extends ExternalResource { + + private UnicastBufferingSubject subject; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + subject = UnicastBufferingSubject.create(Long.MAX_VALUE); + base.evaluate(); + } + }; + } + + public void subscribeAndAssertValues(Observable source, String... values) { + TestSubscriber sub1 = new TestSubscriber<>(); + source.subscribe(sub1); + sub1.assertTerminalEvent(); + sub1.assertNoErrors(); + sub1.assertValues(values); + } + } + +} \ No newline at end of file diff --git a/netty-http-rx/src/test/resources/log4j.properties b/netty-http-rx/src/test/resources/log4j.properties new file mode 100644 index 0000000..176c09e --- /dev/null +++ b/netty-http-rx/src/test/resources/log4j.properties @@ -0,0 +1,21 @@ +# +# Copyright 2015 Netflix, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +log4j.rootLogger=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/netty-http-server-api/build.gradle b/netty-http-server-api/build.gradle new file mode 100644 index 0000000..34cdeeb --- /dev/null +++ b/netty-http-server-api/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compile project(":netty-http-common") +} \ No newline at end of file diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Endpoint.java similarity index 57% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Endpoint.java index b752308..1248bf2 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Endpoint.java @@ -1,7 +1,4 @@ -package org.xbib.netty.http.server.endpoint; - -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +package org.xbib.netty.http.server.api; import java.io.IOException; @@ -15,5 +12,7 @@ public interface Endpoint { void resolveUriTemplate(ServerRequest serverRequest) throws IOException; - void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; + void before(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; + + void after(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; } diff --git a/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDescriptor.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDescriptor.java new file mode 100644 index 0000000..cb49df6 --- /dev/null +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDescriptor.java @@ -0,0 +1,6 @@ +package org.xbib.netty.http.server.api; + +public interface EndpointDescriptor { + + String getSortKey(); +} diff --git a/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDispatcher.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDispatcher.java new file mode 100644 index 0000000..d7a6571 --- /dev/null +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/EndpointDispatcher.java @@ -0,0 +1,9 @@ +package org.xbib.netty.http.server.api; + +import java.io.IOException; + +@FunctionalInterface +public interface EndpointDispatcher> { + + void dispatch(E endpoint, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Filter.java similarity index 60% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Filter.java index f2d0847..0438a9d 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Filter.java @@ -1,15 +1,12 @@ -package org.xbib.netty.http.server.endpoint.service; - -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +package org.xbib.netty.http.server.api; import java.io.IOException; /** - * A {@code Service} is capable of serving requests for resources within its context. + * A {@code Filter} is capable of serving requests for resources within its context. */ @FunctionalInterface -public interface Service { +public interface Filter { /** * Handles the given request by building and returning a response. diff --git a/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/HttpChannelInitializer.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/HttpChannelInitializer.java new file mode 100644 index 0000000..d4d78ea --- /dev/null +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/HttpChannelInitializer.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.server.api; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; + +public interface HttpChannelInitializer extends ChannelHandler { + + void initChannel(Channel channel); + +} diff --git a/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ProtocolProvider.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ProtocolProvider.java new file mode 100644 index 0000000..fa08b64 --- /dev/null +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ProtocolProvider.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.server.api; + +public interface ProtocolProvider { + + boolean supportsMajorVersion(int majorVersion); + + Class initializerClass(); + + Class transportClass(); +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Resource.java similarity index 81% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Resource.java index 254700a..3474f50 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Resource.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.endpoint.service; +package org.xbib.netty.http.server.api; import java.net.URL; import java.time.Instant; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerRequest.java similarity index 87% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerRequest.java index 72553ab..1e6bb23 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerRequest.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server; +package org.xbib.netty.http.server.api; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; @@ -6,7 +6,6 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import org.xbib.net.URL; import org.xbib.netty.http.common.HttpParameters; -import org.xbib.netty.http.server.endpoint.HttpEndpointDescriptor; import javax.net.ssl.SSLSession; import java.io.IOException; @@ -18,8 +17,6 @@ public interface ServerRequest { URL getURL(); - HttpEndpointDescriptor getEndpointDescriptor(); - void setContext(List context); List getContext(); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerResponse.java similarity index 98% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerResponse.java index 6d26ffa..47e0b4c 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/ServerResponse.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server; +package org.xbib.netty.http.server.api; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; @@ -48,7 +48,7 @@ public interface ServerResponse { } static void write(ServerResponse serverResponse, HttpResponseStatus status) { - write(serverResponse, status, "application/octet-stream", status.reasonPhrase()); + write(serverResponse, status, "application/octet-stream", EMPTY_STRING); } /** diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Transport.java similarity index 93% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Transport.java index 60c7178..e47cb52 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/Transport.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.transport; +package org.xbib.netty.http.server.api; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/annotation/Endpoint.java similarity index 83% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java rename to netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/annotation/Endpoint.java index 4cda0fc..3c740d4 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java +++ b/netty-http-server-api/src/main/java/org/xbib/netty/http/server/api/annotation/Endpoint.java @@ -1,6 +1,6 @@ -package org.xbib.netty.http.server.annotation; +package org.xbib.netty.http.server.api.annotation; -import org.xbib.netty.http.server.endpoint.service.Service; +import org.xbib.netty.http.server.api.Filter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,7 +11,7 @@ import java.lang.annotation.Target; * The {@code Endpoint} annotation decorates methods which are mapped * to a HTTP endpoint within the server, and provide its contents. * The annotated methods must have the same signature and contract - * as {@link Service#handle}, but can have arbitrary names. + * as {@link Filter#handle}, but can have arbitrary names. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) diff --git a/netty-http-server/build.gradle b/netty-http-server/build.gradle index 660b2e2..79c328f 100644 --- a/netty-http-server/build.gradle +++ b/netty-http-server/build.gradle @@ -1,5 +1,6 @@ dependencies { compile project(":netty-http-common") + compile project(":netty-http-server-api") compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" compile "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}" diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java index 88b800c..df0cf01 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java @@ -9,9 +9,11 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.security.SecurityUtil; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.endpoint.HttpEndpoint; import org.xbib.netty.http.server.endpoint.HttpEndpointResolver; -import org.xbib.netty.http.server.endpoint.service.Service; +import org.xbib.netty.http.server.api.Filter; import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; import javax.net.ssl.TrustManagerFactory; @@ -282,44 +284,44 @@ public class Domain { return this; } - public Builder singleEndpoint(String path, Service service) { + public Builder singleEndpoint(String path, Filter filter) { Objects.requireNonNull(path); - Objects.requireNonNull(service); + Objects.requireNonNull(filter); addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder() .setPath(path) .build()) - .setDispatcher((endpoint, req, resp) -> service.handle(req, resp)) + .setDispatcher((endpoint, req, resp) -> filter.handle(req, resp)) .build()); return this; } - public Builder singleEndpoint(String prefix, String path, Service service) { + public Builder singleEndpoint(String prefix, String path, Filter filter) { Objects.requireNonNull(prefix); Objects.requireNonNull(path); - Objects.requireNonNull(service); + Objects.requireNonNull(filter); addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder() .setPrefix(prefix) .setPath(path) .build()) - .setDispatcher((endpoint, req, resp) -> service.handle(req, resp)) + .setDispatcher((endpoint, req, resp) -> filter.handle(req, resp)) .build()); return this; } - public Builder singleEndpoint(String prefix, String path, Service service, + public Builder singleEndpoint(String prefix, String path, Filter filter, String... methods) { Objects.requireNonNull(prefix); Objects.requireNonNull(path); - Objects.requireNonNull(service); + Objects.requireNonNull(filter); addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder() .setPrefix(prefix) .setPath(path) .setMethods(Arrays.asList(methods)) .build()) - .setDispatcher((endpoint, req, resp) -> service.handle(req, resp)) + .setDispatcher((endpoint, req, resp) -> filter.handle(req, resp)) .build()); return this; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Http1Provider.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Http1Provider.java new file mode 100644 index 0000000..9f10daf --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Http1Provider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.server; + +import org.xbib.netty.http.server.api.ProtocolProvider; +import org.xbib.netty.http.server.handler.http.Http1ChannelInitializer; +import org.xbib.netty.http.server.transport.Http1Transport; + +public class Http1Provider implements ProtocolProvider { + + @Override + public boolean supportsMajorVersion(int majorVersion) { + return majorVersion == 1; + } + + @Override + public Class initializerClass() { + return Http1ChannelInitializer.class; + } + + @Override + public Class transportClass() { + return Http1Transport.class; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Http2Provider.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Http2Provider.java new file mode 100644 index 0000000..2489a03 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Http2Provider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.server; + +import org.xbib.netty.http.server.api.ProtocolProvider; +import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.server.transport.Http2Transport; + +public class Http2Provider implements ProtocolProvider { + + @Override + public boolean supportsMajorVersion(int majorVersion) { + return majorVersion == 2; + } + + @Override + public Class initializerClass() { + return Http2ChannelInitializer.class; + } + + @Override + public Class transportClass() { + return Http2Transport.class; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java index f0f7ba9..37bd18f 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java @@ -20,16 +20,25 @@ import io.netty.util.DomainNameMapping; import io.netty.util.DomainNameMappingBuilder; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.NetworkUtils; -import org.xbib.netty.http.server.handler.http.HttpChannelInitializer; -import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.server.api.HttpChannelInitializer; +import org.xbib.netty.http.server.api.ProtocolProvider; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.common.security.SecurityUtil; -import org.xbib.netty.http.server.transport.HttpTransport; -import org.xbib.netty.http.server.transport.Http2Transport; -import org.xbib.netty.http.server.transport.Transport; +import org.xbib.netty.http.server.transport.HttpServerRequest; +import org.xbib.netty.http.server.api.Transport; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.ServiceLoader; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; @@ -68,31 +77,48 @@ public final class Server implements AutoCloseable { private final EventLoopGroup childEventLoopGroup; + /** + * A thread pool for executing blocking tasks. + */ + private final BlockingThreadPoolExecutor executor; + private final Class socketChannelClass; private final ServerBootstrap bootstrap; private ChannelFuture channelFuture; + private final List> protocolProviders; + /** - * Create a new HTTP server. Use {@link #builder(HttpAddress)} to build HTTP client instance. + * Create a new HTTP server. + * Use {@link #builder(HttpAddress)} to build HTTP instance. + * * @param serverConfig server configuration * @param byteBufAllocator byte buf allocator * @param parentEventLoopGroup parent event loop group * @param childEventLoopGroup child event loop group * @param socketChannelClass socket channel class */ + @SuppressWarnings("unchecked") private Server(ServerConfig serverConfig, - ByteBufAllocator byteBufAllocator, - EventLoopGroup parentEventLoopGroup, - EventLoopGroup childEventLoopGroup, - Class socketChannelClass) { + ByteBufAllocator byteBufAllocator, + EventLoopGroup parentEventLoopGroup, + EventLoopGroup childEventLoopGroup, + Class socketChannelClass, + BlockingThreadPoolExecutor executor) { Objects.requireNonNull(serverConfig); this.serverConfig = serverConfig; this.byteBufAllocator = byteBufAllocator != null ? byteBufAllocator : ByteBufAllocator.DEFAULT; this.parentEventLoopGroup = createParentEventLoopGroup(serverConfig, parentEventLoopGroup); this.childEventLoopGroup = createChildEventLoopGroup(serverConfig, childEventLoopGroup); this.socketChannelClass = createSocketChannelClass(serverConfig, socketChannelClass); + this.executor = executor; + this.protocolProviders =new ArrayList<>(); + for (ProtocolProvider provider : ServiceLoader.load(ProtocolProvider.class)) { + protocolProviders.add(provider); + logger.log(Level.INFO, "protocol provider up: " + provider.transportClass() ); + } this.bootstrap = new ServerBootstrap() .group(this.parentEventLoopGroup, this.childEventLoopGroup) .channel(this.socketChannelClass) @@ -108,8 +134,8 @@ public final class Server implements AutoCloseable { .childOption(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize()) .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis()) .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverConfig.getWriteBufferWaterMark()); - if (serverConfig.isDebug()) { - bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel())); + if (serverConfig.isTrafficDebug()) { + bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getTrafficDebugLogLevel())); } if (serverConfig.getDefaultDomain() == null) { throw new IllegalStateException("no default named server (with name '*') configured, unable to continue"); @@ -126,15 +152,8 @@ public final class Server implements AutoCloseable { } domainNameMapping = mappingBuilder.build(); } - if (serverConfig.getAddress().getVersion().majorVersion() == 1) { - HttpChannelInitializer httpChannelInitializer = new HttpChannelInitializer(this, - serverConfig.getAddress(), domainNameMapping); - bootstrap.childHandler(httpChannelInitializer); - } else { - Http2ChannelInitializer http2ChannelInitializer = new Http2ChannelInitializer(this, - serverConfig.getAddress(), domainNameMapping); - bootstrap.childHandler(http2ChannelInitializer); - } + bootstrap.childHandler(findChannelInitializer(serverConfig.getAddress().getVersion().majorVersion(), + serverConfig.getAddress(), domainNameMapping)); } public static Builder builder() { @@ -153,21 +172,6 @@ public final class Server implements AutoCloseable { return serverConfig; } - /** - * Returns the named server with the given name. - * - * @param name the name of the virtual host to return or null for the - * default domain - * @return the virtual host with the given name or the default domain - */ - public Domain getNamedServer(String name) { - Domain domain = serverConfig.getDomain(name); - if (domain == null) { - domain = serverConfig.getDefaultDomain(); - } - return domain; - } - /** * Start accepting incoming connections. * @return the channel future @@ -185,6 +189,46 @@ public final class Server implements AutoCloseable { return channelFuture; } + /** + * Returns the named server with the given name. + * + * @param name the name of the virtual host to return or null for the + * default domain + * @return the virtual host with the given name or the default domain + */ + public Domain getNamedServer(String name) { + Domain domain = serverConfig.getDomain(name); + if (domain == null) { + domain = serverConfig.getDefaultDomain(); + } + return domain; + } + + public void handle(Domain domain, HttpServerRequest serverRequest, ServerResponse serverResponse) + throws IOException { + if (executor != null) { + executor.submit(() -> { + try { + domain.handle(serverRequest, serverResponse); + } catch (IOException e) { + executor.afterExecute(null, e); + } finally { + serverRequest.release(); + } + }); + } else { + try { + domain.handle(serverRequest, serverResponse); + } finally { + serverRequest.release(); + } + } + } + + public BlockingThreadPoolExecutor getExecutor() { + return executor; + } + public void logDiagnostics(Level level) { logger.log(level, () -> "JDK ciphers: " + SecurityUtil.Defaults.JDK_CIPHERS); logger.log(level, () -> "OpenSSL ciphers: " + SecurityUtil.Defaults.OPENSSL_CIPHERS); @@ -209,7 +253,18 @@ public final class Server implements AutoCloseable { } public Transport newTransport(HttpVersion httpVersion) { - return httpVersion.majorVersion() == 1 ? new HttpTransport(this) : new Http2Transport(this); + for (ProtocolProvider protocolProvider : protocolProviders) { + if (protocolProvider.supportsMajorVersion(httpVersion.majorVersion())) { + try { + return protocolProvider.transportClass() + .getConstructor(Server.class) + .newInstance(this); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(); + } + } + } + throw new IllegalStateException("no channel initializer found for major version " + httpVersion.majorVersion()); } @Override @@ -236,7 +291,7 @@ public final class Server implements AutoCloseable { } parentEventLoopGroup.shutdownGracefully(1L, amount, timeUnit); try { - childEventLoopGroup.awaitTermination(amount, timeUnit); + parentEventLoopGroup.awaitTermination(amount, timeUnit); } catch (InterruptedException e) { throw new IOException(e); } @@ -250,6 +305,23 @@ public final class Server implements AutoCloseable { } } + private HttpChannelInitializer findChannelInitializer(int majorVersion, + HttpAddress httpAddress, + DomainNameMapping domainNameMapping) { + for (ProtocolProvider protocolProvider : protocolProviders) { + if (protocolProvider.supportsMajorVersion(majorVersion)) { + try { + return protocolProvider.initializerClass() + .getConstructor(Server.class, HttpAddress.class, DomainNameMapping.class) + .newInstance(this, httpAddress, domainNameMapping); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(); + } + } + } + throw new IllegalStateException("no channel initializer found for major version " + majorVersion); + } + private static EventLoopGroup createParentEventLoopGroup(ServerConfig serverConfig, EventLoopGroup parentEventLoopGroup ) { EventLoopGroup eventLoopGroup = parentEventLoopGroup; @@ -309,6 +381,58 @@ public final class Server implements AutoCloseable { } } + static class BlockingThreadFactory implements ThreadFactory { + + private int number = 0; + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "org-xbib-netty-http-server-pool-" + (number++)); + thread.setDaemon(true); + return thread; + } + }; + + public static class BlockingThreadPoolExecutor extends ThreadPoolExecutor { + + private final Logger logger = Logger.getLogger(BlockingThreadPoolExecutor.class.getName()); + + BlockingThreadPoolExecutor(int nThreads, int maxQueue, ThreadFactory threadFactory) { + super(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(maxQueue), threadFactory); + } + + /* + * Examine Throwable or Error of a thread after execution just to log them. + */ + @Override + protected void afterExecute(Runnable runnable, Throwable terminationCause) { + super.afterExecute(runnable, terminationCause); + Throwable throwable = terminationCause; + if (throwable == null && runnable instanceof Future) { + try { + Future future = (Future) runnable; + if (future.isDone()) { + future.get(); + } + } catch (CancellationException ce) { + logger.log(Level.FINEST, ce.getMessage(), ce); + throwable = ce; + } catch (ExecutionException ee) { + logger.log(Level.FINEST, ee.getMessage(), ee); + throwable = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + logger.log(Level.FINEST, ie.getMessage(), ie); + } + } + if (throwable != null) { + logger.log(Level.SEVERE, throwable.getMessage(), throwable); + } + } + } + /** * HTTP server builder. */ @@ -331,7 +455,7 @@ public final class Server implements AutoCloseable { private Builder(Domain defaultDomain) { this.serverConfig = new ServerConfig(); this.serverConfig.setAddress(defaultDomain.getHttpAddress()); - addServer(defaultDomain); + addDomain(defaultDomain); } public Builder enableDebug() { @@ -369,6 +493,16 @@ public final class Server implements AutoCloseable { return this; } + public Builder setReadTimeoutMillis(int readTimeoutMillis) { + this.serverConfig.setReadTimeoutMillis(readTimeoutMillis); + return this; + } + + public Builder setIdleTimeoutMillis(int idleTimeoutMillis) { + this.serverConfig.setIdleTimeoutMillis(idleTimeoutMillis); + return this; + } + public Builder setParentThreadCount(int parentThreadCount) { this.serverConfig.setParentThreadCount(parentThreadCount); return this; @@ -379,6 +513,16 @@ public final class Server implements AutoCloseable { return this; } + public Builder setBlockingThreadCount(int blockingThreadCount) { + this.serverConfig.setBlockingThreadCount(blockingThreadCount); + return this; + } + + public Builder setBlockingQueueCount(int blockingQueueCount) { + this.serverConfig.setBlockingQueueCount(blockingQueueCount); + return this; + } + public Builder setTcpSendBufferSize(int tcpSendBufferSize) { this.serverConfig.setTcpSendBufferSize(tcpSendBufferSize); return this; @@ -429,21 +573,6 @@ public final class Server implements AutoCloseable { return this; } - public Builder setReadTimeoutMillis(int readTimeoutMillis) { - this.serverConfig.setReadTimeoutMillis(readTimeoutMillis); - return this; - } - - public Builder setConnectionTimeoutMillis(int connectionTimeoutMillis) { - this.serverConfig.setConnectTimeoutMillis(connectionTimeoutMillis); - return this; - } - - public Builder setIdleTimeoutMillis(int idleTimeoutMillis) { - this.serverConfig.setIdleTimeoutMillis(idleTimeoutMillis); - return this; - } - public Builder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { this.serverConfig.setWriteBufferWaterMark(writeBufferWaterMark); return this; @@ -454,7 +583,7 @@ public final class Server implements AutoCloseable { return this; } - public Builder enableDeompression(boolean enableDecompression) { + public Builder enableDecompression(boolean enableDecompression) { this.serverConfig.setDecompression(enableDecompression); return this; } @@ -469,14 +598,24 @@ public final class Server implements AutoCloseable { return this; } - public Builder addServer(Domain domain) { + public Builder addDomain(Domain domain) { this.serverConfig.putDomain(domain); logger.log(Level.FINE, "adding named server: " + domain); return this; } public Server build() { - return new Server(serverConfig, byteBufAllocator, parentEventLoopGroup, childEventLoopGroup, socketChannelClass); + int maxThreads = serverConfig.getBlockingThreadCount(); + int maxQueue = serverConfig.getBlockingQueueCount(); + BlockingThreadPoolExecutor executor = null; + if (maxThreads > 0 && maxQueue > 0) { + executor = new BlockingThreadPoolExecutor(maxThreads, maxQueue, new BlockingThreadFactory()); + executor.setRejectedExecutionHandler((runnable, threadPoolExecutor) -> + logger.log(Level.SEVERE, "rejected: " + runnable)); + } + return new Server(serverConfig, byteBufAllocator, + parentEventLoopGroup, childEventLoopGroup, socketChannelClass, + executor); } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java index e4e8d54..a41d715 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java @@ -50,6 +50,16 @@ public class ServerConfig { */ int CHILD_THREAD_COUNT = 0; + /** + * Blocking thread pool count. + */ + int BLOCKING_THREAD_COUNT = Runtime.getRuntime().availableProcessors(); + + /** + * Blocking thread pool queue count. + */ + int BLOCKING_QUEUE_COUNT = 1024; + /** * Default for SO_REUSEADDR. */ @@ -88,7 +98,7 @@ public class ServerConfig { /** * Default idle timeout in milliseconds. */ - int IDLE_TIMEOUT_MILLIS = 30000; + int IDLE_TIMEOUT_MILLIS = 60000; /** * Set HTTP chunk maximum size to 8k. @@ -109,9 +119,15 @@ public class ServerConfig { int MAX_HEADERS_SIZE = 8 * 1024; /** - * Set maximum content length to 100 MB. + * Set maximum content length to 256 MB. */ - int MAX_CONTENT_LENGTH = 100 * 1024 * 1024; + int MAX_CONTENT_LENGTH = 256 * 1024 * 1024; + + /** + * HTTP/1 pipelining capacity. 1024 is very high, it means + * 1024 requests can be present for a single client. + */ + int PIPELINING_CAPACITY = 1024; /** * This is Netty's default. @@ -129,6 +145,12 @@ public class ServerConfig { */ boolean ENABLE_COMPRESSION = true; + /** + * Default compression threshold. If a response size is over this value, + * it will be compressed, otherwise not. + */ + int COMPRESSION_THRESHOLD = 8192; + /** * Default for decompression. */ @@ -156,6 +178,7 @@ public class ServerConfig { /** * Transport layer security protocol versions. + * Do not use SSLv2, SSLv3, TLS 1.0, TLS 1.1. */ String[] PROTOCOLS = new String[] { "TLSv1.3", "TLSv1.2" }; @@ -183,6 +206,10 @@ public class ServerConfig { private int childThreadCount = Defaults.CHILD_THREAD_COUNT; + private int blockingThreadCount = Defaults.BLOCKING_THREAD_COUNT; + + private int blockingQueueCount = Defaults.BLOCKING_QUEUE_COUNT; + private boolean reuseAddr = Defaults.SO_REUSEADDR; private boolean tcpNodelay = Defaults.TCP_NODELAY; @@ -201,6 +228,8 @@ public class ServerConfig { private int maxContentLength = Defaults.MAX_CONTENT_LENGTH; + private int pipeliningCapacity = Defaults.PIPELINING_CAPACITY; + private int maxCompositeBufferComponents = Defaults.MAX_COMPOSITE_BUFFER_COMPONENTS; private int connectTimeoutMillis = Defaults.CONNECT_TIMEOUT_MILLIS; @@ -213,6 +242,8 @@ public class ServerConfig { private boolean enableCompression = Defaults.ENABLE_COMPRESSION; + private int compressionThreshold = Defaults.COMPRESSION_THRESHOLD; + private boolean enableDecompression = Defaults.ENABLE_DECOMPRESSION; private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS; @@ -249,7 +280,7 @@ public class ServerConfig { return this; } - public boolean isDebug() { + public boolean isTrafficDebug() { return debug; } @@ -258,7 +289,7 @@ public class ServerConfig { return this; } - public LogLevel getDebugLogLevel() { + public LogLevel getTrafficDebugLogLevel() { return debugLogLevel; } @@ -299,6 +330,24 @@ public class ServerConfig { return childThreadCount; } + public ServerConfig setBlockingThreadCount(int blockingThreadCount) { + this.blockingThreadCount = blockingThreadCount; + return this; + } + + public int getBlockingThreadCount() { + return blockingThreadCount; + } + + public ServerConfig setBlockingQueueCount(int blockingQueueCount) { + this.blockingQueueCount = blockingQueueCount; + return this; + } + + public int getBlockingQueueCount() { + return blockingQueueCount; + } + public ServerConfig setReuseAddr(boolean reuseAddr) { this.reuseAddr = reuseAddr; return this; @@ -416,6 +465,15 @@ public class ServerConfig { return maxContentLength; } + public ServerConfig setPipeliningCapacity(int pipeliningCapacity) { + this.pipeliningCapacity = pipeliningCapacity; + return this; + } + + public int getPipeliningCapacity() { + return pipeliningCapacity; + } + public ServerConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { this.maxCompositeBufferComponents = maxCompositeBufferComponents; return this; @@ -443,6 +501,15 @@ public class ServerConfig { return enableCompression; } + public ServerConfig setCompressionThreshold(int compressionThreshold) { + this.compressionThreshold = compressionThreshold; + return this; + } + + public int getCompressionThreshold() { + return compressionThreshold; + } + public ServerConfig setDecompression(boolean enabled) { this.enableDecompression = enabled; return this; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java index f2d1138..d58e993 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java @@ -86,7 +86,7 @@ public final class ServerCookieEncoder extends CookieEncoder { Instant expires = Instant.ofEpochMilli(cookie.maxAge() * 1000 + System.currentTimeMillis()); buf.append(CookieHeaderNames.EXPIRES); buf.append(CookieUtil.EQUALS); - buf.append(DateTimeUtil.formatMillis(expires.toEpochMilli())); + buf.append(DateTimeUtil.formatRfc1123(expires.toEpochMilli())); buf.append(CookieUtil.SEMICOLON); buf.append(CookieUtil.SP); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java deleted file mode 100644 index 93d4ece..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.xbib.netty.http.server.endpoint; - -public interface EndpointDescriptor { -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java deleted file mode 100644 index 68759d2..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.xbib.netty.http.server.endpoint; - -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; - -import java.io.IOException; - -@FunctionalInterface -public interface EndpointDispatcher { - - void dispatch(HttpEndpoint endpoint, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java index 4d80321..14f0173 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java @@ -5,9 +5,10 @@ import org.xbib.net.QueryParameters; import org.xbib.net.path.PathMatcher; import org.xbib.net.path.PathNormalizer; import org.xbib.netty.http.common.HttpMethod; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.service.Service; +import org.xbib.netty.http.server.api.Endpoint; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; +import org.xbib.netty.http.server.api.Filter; import java.io.IOException; import java.util.ArrayList; @@ -32,17 +33,21 @@ public class HttpEndpoint implements Endpoint { private final List contentTypes; - private final List filters; + private final List beforeFilters; + + private final List afterFilters; private HttpEndpoint(String prefix, String path, EnumSet methods, List contentTypes, - List filters) { + List beforeFilters, + List afterFilters) { this.prefix = PathNormalizer.normalize(prefix); this.path = PathNormalizer.normalize(path); this.methods = methods; this.contentTypes = contentTypes; - this.filters = filters; + this.beforeFilters = beforeFilters; + this.afterFilters = afterFilters; } public static Builder builder() { @@ -55,7 +60,8 @@ public class HttpEndpoint implements Endpoint { .setPath(endpoint.path) .setMethods(endpoint.methods) .setContentTypes(endpoint.contentTypes) - .setFilters(endpoint.filters); + .setBefore(endpoint.beforeFilters) + .setAfter(endpoint.afterFilters); } @Override @@ -69,11 +75,11 @@ public class HttpEndpoint implements Endpoint { } @Override - public boolean matches(HttpEndpointDescriptor info) { - return pathMatcher.match(prefix + path, info.getPath()) && - (methods == null || methods.isEmpty() || (methods.contains(info.getMethod()))) && - (contentTypes == null || contentTypes.isEmpty() || info.getContentType() == null || - contentTypes.stream().anyMatch(info.getContentType()::startsWith)); + public boolean matches(HttpEndpointDescriptor httpEndpointDescriptor) { + return pathMatcher.match(prefix + path, httpEndpointDescriptor.getPath()) && + (methods == null || methods.isEmpty() || (methods.contains(httpEndpointDescriptor.getMethod()))) && + (contentTypes == null || contentTypes.isEmpty() || httpEndpointDescriptor.getContentType() == null || + contentTypes.stream().anyMatch(httpEndpointDescriptor.getContentType()::startsWith)); } @Override @@ -88,19 +94,29 @@ public class HttpEndpoint implements Endpoint { } @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + public void before(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); - for (Service service : filters) { - service.handle(serverRequest, serverResponse); - if (serverResponse.getStatus() != null) { - break; - } + for (Filter filter : beforeFilters) { + filter.handle(serverRequest, serverResponse); + } + } + + @Override + public void after(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); + for (Filter filter : afterFilters) { + filter.handle(serverRequest, serverResponse); } } @Override public String toString() { - return "Endpoint[prefix=" + prefix + ",path=" + path + ",methods=" + methods + ",contentTypes=" + contentTypes + ",filters=" + filters +"]"; + return "Endpoint[prefix=" + prefix + ",path=" + path + + ",methods=" + methods + + ",contentTypes=" + contentTypes + + ",before=" + beforeFilters + + ",after=" + afterFilters + + "]"; } static class EndpointPathComparator implements Comparator { @@ -127,14 +143,17 @@ public class HttpEndpoint implements Endpoint { private List contentTypes; - private List filters; + private List beforeFilters; + + private List afterFilters; Builder() { this.prefix = "/"; this.path = "/**"; this.methods = DEFAULT_METHODS; this.contentTypes = new ArrayList<>(); - this.filters = new ArrayList<>(); + this.beforeFilters = new ArrayList<>(); + this.afterFilters = new ArrayList<>(); } public Builder setPrefix(String prefix) { @@ -175,20 +194,21 @@ public class HttpEndpoint implements Endpoint { return this; } - public Builder setFilters(List filters) { + public Builder setBefore(List filters) { Objects.requireNonNull(filters); - this.filters = filters; + this.beforeFilters = filters; return this; } - public Builder addFilter(Service filter) { - Objects.requireNonNull(filter); - this.filters.add(filter); + public Builder setAfter(List filters) { + Objects.requireNonNull(filters); + this.afterFilters = filters; return this; } public HttpEndpoint build() { - return new HttpEndpoint(prefix, path, methods, contentTypes, filters); + return new HttpEndpoint(prefix, path, methods, contentTypes, + beforeFilters, afterFilters); } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java index d173f93..cf4db02 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java @@ -1,7 +1,8 @@ package org.xbib.netty.http.server.endpoint; import org.xbib.netty.http.common.HttpMethod; -import org.xbib.netty.http.server.transport.HttpServerRequest; +import org.xbib.netty.http.server.api.EndpointDescriptor; +import org.xbib.netty.http.server.api.ServerRequest; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; @@ -13,12 +14,17 @@ public class HttpEndpointDescriptor implements EndpointDescriptor, Comparable endpoints; - private final EndpointDispatcher endpointDispatcher; + private final EndpointDispatcher endpointDispatcher; private final Map> endpointDescriptors; private HttpEndpointResolver(List endpoints, - EndpointDispatcher endpointDispatcher, + EndpointDispatcher endpointDispatcher, int limit) { Objects.requireNonNull(endpointDispatcher); this.endpoints = endpoints; @@ -44,10 +46,10 @@ public class HttpEndpointResolver { * @return a */ public List matchingEndpointsFor(ServerRequest serverRequest) { - HttpEndpointDescriptor httpEndpointDescriptor = serverRequest.getEndpointDescriptor(); + HttpEndpointDescriptor httpEndpointDescriptor = new HttpEndpointDescriptor(serverRequest); endpointDescriptors.putIfAbsent(httpEndpointDescriptor, endpoints.stream() .filter(endpoint -> endpoint.matches(httpEndpointDescriptor)) - .sorted(new HttpEndpoint.EndpointPathComparator(httpEndpointDescriptor.getPath())) + .sorted(new HttpEndpoint.EndpointPathComparator(httpEndpointDescriptor.getSortKey())) .collect(Collectors.toList())); return endpointDescriptors.get(httpEndpointDescriptor); } @@ -57,15 +59,18 @@ public class HttpEndpointResolver { Objects.requireNonNull(matchingEndpoints); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, () -> - "endpoint = " + serverRequest.getEndpointDescriptor() + - " matching endpoints = " + matchingEndpoints.size() + " --> " + matchingEndpoints); + "matching endpoints = " + matchingEndpoints.size() + " --> " + matchingEndpoints); } for (HttpEndpoint endpoint : matchingEndpoints) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, () -> "executing endpoint = " + endpoint); + } endpoint.resolveUriTemplate(serverRequest); - endpoint.handle(serverRequest, serverResponse); + endpoint.before(serverRequest, serverResponse); endpointDispatcher.dispatch(endpoint, serverRequest, serverResponse); + endpoint.after(serverRequest, serverResponse); if (serverResponse.getStatus() != null) { - logger.log(Level.FINEST, () -> "endpoint " + endpoint + " status = " + serverResponse.getStatus()); + logger.log(Level.FINEST, () -> "endpoint " + endpoint + " break, status = " + serverResponse.getStatus()); break; } } @@ -87,7 +92,7 @@ public class HttpEndpointResolver { private List endpoints; - private EndpointDispatcher endpointDispatcher; + private EndpointDispatcher endpointDispatcher; Builder() { this.limit = DEFAULT_LIMIT; @@ -148,7 +153,7 @@ public class HttpEndpointResolver { .setPath(endpoint.path()) .setMethods(Arrays.asList(endpoint.methods())) .setContentTypes(Arrays.asList(endpoint.contentTypes())) - .addFilter(methodService) + .setBefore(Collections.singletonList(methodService)) .build()); } } @@ -156,7 +161,7 @@ public class HttpEndpointResolver { return this; } - public Builder setDispatcher(EndpointDispatcher endpointDispatcher) { + public Builder setDispatcher(EndpointDispatcher endpointDispatcher) { Objects.requireNonNull(endpointDispatcher); this.endpointDispatcher = endpointDispatcher; return this; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java index 7fe04d6..58af2f8 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java @@ -1,7 +1,8 @@ package org.xbib.netty.http.server.endpoint.service; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.Resource; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import java.io.IOException; import java.net.URL; @@ -18,16 +19,9 @@ public class ClassLoaderService extends ResourceService { private final String prefix; - private final String indexFileName; - public ClassLoaderService(Class clazz, String prefix) { - this(clazz, prefix, "index.html"); - } - - public ClassLoaderService(Class clazz, String prefix, String indexFileName) { this.clazz = clazz; this.prefix = prefix; - this.indexFileName = indexFileName; } @Override @@ -50,6 +44,11 @@ public class ClassLoaderService extends ResourceService { return true; } + @Override + protected int getMaxAgeSeconds() { + return 24 * 3600; + } + class ClassLoaderResource implements Resource { private final String resourcePath; @@ -103,12 +102,21 @@ public class ClassLoaderService extends ResourceService { @Override public boolean isDirectory() { - return resourcePath.endsWith("/"); + return resourcePath.isEmpty() || resourcePath.endsWith("/"); } @Override public String indexFileName() { - return indexFileName; + return null; + } + + @Override + public String toString() { + return "[ClassLoaderResource:resourcePath=" + resourcePath + + ",url=" + url + + ",lastmodified=" + lastModified + + ",length=" + length + + ",isDirectory=" + isDirectory() + "]"; } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java deleted file mode 100644 index ebd8561..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.xbib.netty.http.server.endpoint.service; - -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; - -public class EmptyService implements Service { - @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { - // do nothing - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java index 345179a..6661abb 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java @@ -1,7 +1,8 @@ package org.xbib.netty.http.server.endpoint.service; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.Resource; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import java.io.IOException; import java.net.URL; @@ -44,6 +45,11 @@ public class FileService extends ResourceService { return true; } + @Override + protected int getMaxAgeSeconds() { + return 24 * 3600; + } + class ChunkedFileResource implements Resource { private final String resourcePath; @@ -101,5 +107,14 @@ public class FileService extends ResourceService { public long getLength() { return length; } + + @Override + public String toString() { + return "[FileResource:resourcePath=" + resourcePath + + ",url=" + url + + ",lastmodified=" + lastModified + + ",length=" + length + + ",isDirectory=" + isDirectory() + "]"; + } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java deleted file mode 100644 index 2365d80..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.xbib.netty.http.server.endpoint.service; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.HttpResponseStatus; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.util.MimeTypeUtils; - -import java.io.IOException; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class MappedFileService implements Service { - - private static final Logger logger = Logger.getLogger(MappedFileService.class.getName()); - - private final Path prefix; - - public MappedFileService(Path prefix) { - this.prefix = prefix; - if (!Files.exists(prefix)) { - throw new IllegalArgumentException("prefix: " + prefix + " (does not exist)"); - } - if (!Files.isDirectory(prefix)) { - throw new IllegalArgumentException("prefix: " + prefix + " (not a directory)"); - } - } - - @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - String requestPath = serverRequest.getEffectiveRequestPath().substring(1); // always starts with '/' - Path path = prefix.resolve(requestPath); - if (Files.isReadable(path)) { - try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(path)) { - MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); - ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer); - String contentType = MimeTypeUtils.guessFromPath(requestPath, false); - serverResponse.withStatus(HttpResponseStatus.OK).withContentType(contentType).write(byteBuf); - } - } else { - logger.log(Level.WARNING, "failed to access path " + path + " prefix = " + prefix + " requestPath=" + requestPath); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java index 4e17987..e6d1843 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java @@ -1,18 +1,18 @@ package org.xbib.netty.http.server.endpoint.service; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.Filter; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * The {@code MethodHandler} invokes g a handler method on a specified object. * The method must have the same signature and contract as - * {@link Service#handle}, but can have an arbitrary name. + * {@link Filter#handle}, but can have an arbitrary name. */ -public class MethodService implements Service { +public class MethodService implements Filter { private final Method m; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java index 8f9c334..b5a3b8f 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java @@ -7,8 +7,10 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.stream.ChunkedNioStream; import org.xbib.netty.http.common.util.DateTimeUtil; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.Filter; +import org.xbib.netty.http.server.api.Resource; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.util.MimeTypeUtils; import java.io.IOException; @@ -33,13 +35,13 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -public abstract class ResourceService implements Service { +public abstract class ResourceService implements Filter { private static final Logger logger = Logger.getLogger(ResourceService.class.getName()); @Override public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - handleResource(serverRequest, serverResponse, createResource(serverRequest, serverResponse)); + handleCachedResource(serverRequest, serverResponse, createResource(serverRequest, serverResponse)); } protected abstract Resource createResource(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; @@ -50,9 +52,12 @@ public abstract class ResourceService implements Service { protected abstract boolean isRangeResponseEnabled(); - private void handleResource(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource) { + protected abstract int getMaxAgeSeconds(); + + private void handleCachedResource(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource) { + logger.log(Level.FINE, "resource = " + resource); if (resource.isDirectory()) { - if (!resource.getResourcePath().endsWith("/")) { + if (!resource.getResourcePath().isEmpty() && !resource.getResourcePath().endsWith("/")) { // external redirect to serverResponse.withHeader(HttpHeaderNames.LOCATION, resource.getResourcePath() + "/"); ServerResponse.write(serverResponse, HttpResponseStatus.MOVED_PERMANENTLY); @@ -68,18 +73,22 @@ public abstract class ResourceService implements Service { return; } } + // if resource is length of 0, there is nothing to send. Do not send any content, just flush the status + if (resource.getLength() == 0) { + serverResponse.flush(); + return; + } HttpHeaders headers = serverRequest.getHeaders(); String contentType = MimeTypeUtils.guessFromPath(resource.getResourcePath(), false); - long maxAgeSeconds = 24 * 3600; - long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds; + long expirationMillis = System.currentTimeMillis() + 1000 * getMaxAgeSeconds(); if (isCacheResponseEnabled()) { - serverResponse.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis)) - .withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds); + serverResponse.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)) + .withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + getMaxAgeSeconds()); } boolean sent = false; if (isETagResponseEnabled()) { Instant lastModifiedInstant = resource.getLastModified(); - String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength(); + String eTag = Long.toHexString(resource.getResourcePath().hashCode() + lastModifiedInstant.toEpochMilli() + resource.getLength()); Instant ifUnmodifiedSinceInstant = DateTimeUtil.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE)); if (ifUnmodifiedSinceInstant != null && ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { @@ -94,7 +103,7 @@ public abstract class ResourceService implements Service { String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH); if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) { serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis)); + .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); return; } @@ -102,12 +111,12 @@ public abstract class ResourceService implements Service { if (ifModifiedSinceInstant != null && ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis)); + .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatRfc1123(expirationMillis)); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); return; } serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtil.formatInstant(lastModifiedInstant)); + .withHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtil.formatRfc1123(lastModifiedInstant)); if (isRangeResponseEnabled()) { performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers); sent = true; @@ -263,7 +272,7 @@ public abstract class ResourceService implements Service { } private void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, - ServerResponse serverResponse, long offset, long size) throws IOException { + ServerResponse serverResponse, long offset, long size) { if (fileChannel == null) { ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); } else { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java index 1f26775..a09061d 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java @@ -17,8 +17,8 @@ public class IdleTimeoutHandler extends IdleStateHandler { private final Logger logger = Logger.getLogger(IdleTimeoutHandler.class.getName()); - public IdleTimeoutHandler() { - super(30, 30, 30); + public IdleTimeoutHandler(int idleTimeoutMillis) { + super(idleTimeoutMillis, idleTimeoutMillis, idleTimeoutMillis); } @Override diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/Http1ChannelInitializer.java similarity index 78% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/Http1ChannelInitializer.java index b9de484..dda298b 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/Http1ChannelInitializer.java @@ -1,11 +1,11 @@ package org.xbib.netty.http.server.handler.http; import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContentCompressor; @@ -21,17 +21,20 @@ import io.netty.util.DomainNameMapping; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerConfig; +import org.xbib.netty.http.server.api.HttpChannelInitializer; import org.xbib.netty.http.server.handler.ExtendedSNIHandler; +import org.xbib.netty.http.server.handler.IdleTimeoutHandler; import org.xbib.netty.http.server.handler.TrafficLoggingHandler; -import org.xbib.netty.http.server.transport.Transport; +import org.xbib.netty.http.server.api.Transport; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class HttpChannelInitializer extends ChannelInitializer { +public class Http1ChannelInitializer extends ChannelInitializer + implements HttpChannelInitializer { - private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); + private static final Logger logger = Logger.getLogger(Http1ChannelInitializer.class.getName()); private final Server server; @@ -42,9 +45,9 @@ public class HttpChannelInitializer extends ChannelInitializer { private final DomainNameMapping domainNameMapping; - public HttpChannelInitializer(Server server, - HttpAddress httpAddress, - DomainNameMapping domainNameMapping) { + public Http1ChannelInitializer(Server server, + HttpAddress httpAddress, + DomainNameMapping domainNameMapping) { this.server = server; this.serverConfig = server.getServerConfig(); this.httpAddress = httpAddress; @@ -52,10 +55,10 @@ public class HttpChannelInitializer extends ChannelInitializer { } @Override - public void initChannel(SocketChannel channel) { + public void initChannel(Channel channel) { Transport transport = server.newTransport(httpAddress.getVersion()); channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport); - if (serverConfig.isDebug()) { + if (serverConfig.isTrafficDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } if (httpAddress.isSecure()) { @@ -63,35 +66,39 @@ public class HttpChannelInitializer extends ChannelInitializer { } else { configureCleartext(channel); } - if (serverConfig.isDebug()) { + if (serverConfig.isTrafficDebug()) { logger.log(Level.FINE, "HTTP 1 channel initialized: " + channel.pipeline().names()); } } - private void configureEncrypted(SocketChannel channel) { + private void configureEncrypted(Channel channel) { channel.pipeline().addLast("sni-handler", new ExtendedSNIHandler(domainNameMapping, serverConfig, httpAddress)); configureCleartext(channel); } - private void configureCleartext(SocketChannel channel) { + private void configureCleartext(Channel channel) { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast("http-server-codec", new HttpServerCodec(serverConfig.getMaxInitialLineLength(), serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize())); if (serverConfig.isCompressionEnabled()) { - pipeline.addLast("http-server-compressor", new HttpContentCompressor()); + pipeline.addLast("http-server-compressor", + new HttpContentCompressor(6, 15, 8, + serverConfig.getCompressionThreshold())); } if (serverConfig.isDecompressionEnabled()) { - pipeline.addLast("http-server-decompressor", new HttpContentDecompressor()); + pipeline.addLast("http-server-decompressor", + new HttpContentDecompressor()); } HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(serverConfig.getMaxContentLength(), false); httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); pipeline.addLast("http-server-aggregator", httpObjectAggregator); - pipeline.addLast("http-server-pipelining", new HttpPipeliningHandler(1024)); + pipeline.addLast("http-server-pipelining", new HttpPipeliningHandler(serverConfig.getPipeliningCapacity())); pipeline.addLast("http-server-chunked-write", new ChunkedWriteHandler()); pipeline.addLast("http-server-handler", new HttpHandler(server)); + pipeline.addLast("http-idle-timeout-handler", new IdleTimeoutHandler(serverConfig.getIdleTimeoutMillis())); } @Sharable diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java index 83d2a3a..314c69a 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -29,15 +29,18 @@ import io.netty.util.DomainNameMapping; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerConfig; +import org.xbib.netty.http.server.api.HttpChannelInitializer; import org.xbib.netty.http.server.handler.ExtendedSNIHandler; +import org.xbib.netty.http.server.handler.IdleTimeoutHandler; import org.xbib.netty.http.server.handler.TrafficLoggingHandler; -import org.xbib.netty.http.server.transport.Transport; +import org.xbib.netty.http.server.api.Transport; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; -public class Http2ChannelInitializer extends ChannelInitializer { +public class Http2ChannelInitializer extends ChannelInitializer + implements HttpChannelInitializer { private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); @@ -62,7 +65,7 @@ public class Http2ChannelInitializer extends ChannelInitializer { public void initChannel(Channel channel) { Transport transport = server.newTransport(httpAddress.getVersion()); channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport); - if (serverConfig.isDebug()) { + if (serverConfig.isTrafficDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } if (httpAddress.isSecure()) { @@ -70,7 +73,7 @@ public class Http2ChannelInitializer extends ChannelInitializer { } else { configureCleartext(channel); } - if (server.getServerConfig().isDebug() && logger.isLoggable(Level.FINE)) { + if (server.getServerConfig().isTrafficDebug() && logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "HTTP/2 server channel initialized: " + channel.pipeline().names()); } } @@ -100,15 +103,15 @@ public class Http2ChannelInitializer extends ChannelInitializer { new HttpObjectAggregator(serverConfig.getMaxContentLength())); pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); pipeline.addLast("server-request-handler", new ServerRequestHandler()); + pipeline.addLast("server-idle-timeout-handler", new IdleTimeoutHandler(serverConfig.getIdleTimeoutMillis())); } }; Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(channelHandler) .initialSettings(Http2Settings.defaultSettings()); - if (serverConfig.isDebug()) { + if (serverConfig.isTrafficDebug()) { multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); } Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.build(); - HttpServerCodec serverCodec = new HttpServerCodec(); HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java index 7331db5..146ae35 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java @@ -78,9 +78,7 @@ public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec pathParameters = new LinkedHashMap<>(); - private HttpEndpointDescriptor httpEndpointDescriptor; - private HttpParameters parameters; private URL url; @@ -63,28 +63,48 @@ public class HttpServerRequest implements ServerRequest { ChannelHandlerContext ctx) { this.httpRequest = fullHttpRequest.retainedDuplicate(); this.ctx = ctx; - this.httpEndpointDescriptor = new HttpEndpointDescriptor(this); } - void handleParameters() throws IOException { - try { - HttpParameters httpParameters = new HttpParameters(); - this.url = URL.builder() - .path(httpRequest.uri()) // creates path, query params, fragment - .build(); - QueryParameters queryParameters = url.getQueryParams(); - ByteBuf byteBuf = httpRequest.content(); - if (APPLICATION_FORM_URL_ENCODED.equals(HttpUtil.getMimeType(httpRequest)) && byteBuf != null) { - String content = byteBuf.toString(HttpUtil.getCharset(httpRequest, StandardCharsets.ISO_8859_1)); - queryParameters.addPercentEncodedBody(content); - } - for (Pair pair : queryParameters) { - httpParameters.add(pair.getFirst(), pair.getSecond()); - } - this.parameters = httpParameters; - } catch (MalformedInputException | UnmappableCharacterException e) { - throw new IOException(e); + void handleParameters() { + Charset charset = HttpUtil.getCharset(httpRequest, StandardCharsets.UTF_8); + HttpParameters httpParameters = new HttpParameters(); + this.url = URL.builder() + .charset(charset, CodingErrorAction.REPLACE) + .path(httpRequest.uri()) // creates path, query params, fragment + .build(); + QueryParameters queryParameters = url.getQueryParams(); + CharSequence mimeType = HttpUtil.getMimeType(httpRequest); + ByteBuf byteBuf = httpRequest.content(); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "url = " + url + + " mime type = " + mimeType + + " queryParameters = " + queryParameters + + " body exists = " + (byteBuf != null)); } + if (byteBuf != null) { + if (httpRequest.method().equals(HttpMethod.POST) && mimeType != null) { + String params; + // https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4 + if (HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString().equals(mimeType.toString())) { + Charset htmlCharset = HttpUtil.getCharset(httpRequest, StandardCharsets.ISO_8859_1); + params = byteBuf.toString(htmlCharset).replace('+', ' '); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "html form, charset = " + htmlCharset + " param body = " + params); + } + } else { + params = byteBuf.toString(charset); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "not a html form, charset = " + charset + " param body = " + params); + } + } + queryParameters.addPercentEncodedBody(params); + queryParameters.add("_body", params); + } + } + for (Pair pair : queryParameters) { + httpParameters.add(pair.getFirst(), pair.getSecond()); + } + this.parameters = httpParameters; } @Override @@ -92,11 +112,6 @@ public class HttpServerRequest implements ServerRequest { return url; } - @Override - public HttpEndpointDescriptor getEndpointDescriptor() { - return httpEndpointDescriptor; - } - @Override public void setContext(List context) { this.context = context; @@ -115,7 +130,7 @@ public class HttpServerRequest implements ServerRequest { @Override public String getEffectiveRequestPath() { - String path = getEndpointDescriptor().getPath(); + String path = extractPath(getRequestURI()); String effective = contextPath != null && !PATH_SEPARATOR.equals(contextPath) && path.startsWith(contextPath) ? path.substring(contextPath.length()) : path; return effective.isEmpty() ? PATH_SEPARATOR : effective; @@ -215,4 +230,13 @@ public class HttpServerRequest implements ServerRequest { public String toString() { return "ServerRequest[request=" + httpRequest + "]"; } + + private static String extractPath(String uri) { + String path = uri; + int pos = uri.lastIndexOf('#'); + path = pos >= 0 ? path.substring(0, pos) : path; + pos = uri.lastIndexOf('?'); + path = pos >= 0 ? path.substring(0, pos) : path; + return path; + } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java index 225916e..f78d7a7 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java @@ -21,8 +21,8 @@ import io.netty.handler.stream.ChunkedInput; import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerName; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerRequest; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.cookie.ServerCookieEncoder; import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java deleted file mode 100644 index 6760cd5..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.xbib.netty.http.server.transport; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.ssl.SslHandler; -import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.Domain; - -import java.io.IOException; - -public class HttpTransport extends BaseTransport { - - public HttpTransport(Server server) { - super(server); - } - - @Override - public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) - throws IOException { - Domain domain = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); - HttpServerRequest serverRequest = new HttpServerRequest(server, fullHttpRequest, ctx); - try { - serverRequest.setSequenceId(sequenceId); - serverRequest.setRequestId(server.getRequestCounter().incrementAndGet()); - SslHandler sslHandler = ctx.channel().pipeline().get(SslHandler.class); - if (sslHandler != null) { - serverRequest.setSession(sslHandler.engine().getSession()); - } - HttpServerResponse serverResponse = new HttpServerResponse(server, serverRequest, ctx); - if (acceptRequest(domain, serverRequest, serverResponse)) { - serverRequest.handleParameters(); - domain.handle(serverRequest, serverResponse); - } else { - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_ACCEPTABLE); - } - } finally { - serverRequest.release(); - } - } - - @Override - public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) { - // there are no settings in HTTP 1 - } -} diff --git a/netty-http-server/src/main/resources/META-INF/services/org.xbib.netty.http.server.api.ProtocolProvider b/netty-http-server/src/main/resources/META-INF/services/org.xbib.netty.http.server.api.ProtocolProvider new file mode 100644 index 0000000..113af9e --- /dev/null +++ b/netty-http-server/src/main/resources/META-INF/services/org.xbib.netty.http.server.api.ProtocolProvider @@ -0,0 +1,2 @@ +org.xbib.netty.http.server.Http1Provider +org.xbib.netty.http.server.Http2Provider diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/BindExceptionTest.java similarity index 93% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/BindExceptionTest.java index 82a6365..1db792b 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/BindExceptionTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.Domain; import java.io.IOException; @@ -13,7 +13,7 @@ import java.net.BindException; import static org.junit.jupiter.api.Assertions.assertNotNull; -class DoubleServerTest { +class BindExceptionTest { @Test void testDoubleServer() throws IOException { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/RunServerTest.java similarity index 90% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/RunServerTest.java index 1fea5e4..9930821 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/RunServerTest.java @@ -4,11 +4,11 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.Domain; @Disabled -class ServerTest { +class RunServerTest { @Test void testServer() throws Exception { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java index cda5355..599455a 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.Domain; import java.io.IOException; diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/TransportLayerSecurityServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/TransportLayerSecurityServerTest.java index 24f55ff..f03ad2b 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/TransportLayerSecurityServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/TransportLayerSecurityServerTest.java @@ -6,9 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Domain; @@ -49,8 +49,8 @@ class TransportLayerSecurityServerTest { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .content("Hello Jörg", "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); Transport transport = client.execute(request).get(); logger.log(Level.INFO, "HTTP 1.1 TLS protocol = " + transport.getSession().getProtocol()); assertEquals("TLSv1.2", transport.getSession().getProtocol()); @@ -88,8 +88,8 @@ class TransportLayerSecurityServerTest { .setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content("Hello Jörg", "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); Transport transport = client.execute(request).get(); logger.log(Level.INFO, "HTTP/2 TLS protocol = " + transport.getSession().getProtocol()); assertEquals("TLSv1.3", transport.getSession().getProtocol()); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/ClassloaderServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/ClassloaderServiceTest.java index 14a281d..be9e7d5 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/ClassloaderServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/ClassloaderServiceTest.java @@ -5,7 +5,7 @@ import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; @@ -42,7 +42,6 @@ class ClassloaderServiceTest { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/classloader/test.txt")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -50,7 +49,8 @@ class ClassloaderServiceTest { } else { logger.log(Level.WARNING, resp.getStatus().getMessage()); } - }); + }) + .build(); for (int i = 0; i < max; i++) { client.execute(request).get(); } @@ -72,6 +72,7 @@ class ClassloaderServiceTest { Server server = Server.builder(domain) .build(); Client client = Client.builder() + .enableDebug() .build(); int max = 1; final AtomicInteger count = new AtomicInteger(0); @@ -80,14 +81,14 @@ class ClassloaderServiceTest { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/classloader")) - .build() .setResponseListener(resp -> { - if (resp.getStatus().getCode() == HttpResponseStatus.NOT_FOUND.code()) { + if (resp.getStatus().getCode() == HttpResponseStatus.FORBIDDEN.code()) { count.incrementAndGet(); } else { logger.log(Level.WARNING, resp.getStatus().getMessage()); } - }); + }) + .build(); for (int i = 0; i < max; i++) { client.execute(request).get(); } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/EndpointTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/EndpointTest.java index 051d380..843a5a1 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/EndpointTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/EndpointTest.java @@ -5,15 +5,14 @@ import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.endpoint.HttpEndpoint; import org.xbib.netty.http.server.endpoint.HttpEndpointResolver; import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.endpoint.service.FileService; -import org.xbib.netty.http.server.endpoint.service.Service; import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.io.IOException; @@ -37,14 +36,14 @@ class EndpointTest { @Test void testEmptyPrefixEndpoint() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new FileService(vartmp); + FileService fileService = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req + " req context path = " + req.getContextPath()); - service.handle(req, resp); + fileService.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) @@ -60,11 +59,11 @@ class EndpointTest { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -78,14 +77,14 @@ class EndpointTest { @Test void testPlainPrefixEndpoint() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new FileService(vartmp); + FileService fileService = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req + " req context path = " + req.getContextPath()); - service.handle(req, resp); + fileService.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) @@ -101,11 +100,11 @@ class EndpointTest { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -119,7 +118,7 @@ class EndpointTest { @Test void testSimplePathEndpoints() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new FileService(vartmp); + FileService fileService = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static1").setPath("/**").build()) @@ -128,7 +127,7 @@ class EndpointTest { .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req + " req context path = " + req.getContextPath()); - service.handle(req, resp); + fileService.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) @@ -148,27 +147,27 @@ class EndpointTest { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static1/test1.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg 1", resp.getBodyAsString(StandardCharsets.UTF_8)); success1.set(true); - }); + }) + .build(); client.execute(request).get(); Request request1 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static2/test2.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg 2",resp.getBodyAsString(StandardCharsets.UTF_8)); success2.set(true); - }); + }) + .build(); client.execute(request1).get(); Request request2 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static3/test3.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg 3", resp.getBodyAsString(StandardCharsets.UTF_8)); success3.set(true); - }); + }) + .build(); client.execute(request2).get(); } finally { server.shutdownGracefully(); @@ -186,7 +185,7 @@ class EndpointTest { @Test void testQueryEndpoints() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new FileService(vartmp); + FileService fileService = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static1").setPath("/**").build()) @@ -194,7 +193,7 @@ class EndpointTest { .addEndpoint(HttpEndpoint.builder().setPrefix("/static3").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req); - service.handle(req, resp); + fileService.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) @@ -216,7 +215,6 @@ class EndpointTest { .url(server.getServerConfig().getAddress().base() .resolve("/static1/test1.txt")) .addParameter("a", "b") - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 1", resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -224,12 +222,12 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request1).get(); Request request2 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/static2/test2.txt")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 2", resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -237,13 +235,13 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request2).get(); Request request3 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/static3/test3.txt")) .content("{\"a\":\"b\"}","application/json") - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 3",resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -251,7 +249,8 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request3).get(); } finally { server.shutdownGracefully(); @@ -269,29 +268,29 @@ class EndpointTest { @Test void testMultiResolver() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service1 = new FileService(vartmp); - Service service2 = new FileService(vartmp); - Service service3 = new FileService(vartmp); + FileService fileService1 = new FileService(vartmp); + FileService fileService2 = new FileService(vartmp); + FileService fileService3 = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); HttpEndpointResolver httpEndpointResolver1 = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static1").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " context path = " + req.getContextPath()); - service1.handle(req, resp); + fileService1.handle(req, resp); }) .build(); HttpEndpointResolver httpEndpointResolver2 = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static2").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " context path = " + req.getContextPath()); - service2.handle(req, resp); + fileService2.handle(req, resp); }) .build(); HttpEndpointResolver httpEndpointResolver3 = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static3").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " context path = " + req.getContextPath()); - service3.handle(req, resp); + fileService3.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) @@ -315,7 +314,6 @@ class EndpointTest { .url(server.getServerConfig().getAddress().base() .resolve("/static1/test1.txt")) .addParameter("a", "b") - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 1", resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -323,12 +321,13 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request1).get(); Request request2 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/static2/test2.txt")) - .build() + .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 2", resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -336,13 +335,13 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request2).get(); Request request3 = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/static3/test3.txt")) .content("{\"a\":\"b\"}","application/json") - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg 3",resp.getBodyAsString(StandardCharsets.UTF_8)); @@ -350,7 +349,8 @@ class EndpointTest { } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request3).get(); } finally { server.shutdownGracefully(); @@ -391,14 +391,14 @@ class EndpointTest { for (int i = 0; i < max; i++) { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/" + i + "/test.txt")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { count.incrementAndGet(); } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request).get(); } } finally { @@ -432,14 +432,14 @@ class EndpointTest { for (int i = 0; i < max; i++) { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/" + i + "/test.txt")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { count.incrementAndGet(); } else { logger.log(Level.WARNING, resp.getStatus().getReasonPhrase()); } - }); + }) + .build(); client.execute(request).get(); } } finally { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/FileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/FileServiceTest.java index ac06c73..57ffb20 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/FileServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/FileServiceTest.java @@ -5,7 +5,7 @@ import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; @@ -46,11 +46,11 @@ class FileServiceTest { Request request = Request.get() .setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -79,11 +79,11 @@ class FileServiceTest { Request request = Request.get() .setVersion(HttpVersion.valueOf("HTTP/2.0")) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -113,13 +113,13 @@ class FileServiceTest { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/forward_test")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); } - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -151,13 +151,13 @@ class FileServiceTest { Request request = Request.get() .setVersion(HttpVersion.valueOf("HTTP/2.0")) .url(server.getServerConfig().getAddress().base().resolve("/static/forward_test")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); } - }); + }) + .build(); client.execute(request).get(); // client waits for settings, we wait too Thread.sleep(1000L); @@ -191,13 +191,13 @@ class FileServiceTest { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/forward_test?a=b")) - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); } - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/SecureFileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/SecureFileServiceTest.java index 732c3c2..775f721 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/SecureFileServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/endpoint/SecureFileServiceTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; @@ -50,11 +50,11 @@ class SecureFileServiceTest { Request request = Request.get() .setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); @@ -88,11 +88,11 @@ class SecureFileServiceTest { Request request = Request.get() .setVersion(HttpVersion.valueOf("HTTP/2.0")) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); logger.log(Level.INFO, request.toString()); client.execute(request).get(); logger.log(Level.INFO, "request complete"); @@ -130,11 +130,11 @@ class SecureFileServiceTest { .setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base() .resolve("/static/test.txt")) - .build() .setResponseListener(resp -> { assertEquals("Hello Jörg", resp.getBodyAsString(StandardCharsets.UTF_8)); success.set(true); - }); + }) + .build(); client.execute(request).get(); } finally { server.shutdownGracefully(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/hacks/HttpPipeliningHandlerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/hacks/HttpPipeliningHandlerTest.java index 23e871a..f5b327c 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/hacks/HttpPipeliningHandlerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/hacks/HttpPipeliningHandlerTest.java @@ -168,7 +168,7 @@ class HttpPipeliningHandlerTest { private class WorkEmulatorHandler extends SimpleChannelInboundHandler { - private final ExecutorService executorService = Executors.newFixedThreadPool(8); + private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); @Override protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest pipelinedRequest) { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/CleartextTest.java similarity index 88% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/CleartextTest.java index 4dc301c..d8b210f 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/CleartextTest.java @@ -1,18 +1,19 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http1; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; @@ -25,9 +26,9 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(NettyHttpTestExtension.class) -class CleartextHttp1Test { +class CleartextTest { - private static final Logger logger = Logger.getLogger(CleartextHttp1Test.class.getName()); + private static final Logger logger = Logger.getLogger(CleartextTest.class.getName()); @Test void testSimpleClearTextHttp1() throws Exception { @@ -35,7 +36,7 @@ class CleartextHttp1Test { Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getContent().retain())) + request.getContent().toString(StandardCharsets.UTF_8))) .build(); Server server = Server.builder(domain).build(); server.accept(); @@ -52,8 +53,8 @@ class CleartextHttp1Test { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .content("Hello world", "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); @@ -64,12 +65,12 @@ class CleartextHttp1Test { @Test void testPooledClearTextHttp1() throws Exception { - int loop = 1000; + int loop = 1; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getContent().retain())) + request.getContent().toString(StandardCharsets.UTF_8))) .build(); Server server = Server.builder(domain).build(); server.accept(); @@ -89,8 +90,8 @@ class CleartextHttp1Test { Request request = Request.get().setVersion("HTTP/1.1") .url(server.getServerConfig().getAddress().base()) .content(Integer.toString(i), "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); Transport transport = client.newTransport(); transport.execute(request); if (transport.isFailed()) { @@ -114,7 +115,7 @@ class CleartextHttp1Test { Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getContent().retain())) + request.getContent().toString(StandardCharsets.UTF_8))) .build(); Server server = Server.builder(domain).build(); server.accept(); @@ -140,8 +141,8 @@ class CleartextHttp1Test { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); // note: a new transport is created per execution Transport transport = client.newTransport(); transport.execute(request); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/EncryptedTest.java similarity index 91% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/EncryptedTest.java index 2b73ce7..d232839 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/EncryptedTest.java @@ -1,17 +1,18 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http1; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.io.IOException; import java.util.concurrent.ExecutorService; @@ -24,9 +25,9 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(NettyHttpTestExtension.class) -class SecureHttp1Test { +class EncryptedTest { - private static final Logger logger = Logger.getLogger(SecureHttp1Test.class.getName()); + private static final Logger logger = Logger.getLogger(EncryptedTest.class.getName()); @Test void testSimpleSecureHttp1() throws Exception { @@ -48,8 +49,8 @@ class SecureHttp1Test { server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); client.execute(request).get(); } finally { client.shutdownGracefully(); @@ -83,8 +84,8 @@ class SecureHttp1Test { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .content(Integer.toString(i), "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); Transport transport = client.newTransport(); transport.execute(request); if (transport.isFailed()) { @@ -133,8 +134,8 @@ class SecureHttp1Test { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); // note: a new transport is created per execution final Transport transport = client.newTransport(); transport.execute(request); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FlushTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/FlushTest.java similarity index 88% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/FlushTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/FlushTest.java index cfb5f7e..237a1dd 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FlushTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/FlushTest.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http1; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; @@ -6,13 +6,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,7 +31,7 @@ class FlushTest { * @throws Exception exception */ @Test - void testFlush() throws Exception { + void testFlushHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); Domain domain = Domain.builder(httpAddress) .singleEndpoint("/flush", "/**", (req, resp) -> { @@ -60,8 +61,8 @@ class FlushTest { Request getRequest = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/flush")) .addParameter("a", "b") - .build() - .setResponseListener(responseListener1); + .setResponseListener(responseListener1) + .build(); client.execute(getRequest).get(); // second request to trigger flush() @@ -75,8 +76,8 @@ class FlushTest { getRequest = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/flush")) .addParameter("a", "b") - .build() - .setResponseListener(responseListener2); + .setResponseListener(responseListener2) + .build(); client.execute(getRequest).get(); } finally { server.shutdownGracefully(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PostTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PostTest.java new file mode 100644 index 0000000..f369f39 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PostTest.java @@ -0,0 +1,244 @@ +package org.xbib.netty.http.server.test.http1; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.common.HttpParameters; +import org.xbib.netty.http.common.HttpResponse; +import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.api.ServerResponse; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +@ExtendWith(NettyHttpTestExtension.class) +class PostTest { + + private static final Logger logger = Logger.getLogger(PostTest.class.getName()); + + @Test + void testPostHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/post", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); + if ("Hello World".equals(parameters.getFirst("withspace"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } + ServerResponse.write(resp, HttpResponseStatus.OK); + }, "POST") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success1.set(true); + } + }; + Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.ISO_8859_1) + .addParameter("a", "b") + .addFormParameter("my param", "my value") + .addFormParameter("withspace", "Hello World") + .addFormParameter("name", "Jörg") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); + } + + @Test + void testPostUnicodeHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/post", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); + if ("Hello World".equals(parameters.getFirst("withspace"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } + ServerResponse.write(resp, HttpResponseStatus.OK); + }, "POST") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success1.set(true); + } + }; + Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8) + .addParameter("a", "b") + .addFormParameter("my param", "my value") + .addFormParameter("withspace", "Hello World") + .addFormParameter("name", "Jörg") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); + } + + @Test + void testFormPostHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); + final AtomicBoolean success4 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/post", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); + if ("Hello World".equals(parameters.getFirst("withplus"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } + if ("my value".equals(parameters.getFirst("my param"))) { + success4.set(true); + } + ServerResponse.write(resp, HttpResponseStatus.OK); + }, "POST") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .enableDebug() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + logger.log(Level.INFO, resp.getBodyAsString(StandardCharsets.UTF_8)); + success1.set(true); + } + }; + Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8) + .addParameter("a", "b") + // test 'plus' encoding + .addFormParameter("my param", "my value") + .addFormParameter("withplus", "Hello World") + .addFormParameter("name", "Jörg") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); + assertTrue(success4.get()); + } + + @Test + void testTextPlainPostHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); + final AtomicBoolean success4 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/post", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); + if ("Hello World".equals(parameters.getFirst("withoutplus"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } + if ("my value".equals(parameters.getFirst("my param"))) { + success4.set(true); + } + ServerResponse.write(resp, HttpResponseStatus.OK); + }, "POST") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .enableDebug() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + logger.log(Level.INFO, resp.getBodyAsString(StandardCharsets.UTF_8)); + success1.set(true); + } + }; + Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.TEXT_PLAIN, StandardCharsets.UTF_8) + .addParameter("a", "b") + // test 'plus' encoding + .addFormParameter("my param", "my value") + .addFormParameter("withoutplus", "Hello World") + .addFormParameter("name", "Jörg") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); + assertTrue(success4.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PutTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PutTest.java new file mode 100644 index 0000000..ec46238 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/PutTest.java @@ -0,0 +1,121 @@ +package org.xbib.netty.http.server.test.http1; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.common.HttpResponse; +import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.api.ServerResponse; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +@ExtendWith(NettyHttpTestExtension.class) +class PutTest { + + private static final Logger logger = Logger.getLogger(PutTest.class.getName()); + + @Test + void testPutHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/put", "/**", (req, resp) -> { + logger.log(Level.INFO, "got request " + + req.getContent().toString(StandardCharsets.UTF_8)); + ServerResponse.write(resp, HttpResponseStatus.OK); + success1.set(true); + }, "PUT") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success2.set(true); + } + }; + Request postRequest = Request.put() + .setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base() + .resolve("/put/test.txt")) + .content("Hello Jörg", "text/plain") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } + + @Test + void testLargePutHttp1() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + ByteBuf buffer = Unpooled.buffer(); + ByteBufOutputStream outputStream = new ByteBufOutputStream(buffer); + int max = 64 * 1024 * 1024; + for (int i = 0; i < max; i++) { + outputStream.write(1); + } + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/put", "/**", (req, resp) -> { + logger.log(Level.INFO, "got request, length = " + + req.getContent().readableBytes()); + ServerResponse.write(resp, HttpResponseStatus.OK); + success1.set(true); + }, "PUT") + .build(); + Server server = Server.builder(domain) + .setMaxContentLength(max) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success2.set(true); + } + }; + Request postRequest = Request.put() + .setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base() + .resolve("/put/test.txt")) + .content(buffer) + .contentType("application/octet-stream") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/StreamTest.java similarity index 90% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/StreamTest.java index e9563c6..1cea92f 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http1/StreamTest.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http1; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufOutputStream; @@ -7,10 +7,11 @@ import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicInteger; @@ -21,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class StreamTest { @Test - void testServerStreams() throws Exception { + void testServerBodyInputStreamHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); Domain domain = Domain.builder(httpAddress) .singleEndpoint("/", (request, response) -> { @@ -46,13 +47,13 @@ class StreamTest { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/")) .content("my body parameter", "text/plain") - .build() .setResponseListener(resp -> { if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { assertEquals("Hello World", resp.getBodyAsString(StandardCharsets.UTF_8)); count.incrementAndGet(); } - }); + }) + .build(); for (int i = 0; i < max; i++) { client.execute(request).get(); } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/CleartextTest.java similarity index 95% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/CleartextTest.java index 177a838..a760017 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/CleartextTest.java @@ -1,17 +1,18 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http2; import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; @@ -24,9 +25,9 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(NettyHttpTestExtension.class) -class CleartextHttp2Test { +class CleartextTest { - private static final Logger logger = Logger.getLogger(CleartextHttp2Test.class.getName()); + private static final Logger logger = Logger.getLogger(CleartextTest.class.getName()); @Test void testSimpleCleartextHttp2() throws Exception { @@ -56,8 +57,8 @@ class CleartextHttp2Test { Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); Transport transport = client.newTransport(httpAddress); transport.execute(request); if (transport.isFailed()) { @@ -107,8 +108,8 @@ class CleartextHttp2Test { Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); @@ -164,8 +165,8 @@ class CleartextHttp2Test { Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); @@ -252,8 +253,8 @@ class CleartextHttp2Test { //.url(server1.getServerConfig().getAddress().base()) .uri("/") .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/EncryptedTest.java similarity index 92% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/EncryptedTest.java index c9f8f63..8a4690d 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/EncryptedTest.java @@ -1,16 +1,17 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http2; import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; -import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.client.api.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -24,9 +25,9 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(NettyHttpTestExtension.class) -class SecureHttp2Test { +class EncryptedTest { - private static final Logger logger = Logger.getLogger(SecureHttp2Test.class.getName()); + private static final Logger logger = Logger.getLogger(EncryptedTest.class.getName()); @Test void testSimpleSecureHttp2() throws Exception { @@ -57,8 +58,8 @@ class SecureHttp2Test { .setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); transport.get(); } finally { @@ -97,8 +98,8 @@ class SecureHttp2Test { Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); @@ -149,8 +150,8 @@ class SecureHttp2Test { Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/FlushTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/FlushTest.java new file mode 100644 index 0000000..df5b465 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/FlushTest.java @@ -0,0 +1,87 @@ +package org.xbib.netty.http.server.test.http2; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.common.HttpParameters; +import org.xbib.netty.http.common.HttpResponse; +import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.netty.handler.codec.http.HttpResponseStatus; + +@ExtendWith(NettyHttpTestExtension.class) +class FlushTest { + + private static final Logger logger = Logger.getLogger(FlushTest.class.getName()); + + /** + * This test checks the flush() operation of the server response. + * Should be not critical on HTTP/2.0 + * @throws Exception exception + */ + @Test + void testFlushHttp2() throws Exception { + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/flush", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + ", sending 302 Found"); + resp.withStatus(HttpResponseStatus.FOUND).flush(); + }) + .build(); + Server server = Server.builder(domain) + .enableDebug() + .build(); + Client client = Client.builder() + .build(); + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + try { + server.accept(); + + // first request to trigger flush() + + ResponseListener responseListener1 = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.FOUND.code()) { + success1.set(true); + } + }; + Request getRequest = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base().resolve("/flush")) + .addParameter("a", "b") + .setResponseListener(responseListener1) + .build(); + client.execute(getRequest).get(); + + // second request to trigger flush() + + ResponseListener responseListener2 = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.FOUND.code()) { + success2.set(true); + } + }; + getRequest = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base().resolve("/flush")) + .addParameter("a", "b") + .setResponseListener(responseListener2) + .build(); + client.execute(getRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PostTest.java similarity index 57% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PostTest.java index f95f5a1..0ca30b5 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PostTest.java @@ -1,19 +1,21 @@ -package org.xbib.netty.http.server.test; +package org.xbib.netty.http.server.test.http2; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,87 +27,102 @@ class PostTest { private static final Logger logger = Logger.getLogger(PostTest.class.getName()); - @Test - void testPostHttp1() throws Exception { - HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - Domain domain = Domain.builder(httpAddress) - .singleEndpoint("/post", "/**", (req, resp) -> { - HttpParameters parameters = req.getParameters(); - logger.log(Level.INFO, "got request " + parameters.toString() + " , sending, OK"); - ServerResponse.write(resp, HttpResponseStatus.OK); - }, "GET", "POST") - .build(); - Server server = Server.builder(domain) - .build(); - Client client = Client.builder() - .build(); - final AtomicBoolean success = new AtomicBoolean(false); - try { - server.accept(); - - ResponseListener responseListener = (resp) -> { - logger.log(Level.INFO, "got response = " + resp); - if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { - success.set(true); - } - }; - - Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) - .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) - .addParameter("a", "b") - .addFormParameter("name", "Jörg") - .build() - .setResponseListener(responseListener); - client.execute(postRequest).get(); - - logger.log(Level.INFO, "complete"); - } finally { - server.shutdownGracefully(); - client.shutdownGracefully(); - logger.log(Level.INFO, "server and client shut down"); - } - assertTrue(success.get()); - } - @Test void testPostHttp2() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); Domain domain = Domain.builder(httpAddress) .singleEndpoint("/post", "/**", (req, resp) -> { HttpParameters parameters = req.getParameters(); - logger.log(Level.INFO, "got request " + parameters.toString(), ", sending OK"); + logger.log(Level.INFO, "got request " + parameters.toString() + " , sending, OK"); + if ("Hello World".equals(parameters.getFirst("withspace"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } ServerResponse.write(resp, HttpResponseStatus.OK); - }, "POST") + }, "POST") .build(); Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); - final AtomicBoolean success = new AtomicBoolean(false); try { server.accept(); - ResponseListener responseListener = (resp) -> { - logger.log(Level.INFO, "got response = " + resp); if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { - success.set(true); + success1.set(true); } }; - - Request postRequest = Request.post().setVersion("HTTP/2.0") + Request postRequest = Request.post().setVersion("HTP/2.0") .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.ISO_8859_1) .addParameter("a", "b") + .addFormParameter("withspace", "Hello World") .addFormParameter("name", "Jörg") - .build() - .setResponseListener(responseListener); + .setResponseListener(responseListener) + .build(); client.execute(postRequest).get(); - - logger.log(Level.INFO, "complete"); } finally { server.shutdownGracefully(); client.shutdownGracefully(); logger.log(Level.INFO, "server and client shut down"); } - assertTrue(success.get()); + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); + } + + + @Test + void testPostUnicodeHttp2() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + final AtomicBoolean success3 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/post", "/**", (req, resp) -> { + HttpParameters parameters = req.getParameters(); + logger.log(Level.INFO, "got request " + parameters.toString() + " , sending, OK"); + if ("Hello World".equals(parameters.getFirst("withspace"))) { + success2.set(true); + } + if ("Jörg".equals(parameters.getFirst("name"))) { + success3.set(true); + } + ServerResponse.write(resp, HttpResponseStatus.OK); + }, "POST") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success1.set(true); + } + }; + Request postRequest = Request.post().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) + .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8) + .addParameter("a", "b") + .addFormParameter("withspace", "Hello World") + .addFormParameter("name", "Jörg") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + assertTrue(success3.get()); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PutTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PutTest.java new file mode 100644 index 0000000..52e8988 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/PutTest.java @@ -0,0 +1,121 @@ +package org.xbib.netty.http.server.test.http2; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.client.api.ResponseListener; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.common.HttpResponse; +import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.api.ServerResponse; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.HttpResponseStatus; + +@ExtendWith(NettyHttpTestExtension.class) +class PutTest { + + private static final Logger logger = Logger.getLogger(PutTest.class.getName()); + + @Test + void testPutHttp2() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/put", "/**", (req, resp) -> { + logger.log(Level.INFO, "got request " + + req.getContent().toString(StandardCharsets.UTF_8)); + ServerResponse.write(resp, HttpResponseStatus.OK); + success1.set(true); + }, "PUT") + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success2.set(true); + } + }; + Request postRequest = Request.put() + .setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base() + .resolve("/put/test.txt")) + .addParameter("a", "b") + .content("Hello Jörg", "text/plain") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } + + @Test + void testLargePutHttp2() throws Exception { + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + ByteBuf buffer = Unpooled.buffer(); + ByteBufOutputStream outputStream = new ByteBufOutputStream(buffer); + int max = 64 * 1024 * 1024; + for (int i = 0; i < max; i++) { + outputStream.write(1); + } + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/put", "/**", (req, resp) -> { + logger.log(Level.INFO, "got request, length = " + + req.getContent().readableBytes()); + ServerResponse.write(resp, HttpResponseStatus.OK); + success1.set(true); + }, "PUT") + .build(); + Server server = Server.builder(domain) + .setMaxContentLength(max) + .build(); + Client client = Client.builder() + .build(); + try { + server.accept(); + ResponseListener responseListener = (resp) -> { + logger.log(Level.INFO, "got response = " + resp.getStatus()); + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + success2.set(true); + } + }; + Request postRequest = Request.put() + .setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base() + .resolve("/put/test.txt")) + .content(buffer) + .contentType("application/octet-stream") + .setResponseListener(responseListener) + .build(); + client.execute(postRequest).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success1.get()); + assertTrue(success2.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/StreamTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/StreamTest.java new file mode 100644 index 0000000..5e571c1 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/http2/StreamTest.java @@ -0,0 +1,64 @@ +package org.xbib.netty.http.server.test.http2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.api.Request; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Domain; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.test.NettyHttpTestExtension; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +@ExtendWith(NettyHttpTestExtension.class) +class StreamTest { + + @Test + void testServerBodyInputStreamHttp2() throws Exception { + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/", (request, response) -> { + ByteBufInputStream inputStream = request.getInputStream(); + String content = inputStream.readLine(); + assertEquals("my body parameter", content); + ByteBufOutputStream outputStream = response.getOutputStream(); + outputStream.writeBytes("Hello World"); + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(outputStream); + }) + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + int max = 1; + final AtomicInteger count = new AtomicInteger(0); + try { + server.accept(); + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base().resolve("/")) + .content("my body parameter", "text/plain") + .setResponseListener(resp -> { + if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { + assertEquals("Hello World", resp.getBodyAsString(StandardCharsets.UTF_8)); + count.incrementAndGet(); + } + }) + .build(); + for (int i = 0; i < max; i++) { + client.execute(request).get(); + } + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + } + assertEquals(max, count.get()); + } +} diff --git a/settings.gradle b/settings.gradle index 89ac5f9..decb4d7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,9 @@ - include 'netty-http-common' +include 'netty-http-client-api' include 'netty-http-client' include 'netty-http-client-rest' +include 'netty-http-server-api' include 'netty-http-server' include 'netty-http-server-reactive' include 'netty-http-server-rest' +include 'netty-http-rx'