diff --git a/.travis.yml b/.travis.yml index 0039232f7..2b847142c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ cache: directories: - $HOME/.m2 - $HOME/.gradle - + env: global: - secure: cXHzd2WHqmdmJEyEKlELt8Rp9qCvhTRXTEHpQz0sKt55KorI8vO33sSOBs8uBqknWgGgOzHsB7cw0dJRxCmW+BRy90ELtdg/dVLzU8D8BrI6/DHzd/Bhyt9wx2eVdLmDV7lQ113AqJ7lphbH+U8ceTBlbNYDPKcIjFhsPO0WcPxQYed45na8XRK0UcAOpVmmNlTE6fHy5acQblNO84SN6uevCFqWAZJY7rc6xGrzFzca+ul5kR8xIzdE5jKs2Iw0MDeWi8cshkhj9c0FDtfsNIB1F+NafDtEdqjt6kMqYAUUiTAM2QdNoffzgmWEbVOj3uvthlm+S11XaU3Cn2uC7CiZTn2ebuoqCuV5Ge6KQI0ysEQVUfLhIF7iJG6dJvoyYy8ta8LEcjcsYAdF34BVddoUJkp+eJuhlto2aTZsDdXpmnwRM1PPDRoyrLjRcKiWYPR2tO2RG9sb0nRAGEpHTDd5ju2Ta4zpvgpWGUiKprs5R+YY7TEg16VSTYMmCJj5C9ap2lYIH4EoxsQpuxYig9AV1sOUJujLSa4TXqlcOmSM0IkHJ/i0VE8TZg4nV4XowyH6nKZ63InF4pUDcG13BpJQyTFKbK2D0lFn8MzpWvIV2oOUxNkOaOBg9cGhAnv9Sfw/Iv1UVaUgCNQd2M0R0rwfJoPCg2mmWVxsvh3cW4M= diff --git a/README.md b/README.md index d05fce64d..70f135c39 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It enables the following interaction models via async message passing over a sin - fire-and-forget (no response) - event subscription (infinite stream of many) -This is the core project for Java that implements the protocol and exposes Reactive Stream APIs. Typically most use will come via another library that uses this one. +This is the core project for Java that implements the protocol and exposes Reactive Stream APIs. Typically most use will come via another library that uses this one. For example: @@ -27,7 +27,7 @@ Others can be found in the [ReactiveSocket Github](https://github.com/ReactiveSo -Snapshots are available via JFrog. +Snapshots are available via JFrog. Example: @@ -48,7 +48,6 @@ No releases to Maven Central or JCenter have occurred yet. For bugs, questions and discussions please use the [Github Issues](https://github.com/ReactiveSocket/reactivesocket-java/issues). - ## LICENSE Copyright 2015 Netflix, Inc. diff --git a/build.gradle b/build.gradle index 8fbcaa6c4..18eaca87d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,43 @@ buildscript { - repositories { - jcenter() - } - - dependencies { classpath 'io.reactivesocket:gradle-nebula-plugin-reactivesocket:1.0.5' } + repositories { jcenter() } + dependencies { classpath 'io.reactivesocket:gradle-nebula-plugin-reactivesocket:1.0.6' } } description = 'ReactiveSocket: stream oriented messaging passing with Reactive Stream semantics.' apply plugin: 'reactivesocket-project' -apply plugin: 'java' - -repositories { - maven { url 'https://oss.jfrog.org/libs-snapshot' } -} - -dependencies { - compile 'org.reactivestreams:reactive-streams:1.0.0.final' - compile 'org.agrona:Agrona:0.4.13' - testCompile 'io.reactivex:rxjava:2.0.0-DP0-20151003.214425-143' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' +subprojects { + apply plugin: 'reactivesocket-project' + apply plugin: 'java' + + compileJava { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + repositories { + jcenter() + maven { url 'https://oss.jfrog.org/libs-snapshot' } + maven { url 'https://dl.bintray.com/reactivesocket/ReactiveSocket' } + } + + dependencies { + compile 'org.reactivestreams:reactive-streams:1.0.0.final' + compile 'org.agrona:Agrona:0.4.13' + compile 'io.reactivex:rxjava:latest.release' + compile 'io.reactivex:rxjava-reactive-streams:latest.release' + compile 'org.hdrhistogram:HdrHistogram:latest.release' + compile 'org.slf4j:slf4j-api:latest.release' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testRuntime 'org.slf4j:slf4j-simple:1.7.12' + } + + test { + testLogging.showStandardStreams = true + } } // support for snapshot/final releases via versioned branch names like 1.x @@ -33,12 +49,3 @@ nebulaRelease { if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false } - -test { - testLogging.showStandardStreams = true -} - -compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 -} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9..2c6137b87 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f1df5b75c..546631a96 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Mar 07 16:10:12 PST 2016 +#Tue Mar 15 03:05:19 MSK 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/reactivesocket-core/build.gradle b/reactivesocket-core/build.gradle new file mode 100644 index 000000000..60fb959de --- /dev/null +++ b/reactivesocket-core/build.gradle @@ -0,0 +1,3 @@ +dependencies { + testCompile 'io.reactivex:rxjava:2.0.0-DP0-20151003.214425-143' +} \ No newline at end of file diff --git a/src/main/java/io/reactivesocket/ConnectionSetupHandler.java b/reactivesocket-core/src/main/java/io/reactivesocket/ConnectionSetupHandler.java similarity index 100% rename from src/main/java/io/reactivesocket/ConnectionSetupHandler.java rename to reactivesocket-core/src/main/java/io/reactivesocket/ConnectionSetupHandler.java diff --git a/src/main/java/io/reactivesocket/ConnectionSetupPayload.java b/reactivesocket-core/src/main/java/io/reactivesocket/ConnectionSetupPayload.java similarity index 100% rename from src/main/java/io/reactivesocket/ConnectionSetupPayload.java rename to reactivesocket-core/src/main/java/io/reactivesocket/ConnectionSetupPayload.java diff --git a/src/main/java/io/reactivesocket/DefaultReactiveSocket.java b/reactivesocket-core/src/main/java/io/reactivesocket/DefaultReactiveSocket.java similarity index 100% rename from src/main/java/io/reactivesocket/DefaultReactiveSocket.java rename to reactivesocket-core/src/main/java/io/reactivesocket/DefaultReactiveSocket.java diff --git a/src/main/java/io/reactivesocket/DuplexConnection.java b/reactivesocket-core/src/main/java/io/reactivesocket/DuplexConnection.java similarity index 100% rename from src/main/java/io/reactivesocket/DuplexConnection.java rename to reactivesocket-core/src/main/java/io/reactivesocket/DuplexConnection.java diff --git a/src/main/java/io/reactivesocket/Frame.java b/reactivesocket-core/src/main/java/io/reactivesocket/Frame.java similarity index 100% rename from src/main/java/io/reactivesocket/Frame.java rename to reactivesocket-core/src/main/java/io/reactivesocket/Frame.java diff --git a/src/main/java/io/reactivesocket/FrameType.java b/reactivesocket-core/src/main/java/io/reactivesocket/FrameType.java similarity index 100% rename from src/main/java/io/reactivesocket/FrameType.java rename to reactivesocket-core/src/main/java/io/reactivesocket/FrameType.java diff --git a/src/main/java/io/reactivesocket/LeaseGovernor.java b/reactivesocket-core/src/main/java/io/reactivesocket/LeaseGovernor.java similarity index 100% rename from src/main/java/io/reactivesocket/LeaseGovernor.java rename to reactivesocket-core/src/main/java/io/reactivesocket/LeaseGovernor.java diff --git a/src/main/java/io/reactivesocket/Payload.java b/reactivesocket-core/src/main/java/io/reactivesocket/Payload.java similarity index 100% rename from src/main/java/io/reactivesocket/Payload.java rename to reactivesocket-core/src/main/java/io/reactivesocket/Payload.java diff --git a/src/main/java/io/reactivesocket/ReactiveSocket.java b/reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocket.java similarity index 100% rename from src/main/java/io/reactivesocket/ReactiveSocket.java rename to reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocket.java diff --git a/src/main/java/io/reactivesocket/ReactiveSocketConnector.java b/reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocketConnector.java similarity index 100% rename from src/main/java/io/reactivesocket/ReactiveSocketConnector.java rename to reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocketConnector.java diff --git a/src/main/java/io/reactivesocket/ReactiveSocketFactory.java b/reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocketFactory.java similarity index 100% rename from src/main/java/io/reactivesocket/ReactiveSocketFactory.java rename to reactivesocket-core/src/main/java/io/reactivesocket/ReactiveSocketFactory.java diff --git a/src/main/java/io/reactivesocket/RequestHandler.java b/reactivesocket-core/src/main/java/io/reactivesocket/RequestHandler.java similarity index 100% rename from src/main/java/io/reactivesocket/RequestHandler.java rename to reactivesocket-core/src/main/java/io/reactivesocket/RequestHandler.java diff --git a/src/main/java/io/reactivesocket/exceptions/ApplicationException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/ApplicationException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/ApplicationException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/ApplicationException.java diff --git a/src/main/java/io/reactivesocket/exceptions/CancelException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/CancelException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/CancelException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/CancelException.java diff --git a/src/main/java/io/reactivesocket/exceptions/ConnectionException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/ConnectionException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/ConnectionException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/ConnectionException.java diff --git a/src/main/java/io/reactivesocket/exceptions/Exceptions.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/Exceptions.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/Exceptions.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/Exceptions.java diff --git a/src/main/java/io/reactivesocket/exceptions/InvalidRequestException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/InvalidRequestException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/InvalidRequestException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/InvalidRequestException.java diff --git a/src/main/java/io/reactivesocket/exceptions/InvalidSetupException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/InvalidSetupException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/InvalidSetupException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/InvalidSetupException.java diff --git a/src/main/java/io/reactivesocket/exceptions/RejectedException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/RejectedException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/RejectedException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/RejectedException.java diff --git a/src/main/java/io/reactivesocket/exceptions/RejectedSetupException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/RejectedSetupException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/RejectedSetupException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/RejectedSetupException.java diff --git a/src/main/java/io/reactivesocket/exceptions/Retryable.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/Retryable.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/Retryable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/Retryable.java diff --git a/src/main/java/io/reactivesocket/exceptions/SetupException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/SetupException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/SetupException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/SetupException.java diff --git a/src/main/java/io/reactivesocket/exceptions/TransportException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/TransportException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/TransportException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/TransportException.java diff --git a/src/main/java/io/reactivesocket/exceptions/UnsupportedSetupException.java b/reactivesocket-core/src/main/java/io/reactivesocket/exceptions/UnsupportedSetupException.java similarity index 100% rename from src/main/java/io/reactivesocket/exceptions/UnsupportedSetupException.java rename to reactivesocket-core/src/main/java/io/reactivesocket/exceptions/UnsupportedSetupException.java diff --git a/src/main/java/io/reactivesocket/internal/FragmentedPublisher.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/FragmentedPublisher.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/FragmentedPublisher.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/FragmentedPublisher.java diff --git a/src/main/java/io/reactivesocket/internal/PublisherUtils.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/PublisherUtils.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/PublisherUtils.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/PublisherUtils.java diff --git a/src/main/java/io/reactivesocket/internal/Requester.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/Requester.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/Requester.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/Requester.java diff --git a/src/main/java/io/reactivesocket/internal/Responder.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/Responder.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/Responder.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/Responder.java diff --git a/src/main/java/io/reactivesocket/internal/UnicastSubject.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/UnicastSubject.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/UnicastSubject.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/UnicastSubject.java diff --git a/src/main/java/io/reactivesocket/internal/frame/ByteBufferUtil.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ByteBufferUtil.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/ByteBufferUtil.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ByteBufferUtil.java diff --git a/src/main/java/io/reactivesocket/internal/frame/ErrorFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ErrorFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/ErrorFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ErrorFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/FrameHeaderFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/FrameHeaderFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/FrameHeaderFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/FrameHeaderFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/FramePool.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/FramePool.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/FramePool.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/FramePool.java diff --git a/src/main/java/io/reactivesocket/internal/frame/KeepaliveFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/KeepaliveFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/KeepaliveFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/KeepaliveFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/LeaseFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/LeaseFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/LeaseFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/LeaseFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/PayloadBuilder.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadBuilder.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/PayloadBuilder.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadBuilder.java diff --git a/src/main/java/io/reactivesocket/internal/frame/PayloadFragmenter.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadFragmenter.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/PayloadFragmenter.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadFragmenter.java diff --git a/src/main/java/io/reactivesocket/internal/frame/PayloadReassembler.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadReassembler.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/PayloadReassembler.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/PayloadReassembler.java diff --git a/src/main/java/io/reactivesocket/internal/frame/RequestFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/RequestFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/RequestFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/RequestFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/RequestNFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/RequestNFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/RequestNFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/RequestNFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/SetupFrameFlyweight.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/SetupFrameFlyweight.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/SetupFrameFlyweight.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/SetupFrameFlyweight.java diff --git a/src/main/java/io/reactivesocket/internal/frame/ThreadLocalFramePool.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ThreadLocalFramePool.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/ThreadLocalFramePool.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ThreadLocalFramePool.java diff --git a/src/main/java/io/reactivesocket/internal/frame/ThreadSafeFramePool.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ThreadSafeFramePool.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/ThreadSafeFramePool.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/ThreadSafeFramePool.java diff --git a/src/main/java/io/reactivesocket/internal/frame/UnpooledFrame.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/UnpooledFrame.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/frame/UnpooledFrame.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/frame/UnpooledFrame.java diff --git a/src/main/java/io/reactivesocket/internal/rx/AppendOnlyLinkedArrayList.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/AppendOnlyLinkedArrayList.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/AppendOnlyLinkedArrayList.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/AppendOnlyLinkedArrayList.java diff --git a/src/main/java/io/reactivesocket/internal/rx/BackpressureHelper.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BackpressureHelper.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/BackpressureHelper.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BackpressureHelper.java diff --git a/src/main/java/io/reactivesocket/internal/rx/BackpressureUtils.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BackpressureUtils.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/BackpressureUtils.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BackpressureUtils.java diff --git a/src/main/java/io/reactivesocket/internal/rx/BaseArrayQueue.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BaseArrayQueue.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/BaseArrayQueue.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BaseArrayQueue.java diff --git a/src/main/java/io/reactivesocket/internal/rx/BaseLinkedQueue.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BaseLinkedQueue.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/BaseLinkedQueue.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BaseLinkedQueue.java diff --git a/src/main/java/io/reactivesocket/internal/rx/BooleanDisposable.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BooleanDisposable.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/BooleanDisposable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/BooleanDisposable.java diff --git a/src/main/java/io/reactivesocket/internal/rx/CompositeCompletable.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/CompositeCompletable.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/CompositeCompletable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/CompositeCompletable.java diff --git a/src/main/java/io/reactivesocket/internal/rx/CompositeDisposable.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/CompositeDisposable.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/CompositeDisposable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/CompositeDisposable.java diff --git a/src/main/java/io/reactivesocket/internal/rx/EmptyDisposable.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/EmptyDisposable.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/EmptyDisposable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/EmptyDisposable.java diff --git a/src/main/java/io/reactivesocket/internal/rx/EmptySubscription.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/EmptySubscription.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/EmptySubscription.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/EmptySubscription.java diff --git a/src/main/java/io/reactivesocket/internal/rx/LinkedQueueNode.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/LinkedQueueNode.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/LinkedQueueNode.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/LinkedQueueNode.java diff --git a/src/main/java/io/reactivesocket/internal/rx/MpscLinkedQueue.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/MpscLinkedQueue.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/MpscLinkedQueue.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/MpscLinkedQueue.java diff --git a/src/main/java/io/reactivesocket/internal/rx/NotificationLite.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/NotificationLite.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/NotificationLite.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/NotificationLite.java diff --git a/src/main/java/io/reactivesocket/internal/rx/OperatorConcatMap.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/OperatorConcatMap.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/OperatorConcatMap.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/OperatorConcatMap.java diff --git a/src/main/java/io/reactivesocket/internal/rx/Pow2.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/Pow2.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/Pow2.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/Pow2.java diff --git a/src/main/java/io/reactivesocket/internal/rx/QueueDrainHelper.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/QueueDrainHelper.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/QueueDrainHelper.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/QueueDrainHelper.java diff --git a/src/main/java/io/reactivesocket/internal/rx/README.md b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/README.md similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/README.md rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/README.md diff --git a/src/main/java/io/reactivesocket/internal/rx/SerializedSubscriber.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SerializedSubscriber.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/SerializedSubscriber.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SerializedSubscriber.java diff --git a/src/main/java/io/reactivesocket/internal/rx/SpscArrayQueue.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SpscArrayQueue.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/SpscArrayQueue.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SpscArrayQueue.java diff --git a/src/main/java/io/reactivesocket/internal/rx/SpscExactArrayQueue.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SpscExactArrayQueue.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/SpscExactArrayQueue.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SpscExactArrayQueue.java diff --git a/src/main/java/io/reactivesocket/internal/rx/SubscriptionArbiter.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SubscriptionArbiter.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/SubscriptionArbiter.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SubscriptionArbiter.java diff --git a/src/main/java/io/reactivesocket/internal/rx/SubscriptionHelper.java b/reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SubscriptionHelper.java similarity index 100% rename from src/main/java/io/reactivesocket/internal/rx/SubscriptionHelper.java rename to reactivesocket-core/src/main/java/io/reactivesocket/internal/rx/SubscriptionHelper.java diff --git a/src/main/java/io/reactivesocket/lease/FairLeaseGovernor.java b/reactivesocket-core/src/main/java/io/reactivesocket/lease/FairLeaseGovernor.java similarity index 100% rename from src/main/java/io/reactivesocket/lease/FairLeaseGovernor.java rename to reactivesocket-core/src/main/java/io/reactivesocket/lease/FairLeaseGovernor.java diff --git a/src/main/java/io/reactivesocket/lease/NullLeaseGovernor.java b/reactivesocket-core/src/main/java/io/reactivesocket/lease/NullLeaseGovernor.java similarity index 100% rename from src/main/java/io/reactivesocket/lease/NullLeaseGovernor.java rename to reactivesocket-core/src/main/java/io/reactivesocket/lease/NullLeaseGovernor.java diff --git a/src/main/java/io/reactivesocket/lease/UnlimitedLeaseGovernor.java b/reactivesocket-core/src/main/java/io/reactivesocket/lease/UnlimitedLeaseGovernor.java similarity index 100% rename from src/main/java/io/reactivesocket/lease/UnlimitedLeaseGovernor.java rename to reactivesocket-core/src/main/java/io/reactivesocket/lease/UnlimitedLeaseGovernor.java diff --git a/src/main/java/io/reactivesocket/rx/Completable.java b/reactivesocket-core/src/main/java/io/reactivesocket/rx/Completable.java similarity index 100% rename from src/main/java/io/reactivesocket/rx/Completable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/rx/Completable.java diff --git a/src/main/java/io/reactivesocket/rx/Disposable.java b/reactivesocket-core/src/main/java/io/reactivesocket/rx/Disposable.java similarity index 100% rename from src/main/java/io/reactivesocket/rx/Disposable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/rx/Disposable.java diff --git a/src/main/java/io/reactivesocket/rx/Observable.java b/reactivesocket-core/src/main/java/io/reactivesocket/rx/Observable.java similarity index 100% rename from src/main/java/io/reactivesocket/rx/Observable.java rename to reactivesocket-core/src/main/java/io/reactivesocket/rx/Observable.java diff --git a/src/main/java/io/reactivesocket/rx/Observer.java b/reactivesocket-core/src/main/java/io/reactivesocket/rx/Observer.java similarity index 100% rename from src/main/java/io/reactivesocket/rx/Observer.java rename to reactivesocket-core/src/main/java/io/reactivesocket/rx/Observer.java diff --git a/src/main/java/io/reactivesocket/rx/README.md b/reactivesocket-core/src/main/java/io/reactivesocket/rx/README.md similarity index 100% rename from src/main/java/io/reactivesocket/rx/README.md rename to reactivesocket-core/src/main/java/io/reactivesocket/rx/README.md diff --git a/src/main/java/io/reactivesocket/util/ReactiveSocketProxy.java b/reactivesocket-core/src/main/java/io/reactivesocket/util/ReactiveSocketProxy.java similarity index 100% rename from src/main/java/io/reactivesocket/util/ReactiveSocketProxy.java rename to reactivesocket-core/src/main/java/io/reactivesocket/util/ReactiveSocketProxy.java diff --git a/src/main/java/io/reactivesocket/util/Unsafe.java b/reactivesocket-core/src/main/java/io/reactivesocket/util/Unsafe.java similarity index 100% rename from src/main/java/io/reactivesocket/util/Unsafe.java rename to reactivesocket-core/src/main/java/io/reactivesocket/util/Unsafe.java diff --git a/src/perf/java/io/reactivesocket/FramePerf.java b/reactivesocket-core/src/perf/java/io/reactivesocket/FramePerf.java similarity index 100% rename from src/perf/java/io/reactivesocket/FramePerf.java rename to reactivesocket-core/src/perf/java/io/reactivesocket/FramePerf.java diff --git a/src/perf/java/io/reactivesocket/README.md b/reactivesocket-core/src/perf/java/io/reactivesocket/README.md similarity index 100% rename from src/perf/java/io/reactivesocket/README.md rename to reactivesocket-core/src/perf/java/io/reactivesocket/README.md diff --git a/src/perf/java/io/reactivesocket/ReactiveSocketPerf.java b/reactivesocket-core/src/perf/java/io/reactivesocket/ReactiveSocketPerf.java similarity index 100% rename from src/perf/java/io/reactivesocket/ReactiveSocketPerf.java rename to reactivesocket-core/src/perf/java/io/reactivesocket/ReactiveSocketPerf.java diff --git a/src/perf/java/io/reactivesocket/perfutil/PerfTestConnection.java b/reactivesocket-core/src/perf/java/io/reactivesocket/perfutil/PerfTestConnection.java similarity index 100% rename from src/perf/java/io/reactivesocket/perfutil/PerfTestConnection.java rename to reactivesocket-core/src/perf/java/io/reactivesocket/perfutil/PerfTestConnection.java diff --git a/src/perf/java/io/reactivesocket/perfutil/PerfUnicastSubjectNoBackpressure.java b/reactivesocket-core/src/perf/java/io/reactivesocket/perfutil/PerfUnicastSubjectNoBackpressure.java similarity index 100% rename from src/perf/java/io/reactivesocket/perfutil/PerfUnicastSubjectNoBackpressure.java rename to reactivesocket-core/src/perf/java/io/reactivesocket/perfutil/PerfUnicastSubjectNoBackpressure.java diff --git a/src/test/java/io/reactivesocket/FrameTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/FrameTest.java similarity index 100% rename from src/test/java/io/reactivesocket/FrameTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/FrameTest.java diff --git a/src/test/java/io/reactivesocket/LatchedCompletable.java b/reactivesocket-core/src/test/java/io/reactivesocket/LatchedCompletable.java similarity index 100% rename from src/test/java/io/reactivesocket/LatchedCompletable.java rename to reactivesocket-core/src/test/java/io/reactivesocket/LatchedCompletable.java diff --git a/src/test/java/io/reactivesocket/LeaseTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/LeaseTest.java similarity index 100% rename from src/test/java/io/reactivesocket/LeaseTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/LeaseTest.java diff --git a/src/test/java/io/reactivesocket/ReactiveSocketTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/ReactiveSocketTest.java similarity index 100% rename from src/test/java/io/reactivesocket/ReactiveSocketTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/ReactiveSocketTest.java diff --git a/src/test/java/io/reactivesocket/SerializedEventBus.java b/reactivesocket-core/src/test/java/io/reactivesocket/SerializedEventBus.java similarity index 100% rename from src/test/java/io/reactivesocket/SerializedEventBus.java rename to reactivesocket-core/src/test/java/io/reactivesocket/SerializedEventBus.java diff --git a/src/test/java/io/reactivesocket/TestConnection.java b/reactivesocket-core/src/test/java/io/reactivesocket/TestConnection.java similarity index 100% rename from src/test/java/io/reactivesocket/TestConnection.java rename to reactivesocket-core/src/test/java/io/reactivesocket/TestConnection.java diff --git a/src/test/java/io/reactivesocket/TestConnectionWithControlledRequestN.java b/reactivesocket-core/src/test/java/io/reactivesocket/TestConnectionWithControlledRequestN.java similarity index 100% rename from src/test/java/io/reactivesocket/TestConnectionWithControlledRequestN.java rename to reactivesocket-core/src/test/java/io/reactivesocket/TestConnectionWithControlledRequestN.java diff --git a/src/test/java/io/reactivesocket/TestFlowControlRequestN.java b/reactivesocket-core/src/test/java/io/reactivesocket/TestFlowControlRequestN.java similarity index 100% rename from src/test/java/io/reactivesocket/TestFlowControlRequestN.java rename to reactivesocket-core/src/test/java/io/reactivesocket/TestFlowControlRequestN.java diff --git a/src/test/java/io/reactivesocket/TestTransportRequestN.java b/reactivesocket-core/src/test/java/io/reactivesocket/TestTransportRequestN.java similarity index 100% rename from src/test/java/io/reactivesocket/TestTransportRequestN.java rename to reactivesocket-core/src/test/java/io/reactivesocket/TestTransportRequestN.java diff --git a/src/test/java/io/reactivesocket/TestUtil.java b/reactivesocket-core/src/test/java/io/reactivesocket/TestUtil.java similarity index 100% rename from src/test/java/io/reactivesocket/TestUtil.java rename to reactivesocket-core/src/test/java/io/reactivesocket/TestUtil.java diff --git a/src/test/java/io/reactivesocket/internal/FragmenterTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/internal/FragmenterTest.java similarity index 100% rename from src/test/java/io/reactivesocket/internal/FragmenterTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/internal/FragmenterTest.java diff --git a/src/test/java/io/reactivesocket/internal/ReassemblerTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/internal/ReassemblerTest.java similarity index 100% rename from src/test/java/io/reactivesocket/internal/ReassemblerTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/internal/ReassemblerTest.java diff --git a/src/test/java/io/reactivesocket/internal/RequesterTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/internal/RequesterTest.java similarity index 100% rename from src/test/java/io/reactivesocket/internal/RequesterTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/internal/RequesterTest.java diff --git a/src/test/java/io/reactivesocket/internal/ResponderTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/internal/ResponderTest.java similarity index 100% rename from src/test/java/io/reactivesocket/internal/ResponderTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/internal/ResponderTest.java diff --git a/src/test/java/io/reactivesocket/internal/UnicastSubjectTest.java b/reactivesocket-core/src/test/java/io/reactivesocket/internal/UnicastSubjectTest.java similarity index 100% rename from src/test/java/io/reactivesocket/internal/UnicastSubjectTest.java rename to reactivesocket-core/src/test/java/io/reactivesocket/internal/UnicastSubjectTest.java diff --git a/reactivesocket-mime-types/README.md b/reactivesocket-mime-types/README.md new file mode 100644 index 000000000..02c28524b --- /dev/null +++ b/reactivesocket-mime-types/README.md @@ -0,0 +1,66 @@ +## Overview + +This module provides support for encoding/decoding ReactiveSocket data and metadata into using different mime types as defined by [ReactiveSocket protocol](https://github.com/ReactiveSocket/reactivesocket/blob/master/Protocol.md#setup-frame). +The support for mime types is not comprehensive but it will at least support the [default metadata mime type](https://github.com/ReactiveSocket/reactivesocket/blob/mimetypes/MimeTypes.md) + +## Usage + +#### Supported Codecs + +Supported mime types are listed as [SupportedMimeTypes](src/main/java/io/reactivesocket/mimetypes/SupportedMimeTypes.java). + +#### Obtaining the appropriate codec + +[MimeType](src/main/java/io/reactivesocket/mimetypes/MimeType.java) is the interface that provides different methods for encoding/decoding ReactiveSocket data and metadata. +An instance of `MimeType` can be obtained via [MimeTypeFactory](src/main/java/io/reactivesocket/mimetypes/MimeTypeFactory.java). + +A simple usage of `MimeType` is as follows: + +```java +public class ConnectionSetupHandlerImpl implements ConnectionSetupHandler { + + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket reactiveSocket) throws SetupException { + + final MimeType mimeType = MimeTypeFactory.from(setupPayload); // If the mime types aren't supported, throws an error. + + return new RequestHandler() { + + // Not a complete implementation, just a method to demonstrate usage. + @Override + public Publisher handleRequestResponse(Payload payload) { + // use (en/de)codeMetadata() methods to encode/decode metadata + mimeType.decodeMetadata(payload.getMetadata(), KVMetadata.class); + // use (en/de)codeData() methods to encode/decode data + mimeType.decodeData(payload.getData(), Person.class); + return PublisherUtils.empty(); // Do something useful in reality! + } + }; + } +} +``` + +## Build and Binaries + + + +Artifacts are available via JCenter. + +Example: + +```groovy +repositories { + maven { url 'https://jcenter.bintray.com' } +} + +dependencies { + compile 'io.reactivesocket:reactivesocket-mime-types:x.y.z' +} +``` + +No releases to Maven Central have occurred yet. + + +## Bugs and Feedback + +For bugs, questions and discussions please use the [Github Issues](https://github.com/ReactiveSocket/reactivesocket-java-impl/issues). diff --git a/reactivesocket-mime-types/build.gradle b/reactivesocket-mime-types/build.gradle new file mode 100644 index 000000000..869c17c79 --- /dev/null +++ b/reactivesocket-mime-types/build.gradle @@ -0,0 +1,26 @@ +/* + * 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. + * + */ + +dependencies { + compile project(':reactivesocket-core') + compile 'com.fasterxml.jackson.core:jackson-core:latest.release' + compile 'com.fasterxml.jackson.core:jackson-databind:latest.release' + compile 'com.fasterxml.jackson.module:jackson-module-afterburner:latest.release' + compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:latest.release' + + testCompile "org.hamcrest:hamcrest-library:1.3" +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/KVMetadata.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/KVMetadata.java new file mode 100644 index 000000000..47f333ffd --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/KVMetadata.java @@ -0,0 +1,37 @@ +package io.reactivesocket.mimetypes; + +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.function.Function; + +/** + * A representation of ReactiveSocket metadata as a key-value pair. + * + * Implementations are not required to be thread-safe. + */ +public interface KVMetadata extends Map { + + /** + * Lookup the value for the passed key and return the value as a string. + * + * @param key To Lookup. + * @param valueEncoding Encoding for the value. + * + * @return Value as a string with the passed {@code valueEncoding} + * @throws NullPointerException If the key does not exist. + */ + String getAsString(String key, Charset valueEncoding); + + /** + * Creates a new copy of this metadata. + * + * @param newBufferFactory A factory to create new buffer instances to copy, if required. The argument to the + * function is the capacity of the new buffer. + * + * @return New copy of this metadata. + */ + KVMetadata duplicate(Function newBufferFactory); +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeType.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeType.java new file mode 100644 index 000000000..68caac399 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeType.java @@ -0,0 +1,177 @@ +package io.reactivesocket.mimetypes; + +import io.reactivesocket.Frame; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; + +/** + * Encoding and decoding operations for a ReactiveSocket. Since, mime-types for data and metadata do not change once + * setup, a MimeType instance can be stored per ReactiveSocket instance and can be used for repeated encode/decode of + * data and metadata. + */ +public interface MimeType { + + /** + * Decodes metadata of the passed frame to the specified {@code clazz}. + * + * @param toDecode Frame for which metadata is to be decoded. + * @param clazz Class to which metadata will be decoded. + * + * @param Type of the class to which metadata will be decoded. + * + * @return Instance of the class post decode. + */ + default T decodeMetadata(Frame toDecode, Class clazz) { + return decodeMetadata(toDecode.getMetadata(), clazz); + } + + /** + * Decodes the passed buffer to the specified {@code clazz}. + * + * @param toDecode buffer to be decoded. + * @param clazz Class to which the buffer will be decoded. + * + * @param Type of the class to which the buffer will be decoded. + * + * @return Instance of the class post decode. + */ + T decodeMetadata(ByteBuffer toDecode, Class clazz); + + /** + * Decodes the passed buffer to the specified {@code clazz}. + * + * @param Type of the class to which the buffer will be decoded. + * + * @param toDecode buffer to be decoded. + * @param clazz Class to which the buffer will be decoded. + * @param offset Offset in the buffer. + * + * @return Instance of the class post decode. + */ + T decodeMetadata(DirectBuffer toDecode, Class clazz, int offset); + + /** + * Encodes passed metadata to a buffer. + * + * @param toEncode Object to encode as metadata. + * + * @param Type of the object to encode. + * + * @return Buffer with encoded data. + */ + ByteBuffer encodeMetadata(T toEncode); + + /** + * Encodes passed metadata to a buffer. + * + * @param toEncode Object to encode as metadata. + * + * @param Type of the object to encode. + * + * @return Buffer with encoded data. + */ + DirectBuffer encodeMetadataDirect(T toEncode); + + /** + * Encodes passed metadata to the passed buffer. + * + * @param Type of the object to encode. + * @param buffer Encodes the metadata to this buffer. + * @param toEncode Metadata to encode. + * @param offset Offset in the buffer to start writing. + */ + void encodeMetadataTo(MutableDirectBuffer buffer, T toEncode, int offset); + + /** + * Encodes passed metadata to the passed buffer. + * + * @param buffer Encodes the metadata to this buffer. + * @param toEncode Metadata to encode. + * + * @param Type of the object to encode. + */ + void encodeMetadataTo(ByteBuffer buffer, T toEncode); + + /** + * Decodes data of the passed frame to the specified {@code clazz}. + * + * @param toDecode Frame for which metadata is to be decoded. + * @param clazz Class to which metadata will be decoded. + * + * @param Type of the class to which metadata will be decoded. + * + * @return Instance of the class post decode. + */ + default T decodeData(Frame toDecode, Class clazz) { + return decodeData(toDecode.getData(), clazz); + } + + /** + * Decodes the passed buffer to the specified {@code clazz}. + * + * @param toDecode buffer to be decoded. + * @param clazz Class to which the buffer will be decoded. + * + * @param Type of the class to which the buffer will be decoded. + * + * @return Instance of the class post decode. + */ + T decodeData(ByteBuffer toDecode, Class clazz); + + /** + * Decodes the passed buffer to the specified {@code clazz}. + * + * @param Type of the class to which the buffer will be decoded. + * + * @param toDecode buffer to be decoded. + * @param clazz Class to which the buffer will be decoded. + * @param offset Offset in the buffer. + * + * @return Instance of the class post decode. + */ + T decodeData(DirectBuffer toDecode, Class clazz, int offset); + + /** + * Encodes passed data to a buffer. + * + * @param toEncode Object to encode as data. + * + * @param Type of the object to encode. + * + * @return Buffer with encoded data. + */ + ByteBuffer encodeData(T toEncode); + + /** + * Encodes passed data to a buffer. + * + * @param toEncode Object to encode as data. + * + * @param Type of the object to encode. + * + * @return Buffer with encoded data. + */ + DirectBuffer encodeDataDirect(T toEncode); + + /** + * Encodes passed data to the passed buffer. + * + * @param Type of the object to encode. + * @param buffer Encodes the data to this buffer. + * @param toEncode Data to encode. + * @param offset Offset in the buffer to start writing. + */ + void encodeDataTo(MutableDirectBuffer buffer, T toEncode, int offset); + + /** + * Encodes passed data to the passed buffer. + * + * @param buffer Encodes the data to this buffer. + * @param toEncode Data to encode. + * + * @param Type of the object to encode. + */ + void encodeDataTo(ByteBuffer buffer, T toEncode); +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeTypeFactory.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeTypeFactory.java new file mode 100644 index 000000000..6842d7abc --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/MimeTypeFactory.java @@ -0,0 +1,179 @@ +package io.reactivesocket.mimetypes; + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.mimetypes.internal.Codec; +import io.reactivesocket.mimetypes.internal.cbor.CborCodec; +import io.reactivesocket.mimetypes.internal.cbor.ReactiveSocketDefaultMetadataCodec; +import io.reactivesocket.mimetypes.internal.json.JsonCodec; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; +import java.util.EnumMap; + +import static io.reactivesocket.mimetypes.SupportedMimeTypes.*; + +/** + * A factory to retrieve {@link MimeType} instances for {@link SupportedMimeTypes}. The retrieved mime type instances + * are thread-safe. + */ +public final class MimeTypeFactory { + + private static final EnumMap codecs; + private static final EnumMap> mimeTypes; + + static { + codecs = new EnumMap(SupportedMimeTypes.class); + codecs.put(CBOR, CborCodec.create()); + codecs.put(JSON, JsonCodec.create()); + codecs.put(ReactiveSocketDefaultMetadata, ReactiveSocketDefaultMetadataCodec.create()); + + mimeTypes = new EnumMap<>(SupportedMimeTypes.class); + mimeTypes.put(CBOR, getEnumMapForMetadataCodec(CBOR)); + mimeTypes.put(JSON, getEnumMapForMetadataCodec(JSON)); + mimeTypes.put(ReactiveSocketDefaultMetadata, getEnumMapForMetadataCodec(ReactiveSocketDefaultMetadata)); + } + + private MimeTypeFactory() { + } + + /** + * Provides an appropriate {@link MimeType} for the passed {@link ConnectionSetupPayload}. + * Only the mime types represented by {@link SupportedMimeTypes} are supported by this factory. For any other mime + * type, this method will throw an exception. + * It is safer to first retrieve the mime types and then use {@link #from(SupportedMimeTypes, SupportedMimeTypes)} + * method. + * + * @param setup Setup for which the mime type is to be fetched. + * + * @return Appropriate {@link MimeType} for the passed {@code setup}. + * + * @throws IllegalArgumentException If the mime type for either data or metadata is not supported by this factory. + */ + public static MimeType from(ConnectionSetupPayload setup) { + SupportedMimeTypes metaMimeType = parseOrDie(setup.metadataMimeType()); + SupportedMimeTypes dataMimeType = parseOrDie(setup.dataMimeType()); + + return from(metaMimeType, dataMimeType); + } + + /** + * Same as calling {@code from(mimeType, mimeType}. + * + * @param mimeType Mime type to be used for both data and metadata. + * + * @return MimeType for the passed {@code mimeType} + */ + public static MimeType from(SupportedMimeTypes mimeType) { + return from(mimeType, mimeType); + } + + /** + * Provides an appropriate {@link MimeType} for the passed date and metadata mime types. + * + * @param metadataMimeType Mime type for metadata. + * @param dataMimeType Mime type for data. + * + * @return Appropriate {@link MimeType} to use. + */ + public static MimeType from(SupportedMimeTypes metadataMimeType, SupportedMimeTypes dataMimeType) { + if (null == metadataMimeType) { + throw new IllegalArgumentException("Metadata mime type can not be null."); + } + if (null == dataMimeType) { + throw new IllegalArgumentException("Data mime type can not be null."); + } + + return mimeTypes.get(metadataMimeType).get(dataMimeType); + } + + private static EnumMap getEnumMapForMetadataCodec(SupportedMimeTypes metaMime) { + + final Codec metaMimeCodec = codecs.get(metaMime); + + EnumMap toReturn = + new EnumMap(SupportedMimeTypes.class); + + toReturn.put(CBOR, new MimeTypeImpl(metaMimeCodec, codecs.get(CBOR))); + toReturn.put(JSON, new MimeTypeImpl(metaMimeCodec, codecs.get(JSON))); + toReturn.put(ReactiveSocketDefaultMetadata, + new MimeTypeImpl(metaMimeCodec, codecs.get(ReactiveSocketDefaultMetadata))); + + return toReturn; + } + + /*Visible for testing*/ Codec getCodec(SupportedMimeTypes mimeType) { + return codecs.get(mimeType); + } + + private static class MimeTypeImpl implements MimeType { + + private final Codec metaCodec; + private final Codec dataCodec; + + public MimeTypeImpl(Codec metaCodec, Codec dataCodec) { + this.metaCodec = metaCodec; + this.dataCodec = dataCodec; + } + + @Override + public T decodeMetadata(ByteBuffer toDecode, Class clazz) { + return metaCodec.decode(toDecode, clazz); + } + + @Override + public T decodeMetadata(DirectBuffer toDecode, Class clazz, int offset) { + return metaCodec.decode(toDecode, offset, clazz); + } + + @Override + public ByteBuffer encodeMetadata(T toEncode) { + return metaCodec.encode(toEncode); + } + + @Override + public DirectBuffer encodeMetadataDirect(T toEncode) { + return metaCodec.encodeDirect(toEncode); + } + + @Override + public void encodeMetadataTo(MutableDirectBuffer buffer, T toEncode, int offset) { + metaCodec.encodeTo(buffer, toEncode, offset); + } + + @Override + public void encodeMetadataTo(ByteBuffer buffer, T toEncode) { + metaCodec.encodeTo(buffer, toEncode); + } + + @Override + public T decodeData(ByteBuffer toDecode, Class clazz) { + return dataCodec.decode(toDecode, clazz); + } + + @Override + public T decodeData(DirectBuffer toDecode, Class clazz, int offset) { + return dataCodec.decode(toDecode, offset, clazz); + } + + @Override + public ByteBuffer encodeData(T toEncode) { + return dataCodec.encode(toEncode); + } + + @Override + public DirectBuffer encodeDataDirect(T toEncode) { + return dataCodec.encodeDirect(toEncode); + } + + @Override + public void encodeDataTo(MutableDirectBuffer buffer, T toEncode, int offset) { + dataCodec.encodeTo(buffer, toEncode, offset); + } + + @Override + public void encodeDataTo(ByteBuffer buffer, T toEncode) { + dataCodec.encodeTo(buffer, toEncode); + } + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/SupportedMimeTypes.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/SupportedMimeTypes.java new file mode 100644 index 000000000..c1d83cb1b --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/SupportedMimeTypes.java @@ -0,0 +1,59 @@ +package io.reactivesocket.mimetypes; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public enum SupportedMimeTypes { + + /*CBOR encoding*/ + CBOR ("application/cbor"), + /*JSON encoding*/ + JSON ("application/json"), + /*Default ReactiveSocket metadata encoding as specified in + https://github.com/ReactiveSocket/reactivesocket/blob/mimetypes/MimeTypes.md*/ + ReactiveSocketDefaultMetadata ("application/x.reactivesocket.meta+cbor"); + + private final List mimeTypes; + + SupportedMimeTypes(String... mimeTypes) { + this.mimeTypes = Collections.unmodifiableList(Arrays.asList(mimeTypes)); + } + + /** + * Parses the passed string to this enum. + * + * @param mimeType Mimetype to parse. + * + * @return This enum if the mime type is supported, else {@code null} + */ + public static SupportedMimeTypes parse(String mimeType) { + for (SupportedMimeTypes aMimeType : SupportedMimeTypes.values()) { + if (aMimeType.mimeTypes.contains(mimeType)) { + return aMimeType; + } + } + return null; + } + + /** + * Same as {@link #parse(String)} but throws an exception if the passed mime type is not supported. + * + * @param mimeType Mime-type to parse. + * + * @return This enum instance. + * + * @throws IllegalArgumentException If the mime-type is not supported. + */ + public static SupportedMimeTypes parseOrDie(String mimeType) { + SupportedMimeTypes parsed = parse(mimeType); + if (null == parsed) { + throw new IllegalArgumentException("Unsupported mime-type: " + mimeType); + } + return parsed; + } + + public List getMimeTypes() { + return mimeTypes; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodec.java new file mode 100644 index 000000000..ef3119d5c --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodec.java @@ -0,0 +1,129 @@ +package io.reactivesocket.mimetypes.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonGenerator.Feature; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.agrona.DirectBuffer; +import org.agrona.LangUtil; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.agrona.io.DirectBufferInputStream; +import org.agrona.io.MutableDirectBufferOutputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +public abstract class AbstractJacksonCodec implements Codec { + + private static final ThreadLocal directInWrappers = + ThreadLocal.withInitial(DirectBufferInputStream::new); + + private static final ThreadLocal directOutWrappers = + ThreadLocal.withInitial(MutableDirectBufferOutputStream::new); + + private static final ThreadLocal bbInWrappers = + ThreadLocal.withInitial(ByteBufferInputStream::new); + + private static final ThreadLocal bbOutWrappers = + ThreadLocal.withInitial(ByteBufferOutputStream::new); + + private static final byte[] emptyByteArray = new byte[0]; + private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); + private static final DirectBuffer EMPTY_DIRECT_BUFFER = new UnsafeBuffer(emptyByteArray); + + private final ObjectMapper mapper; + + protected AbstractJacksonCodec(ObjectMapper mapper) { + this.mapper = mapper; + } + + protected static void configureDefaults(ObjectMapper mapper) { + mapper.registerModule(new AfterburnerModule()); + mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true); + mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + mapper.configure(SerializationFeature.INDENT_OUTPUT, true); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.enable(Feature.AUTO_CLOSE_TARGET); // encodeTo methods do not close the OutputStream. + SimpleModule module = new SimpleModule(Version.unknownVersion()); + mapper.registerModule(module); + } + + @Override + public T decode(ByteBuffer buffer, Class tClass) { + return _decode(bbInWrappers.get().wrap(buffer), tClass); + } + + @Override + public T decode(DirectBuffer buffer, int offset, Class tClass) { + DirectBufferInputStream stream = directInWrappers.get(); + stream.wrap(buffer, offset, buffer.capacity()); + return _decode(stream, tClass); + } + + @Override + public ByteBuffer encode(T toEncode) { + byte[] bytes = _encode(toEncode); + return bytes == emptyByteArray ? EMPTY_BUFFER : ByteBuffer.wrap(bytes); + } + + @Override + public DirectBuffer encodeDirect(T toEncode) { + byte[] bytes = _encode(toEncode); + return bytes == emptyByteArray ? EMPTY_DIRECT_BUFFER : new UnsafeBuffer(bytes); + } + + @Override + public void encodeTo(ByteBuffer buffer, T toEncode) { + _encodeTo(bbOutWrappers.get().wrap(buffer), toEncode); + } + + @Override + public void encodeTo(MutableDirectBuffer buffer, T toEncode, int offset) { + MutableDirectBufferOutputStream stream = directOutWrappers.get(); + stream.wrap(buffer, offset, buffer.capacity()); + _encodeTo(stream, toEncode); + } + + private T _decode(InputStream stream, Class clazz) { + T v = null; + try { + v = mapper.readValue(stream, clazz); + } catch (IOException e) { + LangUtil.rethrowUnchecked(e); + } + + return v; + } + + private byte[] _encode(Object toEncode) { + byte[] encode = emptyByteArray; + + try { + encode = mapper.writeValueAsBytes(toEncode); + } catch (JsonProcessingException e) { + LangUtil.rethrowUnchecked(e); + } + + return encode; + } + + private void _encodeTo(OutputStream stream, Object toEncode) { + try { + mapper.writeValue(stream, toEncode); + } catch (JsonProcessingException e) { + LangUtil.rethrowUnchecked(e); + } catch (IOException e) { + LangUtil.rethrowUnchecked(e); + } + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferInputStream.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferInputStream.java new file mode 100644 index 000000000..9997317f9 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferInputStream.java @@ -0,0 +1,52 @@ +package io.reactivesocket.mimetypes.internal; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * This code is copied from {@link com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream} with the + * modifications to make the stream mutable per thread. + */ +final class ByteBufferInputStream extends InputStream { + + private ByteBuffer _b; + + public ByteBufferInputStream() { + } + + public ByteBufferInputStream(ByteBuffer _b) { + this._b = _b; + } + + /** + * Returns a {@link ThreadLocal} instance of the {@link InputStream} which wraps the passed buffer. + * + * This instance must not leak from the calling thread. + * + * @param buffer Buffer to wrap. + */ + public ByteBufferInputStream wrap(ByteBuffer buffer) { + _b = buffer; + return this; + } + + @Override + public int available() { + return _b.remaining(); + } + + @Override + public int read() { + return _b.hasRemaining() ? _b.get() & 0xFF : -1; + } + + @Override + public int read(byte[] bytes, int off, int len) { + if (!_b.hasRemaining()) { + return -1; + } + len = Math.min(len, _b.remaining()); + _b.get(bytes, off, len); + return len; + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferOutputStream.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferOutputStream.java new file mode 100644 index 000000000..975803c53 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/ByteBufferOutputStream.java @@ -0,0 +1,38 @@ +package io.reactivesocket.mimetypes.internal; + +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * An {@link OutputStream} backed by a {@link ByteBuffer} which must only be used within the thread that retrieved it + * via {@link #wrap(ByteBuffer)} method. + *

+ * This code is copied from {@link com.fasterxml.jackson.databind.util.ByteBufferBackedOutputStream} with the + * modifications to make the stream mutable per thread. + */ +final class ByteBufferOutputStream extends OutputStream { + + private ByteBuffer _b; + + public ByteBufferOutputStream() { + } + + public ByteBufferOutputStream(ByteBuffer _b) { + this._b = _b; + } + + public ByteBufferOutputStream wrap(ByteBuffer buffer) { + _b = buffer; + return this; + } + + @Override + public void write(int b) { + _b.put((byte) b); + } + + @Override + public void write(byte[] bytes, int off, int len) { + _b.put(bytes, off, len); + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/Codec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/Codec.java new file mode 100644 index 000000000..d04acfe6e --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/Codec.java @@ -0,0 +1,21 @@ +package io.reactivesocket.mimetypes.internal; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; + +public interface Codec { + + T decode(ByteBuffer buffer, Class tClass); + + T decode(DirectBuffer buffer, int offset, Class tClass); + + ByteBuffer encode(T toEncode); + + DirectBuffer encodeDirect(T toEncode); + + void encodeTo(ByteBuffer buffer, T toEncode); + + void encodeTo(MutableDirectBuffer buffer, T toEncode, int offset); +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/KVMetadataImpl.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/KVMetadataImpl.java new file mode 100644 index 000000000..e8bf2f937 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/KVMetadataImpl.java @@ -0,0 +1,143 @@ +package io.reactivesocket.mimetypes.internal; + +import io.reactivesocket.mimetypes.KVMetadata; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +public class KVMetadataImpl implements KVMetadata { + + private Map store; + + public KVMetadataImpl(Map store) { + this.store = store; + } + + public KVMetadataImpl() { + store = new HashMap<>(); + } + + public void setStore(Map store) { + if (null == store) { + throw new IllegalArgumentException("Store can not be null"); + } + this.store = store; + } + + public Map getStore() { + return store; + } + + @Override + public String getAsString(String key, Charset valueEncoding) { + ByteBuffer toReturn = get(key); + + if (null != toReturn) { + byte[] dst = new byte[toReturn.remaining()]; + toReturn.get(dst); + return new String(dst, valueEncoding); + } + + return null; + } + + @Override + public KVMetadata duplicate(Function newBufferFactory) { + Map copy = new HashMap<>(store); + return new KVMetadataImpl(copy); + } + + @Override + public int size() { + return store.size(); + } + + @Override + public boolean isEmpty() { + return store.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return store.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return store.containsValue(value); + } + + @Override + public ByteBuffer get(Object key) { + return store.get(key); + } + + @Override + public ByteBuffer put(String key, ByteBuffer value) { + return store.put(key, value); + } + + @Override + public ByteBuffer remove(Object key) { + return store.remove(key); + } + + @Override + public void putAll(Map m) { + store.putAll(m); + } + + @Override + public void clear() { + store.clear(); + } + + @Override + public Set keySet() { + return store.keySet(); + } + + @Override + public Collection values() { + return store.values(); + } + + @Override + public Set> entrySet() { + return store.entrySet(); + } + + @Override + public String toString() { + return "KVMetadataImpl{" + "store=" + store + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KVMetadataImpl)) { + return false; + } + + KVMetadataImpl that = (KVMetadataImpl) o; + + if (!store.equals(that.store)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return store.hashCode(); + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/MalformedInputException.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/MalformedInputException.java new file mode 100644 index 000000000..95d1d435a --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/MalformedInputException.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.reactivesocket.mimetypes.internal; + +public class MalformedInputException extends RuntimeException { + + private static final long serialVersionUID = 3130502874275862715L; + + public MalformedInputException(String message) { + super(message); + } + + public MalformedInputException(String message, Throwable cause) { + super(message, cause); + } + + public MalformedInputException(Throwable cause) { + super(cause); + } + + public MalformedInputException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORMap.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORMap.java new file mode 100644 index 000000000..7a7f43ac5 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORMap.java @@ -0,0 +1,293 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.internal.frame.ByteBufferUtil; +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static io.reactivesocket.mimetypes.internal.cbor.CborMajorType.*; + +/** + * A representation of CBOR map as defined in the spec.

+ * + * The benefit of this class is that it does not create additional buffers for the values of the metadata, when + * possible. Instead it holds views into the original buffer and when queried creates a slice of the underlying buffer + * that represents the value. Thus, it is lean in terms of memory usage as compared to other standard libraries that + * allocate memory for all values. + * + *

Allocations

+ * + *

Modifications

+ * + * Any additions to the map (adding one or more key-value pairs) will create a map with all keys and values, where + * values are the buffer slices of the original buffer. From then onwards, the newly created map will be used for all + * further queries.

+ * So, additions to this map will result in more allocations than usual but it still does not allocate fresh memory + * for existing entries. + * + *

Access

+ * + * Bulk queries like {@link #entrySet()}, {@link #values()} and value queries like {@link #containsValue(Object)} will + * switch to a new map as described in case of modifications above. + * + *

Structure

+ * + * In absence of the above cases for allocations, this map uses an index of {@code String} keys to a {@code Long}. The + * first 32 bits of this {@code Long} holds the length of the value and the next 32 bits contain the offset in the + * original buffer. {@link #encodeValueMask(int, long)} encodes this mask and {@link #decodeLengthFromMask(long)}, + * {@link #decodeOffsetFromMask(long)} decodes the mask. + */ +public class CBORMap implements Map { + + protected final DirectBuffer backingBuffer; + protected final int offset; + protected final Map keysVsOffsets; + protected Map storeWhenModified; + + public CBORMap(DirectBuffer backingBuffer, int offset) { + this(backingBuffer, offset, 16, 0.75f); + } + + public CBORMap(DirectBuffer backingBuffer, int offset, int initialCapacity) { + this(backingBuffer, offset, initialCapacity, 0.75f); + } + + public CBORMap(DirectBuffer backingBuffer, int offset, int initialCapacity, float loadFactor) { + this.backingBuffer = backingBuffer; + this.offset = offset; + keysVsOffsets = new HashMap<>(initialCapacity, loadFactor); + } + + protected CBORMap(Map storeWhenModified) { + backingBuffer = new UnsafeBuffer(IndexedUnsafeBuffer.EMPTY_ARRAY); + offset = 0; + this.storeWhenModified = storeWhenModified; + keysVsOffsets = Collections.emptyMap(); + } + + protected CBORMap(DirectBuffer backingBuffer, int offset, Map keysVsOffsets) { + this.backingBuffer = backingBuffer; + this.offset = offset; + this.keysVsOffsets = keysVsOffsets; + storeWhenModified = null; + } + + public Long putValueOffset(String key, int offset, int length) { + return keysVsOffsets.put(key, encodeValueMask(offset, length)); + } + + @Override + public int size() { + return null != storeWhenModified ? storeWhenModified.size() : keysVsOffsets.size(); + } + + @Override + public boolean isEmpty() { + return null != storeWhenModified ? storeWhenModified.isEmpty() : keysVsOffsets.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return null != storeWhenModified ? storeWhenModified.containsKey(key) : keysVsOffsets.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + switchToAlternateMap(); + return storeWhenModified.containsValue(value); + } + + @Override + public ByteBuffer get(Object key) { + return getByteBuffer((String) key); + } + + @Override + public ByteBuffer put(String key, ByteBuffer value) { + if (null != storeWhenModified) { + return storeWhenModified.put(key, value); + } + + switchToAlternateMap(); + return storeWhenModified.put(key, value); + } + + @Override + public ByteBuffer remove(Object key) { + if (null != storeWhenModified) { + return storeWhenModified.remove(key); + } + + Long removed = keysVsOffsets.remove(key); + return getFromBackingBuffer(removed); + } + + @Override + public void putAll(Map m) { + if (null != storeWhenModified) { + storeWhenModified.putAll(m); + } else { + switchToAlternateMap(); + storeWhenModified.putAll(m); + } + } + + @Override + public void clear() { + if (null != storeWhenModified) { + storeWhenModified.clear(); + } else { + keysVsOffsets.clear(); + } + } + + @Override + public Set keySet() { + return null != storeWhenModified ? storeWhenModified.keySet() : keysVsOffsets.keySet(); + } + + @Override + public Collection values() { + switchToAlternateMap(); + return storeWhenModified.values(); + } + + @Override + public Set> entrySet() { + switchToAlternateMap(); + return storeWhenModified.entrySet(); + } + + public void encodeTo(IndexedUnsafeBuffer dst) { + if (null == storeWhenModified) { + final int size = keysVsOffsets.size(); + CborHeader.forLengthToEncode(size).encode(dst, MAP, size); + for (Entry entry : keysVsOffsets.entrySet()) { + CborUtf8StringCodec.encode(dst, entry.getKey()); + + Long valueMask = entry.getValue(); + int valueLength = decodeLengthFromMask(valueMask); + int valueOffset = decodeOffsetFromMask(valueMask); + CborBinaryStringCodec.encode(dst, backingBuffer, valueOffset, valueLength); + } + } else { + CborMapCodec.encode(dst, storeWhenModified); + } + } + + DirectBuffer getBackingBuffer() { + return backingBuffer; + } + + private ByteBuffer getByteBuffer(String key) { + if (null == storeWhenModified) { + Long valueMask = keysVsOffsets.get(key); + return null == valueMask ? null : getFromBackingBuffer(valueMask); + } else { + return storeWhenModified.get(key); + } + } + + private void switchToAlternateMap() { + if (null != storeWhenModified) { + return; + } + storeWhenModified = new HashMap<>(keysVsOffsets.size()); + for (Entry entry : keysVsOffsets.entrySet()) { + storeWhenModified.put(entry.getKey(), getFromBackingBuffer(entry.getValue())); + } + } + + private ByteBuffer getFromBackingBuffer(Long valueMask) { + int offset = this.offset + decodeOffsetFromMask(valueMask); + int length = decodeLengthFromMask(valueMask); + + ByteBuffer bb = backingBuffer.byteBuffer(); + if (null == bb) { + bb = ByteBuffer.wrap(backingBuffer.byteArray()); + } + return ByteBufferUtil.preservingSlice(bb, offset, offset + length); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CBORMap)) { + return false; + } + + CBORMap that = (CBORMap) o; + + if (offset != that.offset) { + return false; + } + if (backingBuffer != null? !backingBuffer.equals(that.backingBuffer) : that.backingBuffer != null) { + return false; + } + if (keysVsOffsets != null? !keysVsOffsets.equals(that.keysVsOffsets) : that.keysVsOffsets != null) { + return false; + } + if (storeWhenModified != null? !storeWhenModified.equals(that.storeWhenModified) : + that.storeWhenModified != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = backingBuffer != null? backingBuffer.hashCode() : 0; + result = 31 * result + offset; + result = 31 * result + (keysVsOffsets != null? keysVsOffsets.hashCode() : 0); + result = 31 * result + (storeWhenModified != null? storeWhenModified.hashCode() : 0); + return result; + } + + @Override + public String toString() { + String sb = "CBORMap{" + "backingBuffer=" + backingBuffer + + ", offset=" + offset + + ", keysVsOffsets=" + keysVsOffsets + + ", storeWhenModified=" + (null == storeWhenModified ? "null" : storeWhenModified) + + '}'; + return sb; + } + + static long encodeValueMask(int offset, long length) { + return length << 32 | offset & 0xFFFFFFFFL; + } + + static int decodeLengthFromMask(long valueMask) { + return (int) (valueMask >> 32); + } + + static int decodeOffsetFromMask(long valueMask) { + return (int) valueMask; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtils.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtils.java new file mode 100644 index 000000000..a61b2f320 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtils.java @@ -0,0 +1,125 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.MalformedInputException; + +import java.util.function.Function; + +import static io.reactivesocket.mimetypes.internal.cbor.CborMajorType.*; + +public final class CBORUtils { + + public static final StackTraceElement[] EMPTY_STACK = new StackTraceElement[0]; + public static final Function BREAK_SCANNER = aByte -> aByte != CBOR_BREAK; + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final MalformedInputException TOO_LONG_LENGTH = + new MalformedInputException("Length of a field is longer than: " + Integer.MAX_VALUE + " bytes."); + + static { + TOO_LONG_LENGTH.setStackTrace(EMPTY_STACK); + } + + private CBORUtils() { + } + + /** + * Parses the passed {@code buffer} and returns the length of the data following the index at the + * {@link IndexedUnsafeBuffer#getReaderIndex()}.

+ * + *

Special cases

+ *
    +
  • If the next data is a "Break" then -1 is + returned.
  • +
+ * + * @param buffer Buffer which will be parsed to determine the length of the next data item. + * @param expectedType {@link CborMajorType} to expect. + * @param errorIfMismatchType Error to throw if the type is not as expected. + * + * @return Length of the following data item. {@code -1} if the type is a Break. + */ + public static long parseDataLengthOrDie(IndexedUnsafeBuffer buffer, CborMajorType expectedType, + RuntimeException errorIfMismatchType) { + final byte header = (byte) buffer.readUnsignedByte(); + final CborMajorType type = fromUnsignedByte(header); + + if (type == Break) { + return -1; + } + + if (type != expectedType) { + throw errorIfMismatchType; + } + + return CborHeader.readDataLength(buffer, header); + } + + /** + * Returns the length in bytes that the passed {@code bytesToEncode} will be when encoded as CBOR. + * + * @param bytesToEncode Length in bytes to encode. + * + * @return Length in bytes post encode. + */ + public static long getEncodeLength(long bytesToEncode) { + CborHeader header = CborHeader.forLengthToEncode(bytesToEncode); + return bytesToEncode + header.getSizeInBytes(); + } + + /** + * Encodes the passed {@code type} with {@code length} as a CBOR data header. The encoding is written on to the + * passed {@code buffer} + * + * @param buffer Buffer to encode to. + * @param type Type to encode. + * @param length Length of data that will be encoded. + * + * @return Number of bytes written on to the buffer for this encoding. + */ + public static int encodeTypeHeader(IndexedUnsafeBuffer buffer, CborMajorType type, long length) { + CborHeader header = CborHeader.forLengthToEncode(length); + header.encode(buffer, type, length); + return header.getSizeInBytes(); + } + + /** + * Encodes the passed {@code type} with indefinite length. The encoding is written on to the passed {@code buffer} + * + * @param buffer Buffer to encode to. + * @param type Type to encode. + * + * @return Number of bytes written on to the buffer for this encoding. + */ + public static int encodeIndefiniteTypeHeader(IndexedUnsafeBuffer buffer, CborMajorType type) { + return encodeTypeHeader(buffer, type, -1); + } + + /** + * Returns the length(in bytes) till the next CBOR break i.e. {@link CborMajorType#CBOR_BREAK}. + * This method does not move the {@code readerIndex} for the passed buffer. + * @param src Buffer to scan. + * + * @return Index of the next CBOR break in the source buffer. {@code -1} if break is not found. + */ + public static int scanToBreak(IndexedUnsafeBuffer src) { + int i = src.forEachByte(BREAK_SCANNER); + return i == src.getBackingBuffer().capacity() ? -1 : i; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodec.java new file mode 100644 index 000000000..7a2bd8be2 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodec.java @@ -0,0 +1,85 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.MalformedInputException; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; + +import static io.reactivesocket.mimetypes.internal.cbor.CBORUtils.*; +import static io.reactivesocket.mimetypes.internal.cbor.CborMajorType.*; + +final class CborBinaryStringCodec { + + @SuppressWarnings("ThrowableInstanceNeverThrown") + static final MalformedInputException NOT_BINARY_STRING = + new MalformedInputException("Data is not a definite length binary string."); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + static final MalformedInputException INDEFINITE_LENGTH_NOT_SUPPORTED = + new MalformedInputException("Indefinite length binary string parsing not supported."); + + static { + NOT_BINARY_STRING.setStackTrace(EMPTY_STACK); + INDEFINITE_LENGTH_NOT_SUPPORTED.setStackTrace(EMPTY_STACK); + } + + private CborBinaryStringCodec() { + } + + public static void encode(IndexedUnsafeBuffer dst, DirectBuffer src, int offset, int length) { + encodeTypeHeader(dst, ByteString, length); + dst.writeBytes(src, offset, length); + } + + public static void encode(IndexedUnsafeBuffer dst, ByteBuffer src) { + encodeTypeHeader(dst, ByteString, src.remaining()); + dst.writeBytes(src, src.remaining()); + } + + public static void decode(IndexedUnsafeBuffer src, IndexedUnsafeBuffer dst) { + int length = decode(src, dst.getBackingBuffer(), 0); + dst.incrementWriterIndex(length); + } + + public static int decode(IndexedUnsafeBuffer src, MutableDirectBuffer dst, int offset) { + int length = (int) parseDataLengthOrDie(src, ByteString, NOT_BINARY_STRING); + if (length < 0) { + throw NOT_BINARY_STRING; + } + + if (length == CborHeader.INDEFINITE.getCode()) { + while (true) { + byte aByte = src.getBackingBuffer().getByte(src.getReaderIndex()); + if (aByte == CBOR_BREAK) { + break; + } + + int chunkLength = (int) parseDataLengthOrDie(src, ByteString, NOT_BINARY_STRING); + src.readBytes(dst, offset, chunkLength); + offset += chunkLength; + } + } else { + src.readBytes(dst, offset, length); + } + + return length; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborCodec.java new file mode 100644 index 000000000..9dd7dac38 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborCodec.java @@ -0,0 +1,33 @@ +package io.reactivesocket.mimetypes.internal.cbor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import io.reactivesocket.mimetypes.internal.AbstractJacksonCodec; + +public class CborCodec extends AbstractJacksonCodec { + + private CborCodec(ObjectMapper mapper) { + super(mapper); + } + + /** + * Creates a {@link CborCodec} with default configurations. Use {@link #create(ObjectMapper)} for custom mapper + * configurations. + * + * @return A new instance of {@link CborCodec} with default mapper configurations. + */ + public static CborCodec create() { + ObjectMapper mapper = new ObjectMapper(new CBORFactory()); + configureDefaults(mapper); + return create(mapper); + } + + /** + * Creates a {@link CborCodec} with custom mapper. Use {@link #create()} for default mapper configurations. + * + * @return A new instance of {@link CborCodec} with the passed mapper. + */ + public static CborCodec create(ObjectMapper mapper) { + return new CborCodec(mapper); + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborHeader.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborHeader.java new file mode 100644 index 000000000..da9813634 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborHeader.java @@ -0,0 +1,221 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.agrona.BitUtil; +import rx.functions.Action2; +import rx.functions.Actions; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * CBOR uses a compact format to encode the length and type of data that is written. More details can be found in the + * spec but it follows the following format: + * + *

First Byte

+ * The first byte of the header has the following data encoded: + *
    +
  • Data type as specified by {@link CborMajorType#getTypeCode()}.
  • +
  • Actual length of the following data element.
  • +
+ * + * The byte layout is as follows: + *
+ 0
+ 0 1 2 3 4 5 6 7
+ +-+-+-+-+-+-+-+
+ | T |  Code   |
+ +-------------+
+ * 
+ * + * <T> above is the Data type.

+ * <Code> above is the actual length for Header {@link #SMALL} and the code {@link #getCode()} for all other + * headers. + * + *

Remaining Bytes

+ * + * Headers {@link #SMALL} and {@link #INDEFINITE} does not contain any other bytes after the first bytes. The other + * headers contain {@link #getSizeInBytes()} {@code - 1} more bytes containing the actual length of the following data. + * + * This class abstracts all the above rules to correctly encode and decode this type headers. + */ +public enum CborHeader { + + INDEFINITE(1, 31, Actions.empty(), + aLong -> aLong < 0, + buffer -> 31L, + aLong -> (byte) 31), + SMALL(1, -1, + Actions.empty(), + aLong -> aLong < 24, buffer -> -1L, + aLong -> aLong.byteValue()), + BYTE(1 + BitUtil.SIZE_OF_BYTE, 24, + (buffer, aLong) -> buffer.writeByte((byte) aLong.shortValue()), + aLong -> aLong <= Byte.MAX_VALUE, + buffer -> (long)buffer.readByte(), aLong -> (byte) 24), + SHORT(1 + BitUtil.SIZE_OF_SHORT, 25, + (buffer, aLong) -> buffer.writeShort(aLong.shortValue()), + aLong -> aLong <= Short.MAX_VALUE, + buffer -> (long)buffer.readShort(), + aLong -> (byte) 25), + INT(1 + BitUtil.SIZE_OF_INT, 26, + (buffer, aLong) -> buffer.writeInt(aLong.intValue()), + aLong -> aLong <= Integer.MAX_VALUE, + buffer -> (long)buffer.readInt(), + aLong -> (byte) 26), + LONG(1 + BitUtil.SIZE_OF_LONG, 27, + (buffer, aLong) -> buffer.writeLong(aLong), + aLong -> aLong <= Long.MAX_VALUE, + buffer -> (long)buffer.readLong(), + aLong -> (byte) 27); + + private static final int LENGTH_MASK = 0b000_11111; + private final static Map reverseIndex; + + static { + reverseIndex = new HashMap<>(CborHeader.values().length); + for (CborHeader h : CborHeader.values()) { + reverseIndex.put(h.code, h); + } + } + + private final short sizeInBytes; + private final int code; + private final Action2 encodeFunction; + private final Function matchFunction; + private final Function decodeFunction; + private final Function codeFunction; + + CborHeader(int sizeInBytes, int code, Action2 encodeFunction, + Function matchFunction, Function decodeFunction, + Function codeFunction) { + this.sizeInBytes = (short) sizeInBytes; + this.code = code; + this.encodeFunction = encodeFunction; + this.matchFunction = matchFunction; + this.decodeFunction = decodeFunction; + this.codeFunction = codeFunction; + } + + + + /** + * Returns {@link CborHeader} instance appropriate for encoding the passed {@code bytesToEncode}. + * + * @param bytesToEncode Number of bytes to encode. + * + * @return {@link CborHeader} appropriate for encoding the passed number of bytes. + */ + public static CborHeader forLengthToEncode(long bytesToEncode) { + if (INDEFINITE.matchFunction.apply(bytesToEncode)) { + return INDEFINITE; + } + + if (SMALL.matchFunction.apply(bytesToEncode)) { + return SMALL; + } + + if (BYTE.matchFunction.apply(bytesToEncode)) { + return BYTE; + } + + if (SHORT.matchFunction.apply(bytesToEncode)) { + return SHORT; + } + + if (INT.matchFunction.apply(bytesToEncode)) { + return INT; + } + + return LONG; + } + + /** + * Returns the number of bytes that this header will encode to. + * + * @return The number of bytes that this header will encode to. + */ + public short getSizeInBytes() { + return sizeInBytes; + } + + /** + * The CBOR code that will be encoded in the first byte of the header. + * {@link CborHeader#SMALL} will return -1 as there is no code for it. Instead it encodes the actual length. + * + * @return The number of bytes that this header will encode to. + */ + public int getCode() { + return code; + } + + /** + * Encodes the passed {@code type} and {@code length} into the passed {@code buffer}. + * + * @param buffer Destination for the encoding. + * @param type Type to encode. + * @param length Length to encode. Can be {@code -1} for {@link #INDEFINITE}, otherwise has to be a positive + * number. + * + * @throws IllegalArgumentException If the length is negative (for all headers except {@link #INDEFINITE}. + */ + public void encode(IndexedUnsafeBuffer buffer, CborMajorType type, long length) { + if (length == -1 && this != INDEFINITE && length < 0) { + throw new IllegalArgumentException("Length must be positive."); + } + + byte code = codeFunction.apply(length); + int firstByte = type.getTypeCode() << 5 | code; + + buffer.writeByte((byte) firstByte); + encodeFunction.call(buffer, length); + } + + /** + * Encodes the passed {@code type} for indefinite length into the passed {@code buffer}. Same as calling + * {@link #encode(IndexedUnsafeBuffer, CborMajorType, long)} with {@code -1} as length. + */ + public void encodeIndefiniteLength(IndexedUnsafeBuffer buffer, CborMajorType type) { + encode(buffer, type, -1); + } + + /** + * Given the first byte of a CBOR data type and length header, returns the length of the data item that follows the + * header. This will read {@link #getSizeInBytes()} number of bytes from the passed buffer corresponding to the + * {@link CborHeader} encoded in the first byte. + */ + public static long readDataLength(IndexedUnsafeBuffer buffer, short firstHeaderByte) { + int firstLength = readLengthFromFirstHeaderByte(firstHeaderByte); + CborHeader cborHeader = reverseIndex.get(firstLength); + if (null != cborHeader) { + return cborHeader.decodeFunction.apply(buffer); + } + + return firstLength; + } + + /** + * Reads the length code from the first byte of CBOR header. This can be the actual length if the header is of type + * {@link #SMALL} or {@link #getCode()} for all other types. + */ + public static int readLengthFromFirstHeaderByte(short firstHeaderByte) { + return firstHeaderByte & LENGTH_MASK; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMajorType.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMajorType.java new file mode 100644 index 000000000..66342b425 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMajorType.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.reactivesocket.mimetypes.internal.cbor; + +import java.util.HashMap; +import java.util.Map; + +/** + * A representation of all supported CBOR major types as defined in the spec. + */ +public enum CborMajorType { + + UnsignedInteger(0), + NegativeInteger(1), + ByteString(2), + Utf8String(3), + ARRAY(4), + MAP(5), + Break(7), + Unknown(-1); + + private final int typeCode; + private final static Map reverseIndex; + + public static final byte CBOR_BREAK = (byte) 0b111_11111; + + static { + reverseIndex = new HashMap<>(CborMajorType.values().length); + for (CborMajorType type : CborMajorType.values()) { + reverseIndex.put(type.typeCode, type); + } + } + + CborMajorType(int typeCode) { + this.typeCode = typeCode; + } + + public int getTypeCode() { + return typeCode; + } + + /** + * Reads the first byte of the CBOR type header ({@link CborHeader}) to determine which type is encoded in the + * header. + * + * @param unsignedByte First byte of the type header. + * + * @return The major type as encoded in the header. + */ + public static CborMajorType fromUnsignedByte(short unsignedByte) { + int type = unsignedByte >> 5 & 0x7; + CborMajorType t = reverseIndex.get(type); + if (null == t) { + return Unknown; + } + + if (t == Break) { + final int length = CborHeader.readLengthFromFirstHeaderByte(unsignedByte); + if (31 == length) { + return Break; + } + return Unknown; + } + + return t; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodec.java new file mode 100644 index 000000000..f7aa4fb96 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodec.java @@ -0,0 +1,107 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.MalformedInputException; +import org.agrona.DirectBuffer; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Map.Entry; + +import static io.reactivesocket.mimetypes.internal.cbor.CBORUtils.*; +import static io.reactivesocket.mimetypes.internal.cbor.CborMajorType.*; + +final class CborMapCodec { + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final MalformedInputException NOT_MAP = new MalformedInputException("Data is not a Map."); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final MalformedInputException VALUE_NOT_BINARY = + new MalformedInputException("Value for a map entry is not binary."); + + static { + NOT_MAP.setStackTrace(EMPTY_STACK); + VALUE_NOT_BINARY.setStackTrace(EMPTY_STACK); + } + + private CborMapCodec() { + } + + public static void encode(IndexedUnsafeBuffer dst, CBORMap cborMap) { + cborMap.encodeTo(dst); + } + + public static void encode(IndexedUnsafeBuffer dst, Map toEncode) { + final int size = toEncode.size(); + CborHeader.forLengthToEncode(size).encode(dst, MAP, size); + for (Entry entry : toEncode.entrySet()) { + CborUtf8StringCodec.encode(dst, entry.getKey()); + + CborBinaryStringCodec.encode(dst, entry.getValue()); + } + } + + public static CBORMap decode(IndexedUnsafeBuffer src, CborMapFactory mapFactory) { + long mapSize = parseDataLengthOrDie(src, MAP, NOT_MAP); + final CBORMap dst = mapFactory.newMap(src.getBackingBuffer(), src.getBackingBufferOffset(), + mapSize == CborHeader.INDEFINITE.getCode() ? 16 : (int) mapSize); + _decode(src, mapSize, dst); + return dst; + } + + public static void decode(IndexedUnsafeBuffer src, CBORMap dst) { + long mapSize = parseDataLengthOrDie(src, MAP, NOT_MAP); + _decode(src, mapSize, dst); + } + + private static void _decode(IndexedUnsafeBuffer src, long mapSize, CBORMap dst) { + boolean isIndefiniteMap = mapSize == CborHeader.INDEFINITE.getCode(); + int i = 0; + while (true) { + String key = CborUtf8StringCodec.decode(src, isIndefiniteMap); + if (null == key) { + break; + } + + int valLength = (int) parseDataLengthOrDie(src, ByteString, VALUE_NOT_BINARY); + int valueOffset = src.getReaderIndex(); + if (valLength < 0) { + throw VALUE_NOT_BINARY; + } + + if (valLength == CborHeader.INDEFINITE.getCode()) { + throw CborBinaryStringCodec.INDEFINITE_LENGTH_NOT_SUPPORTED; + } + dst.putValueOffset(key, valueOffset, valLength); + src.incrementReaderIndex(valLength); + + if (!isIndefiniteMap && ++i >= mapSize) { + break; + } + } + } + + public interface CborMapFactory { + + CBORMap newMap(DirectBuffer backingBuffer, int offset, int initialCapacity); + + } + +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodec.java new file mode 100644 index 000000000..403c8994f --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodec.java @@ -0,0 +1,89 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.MalformedInputException; + +import java.nio.charset.StandardCharsets; + +import static io.reactivesocket.mimetypes.internal.cbor.CBORUtils.*; +import static io.reactivesocket.mimetypes.internal.cbor.CborMajorType.*; + +final class CborUtf8StringCodec { + + @SuppressWarnings("ThrowableInstanceNeverThrown") + static final MalformedInputException NOT_UTF8_STRING = + new MalformedInputException("Data is not a definite length UTF-8 encoded string."); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + static final MalformedInputException BREAK_NOT_FOUND_FOR_INDEFINITE_LENGTH = + new MalformedInputException("End of string not found for indefinite length string."); + + static { + NOT_UTF8_STRING.setStackTrace(EMPTY_STACK); + BREAK_NOT_FOUND_FOR_INDEFINITE_LENGTH.setStackTrace(EMPTY_STACK); + } + + private CborUtf8StringCodec() { + } + + public static void encode(IndexedUnsafeBuffer dst, String utf8String) { + byte[] bytes = utf8String.getBytes(StandardCharsets.UTF_8); + encodeTypeHeader(dst, Utf8String, bytes.length); + dst.writeBytes(bytes, 0, bytes.length); + } + + public static String decode(IndexedUnsafeBuffer src) { + return decode(src, false); + } + + public static String decode(IndexedUnsafeBuffer src, boolean returnNullIfBreak) { + int length = (int) parseDataLengthOrDie(src, Utf8String, NOT_UTF8_STRING); + if (length < 0) { + if (returnNullIfBreak) { + return null; + } else { + throw NOT_UTF8_STRING; + } + } + + if (length == CborHeader.INDEFINITE.getCode()) { + String chunk = null; + while (true) { + byte aByte = src.getBackingBuffer().getByte(src.getReaderIndex()); + if (aByte == CBOR_BREAK) { + break; + } + + int chunkLength = (int) parseDataLengthOrDie(src, Utf8String, NOT_UTF8_STRING); + String thisChunk = readIntoString(src, chunkLength); + chunk = null == chunk ? thisChunk : chunk + thisChunk; + } + + return chunk; + } else { + return readIntoString(src, length); + } + } + + private static String readIntoString(IndexedUnsafeBuffer src, int chunkLength) { + byte[] keyBytes = new byte[chunkLength]; + src.readBytes(keyBytes, 0, keyBytes.length); + return new String(keyBytes, StandardCharsets.UTF_8); + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBuffer.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBuffer.java new file mode 100644 index 000000000..0f1b0a873 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBuffer.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.reactivesocket.mimetypes.internal.cbor; + +import org.agrona.BitUtil; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.function.Function; + +public class IndexedUnsafeBuffer { + + public static final byte[] EMPTY_ARRAY = new byte[0]; + + private int readerIndex; + private int writerIndex; + private int backingBufferOffset; + private final UnsafeBuffer delegate; + private final ByteOrder byteOrder; + + public IndexedUnsafeBuffer(ByteOrder byteOrder) { + this.byteOrder = byteOrder; + delegate = new UnsafeBuffer(EMPTY_ARRAY); + } + + public void wrap(ByteBuffer buffer) { + wrap(buffer, 0, buffer.capacity()); + } + + public void wrap(ByteBuffer buffer, int offset, int length) { + delegate.wrap(buffer, offset, length); + readerIndex = 0; + writerIndex = 0; + backingBufferOffset = offset; + } + + public void wrap(DirectBuffer buffer) { + wrap(buffer, 0, buffer.capacity()); + } + + public void wrap(DirectBuffer buffer, int offset, int length) { + delegate.wrap(buffer, offset, length); + readerIndex = 0; + writerIndex = 0; + backingBufferOffset = offset; + } + + public short readUnsignedByte() { + return (short) (delegate.getByte(readerIndex++) & 0xff); + } + + public byte readByte() { + return delegate.getByte(readerIndex++); + } + + public int readShort() { + short s = delegate.getShort(readerIndex, byteOrder); + readerIndex += BitUtil.SIZE_OF_SHORT; + return s; + } + + public int readInt() { + int i = delegate.getInt(readerIndex, byteOrder); + readerIndex += BitUtil.SIZE_OF_INT; + return i; + } + + public long readLong() { + long l = delegate.getLong(readerIndex, byteOrder); + readerIndex += BitUtil.SIZE_OF_LONG; + return l; + } + + public void readBytes(byte[] dst, int dstOffset, int length) { + delegate.getBytes(readerIndex, dst, dstOffset, length); + readerIndex += length - dstOffset; + } + + public void readBytes(MutableDirectBuffer dst, int offset, int length) { + delegate.getBytes(readerIndex, dst, offset, length); + readerIndex += length; + } + + public void readBytes(IndexedUnsafeBuffer dst, int length) { + delegate.getBytes(readerIndex, dst.getBackingBuffer(), dst.getWriterIndex(), length); + readerIndex += length; + } + + public void writeByte(byte toWrite) { + delegate.putByte(writerIndex++, toWrite); + } + + public void writeShort(short toWrite) { + delegate.putShort(writerIndex, toWrite, byteOrder); + writerIndex += BitUtil.SIZE_OF_SHORT; + } + + public void writeInt(int toWrite) { + delegate.putInt(writerIndex, toWrite, byteOrder); + writerIndex += BitUtil.SIZE_OF_INT; + } + + public void writeLong(long toWrite) { + delegate.putLong(writerIndex, toWrite, byteOrder); + writerIndex += BitUtil.SIZE_OF_LONG; + } + + public void writeBytes(byte[] src, int offset, int length) { + delegate.putBytes(writerIndex, src, offset, length); + writerIndex += length; + } + + public void writeBytes(ByteBuffer src, int length) { + delegate.putBytes(writerIndex, src, src.position(), length); + writerIndex += length; + } + + public void writeBytes(DirectBuffer src, int offset, int length) { + delegate.putBytes(writerIndex, src, offset, length); + writerIndex += length; + } + + public int getReaderIndex() { + return readerIndex; + } + + public int getWriterIndex() { + return writerIndex; + } + + public UnsafeBuffer getBackingBuffer() { + return delegate; + } + + public int getBackingBufferOffset() { + return backingBufferOffset; + } + + public void incrementReaderIndex(int increment) { + readerIndex += increment; + } + + public void setReaderIndex(int readerIndex) { + if (readerIndex >= delegate.capacity()) { + throw new IllegalArgumentException( + String.format("Reader Index should be less than capacity. Reader Index: %d, Capacity: %d", + readerIndex, delegate.capacity())); + } + this.readerIndex = readerIndex; + } + + public void setWriterIndex(int writerIndex) { + if (writerIndex >= delegate.capacity()) { + throw new IllegalArgumentException( + String.format("Writer Index should be less than capacity. Writer Index: %d, Capacity: %d", + writerIndex, delegate.capacity())); + } + this.writerIndex = writerIndex; + } + + public void incrementWriterIndex(int increment) { + writerIndex += increment; + } + + /** + * Scans this buffer and invokes the passed {@code scanner} for every byte. The scan stops if it has reached the end + * of buffer or the {@code scanner} returns {@code false}. This method does not move the {@code readerIndex} for + * this buffer. + * + * @param scanner Scanner that determines to scan further for every byte. + * + * @return Index in the buffer at which this scan stopped. + */ + public int forEachByte(Function scanner) { + int i; + for (i = readerIndex; i < delegate.capacity(); i++) { + if (!scanner.apply(delegate.getByte(i))) { + break; + } + } + return i; + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodec.java new file mode 100644 index 000000000..1e4ae7405 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodec.java @@ -0,0 +1,158 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.KVMetadata; +import io.reactivesocket.mimetypes.internal.Codec; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map.Entry; + +import static io.reactivesocket.mimetypes.internal.cbor.CBORUtils.*; + +/** + * This is a custom codec for default {@link KVMetadata} as defined by + * the spec. Since, the format + * is simple, it does not use a third-party library to do the encoding, but the logic is contained in this class. + */ +public class MetadataCodec implements Codec { + + public static final MetadataCodec INSTANCE = new MetadataCodec(); + + private static final ThreadLocal indexedBuffers = + ThreadLocal.withInitial(() -> new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN)); + + protected MetadataCodec() { + } + + @Override + public T decode(ByteBuffer buffer, Class tClass) { + isValidType(tClass); + + IndexedUnsafeBuffer tmp = indexedBuffers.get(); + tmp.wrap(buffer); + + return _decode(tmp); + } + + @Override + public T decode(DirectBuffer buffer, int offset, Class tClass) { + isValidType(tClass); + + IndexedUnsafeBuffer tmp = indexedBuffers.get(); + tmp.wrap(buffer); + + return _decode(tmp); + } + + @Override + public ByteBuffer encode(T toEncode) { + isValidType(toEncode.getClass()); + + if (toEncode instanceof SlicedBufferKVMetadata) { + return ((SlicedBufferKVMetadata) toEncode).getBackingBuffer().byteBuffer(); + } + + ByteBuffer dst = ByteBuffer.allocate(getSizeAsBytes((KVMetadata) toEncode)); + encodeTo(dst, toEncode); + return dst; + } + + @Override + public DirectBuffer encodeDirect(T toEncode) { + isValidType(toEncode.getClass()); + + final KVMetadata input = (KVMetadata) toEncode; + + if (toEncode instanceof SlicedBufferKVMetadata) { + return ((SlicedBufferKVMetadata) toEncode).getBackingBuffer(); + } + + MutableDirectBuffer toReturn = new UnsafeBuffer(ByteBuffer.allocate(getSizeAsBytes(input))); + encodeTo(toReturn, toEncode, 0); + return toReturn; + } + + @Override + public void encodeTo(ByteBuffer buffer, T toEncode) { + isValidType(toEncode.getClass()); + + final KVMetadata input = (KVMetadata) toEncode; + + IndexedUnsafeBuffer tmp = indexedBuffers.get(); + tmp.wrap(buffer); + + _encodeTo(tmp, input); + } + + @Override + public void encodeTo(MutableDirectBuffer buffer, T toEncode, int offset) { + isValidType(toEncode.getClass()); + + final KVMetadata input = (KVMetadata) toEncode; + + IndexedUnsafeBuffer tmp = indexedBuffers.get(); + tmp.wrap(buffer, offset, buffer.capacity()); + + _encodeTo(tmp, input); + } + + private static void _encodeTo(IndexedUnsafeBuffer buffer, KVMetadata toEncode) { + if (toEncode instanceof SlicedBufferKVMetadata) { + SlicedBufferKVMetadata s = (SlicedBufferKVMetadata) toEncode; + DirectBuffer backingBuffer = s.getBackingBuffer(); + backingBuffer.getBytes(0, buffer.getBackingBuffer(), buffer.getWriterIndex(), backingBuffer.capacity()); + return; + } + + CborMapCodec.encode(buffer, toEncode); + } + + private static T _decode(IndexedUnsafeBuffer src) { + CBORMap m = CborMapCodec.decode(src, + (backingBuffer, offset, initialCapacity) -> new SlicedBufferKVMetadata( + backingBuffer, offset, initialCapacity)); + @SuppressWarnings("unchecked") + T t = (T) m; + return t; + } + + private static int getSizeAsBytes(KVMetadata toEncode) { + int toReturn = 1 + (int) getEncodeLength(toEncode.size()); // Map Starting + break + for (Entry entry : toEncode.entrySet()) { + toReturn += getEncodeLength(entry.getKey().length()); + toReturn += entry.getKey().length(); + + int valueLength = entry.getValue().remaining(); + toReturn += getEncodeLength(valueLength); + toReturn += valueLength; + } + return toReturn; + } + + private static void isValidType(Class tClass) { + if (!KVMetadata.class.isAssignableFrom(tClass)) { + throw new IllegalArgumentException("Metadata codec only supports encoding/decoding of: " + + KVMetadata.class.getName()); + } + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/ReactiveSocketDefaultMetadataCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/ReactiveSocketDefaultMetadataCodec.java new file mode 100644 index 000000000..8d15c1cf0 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/ReactiveSocketDefaultMetadataCodec.java @@ -0,0 +1,104 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.KVMetadata; +import io.reactivesocket.mimetypes.internal.Codec; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; + +public class ReactiveSocketDefaultMetadataCodec implements Codec { + + private final CborCodec cborCodec; + + private ReactiveSocketDefaultMetadataCodec(CborCodec cborCodec) { + this.cborCodec = cborCodec; + } + + public KVMetadata decodeDefault(ByteBuffer buffer) { + return MetadataCodec.INSTANCE.decode(buffer, SlicedBufferKVMetadata.class); + } + + public KVMetadata decodeDefault(DirectBuffer buffer, int offset) { + return MetadataCodec.INSTANCE.decode(buffer, offset, SlicedBufferKVMetadata.class); + } + + @Override + public T decode(ByteBuffer buffer, Class tClass) { + if (KVMetadata.class.isAssignableFrom(tClass)) { + @SuppressWarnings("unchecked") + T t = (T) decodeDefault(buffer); + return t; + } + return cborCodec.decode(buffer, tClass); + } + + @Override + public T decode(DirectBuffer buffer, int offset, Class tClass) { + if (KVMetadata.class.isAssignableFrom(tClass)) { + @SuppressWarnings("unchecked") + T t = (T) decodeDefault(buffer, offset); + return t; + } + return cborCodec.decode(buffer, offset, tClass); + } + + @Override + public ByteBuffer encode(T toEncode) { + if (KVMetadata.class.isAssignableFrom(toEncode.getClass())) { + return MetadataCodec.INSTANCE.encode(toEncode); + } + return cborCodec.encode(toEncode); + } + + @Override + public DirectBuffer encodeDirect(T toEncode) { + if (KVMetadata.class.isAssignableFrom(toEncode.getClass())) { + return MetadataCodec.INSTANCE.encodeDirect(toEncode); + } + return cborCodec.encodeDirect(toEncode); + } + + @Override + public void encodeTo(ByteBuffer buffer, T toEncode) { + if (KVMetadata.class.isAssignableFrom(toEncode.getClass())) { + MetadataCodec.INSTANCE.encodeTo(buffer, toEncode); + } else { + cborCodec.encodeTo(buffer, toEncode); + } + } + + @Override + public void encodeTo(MutableDirectBuffer buffer, T toEncode, int offset) { + if (KVMetadata.class.isAssignableFrom(toEncode.getClass())) { + MetadataCodec.INSTANCE.encodeTo(buffer, toEncode, offset); + } else { + cborCodec.encodeTo(buffer, toEncode, offset); + } + } + + public static ReactiveSocketDefaultMetadataCodec create() { + return new ReactiveSocketDefaultMetadataCodec(CborCodec.create()); + } + + public static ReactiveSocketDefaultMetadataCodec create(CborCodec cborCodec) { + return new ReactiveSocketDefaultMetadataCodec(cborCodec); + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadata.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadata.java new file mode 100644 index 000000000..c6ab714d3 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadata.java @@ -0,0 +1,84 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.KVMetadata; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * An implementation of {@link KVMetadata} that does not allocate buffers for values of metadata, instead it keeps a + * view of the original buffer with offsets to the values. See {@link CBORMap} to learn more about the structure and + * cases when this creates allocations. + * + *

Lifecycle

+ * + * This assumes exclusive access to the underlying buffer. If that is not the case, then {@link #duplicate(Function)} + * must be used to create a copy of the underlying buffer. + * + * @see CBORMap + */ +public class SlicedBufferKVMetadata extends CBORMap implements KVMetadata { + + public SlicedBufferKVMetadata(DirectBuffer backingBuffer, int offset, int initialCapacity) { + super(backingBuffer, offset, initialCapacity); + } + + public SlicedBufferKVMetadata(DirectBuffer backingBuffer, int offset) { + super(backingBuffer, offset); + } + + protected SlicedBufferKVMetadata(Map storeWhenModified) { + super(storeWhenModified); + } + + protected SlicedBufferKVMetadata(DirectBuffer backingBuffer, int offset, Map keysVsOffsets) { + super(backingBuffer, offset, keysVsOffsets); + } + + @Override + public String getAsString(String key, Charset valueEncoding) { + ByteBuffer v = get(key); + byte[] vBytes; + if (v.hasArray()) { + return new String(v.array(), v.arrayOffset(), v.limit(), valueEncoding); + } else { + vBytes = new byte[v.remaining()]; + v.get(vBytes); + return new String(vBytes, valueEncoding); + } + } + + @Override + public KVMetadata duplicate(Function newBufferFactory) { + if (null == storeWhenModified) { + int newCap = backingBuffer.capacity(); + MutableDirectBuffer newBuffer = newBufferFactory.apply(newCap); + backingBuffer.getBytes(0, newBuffer, 0, newCap); + return new SlicedBufferKVMetadata(newBuffer, 0, new HashMap<>(keysVsOffsets)); + } else { + return new SlicedBufferKVMetadata(storeWhenModified); + } + } +} diff --git a/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/json/JsonCodec.java b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/json/JsonCodec.java new file mode 100644 index 000000000..2dd535d67 --- /dev/null +++ b/reactivesocket-mime-types/src/main/java/io/reactivesocket/mimetypes/internal/json/JsonCodec.java @@ -0,0 +1,32 @@ +package io.reactivesocket.mimetypes.internal.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.reactivesocket.mimetypes.internal.AbstractJacksonCodec; + +public class JsonCodec extends AbstractJacksonCodec { + + private JsonCodec(ObjectMapper mapper) { + super(mapper); + } + + /** + * Creates a {@link JsonCodec} with default configurations. Use {@link #create(ObjectMapper)} for custom mapper + * configurations. + * + * @return A new instance of {@link JsonCodec} with default mapper configurations. + */ + public static JsonCodec create() { + ObjectMapper mapper = new ObjectMapper(); + configureDefaults(mapper); + return create(mapper); + } + + /** + * Creates a {@link JsonCodec} with custom mapper. Use {@link #create()} for default mapper configurations. + * + * @return A new instance of {@link JsonCodec} with the passed mapper. + */ + public static JsonCodec create(ObjectMapper mapper) { + return new JsonCodec(mapper); + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/MimeTypeFactoryTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/MimeTypeFactoryTest.java new file mode 100644 index 000000000..19771642d --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/MimeTypeFactoryTest.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.reactivesocket.mimetypes; + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.mimetypes.internal.Codec; +import io.reactivesocket.mimetypes.internal.CustomObject; +import io.reactivesocket.mimetypes.internal.CustomObjectRule; +import io.reactivesocket.mimetypes.internal.KVMetadataImpl; +import io.reactivesocket.mimetypes.internal.MetadataRule; +import io.reactivesocket.mimetypes.internal.cbor.CborCodec; +import io.reactivesocket.mimetypes.internal.cbor.ReactiveSocketDefaultMetadataCodec; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static io.reactivesocket.mimetypes.SupportedMimeTypes.*; +import static io.reactivesocket.mimetypes.internal.cbor.ByteBufferMapMatcher.*; + +public class MimeTypeFactoryTest { + + @Rule + public final CustomObjectRule objectRule = new CustomObjectRule(); + @Rule + public final MetadataRule metadataRule = new MetadataRule(); + + @Test(timeout = 60000) + public void testFromSetup() throws Exception { + objectRule.populateDefaultData(); + metadataRule.populateDefaultMetadataData(); + + MimeType mimeType = getMimeTypeFromSetup(ReactiveSocketDefaultMetadata, CBOR); + + testMetadataCodec(mimeType, ReactiveSocketDefaultMetadataCodec.create()); + testDataCodec(mimeType, CborCodec.create()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnsupportedMimetype() throws Exception { + ConnectionSetupPayload setup = new ConnectionSetupPayloadImpl("blah", "blah"); + MimeTypeFactory.from(setup); + } + + @Test(timeout = 60000) + public void testOneMimetype() throws Exception { + objectRule.populateDefaultData(); + metadataRule.populateDefaultMetadataData(); + + MimeType mimeType = MimeTypeFactory.from(CBOR); + + testMetadataCodec(mimeType, CborCodec.create()); + testDataCodec(mimeType, CborCodec.create()); + } + + @Test(timeout = 60000) + public void testDifferentMimetypes() throws Exception { + objectRule.populateDefaultData(); + metadataRule.populateDefaultMetadataData(); + + MimeType mimeType = MimeTypeFactory.from(ReactiveSocketDefaultMetadata, CBOR); + + testMetadataCodec(mimeType, ReactiveSocketDefaultMetadataCodec.create()); + testDataCodec(mimeType, ReactiveSocketDefaultMetadataCodec.create()); + } + + private void testMetadataCodec(MimeType mimeType, Codec expectedCodec) { + ByteBuffer encode = mimeType.encodeMetadata(metadataRule.getKvMetadata()); + ByteBuffer encode1 = expectedCodec.encode(metadataRule.getKvMetadata()); + + MatcherAssert.assertThat("Unexpected encode from mime type.", encode, Matchers.equalTo(encode1)); + MatcherAssert.assertThat("Unexpected decode from encode.", metadataRule.getKvMetadata(), + mapEqualTo(mimeType.decodeMetadata(encode, KVMetadataImpl.class))); + + + DirectBuffer dencode = mimeType.encodeMetadataDirect(metadataRule.getKvMetadata()); + DirectBuffer dencode1 = expectedCodec.encodeDirect(metadataRule.getKvMetadata()); + + MatcherAssert.assertThat("Unexpected direct buffer encode from mime type.", dencode, Matchers.equalTo(dencode1)); + MatcherAssert.assertThat("Unexpected decode from direct encode.", metadataRule.getKvMetadata(), + mapEqualTo(mimeType.decodeMetadata(dencode, KVMetadataImpl.class, 0))); + + ByteBuffer dst = ByteBuffer.allocate(100); + ByteBuffer dst1 = ByteBuffer.allocate(100); + + mimeType.encodeMetadataTo(dst, metadataRule.getKvMetadata()); + dst.flip(); + expectedCodec.encodeTo(dst1, metadataRule.getKvMetadata()); + dst1.flip(); + + MatcherAssert.assertThat("Unexpected encodeTo buffer encode from mime type.", dst, Matchers.equalTo(dst1)); + MatcherAssert.assertThat("Unexpected decode from encodeTo encode.", metadataRule.getKvMetadata(), + mapEqualTo(mimeType.decodeMetadata(dst, KVMetadataImpl.class))); + + MutableDirectBuffer mdst = new UnsafeBuffer(new byte[100]); + MutableDirectBuffer mdst1 = new UnsafeBuffer(new byte[100]); + + mimeType.encodeMetadataTo(mdst, metadataRule.getKvMetadata(), 0); + expectedCodec.encodeTo(mdst1, metadataRule.getKvMetadata(), 0); + + MatcherAssert.assertThat("Unexpected encodeTo buffer encode from mime type.", mdst, Matchers.equalTo(mdst1)); + MatcherAssert.assertThat("Unexpected decode from encodeTo encode.", metadataRule.getKvMetadata(), + mapEqualTo(mimeType.decodeMetadata(mdst, KVMetadataImpl.class, 0))); + } + + private void testDataCodec(MimeType mimeType, Codec expectedCodec) { + ByteBuffer encode = mimeType.encodeData(objectRule.getData()); + ByteBuffer encode1 = expectedCodec.encode(objectRule.getData()); + + MatcherAssert.assertThat("Unexpected encode from mime type.", encode, Matchers.equalTo(encode1)); + MatcherAssert.assertThat("Unexpected decode from encode.", objectRule.getData(), + Matchers.equalTo(mimeType.decodeData(encode, CustomObject.class))); + + + DirectBuffer dencode = mimeType.encodeDataDirect(objectRule.getData()); + DirectBuffer dencode1 = expectedCodec.encodeDirect(objectRule.getData()); + + MatcherAssert.assertThat("Unexpected direct buffer encode from mime type.", dencode, Matchers.equalTo(dencode1)); + MatcherAssert.assertThat("Unexpected decode from direct encode.", objectRule.getData(), + Matchers.equalTo(mimeType.decodeData(dencode, CustomObject.class, 0))); + + ByteBuffer dst = ByteBuffer.allocate(100); + ByteBuffer dst1 = ByteBuffer.allocate(100); + + mimeType.encodeDataTo(dst, objectRule.getData()); + dst.flip(); + expectedCodec.encodeTo(dst1, objectRule.getData()); + dst1.flip(); + + MatcherAssert.assertThat("Unexpected encodeTo buffer encode from mime type.", dst, Matchers.equalTo(dst1)); + MatcherAssert.assertThat("Unexpected decode from encodeTo encode.", objectRule.getData(), + Matchers.equalTo(mimeType.decodeData(dst, CustomObject.class))); + + MutableDirectBuffer mdst = new UnsafeBuffer(new byte[100]); + MutableDirectBuffer mdst1 = new UnsafeBuffer(new byte[100]); + + mimeType.encodeDataTo(mdst, objectRule.getData(), 0); + expectedCodec.encodeTo(mdst1, objectRule.getData(), 0); + + MatcherAssert.assertThat("Unexpected encodeTo buffer encode from mime type.", mdst, Matchers.equalTo(mdst1)); + MatcherAssert.assertThat("Unexpected decode from encodeTo encode.", objectRule.getData(), + Matchers.equalTo(mimeType.decodeData(mdst, CustomObject.class, 0))); + } + + private static MimeType getMimeTypeFromSetup(SupportedMimeTypes metaMime, SupportedMimeTypes dataMime) { + ConnectionSetupPayload setup = new ConnectionSetupPayloadImpl(dataMime.getMimeTypes().get(0), + metaMime.getMimeTypes().get(0)); + + return MimeTypeFactory.from(setup); + } + + private static class ConnectionSetupPayloadImpl extends ConnectionSetupPayload { + + private final String dataMime; + private final String metadataMime; + + private ConnectionSetupPayloadImpl(String dataMime, String metadataMime) { + this.dataMime = dataMime; + this.metadataMime = metadataMime; + } + + @Override + public String metadataMimeType() { + return metadataMime; + } + + @Override + public String dataMimeType() { + return dataMime; + } + + @Override + public ByteBuffer getData() { + return ByteBuffer.allocate(0); + } + + @Override + public ByteBuffer getMetadata() { + return ByteBuffer.allocate(0); + } + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodecTest.java new file mode 100644 index 000000000..c1dd271e7 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/AbstractJacksonCodecTest.java @@ -0,0 +1,81 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; + +import java.nio.ByteBuffer; + +public abstract class AbstractJacksonCodecTest { + + @Rule + public final CustomObjectRule customObjectRule = new CustomObjectRule(); + @Rule + public final MetadataRule metadataRule = new MetadataRule(); + + @Test(timeout = 60000) + public void encodeDecode() throws Exception { + customObjectRule.populateDefaultData(); + ByteBuffer encode = getCodecRule().getCodec().encode(customObjectRule.getData()); + + CustomObject decode = getCodecRule().getCodec().decode(encode, CustomObject.class); + MatcherAssert.assertThat("Unexpected decode.", decode, Matchers.equalTo(customObjectRule.getData())); + } + + @Test(timeout = 60000) + public void encodeDecodeDirect() throws Exception { + customObjectRule.populateDefaultData(); + DirectBuffer encode = getCodecRule().getCodec().encodeDirect(customObjectRule.getData()); + + CustomObject decode = getCodecRule().getCodec().decode(encode, 0, CustomObject.class); + MatcherAssert.assertThat("Unexpected decode.", decode, Matchers.equalTo(customObjectRule.getData())); + } + + @Test(timeout = 60000) + public void encodeTo() throws Exception { + customObjectRule.populateDefaultData(); + ByteBuffer encodeDest = ByteBuffer.allocate(10000); + getCodecRule().getCodec().encodeTo(encodeDest, customObjectRule.getData()); + + encodeDest.flip(); /*Since we want to decode it now*/ + + CustomObject decode = getCodecRule().getCodec().decode(encodeDest, CustomObject.class); + + MatcherAssert.assertThat("Unexpected decode.", decode, Matchers.equalTo(customObjectRule.getData())); + } + + @Test(timeout = 60000) + public void encodeToDirect() throws Exception { + customObjectRule.populateDefaultData(); + byte[] destArr = new byte[1000]; + MutableDirectBuffer encodeDest = new UnsafeBuffer(destArr); + getCodecRule().getCodec().encodeTo(encodeDest, customObjectRule.getData(), 0); + + CustomObject decode = getCodecRule().getCodec().decode(encodeDest, 0, CustomObject.class); + + MatcherAssert.assertThat("Unexpected decode.", decode, Matchers.equalTo(customObjectRule.getData())); + } + + protected abstract CodecRule getCodecRule(); +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CodecRule.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CodecRule.java new file mode 100644 index 000000000..4a15b8c4d --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CodecRule.java @@ -0,0 +1,48 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import rx.functions.Func0; + +public class CodecRule extends ExternalResource { + + private T codec; + private final Func0 codecFactory; + + public CodecRule(Func0 codecFactory) { + this.codecFactory = codecFactory; + } + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + codec = codecFactory.call(); + base.evaluate(); + } + }; + } + + public T getCodec() { + return codec; + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObject.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObject.java new file mode 100644 index 000000000..82a7f69ba --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObject.java @@ -0,0 +1,92 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import java.util.Map; + +public class CustomObject { + + private String name; + private int age; + private Map attributes; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + String sb = "CustomObject{" + "name='" + name + '\'' + + ", age=" + age + + ", attributes=" + attributes + + '}'; + return sb; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomObject)) { + return false; + } + + CustomObject that = (CustomObject) o; + + if (age != that.age) { + return false; + } + if (name != null? !name.equals(that.name) : that.name != null) { + return false; + } + if (attributes != null? !attributes.equals(that.attributes) : that.attributes != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = name != null? name.hashCode() : 0; + result = 31 * result + age; + result = 31 * result + (attributes != null? attributes.hashCode() : 0); + return result; + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObjectRule.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObjectRule.java new file mode 100644 index 000000000..9e002d8ac --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/CustomObjectRule.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.reactivesocket.mimetypes.internal; + +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.HashMap; +import java.util.Map; + +public class CustomObjectRule extends ExternalResource { + + private CustomObject data; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + data = new CustomObject(); + base.evaluate(); + } + }; + } + public CustomObject getData() { + return data; + } + + public void populateDefaultData() { + data.setAge(100); + data.setName("Dummy"); + Map attribs = new HashMap<>(); + attribs.put("1K", 1); + attribs.put("2K", 2); + data.setAttributes(attribs); + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/KVMetadataImplTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/KVMetadataImplTest.java new file mode 100644 index 000000000..b3e4836b9 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/KVMetadataImplTest.java @@ -0,0 +1,66 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import org.hamcrest.MatcherAssert; +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.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.Matchers.*; + +public class KVMetadataImplTest { + + @Rule + public final MetadataRule metadataRule = new MetadataRule(); + + @Test(timeout = 60000) + public void testGetAsString() throws Exception { + String key = "Key1"; + String value = "Value1"; + metadataRule.addMetadata(key, value); + + String lookup = metadataRule.getKvMetadata().getAsString(key, StandardCharsets.UTF_8); + + MatcherAssert.assertThat("Unexpected lookup value.", lookup, equalTo(value)); + } + + @Test(timeout = 60000) + public void testGetAsEmptyString() throws Exception { + String key = "Key1"; + String value = ""; + metadataRule.addMetadata(key, value); + + String lookup = metadataRule.getKvMetadata().getAsString(key, StandardCharsets.UTF_8); + + MatcherAssert.assertThat("Unexpected lookup value.", lookup, equalTo(value)); + } + + @Test(timeout = 60000) + public void testGetAsStringInvalid() throws Exception { + + String lookup = metadataRule.getKvMetadata().getAsString("Key", StandardCharsets.UTF_8); + + MatcherAssert.assertThat("Unexpected lookup value.", lookup, nullValue()); + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/MetadataRule.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/MetadataRule.java new file mode 100644 index 000000000..ffa4bb484 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/MetadataRule.java @@ -0,0 +1,62 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; + +public class MetadataRule extends ExternalResource { + + private KVMetadataImpl kvMetadata; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + kvMetadata = new KVMetadataImpl(new HashMap<>()); + base.evaluate(); + } + }; + } + + public void populateDefaultMetadataData() { + addMetadata("Hello1", "HelloVal1"); + addMetadata("Hello2", "HelloVal2"); + } + + public void addMetadata(String key, String value) { + ByteBuffer allocate = ByteBuffer.allocate(value.length()); + allocate.put(value.getBytes(StandardCharsets.UTF_8)).flip(); + kvMetadata.put(key, allocate); + } + + public void addMetadata(String key, ByteBuffer value) { + kvMetadata.put(key, value); + } + + public KVMetadataImpl getKvMetadata() { + return kvMetadata; + } + +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/ReactiveSocketDefaultMetadataCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/ReactiveSocketDefaultMetadataCodecTest.java new file mode 100644 index 000000000..25bf65383 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/ReactiveSocketDefaultMetadataCodecTest.java @@ -0,0 +1,61 @@ +/* + * 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.reactivesocket.mimetypes.internal; + +import io.reactivesocket.mimetypes.KVMetadata; +import io.reactivesocket.mimetypes.internal.cbor.ReactiveSocketDefaultMetadataCodec; +import org.agrona.DirectBuffer; +import org.hamcrest.MatcherAssert; +import org.junit.Rule; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static io.reactivesocket.mimetypes.internal.cbor.ByteBufferMapMatcher.*; + +public class ReactiveSocketDefaultMetadataCodecTest { + + @Rule + public final CodecRule codecRule = + new CodecRule<>(ReactiveSocketDefaultMetadataCodec::create); + @Rule + public final MetadataRule metadataRule = new MetadataRule(); + + @Test(timeout = 60000) + public void testDecodeDefault() throws Exception { + + metadataRule.populateDefaultMetadataData(); + + ByteBuffer encode = codecRule.getCodec().encode(metadataRule.getKvMetadata()); + KVMetadata kvMetadata = codecRule.getCodec().decodeDefault(encode); + + MatcherAssert.assertThat("Unexpected decoded metadata.", kvMetadata, mapEqualTo(metadataRule.getKvMetadata())); + } + + @Test(timeout = 60000) + public void testDecodeDefaultDirect() throws Exception { + + metadataRule.populateDefaultMetadataData(); + + DirectBuffer encode = codecRule.getCodec().encodeDirect(metadataRule.getKvMetadata()); + KVMetadata kvMetadata = codecRule.getCodec().decodeDefault(encode, 0); + + MatcherAssert.assertThat("Unexpected decoded metadata.", kvMetadata, mapEqualTo(metadataRule.getKvMetadata())); + } + +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/AbstractCborMapRule.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/AbstractCborMapRule.java new file mode 100644 index 000000000..69bc95b5a --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/AbstractCborMapRule.java @@ -0,0 +1,87 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public abstract class AbstractCborMapRule extends ExternalResource { + + protected T map; + protected ByteBuffer valueBuffer; + protected IndexedUnsafeBuffer indexed; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + init(); + base.evaluate(); + } + }; + } + + protected abstract void init(); + + public void addMockEntries(int count) { + for (int i =0; i < count; i++) { + addEntry(getMockKey(i), getMockValue(i)); + } + } + + public void addEntry(String utf8Key, String value) { + ByteBuffer vBuf = toValueBuffer(value); + int valueLength = vBuf.remaining(); + int offset = indexed.getWriterIndex(); + indexed.writeBytes(vBuf, valueLength); + map.putValueOffset(utf8Key, offset, valueLength); + } + + public String getMockKey(int index) { + return "Key" + index; + } + + public String getMockValue(int index) { + return "Value" + (index + 10); + } + + public ByteBuffer getMockValueAsBuffer(int index) { + String mockValue = getMockValue(index); + return toValueBuffer(mockValue); + } + + public ByteBuffer toValueBuffer(String value) { + byte[] vBytes = value.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.wrap(vBytes); + } + + public void assertValueForKey(int index) { + String k = getMockKey(index); + ByteBuffer vBuf = map.get(k); + + assertThat("Unexpected lookup value for key: " + k, vBuf, equalTo(getMockValueAsBuffer(index))); + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/ByteBufferMapMatcher.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/ByteBufferMapMatcher.java new file mode 100644 index 000000000..967992b02 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/ByteBufferMapMatcher.java @@ -0,0 +1,61 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.mockito.ArgumentMatcher; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Map.Entry; + +public final class ByteBufferMapMatcher { + + private ByteBufferMapMatcher() { + } + + public static Matcher> mapEqualTo(Map toCheck) { + return new ArgumentMatcher>() { + + @Override + public boolean matches(Object argument) { + if (argument instanceof Map) { + @SuppressWarnings("unchecked") + Map arg = (Map) argument; + if (arg.size() == toCheck.size()) { + for (Entry e : arg.entrySet()) { + ByteBuffer v = toCheck.get(e.getKey()); + v.rewind(); + if (null == v || !e.getValue().equals(v)) { + return false; + } + } + return true; + } + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Map Equals " + toCheck); + } + }; + } +} diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapTest.java new file mode 100644 index 000000000..5aef94e37 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapTest.java @@ -0,0 +1,168 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.agrona.concurrent.UnsafeBuffer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CBORMapTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + {100, 0, 100, 5}, {400, 20, 200, 20}, {400, 20, 140, 20} + }); + } + + @Parameter + public int bufferSize; + @Parameter(1) + public int bufferOffset; + @Parameter(2) + public int bufferLength; + @Parameter(3) + public int entrySize; + + @Rule + public final CborMapRule mapRule = new CborMapRule(); + + @Test(timeout = 60000) + public void testGet() throws Exception { + mapRule.addMockEntries(entrySize); + for (int i = 0; i < entrySize; i++) { + mapRule.assertValueForKey(i); + } + } + + @Test(timeout = 60000) + public void testGetWithArrayWrappedBuffer() throws Exception { + mapRule.initWithArray(); + mapRule.addMockEntries(entrySize); + for (int i = 0; i < entrySize; i++) { + mapRule.assertValueForKey(i); + } + } + + @Test(timeout = 60000) + public void testContainsKey() throws Exception { + mapRule.addMockEntries(entrySize); + for (int i = 0; i < entrySize; i++) { + String k = mapRule.getMockKey(i); + assertThat("Key: " + k + " not found.", mapRule.map.containsKey(k), is(true)); + } + } + + @Test(timeout = 60000) + public void testNonExistentContainsKey() throws Exception { + mapRule.addMockEntries(entrySize); + String k = "dummy"; + assertThat("Key: " + k + " not found.", mapRule.map.containsKey(k), is(false)); + } + + @Test(timeout = 60000) + public void testSize() throws Exception { + mapRule.addMockEntries(entrySize); + assertThat("Unexpected size.", mapRule.map.size(), is(entrySize)); + } + + @Test(timeout = 60000) + public void testIsEmpty() throws Exception { + mapRule.addMockEntries(entrySize); + assertThat("isEmpty?.", mapRule.map.isEmpty(), is(false)); + } + + @Test(timeout = 60000) + public void testIsEmptyWithEmpty() throws Exception { + assertThat("isEmpty?.", mapRule.map.isEmpty(), is(true)); + } + + @Test(timeout = 60000) + public void testContainsValue() throws Exception { + mapRule.addMockEntries(entrySize); + for (int i = 0; i < entrySize; i++) { + String v = mapRule.getMockValue(i); + ByteBuffer vBuf = mapRule.getMockValueAsBuffer(i); + assertThat("Value: " + v + " not found.", mapRule.map.containsValue(vBuf), is(true)); + } + } + + @Test(timeout = 60000) + public void testNonExistentValue() throws Exception { + mapRule.addMockEntries(entrySize); + ByteBuffer vBuf = mapRule.toValueBuffer("dummy"); + assertThat("Unexpected value found.", mapRule.map.containsValue(vBuf), is(false)); + } + + @Test(timeout = 60000) + public void testPut() throws Exception { + mapRule.addMockEntries(entrySize); + String addnKey1 = "AddnKey1"; + ByteBuffer addnValue1 = mapRule.toValueBuffer("AddnValue1"); + mapRule.map.put(addnKey1, addnValue1); + for (int i = 0; i < entrySize; i++) { + mapRule.assertValueForKey(i); + } + ByteBuffer vBuf = mapRule.map.get(addnKey1); + + assertThat("Unexpected lookup value for key: " + addnKey1, vBuf, equalTo(addnValue1)); + } + + @Test(timeout = 60000) + public void testRemove() throws Exception { + mapRule.addMockEntries(entrySize); + int indexToRemove = 0 == entrySize? 0 : entrySize - 1; + String keyToRemove = mapRule.getMockKey(indexToRemove); + ByteBuffer removed = mapRule.map.remove(keyToRemove); + + assertThat("Unexpected value removed", removed, equalTo(mapRule.getMockValueAsBuffer(indexToRemove))); + assertThat("Value not removed from map.", mapRule.map.get(keyToRemove), is(nullValue())); + } + + public class CborMapRule extends AbstractCborMapRule { + + @Override + protected void init() { + valueBuffer = ByteBuffer.allocate(bufferSize); + indexed = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + indexed.wrap(valueBuffer, bufferOffset, bufferLength); + map = new CBORMap(indexed.getBackingBuffer(), bufferOffset); + } + + protected void initWithArray() { + byte[] src = new byte[bufferSize]; + UnsafeBuffer unsafeBuffer = new UnsafeBuffer(src, bufferOffset, bufferLength); + indexed = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + indexed.wrap(unsafeBuffer); + map = new CBORMap(indexed.getBackingBuffer(), bufferOffset); + } + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapValueMaskTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapValueMaskTest.java new file mode 100644 index 000000000..daf65c162 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORMapValueMaskTest.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.reactivesocket.mimetypes.internal.cbor; + +import org.hamcrest.MatcherAssert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CBORMapValueMaskTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + {0, 0}, {20, 6}, {100, Integer.MAX_VALUE} + }); + } + + @Parameter + public int offset; + @Parameter(1) + public int length; + + @Test(timeout = 60000) + public void testMask() throws Exception { + long mask = CBORMap.encodeValueMask(offset, length); + int offset = CBORMap.decodeOffsetFromMask(mask); + int length = CBORMap.decodeLengthFromMask(mask); + + MatcherAssert.assertThat("Unexpected offset post decode.", offset, is(this.offset)); + MatcherAssert.assertThat("Unexpected length post decode.", length, is(this.length)); + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtilsTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtilsTest.java new file mode 100644 index 000000000..fa00affd1 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CBORUtilsTest.java @@ -0,0 +1,154 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.agrona.BitUtil; +import org.hamcrest.MatcherAssert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CBORUtilsTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + {CborMajorType.UnsignedInteger, 22}, + {CborMajorType.UnsignedInteger, -1}, + {CborMajorType.UnsignedInteger, 0}, + {CborMajorType.UnsignedInteger, Byte.MAX_VALUE}, + {CborMajorType.UnsignedInteger, Short.MAX_VALUE}, + {CborMajorType.UnsignedInteger, Integer.MAX_VALUE}, + + {CborMajorType.NegativeInteger, 2}, + {CborMajorType.NegativeInteger, -1}, + {CborMajorType.NegativeInteger, 0}, + {CborMajorType.NegativeInteger, Byte.MAX_VALUE}, + {CborMajorType.NegativeInteger, Short.MAX_VALUE}, + {CborMajorType.NegativeInteger, Integer.MAX_VALUE}, + + {CborMajorType.Utf8String, 2}, + {CborMajorType.Utf8String, -1}, + {CborMajorType.Utf8String, 0}, + {CborMajorType.Utf8String, Byte.MAX_VALUE}, + {CborMajorType.Utf8String, Short.MAX_VALUE}, + {CborMajorType.Utf8String, Integer.MAX_VALUE}, + {CborMajorType.Utf8String, Long.MAX_VALUE}, + + {CborMajorType.ByteString, 2}, + {CborMajorType.ByteString, -1}, + {CborMajorType.ByteString, 0}, + {CborMajorType.ByteString, Byte.MAX_VALUE}, + {CborMajorType.ByteString, Short.MAX_VALUE}, + {CborMajorType.ByteString, Integer.MAX_VALUE}, + {CborMajorType.ByteString, Long.MAX_VALUE}, + + {CborMajorType.MAP, 2}, + {CborMajorType.MAP, -1}, + {CborMajorType.MAP, 0}, + {CborMajorType.MAP, Byte.MAX_VALUE}, + {CborMajorType.MAP, Short.MAX_VALUE}, + {CborMajorType.MAP, Integer.MAX_VALUE}, + {CborMajorType.MAP, Long.MAX_VALUE}, + + {CborMajorType.ARRAY, 2}, + {CborMajorType.ARRAY, -1}, + {CborMajorType.ARRAY, 0}, + {CborMajorType.ARRAY, Byte.MAX_VALUE}, + {CborMajorType.ARRAY, Short.MAX_VALUE}, + {CborMajorType.ARRAY, Integer.MAX_VALUE}, + {CborMajorType.ARRAY, Long.MAX_VALUE}, + }); + } + + @Parameter + public CborMajorType type; + @Parameter(1) + public long length; + + @Test(timeout = 60000) + public void parseDataLengthOrDie() throws Exception { + IndexedUnsafeBuffer ib = newBufferWithHeader(); + long length = CBORUtils.parseDataLengthOrDie(ib, type, new NullPointerException()); + + MatcherAssert.assertThat("Unexpected length post decode.", length, is(normalizeLength(this.length))); + } + + @Test(timeout = 60000, expected = RuntimeException.class) + public void parseDataLengthOrDieWrongType() throws Exception { + IndexedUnsafeBuffer ib = newBufferWithHeader(); + CBORUtils.parseDataLengthOrDie(ib, CborMajorType.Unknown, new NullPointerException()); + } + + @Test(timeout = 60000) + public void getEncodeLength() throws Exception { + long encodeLength = CBORUtils.getEncodeLength(length); + long expectedLength = length; + if (length < 24 || expectedLength == 31) { + expectedLength++; + } else if (length <= Byte.MAX_VALUE) { + expectedLength++; + expectedLength += BitUtil.SIZE_OF_BYTE; + } else if (length <= Short.MAX_VALUE) { + expectedLength++; + expectedLength += BitUtil.SIZE_OF_SHORT; + } else if (length <= Integer.MAX_VALUE) { + expectedLength++; + expectedLength += BitUtil.SIZE_OF_INT; + } else if (length <= Long.MAX_VALUE) { + expectedLength++; + expectedLength += BitUtil.SIZE_OF_LONG; + } + + MatcherAssert.assertThat("Unexpected encoded length.", encodeLength, is(expectedLength)); + } + + @Test(timeout = 60000) + public void encodeTypeHeader() throws Exception { + ByteBuffer allocate = ByteBuffer.allocate(100); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(allocate); + int expected = CborHeader.forLengthToEncode(length).getSizeInBytes(); + int encodedLength = CBORUtils.encodeTypeHeader(iub, type, length); + + MatcherAssert.assertThat("Unexpected number of bytes written.", encodedLength, is(expected)); + } + + private IndexedUnsafeBuffer newBufferWithHeader() { + ByteBuffer src = ByteBuffer.allocate(100); + IndexedUnsafeBuffer ib = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + ib.wrap(src); + CborHeader cborHeader = CborHeader.forLengthToEncode(length); + cborHeader.encode(ib, type, normalizeLength(length)); + return ib; + } + + private long normalizeLength(long length) { + return -1 == length ? CborHeader.INDEFINITE.getCode() : this.length; + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodecTest.java new file mode 100644 index 000000000..dee99db8b --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborBinaryStringCodecTest.java @@ -0,0 +1,131 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.CodecRule; +import org.hamcrest.MatcherAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CborBinaryStringCodecTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + { 0 }, + { Integer.valueOf(Byte.MAX_VALUE) }, + { Integer.valueOf(Short.MAX_VALUE) }, + }); + } + + @Parameter + public int bufLength; + + @Rule + public final CodecRule cborCodecRule = new CodecRule<>(CborCodec::create); + + @Test(timeout = 60000) + public void testInfiniteDecode() throws Exception { + ByteBuffer toEncode = newBuffer(bufLength); + ByteBuffer encode = encodeChunked(toEncode); + + testDecode(toEncode, encode); + } + + @Test(timeout = 60000) + public void testEncodeWithJacksonAndDecode() throws Exception { + ByteBuffer toEncode = newBuffer(bufLength); + ByteBuffer encode = cborCodecRule.getCodec().encode(toEncode); + testDecode(toEncode, encode); + } + + @Test(timeout = 60000) + public void testEncodeAndDecodeWithJackson() throws Exception { + ByteBuffer toEncode = newBuffer(bufLength); + ByteBuffer dst = ByteBuffer.allocate(bufLength + 10); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(dst); + CborBinaryStringCodec.encode(iub, toEncode); + dst.rewind(); + + ByteBuffer decode = cborCodecRule.getCodec().decode(dst, ByteBuffer.class); + + toEncode.rewind(); + MatcherAssert.assertThat("Unexpected decode.", decode, equalTo(toEncode)); + } + + private static ByteBuffer newBuffer(int len) { + byte[] b = new byte[len]; + Arrays.fill(b, (byte) 'a'); + return ByteBuffer.wrap(b); + } + + private static void testDecode(ByteBuffer toEncode, ByteBuffer encode) { + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(encode); + + ByteBuffer dst = ByteBuffer.allocate(toEncode.remaining()); + IndexedUnsafeBuffer idst = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + idst.wrap(dst); + CborBinaryStringCodec.decode(iub, idst); + + MatcherAssert.assertThat("Unexpected decode.", dst, equalTo(toEncode)); + } + + private ByteBuffer encodeChunked(ByteBuffer toEncode) { + int chunkCount = 5; + int chunkSize = bufLength / chunkCount; + CborHeader chunkHeader = CborHeader.forLengthToEncode(chunkSize); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(toEncode); + + ByteBuffer encode = ByteBuffer.allocate(bufLength + 100); + IndexedUnsafeBuffer encodeB = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + encodeB.wrap(encode); + + int offset = 0; + CborHeader.INDEFINITE.encodeIndefiniteLength(encodeB, CborMajorType.ByteString); + + int remaining = bufLength - offset; + + while (remaining > 0) { + int thisChunkSize = Math.min(chunkSize, remaining); + chunkHeader.encode(encodeB, CborMajorType.ByteString, thisChunkSize); + iub.readBytes(encodeB, thisChunkSize); + encodeB.incrementWriterIndex(thisChunkSize); + offset += thisChunkSize; + remaining = bufLength - offset; + } + + CborHeader.SMALL.encode(encodeB, CborMajorType.Break, 31); + + return encode; + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborCodecTest.java new file mode 100644 index 000000000..f70564d63 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborCodecTest.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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.AbstractJacksonCodecTest; +import io.reactivesocket.mimetypes.internal.CodecRule; +import org.junit.Rule; + +public class CborCodecTest extends AbstractJacksonCodecTest { + + @Rule + public final CodecRule codecRule = new CodecRule<>(CborCodec::create); + + @Override + protected CodecRule getCodecRule() { + return codecRule; + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborHeaderTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborHeaderTest.java new file mode 100644 index 000000000..d128a30ef --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborHeaderTest.java @@ -0,0 +1,131 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.hamcrest.MatcherAssert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CborHeaderTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + {CborMajorType.UnsignedInteger, 22}, + {CborMajorType.UnsignedInteger, -1}, + {CborMajorType.UnsignedInteger, 0}, + {CborMajorType.UnsignedInteger, Byte.MAX_VALUE}, + {CborMajorType.UnsignedInteger, Short.MAX_VALUE}, + {CborMajorType.UnsignedInteger, Integer.MAX_VALUE}, + + {CborMajorType.NegativeInteger, 2}, + {CborMajorType.NegativeInteger, -1}, + {CborMajorType.NegativeInteger, 0}, + {CborMajorType.NegativeInteger, Byte.MAX_VALUE}, + {CborMajorType.NegativeInteger, Short.MAX_VALUE}, + {CborMajorType.NegativeInteger, Integer.MAX_VALUE}, + + + {CborMajorType.Utf8String, 2}, + {CborMajorType.Utf8String, -1}, + {CborMajorType.Utf8String, 0}, + {CborMajorType.Utf8String, Byte.MAX_VALUE}, + {CborMajorType.Utf8String, Short.MAX_VALUE}, + {CborMajorType.Utf8String, Integer.MAX_VALUE}, + {CborMajorType.Utf8String, Long.MAX_VALUE}, + + {CborMajorType.ByteString, 2}, + {CborMajorType.ByteString, -1}, + {CborMajorType.ByteString, 0}, + {CborMajorType.ByteString, Byte.MAX_VALUE}, + {CborMajorType.ByteString, Short.MAX_VALUE}, + {CborMajorType.ByteString, Integer.MAX_VALUE}, + {CborMajorType.ByteString, Long.MAX_VALUE}, + + {CborMajorType.MAP, 2}, + {CborMajorType.MAP, -1}, + {CborMajorType.MAP, 0}, + {CborMajorType.MAP, Byte.MAX_VALUE}, + {CborMajorType.MAP, Short.MAX_VALUE}, + {CborMajorType.MAP, Integer.MAX_VALUE}, + {CborMajorType.MAP, Long.MAX_VALUE}, + + {CborMajorType.ARRAY, 2}, + {CborMajorType.ARRAY, -1}, + {CborMajorType.ARRAY, 0}, + {CborMajorType.ARRAY, Byte.MAX_VALUE}, + {CborMajorType.ARRAY, Short.MAX_VALUE}, + {CborMajorType.ARRAY, Integer.MAX_VALUE}, + {CborMajorType.ARRAY, Long.MAX_VALUE}, + }); + } + + @Parameter + public CborMajorType type; + @Parameter(1) + public long length; + + @Test(timeout = 60000) + public void testEncodeDecode() throws Exception { + CborHeader cborHeader = CborHeader.forLengthToEncode(length); + CborHeader expected = null; + if (length == -1) { + expected = CborHeader.INDEFINITE; + } else if (length < 24) { + expected = CborHeader.SMALL; + } else if (length <= Byte.MAX_VALUE) { + expected = CborHeader.BYTE; + } else if (length <= Short.MAX_VALUE) { + expected = CborHeader.SHORT; + } else if (length <= Integer.MAX_VALUE) { + expected = CborHeader.INT; + } else if (length <= Long.MAX_VALUE) { + expected = CborHeader.LONG; + } + + MatcherAssert.assertThat("Unexpected CBOR header type for length: " + length, cborHeader, is(expected)); + + if (length < 0) { + return; + } + + ByteBuffer allocate = ByteBuffer.allocate(CborHeader.LONG.getSizeInBytes()); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(allocate); + + cborHeader.encode(iub, type, length); + + MatcherAssert.assertThat("Unxexpected bytes written for type: " + type + " and length: " + length, + (short) iub.getWriterIndex(), equalTo(cborHeader.getSizeInBytes())); + + iub.setReaderIndex(0); + long l = CborHeader.readDataLength(iub, iub.readUnsignedByte()); + MatcherAssert.assertThat("Unexpected data length read from encode.", l, equalTo(length)); + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodecTest.java new file mode 100644 index 000000000..2798dafc5 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborMapCodecTest.java @@ -0,0 +1,125 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.CodecRule; +import io.reactivesocket.mimetypes.internal.KVMetadataImpl; +import org.hamcrest.MatcherAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static io.reactivesocket.mimetypes.internal.cbor.ByteBufferMapMatcher.*; + +@RunWith(Parameterized.class) +public class CborMapCodecTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + { 0 }, + { 1 }, + { Integer.valueOf(Byte.MAX_VALUE) }, + { Integer.valueOf(Short.MAX_VALUE) }, + }); + } + + @Parameter + public int mapSize; + + @Rule + public final CborMapRule mapRule = new CborMapRule(); + + @Rule + public final CodecRule cborCodecRule = new CodecRule<>(CborCodec::create); + + @Test(timeout = 60000) + public void testEncodeWithJacksonAndDecode() throws Exception { + Map map = mapRule.newMap(mapSize); + ByteBuffer encode = cborCodecRule.getCodec().encode(map); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(encode); + + CBORMap decode = CborMapCodec.decode(iub, (b, o, i) -> new CBORMap(b, o, i)); + + MatcherAssert.assertThat("Unexpected decode.", decode, mapEqualTo(map)); + } + + @Test(timeout = 60000) + public void testEncodeAndDecodeWithJackson() throws Exception { + Map map = mapRule.newMap(mapSize); + ByteBuffer encode = ByteBuffer.allocate(mapSize == 0 ? 20 : mapSize * 100); + IndexedUnsafeBuffer iencode = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iencode.wrap(encode); + + CborMapCodec.encode(iencode, map); + + @SuppressWarnings("unchecked") + Map decode = cborCodecRule.getCodec().decode(encode, KVMetadataImpl.class); + + MatcherAssert.assertThat("Unexpected decode.", decode, mapEqualTo(map)); + } + + @Test(timeout = 60000) + public void testEncodeCborMapAsIs() throws Exception { + mapRule.addMockEntries(mapSize); + ByteBuffer encode = ByteBuffer.allocate(mapRule.map.getBackingBuffer().capacity() + 100); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(encode); + + CborMapCodec.encode(iub, mapRule.map); + + @SuppressWarnings("unchecked") + Map decode = cborCodecRule.getCodec().decode(encode, KVMetadataImpl.class); + + MatcherAssert.assertThat("Unexpected decode.", decode, mapEqualTo(mapRule.map)); + } + + public class CborMapRule extends AbstractCborMapRule { + + @Override + protected void init() { + valueBuffer = ByteBuffer.allocate(mapSize == 0 ? 20 : mapSize * 100); + indexed = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + indexed.wrap(valueBuffer); + map = new CBORMap(indexed.getBackingBuffer(), 0); + } + + private Map newMap(int mapSize) { + Map map = new HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + String key = "Key" + i; + byte[] val = ("Value" + i).getBytes(StandardCharsets.UTF_8); + ByteBuffer v = ByteBuffer.wrap(val); + map.put(key, v); + } + return map; + } + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodecTest.java new file mode 100644 index 000000000..0e7623760 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/CborUtf8StringCodecTest.java @@ -0,0 +1,85 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.internal.CodecRule; +import org.hamcrest.MatcherAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class CborUtf8StringCodecTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + { 0 }, + { Integer.valueOf(Byte.MAX_VALUE) }, + { Integer.valueOf(Short.MAX_VALUE) }, + }); + } + + @Parameter + public int stringLength; + + @Rule + public final CodecRule cborCodecRule = new CodecRule<>(CborCodec::create); + + @Test(timeout = 60000) + public void testEncodeWithJacksonAndDecode() throws Exception { + String toEncode = newString(stringLength); + ByteBuffer encode = cborCodecRule.getCodec().encode(toEncode); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(encode); + + String decode = CborUtf8StringCodec.decode(iub); + + MatcherAssert.assertThat("Unexpected decode.", decode, equalTo(toEncode)); + } + + @Test(timeout = 60000) + public void testEncodeWithDecodeWithJackson() throws Exception { + String toEncode = newString(stringLength); + ByteBuffer dst = ByteBuffer.allocate(stringLength + 10); + IndexedUnsafeBuffer iub = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + iub.wrap(dst); + CborUtf8StringCodec.encode(iub, toEncode); + dst.rewind(); + + String decode = cborCodecRule.getCodec().decode(dst, String.class); + + MatcherAssert.assertThat("Unexpected decode.", decode, equalTo(toEncode)); + } + + private static String newString(int stringLength) { + byte[] b = new byte[stringLength]; + Arrays.fill(b, (byte) 'a'); + return new String(b); + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBufferTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBufferTest.java new file mode 100644 index 000000000..38596b01d --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/IndexedUnsafeBufferTest.java @@ -0,0 +1,115 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import org.hamcrest.MatcherAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.model.Statement; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class IndexedUnsafeBufferTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + {0, 0, 0}, + {10, 0, 10}, + {100, 0, 100}, + {500, 0, 500}, + {500, 10, 400}, + {500, 10, 490}, + }); + } + + @Parameter + public int bufferSize; + @Parameter(1) + public int bufferOffset; + @Parameter(2) + public int bufferLength; + + @Rule + public final BufferRule bufferRule = new BufferRule(); + + @Test(timeout = 60000) + public void testForEachByteFound() throws Exception { + testScanForBreak(bufferLength / 2); + } + + @Test(timeout = 60000) + public void testForEachByteNotFound() throws Exception { + bufferRule.initBuffer(bufferSize, bufferOffset, bufferLength); + int i = bufferRule.buffer.forEachByte(CBORUtils.BREAK_SCANNER); + MatcherAssert.assertThat("Unexpected index.", i, is(bufferRule.buffer.getBackingBuffer().capacity())); + } + + @Test(timeout = 60000) + public void testForEachByteLastByte() throws Exception { + testScanForBreak(bufferLength - bufferOffset - 1); + } + + private void testScanForBreak(int indexForBreak) { + bufferRule.initBuffer(bufferSize, bufferOffset, bufferLength); + if (bufferSize > 0) { + bufferRule.buffer.getBackingBuffer().putByte(indexForBreak, CborMajorType.CBOR_BREAK); + } + + int i = bufferRule.buffer.forEachByte(CBORUtils.BREAK_SCANNER); + MatcherAssert.assertThat("Unexpected index.", i, is(Math.max(0, indexForBreak))); + } + + public static class BufferRule extends ExternalResource { + + private IndexedUnsafeBuffer buffer; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + initBuffer(0, 0, 0); + base.evaluate(); + } + }; + } + + public void initBuffer(int size, int offset, int length) { + ByteBuffer b = ByteBuffer.allocate(size); + buffer = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + buffer.wrap(b, 0, length - offset); + if (length != 0) { + buffer.setReaderIndex(offset); + buffer.setWriterIndex(offset); + } + } + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodecTest.java new file mode 100644 index 000000000..2640caf69 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/MetadataCodecTest.java @@ -0,0 +1,148 @@ +/* + * 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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.KVMetadata; +import io.reactivesocket.mimetypes.internal.KVMetadataImpl; +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +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.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import static io.reactivesocket.mimetypes.internal.cbor.ByteBufferMapMatcher.*; +import static org.hamcrest.MatcherAssert.*; + +public class MetadataCodecTest { + + @Rule + public final CodecRule metaCodecRule = new CodecRule(); + @Rule + public final io.reactivesocket.mimetypes.internal.CodecRule cborCodecRule = + new io.reactivesocket.mimetypes.internal.CodecRule<>(CborCodec::create); + + @Test(timeout = 60000) + public void testDecode() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + ByteBuffer encode = cborCodecRule.getCodec().encode(metaCodecRule.testDataHolder); + + KVMetadata decode = metaCodecRule.codec.decode(encode, KVMetadata.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testDataHolder)); + } + + @Test(timeout = 60000) + public void testDecodeDirect() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + ByteBuffer encode = cborCodecRule.getCodec().encode(metaCodecRule.testDataHolder); + UnsafeBuffer ub = new UnsafeBuffer(encode); + KVMetadata decode = metaCodecRule.codec.decode(ub, 0, KVMetadata.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testDataHolder)); + } + + @Test(timeout = 60000) + public void encode() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + ByteBuffer encode = metaCodecRule.codec.encode(metaCodecRule.testData); + KVMetadata decode = cborCodecRule.getCodec().decode(encode, KVMetadataImpl.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testData)); + } + + @Test(timeout = 60000) + public void encodeDirect() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + DirectBuffer encode = metaCodecRule.codec.encodeDirect(metaCodecRule.testData); + KVMetadata decode = cborCodecRule.getCodec().decode(encode.byteBuffer(), KVMetadataImpl.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testData)); + } + + @Test(timeout = 60000) + public void encodeTo() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + ByteBuffer dst = ByteBuffer.allocate(500); + metaCodecRule.codec.encodeTo(dst, metaCodecRule.testData); + KVMetadata decode = cborCodecRule.getCodec().decode(dst, KVMetadataImpl.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testData)); + } + + @Test(timeout = 60000) + public void encodeToDirect() throws Exception { + metaCodecRule.addTestData("Key1", "Value1"); + metaCodecRule.addTestData("Key2", "Value2"); + metaCodecRule.addTestData("Key3", "Value3"); + + ByteBuffer dst = ByteBuffer.allocate(500); + UnsafeBuffer ub = new UnsafeBuffer(dst); + metaCodecRule.codec.encodeTo(ub, metaCodecRule.testData, 0); + KVMetadata decode = cborCodecRule.getCodec().decode(dst, KVMetadataImpl.class); + + assertThat("Unexpected decode.", decode, mapEqualTo(metaCodecRule.testData)); + } + + public static class CodecRule extends ExternalResource { + + private MetadataCodec codec; + private Map testDataHolder; + private KVMetadata testData; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + codec = MetadataCodec.INSTANCE; + testDataHolder = new HashMap<>(); + testData = new KVMetadataImpl(testDataHolder); + base.evaluate(); + } + }; + } + + public void addTestData(String key, String value) { + ByteBuffer vBuff = ByteBuffer.allocate(value.length()).put(value.getBytes()); + vBuff.flip(); + testDataHolder.put(key, vBuff); + } + } + +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadataTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadataTest.java new file mode 100644 index 000000000..a28450838 --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/cbor/SlicedBufferKVMetadataTest.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.reactivesocket.mimetypes.internal.cbor; + +import io.reactivesocket.mimetypes.KVMetadata; +import org.agrona.concurrent.UnsafeBuffer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +@RunWith(Parameterized.class) +public class SlicedBufferKVMetadataTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Integer[][] { + {100, 0, 100, 5}, {400, 20, 200, 20}, {400, 20, 140, 20} + }); + } + + @Parameter + public int bufferSize; + @Parameter(1) + public int bufferOffset; + @Parameter(2) + public int bufferLength; + @Parameter(3) + public int entrySize; + + @Rule + public final SlicedBufferKVMetadataRule mapRule = new SlicedBufferKVMetadataRule(); + + @Test(timeout = 60000) + public void getAsString() throws Exception { + mapRule.addMockEntries(entrySize); + for (int i = 0; i < entrySize; i++) { + mapRule.assertValueAsStringForKey(i); + } + } + + @Test(timeout = 60000) + public void duplicate() throws Exception { + mapRule.addMockEntries(entrySize); + KVMetadata duplicate = mapRule.map.duplicate(capacity -> new UnsafeBuffer(ByteBuffer.allocate(capacity))); + + assertThat("Unexpected type of duplicate.", duplicate, instanceOf(SlicedBufferKVMetadata.class)); + + SlicedBufferKVMetadata dup = (SlicedBufferKVMetadata) duplicate; + + for (int i = 0; i < entrySize; i++) { + mapRule.assertValueAsStringForKey(i, dup); + } + } + + public class SlicedBufferKVMetadataRule extends AbstractCborMapRule { + + @Override + protected void init() { + valueBuffer = ByteBuffer.allocate(bufferSize); + indexed = new IndexedUnsafeBuffer(ByteOrder.BIG_ENDIAN); + indexed.wrap(valueBuffer, bufferOffset, bufferLength); + map = new SlicedBufferKVMetadata(indexed.getBackingBuffer(), bufferOffset); + } + + public void assertValueAsStringForKey(int index) { + assertValueAsStringForKey(index, map); + } + + public void assertValueAsStringForKey(int index, SlicedBufferKVMetadata map) { + String k = getMockKey(index); + assertThat("Unexpected lookup value for key: " + k, map.getAsString(k, StandardCharsets.UTF_8), + equalTo(getMockValue(index))); + } + } + +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/json/JsonCodecTest.java b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/json/JsonCodecTest.java new file mode 100644 index 000000000..757e12c3a --- /dev/null +++ b/reactivesocket-mime-types/src/test/java/io/reactivesocket/mimetypes/internal/json/JsonCodecTest.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.reactivesocket.mimetypes.internal.json; + +import io.reactivesocket.mimetypes.internal.AbstractJacksonCodecTest; +import io.reactivesocket.mimetypes.internal.CodecRule; +import org.junit.Rule; + +public class JsonCodecTest extends AbstractJacksonCodecTest { + + @Rule + public final CodecRule codecRule = new CodecRule<>(JsonCodec::create); + + @Override + protected CodecRule getCodecRule() { + return codecRule; + } +} \ No newline at end of file diff --git a/reactivesocket-mime-types/src/test/resources/log4j.properties b/reactivesocket-mime-types/src/test/resources/log4j.properties new file mode 100644 index 000000000..70bc4badb --- /dev/null +++ b/reactivesocket-mime-types/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=DEBUG, 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/reactivesocket-test/build.gradle b/reactivesocket-test/build.gradle new file mode 100644 index 000000000..5eff68dbc --- /dev/null +++ b/reactivesocket-test/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compile project(':reactivesocket-core') +} diff --git a/reactivesocket-test/src/main/java/io/reactivesocket/test/TestUtil.java b/reactivesocket-test/src/main/java/io/reactivesocket/test/TestUtil.java new file mode 100644 index 000000000..6100102af --- /dev/null +++ b/reactivesocket-test/src/main/java/io/reactivesocket/test/TestUtil.java @@ -0,0 +1,131 @@ +/** + * 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.reactivesocket.test; + +import io.reactivesocket.Frame; +import io.reactivesocket.FrameType; +import io.reactivesocket.Payload; +import org.agrona.MutableDirectBuffer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +public class TestUtil +{ + public static Frame utf8EncodedRequestFrame(final int streamId, final FrameType type, final String data, final int initialRequestN) + { + return Frame.Request.from(streamId, type, new Payload() + { + public ByteBuffer getData() + { + return byteBufferFromUtf8String(data); + } + + public ByteBuffer getMetadata() + { + return Frame.NULL_BYTEBUFFER; + } + }, initialRequestN); + } + + public static Frame utf8EncodedResponseFrame(final int streamId, final FrameType type, final String data) + { + return Frame.Response.from(streamId, type, utf8EncodedPayload(data, null)); + } + + public static Frame utf8EncodedErrorFrame(final int streamId, final String data) + { + return Frame.Error.from(streamId, new Exception(data)); + } + + public static Payload utf8EncodedPayload(final String data, final String metadata) + { + return new PayloadImpl(data, metadata); + } + + public static String byteToString(ByteBuffer byteBuffer) + { + byteBuffer = byteBuffer.duplicate(); + + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + public static ByteBuffer byteBufferFromUtf8String(final String data) + { + final byte[] bytes = data.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.wrap(bytes); + } + + public static void copyFrame(final MutableDirectBuffer dst, final int offset, final Frame frame) + { + dst.putBytes(offset, frame.getByteBuffer(), frame.offset(), frame.length()); + } + + private static class PayloadImpl implements Payload // some JDK shoutout + { + private ByteBuffer data; + private ByteBuffer metadata; + + public PayloadImpl(final String data, final String metadata) + { + if (null == data) + { + this.data = ByteBuffer.allocate(0); + } + else + { + this.data = byteBufferFromUtf8String(data); + } + + if (null == metadata) + { + this.metadata = ByteBuffer.allocate(0); + } + else + { + this.metadata = byteBufferFromUtf8String(metadata); + } + } + + public boolean equals(Object obj) + { + System.out.println("equals: " + obj); + final Payload rhs = (Payload)obj; + + return (TestUtil.byteToString(data).equals(TestUtil.byteToString(rhs.getData()))) && + (TestUtil.byteToString(metadata).equals(TestUtil.byteToString(rhs.getMetadata()))); + } + + public ByteBuffer getData() + { + return data; + } + + public ByteBuffer getMetadata() + { + return metadata; + } + } +} diff --git a/reactivesocket-transport-aeron/build.gradle b/reactivesocket-transport-aeron/build.gradle new file mode 100644 index 000000000..ce9b7eb0f --- /dev/null +++ b/reactivesocket-transport-aeron/build.gradle @@ -0,0 +1,5 @@ +dependencies { + compile project(':reactivesocket-core') + compile project(':reactivesocket-test') + compile 'io.aeron:aeron-all:0.9.5' +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/MediaDriver.java b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/MediaDriver.java new file mode 100644 index 000000000..3d89d02e0 --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/MediaDriver.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.reactivesocket.aeron.example; + +import io.aeron.driver.ThreadingMode; +import org.agrona.concurrent.BackoffIdleStrategy; + +public class MediaDriver { + public static void main(String... args) { + ThreadingMode threadingMode = ThreadingMode.SHARED; + + boolean dedicated = Boolean.getBoolean("dedicated"); + + if (dedicated) { + threadingMode = ThreadingMode.DEDICATED; + } + + System.out.println("ThreadingMode => " + threadingMode); + + final io.aeron.driver.MediaDriver.Context ctx = new io.aeron.driver.MediaDriver.Context() + .threadingMode(threadingMode) + .dirsDeleteOnStart(true) + .conductorIdleStrategy(new BackoffIdleStrategy(1, 1, 100, 1000)) + .receiverIdleStrategy(new BackoffIdleStrategy(1, 1, 100, 1000)) + .senderIdleStrategy(new BackoffIdleStrategy(1, 1, 100, 1000)); + + final io.aeron.driver.MediaDriver ignored = io.aeron.driver.MediaDriver.launch(ctx); + + } +} diff --git a/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Fire.java b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Fire.java new file mode 100644 index 000000000..2b3e7db61 --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Fire.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.reactivesocket.aeron.example.fireandforget; + + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.aeron.client.AeronClientDuplexConnection; +import io.reactivesocket.aeron.client.AeronClientDuplexConnectionFactory; +import io.reactivesocket.aeron.client.FrameHolder; +import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import rx.RxReactiveStreams; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Fire { + public static void main(String... args) throws Exception { + String host = System.getProperty("host", "localhost"); + String server = System.getProperty("server", "localhost"); + + System.out.println("Setting host to => " + host); + + System.out.println("Setting ping is listening to => " + server); + + + byte[] payload = new byte[40]; + Random r = new Random(); + r.nextBytes(payload); + + System.out.println("Sending data of size => " + payload.length); + + InetSocketAddress listenAddress = new InetSocketAddress("localhost", 39790); + InetSocketAddress clientAddress = new InetSocketAddress("localhost", 39790); + + AeronClientDuplexConnectionFactory cf = AeronClientDuplexConnectionFactory.getInstance(); + cf.addSocketAddressToHandleResponses(listenAddress); + Publisher udpConnection = cf.createAeronClientDuplexConnection(clientAddress); + + System.out.println("Creating new duplex connection"); + AeronClientDuplexConnection connection = RxReactiveStreams.toObservable(udpConnection).toBlocking().single(); + System.out.println("Created duplex connection"); + + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, ConnectionSetupPayload.create("UTF-8", "UTF-8", ConnectionSetupPayload.NO_FLAGS)); + reactiveSocket.startAndWait(); + + CountDownLatch latch = new CountDownLatch(Integer.MAX_VALUE); + + final Recorder histogram = new Recorder(3600000000000L, 3); + + Schedulers + .computation() + .createWorker() + .schedulePeriodically(() -> { + System.out.println("---- FRAME HOLDER HISTO ----"); + FrameHolder.histogram.getIntervalHistogram().outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- FRAME HOLDER HISTO ----"); + + System.out.println("---- Fire / Forget HISTO ----"); + histogram.getIntervalHistogram().outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- Fire / Forget HISTO ----"); + + + }, 10, 10, TimeUnit.SECONDS); + + + for (int i = 0; i < Integer.MAX_VALUE; i++) { + long start = System.nanoTime(); + + Payload keyPayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(payload); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + reactiveSocket + .fireAndForget(keyPayload) + .subscribe(new org.reactivestreams.Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + + } + + @Override + public void onError(Throwable t) { + + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + latch.countDown(); + } + + @Override + public void onComplete() { + + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + latch.countDown(); + } + }); + } + latch.await(); + System.out.println("Sent => " + Integer.MAX_VALUE); + System.exit(0); + } + +} diff --git a/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Forget.java b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Forget.java new file mode 100644 index 000000000..37230114d --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/fireandforget/Forget.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.reactivesocket.aeron.example.fireandforget; + +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.aeron.server.ReactiveSocketAeronServer; +import io.reactivesocket.exceptions.SetupException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public class Forget { + public static void main(String... args) { + + String host = System.getProperty("host", "localhost"); + + System.out.println("Setting host to => " + host); + + ReactiveSocketAeronServer server = ReactiveSocketAeronServer.create(host, 39790, new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket rs) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + s.onComplete(); + } + }; + } + + @Override + public Publisher handleChannel(Payload initial, Publisher payloads) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + } +} diff --git a/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Ping.java b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Ping.java new file mode 100644 index 000000000..7e7e7fa68 --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Ping.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.reactivesocket.aeron.example.requestreply; + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.aeron.client.AeronClientDuplexConnection; +import io.reactivesocket.aeron.client.AeronClientDuplexConnectionFactory; +import io.reactivesocket.aeron.client.FrameHolder; +import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscriber; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Ping { + + public static void main(String... args) throws Exception { + String host = System.getProperty("host", "localhost"); + String server = System.getProperty("server", "localhost"); + + System.out.println("Setting host to => " + host); + + System.out.println("Setting ping is listening to => " + server); + + + byte[] key = new byte[4]; + //byte[] key = new byte[BitUtil.SIZE_OF_INT]; + Random r = new Random(); + r.nextBytes(key); + + System.out.println("Sending data of size => " + key.length); + + InetSocketAddress listenAddress = new InetSocketAddress("localhost", 39790); + InetSocketAddress clientAddress = new InetSocketAddress("localhost", 39790); + + AeronClientDuplexConnectionFactory cf = AeronClientDuplexConnectionFactory.getInstance(); + cf.addSocketAddressToHandleResponses(listenAddress); + Publisher udpConnection = cf.createAeronClientDuplexConnection(clientAddress); + + System.out.println("Creating new duplex connection"); + AeronClientDuplexConnection connection = RxReactiveStreams.toObservable(udpConnection).toBlocking().single(); + System.out.println("Created duplex connection"); + + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, ConnectionSetupPayload.create("UTF-8", "UTF-8", ConnectionSetupPayload.NO_FLAGS)); + reactiveSocket.startAndWait(); + + CountDownLatch latch = new CountDownLatch(Integer.MAX_VALUE); + + final Recorder histogram = new Recorder(3600000000000L, 3); + + Schedulers + .computation() + .createWorker() + .schedulePeriodically(() -> { + System.out.println("---- FRAME HOLDER HISTO ----"); + FrameHolder.histogram.getIntervalHistogram().outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- FRAME HOLDER HISTO ----"); + + System.out.println("---- PING/ PONG HISTO ----"); + histogram.getIntervalHistogram().outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- PING/ PONG HISTO ----"); + + + }, 1, 1, TimeUnit.SECONDS); + + Observable + .range(1, Integer.MAX_VALUE) + .flatMap(i -> { + long start = System.nanoTime(); + + Payload keyPayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(key); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + return RxReactiveStreams + .toObservable( + reactiveSocket + .requestResponse(keyPayload)) + .doOnNext(s -> { + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + }); + }, 16) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + latch.countDown(); + } + }); + + latch.await(); + System.out.println("Sent => " + Integer.MAX_VALUE); + System.exit(0); + } + +} diff --git a/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Pong.java b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Pong.java new file mode 100644 index 000000000..baa733309 --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/java/io/reactivesocket/aeron/example/requestreply/Pong.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.reactivesocket.aeron.example.requestreply; + +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.aeron.server.ReactiveSocketAeronServer; +import io.reactivesocket.exceptions.SetupException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +import java.nio.ByteBuffer; +import java.util.Random; + +public class Pong { + + public static void main(String... args) { + + String host = System.getProperty("host", "localhost"); + + System.out.println("Setting host to => " + host); + + byte[] response = new byte[1024]; + //byte[] response = new byte[1024]; + Random r = new Random(); + r.nextBytes(response); + + System.out.println("Sending data of size => " + response.length); + + ReactiveSocketAeronServer server = ReactiveSocketAeronServer.create(host, 39790, new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket rs) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + long time = System.currentTimeMillis(); + + Publisher publisher = new Publisher() { + @Override + public void subscribe(Subscriber s) { + Payload responsePayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(response); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + s.onNext(responsePayload); + + long diff = System.currentTimeMillis() - time; + //timer.update(diff, TimeUnit.NANOSECONDS); + s.onComplete(); + } + }; + + return publisher; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleChannel(Payload initial, Publisher payloads) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + } + +} diff --git a/reactivesocket-transport-aeron/src/examples/resources/simplelogger.properties b/reactivesocket-transport-aeron/src/examples/resources/simplelogger.properties new file mode 100644 index 000000000..463129958 --- /dev/null +++ b/reactivesocket-transport-aeron/src/examples/resources/simplelogger.properties @@ -0,0 +1,35 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +#org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=trace + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=true + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss + +# Set to true if you want to output the current thread name. +# Defaults to true. +org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnection.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnection.java new file mode 100644 index 000000000..795b39f4b --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnection.java @@ -0,0 +1,145 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.aeron.Publication; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.aeron.internal.Loggable; +import io.reactivesocket.aeron.internal.NotConnectedException; +import io.reactivesocket.exceptions.TransportException; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Disposable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.agrona.concurrent.AbstractConcurrentArrayQueue; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +public class AeronClientDuplexConnection implements DuplexConnection, Loggable { + + private final Publication publication; + private final CopyOnWriteArrayList> subjects; + private final AbstractConcurrentArrayQueue frameSendQueue; + private final Consumer onClose; + + public AeronClientDuplexConnection( + Publication publication, + AbstractConcurrentArrayQueue frameSendQueue, + Consumer onClose) { + this.publication = publication; + this.subjects = new CopyOnWriteArrayList<>(); + this.frameSendQueue = frameSendQueue; + this.onClose = onClose; + } + + @Override + public final Observable getInput() { + if (isTraceEnabled()) { + trace("getting input for publication session id {} ", publication.sessionId()); + } + + return new Observable() { + public void subscribe(Observer o) { + o.onSubscribe(new Disposable() { + @Override + public void dispose() { + if (isTraceEnabled()) { + trace("removing Observer for publication with session id {} ", publication.sessionId()); + } + + subjects.removeIf(s -> s == o); + } + }); + + subjects.add(o); + } + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o + .subscribe(new Subscriber() { + private Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + s.request(128); + + } + + @Override + public void onNext(Frame frame) { + if (isTraceEnabled()) { + trace("onNext subscription => {} and frame => {}", subscription.toString(), frame.toString()); + } + + final FrameHolder fh = FrameHolder.get(frame, publication, subscription); + boolean offer; + do { + offer = frameSendQueue.offer(fh); + } while (!offer); + } + + @Override + public void onError(Throwable t) { + if (t instanceof NotConnectedException) { + callback.error(new TransportException(t)); + subscription.cancel(); + } else { + callback.error(t); + } + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return publication.isClosed() ? 0.0 : 1.0; + } + + @Override + public void close() throws IOException { + onClose.accept(publication); + } + + public CopyOnWriteArrayList> getSubjects() { + return subjects; + } + + public String toString() { + if (publication == null) { + return getClass().getName() + ":publication=null"; + } + + return getClass().getName() + ":publication=[" + + "channel=" + publication.channel() + "," + + "streamId=" + publication.streamId() + "," + + "sessionId=" + publication.sessionId() + "]"; + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnectionFactory.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnectionFactory.java new file mode 100644 index 000000000..576a2e4ba --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronClientDuplexConnectionFactory.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.reactivesocket.aeron.client; + +import io.reactivesocket.Frame; +import io.reactivesocket.aeron.internal.AeronUtil; +import io.reactivesocket.aeron.internal.Constants; +import io.reactivesocket.aeron.internal.Loggable; +import io.reactivesocket.aeron.internal.MessageType; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import io.aeron.Publication; +import io.aeron.logbuffer.FragmentHandler; +import io.aeron.logbuffer.Header; +import org.agrona.BitUtil; +import org.agrona.DirectBuffer; +import org.agrona.concurrent.ManyToManyConcurrentArrayQueue; +import org.agrona.concurrent.UnsafeBuffer; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static io.reactivesocket.aeron.internal.Constants.SERVER_STREAM_ID; + +public final class AeronClientDuplexConnectionFactory implements Loggable { + private static final AeronClientDuplexConnectionFactory instance = new AeronClientDuplexConnectionFactory(); + + private static ThreadLocal buffers = ThreadLocal.withInitial(() -> new UnsafeBuffer(Constants.EMTPY)); + + private final ConcurrentSkipListMap connections; + + private final ManyToManyConcurrentArrayQueue frameSendQueue = new ManyToManyConcurrentArrayQueue<>(Constants.QUEUE_SIZE); + + private final ConcurrentHashMap establishConnectionHolders; + + private final ClientAeronManager manager; + + private AeronClientDuplexConnectionFactory() { + connections = new ConcurrentSkipListMap<>(); + establishConnectionHolders = new ConcurrentHashMap<>(); + manager = ClientAeronManager.getInstance(); + + manager.addClientAction(() -> { + final boolean traceEnabled = isTraceEnabled(); + frameSendQueue + .drain(fh -> { + final Frame frame = fh.getFrame(); + final ByteBuffer byteBuffer = frame.getByteBuffer(); + final Publication publication = fh.getPublication(); + final int length = frame.length() + BitUtil.SIZE_OF_INT; + + // Can release the FrameHolder at this point as we got everything we need + fh.release(); + + if (!publication.isClosed()) { + AeronUtil + .tryClaimOrOffer(publication, (offset, buffer) -> { + if (traceEnabled) { + trace("Sending Frame => {} on Aeron", frame.toString()); + } + + buffer.putShort(offset, (short) 0); + buffer.putShort(offset + BitUtil.SIZE_OF_SHORT, (short) MessageType.FRAME.getEncodedType()); + buffer.putBytes(offset + BitUtil.SIZE_OF_INT, byteBuffer, frame.offset(), frame.length()); + }, length); + } + }); + }); + } + + public static AeronClientDuplexConnectionFactory getInstance() { + return instance; + } + + /** + * Adds a {@link java.net.SocketAddress} for Aeron to listen for responses on + * + * @param socketAddress + */ + public void addSocketAddressToHandleResponses(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + addUDPSocketAddressToHandleResponses((InetSocketAddress) socketAddress); + } else { + throw new RuntimeException("unknown socket address type => " + socketAddress.getClass()); + } + } + + void addUDPSocketAddressToHandleResponses(InetSocketAddress socketAddress) { + String serverChannel = "udp://" + socketAddress.getHostName() + ":" + socketAddress.getPort(); + + manager.addSubscription( + serverChannel, + Constants.CLIENT_STREAM_ID, + new FragmentHandler() { + @Override + public void onFragment(DirectBuffer buffer, int offset, int length, Header header) { + fragmentHandler(buffer, offset, length, header); + } + }); + } + + public Publisher createAeronClientDuplexConnection(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + return createUDPConnection((InetSocketAddress) socketAddress); + } else { + throw new RuntimeException("unknown socket address type => " + socketAddress.getClass()); + } + } + + Publisher createUDPConnection(InetSocketAddress inetSocketAddress) { + final String channel = "udp://" + inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort(); + debug("Creating a publication to channel => {}", channel); + final Publication publication = manager.getAeron().addPublication(channel, SERVER_STREAM_ID); + debug("Created a publication with sessionId => {} to channel => {}", publication.sessionId(), channel); + + return subscriber -> { + EstablishConnectionHolder establishConnectionHolder = new EstablishConnectionHolder(publication, subscriber); + establishConnectionHolders.putIfAbsent(publication.sessionId(), establishConnectionHolder); + + establishConnection(publication); + }; + } + + /** + * Establishes a connection between the client and server. Waits for 30 seconds before throwing a exception. + */ + void establishConnection(final Publication publication) { + final int sessionId = publication.sessionId(); + + debug("Establishing connection for channel => {}, stream id => {}", + publication.channel(), + publication.sessionId()); + + UnsafeBuffer buffer = buffers.get(); + buffer.wrap(new byte[BitUtil.SIZE_OF_INT]); + buffer.putShort(0, (short) 0); + buffer.putShort(BitUtil.SIZE_OF_SHORT, (short) MessageType.ESTABLISH_CONNECTION_REQUEST.getEncodedType()); + + long offer = -1; + final long start = System.nanoTime(); + for (;;) { + final long current = System.nanoTime(); + if ((current - start) > TimeUnit.MILLISECONDS.toNanos(Constants.CLIENT_ESTABLISH_CONNECT_TIMEOUT_MS)) { + throw new RuntimeException("Timed out waiting to establish connection for session id => " + sessionId); + } + + if (offer < 0) { + if (publication.isClosed()) { + throw new RuntimeException("A closed publication was found when trying to establish for session id => " + sessionId); + } + + offer = publication.offer(buffer); + } else { + break; + } + + } + + } + + void fragmentHandler(DirectBuffer buffer, int offset, int length, Header header) { + try { + short messageCount = buffer.getShort(offset); + short messageTypeInt = buffer.getShort(offset + BitUtil.SIZE_OF_SHORT); + + final MessageType messageType = MessageType.from(messageTypeInt); + if (messageType == MessageType.FRAME) { + AeronClientDuplexConnection aeronClientDuplexConnection = connections.get(header.sessionId()); + if (aeronClientDuplexConnection != null) { + CopyOnWriteArrayList> subjects = aeronClientDuplexConnection.getSubjects(); + if (!subjects.isEmpty()) { + //TODO think about how to recycle these, hard because could be handed to another thread I think? + final ByteBuffer bytes = ByteBuffer.allocate(length); + buffer.getBytes(BitUtil.SIZE_OF_INT + offset, bytes, length); + final Frame frame = Frame.from(bytes); + int i = 0; + final int size = subjects.size(); + do { + Observer frameObserver = subjects.get(i); + frameObserver.onNext(frame); + + i++; + } while (i < size); + } + } else { + debug("no connection found for Aeron Session Id {}", header.sessionId()); + } + } else if (messageType == MessageType.ESTABLISH_CONNECTION_RESPONSE) { + final int ackSessionId = buffer.getInt(offset + BitUtil.SIZE_OF_INT); + EstablishConnectionHolder establishConnectionHolder = establishConnectionHolders.remove(ackSessionId); + if (establishConnectionHolder != null) { + try { + AeronClientDuplexConnection aeronClientDuplexConnection + = new AeronClientDuplexConnection(establishConnectionHolder.getPublication(), frameSendQueue, new Consumer() { + @Override + public void accept(Publication publication) { + connections.remove(publication.sessionId()); + + // Send a message to the server that the connection is closed and that it needs to clean-up resources on it's side + if (publication != null && !publication.isClosed()) { + try { + AeronUtil.tryClaimOrOffer(publication, (offset, buffer) -> { + buffer.putShort(offset, (short) 0); + buffer.putShort(offset + BitUtil.SIZE_OF_SHORT, (short) MessageType.CONNECTION_DISCONNECT.getEncodedType()); + }, BitUtil.SIZE_OF_INT, Constants.CLIENT_SEND_ESTABLISH_CONNECTION_MSG_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Throwable t) { + debug("error closing publication with session id => {}", publication.sessionId()); + } + publication.close(); + } + } + }); + + connections.put(header.sessionId(), aeronClientDuplexConnection); + + establishConnectionHolder.getSubscriber().onNext(aeronClientDuplexConnection); + establishConnectionHolder.getSubscriber().onComplete(); + + debug("Connection established for channel => {}, stream id => {}", + establishConnectionHolder.getPublication().channel(), + establishConnectionHolder.getPublication().sessionId()); + } catch (Throwable t) { + establishConnectionHolder.getSubscriber().onError(t); + } + } + } else { + debug("Unknown message type => " + messageTypeInt); + } + } catch (Throwable t) { + error("error handling framement", t); + } + } + + /* + * Inner Classes + */ + class EstablishConnectionHolder { + private Publication publication; + private Subscriber subscriber; + + public EstablishConnectionHolder(Publication publication, Subscriber subscriber) { + this.publication = publication; + this.subscriber = subscriber; + } + + public Publication getPublication() { + return publication; + } + + public Subscriber getSubscriber() { + return subscriber; + } + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronReactiveSocketConnector.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronReactiveSocketConnector.java new file mode 100644 index 000000000..a96af7f93 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/AeronReactiveSocketConnector.java @@ -0,0 +1,131 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.reactivesocket.*; +import io.reactivesocket.rx.Completable; +import org.agrona.LangUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.util.Enumeration; +import java.util.function.Consumer; + +/** + * An implementation of {@link ReactiveSocketFactory} that creates Aeron ReactiveSockets. + */ +public class AeronReactiveSocketConnector implements ReactiveSocketConnector { + private static final Logger logger = LoggerFactory.getLogger(AeronReactiveSocketConnector.class); + + private final ConnectionSetupPayload connectionSetupPayload; + private final Consumer errorStream; + + public AeronReactiveSocketConnector(ConnectionSetupPayload connectionSetupPayload, Consumer errorStream) { + this(getIPv4InetAddress().getHostAddress(), 39790, connectionSetupPayload, errorStream); + } + + public AeronReactiveSocketConnector(String host, int port, ConnectionSetupPayload connectionSetupPayload, Consumer errorStream) { + this.connectionSetupPayload = connectionSetupPayload; + this.errorStream = errorStream; + + try { + InetSocketAddress inetSocketAddress = new InetSocketAddress(host, port); + logger.info("Listen to ReactiveSocket Aeron response on host {} port {}", host, port); + AeronClientDuplexConnectionFactory.getInstance().addSocketAddressToHandleResponses(inetSocketAddress); + } catch (Exception e) { + logger.error(e.getMessage(), e); + LangUtil.rethrowUnchecked(e); + } + } + + @Override + public Publisher connect(SocketAddress address) { + Publisher connection + = AeronClientDuplexConnectionFactory.getInstance().createAeronClientDuplexConnection(address); + + Observable result = Observable.create(s -> + connection.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); + } + + @Override + public void onNext(AeronClientDuplexConnection connection) { + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, connectionSetupPayload, errorStream); + reactiveSocket.start(new Completable() { + @Override + public void success() { + s.onNext(reactiveSocket); + s.onCompleted(); + } + + @Override + public void error(Throwable e) { + s.onError(e); + } + }); + } + + @Override + public void onError(Throwable t) { + s.onError(t); + } + + @Override + public void onComplete() { + } + }) + ); + + return RxReactiveStreams.toPublisher(result); + } + + private static InetAddress getIPv4InetAddress() { + InetAddress iaddress = null; + try { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("nix") || os.contains("nux")) { + NetworkInterface ni = NetworkInterface.getByName("eth0"); + + Enumeration ias = ni.getInetAddresses(); + + do { + iaddress = ias.nextElement(); + } while (!(iaddress instanceof Inet4Address)); + + } + + iaddress = InetAddress.getLocalHost(); // for Windows and OS X it should work well + } catch (Exception e) { + logger.error(e.getMessage(), e); + LangUtil.rethrowUnchecked(e); + } + + return iaddress; + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/ClientAeronManager.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/ClientAeronManager.java new file mode 100644 index 000000000..ab4d63f92 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/ClientAeronManager.java @@ -0,0 +1,182 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.aeron.Aeron; +import io.aeron.FragmentAssembler; +import io.aeron.Image; +import io.aeron.Subscription; +import io.aeron.driver.MediaDriver; +import io.aeron.driver.ThreadingMode; +import io.aeron.logbuffer.FragmentHandler; +import io.reactivesocket.aeron.internal.Constants; +import io.reactivesocket.aeron.internal.Loggable; +import org.agrona.concurrent.BackoffIdleStrategy; +import org.agrona.concurrent.SleepingIdleStrategy; +import rx.Scheduler; +import rx.schedulers.Schedulers; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +/** + * Class for managing the Aeron on the client side. + */ +public class ClientAeronManager implements Loggable { + private static final ClientAeronManager INSTANCE = new ClientAeronManager(); + + /** + * Enables running the client with an embedded Aeron {@link MediaDriver} so you don't have to run + * the driver in a separate process. To enable this option you need to set the reactivesocket.aeron.clientEmbeddedDriver + * to true + */ + static { + if (Constants.CLIENT_EMBEDDED_AERON_DRIVER) { + System.out.println("+++ Launching embedded media driver"); + final MediaDriver.Context context = new MediaDriver.Context(); + context.dirsDeleteOnStart(true); + context.threadingMode(ThreadingMode.SHARED_NETWORK); + context.conductorIdleStrategy(new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(10))); + context.senderIdleStrategy(new BackoffIdleStrategy(5, 10, 100, 1000)); + context.receiverIdleStrategy(new BackoffIdleStrategy(5, 10, 100, 1000)); + MediaDriver.launch(context); + } + } + + private final CopyOnWriteArrayList clientActions; + + private final CopyOnWriteArrayList subscriptionGroups; + + private final Aeron aeron; + + private final Scheduler.Worker worker; + + private ClientAeronManager() { + this.clientActions = new CopyOnWriteArrayList<>(); + this.subscriptionGroups = new CopyOnWriteArrayList<>(); + + final Aeron.Context ctx = new Aeron.Context(); + ctx.errorHandler(t -> error("an exception occurred", t)); + ctx.availableImageHandler((Image image) -> + debug("New image available with session id => {} and sourceIdentity => {} and subscription => {}", image.sessionId(), image.sourceIdentity(), image.subscription().toString()) + ); + + aeron = Aeron.connect(ctx); + worker = Schedulers.computation().createWorker(); + poll(); + } + + public static ClientAeronManager getInstance() { + return INSTANCE; + } + + /** + * Adds a ClientAction on the a list that is run by the polling loop. + * + * @param clientAction the {@link io.reactivesocket.aeron.client.ClientAeronManager.ClientAction} to add + */ + public void addClientAction(ClientAction clientAction) { + clientActions.add(clientAction); + } + + + public boolean hasSubscriptionForChannel(String subscriptionChannel) { + return subscriptionGroups + .stream() + .anyMatch(sg -> sg.getChannel().equals(subscriptionChannel)); + } + + public Aeron getAeron() { + return aeron; + } + + /** + * Adds an Aeron subscription to be polled. This method will create a subscription for each of the polling threads. + * + * @param subscriptionChannel the channel to create subscriptions on + * @param streamId the stream id to create subscriptions on + * @param fragmentHandler fragment handler that is aware of the thread that is call it. + */ + public void addSubscription(String subscriptionChannel, int streamId, FragmentHandler fragmentHandler) { + if (!hasSubscriptionForChannel(subscriptionChannel)) { + + debug("Creating a subscriptions to channel => {}", subscriptionChannel); + Subscription subscription = aeron.addSubscription(subscriptionChannel, streamId); + debug("Subscription created channel => {} ", subscriptionChannel); + SubscriptionGroup subscriptionGroup = new SubscriptionGroup(subscriptionChannel, subscription, fragmentHandler); + subscriptionGroups.add(subscriptionGroup); + debug("Subscriptions created to channel => {}", subscriptionChannel); + + } else { + debug("Subscription already exists for channel => {}", subscriptionChannel); + } + } + + /* + * Starts polling for the Aeron client. Will run registered client actions and will automatically start polling + * subscriptions + */ + void poll() { + info("ReactiveSocket Aeron Client poll"); + worker.schedulePeriodically(new PollingAction(subscriptionGroups, clientActions), + 0, 20, TimeUnit.MICROSECONDS); + } + + /* + * Inner Classes + */ + + /** + * Creates a logic group of {@link io.aeron.Subscription}s to a particular channel. + */ + public static class SubscriptionGroup { + + private final static ThreadLocal threadLocalFragmentAssembler = new ThreadLocal<>(); + private final String channel; + private final Subscription subscription; + private final FragmentHandler fragmentHandler; + + public SubscriptionGroup(String channel, Subscription subscription, FragmentHandler fragmentHandler) { + this.channel = channel; + this.subscription = subscription; + this.fragmentHandler = fragmentHandler; + } + + public String getChannel() { + return channel; + } + + public Subscription getSubscription() { + return subscription; + } + + public FragmentAssembler getFragmentAssembler() { + FragmentAssembler assembler = threadLocalFragmentAssembler.get(); + + if (assembler == null) { + assembler = new FragmentAssembler(fragmentHandler); + threadLocalFragmentAssembler.set(assembler); + } + + return assembler; + } + } + + @FunctionalInterface + public interface ClientAction { + void call(); + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/FrameHolder.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/FrameHolder.java new file mode 100644 index 000000000..6e5a2017c --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/FrameHolder.java @@ -0,0 +1,73 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.aeron.Publication; +import io.reactivesocket.Frame; +import org.HdrHistogram.Recorder; +import org.agrona.concurrent.OneToOneConcurrentArrayQueue; +import org.reactivestreams.Subscription; +/** + * Holds a frame and the publication that it's supposed to be sent on. + * Pools instances on an {@link OneToOneConcurrentArrayQueue} + */ +public class FrameHolder { + private static final ThreadLocal> FRAME_HOLDER_QUEUE + = ThreadLocal.withInitial(() -> new OneToOneConcurrentArrayQueue<>(16)); + + public static final Recorder histogram = new Recorder(3600000000000L, 3); + + private Frame frame; + private Publication publication; + private Subscription s; + private long getTime; + + private FrameHolder() {} + + public static FrameHolder get(Frame frame, Publication publication, Subscription s) { + FrameHolder frameHolder = FRAME_HOLDER_QUEUE.get().poll(); + + if (frameHolder == null) { + frameHolder = new FrameHolder(); + } + + frameHolder.frame = frame; + frameHolder.s = s; + frameHolder.publication = publication; + frameHolder.getTime = System.nanoTime(); + + return frameHolder; + } + + public Frame getFrame() { + return frame; + } + + public Publication getPublication() { + return publication; + } + + public void release() { + if (s != null) { + s.request(1); + } + + frame.release(); + FRAME_HOLDER_QUEUE.get().offer(this); + + histogram.recordValue(System.nanoTime() - getTime); + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/PollingAction.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/PollingAction.java new file mode 100644 index 000000000..2a4b8140a --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/client/PollingAction.java @@ -0,0 +1,60 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.aeron.Subscription; +import io.reactivesocket.aeron.internal.Loggable; +import rx.functions.Action0; + +import java.util.List; + +class PollingAction implements Action0, Loggable { + private final List subscriptionGroups; + private final List clientActions; + + public PollingAction( + List subscriptionGroups, + List clientActions) { + this.subscriptionGroups = subscriptionGroups; + this.clientActions = clientActions; + } + + @Override + public void call() { + try { + for (ClientAeronManager.SubscriptionGroup sg : subscriptionGroups) { + try { + int poll = 0; + do { + Subscription subscription = sg.getSubscription(); + if (!subscription.isClosed()) { + poll = subscription.poll(sg.getFragmentAssembler(), Integer.MAX_VALUE); + } + } while (poll > 0); + + for (ClientAeronManager.ClientAction action : clientActions) { + action.call(); + } + } catch (Throwable t) { + error("error polling aeron subscription", t); + } + } + + } catch (Throwable t) { + error("error in client polling loop", t); + } + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/AeronUtil.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/AeronUtil.java new file mode 100644 index 000000000..5088ca33b --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/AeronUtil.java @@ -0,0 +1,170 @@ +/** + * 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.reactivesocket.aeron.internal; + +import io.aeron.Publication; +import io.aeron.logbuffer.BufferClaim; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.OneToOneConcurrentArrayQueue; +import org.agrona.concurrent.UnsafeBuffer; + +import java.util.concurrent.TimeUnit; + +import static io.reactivesocket.aeron.internal.Constants.DEFAULT_OFFER_TO_AERON_TIMEOUT_MS; + +/** + * Utils for dealing with Aeron + */ +public class AeronUtil implements Loggable { + + private static final ThreadLocal bufferClaims = ThreadLocal.withInitial(BufferClaim::new); + + private static final ThreadLocal> unsafeBuffers + = ThreadLocal.withInitial(() -> new OneToOneConcurrentArrayQueue<>(16)); + + /** + * Sends a message using offer. This method will spin-lock if Aeron signals back pressure. + *

+ * This method of sending data does need to know how long the message is. + * + * @param publication publication to send the message on + * @param fillBuffer closure passed in to fill a {@link MutableDirectBuffer} + * that is send over Aeron + */ + public static void offer(Publication publication, BufferFiller fillBuffer, int length, int timeout, TimeUnit timeUnit) { + if (publication.isClosed()) { + throw new NotConnectedException(); + } + + final MutableDirectBuffer buffer = getDirectBuffer(length); + fillBuffer.fill(0, buffer); + final long start = System.nanoTime(); + do { + final long current = System.nanoTime(); + if ((current - start) > timeUnit.toNanos(timeout)) { + throw new TimedOutException(); + } + + final long offer = publication.offer(buffer); + if (offer >= 0) { + break; + } else if (Publication.NOT_CONNECTED == offer) { + throw new NotConnectedException(); + } + } while (true); + + recycleDirectBuffer(buffer); + } + + /** + * Sends a message using tryClaim. This method will spin-lock if Aeron signals back pressure. The message + * being sent needs to be equal or smaller than Aeron's MTU size or an exception will be thrown. + *

+ * In order to use this method of sending data you need to know the length of data. + * + * @param publication publication to send the message on + * @param fillBuffer closure passed in to fill a {@link MutableDirectBuffer} + * that is send over Aeron + * @param length the length of data + */ + public static void tryClaim(Publication publication, BufferFiller fillBuffer, int length, int timeout, TimeUnit timeUnit) { + if (publication.isClosed()) { + throw new NotConnectedException(); + } + + final BufferClaim bufferClaim = bufferClaims.get(); + final long start = System.nanoTime(); + do { + final long current = System.nanoTime(); + if ((current - start) > timeUnit.toNanos(timeout)) { + throw new TimedOutException(); + } + + final long offer = publication.tryClaim(length, bufferClaim); + if (offer >= 0) { + try { + final MutableDirectBuffer buffer = bufferClaim.buffer(); + final int offset = bufferClaim.offset(); + fillBuffer.fill(offset, buffer); + break; + } finally { + bufferClaim.commit(); + } + } else if (Publication.NOT_CONNECTED == offer) { + throw new NotConnectedException(); + } + } while (true); + } + + /** + * Attempts to send the data using tryClaim. If the message data length is large then the Aeron MTU + * size it will use offer instead. + * + * @param publication publication to send the message on + * @param fillBuffer closure passed in to fill a {@link MutableDirectBuffer} + * that is send over Aeron + * @param length the length of data + */ + public static void tryClaimOrOffer(Publication publication, BufferFiller fillBuffer, int length) { + tryClaimOrOffer(publication, fillBuffer, length, DEFAULT_OFFER_TO_AERON_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + public static void tryClaimOrOffer(Publication publication, BufferFiller fillBuffer, int length, int timeout, TimeUnit timeUnit) { + if (length < Constants.AERON_MTU_SIZE) { + tryClaim(publication, fillBuffer, length, timeout, timeUnit); + } else { + offer(publication, fillBuffer, length, timeout, timeUnit); + } + } + + + /** + * Try to get a MutableDirectBuffer from a thread-safe pool for a given length. If the buffer found + * is bigger then the buffer in the pool creates a new buffer. If no buffer is found creates a new buffer + * + * @param length the requested length + * @return either a new MutableDirectBuffer or a recycled one that has the capacity to hold the data from the old one + */ + public static MutableDirectBuffer getDirectBuffer(int length) { + OneToOneConcurrentArrayQueue queue = unsafeBuffers.get(); + MutableDirectBuffer buffer = queue.poll(); + + if (buffer != null && buffer.capacity() >= length) { + return buffer; + } else { + byte[] bytes = new byte[length]; + buffer = new UnsafeBuffer(bytes); + return buffer; + } + } + + /** + * Sends a DirectBuffer back to the thread pools to be recycled. + * + * @param directBuffer the DirectBuffer to recycle + */ + public static void recycleDirectBuffer(MutableDirectBuffer directBuffer) { + OneToOneConcurrentArrayQueue queue = unsafeBuffers.get(); + queue.offer(directBuffer); + } + + /** + * Implement this to fill a DirectBuffer passed in by either the offer or tryClaim methods. + */ + public interface BufferFiller { + void fill(int offset, MutableDirectBuffer buffer); + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Constants.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Constants.java new file mode 100644 index 000000000..4ba15f277 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Constants.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.reactivesocket.aeron.internal; + + +import org.agrona.concurrent.BackoffIdleStrategy; +import org.agrona.concurrent.IdleStrategy; +import org.agrona.concurrent.NoOpIdleStrategy; +import org.agrona.concurrent.SleepingIdleStrategy; + +import java.util.concurrent.TimeUnit; + +public final class Constants { + + public static final int SERVER_STREAM_ID = 1; + public static final int CLIENT_STREAM_ID = 2; + public static final byte[] EMTPY = new byte[0]; + public static final int QUEUE_SIZE = Integer.getInteger("reactivesocket.aeron.framesSendQueueSize", 262144); + public static final IdleStrategy SERVER_IDLE_STRATEGY; + public static final int AERON_MTU_SIZE = Integer.getInteger("aeron.mtu.length", 4096); + public static final boolean TRACING_ENABLED = Boolean.getBoolean("reactivesocket.aeron.tracingEnabled"); + public static final int CLIENT_ESTABLISH_CONNECT_TIMEOUT_MS = 6000; + public static final int CLIENT_SEND_ESTABLISH_CONNECTION_MSG_TIMEOUT_MS = 5000; + public static final int SERVER_ACK_ESTABLISH_CONNECTION_TIMEOUT_MS = 3000; + public static final int SERVER_ESTABLISH_CONNECTION_REQUEST_TIMEOUT_MS = 5000; + public static final int SERVER_TIMER_WHEEL_TICK_DURATION_MS = 10; + public static final int SERVER_TIMER_WHEEL_BUCKETS = 128; + public static final int DEFAULT_OFFER_TO_AERON_TIMEOUT_MS = 30_000; + public static final boolean CLIENT_EMBEDDED_AERON_DRIVER = Boolean.getBoolean("reactivesocket.aeron.clientEmbeddedDriver"); + + static { + String idlStrategy = System.getProperty("idleStrategy"); + + if (NoOpIdleStrategy.class.getName().equalsIgnoreCase(idlStrategy)) { + SERVER_IDLE_STRATEGY = new NoOpIdleStrategy(); + } else if (SleepingIdleStrategy.class.getName().equalsIgnoreCase(idlStrategy)) { + SERVER_IDLE_STRATEGY = new SleepingIdleStrategy(TimeUnit.MILLISECONDS.toNanos(250)); + } else { + SERVER_IDLE_STRATEGY = new BackoffIdleStrategy(1, 10, 100, 1000); + } + } + + private Constants() { + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Loggable.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Loggable.java new file mode 100644 index 000000000..7de001478 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/Loggable.java @@ -0,0 +1,53 @@ +/** + * 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.reactivesocket.aeron.internal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * No more needed to type Logger LOGGER = LoggerFactory.getLogger.... + */ +public interface Loggable { + + default void info(String message, Object... args) { + logger().info(message, args); + } + + default void error(String message, Throwable t) { + logger().error(message, t); + } + + default void debug(String message, Object... args) { + logger().debug(message, args); + } + + default void trace(String message, Object... args) { + logger().trace(message, args); + } + + default boolean isTraceEnabled() { + if (Constants.TRACING_ENABLED) { + return logger().isTraceEnabled(); + } else { + return false; + } + } + + default Logger logger() { + return LoggerFactory.getLogger(getClass()); + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/MessageType.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/MessageType.java new file mode 100644 index 000000000..c294d232d --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/MessageType.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.reactivesocket.aeron.internal; + +/** + * Type of message being sent. + */ +public enum MessageType { + ESTABLISH_CONNECTION_REQUEST(0x01), + ESTABLISH_CONNECTION_RESPONSE(0x02), + CONNECTION_DISCONNECT(0x3), + FRAME(0x04); + + private static MessageType[] typesById; + + /** + * Index types by id for indexed lookup. + */ + static { + int max = 0; + + for (MessageType t : values()) { + max = Math.max(t.id, max); + } + + typesById = new MessageType[max + 1]; + + for (MessageType t : values()) { + typesById[t.id] = t; + } + } + + private final int id; + + MessageType(int id) { + this.id = id; + } + + public int getEncodedType() { + return id; + } + + public static MessageType from(int id) { + return typesById[id]; + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/NotConnectedException.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/NotConnectedException.java new file mode 100644 index 000000000..ce87e7712 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/NotConnectedException.java @@ -0,0 +1,25 @@ +/** + * 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.reactivesocket.aeron.internal; + +public class NotConnectedException extends RuntimeException { + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/TimedOutException.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/TimedOutException.java new file mode 100644 index 000000000..67d2fbc00 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/internal/TimedOutException.java @@ -0,0 +1,24 @@ +/** + * 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.reactivesocket.aeron.internal; + +public class TimedOutException extends RuntimeException { + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/AeronServerDuplexConnection.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/AeronServerDuplexConnection.java new file mode 100644 index 000000000..5b68a0f9a --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/AeronServerDuplexConnection.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.reactivesocket.aeron.server; + +import io.aeron.Publication; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.aeron.internal.AeronUtil; +import io.reactivesocket.aeron.internal.Constants; +import io.reactivesocket.aeron.internal.Loggable; +import io.reactivesocket.aeron.internal.MessageType; +import io.reactivesocket.aeron.internal.NotConnectedException; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Disposable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.agrona.BitUtil; +import org.reactivestreams.Publisher; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +public class AeronServerDuplexConnection implements DuplexConnection, Loggable { + private final Publication publication; + private final CopyOnWriteArrayList> subjects; + private volatile boolean isClosed; + + public AeronServerDuplexConnection( + Publication publication) { + this.publication = publication; + this.subjects = new CopyOnWriteArrayList<>(); + } + + public List> getSubscriber() { + return subjects; + } + + @Override + public final Observable getInput() { + if (isTraceEnabled()) { + trace("-------getting input for publication session id {} ", publication.sessionId()); + } + + return new Observable() { + public void subscribe(Observer o) { + o.onSubscribe(new Disposable() { + @Override + public void dispose() { + if (isTraceEnabled()) { + trace("removing Observer for publication with session id {} ", publication.sessionId()); + } + + subjects.removeIf(s -> s == o); + } + }); + + subjects.add(o); + } + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o.subscribe(new ServerSubscription(publication, callback)); + } + + @Override + public double availability() { + return isClosed ? 0.0 : 1.0; + } + + // TODO - this is bad - I need to queue this up somewhere and process this on the polling thread so it doesn't just block everything + void ackEstablishConnection(int ackSessionId) { + debug("Acking establish connection for session id => {}", ackSessionId); + for (;;) { + try { + AeronUtil.tryClaimOrOffer(publication, (offset, buffer) -> { + buffer.putShort(offset, (short) 0); + buffer.putShort(offset + BitUtil.SIZE_OF_SHORT, (short) MessageType.ESTABLISH_CONNECTION_RESPONSE.getEncodedType()); + buffer.putInt(offset + BitUtil.SIZE_OF_INT, ackSessionId); + }, 2 * BitUtil.SIZE_OF_INT, Constants.SERVER_ACK_ESTABLISH_CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + debug("Ack sent for session i => {}", ackSessionId); + } catch (NotConnectedException ne) { + continue; + } + break; + } + } + + public boolean isClosed() { + return isClosed; + } + + @Override + public void close() { + isClosed = true; + try { + publication.close(); + } catch (Throwable t) {} + } + + public String toString() { + if (publication == null) { + return getClass().getName() + ":publication=null"; + } + + return getClass().getName() + ":publication=[" + + "channel=" + publication.channel() + "," + + "streamId=" + publication.streamId() + "," + + "sessionId=" + publication.sessionId() + "]"; + + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ReactiveSocketAeronServer.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ReactiveSocketAeronServer.java new file mode 100644 index 000000000..452158029 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ReactiveSocketAeronServer.java @@ -0,0 +1,212 @@ +/** + * 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.reactivesocket.aeron.server; + +import io.aeron.Aeron; +import io.aeron.FragmentAssembler; +import io.aeron.Image; +import io.aeron.Publication; +import io.aeron.Subscription; +import io.aeron.logbuffer.Header; +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Frame; +import io.reactivesocket.LeaseGovernor; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.aeron.internal.Loggable; +import io.reactivesocket.aeron.internal.MessageType; +import io.reactivesocket.rx.Observer; +import org.agrona.BitUtil; +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static io.reactivesocket.aeron.internal.Constants.CLIENT_STREAM_ID; +import static io.reactivesocket.aeron.internal.Constants.SERVER_ESTABLISH_CONNECTION_REQUEST_TIMEOUT_MS; +import static io.reactivesocket.aeron.internal.Constants.SERVER_STREAM_ID; + +public class ReactiveSocketAeronServer implements AutoCloseable, Loggable { + private static final UnsafeBuffer BUFFER = new UnsafeBuffer(ByteBuffer.allocate(0)); + private static final ServerAeronManager manager = ServerAeronManager.getInstance(); + private final int port; + private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); + private final ConcurrentHashMap sockets = new ConcurrentHashMap<>(); + private final Subscription subscription; + private final ConnectionSetupHandler connectionSetupHandler; + private final LeaseGovernor leaseGovernor; + + private ReactiveSocketAeronServer(String host, int port, ConnectionSetupHandler connectionSetupHandler, LeaseGovernor leaseGovernor) { + this.port = port; + this.connectionSetupHandler = connectionSetupHandler; + this.leaseGovernor = leaseGovernor; + + manager.addAvailableImageHander(this::availableImageHandler); + manager.addUnavailableImageHandler(this::unavailableImage); + + Aeron aeron = manager.getAeron(); + + final String serverChannel = "udp://" + host + ":" + port; + info("Starting new ReactiveSocketAeronServer on channel {}", serverChannel); + subscription = aeron.addSubscription(serverChannel, SERVER_STREAM_ID); + + FragmentAssembler fragmentAssembler = new FragmentAssembler(this::fragmentHandler); + manager.addSubscription(subscription, fragmentAssembler); + } + + /* + * Factory Methods + */ + public static ReactiveSocketAeronServer create(String host, int port, ConnectionSetupHandler connectionSetupHandler, LeaseGovernor leaseGovernor) { + return new ReactiveSocketAeronServer(host, port, connectionSetupHandler, leaseGovernor); + } + + public static ReactiveSocketAeronServer create(int port, ConnectionSetupHandler connectionSetupHandler, LeaseGovernor leaseGovernor) { + return create("127.0.0.1", port, connectionSetupHandler, leaseGovernor); + } + + public static ReactiveSocketAeronServer create(ConnectionSetupHandler connectionSetupHandler, LeaseGovernor leaseGovernor) { + return create(39790, connectionSetupHandler, leaseGovernor); + } + + public static ReactiveSocketAeronServer create(String host, int port, ConnectionSetupHandler connectionSetupHandler) { + return new ReactiveSocketAeronServer(host, port, connectionSetupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + public static ReactiveSocketAeronServer create(int port, ConnectionSetupHandler connectionSetupHandler) { + return create("localhost", port, connectionSetupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + public static ReactiveSocketAeronServer create(ConnectionSetupHandler connectionSetupHandler) { + return create(39790, connectionSetupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + void fragmentHandler(DirectBuffer buffer, int offset, int length, Header header) { + final int sessionId = header.sessionId(); + + short messageTypeInt = buffer.getShort(offset + BitUtil.SIZE_OF_SHORT); + MessageType type = MessageType.from(messageTypeInt); + + if (MessageType.FRAME == type) { + AeronServerDuplexConnection connection = connections.get(sessionId); + if (connection != null && !connection.isClosed()) { + List> subscribers = connection.getSubscriber(); + + ByteBuffer bb = ByteBuffer.allocate(length); + BUFFER.wrap(bb); + buffer.getBytes(offset, BUFFER, 0, length); + + final Frame frame = Frame.from(BUFFER, BitUtil.SIZE_OF_INT, length - BitUtil.SIZE_OF_INT); + + if (isTraceEnabled()) { + trace("server received frame payload {} on session id {}", frame.getData(), sessionId); + } + + subscribers.forEach(s -> { + try { + s.onNext(frame); + } catch (Throwable t) { + s.onError(t); + } + }); + } + } else if (MessageType.ESTABLISH_CONNECTION_REQUEST == type) { + final long start = System.nanoTime(); + AeronServerDuplexConnection connection = null; + debug("Looking for an AeronServerDuplexConnection connection to ack establish connection for session id => {}", sessionId); + while (connection == null) { + final long current = System.nanoTime(); + + if ((current - start) > TimeUnit.MILLISECONDS.toNanos(SERVER_ESTABLISH_CONNECTION_REQUEST_TIMEOUT_MS)) { + throw new RuntimeException("unable to find connection to ack establish connection for session id => " + sessionId); + } + + connection = connections.get(sessionId); + } + debug("Found a connection to ack establish connection for session id => {}", sessionId); + connection.ackEstablishConnection(sessionId); + } else if (MessageType.CONNECTION_DISCONNECT == type) { + closeReactiveSocket(sessionId); + } + + } + + void availableImageHandler(Image image) { + final int streamId = subscription.streamId(); + final int sessionId = image.sessionId(); + if (SERVER_STREAM_ID == streamId) { + debug("Handling new image for session id => {} and stream id => {}", streamId, sessionId); + final AeronServerDuplexConnection connection = connections.computeIfAbsent(sessionId, (_s) -> { + final String responseChannel = "udp://" + image.sourceIdentity().substring(0, image.sourceIdentity().indexOf(':')) + ":" + port; + Publication publication = manager.getAeron().addPublication(responseChannel, CLIENT_STREAM_ID); + int responseSessionId = publication.sessionId(); + debug("Creating new connection for responseChannel => {}, streamId => {}, and sessionId => {}", responseChannel, streamId, responseSessionId); + return new AeronServerDuplexConnection(publication); + }); + debug("Accepting ReactiveSocket connection"); + ReactiveSocket socket = DefaultReactiveSocket.fromServerConnection( + connection, + connectionSetupHandler, + leaseGovernor, + new Consumer() { + @Override + public void accept(Throwable throwable) { + error(String.format("Error creating ReactiveSocket for Aeron session id => %d and stream id => %d", streamId, sessionId), throwable); + } + }); + + sockets.put(sessionId, socket); + + socket.startAndWait(); + } else { + debug("Unsupported stream id {}", streamId); + } + } + + void unavailableImage(Image image) { + closeReactiveSocket(image.sessionId()); + } + + private void closeReactiveSocket(int sessionId) { + ServerAeronManager.getInstance().getTimerWheel().newTimeout(200, TimeUnit.MILLISECONDS, () -> { + debug("closing connection for session id => " + sessionId); + ReactiveSocket socket = sockets.remove(sessionId); + connections.remove(sessionId); + + if (socket != null) { + try { + socket.close(); + } catch (Throwable t) { + error("error closing socket for session id => " + sessionId, t); + } + } + }); + } + + public boolean hasConnections() { + return !connections.isEmpty(); + } + + @Override + public void close() throws Exception { + manager.removeSubscription(subscription); + } + +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerAeronManager.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerAeronManager.java new file mode 100644 index 000000000..bfec5d023 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerAeronManager.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.reactivesocket.aeron.server; + +import io.aeron.Aeron; +import io.aeron.AvailableImageHandler; +import io.aeron.FragmentAssembler; +import io.aeron.Image; +import io.aeron.Subscription; +import io.aeron.UnavailableImageHandler; +import io.reactivesocket.aeron.internal.Constants; +import io.reactivesocket.aeron.internal.Loggable; +import org.agrona.TimerWheel; +import org.agrona.concurrent.ManyToOneConcurrentArrayQueue; +import rx.Observable; +import rx.Scheduler; +import rx.Single; +import rx.functions.Action0; +import rx.functions.Func0; +import rx.schedulers.Schedulers; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import static io.reactivesocket.aeron.internal.Constants.SERVER_IDLE_STRATEGY; + +/** + * Class that manages the Aeron instance and the server's polling thread. Lets you register more + * than one NewImageHandler to Aeron after the it's the Aeron instance has started + */ +public class ServerAeronManager implements Loggable { + private static final ServerAeronManager INSTANCE = new ServerAeronManager(); + + private final Aeron aeron; + + private final CopyOnWriteArrayList availableImageHandlers = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList unavailableImageHandlers = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList fragmentAssemblerHolders = new CopyOnWriteArrayList<>(); + + private final ManyToOneConcurrentArrayQueue actions = new ManyToOneConcurrentArrayQueue<>(1024); + + private final TimerWheel timerWheel; + + private final Thread dutyThread; + + private ServerAeronManager() { + final Aeron.Context ctx = new Aeron.Context(); + ctx.availableImageHandler(this::availableImageHandler); + ctx.unavailableImageHandler(this::unavailableImage); + ctx.errorHandler(t -> error("an exception occurred", t)); + + aeron = Aeron.connect(ctx); + + this.timerWheel = new TimerWheel(Constants.SERVER_TIMER_WHEEL_TICK_DURATION_MS, TimeUnit.MILLISECONDS, Constants.SERVER_TIMER_WHEEL_BUCKETS); + + dutyThread = new Thread(() -> { + for (; ; ) { + try { + int poll = 0; + for (FragmentAssemblerHolder sh : fragmentAssemblerHolders) { + try { + if (sh.subscription.isClosed()) { + continue; + } + + poll += sh.subscription.poll(sh.fragmentAssembler, Integer.MAX_VALUE); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + poll += actions.drain(Action0::call); + + if (timerWheel.computeDelayInMs() < 0) { + poll += timerWheel.expireTimers(); + } + + SERVER_IDLE_STRATEGY.idle(poll); + + } catch (Throwable t) { + t.printStackTrace(); + } + + } + + }); + dutyThread.setName("reactive-socket-aeron-server"); + dutyThread.setDaemon(true); + dutyThread.start(); + } + + public static ServerAeronManager getInstance() { + return INSTANCE; + } + + public void addAvailableImageHander(AvailableImageHandler handler) { + availableImageHandlers.add(handler); + } + + public void addUnavailableImageHandler(UnavailableImageHandler handler) { + unavailableImageHandlers.add(handler); + } + + public void addSubscription(Subscription subscription, FragmentAssembler fragmentAssembler) { + debug("Adding subscription with session id {}", subscription.streamId()); + fragmentAssemblerHolders.add(new FragmentAssemblerHolder(subscription, fragmentAssembler)); + } + + public void removeSubscription(Subscription subscription) { + debug("Removing subscription with session id {}", subscription.streamId()); + fragmentAssemblerHolders.removeIf(s -> s.subscription == subscription); + } + + private void availableImageHandler(Image image) { + availableImageHandlers + .forEach(handler -> handler.onAvailableImage(image)); + } + + private void unavailableImage(Image image) { + unavailableImageHandlers + .forEach(handler -> handler.onUnavailableImage(image)); + } + + public Aeron getAeron() { + return aeron; + } + + public TimerWheel getTimerWheel() { + return timerWheel; + } + + /** + * Submits an Action0 to be run but the duty thread. + * @param action the action to be executed + * @return true if it was successfully submitted + */ + public boolean submitAction(Action0 action) { + boolean submitted = true; + Thread currentThread = Thread.currentThread(); + if (currentThread.equals(dutyThread)) { + action.call(); + } else { + submitted = actions.offer(action); + } + + return submitted; + } + + /** + * Submits a task that is implemeted as a {@link Func0} that runs on the + * server polling thread and returns an {@link Single} + * @param task task to the run + * @param expected return type + * @return an {@link Single} of type R + */ + public Single submitTask(Func0 task) { + return Single.create(s -> + submitAction(() -> { + try { + s.onSuccess(task.call()); + } catch (Throwable t) { + s.onError(t); + } + }) + ); + } + + /** + * + * @param tasks + * @param + * @return + */ + public Observable submitTasks(Observable> tasks) { + return submitTasks(tasks, Schedulers.computation()); + } + + /** + * Submits an observable of tasks to be run on a specific scheduler + * @param tasks + * @param scheduler + * @param + * @return + */ + public Observable submitTasks(Observable> tasks, Scheduler scheduler) { + return tasks + .observeOn(scheduler, true) + .concatMap(task -> submitTask(task).toObservable()); + } + + /** + * Schedules timeout on the TimerWheel in a thread-safe manner + * @param delayTime + * @param unit + * @param action + * @return true if it was successfully scheduled, otherwise false. + */ + public boolean threadSafeTimeout(long delayTime, TimeUnit unit, Action0 action) { + boolean scheduled = true; + Thread currentThread = Thread.currentThread(); + if (currentThread.equals(dutyThread)) { + timerWheel.newTimeout(delayTime, unit, action::call); + } else { + scheduled = actions.offer(() -> timerWheel.newTimeout(delayTime, unit, action::call)); + } + + return scheduled; + } + + private class FragmentAssemblerHolder { + private Subscription subscription; + private FragmentAssembler fragmentAssembler; + + public FragmentAssemblerHolder(Subscription subscription, FragmentAssembler fragmentAssembler) { + this.subscription = subscription; + this.fragmentAssembler = fragmentAssembler; + } + } +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerSubscription.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerSubscription.java new file mode 100644 index 000000000..cd8f2bb12 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/ServerSubscription.java @@ -0,0 +1,101 @@ +/** + * 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.reactivesocket.aeron.server; + +import io.aeron.Publication; +import io.reactivesocket.Frame; +import io.reactivesocket.aeron.internal.AeronUtil; +import io.reactivesocket.aeron.internal.Loggable; +import io.reactivesocket.aeron.internal.MessageType; +import io.reactivesocket.rx.Completable; +import org.agrona.BitUtil; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.nio.ByteBuffer; + +/** + * Subscription used by the AeronServerDuplexConnection to handle incoming frames and send them + * on a publication. + * + * @see AeronServerDuplexConnection + */ +class ServerSubscription implements Subscriber, Loggable { + + /** + * Count is used to by the client to round-robin request between threads. + */ + private short count; + + private final Publication publication; + + private final Completable completable; + + public ServerSubscription(Publication publication, Completable completable) { + this.publication = publication; + this.completable = completable; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + + if (isTraceEnabled()) { + trace("Server with publication session id {} sending frame => {}", publication.sessionId(), frame.toString()); + } + + final ByteBuffer byteBuffer = frame.getByteBuffer(); + final int length = frame.length() + BitUtil.SIZE_OF_INT; + + try { + AeronUtil.tryClaimOrOffer(publication, (offset, buffer) -> { + buffer.putShort(offset, getCount()); + buffer.putShort(offset + BitUtil.SIZE_OF_SHORT, (short) MessageType.FRAME.getEncodedType()); + buffer.putBytes(offset + BitUtil.SIZE_OF_INT, byteBuffer, frame.offset(), frame.length()); + }, length); + } catch (Throwable t) { + onError(t); + } + + if (isTraceEnabled()) { + trace("Server with publication session id {} sent frame with ReactiveSocket stream id => {}", publication.sessionId(), frame.getStreamId()); + } + + + } + + @Override + public void onError(Throwable t) { + completable.error(t); + } + + @Override + public void onComplete() { + if (isTraceEnabled()) { + trace("Server with publication session id {} completing", publication.sessionId()); + } + completable.success(); + } + + private short getCount() { + return count++; + } + +} diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/TimerWheelFairLeaseGovernor.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/TimerWheelFairLeaseGovernor.java new file mode 100644 index 000000000..98d6ad4d2 --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/TimerWheelFairLeaseGovernor.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.reactivesocket.aeron.server; + +import io.reactivesocket.Frame; +import io.reactivesocket.LeaseGovernor; +import io.reactivesocket.internal.Responder; +import org.agrona.TimerWheel; +import org.agrona.collections.Int2IntHashMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Lease Governor that evenly distributes requests all connected clients. The work is done using the + * {@link ServerAeronManager}'s {@link TimerWheel} + */ +public class TimerWheelFairLeaseGovernor implements LeaseGovernor, Runnable { + private final int tickets; + private final long period; + private final int ttlMs; + private final TimeUnit unit; + private final TimerWheel.Timer timer; + private final List responders; + private final Int2IntHashMap leaseCount; + + private boolean running = false; + + private int ticketsPerResponder = 0; + + private int extra = 0; + + public TimerWheelFairLeaseGovernor(int tickets, long period, TimeUnit unit) { + this.responders = new ArrayList<>(); + this.leaseCount = new Int2IntHashMap(0); + this.tickets = tickets; + this.period = period; + this.unit = unit; + this.ttlMs = (int) unit.toMillis(period); + this.timer = ServerAeronManager + .getInstance() + .getTimerWheel() + .newBlankTimer(); + } + + @Override + public void run() { + if (running) { + try { + final int numResponders = responders.size(); + if (numResponders > 0) { + int extraTicketsLeft = extra; + + for (int i = 0; i < numResponders; i++) { + int amountToSend = ticketsPerResponder; + if (extraTicketsLeft > 0) { + amountToSend++; + extraTicketsLeft--; + } + Responder responder = responders.get(i); + leaseCount.put(responder.hashCode(), amountToSend); + responder.sendLease(ttlMs, amountToSend); + } + + } + } finally { + ServerAeronManager + .getInstance() + .getTimerWheel() + .rescheduleTimeout(period, unit, timer, this::run); + } + } + } + + @Override + public void register(Responder responder) { + ServerAeronManager.getInstance().submitAction(() -> { + responders.add(responder); + + calculateTicketsToSendPerResponder(); + + if (!running) { + running = true; + run(); + } + }); + } + + @Override + public void unregister(Responder responder) { + ServerAeronManager.getInstance().submitAction(() -> { + responders.remove(responder); + + calculateTicketsToSendPerResponder(); + + if (running && responders.isEmpty()) { + running = false; + } + }); + } + + void calculateTicketsToSendPerResponder() { + int size = this.responders.size(); + if (size > 0) { + ticketsPerResponder = tickets / size; + extra = tickets - ticketsPerResponder * size; + } + } + + @Override + public boolean accept(Responder responder, Frame frame) { + int count = leaseCount.get(responder.hashCode()) - 1; + + if (count >= 0) { + leaseCount.put(responder.hashCode(), count); + } + + return count > 0; + + } +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronScheduler.java b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronScheduler.java new file mode 100644 index 000000000..9af4546ed --- /dev/null +++ b/reactivesocket-transport-aeron/src/main/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronScheduler.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.reactivesocket.aeron.server.rx; + +import io.reactivesocket.aeron.server.ServerAeronManager; +import rx.Scheduler; +import rx.Subscription; +import rx.functions.Action0; + +import java.util.concurrent.TimeUnit; + +/** + * An implementation of {@link Scheduler} that lets you schedule work on the {@link ServerAeronManager} polling thread. + * The work is scheduled on to the thread use a {@link org.agrona.TimerWheel}. This is useful if you have done work on another + * thread, and than want the work to end up back on the polling thread. + */ +public class ReactiveSocketAeronScheduler extends Scheduler { + private static final ReactiveSocketAeronScheduler instance = new ReactiveSocketAeronScheduler(); + + private ReactiveSocketAeronScheduler() {} + + public static ReactiveSocketAeronScheduler getInstance() { + return instance; + } + + @Override + public Worker createWorker() { + return new Worker(); + } + + static class Worker extends Scheduler.Worker { + private volatile boolean subscribed = true; + + @Override + public Subscription schedule(Action0 action) { + boolean submitted; + do { + submitted = ServerAeronManager.getInstance().submitAction(action); + } while (!submitted); + + return this; + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + boolean scheduled; + do { + scheduled = ServerAeronManager.getInstance().threadSafeTimeout(delayTime, unit, action); + } while (!scheduled); + + return this; + } + + @Override + public void unsubscribe() { + subscribed = false; + } + + @Override + public boolean isUnsubscribed() { + return !subscribed; + } + } +} diff --git a/reactivesocket-transport-aeron/src/perf/java/io/aeron/DummySubscription.java b/reactivesocket-transport-aeron/src/perf/java/io/aeron/DummySubscription.java new file mode 100644 index 000000000..8a3ea7135 --- /dev/null +++ b/reactivesocket-transport-aeron/src/perf/java/io/aeron/DummySubscription.java @@ -0,0 +1,73 @@ +/** + * 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.aeron; + + +import io.aeron.logbuffer.BlockHandler; +import io.aeron.logbuffer.FileBlockHandler; +import io.aeron.logbuffer.FragmentHandler; + +import java.util.List; + +public class DummySubscription extends Subscription { + DummySubscription(ClientConductor conductor, String channel, int streamId, long registrationId) { + super(conductor, channel, streamId, registrationId); + } + + public DummySubscription() { + super(null, null, 0, 0); + } + + @Override + public String channel() { + return ""; + } + + @Override + public int streamId() { + return 0; + } + + @Override + public int poll(FragmentHandler fragmentHandler, int fragmentLimit) { + return 0; + } + + @Override + public long blockPoll(BlockHandler blockHandler, int blockLengthLimit) { + return 0; + } + + @Override + public long filePoll(FileBlockHandler fileBlockHandler, int blockLengthLimit) { + return 0; + } + + @Override + public Image getImage(int sessionId) { + return null; + } + + @Override + public List images() { + return null; + } + + @Override + public void close() { + + } +} diff --git a/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/client/PollingActionPerf.java b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/client/PollingActionPerf.java new file mode 100644 index 000000000..661d08358 --- /dev/null +++ b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/client/PollingActionPerf.java @@ -0,0 +1,95 @@ +/** + * 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.reactivesocket.aeron.client; + + +public class PollingActionPerf { +/* + @State(Scope.Benchmark) + public static class TestState { + PollingAction pa; + + AtomicLong counter = new AtomicLong(); + + @Setup + public void init() { + ClientAeronManager.SubscriptionGroup sg + = new ClientAeronManager + .SubscriptionGroup("foo", + new Subscription[]{new DummySubscription()}, new Func1() { + @Override + public ClientAeronManager.ThreadIdAwareFragmentHandler call(Integer integer) { + return new ClientAeronManager.ThreadIdAwareFragmentHandler(0) { + @Override + public void onFragment(DirectBuffer buffer, int offset, int length, Header header) { + counter.getAndIncrement(); + } + }; + } + }); + + // 5 connections .... + for (int i = 0; i < 5; i++) { + sg.getClientActions().add(new ClientAeronManager.ClientAction(0) { + @Override + void call(int threadId) { + counter.getAndIncrement(); + } + + @Override + public void close() throws Exception { + + } + }); + } + + List group = new CopyOnWriteArrayList<>(); + group.add(sg); + + pa = new PollingAction(0, new ReentrantLock(), new ReentrantLock(), group); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(1) + public void call1(TestState state) { + state.pa.call(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(2) + public void call2(TestState state) { + state.pa.call(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(3) + public void call3(TestState state) { + state.pa.call(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(4) + public void call4(TestState state) { + state.pa.call(); + } + */ + +} diff --git a/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/InputWithIncrementingInteger.java b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/InputWithIncrementingInteger.java new file mode 100644 index 000000000..bd7cfbf04 --- /dev/null +++ b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/InputWithIncrementingInteger.java @@ -0,0 +1,144 @@ +/** + * 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.reactivesocket.aeron.jmh; +/** + * Copyright 2014 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. + */ + +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.infra.Blackhole; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscriber; + +import java.util.Iterator; + +/** + * Exposes an Observable and Observer that increments n Integers and consumes them in a Blackhole. + */ +public abstract class InputWithIncrementingInteger { + public Iterable iterable; + public Observable observable; + public Observable firehose; + public Blackhole bh; + public Observer observer; + + public abstract int getSize(); + + @Setup + public void setup(final Blackhole bh) { + this.bh = bh; + observable = Observable.range(0, getSize()); + + firehose = Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber s) { + for (int i = 0; i < getSize(); i++) { + s.onNext(i); + } + s.onCompleted(); + } + + }); + + iterable = new Iterable() { + + @Override + public Iterator iterator() { + return new Iterator() { + + int i = 0; + + @Override + public boolean hasNext() { + return i < getSize(); + } + + @Override + public Integer next() { + return i++; + } + + @Override + public void remove() { + + } + + }; + } + + }; + observer = new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + bh.consume(t); + } + + }; + + } + + public LatchedObserver newLatchedObserver() { + return new LatchedObserver(bh); + } + + public Subscriber newSubscriber() { + return new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + bh.consume(t); + } + + }; + } + +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/LatchedObserver.java b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/LatchedObserver.java new file mode 100644 index 000000000..a2f89c239 --- /dev/null +++ b/reactivesocket-transport-aeron/src/perf/java/io/reactivesocket/aeron/jmh/LatchedObserver.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.reactivesocket.aeron.jmh; + +/** + * Copyright 2014 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. + */ + +import org.openjdk.jmh.infra.Blackhole; +import rx.Observer; + +import java.util.concurrent.CountDownLatch; + +public class LatchedObserver implements Observer { + + public CountDownLatch latch = new CountDownLatch(1); + private final Blackhole bh; + + public LatchedObserver(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onCompleted() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onNext(T t) { + bh.consume(t); + } + +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/client/ReactiveSocketAeronTest.java b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/client/ReactiveSocketAeronTest.java new file mode 100644 index 000000000..4ff58a8af --- /dev/null +++ b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/client/ReactiveSocketAeronTest.java @@ -0,0 +1,951 @@ +/** + * 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.reactivesocket.aeron.client; + +import io.aeron.driver.MediaDriver; +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Frame; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.aeron.server.ReactiveSocketAeronServer; +import io.reactivesocket.exceptions.SetupException; +import io.reactivesocket.test.TestUtil; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscriber; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Function; + +/** + * Aeron integration tests + */ +@Ignore +public class ReactiveSocketAeronTest { + static { + // Uncomment to enable tracing + //System.setProperty("reactivesocket.aeron.tracingEnabled", "true"); + } + + @BeforeClass + public static void init() { + + final MediaDriver.Context context = new MediaDriver.Context(); + context.dirsDeleteOnStart(true); + final MediaDriver mediaDriver = MediaDriver.launch(context); + + } + + @Test(timeout = 3000) + public void testRequestReponse1() throws Exception { + requestResponseN(1); + } + + @Test(timeout = 3000) + public void testRequestReponse10() throws Exception { + requestResponseN(10); + } + + @Test(timeout = 30_000) + public void testRequestReponse10_000() throws Exception { + requestResponseN(10_000); + } + + @Test(timeout = 120_000) + public void testRequestReponse100_000() throws Exception { + requestResponseN(100_000); + } + + @Test(timeout = 120_000) + public void testRequestReponse1_000_000() throws Exception { + requestResponseN(1_000_000); + } + + @Test(timeout = 10_000) + public void testRequestStream1() throws Exception { + requestStreamN(1); + } + + @Test(timeout = 10_000) + public void testRequestStream10() throws Exception { + requestStreamN(10); + } + + @Test(timeout = 30_000) + public void testRequestStream10_000() throws Exception { + requestStreamN(10_000); + } + + @Test(timeout = 120_000) + public void testRequestStream100_000() throws Exception { + requestStreamN(100_000); + } + + public void requestResponseN(int count) throws Exception { + AtomicLong counter = new AtomicLong(); + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket rs) throws SetupException { + return new RequestHandler.Builder() + .withRequestResponse(new Function>() { + Frame frame = Frame.from(ByteBuffer.allocate(1)); + @Override + public Publisher apply(Payload payload) { + counter.incrementAndGet(); + ByteBuffer data = payload.getData(); + String s = TestUtil.byteToString(data); + String m = TestUtil.byteToString(payload.getMetadata()); + + try { + Assert.assertEquals(s, "client_request"); + Assert.assertEquals(m, "client_metadata"); + } catch (Throwable t) { + long l = counter.get(); + System.out.println("Count => " + l); + System.out.println("contains $ => " + s.contains("$")); + throw new RuntimeException(t); + } + + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("server_response", "server_metadata")); + return RxReactiveStreams.toPublisher(pong); + } + }).build(); + } + }); + + InetSocketAddress listenAddress = new InetSocketAddress("localhost", 39790); + InetSocketAddress clientAddress = new InetSocketAddress("localhost", 39790); + + AeronClientDuplexConnectionFactory cf = AeronClientDuplexConnectionFactory.getInstance(); + cf.addSocketAddressToHandleResponses(listenAddress); + Publisher udpConnection = cf.createUDPConnection(clientAddress); + + System.out.println("Creating new duplex connection"); + AeronClientDuplexConnection connection = RxReactiveStreams.toObservable(udpConnection).toBlocking().single(); + System.out.println("Created duplex connection"); + + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, ConnectionSetupPayload.create("UTF-8", "UTF-8", ConnectionSetupPayload.NO_FLAGS)); + reactiveSocket.startAndWait(); + + CountDownLatch latch = new CountDownLatch(count); + + Observable + .range(1, count) + .flatMap(i -> { + Payload payload = TestUtil.utf8EncodedPayload("client_request", "client_metadata"); + Publisher publisher = reactiveSocket.requestResponse(payload); + return RxReactiveStreams + .toObservable(publisher) + .doOnNext(resPayload -> { + ByteBuffer data = resPayload.getData(); + String s = TestUtil.byteToString(data); + String m = TestUtil.byteToString(resPayload.getMetadata()); + Assert.assertEquals(s, "server_response"); + Assert.assertEquals(m, "server_metadata"); + }) + .doOnNext(f -> latch.countDown()); + }, 8) + .subscribeOn(Schedulers.computation()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + System.out.println("I HAVE COMPLETED $$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + } + + @Override + public void onError(Throwable e) { + System.out.println(Thread.currentThread() + " counted to => " + latch.getCount()); + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + } + }); + + latch.await(); + } + + public void requestStreamN(int count) throws Exception { + ReactiveSocketAeronServer.create((setupPayload, rs) -> + new RequestHandler.Builder() + .withRequestStream(payload -> { + ByteBuffer data = payload.getData(); + String s = TestUtil.byteToString(data); + String m = TestUtil.byteToString(payload.getMetadata()); + + try { + Assert.assertEquals(s, "client_request"); + Assert.assertEquals(m, "client_metadata"); + } catch (Throwable t) { + System.out.println("contains $ => " + s.contains("$")); + throw new RuntimeException(t); + } + + Observable payloadObservable = Observable.range(1, count) + .map(i -> TestUtil.utf8EncodedPayload("server_response", "server_metadata")); + return RxReactiveStreams.toPublisher(payloadObservable); + }).build()); + + InetSocketAddress listenAddress = new InetSocketAddress("localhost", 39790); + InetSocketAddress clientAddress = new InetSocketAddress("localhost", 39790); + + AeronClientDuplexConnectionFactory cf = AeronClientDuplexConnectionFactory.getInstance(); + cf.addSocketAddressToHandleResponses(listenAddress); + Publisher udpConnection = cf.createUDPConnection(clientAddress); + + System.out.println("Creating new duplex connection"); + AeronClientDuplexConnection connection = RxReactiveStreams.toObservable(udpConnection).toBlocking().single(); + System.out.println("Created duplex connection"); + + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, ConnectionSetupPayload.create("UTF-8", "UTF-8", ConnectionSetupPayload.NO_FLAGS)); + reactiveSocket.startAndWait(); + + CountDownLatch latch = new CountDownLatch(count); + Payload payload = TestUtil.utf8EncodedPayload("client_request", "client_metadata"); + RxReactiveStreams.toObservable(reactiveSocket.requestStream(payload)) + .subscribeOn(Schedulers.computation()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + System.out.println("I HAVE COMPLETED $$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + } + + @Override + public void onError(Throwable e) { + System.out.println(Thread.currentThread() + " counted to => " + latch.getCount()); + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + ByteBuffer data = payload.getData(); + String s = TestUtil.byteToString(data); + String m = TestUtil.byteToString(payload.getMetadata()); + Assert.assertEquals(s, "server_response"); + Assert.assertEquals(m, "server_metadata"); + latch.countDown(); + } + }); + latch.await(); + } + + + @Test(timeout = 75000) + public void testReconnection() throws Exception { + System.out.println("--------------------------------------------------------------------------------"); + + ReactiveSocketAeronServer server = ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket rs) throws SetupException { + return new RequestHandler() { + Frame frame = Frame.from(ByteBuffer.allocate(1)); + + @Override + public Publisher handleRequestResponse(Payload payload) { + String request = TestUtil.byteToString(payload.getData()); + System.out.println(Thread.currentThread() + " Server got => " + request); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong", null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initial, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + System.out.println("--------------------------------------------------------------------------------"); + + + InetSocketAddress listenAddress = new InetSocketAddress("localhost", 39790); + InetSocketAddress clientAddress = new InetSocketAddress("localhost", 39790); + + AeronClientDuplexConnectionFactory cf = AeronClientDuplexConnectionFactory.getInstance(); + cf.addSocketAddressToHandleResponses(listenAddress); + + int j; + for (j = 0; j < 30; j++) { + CountDownLatch latch = new CountDownLatch(10); + + Publisher udpConnection = cf.createUDPConnection(clientAddress); + + System.out.println("Creating new duplex connection => " + j); + AeronClientDuplexConnection connection = RxReactiveStreams.toObservable(udpConnection).toBlocking().single(); + System.out.println("Created duplex connection => " + j); + + ReactiveSocket client = DefaultReactiveSocket.fromClientConnection(connection, ConnectionSetupPayload.create("UTF-8", "UTF-8", ConnectionSetupPayload.NO_FLAGS)); + client.startAndWait(); + + Observable + .range(1, 10) + .flatMap(i -> { + Payload payload = TestUtil.utf8EncodedPayload("ping =>" + i, null); + return RxReactiveStreams.toObservable(client.requestResponse(payload)); + } + ) + .doOnNext(p -> { + Assert.assertEquals("pong", TestUtil.byteToString(p.getData())); + }) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + System.out.println("I HAVE COMPLETED $$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + } + + @Override + public void onError(Throwable e) { + System.out.println(Thread.currentThread() + " counted to => " + latch.getCount()); + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + System.out.println(Thread.currentThread() + " countdown => " + latch.getCount()); + latch.countDown(); + } + }); + + latch.await(); + + + client.close(); + + while (server.hasConnections()) { + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); + } + + System.out.println("--------------------------------------------------------------------------------"); + + } + + Assert.assertEquals(j, 30); + + System.out.println("+++ GOT HERE"); + } + +/* + + + @Test(timeout = 100000) + public void testRequestReponse() throws Exception { + AtomicLong server = new AtomicLong(); + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + Frame frame = Frame.from(ByteBuffer.allocate(1)); + + @Override + public Publisher handleRequestResponse(Payload payload) { + String request = TestUtil.byteToString(payload.getData()); + //System.out.println(Thread.currentThread() + " Server got => " + request); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong => " + server.incrementAndGet(), null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + CountDownLatch latch = new CountDownLatch(1300000); + + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + Observable + .range(1, 1300000) + .flatMap(i -> { + //System.out.println("pinging => " + i); + Payload payload = TestUtil.utf8EncodedPayload("ping =>" + i, null); + return RxReactiveStreams + .toObservable(client.requestResponse(payload)) + .doOnNext(f -> { + if (i % 1000 == 0) { + System.out.println("Got => " + i); + } + }) + .doOnNext(f -> latch.countDown()); + } + ) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + System.out.println("I HAVE COMPLETED $$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + } + + @Override + public void onError(Throwable e) { + System.out.println(Thread.currentThread() + " counted to => " + latch.getCount()); + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + //System.out.println(Thread.currentThread() + " countdown => " + latch.getCount()); + //latch.countDown(); + } + }); + + latch.await(); + } + + @Test(timeout = 100000) + public void testRequestReponseMultiThreaded() throws Exception { + AtomicLong server = new AtomicLong(); + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + Frame frame = Frame.from(ByteBuffer.allocate(1)); + + @Override + public Publisher handleRequestResponse(Payload payload) { + String request = TestUtil.byteToString(payload.getData()); + //System.out.println(Thread.currentThread() + " Server got => " + request); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong => " + server.incrementAndGet(), null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + CountDownLatch latch = new CountDownLatch(10_000); + + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + Observable + .range(1, 10_000) + .flatMap(i -> { + //System.out.println("pinging => " + i); + Payload payload = TestUtil.utf8EncodedPayload("ping =>" + i, null); + return RxReactiveStreams + .toObservable(client.requestResponse(payload)) + .doOnNext(f -> { + if (i % 1000 == 0) { + System.out.println("Got => " + i); + } + }) + .doOnNext(f -> latch.countDown()) + .subscribeOn(Schedulers.newThread()); + } + , 8) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + System.out.println("I HAVE COMPLETED $$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + } + + @Override + public void onError(Throwable e) { + System.out.println(Thread.currentThread() + " counted to => " + latch.getCount()); + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + //System.out.println(Thread.currentThread() + " countdown => " + latch.getCount()); + //latch.countDown(); + } + }); + + latch.await(); + } + + @Test(timeout = 10000) + public void sendLargeMessage() throws Exception { + + Random random = new Random(); + byte[] b = new byte[1_000_000]; + random.nextBytes(b); + + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + System.out.println("Server got => " + b.length); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong", null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + CountDownLatch latch = new CountDownLatch(2); + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + Observable + .range(1, 2) + .flatMap(i -> { + System.out.println("pinging => " + i); + Payload payload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(b); + } + + @Override + public ByteBuffer getMetadata() { + return ByteBuffer.allocate(0); + } + }; + return RxReactiveStreams.toObservable(client.requestResponse(payload)); + } + ) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + System.out.println(s + "countdown => " + latch.getCount()); + latch.countDown(); + } + }); + + latch.await(); + } + + + @Test(timeout = 10000) + public void createTwoServersAndTwoClients()throws Exception { + Random random = new Random(); + byte[] b = new byte[1]; + random.nextBytes(b); + + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + System.out.println("pong 1 => " + payload.getData()); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong server 1", null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + ReactiveSocketAeronServer.create(12345, new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + System.out.println("pong 2 => " + payload.getData()); + Observable pong = Observable.just(TestUtil.utf8EncodedPayload("pong server 2", null)); + return RxReactiveStreams.toPublisher(pong); + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + int count = 64; + + CountDownLatch latch = new CountDownLatch(2 * count); + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + ReactiveSocketAeronClient client2 = ReactiveSocketAeronClient.create("localhost", "localhost", 12345); + + Observable + .range(1, count) + .flatMap(i -> { + System.out.println("pinging server 1 => " + i); + Payload payload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(b); + } + + @Override + public ByteBuffer getMetadata() { + return ByteBuffer.allocate(0); + } + }; + + return RxReactiveStreams.toObservable(client.requestResponse(payload)); + } + ) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + latch.countDown(); + System.out.println(s + " countdown server 1 => " + latch.getCount()); + } + }); + + Observable + .range(1, count) + .flatMap(i -> { + System.out.println("pinging server 2 => " + i); + Payload payload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(b); + } + + @Override + public ByteBuffer getMetadata() { + return ByteBuffer.allocate(0); + } + }; + + return RxReactiveStreams.toObservable(client2.requestResponse(payload)); + } + ) + .subscribe(new rx.Subscriber() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload s) { + latch.countDown(); + System.out.println(s + " countdown server 2 => " + latch.getCount()); + } + }); + + latch.await(); + } + + @Test(timeout = 10000) + public void testFireAndForget() throws Exception { + CountDownLatch latch = new CountDownLatch(130); + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + + @Override + public Publisher handleRequestResponse(Payload payload) { + return null; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return null; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + latch.countDown(); + s.onComplete(); + } + }; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + Observable + .range(1, 130) + .flatMap(i -> { + System.out.println("pinging => " + i); + Payload payload = TestUtil.utf8EncodedPayload("ping =>" + i, null); + return RxReactiveStreams.toObservable(client.fireAndForget(payload)); + } + ) + .subscribe(); + + latch.await(); + } + + @Test(timeout = 10000) + public void testRequestStream() throws Exception { + CountDownLatch latch = new CountDownLatch(130); + ReactiveSocketAeronServer.create(new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { + return new RequestHandler() { + + @Override + public Publisher handleRequestResponse(Payload payload) { + return null; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher payloads) { + return null; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + for (int i = 0; i < 1_000_000; i++) { + s.onNext(new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.allocate(0); + } + + @Override + public ByteBuffer getMetadata() { + return ByteBuffer.allocate(0); + } + }); + } + + } + }; + } + + @Override + public Publisher handleSubscription(Payload payload) { + return null; + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + ReactiveSocketAeronClient client = ReactiveSocketAeronClient.create("localhost", "localhost"); + + Observable + .range(1, 1) + .flatMap(i -> { + System.out.println("pinging => " + i); + Payload payload = TestUtil.utf8EncodedPayload("ping =>" + i, null); + return RxReactiveStreams.toObservable(client.requestStream(payload)); + } + ) + .doOnNext(i -> latch.countDown()) + .subscribe(); + + latch.await(); + } + + + + }*/ + +} diff --git a/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/internal/AeronUtilTest.java b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/internal/AeronUtilTest.java new file mode 100644 index 000000000..b494fbc02 --- /dev/null +++ b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/internal/AeronUtilTest.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.reactivesocket.aeron.internal; + +import io.aeron.Publication; +import io.aeron.logbuffer.BufferClaim; +import org.agrona.DirectBuffer; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AeronUtilTest { + + @Test(expected = TimedOutException.class) + public void testOfferShouldTimeOut() { + Publication publication = mock(Publication.class); + AeronUtil.BufferFiller bufferFiller = mock(AeronUtil.BufferFiller.class); + + when(publication.offer(any(DirectBuffer.class))).thenReturn(Publication.BACK_PRESSURED); + + AeronUtil + .offer(publication, bufferFiller, 1, 100, TimeUnit.MILLISECONDS); + + } + + @Test(expected = TimedOutException.class) + public void testTryClaimShouldTimeOut() { + Publication publication = mock(Publication.class); + AeronUtil.BufferFiller bufferFiller = mock(AeronUtil.BufferFiller.class); + + when(publication.tryClaim(anyInt(), any(BufferClaim.class))) + .thenReturn(Publication.BACK_PRESSURED); + + AeronUtil + .tryClaim(publication, bufferFiller, 1, 100, TimeUnit.MILLISECONDS); + + } +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/ServerAeronManagerTest.java b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/ServerAeronManagerTest.java new file mode 100644 index 000000000..3e7ae5fad --- /dev/null +++ b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/ServerAeronManagerTest.java @@ -0,0 +1,87 @@ +/** + * 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.reactivesocket.aeron.server; + +import io.aeron.driver.MediaDriver; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import rx.Observable; +import rx.Single; +import rx.functions.Func0; +import rx.observers.TestSubscriber; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@Ignore +public class ServerAeronManagerTest { + @BeforeClass + public static void init() { + + final MediaDriver.Context context = new MediaDriver.Context(); + context.dirsDeleteOnStart(true); + final MediaDriver mediaDriver = MediaDriver.launch(context); + + } + + @Test(timeout = 2_000) + public void testSubmitAction() throws Exception { + ServerAeronManager instance = ServerAeronManager.getInstance(); + CountDownLatch latch = new CountDownLatch(1); + instance.submitAction(() -> latch.countDown()); + latch.await(); + } + + @Test(timeout = 2_000) + public void testSubmitTask() { + ServerAeronManager instance = ServerAeronManager.getInstance(); + CountDownLatch latch = new CountDownLatch(1); + Single longSingle = instance.submitTask(() -> + { + latch.countDown(); + return latch.getCount(); + }); + + TestSubscriber testSubscriber = new TestSubscriber(); + longSingle.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertCompleted(); + } + + @Test(timeout = 2_0000) + public void testSubmitTasks() { + ServerAeronManager instance = ServerAeronManager.getInstance(); + int number = 1; + List> func0List = new ArrayList<>(); + for (int i = 0; i < 100_000; i++) { + func0List.add(() -> number + 1); + } + + TestSubscriber testSubscriber = new TestSubscriber(); + + instance + .submitTasks(Observable.from(func0List)) + .reduce((a,b) -> a + b) + .doOnError(t -> t.printStackTrace()) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + testSubscriber.assertCompleted(); + testSubscriber.assertValue(200_000); + } +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronSchedulerTest.java b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronSchedulerTest.java new file mode 100644 index 000000000..b9220c64d --- /dev/null +++ b/reactivesocket-transport-aeron/src/test/java/io/reactivesocket/aeron/server/rx/ReactiveSocketAeronSchedulerTest.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.reactivesocket.aeron.server.rx; + +import io.aeron.driver.MediaDriver; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import rx.Observable; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +import java.util.concurrent.TimeUnit; + + +@Ignore +public class ReactiveSocketAeronSchedulerTest { + @BeforeClass + public static void init() { + + final MediaDriver.Context context = new MediaDriver.Context(); + context.dirsDeleteOnStart(true); + final MediaDriver mediaDriver = MediaDriver.launch(context); + + } + + @Test + public void test() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Observable + .range(0, 10) + .subscribeOn(ReactiveSocketAeronScheduler.getInstance()) + .doOnNext(i -> { + String name = Thread.currentThread().getName(); + Assert.assertTrue(name.contains("reactive-socket-aeron-server")); + System.out.println(name + " - " + i); + }) + .observeOn(Schedulers.computation()) + .doOnNext(i -> { + String name = Thread.currentThread().getName(); + Assert.assertTrue(name.contains("RxComputationThreadPool")); + System.out.println(name + " - " + i); + }) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(1, TimeUnit.SECONDS); + testSubscriber.assertValueCount(10); + } + + @Test + public void testWithFlatMap() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Observable + .range(0, 10) + .flatMap(i -> + Observable + .just(i) + .subscribeOn(ReactiveSocketAeronScheduler.getInstance()) + ) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(1, TimeUnit.SECONDS); + testSubscriber.assertValueCount(10); + + } + + @Test + public void testMovingOnAndOffAndOnThePollingThread() { + TestSubscriber testSubscriber = new TestSubscriber(); + Observable + .range(0, 10) + .subscribeOn(ReactiveSocketAeronScheduler.getInstance()) + .doOnNext(i -> { + String name = Thread.currentThread().getName(); + Assert.assertTrue(name.contains("reactive-socket-aeron-server")); + System.out.println(name + " - " + i); + }) + .flatMap(i -> + Observable + .just(i) + .subscribeOn(Schedulers.computation()) + .doOnNext(j -> { + String name = Thread.currentThread().getName(); + Assert.assertTrue(name.contains("RxComputationThreadPool")); + System.out.println(name + " - " + i); + }) + ) + .observeOn(ReactiveSocketAeronScheduler.getInstance()) + .doOnNext(i -> { + String name = Thread.currentThread().getName(); + Assert.assertTrue(name.contains("reactive-socket-aeron-server")); + System.out.println(name + " - " + i); + }) + .subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(1, TimeUnit.SECONDS); + testSubscriber.assertValueCount(10); + } + +} \ No newline at end of file diff --git a/reactivesocket-transport-aeron/src/test/resources/simplelogger.properties b/reactivesocket-transport-aeron/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..463129958 --- /dev/null +++ b/reactivesocket-transport-aeron/src/test/resources/simplelogger.properties @@ -0,0 +1,35 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +#org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=trace + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=true + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss + +# Set to true if you want to output the current thread name. +# Defaults to true. +org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file diff --git a/reactivesocket-transport-jsr-356/build.gradle b/reactivesocket-transport-jsr-356/build.gradle new file mode 100644 index 000000000..d78cacae5 --- /dev/null +++ b/reactivesocket-transport-jsr-356/build.gradle @@ -0,0 +1,8 @@ +dependencies { + compile project(':reactivesocket-core') + compile 'org.glassfish.tyrus:tyrus-client:1.12' + + testCompile 'org.glassfish.tyrus:tyrus-server:1.12' + testCompile 'org.glassfish.tyrus:tyrus-container-grizzly-server:1.12' + testCompile project(':reactivesocket-test') +} \ No newline at end of file diff --git a/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/WebSocketDuplexConnection.java b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/WebSocketDuplexConnection.java new file mode 100644 index 000000000..04c978912 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/WebSocketDuplexConnection.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.reactivesocket.javax.websocket; + +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import org.reactivestreams.Publisher; +import rx.RxReactiveStreams; +import rx.Subscription; +import rx.subscriptions.BooleanSubscription; + +import javax.websocket.Session; +import java.io.IOException; + +public class WebSocketDuplexConnection implements DuplexConnection { + private final Session session; + private final rx.Observable input; + + public WebSocketDuplexConnection(Session session, rx.Observable input) { + this.session = session; + this.input = input; + } + + @Override + public Observable getInput() { + return o -> { + Subscription subscription = input.subscribe(o::onNext, o::onError, o::onComplete); + o.onSubscribe(subscription::unsubscribe); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + rx.Completable sent = rx.Completable.concat(RxReactiveStreams.toObservable(o).map(frame -> + rx.Completable.create(s -> { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + session.getAsyncRemote().sendBinary(frame.getByteBuffer(), result -> { + if (!bs.isUnsubscribed()) { + if (result.isOK()) { + s.onCompleted(); + } else { + s.onError(result.getException()); + } + } + }); + }) + )); + + sent.subscribe(new rx.Completable.CompletableSubscriber() { + @Override + public void onCompleted() { + callback.success(); + } + + @Override + public void onError(Throwable e) { + callback.error(e); + } + + @Override + public void onSubscribe(Subscription s) { + } + }); + } + + @Override + public double availability() { + return session.isOpen() ? 1.0 : 0.0; + } + + @Override + public void close() throws IOException { + session.close(); + } + + public String toString() { + if (session == null) { + return getClass().getName() + ":session=null"; + } + + return getClass().getName() + ":session=[" + session.toString() + "]"; + + } +} diff --git a/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/ReactiveSocketWebSocketClient.java b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/ReactiveSocketWebSocketClient.java new file mode 100644 index 000000000..d952b8ee5 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/ReactiveSocketWebSocketClient.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.reactivesocket.javax.websocket.client; + +import io.reactivesocket.Frame; +import io.reactivesocket.javax.websocket.WebSocketDuplexConnection; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.subjects.PublishSubject; + +import javax.websocket.*; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +public class ReactiveSocketWebSocketClient extends Endpoint { + private final PublishSubject input = PublishSubject.create(); + private final Subscriber subscriber; + + public ReactiveSocketWebSocketClient(Subscriber subscriber) { + this.subscriber = subscriber; + } + + public Observable getInput() { + return input; + } + + public static Publisher create(SocketAddress socketAddress, String path, ClientManager clientManager) { + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress)socketAddress; + try { + return create(new URI("ws", null, address.getHostName(), address.getPort(), path, null, null), clientManager); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } else { + throw new IllegalArgumentException("unknown socket address type => " + socketAddress.getClass()); + } + } + + public static Publisher create(URI uri, ClientManager clientManager) { + return s -> { + try { + clientManager.getProperties().put(ClientProperties.RECONNECT_HANDLER, new ClientManager.ReconnectHandler() { + @Override + public boolean onConnectFailure(Exception exception) { + s.onError(exception); + return false; + } + }); + ReactiveSocketWebSocketClient endpoint = new ReactiveSocketWebSocketClient(s); + clientManager.asyncConnectToServer(endpoint, null, uri); + } catch (DeploymentException e) { + s.onError(e); + } + }; + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + subscriber.onNext(new WebSocketDuplexConnection(session, input)); + subscriber.onComplete(); + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(ByteBuffer message) { + Frame frame = Frame.from(message); + input.onNext(frame); + } + }); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + input.onCompleted(); + } + + @Override + public void onError(Session session, Throwable thr) { + input.onError(thr); + } +} diff --git a/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/WebSocketReactiveSocketConnector.java b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/WebSocketReactiveSocketConnector.java new file mode 100644 index 000000000..51ce9120d --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/client/WebSocketReactiveSocketConnector.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.reactivesocket.javax.websocket.client; + +import io.reactivesocket.*; +import io.reactivesocket.javax.websocket.WebSocketDuplexConnection; +import io.reactivesocket.rx.Completable; +import org.glassfish.tyrus.client.ClientManager; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.net.SocketAddress; +import java.util.function.Consumer; + +/** + * An implementation of {@link ReactiveSocketConnector} that creates JSR-356 WebSocket ReactiveSockets. + */ +public class WebSocketReactiveSocketConnector implements ReactiveSocketConnector { + private static final Logger logger = LoggerFactory.getLogger(WebSocketReactiveSocketConnector.class); + + private final ConnectionSetupPayload connectionSetupPayload; + private final Consumer errorStream; + private final String path; + private final ClientManager clientManager; + + public WebSocketReactiveSocketConnector(String path, ClientManager clientManager, ConnectionSetupPayload connectionSetupPayload, Consumer errorStream) { + this.connectionSetupPayload = connectionSetupPayload; + this.errorStream = errorStream; + this.path = path; + this.clientManager = clientManager; + } + + @Override + public Publisher connect(SocketAddress address) { + Publisher connection + = ReactiveSocketWebSocketClient.create(address, path, clientManager); + + Observable result = Observable.create(s -> + connection.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); + } + + @Override + public void onNext(WebSocketDuplexConnection connection) { + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, connectionSetupPayload, errorStream); + reactiveSocket.start(new Completable() { + @Override + public void success() { + s.onNext(reactiveSocket); + s.onCompleted(); + } + + @Override + public void error(Throwable e) { + s.onError(e); + } + }); + } + + @Override + public void onError(Throwable t) { + s.onError(t); + } + + @Override + public void onComplete() { + } + }) + ); + + return RxReactiveStreams.toPublisher(result); + } +} diff --git a/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/server/ReactiveSocketWebSocketServer.java b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/server/ReactiveSocketWebSocketServer.java new file mode 100644 index 000000000..085e57cd9 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/main/java/io/reactivesocket/javax/websocket/server/ReactiveSocketWebSocketServer.java @@ -0,0 +1,102 @@ +/** + * 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.reactivesocket.javax.websocket.server; + +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Frame; +import io.reactivesocket.LeaseGovernor; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.javax.websocket.WebSocketDuplexConnection; +import io.reactivesocket.rx.Completable; +import org.agrona.LangUtil; +import rx.subjects.PublishSubject; + +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; + +public class ReactiveSocketWebSocketServer extends Endpoint { + private final PublishSubject input = PublishSubject.create(); + private final ConcurrentHashMap reactiveSockets = new ConcurrentHashMap<>(); + + private final ConnectionSetupHandler setupHandler; + private final LeaseGovernor leaseGovernor; + + protected ReactiveSocketWebSocketServer(ConnectionSetupHandler setupHandler, LeaseGovernor leaseGovernor) { + this.setupHandler = setupHandler; + this.leaseGovernor = leaseGovernor; + } + + protected ReactiveSocketWebSocketServer(ConnectionSetupHandler setupHandler) { + this(setupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(ByteBuffer message) { + Frame frame = Frame.from(message); + input.onNext(frame); + } + }); + + WebSocketDuplexConnection connection = new WebSocketDuplexConnection(session, input); + + final ReactiveSocket reactiveSocket = reactiveSockets.computeIfAbsent(session.getId(), id -> + DefaultReactiveSocket.fromServerConnection( + connection, + setupHandler, + leaseGovernor, + t -> t.printStackTrace() + ) + ); + + reactiveSocket.start(new Completable() { + @Override + public void success() { + } + + @Override + public void error(Throwable e) { + e.printStackTrace(); + } + }); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + input.onCompleted(); + try { + ReactiveSocket reactiveSocket = reactiveSockets.remove(session.getId()); + if (reactiveSocket != null) { + reactiveSocket.close(); + } + } catch (Exception e) { + LangUtil.rethrowUnchecked(e); + } + } + + @Override + public void onError(Session session, Throwable thr) { + input.onError(thr); + } +} diff --git a/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerEndpoint.java b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerEndpoint.java new file mode 100644 index 000000000..669e21d3f --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerEndpoint.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.reactivesocket.javax.websocket; + +import io.reactivesocket.Payload; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.javax.websocket.server.ReactiveSocketWebSocketServer; +import io.reactivesocket.test.TestUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; + +public class ClientServerEndpoint extends ReactiveSocketWebSocketServer { + public ClientServerEndpoint() { + super((setupPayload, rs) -> new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return s -> { + //System.out.println("Handling request/response payload => " + s.toString()); + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + s.onNext(response); + s.onComplete(); + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response) + .repeat()); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }); + } +} diff --git a/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerTest.java b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerTest.java new file mode 100644 index 000000000..bab516104 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/ClientServerTest.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.reactivesocket.javax.websocket; + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.javax.websocket.client.ReactiveSocketWebSocketClient; +import io.reactivesocket.test.TestUtil; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.server.Server; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.observers.TestSubscriber; + +import javax.websocket.DeploymentException; +import javax.websocket.Endpoint; +import javax.websocket.server.ServerApplicationConfig; +import javax.websocket.server.ServerEndpointConfig; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class ClientServerTest { + + static ReactiveSocket client; + static Server server; + + public static class ApplicationConfig implements ServerApplicationConfig { + @Override + public Set getEndpointConfigs(Set> endpointClasses) { + Set cfgs = new HashSet<>(); + cfgs.add(ServerEndpointConfig.Builder + .create(ClientServerEndpoint.class, "/rs") + .build()); + return cfgs; + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + return Collections.emptySet(); + } + } + + @BeforeClass + public static void setup() throws URISyntaxException, DeploymentException, IOException { + server = new Server("localhost", 8025, null, null, ApplicationConfig.class); + server.start(); + + WebSocketDuplexConnection duplexConnection = RxReactiveStreams.toObservable( + ReactiveSocketWebSocketClient.create(InetSocketAddress.createUnresolved("localhost", 8025), "/rs", ClientManager.createClient()) + ).toBlocking().single(); + + client = DefaultReactiveSocket.fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8"), t -> t.printStackTrace()); + + client.startAndWait(); + } + + @AfterClass + public static void tearDown() { + //server.shutdown(); + server.stop(); + } + + @Test + public void testRequestResponse1() { + requestResponseN(1500, 1); + } + + @Test + public void testRequestResponse10() { + requestResponseN(1500, 10); + } + + + @Test + public void testRequestResponse100() { + requestResponseN(1500, 100); + } + + @Test + public void testRequestResponse10_000() { + requestResponseN(60_000, 10_000); + } + + @Test + public void testRequestStream() { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestStream(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .subscribe(ts); + + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testRequestSubscription() throws InterruptedException { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestSubscription(TestUtil.utf8EncodedPayload("hello sub", "metadata sub"))) + .take(10) + .subscribe(ts); + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + } + + + public void requestResponseN(int timeout, int count) { + + TestSubscriber ts = TestSubscriber.create(); + + Observable + .range(1, count) + .flatMap(i -> + RxReactiveStreams + .toObservable( + client.requestResponse(TestUtil.utf8EncodedPayload("hello", "metadata")) + ) + .map(payload -> + TestUtil.byteToString(payload.getData()) + ) + //.doOnNext(System.out::println) + ) + .subscribe(ts); + + ts.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + ts.assertValueCount(count); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + +} diff --git a/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Ping.java b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Ping.java new file mode 100644 index 000000000..20193551f --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Ping.java @@ -0,0 +1,108 @@ +/** + * 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.reactivesocket.javax.websocket; + +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.javax.websocket.client.ReactiveSocketWebSocketClient; +import org.HdrHistogram.Recorder; +import org.glassfish.tyrus.client.ClientManager; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscriber; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Ping { + public static void main(String... args) throws Exception { + Publisher publisher = ReactiveSocketWebSocketClient.create(InetSocketAddress.createUnresolved("localhost", 8025), "/rs", ClientManager.createClient()); + + WebSocketDuplexConnection duplexConnection = RxReactiveStreams.toObservable(publisher).toBlocking().single(); + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8")); + + reactiveSocket.startAndWait(); + + byte[] data = "hello".getBytes(); + + Payload keyPayload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(data); + } + + @Override + public ByteBuffer getMetadata() { + return null; + } + }; + + int n = 1_000_000; + CountDownLatch latch = new CountDownLatch(n); + final Recorder histogram = new Recorder(3600000000000L, 3); + + Schedulers + .computation() + .createWorker() + .schedulePeriodically(() -> { + System.out.println("---- PING/ PONG HISTO ----"); + histogram.getIntervalHistogram() + .outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- PING/ PONG HISTO ----"); + }, 10, 10, TimeUnit.SECONDS); + + Observable + .range(1, Integer.MAX_VALUE) + .flatMap(i -> { + long start = System.nanoTime(); + + return RxReactiveStreams + .toObservable( + reactiveSocket + .requestResponse(keyPayload)) + .doOnNext(s -> { + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + }); + }) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + latch.countDown(); + } + }); + + latch.await(1, TimeUnit.HOURS); + System.out.println("Sent => " + n); + System.exit(0); + } +} diff --git a/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Pong.java b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Pong.java new file mode 100644 index 000000000..c55e3dc87 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/Pong.java @@ -0,0 +1,67 @@ +/** + * 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.reactivesocket.javax.websocket; + +import org.glassfish.tyrus.server.Server; + +import javax.websocket.DeploymentException; +import javax.websocket.Endpoint; +import javax.websocket.server.ServerApplicationConfig; +import javax.websocket.server.ServerEndpointConfig; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Pong { + public static class ApplicationConfig implements ServerApplicationConfig { + @Override + public Set getEndpointConfigs(Set> endpointClasses) { + Set cfgs = new HashSet<>(); + cfgs.add(ServerEndpointConfig.Builder + .create(PongEndpoint.class, "/rs") + .build()); + return cfgs; + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + return Collections.emptySet(); + } + } + + public static void main(String... args) throws DeploymentException { + byte[] response = new byte[1024]; + Random r = new Random(); + r.nextBytes(response); + + Server server = new Server("localhost", 8025, null, null, ApplicationConfig.class); + server.start(); + + // Tyrus spawns all of its threads as daemon threads so we need to prop open the JVM with a blocking call. + CountDownLatch latch = new CountDownLatch(1); + try { + latch.await(1, TimeUnit.HOURS); + } catch (InterruptedException e) { + System.out.println("Interrupted main thread"); + } finally { + server.stop(); + } + System.exit(0); + } +} diff --git a/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/PongEndpoint.java b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/PongEndpoint.java new file mode 100644 index 000000000..127df6b8c --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/java/io/reactivesocket/javax/websocket/PongEndpoint.java @@ -0,0 +1,104 @@ +/** + * 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.reactivesocket.javax.websocket; + +import io.reactivesocket.Payload; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.javax.websocket.server.ReactiveSocketWebSocketServer; +import io.reactivesocket.test.TestUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.nio.ByteBuffer; +import java.util.Random; + +public class PongEndpoint extends ReactiveSocketWebSocketServer { + static byte[] response = new byte[1024]; + static { + Random r = new Random(); + r.nextBytes(response); + } + + public PongEndpoint() { + super((setupPayload, rs) -> new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + Payload responsePayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(response); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + s.onNext(responsePayload); + s.onComplete(); + } + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response1 = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response1)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response1 = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response1)); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + Observable observable = + RxReactiveStreams + .toObservable(inputs) + .map(input -> input); + return RxReactiveStreams.toPublisher(observable); + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }); + } +} diff --git a/reactivesocket-transport-jsr-356/src/test/resources/simplelogger.properties b/reactivesocket-transport-jsr-356/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..463129958 --- /dev/null +++ b/reactivesocket-transport-jsr-356/src/test/resources/simplelogger.properties @@ -0,0 +1,35 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +#org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=trace + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=true + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss + +# Set to true if you want to output the current thread name. +# Defaults to true. +org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file diff --git a/reactivesocket-transport-local/build.gradle b/reactivesocket-transport-local/build.gradle new file mode 100644 index 000000000..887584cdc --- /dev/null +++ b/reactivesocket-transport-local/build.gradle @@ -0,0 +1,4 @@ +dependencies { + compile project(':reactivesocket-core') + testCompile project(':reactivesocket-test') +} diff --git a/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientDuplexConnection.java b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientDuplexConnection.java new file mode 100644 index 000000000..77d330f92 --- /dev/null +++ b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientDuplexConnection.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.reactivesocket.local; + +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +class LocalClientDuplexConnection implements DuplexConnection { + private final String name; + + private final CopyOnWriteArrayList> subjects; + + public LocalClientDuplexConnection(String name) { + this.name = name; + this.subjects = new CopyOnWriteArrayList<>(); + } + + @Override + public Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + + o + .subscribe(new Subscriber() { + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + LocalReactiveSocketManager + .getInstance() + .getServerConnection(name) + .write(frame); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return 1.0; + } + + void write(Frame frame) { + subjects + .forEach(o -> o.onNext(frame)); + } + + @Override + public void close() throws IOException { + LocalReactiveSocketManager + .getInstance() + .removeClientConnection(name); + + } +} diff --git a/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientReactiveSocketConnector.java b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientReactiveSocketConnector.java new file mode 100644 index 000000000..43740f4b1 --- /dev/null +++ b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalClientReactiveSocketConnector.java @@ -0,0 +1,71 @@ +/** + * 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.reactivesocket.local; + +import io.reactivesocket.*; +import io.reactivesocket.internal.rx.EmptySubscription; +import org.reactivestreams.Publisher; + +public class LocalClientReactiveSocketConnector implements ReactiveSocketConnector { + public static final LocalClientReactiveSocketConnector INSTANCE = new LocalClientReactiveSocketConnector(); + + private LocalClientReactiveSocketConnector() {} + + @Override + public Publisher connect(Config config) { + return s -> { + try { + s.onSubscribe(EmptySubscription.INSTANCE); + LocalClientDuplexConnection clientConnection = LocalReactiveSocketManager + .getInstance() + .getClientConnection(config.getName()); + ReactiveSocket reactiveSocket = DefaultReactiveSocket + .fromClientConnection(clientConnection, ConnectionSetupPayload.create(config.getMetadataMimeType(), config.getDataMimeType())); + + reactiveSocket.startAndWait(); + + s.onNext(reactiveSocket); + s.onComplete(); + } catch (Throwable t) { + s.onError(t); + } + }; + } + + public static class Config { + final String name; + final String metadataMimeType; + final String dataMimeType; + + public Config(String name, String metadataMimeType, String dataMimeType) { + this.name = name; + this.metadataMimeType = metadataMimeType; + this.dataMimeType = dataMimeType; + } + + public String getName() { + return name; + } + + public String getMetadataMimeType() { + return metadataMimeType; + } + + public String getDataMimeType() { + return dataMimeType; + } + } +} diff --git a/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalReactiveSocketManager.java b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalReactiveSocketManager.java new file mode 100644 index 000000000..60d246266 --- /dev/null +++ b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalReactiveSocketManager.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.reactivesocket.local; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by rroeser on 4/2/16. + */ +class LocalReactiveSocketManager { + private static final LocalReactiveSocketManager INSTANCE = new LocalReactiveSocketManager(); + + private final ConcurrentHashMap serverConnections; + private final ConcurrentHashMap clientConnections; + + private LocalReactiveSocketManager() { + serverConnections = new ConcurrentHashMap<>(); + clientConnections = new ConcurrentHashMap<>(); + } + + public static LocalReactiveSocketManager getInstance() { + return INSTANCE; + } + + public LocalClientDuplexConnection getClientConnection(String name) { + return clientConnections.computeIfAbsent(name, LocalClientDuplexConnection::new); + } + + public void removeClientConnection(String name) { + clientConnections.remove(name); + } + + public LocalServerDuplexConection getServerConnection(String name) { + return serverConnections.computeIfAbsent(name, LocalServerDuplexConection::new); + } + + public void removeServerDuplexConnection(String name) { + serverConnections.remove(name); + } + +} diff --git a/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerDuplexConection.java b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerDuplexConection.java new file mode 100644 index 000000000..baaf3800d --- /dev/null +++ b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerDuplexConection.java @@ -0,0 +1,99 @@ +/** + * 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.reactivesocket.local; + +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +class LocalServerDuplexConection implements DuplexConnection { + private final String name; + + private final CopyOnWriteArrayList> subjects; + + public LocalServerDuplexConection(String name) { + this.name = name; + this.subjects = new CopyOnWriteArrayList<>(); + } + + @Override + public Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o + .subscribe(new Subscriber() { + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + LocalReactiveSocketManager + .getInstance() + .getClientConnection(name) + .write(frame); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return 1.0; + } + + void write(Frame frame) { + subjects + .forEach(o -> o.onNext(frame)); + } + + @Override + public void close() throws IOException { + LocalReactiveSocketManager + .getInstance() + .removeServerDuplexConnection(name); + + } +} diff --git a/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerReactiveSocketConnector.java b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerReactiveSocketConnector.java new file mode 100644 index 000000000..58ad1d62b --- /dev/null +++ b/reactivesocket-transport-local/src/main/java/io/reactivesocket/local/LocalServerReactiveSocketConnector.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.reactivesocket.local; + +import io.reactivesocket.*; +import io.reactivesocket.internal.rx.EmptySubscription; +import org.reactivestreams.Publisher; + +public class LocalServerReactiveSocketConnector implements ReactiveSocketConnector { + public static final LocalServerReactiveSocketConnector INSTANCE = new LocalServerReactiveSocketConnector(); + + private LocalServerReactiveSocketConnector() {} + + @Override + public Publisher connect(Config config) { + return s -> { + try { + s.onSubscribe(EmptySubscription.INSTANCE); + LocalServerDuplexConection clientConnection = LocalReactiveSocketManager + .getInstance() + .getServerConnection(config.getName()); + ReactiveSocket reactiveSocket = DefaultReactiveSocket + .fromServerConnection(clientConnection, config.getConnectionSetupHandler()); + + reactiveSocket.startAndWait(); + s.onNext(reactiveSocket); + s.onComplete(); + } catch (Throwable t) { + s.onError(t); + } + }; + } + + public static class Config { + final String name; + final ConnectionSetupHandler connectionSetupHandler; + + public Config(String name, ConnectionSetupHandler connectionSetupHandler) { + this.name = name; + this.connectionSetupHandler = connectionSetupHandler; + } + + public ConnectionSetupHandler getConnectionSetupHandler() { + return connectionSetupHandler; + } + + public String getName() { + return name; + } + } +} diff --git a/reactivesocket-transport-local/src/test/java/io/reactivesocket/local/ClientServerTest.java b/reactivesocket-transport-local/src/test/java/io/reactivesocket/local/ClientServerTest.java new file mode 100644 index 000000000..a62871e8f --- /dev/null +++ b/reactivesocket-transport-local/src/test/java/io/reactivesocket/local/ClientServerTest.java @@ -0,0 +1,185 @@ +/** + * 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.reactivesocket.local; + +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.exceptions.SetupException; +import io.reactivesocket.test.TestUtil; +import org.junit.BeforeClass; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.observers.TestSubscriber; + +import java.util.concurrent.TimeUnit; + +import static io.reactivesocket.util.Unsafe.toSingleFuture; + +public class ClientServerTest { + + static ReactiveSocket client; + + static ReactiveSocket server; + + @BeforeClass + public static void setup() throws Exception { + LocalServerReactiveSocketConnector.Config serverConfig = new LocalServerReactiveSocketConnector.Config("test", new ConnectionSetupHandler() { + @Override + public RequestHandler apply(ConnectionSetupPayload setupPayload, ReactiveSocket rs) throws SetupException { + return new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return s -> { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + s.onNext(response); + s.onComplete(); + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response) + .repeat()); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }; + } + }); + + server = toSingleFuture(LocalServerReactiveSocketConnector.INSTANCE.connect(serverConfig)).get(5, TimeUnit.SECONDS); + + LocalClientReactiveSocketConnector.Config clientConfig = new LocalClientReactiveSocketConnector.Config("test", "text", "text"); + client = toSingleFuture(LocalClientReactiveSocketConnector.INSTANCE.connect(clientConfig)).get(5, TimeUnit.SECONDS);; + } + + @Test + public void testRequestResponse1() { + requestResponseN(1500, 1); + } + + @Test + public void testRequestResponse10() { + requestResponseN(1500, 10); + } + + + @Test + public void testRequestResponse100() { + requestResponseN(1500, 100); + } + + @Test + public void testRequestResponse10_000() { + requestResponseN(60_000, 10_000); + } + + + @Test + public void testRequestResponse100_000() { + requestResponseN(60_000, 10_000); + } + @Test + public void testRequestResponse1_000_000() { + requestResponseN(60_000, 10_000); + } + + @Test + public void testRequestStream() { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestStream(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .subscribe(ts); + + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testRequestSubscription() throws InterruptedException { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestSubscription(TestUtil.utf8EncodedPayload("hello sub", "metadata sub"))) + .take(10) + .subscribe(ts); + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + } + + + public void requestResponseN(int timeout, int count) { + + TestSubscriber ts = TestSubscriber.create(); + + Observable + .range(1, count) + .flatMap(i -> + RxReactiveStreams + .toObservable(client.requestResponse(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .map(payload -> TestUtil.byteToString(payload.getData())) + ) + .doOnError(Throwable::printStackTrace) + .subscribe(ts); + + ts.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + ts.assertValueCount(count); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + +} \ No newline at end of file diff --git a/reactivesocket-transport-netty/build.gradle b/reactivesocket-transport-netty/build.gradle new file mode 100644 index 000000000..5bea1d904 --- /dev/null +++ b/reactivesocket-transport-netty/build.gradle @@ -0,0 +1,12 @@ +dependencies { + compile project(':reactivesocket-core') + compile 'io.netty:netty-handler:4.1.0.CR7' + compile 'io.netty:netty-codec-http:4.1.0.CR7' + + testCompile project(':reactivesocket-test') +} + +task echoServer(type: JavaExec) { + classpath = sourceSets.examples.runtimeClasspath + main = 'io.reactivesocket.netty.EchoServer' +} diff --git a/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServer.java b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServer.java new file mode 100644 index 000000000..a15d96854 --- /dev/null +++ b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServer.java @@ -0,0 +1,38 @@ +package io.reactivesocket.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public class EchoServer { + public static void main(String... args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new EchoServerHandler()); + } + }); + + Channel localhost = b.bind("0.0.0.0", 8025).sync().channel(); + localhost.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} \ No newline at end of file diff --git a/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServerHandler.java b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServerHandler.java new file mode 100644 index 000000000..dd1939b80 --- /dev/null +++ b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/EchoServerHandler.java @@ -0,0 +1,67 @@ +package io.reactivesocket.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.netty.tcp.server.ReactiveSocketServerHandler; + +import java.util.List; + +public class EchoServerHandler extends ByteToMessageDecoder { + private static SimpleChannelInboundHandler httpHandler = new HttpServerHandler(); + + private static ReactiveSocketServerHandler reactiveSocketHandler = ReactiveSocketServerHandler.create((setupPayload, rs) -> + new RequestHandler.Builder().withRequestResponse(payload -> s -> { + s.onNext(payload); + s.onComplete(); + }).build()); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // Will use the first five bytes to detect a protocol. + if (in.readableBytes() < 5) { + return; + } + + final int magic1 = in.getUnsignedByte(in.readerIndex()); + final int magic2 = in.getUnsignedByte(in.readerIndex() + 1); + if (isHttp(magic1, magic2)) { + switchToHttp(ctx); + } else { + switchToReactiveSocket(ctx); + } + } + + private static boolean isHttp(int magic1, int magic2) { + return + magic1 == 'G' && magic2 == 'E' || // GET + magic1 == 'P' && magic2 == 'O' || // POST + magic1 == 'P' && magic2 == 'U' || // PUT + magic1 == 'H' && magic2 == 'E' || // HEAD + magic1 == 'O' && magic2 == 'P' || // OPTIONS + magic1 == 'P' && magic2 == 'A' || // PATCH + magic1 == 'D' && magic2 == 'E' || // DELETE + magic1 == 'T' && magic2 == 'R' || // TRACE + magic1 == 'C' && magic2 == 'O'; // CONNECT + } + + private void switchToHttp(ChannelHandlerContext ctx) { + ChannelPipeline p = ctx.pipeline(); + p.addLast(new HttpServerCodec()); + p.addLast(new HttpObjectAggregator(256 * 1024)); + p.addLast(httpHandler); + p.remove(this); + } + + private void switchToReactiveSocket(ChannelHandlerContext ctx) { + ChannelPipeline p = ctx.pipeline(); + p.addLast(reactiveSocketHandler); + p.remove(this); + } +} diff --git a/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/HttpServerHandler.java b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/HttpServerHandler.java new file mode 100644 index 000000000..248fbd682 --- /dev/null +++ b/reactivesocket-transport-netty/src/examples/java/io/reactivesocket/netty/HttpServerHandler.java @@ -0,0 +1,22 @@ +package io.reactivesocket.netty; + +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; + +@ChannelHandler.Sharable +public class HttpServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, request.content().retain()); + HttpUtil.setContentLength(response, response.content().readableBytes()); + if (HttpUtil.isKeepAlive(request)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + ctx.writeAndFlush(response); + } else { + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/MutableDirectByteBuf.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/MutableDirectByteBuf.java new file mode 100644 index 000000000..a72f16b6e --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/MutableDirectByteBuf.java @@ -0,0 +1,424 @@ +/** + * 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.reactivesocket.netty; + +import io.netty.buffer.ByteBuf; +import org.agrona.BitUtil; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +public class MutableDirectByteBuf implements MutableDirectBuffer +{ + private ByteBuf byteBuf; + + public MutableDirectByteBuf(final ByteBuf byteBuf) + { + this.byteBuf = byteBuf; + } + + public void wrap(final ByteBuf byteBuf) + { + this.byteBuf = byteBuf; + } + + public ByteBuf byteBuf() + { + return byteBuf; + } + + // TODO: make utility in reactivesocket-java + public static ByteBuffer slice(final ByteBuffer byteBuffer, final int position, final int limit) + { + final int savedPosition = byteBuffer.position(); + final int savedLimit = byteBuffer.limit(); + + byteBuffer.limit(limit).position(position); + + final ByteBuffer result = byteBuffer.slice(); + + byteBuffer.limit(savedLimit).position(savedPosition); + return result; + } + + @Override + public void setMemory(int index, int length, byte value) + { + for (int i = index; i < (index + length); i++) + { + byteBuf.setByte(i, value); + } + } + + @Override + public void putLong(int index, long value, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + byteBuf.setLong(index, value); + } + + @Override + public void putLong(int index, long value) + { + byteBuf.setLong(index, value); + } + + @Override + public void putInt(int index, int value, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + byteBuf.setInt(index, value); + } + + @Override + public void putInt(int index, int value) + { + byteBuf.setInt(index, value); + } + + @Override + public void putDouble(int index, double value, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + byteBuf.setDouble(index, value); + } + + @Override + public void putDouble(int index, double value) + { + byteBuf.setDouble(index, value); + } + + @Override + public void putFloat(int index, float value, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + byteBuf.setFloat(index, value); + } + + @Override + public void putFloat(int index, float value) + { + byteBuf.setFloat(index, value); + } + + @Override + public void putShort(int index, short value, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + byteBuf.setShort(index, value); + } + + @Override + public void putShort(int index, short value) + { + byteBuf.setShort(index, value); + } + + @Override + public void putByte(int index, byte value) + { + byteBuf.setByte(index, value); + } + + @Override + public void putBytes(int index, byte[] src) + { + byteBuf.setBytes(index, src); + } + + @Override + public void putBytes(int index, byte[] src, int offset, int length) + { + byteBuf.setBytes(index, src, offset, length); + } + + @Override + public void putBytes(int index, ByteBuffer srcBuffer, int length) + { + final ByteBuffer sliceBuffer = slice(srcBuffer, 0, length); + byteBuf.setBytes(index, sliceBuffer); + } + + @Override + public void putBytes(int index, ByteBuffer srcBuffer, int srcIndex, int length) + { + final ByteBuffer sliceBuffer = slice(srcBuffer, srcIndex, srcIndex + length); + byteBuf.setBytes(index, sliceBuffer); + } + + @Override + public void putBytes(int index, DirectBuffer srcBuffer, int srcIndex, int length) + { + throw new UnsupportedOperationException("putBytes(DirectBuffer) not supported"); + } + + @Override + public int putStringUtf8(int offset, String value, ByteOrder byteOrder) + { + throw new UnsupportedOperationException("putStringUtf8 not supported"); + } + + @Override + public int putStringUtf8(int offset, String value, ByteOrder byteOrder, int maxEncodedSize) + { + throw new UnsupportedOperationException("putStringUtf8 not supported"); + } + + @Override + public int putStringWithoutLengthUtf8(int offset, String value) + { + throw new UnsupportedOperationException("putStringUtf8 not supported"); + } + + @Override + public void wrap(byte[] buffer) + { + throw new UnsupportedOperationException("wrap(byte[]) not supported"); + } + + @Override + public void wrap(byte[] buffer, int offset, int length) + { + throw new UnsupportedOperationException("wrap(byte[]) not supported"); + } + + @Override + public void wrap(ByteBuffer buffer) + { + throw new UnsupportedOperationException("wrap(ByteBuffer) not supported"); + } + + @Override + public void wrap(ByteBuffer buffer, int offset, int length) + { + throw new UnsupportedOperationException("wrap(ByteBuffer) not supported"); + } + + @Override + public void wrap(DirectBuffer buffer) + { + throw new UnsupportedOperationException("wrap(DirectBuffer) not supported"); + } + + @Override + public void wrap(DirectBuffer buffer, int offset, int length) + { + throw new UnsupportedOperationException("wrap(DirectBuffer) not supported"); + } + + @Override + public void wrap(long address, int length) + { + throw new UnsupportedOperationException("wrap(address) not supported"); + } + + @Override + public long addressOffset() + { + return byteBuf.memoryAddress(); + } + + @Override + public byte[] byteArray() + { + return byteBuf.array(); + } + + @Override + public ByteBuffer byteBuffer() + { + return byteBuf.nioBuffer(); + } + + @Override + public int capacity() + { + return byteBuf.capacity(); + } + + @Override + public void checkLimit(int limit) + { + throw new UnsupportedOperationException("checkLimit not supported"); + } + + @Override + public long getLong(int index, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + return byteBuf.getLong(index); + } + + @Override + public long getLong(int index) + { + return byteBuf.getLong(index); + } + + @Override + public int getInt(int index, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + return byteBuf.getInt(index); + } + + @Override + public int getInt(int index) + { + return byteBuf.getInt(index); + } + + @Override + public double getDouble(int index, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + return byteBuf.getDouble(index); + } + + @Override + public double getDouble(int index) + { + return byteBuf.getDouble(index); + } + + @Override + public float getFloat(int index, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + return byteBuf.getFloat(index); + } + + @Override + public float getFloat(int index) + { + return byteBuf.getFloat(index); + } + + @Override + public short getShort(int index, ByteOrder byteOrder) + { + ensureByteOrder(byteOrder); + return byteBuf.getShort(index); + } + + @Override + public short getShort(int index) + { + return byteBuf.getShort(index); + } + + @Override + public byte getByte(int index) + { + return byteBuf.getByte(index); + } + + @Override + public void getBytes(int index, byte[] dst) + { + byteBuf.getBytes(index, dst); + } + + @Override + public void getBytes(int index, byte[] dst, int offset, int length) + { + byteBuf.getBytes(index, dst, offset, length); + } + + @Override + public void getBytes(int index, MutableDirectBuffer dstBuffer, int dstIndex, int length) + { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public void getBytes(int index, ByteBuffer dstBuffer, int length) + { + throw new UnsupportedOperationException("getBytes(ByteBuffer) not supported"); + } + + @Override + public String getStringUtf8(int offset, ByteOrder byteOrder) + { + final int length = getInt(offset, byteOrder); + return byteBuf.toString(offset + BitUtil.SIZE_OF_INT, length, StandardCharsets.UTF_8); + } + + @Override + public String getStringUtf8(int offset, int length) + { + return byteBuf.toString(offset, length, StandardCharsets.UTF_8); + } + + @Override + public String getStringWithoutLengthUtf8(int offset, int length) + { + return byteBuf.toString(offset, length, StandardCharsets.UTF_8); + } + + @Override + public void boundsCheck(int index, int length) + { + throw new UnsupportedOperationException("boundsCheck not supported"); + } + + private void ensureByteOrder(final ByteOrder byteOrder) + { + if (byteBuf.order() != byteOrder) + { + byteBuf.order(byteOrder); + } + } + + @Override + public void putChar(int index, char value, ByteOrder byteOrder) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public void putChar(int index, char value) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public int putStringUtf8(int offset, String value) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public int putStringUtf8(int index, String value, int maxEncodedSize) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public char getChar(int index, ByteOrder byteOrder) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public char getChar(int index) { + throw new UnsupportedOperationException("getBytes(MutableDirectBuffer) not supported"); + } + + @Override + public String getStringUtf8(int index) { + return null; + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ClientTcpDuplexConnection.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ClientTcpDuplexConnection.java new file mode 100644 index 000000000..4c1e05bfc --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ClientTcpDuplexConnection.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.reactivesocket.netty.tcp.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.exceptions.TransportException; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.agrona.BitUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ClientTcpDuplexConnection implements DuplexConnection { + private final Channel channel; + private final CopyOnWriteArrayList> subjects; + + private ClientTcpDuplexConnection(Channel channel, CopyOnWriteArrayList> subjects) { + this.subjects = subjects; + this.channel = channel; + } + + public static Publisher create(SocketAddress address, EventLoopGroup eventLoopGroup) { + return s -> { + CopyOnWriteArrayList> subjects = new CopyOnWriteArrayList<>(); + ReactiveSocketClientHandler clientHandler = new ReactiveSocketClientHandler(subjects); + Bootstrap bootstrap = new Bootstrap(); + ChannelFuture connect = bootstrap + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.AUTO_READ, true) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE >> 1, 0, BitUtil.SIZE_OF_INT, -1 * BitUtil.SIZE_OF_INT, 0), + clientHandler + ); + } + }).connect(address); + + connect.addListener(connectFuture -> { + if (connectFuture.isSuccess()) { + Channel ch = connect.channel(); + s.onNext(new ClientTcpDuplexConnection(ch, subjects)); + s.onComplete(); + } else { + s.onError(connectFuture.cause()); + } + }); + }; + } + + @Override + public final Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o.subscribe(new Subscriber() { + private Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + // TODO: wire back pressure + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + ByteBuf byteBuf = Unpooled.wrappedBuffer(frame.getByteBuffer()); + ChannelFuture channelFuture = channel.writeAndFlush(byteBuf); + channelFuture.addListener(future -> { + Throwable cause = future.cause(); + if (cause != null) { + if (cause instanceof ClosedChannelException) { + onError(new TransportException(cause)); + } else { + onError(cause); + } + } + }); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + subscription.cancel(); + } + + @Override + public void onComplete() { + callback.success(); + subscription.cancel(); + } + }); + } + + @Override + public double availability() { + return channel.isOpen() ? 1.0 : 0.0; + } + + @Override + public void close() throws IOException { + channel.close(); + } + + public String toString() { + if (channel == null) { + return "ClientTcpDuplexConnection(channel=null)"; + } + + return "ClientTcpDuplexConnection(channel=[" + + "remoteAddress=" + channel.remoteAddress() + "," + + "isActive=" + channel.isActive() + "," + + "isOpen=" + channel.isOpen() + "," + + "isRegistered=" + channel.isRegistered() + "," + + "isWritable=" + channel.isWritable() + "," + + "channelId=" + channel.id().asLongText() + + "])"; + + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ReactiveSocketClientHandler.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ReactiveSocketClientHandler.java new file mode 100644 index 000000000..5632cf03b --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/ReactiveSocketClientHandler.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.reactivesocket.netty.tcp.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.reactivesocket.Frame; +import io.reactivesocket.netty.MutableDirectByteBuf; +import io.reactivesocket.rx.Observer; + +import java.util.concurrent.CopyOnWriteArrayList; + +@ChannelHandler.Sharable +public class ReactiveSocketClientHandler extends ChannelInboundHandlerAdapter { + + private final CopyOnWriteArrayList> subjects; + + public ReactiveSocketClientHandler(CopyOnWriteArrayList> subjects) { + this.subjects = subjects; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object content) { + ByteBuf byteBuf = (ByteBuf) content; + try { + MutableDirectByteBuf mutableDirectByteBuf = new MutableDirectByteBuf(byteBuf); + final Frame from = Frame.from(mutableDirectByteBuf, 0, mutableDirectByteBuf.capacity()); + subjects.forEach(o -> o.onNext(from)); + } finally { + byteBuf.release(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/TcpReactiveSocketConnector.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/TcpReactiveSocketConnector.java new file mode 100644 index 000000000..2bcc3a197 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/client/TcpReactiveSocketConnector.java @@ -0,0 +1,80 @@ +/** + * 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.reactivesocket.netty.tcp.client; + +import io.netty.channel.EventLoopGroup; +import io.reactivesocket.*; +import io.reactivesocket.rx.Completable; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.net.SocketAddress; +import java.util.function.Consumer; + +/** + * An implementation of {@link ReactiveSocketConnecot} that creates Netty TCP ReactiveSockets. + */ +public class TcpReactiveSocketConnector implements ReactiveSocketConnector { + private final ConnectionSetupPayload connectionSetupPayload; + private final Consumer errorStream; + private final EventLoopGroup eventLoopGroup; + + public TcpReactiveSocketConnector(EventLoopGroup eventLoopGroup, ConnectionSetupPayload connectionSetupPayload, Consumer errorStream) { + this.connectionSetupPayload = connectionSetupPayload; + this.errorStream = errorStream; + this.eventLoopGroup = eventLoopGroup; + } + + @Override + public Publisher connect(SocketAddress address) { + Publisher connection + = ClientTcpDuplexConnection.create(address, eventLoopGroup); + + return s -> connection.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); + } + + @Override + public void onNext(ClientTcpDuplexConnection connection) { + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection( + connection, connectionSetupPayload, errorStream); + reactiveSocket.start(new Completable() { + @Override + public void success() { + s.onNext(reactiveSocket); + s.onComplete(); + } + + @Override + public void error(Throwable e) { + s.onError(e); + } + }); + } + + @Override + public void onError(Throwable t) { + s.onError(t); + } + + @Override + public void onComplete() {} + }); + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ReactiveSocketServerHandler.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ReactiveSocketServerHandler.java new file mode 100644 index 000000000..7557a7d52 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ReactiveSocketServerHandler.java @@ -0,0 +1,113 @@ +/** + * 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.reactivesocket.netty.tcp.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Frame; +import io.reactivesocket.LeaseGovernor; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.netty.MutableDirectByteBuf; +import org.agrona.BitUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; + +@ChannelHandler.Sharable +public class ReactiveSocketServerHandler extends ChannelInboundHandlerAdapter { + private Logger logger = LoggerFactory.getLogger(ReactiveSocketServerHandler.class); + + private ConcurrentHashMap duplexConnections = new ConcurrentHashMap<>(); + + private ConnectionSetupHandler setupHandler; + + private LeaseGovernor leaseGovernor; + + protected ReactiveSocketServerHandler(ConnectionSetupHandler setupHandler, LeaseGovernor leaseGovernor) { + this.setupHandler = setupHandler; + this.leaseGovernor = leaseGovernor; + } + + public static ReactiveSocketServerHandler create(ConnectionSetupHandler setupHandler) { + return create(setupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + public static ReactiveSocketServerHandler create(ConnectionSetupHandler setupHandler, LeaseGovernor leaseGovernor) { + return new + ReactiveSocketServerHandler( + setupHandler, + leaseGovernor); + + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + ChannelPipeline cp = ctx.pipeline(); + if (cp.get(LengthFieldBasedFrameDecoder.class) == null) { + ctx + .pipeline() + .addBefore( + ctx.name(), + LengthFieldBasedFrameDecoder.class.getName(), + new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE >> 1, 0, BitUtil.SIZE_OF_INT, -1 * BitUtil.SIZE_OF_INT, 0)); + } + + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf content = (ByteBuf) msg; + try { + MutableDirectByteBuf mutableDirectByteBuf = new MutableDirectByteBuf(content); + Frame from = Frame.from(mutableDirectByteBuf, 0, mutableDirectByteBuf.capacity()); + channelRegistered(ctx); + ServerTcpDuplexConnection connection = duplexConnections.computeIfAbsent(ctx.channel().id(), i -> { + logger.info("No connection found for channel id: " + i + " from host " + ctx.channel().remoteAddress().toString()); + ServerTcpDuplexConnection c = new ServerTcpDuplexConnection(ctx); + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromServerConnection(c, setupHandler, leaseGovernor, throwable -> throwable.printStackTrace()); + reactiveSocket.startAndWait(); + return c; + }); + if (connection != null) { + connection + .getSubscribers() + .forEach(o -> o.onNext(from)); + } + } finally { + content.release(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + + logger.error("caught an unhandled exception", cause); + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ServerTcpDuplexConnection.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ServerTcpDuplexConnection.java new file mode 100644 index 000000000..c4a9ee952 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/tcp/server/ServerTcpDuplexConnection.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.reactivesocket.netty.tcp.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ServerTcpDuplexConnection implements DuplexConnection { + private final CopyOnWriteArrayList> subjects; + + private final ChannelHandlerContext ctx; + + public ServerTcpDuplexConnection(ChannelHandlerContext ctx) { + this.subjects = new CopyOnWriteArrayList<>(); + this.ctx = ctx; + } + + public List> getSubscribers() { + return subjects; + } + + @Override + public final Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + ByteBuffer data = frame.getByteBuffer(); + ByteBuf byteBuf = Unpooled.wrappedBuffer(data); + ChannelFuture channelFuture = ctx.writeAndFlush(byteBuf); + channelFuture.addListener(future -> { + Throwable cause = future.cause(); + if (cause != null) { + cause.printStackTrace(); + callback.error(cause); + } + }); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return ctx.channel().isOpen() ? 1.0 : 0.0; + } + + @Override + public void close() throws IOException { + + } + + public String toString() { + if (ctx ==null || ctx.channel() == null) { + return getClass().getName() + ":channel=null"; + } + + Channel channel = ctx.channel(); + return getClass().getName() + ":channel=[" + + "remoteAddress=" + channel.remoteAddress() + "," + + "isActive=" + channel.isActive() + "," + + "isOpen=" + channel.isOpen() + "," + + "isRegistered=" + channel.isRegistered() + "," + + "isWritable=" + channel.isWritable() + "," + + "channelId=" + channel.id().asLongText() + + "]"; + + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ClientWebSocketDuplexConnection.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ClientWebSocketDuplexConnection.java new file mode 100644 index 000000000..d26c25697 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ClientWebSocketDuplexConnection.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.reactivesocket.netty.websocket.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.*; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.exceptions.TransportException; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ClientWebSocketDuplexConnection implements DuplexConnection { + private Channel channel; + + private final CopyOnWriteArrayList> subjects; + + private ClientWebSocketDuplexConnection(Channel channel, CopyOnWriteArrayList> subjects) { + this.subjects = subjects; + this.channel = channel; + } + + public static Publisher create(InetSocketAddress address, String path, EventLoopGroup eventLoopGroup) { + try { + return create(new URI("ws", null, address.getHostName(), address.getPort(), path, null, null), eventLoopGroup); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public static Publisher create(URI uri, EventLoopGroup eventLoopGroup) { + return s -> { + WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( + uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()); + + CopyOnWriteArrayList> subjects = new CopyOnWriteArrayList<>(); + ReactiveSocketClientHandler clientHandler = new ReactiveSocketClientHandler(subjects); + Bootstrap bootstrap = new Bootstrap(); + ChannelFuture connect = bootstrap + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast( + new HttpClientCodec(), + new HttpObjectAggregator(8192), + new WebSocketClientProtocolHandler(handshaker), + clientHandler + ); + } + }).connect(uri.getHost(), uri.getPort()); + + connect.addListener(connectFuture -> { + if (connectFuture.isSuccess()) { + final Channel ch = connect.channel(); + clientHandler + .getHandshakePromise() + .addListener(handshakeFuture -> { + if (handshakeFuture.isSuccess()) { + s.onNext(new ClientWebSocketDuplexConnection(ch, subjects)); + s.onComplete(); + } else { + s.onError(handshakeFuture.cause()); + } + }); + } else { + s.onError(connectFuture.cause()); + } + }); + }; + } + + @Override + public final Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o.subscribe(new Subscriber() { + private Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + ByteBuf byteBuf = Unpooled.wrappedBuffer(frame.getByteBuffer()); + BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(byteBuf); + ChannelFuture channelFuture = channel.writeAndFlush(binaryWebSocketFrame); + channelFuture.addListener(future -> { + Throwable cause = future.cause(); + if (cause != null) { + if (cause instanceof ClosedChannelException) { + onError(new TransportException(cause)); + } else { + onError(cause); + } + } + }); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + if (t instanceof TransportException) { + subscription.cancel(); + } + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return channel.isOpen() ? 1.0 : 0.0; + } + + @Override + public void close() throws IOException { + channel.close(); + } + + public String toString() { + if (channel == null) { + return "ClientWebSocketDuplexConnection(channel=null)"; + } + + return "ClientWebSocketDuplexConnection(channel=[" + + "remoteAddress=" + channel.remoteAddress() + + ", isActive=" + channel.isActive() + + ", isOpen=" + channel.isOpen() + + ", isRegistered=" + channel.isRegistered() + + ", channelId=" + channel.id().asLongText() + + "])"; + + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ReactiveSocketClientHandler.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ReactiveSocketClientHandler.java new file mode 100644 index 000000000..fc1eb7d2e --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/ReactiveSocketClientHandler.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.reactivesocket.netty.websocket.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler; +import io.reactivesocket.Frame; +import io.reactivesocket.netty.MutableDirectByteBuf; +import io.reactivesocket.rx.Observer; + + +import java.util.concurrent.CopyOnWriteArrayList; + +@ChannelHandler.Sharable +public class ReactiveSocketClientHandler extends SimpleChannelInboundHandler { + + private final CopyOnWriteArrayList> subjects; + + private ChannelPromise handshakePromise; + + public ReactiveSocketClientHandler(CopyOnWriteArrayList> subjects) { + this.subjects = subjects; + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + this.handshakePromise = ctx.newPromise(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame bFrame) throws Exception { + ByteBuf content = bFrame.content(); + MutableDirectByteBuf mutableDirectByteBuf = new MutableDirectByteBuf(content); + final Frame from = Frame.from(mutableDirectByteBuf, 0, mutableDirectByteBuf.capacity()); + subjects.forEach(o -> o.onNext(from)); + } + + public ChannelPromise getHandshakePromise() { + return handshakePromise; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof WebSocketClientProtocolHandler.ClientHandshakeStateEvent) { + WebSocketClientProtocolHandler.ClientHandshakeStateEvent evt1 = (WebSocketClientProtocolHandler.ClientHandshakeStateEvent) evt; + if (evt1.equals(WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE)) { + handshakePromise.setSuccess(); + } + } + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/WebSocketReactiveSocketConnector.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/WebSocketReactiveSocketConnector.java new file mode 100644 index 000000000..d14c540fc --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/client/WebSocketReactiveSocketConnector.java @@ -0,0 +1,97 @@ +/** + * 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.reactivesocket.netty.websocket.client; + +import io.netty.channel.EventLoopGroup; +import io.reactivesocket.*; +import io.reactivesocket.rx.Completable; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.function.Consumer; + +/** + * An implementation of {@link ReactiveSocketConnector} that creates Netty WebSocket ReactiveSockets. + */ +public class WebSocketReactiveSocketConnector implements ReactiveSocketConnector { + private static final Logger logger = LoggerFactory.getLogger(WebSocketReactiveSocketConnector.class); + + private final ConnectionSetupPayload connectionSetupPayload; + private final Consumer errorStream; + private final String path; + private final EventLoopGroup eventLoopGroup; + + public WebSocketReactiveSocketConnector(String path, EventLoopGroup eventLoopGroup, ConnectionSetupPayload connectionSetupPayload, Consumer errorStream) { + this.connectionSetupPayload = connectionSetupPayload; + this.errorStream = errorStream; + this.path = path; + this.eventLoopGroup = eventLoopGroup; + } + + @Override + public Publisher connect(SocketAddress address) { + if (address instanceof InetSocketAddress) { + Publisher connection + = ClientWebSocketDuplexConnection.create((InetSocketAddress)address, path, eventLoopGroup); + + Observable result = Observable.create(s -> + connection.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); + } + + @Override + public void onNext(ClientWebSocketDuplexConnection connection) { + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(connection, connectionSetupPayload, errorStream); + reactiveSocket.start(new Completable() { + @Override + public void success() { + s.onNext(reactiveSocket); + s.onCompleted(); + } + + @Override + public void error(Throwable e) { + s.onError(e); + } + }); + } + + @Override + public void onError(Throwable t) { + s.onError(t); + } + + @Override + public void onComplete() { + } + }) + ); + + return RxReactiveStreams.toPublisher(result); + } else { + throw new IllegalArgumentException("unknown socket address type => " + address.getClass()); + } + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ReactiveSocketServerHandler.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ReactiveSocketServerHandler.java new file mode 100644 index 000000000..8e77d56e2 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ReactiveSocketServerHandler.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.reactivesocket.netty.websocket.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.reactivesocket.ConnectionSetupHandler; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Frame; +import io.reactivesocket.LeaseGovernor; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.netty.MutableDirectByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; + +@ChannelHandler.Sharable +public class ReactiveSocketServerHandler extends SimpleChannelInboundHandler { + private Logger logger = LoggerFactory.getLogger(ReactiveSocketServerHandler.class); + + private ConcurrentHashMap duplexConnections = new ConcurrentHashMap<>(); + + private ConnectionSetupHandler setupHandler; + + private LeaseGovernor leaseGovernor; + + protected ReactiveSocketServerHandler(ConnectionSetupHandler setupHandler, LeaseGovernor leaseGovernor) { + this.setupHandler = setupHandler; + this.leaseGovernor = leaseGovernor; + } + + public static ReactiveSocketServerHandler create(ConnectionSetupHandler setupHandler) { + return create(setupHandler, LeaseGovernor.UNLIMITED_LEASE_GOVERNOR); + } + + public static ReactiveSocketServerHandler create(ConnectionSetupHandler setupHandler, LeaseGovernor leaseGovernor) { + return new + ReactiveSocketServerHandler( + setupHandler, + leaseGovernor); + + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception { + ByteBuf content = msg.content(); + MutableDirectByteBuf mutableDirectByteBuf = new MutableDirectByteBuf(content); + Frame from = Frame.from(mutableDirectByteBuf, 0, mutableDirectByteBuf.capacity()); + channelRegistered(ctx); + ServerWebSocketDuplexConnection connection = duplexConnections.computeIfAbsent(ctx.channel().id(), i -> { + System.out.println("No connection found for channel id: " + i); + ServerWebSocketDuplexConnection c = new ServerWebSocketDuplexConnection(ctx); + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromServerConnection(c, setupHandler, leaseGovernor, throwable -> throwable.printStackTrace()); + reactiveSocket.startAndWait(); + return c; + }); + if (connection != null) { + connection + .getSubscribers() + .forEach(o -> o.onNext(from)); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + + logger.error("caught an unhandled exception", cause); + } +} diff --git a/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ServerWebSocketDuplexConnection.java b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ServerWebSocketDuplexConnection.java new file mode 100644 index 000000000..a1ae2c2f5 --- /dev/null +++ b/reactivesocket-transport-netty/src/main/java/io/reactivesocket/netty/websocket/server/ServerWebSocketDuplexConnection.java @@ -0,0 +1,125 @@ +/** + * 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.reactivesocket.netty.websocket.server; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.reactivesocket.DuplexConnection; +import io.reactivesocket.Frame; +import io.reactivesocket.rx.Completable; +import io.reactivesocket.rx.Observable; +import io.reactivesocket.rx.Observer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ServerWebSocketDuplexConnection implements DuplexConnection { + private final CopyOnWriteArrayList> subjects; + + private final ChannelHandlerContext ctx; + + public ServerWebSocketDuplexConnection(ChannelHandlerContext ctx) { + this.subjects = new CopyOnWriteArrayList<>(); + this.ctx = ctx; + } + + public List> getSubscribers() { + return subjects; + } + + @Override + public final Observable getInput() { + return o -> { + o.onSubscribe(() -> subjects.removeIf(s -> s == o)); + subjects.add(o); + }; + } + + @Override + public void addOutput(Publisher o, Completable callback) { + o.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Frame frame) { + try { + ByteBuffer data = frame.getByteBuffer(); + ByteBuf byteBuf = Unpooled.wrappedBuffer(data); + BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(byteBuf); + ChannelFuture channelFuture = ctx.writeAndFlush(binaryWebSocketFrame); + channelFuture.addListener(future -> { + Throwable cause = future.cause(); + if (cause != null) { + cause.printStackTrace(); + callback.error(cause); + } + }); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable t) { + callback.error(t); + } + + @Override + public void onComplete() { + callback.success(); + } + }); + } + + @Override + public double availability() { + return ctx.channel().isOpen() ? 1.0 : 0.0; + } + + @Override + public void close() throws IOException { + + } + + public String toString() { + if (ctx ==null || ctx.channel() == null) { + return getClass().getName() + ":channel=null"; + } + + Channel channel = ctx.channel(); + return getClass().getName() + ":channel=[" + + "remoteAddress=" + channel.remoteAddress() + "," + + "isActive=" + channel.isActive() + "," + + "isOpen=" + channel.isOpen() + "," + + "isRegistered=" + channel.isRegistered() + "," + + "isWritable=" + channel.isWritable() + "," + + "channelId=" + channel.id().asLongText() + + "]"; + + } +} diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/ClientServerTest.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/ClientServerTest.java new file mode 100644 index 000000000..a04768947 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/ClientServerTest.java @@ -0,0 +1,212 @@ +/** + * 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.reactivesocket.netty.tcp; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.netty.tcp.client.ClientTcpDuplexConnection; +import io.reactivesocket.netty.tcp.server.ReactiveSocketServerHandler; +import io.reactivesocket.test.TestUtil; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public class ClientServerTest { + + static ReactiveSocket client; + static Channel serverChannel; + + static EventLoopGroup bossGroup = new NioEventLoopGroup(1); + static EventLoopGroup workerGroup = new NioEventLoopGroup(4); + + static ReactiveSocketServerHandler serverHandler = ReactiveSocketServerHandler.create((setupPayload, rs) -> + new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return s -> { + //System.out.println("Handling request/response payload => " + s.toString()); + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + s.onNext(response); + s.onComplete(); + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response) + .repeat()); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + } + ); + + @BeforeClass + public static void setup() throws Exception { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(serverHandler); + } + }); + + serverChannel = b.bind("localhost", 7878).sync().channel(); + + ClientTcpDuplexConnection duplexConnection = RxReactiveStreams.toObservable( + ClientTcpDuplexConnection.create(InetSocketAddress.createUnresolved("localhost", 7878), new NioEventLoopGroup()) + ).toBlocking().single(); + + client = DefaultReactiveSocket + .fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8"), t -> t.printStackTrace()); + + client.startAndWait(); + } + + @AfterClass + public static void tearDown() { + serverChannel.close(); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + + @Test + public void testRequestResponse1() { + requestResponseN(1500, 1); + } + + @Test + public void testRequestResponse10() { + requestResponseN(1500, 10); + } + + + @Test + public void testRequestResponse100() { + requestResponseN(1500, 100); + } + + @Test + public void testRequestResponse10_000() { + requestResponseN(60_000, 10_000); + } + + @Test + public void testRequestStream() { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestStream(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .subscribe(ts); + + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testRequestSubscription() throws InterruptedException { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestSubscription( + TestUtil.utf8EncodedPayload("hello sub", "metadata sub"))) + .take(10) + .subscribe(ts); + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + } + + + public void requestResponseN(int timeout, int count) { + + TestSubscriber ts = TestSubscriber.create(); + + Observable + .range(1, count) + .flatMap(i -> + RxReactiveStreams + .toObservable(client + .requestResponse(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .map(payload -> TestUtil.byteToString(payload.getData())) + ) + .doOnError(Throwable::printStackTrace) + .subscribe(ts); + + ts.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + ts.assertValueCount(count); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + +} \ No newline at end of file diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Ping.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Ping.java new file mode 100644 index 000000000..61905bf71 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Ping.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.reactivesocket.netty.tcp; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.netty.tcp.client.ClientTcpDuplexConnection; +import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscriber; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Ping { + public static void main(String... args) throws Exception { + Publisher publisher = ClientTcpDuplexConnection + .create(InetSocketAddress.createUnresolved("localhost", 7878), new NioEventLoopGroup(1)); + + ClientTcpDuplexConnection duplexConnection = RxReactiveStreams.toObservable(publisher).toBlocking().last(); + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8"), t -> t.printStackTrace()); + + reactiveSocket.startAndWait(); + + byte[] data = "hello".getBytes(); + + Payload keyPayload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(data); + } + + @Override + public ByteBuffer getMetadata() { + return null; + } + }; + + int n = 1_000_000; + CountDownLatch latch = new CountDownLatch(n); + final Recorder histogram = new Recorder(3600000000000L, 3); + + Schedulers + .computation() + .createWorker() + .schedulePeriodically(() -> { + System.out.println("---- PING/ PONG HISTO ----"); + histogram.getIntervalHistogram() + .outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- PING/ PONG HISTO ----"); + }, 1, 1, TimeUnit.SECONDS); + + Observable + .range(1, Integer.MAX_VALUE) + .flatMap(i -> { + long start = System.nanoTime(); + + return RxReactiveStreams + .toObservable( + reactiveSocket + .requestResponse(keyPayload)) + .doOnError(t -> t.printStackTrace()) + .doOnNext(s -> { + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + }); + }, 16) + .doOnError(t -> t.printStackTrace()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + latch.countDown(); + } + }); + + latch.await(1, TimeUnit.HOURS); + System.out.println("Sent => " + n); + System.exit(0); + } +} diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Pong.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Pong.java new file mode 100644 index 000000000..76cc1bac2 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/tcp/Pong.java @@ -0,0 +1,172 @@ +/** + * 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.reactivesocket.netty.tcp; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivesocket.Payload; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.netty.tcp.server.ReactiveSocketServerHandler; +import io.reactivesocket.test.TestUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.nio.ByteBuffer; +import java.util.Random; + +public class Pong { + public static void main(String... args) throws Exception { + byte[] response = new byte[1024]; + Random r = new Random(); + r.nextBytes(response); + + ReactiveSocketServerHandler serverHandler = + ReactiveSocketServerHandler.create((setupPayload, rs) -> new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + Payload responsePayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(response); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + s.onNext(responsePayload); + s.onComplete(); + } + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + Observable observable = + RxReactiveStreams + .toObservable(inputs) + .map(input -> input); + return RxReactiveStreams.toPublisher(observable); + +// return outputSubscriber -> { +// inputs.subscribe(new Subscriber() { +// private int count = 0; +// private boolean completed = false; +// +// @Override +// public void onSubscribe(Subscription s) { +// //outputSubscriber.onSubscribe(s); +// s.request(128); +// } +// +// @Override +// public void onNext(Payload input) { +// if (completed) { +// return; +// } +// count += 1; +// outputSubscriber.onNext(input); +// outputSubscriber.onNext(input); +// if (count > 10) { +// completed = true; +// outputSubscriber.onComplete(); +// } +// } +// +// @Override +// public void onError(Throwable t) { +// if (!completed) { +// outputSubscriber.onError(t); +// } +// } +// +// @Override +// public void onComplete() { +// if (!completed) { +// outputSubscriber.onComplete(); +// } +// } +// }); +// }; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }); + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(serverHandler); + } + }); + + Channel localhost = b.bind("localhost", 7878).sync().channel(); + localhost.closeFuture().sync(); + + } +} diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/ClientServerTest.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/ClientServerTest.java new file mode 100644 index 000000000..6b4deee30 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/ClientServerTest.java @@ -0,0 +1,219 @@ +/** + * 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.reactivesocket.netty.websocket; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.netty.websocket.client.ClientWebSocketDuplexConnection; +import io.reactivesocket.netty.websocket.server.ReactiveSocketServerHandler; +import io.reactivesocket.test.TestUtil; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.observers.TestSubscriber; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public class ClientServerTest { + + static ReactiveSocket client; + static Channel serverChannel; + + static EventLoopGroup bossGroup = new NioEventLoopGroup(1); + static EventLoopGroup workerGroup = new NioEventLoopGroup(4); + + static ReactiveSocketServerHandler serverHandler = ReactiveSocketServerHandler.create((setupPayload, rs) -> + new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return s -> { + //System.out.println("Handling request/response payload => " + s.toString()); + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + s.onNext(response); + s.onComplete(); + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = TestUtil.utf8EncodedPayload("hello world", "metadata"); + + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response) + .repeat()); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + return null; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + } + ); + + @BeforeClass + public static void setup() throws Exception { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(64 * 1024)); + pipeline.addLast(new WebSocketServerProtocolHandler("/rs")); + pipeline.addLast(serverHandler); + } + }); + + serverChannel = b.bind("localhost", 8025).sync().channel(); + + ClientWebSocketDuplexConnection duplexConnection = RxReactiveStreams.toObservable( + ClientWebSocketDuplexConnection.create(InetSocketAddress.createUnresolved("localhost", 8025), "/rs", new NioEventLoopGroup()) + ).toBlocking().single(); + + client = DefaultReactiveSocket + .fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8"), t -> t.printStackTrace()); + + client.startAndWait(); + + } + + @AfterClass + public static void tearDown() { + serverChannel.close(); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + + @Test + public void testRequestResponse1() { + requestResponseN(1500, 1); + } + + @Test + public void testRequestResponse10() { + requestResponseN(1500, 10); + } + + + @Test + public void testRequestResponse100() { + requestResponseN(1500, 100); + } + + @Test + public void testRequestResponse10_000() { + requestResponseN(60_000, 10_000); + } + + @Test + public void testRequestStream() { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestStream(TestUtil.utf8EncodedPayload("hello", "metadata"))) + .subscribe(ts); + + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testRequestSubscription() throws InterruptedException { + TestSubscriber ts = TestSubscriber.create(); + + RxReactiveStreams + .toObservable(client.requestSubscription( + TestUtil.utf8EncodedPayload("hello sub", "metadata sub"))) + .take(10) + .subscribe(ts); + + ts.awaitTerminalEvent(3_000, TimeUnit.MILLISECONDS); + ts.assertValueCount(10); + ts.assertNoErrors(); + } + + + public void requestResponseN(int timeout, int count) { + + TestSubscriber ts = TestSubscriber.create(); + + Observable + .range(1, count) + .flatMap(i -> + RxReactiveStreams + .toObservable(client.requestResponse( + TestUtil.utf8EncodedPayload("hello", "metadata"))) + .map(payload -> TestUtil.byteToString(payload.getData())) + ) + .doOnError(Throwable::printStackTrace) + .subscribe(ts); + + ts.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + ts.assertValueCount(count); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + +} \ No newline at end of file diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Ping.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Ping.java new file mode 100644 index 000000000..0275983a7 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Ping.java @@ -0,0 +1,108 @@ +/** + * 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.reactivesocket.netty.websocket; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.reactivesocket.ConnectionSetupPayload; +import io.reactivesocket.DefaultReactiveSocket; +import io.reactivesocket.Payload; +import io.reactivesocket.ReactiveSocket; +import io.reactivesocket.netty.websocket.client.ClientWebSocketDuplexConnection; +import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscriber; +import rx.schedulers.Schedulers; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Ping { + public static void main(String... args) throws Exception { + Publisher publisher = ClientWebSocketDuplexConnection.create(InetSocketAddress.createUnresolved("localhost", 8025), "/rs", new NioEventLoopGroup(1)); + + ClientWebSocketDuplexConnection duplexConnection = RxReactiveStreams.toObservable(publisher).toBlocking().last(); + ReactiveSocket reactiveSocket = DefaultReactiveSocket.fromClientConnection(duplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8"), t -> t.printStackTrace()); + + reactiveSocket.startAndWait(); + + byte[] data = "hello".getBytes(); + + Payload keyPayload = new Payload() { + @Override + public ByteBuffer getData() { + return ByteBuffer.wrap(data); + } + + @Override + public ByteBuffer getMetadata() { + return null; + } + }; + + int n = 1_000_000; + CountDownLatch latch = new CountDownLatch(n); + final Recorder histogram = new Recorder(3600000000000L, 3); + + Schedulers + .computation() + .createWorker() + .schedulePeriodically(() -> { + System.out.println("---- PING/ PONG HISTO ----"); + histogram.getIntervalHistogram() + .outputPercentileDistribution(System.out, 5, 1000.0, false); + System.out.println("---- PING/ PONG HISTO ----"); + }, 1, 1, TimeUnit.SECONDS); + + Observable + .range(1, Integer.MAX_VALUE) + .flatMap(i -> { + long start = System.nanoTime(); + + return RxReactiveStreams + .toObservable( + reactiveSocket + .requestResponse(keyPayload)) + .doOnNext(s -> { + long diff = System.nanoTime() - start; + histogram.recordValue(diff); + }); + }, 16) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Payload payload) { + latch.countDown(); + } + }); + + latch.await(1, TimeUnit.HOURS); + System.out.println("Sent => " + n); + System.exit(0); + } +} diff --git a/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Pong.java b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Pong.java new file mode 100644 index 000000000..bb9c7ab3f --- /dev/null +++ b/reactivesocket-transport-netty/src/test/java/io/reactivesocket/netty/websocket/Pong.java @@ -0,0 +1,178 @@ +/** + * 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.reactivesocket.netty.websocket; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.reactivesocket.Payload; +import io.reactivesocket.RequestHandler; +import io.reactivesocket.netty.websocket.server.ReactiveSocketServerHandler; +import io.reactivesocket.test.TestUtil; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import rx.Observable; +import rx.RxReactiveStreams; + +import java.nio.ByteBuffer; +import java.util.Random; + +public class Pong { + public static void main(String... args) throws Exception { + byte[] response = new byte[1024]; + Random r = new Random(); + r.nextBytes(response); + + ReactiveSocketServerHandler serverHandler = + ReactiveSocketServerHandler.create((setupPayload, rs) -> new RequestHandler() { + @Override + public Publisher handleRequestResponse(Payload payload) { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + Payload responsePayload = new Payload() { + ByteBuffer data = ByteBuffer.wrap(response); + ByteBuffer metadata = ByteBuffer.allocate(0); + + public ByteBuffer getData() { + return data; + } + + @Override + public ByteBuffer getMetadata() { + return metadata; + } + }; + + s.onNext(responsePayload); + s.onComplete(); + } + }; + } + + @Override + public Publisher handleRequestStream(Payload payload) { + Payload response = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleSubscription(Payload payload) { + Payload response = + TestUtil.utf8EncodedPayload("hello world", "metadata"); + return RxReactiveStreams + .toPublisher(Observable + .range(1, 10) + .map(i -> response)); + } + + @Override + public Publisher handleFireAndForget(Payload payload) { + return Subscriber::onComplete; + } + + @Override + public Publisher handleChannel(Payload initialPayload, Publisher inputs) { + Observable observable = + RxReactiveStreams + .toObservable(inputs) + .map(input -> input); + return RxReactiveStreams.toPublisher(observable); + +// return outputSubscriber -> { +// inputs.subscribe(new Subscriber() { +// private int count = 0; +// private boolean completed = false; +// +// @Override +// public void onSubscribe(Subscription s) { +// //outputSubscriber.onSubscribe(s); +// s.request(128); +// } +// +// @Override +// public void onNext(Payload input) { +// if (completed) { +// return; +// } +// count += 1; +// outputSubscriber.onNext(input); +// outputSubscriber.onNext(input); +// if (count > 10) { +// completed = true; +// outputSubscriber.onComplete(); +// } +// } +// +// @Override +// public void onError(Throwable t) { +// if (!completed) { +// outputSubscriber.onError(t); +// } +// } +// +// @Override +// public void onComplete() { +// if (!completed) { +// outputSubscriber.onComplete(); +// } +// } +// }); +// }; + } + + @Override + public Publisher handleMetadataPush(Payload payload) { + return null; + } + }); + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(64 * 1024)); + pipeline.addLast(new WebSocketServerProtocolHandler("/rs")); + pipeline.addLast(serverHandler); + } + }); + + Channel localhost = b.bind("localhost", 8025).sync().channel(); + localhost.closeFuture().sync(); + + } +} diff --git a/reactivesocket-transport-netty/src/test/resources/simplelogger.properties b/reactivesocket-transport-netty/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..e82e3ef30 --- /dev/null +++ b/reactivesocket-transport-netty/src/test/resources/simplelogger.properties @@ -0,0 +1,35 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +#org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=info + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=true + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss + +# Set to true if you want to output the current thread name. +# Defaults to true. +org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5aea80f18..47d88f0fe 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,8 @@ rootProject.name='reactivesocket' +include 'reactivesocket-core' +include 'reactivesocket-transport-aeron' +include 'reactivesocket-transport-jsr-356' +include 'reactivesocket-transport-netty' +include 'reactivesocket-transport-local' +include 'reactivesocket-mime-types' +include 'reactivesocket-test'