diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 00000000..28418cdb --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +demos/bazel diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e83c6b5..6693c821 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ shared: &shared # fallback to using the latest cache if no exact match is found - v1-dependencies- - - run: mvn install + - run: mvn install -DexcludedGroups=unstable - save_cache: paths: @@ -39,9 +39,15 @@ jobs: - image: circleci/openjdk:11-jdk <<: *shared + java-17: + docker: + - image: circleci/openjdk:17-jdk-buster + <<: *shared + workflows: version: 2 java-8-and-11: jobs: - java-8 - - java-11 \ No newline at end of file + - java-11 + - java-17 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 55aea38e..331f88ef 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ dependency-reduced-pom.xml .settings/ .checkstyle .classpath -.project \ No newline at end of file +.project +bazel-* +.ijwb/ diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..62593409 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +1.8 diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 00000000..e69de29b diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..a3ce096a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. +#ECCN:Open Source +#GUSINFO:Open Source,Open Source Workflow diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..0874341f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,105 @@ +# Salesforce Open Source Community Code of Conduct + +## About the Code of Conduct + +Equality is a core value at Salesforce. We believe a diverse and inclusive +community fosters innovation and creativity, and are committed to building a +culture where everyone feels included. + +Salesforce open-source projects are committed to providing a friendly, safe, and +welcoming environment for all, regardless of gender identity and expression, +sexual orientation, disability, physical appearance, body size, ethnicity, nationality, +race, age, religion, level of experience, education, socioeconomic status, or +other similar personal characteristics. + +The goal of this code of conduct is to specify a baseline standard of behavior so +that people with different social values and communication styles can work +together effectively, productively, and respectfully in our open source community. +It also establishes a mechanism for reporting issues and resolving conflicts. + +All questions and reports of abusive, harassing, or otherwise unacceptable behavior +in a Salesforce open-source project may be reported by contacting the Salesforce +Open Source Conduct Committee at ossconduct@salesforce.com. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of gender +identity and expression, sexual orientation, disability, physical appearance, +body size, ethnicity, nationality, race, age, religion, level of experience, education, +socioeconomic status, or other similar personal characteristics. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy toward other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Personal attacks, insulting/derogatory comments, or trolling +* Public or private harassment +* Publishing, or threatening to publish, others' private information—such as + a physical or electronic address—without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting +* Advocating for or encouraging any of the above behaviors + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned with this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project email +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the Salesforce Open Source Conduct Committee +at ossconduct@salesforce.com. All complaints will be reviewed and investigated +and will result in a response that is deemed necessary and appropriate to the +circumstances. The committee is obligated to maintain confidentiality with +regard to the reporter of an incident. Further details of specific enforcement +policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership and the Salesforce Open Source Conduct +Committee. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], +version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. +It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], +[CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. + +This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. + +[contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) +[golang-coc]: https://golang.org/conduct +[cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md +[microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ +[cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..958fa914 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing Guide For reactive-grpc + +This page lists the operational governance model of this project, as well as the recommendations and requirements for +how to best contribute to reactive-grpc. We strive to obey these as best as possible. As always, thanks for contributing – +we hope these guidelines make it easier and shed some light on our approach and processes. + +# Governance Model + +## Community Based + +The intent and goal of open sourcing this project is to increase the contributor and user base. The governance model is +one where new project leads (`admins`) will be added to the project based on their contributions and efforts, a so-called +"do-acracy" or "meritocracy" similar to that used by all Apache Software Foundation projects. + +# Issues, requests & ideas + +Use GitHub Issues page to submit issues, enhancement requests and discuss ideas. + +### Bug Reports and Fixes +- If you find a bug, please search for it in the [Issues](https://github.com/reactive-grpc/issues), and if it isn't + already tracked, [create a new issue](https://github.com/reactive-grpc/issues/new). Fill out the "Bug Report" section + of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still be reviewed. +- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. +- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. +- Include tests that isolate the bug and verifies that it was fixed. + +### New Features +- If you'd like to add new functionality to this project, describe the problem you want to solve in a + [new Issue](https://github.com/reactive-grpc/issues/new). +- Issues that have been identified as a feature request will be labelled `enhancement`. +- If you'd like to implement the new feature, please wait for feedback from the project + maintainers before spending too much time writing the code. In some cases, `enhancement`s may + not align well with the project objectives at the time. + +### Tests, Documentation, Miscellaneous +- If you'd like to improve the tests, you want to make the documentation clearer, you have an + alternative implementation of something that may have advantages over the way its currently + done, or you have any other change, we would be happy to hear about it! +- If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. +- If not, [open an Issue](https://github.com/reactive-grpc/issues/new) to discuss the idea first. + +If you're new to our project and looking for some way to make your first contribution, look for +Issues labelled `good first contribution`. + +# Contribution Checklist + +- [x] Clean, simple, well styled code +- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. +- [x] Comments + - Module-level & function-level comments. + - Comments on complex blocks of code or algorithms (include references to sources). +- [x] Tests + - The test suite, if provided, must be complete and pass + - Increase code coverage, not versa. +- [x] Dependencies + - Minimize number of dependencies. + - Prefer Apache 2.0, BSD3, MIT, ISC and MPL licenses. +- [x] Reviews + - Changes must be approved via peer code review + +# Creating a Pull Request + +1. **Ensure the bug/feature was not already reported** by searching on GitHub under Issues. If none exists, create a + new issue so that other contributors can keep track of what you are trying to add/fix and offer suggestions (or let + you know if there is already an effort in progress). +2. **Clone** the forked repo to your machine. +3. **Create** a new branch to contain your work (e.g. `git br fix-issue-11`) +4. **Commit** changes to your own branch. +5. **Push** your work back up to your fork. (e.g. `git push fix-issue-11`) +6. **Submit** a Pull Request against the `main` branch and refer to the issue(s) you are fixing. Try not to pollute + your pull request with unintended changes. Keep it simple and small. +7. **Sign** the Salesforce CLA (you will be prompted to do so when submitting the Pull Request) + +> **NOTE**: Be sure to [sync your fork](https://help.github.com/articles/syncing-a-fork/) before making a pull request. + +# Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Salesforce's open source projects. + +Complete your CLA here: + +# Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +# Code of Conduct +Please follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +# License +By contributing your code, you agree to license your contribution under the terms of our project [LICENSE](LICENSE.txt) and +to sign the [Salesforce CLA](https://cla.salesforce.com/sign-cla) \ No newline at end of file diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index eb2b6309..574e1acc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![CircleCI](https://circleci.com/gh/salesforce/reactive-grpc/tree/master.svg?style=svg)](https://circleci.com/gh/salesforce/reactive-grpc/tree/master) - # What is reactive-grpc? Reactive gRPC is a suite of libraries for using gRPC with [Reactive Streams](http://www.reactive-streams.org/) programming libraries. Using a protocol buffers compiler plugin, Reactive gRPC generates alternative gRPC bindings for each reactive technology. @@ -7,17 +5,22 @@ The reactive bindings support unary and streaming operations in both directions. back-pressure support, to deliver end-to-end back-pressure-based flow control in line with Reactive Streams back-pressure model. +:warning: Reactive-gRPC is [effectively paused](https://github.com/salesforce/reactive-grpc/issues/337). It's seeking +a new home and maintainers. + Reactive gRPC supports the following reactive programming models: * [RxJava 2](https://github.com/salesforce/reactive-grpc/tree/master/rx-java) +* [RxJava 3](https://github.com/salesforce/reactive-grpc/tree/master/rx3-java) * [Spring Reactor](https://github.com/salesforce/reactive-grpc/tree/master/reactor) -* (Eventually) [Java9 Flow](https://community.oracle.com/docs/DOC-1006738) -* (Eventually) [Akka Streams](https://doc.akka.io/docs/akka/2.5/stream/index.html) + +[Akka gRPC](https://github.com/akka/akka-grpc) is now mature and production ready. Use that for Akka-based services. # Usage See the readme in each technology-specific sub-directory for usage details. * [Rx-Java](https://github.com/salesforce/reactive-grpc/tree/master/rx-java) +* [Rx3-Java](https://github.com/salesforce/reactive-grpc/tree/master/rx3-java) * [Spring Reactor](https://github.com/salesforce/reactive-grpc/tree/master/reactor) # Demos @@ -27,7 +30,7 @@ See the readme in each technology-specific sub-directory for usage details. # Android support Reactive gRPC supports Android to the same level of the underlying reactive technologies. -* Rx-Java - Generated code targets Java 6, so it _should_ work with all versions of Android >= 2.3 (SDK 9). +* Rx-Java - Generated code targets Java 8, so it _should_ work with Android. * Spring Reactor - [Not officially supported.](http://projectreactor.io/docs/core/release/reference/docs/index.html#prerequisites) "Reactor 3 does not officially support or target Android, however, it should work fine with Android SDK 26 (Android O) and above." diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..e31774df --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +## Security + +Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) +as soon as it is discovered. This library limits its runtime dependencies in +order to reduce the total cost of ownership as much as can be, but all consumers +should remain vigilant and have their security stakeholders review all third-party +products (3PP) like this one and their dependencies. \ No newline at end of file diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000..a2e69038 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,9 @@ +workspace(name = "com_salesforce_servicelibs_reactive_grpc") + +load("//bazel:repositories.bzl", "repositories") + +repositories() + +load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") + +grpc_java_repositories() diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel new file mode 100644 index 00000000..e69de29b diff --git a/bazel/java_reactive_grpc_library.bzl b/bazel/java_reactive_grpc_library.bzl new file mode 100644 index 00000000..a80d4b4d --- /dev/null +++ b/bazel/java_reactive_grpc_library.bzl @@ -0,0 +1,145 @@ +load("@bazel_tools//tools/jdk:toolchain_utils.bzl", "find_java_runtime_toolchain", "find_java_toolchain") + +# Taken from bazelbuild/rules_go license: Apache 2 +# https://github.com/bazelbuild/rules_go/blob/528f6faf83f85c23da367d61f784893d1b3bd72b/proto/compiler.bzl#L94 +# replaced `prefix = paths.join(..` with `prefix = "/".join(..` +def _proto_path(src, proto): + """proto_path returns the string used to import the proto. This is the proto + source path within its repository, adjusted by import_prefix and + strip_import_prefix. + + Args: + src: the proto source File. + proto: the ProtoInfo provider. + + Returns: + An import path string. + """ + if not hasattr(proto, "proto_source_root"): + # Legacy path. Remove when Bazel minimum version >= 0.21.0. + path = src.path + root = src.root.path + ws = src.owner.workspace_root + if path.startswith(root): + path = path[len(root):] + if path.startswith("/"): + path = path[1:] + if path.startswith(ws): + path = path[len(ws):] + if path.startswith("/"): + path = path[1:] + return path + + if proto.proto_source_root == ".": + # true if proto sources were generated + prefix = src.root.path + "/" + elif proto.proto_source_root.startswith(src.root.path): + # sometimes true when import paths are adjusted with import_prefix + prefix = proto.proto_source_root + "/" + else: + # usually true when paths are not adjusted + prefix = "/".join([src.root.path, proto.proto_source_root]) + "/" + if not src.path.startswith(prefix): + # sometimes true when importing multiple adjusted protos + return src.path + return src.path[len(prefix):] + +def _reactive_grpc_library_impl(ctx): + proto = ctx.attr.proto[ProtoInfo] + descriptor_set_in = proto.transitive_descriptor_sets + + gensrcjar = ctx.actions.declare_file("%s-proto-gensrc.jar" % ctx.label.name) + + args = ctx.actions.args() + args.add(ctx.executable.reactive_plugin.path, format = "--plugin=protoc-gen-reactive-grpc-plugin=%s") + args.add("--reactive-grpc-plugin_out=:{0}".format(gensrcjar.path)) + args.add_joined("--descriptor_set_in", descriptor_set_in, join_with = ctx.host_configuration.host_path_separator) + for src in proto.check_deps_sources.to_list(): + args.add(_proto_path(src, proto)) + + ctx.actions.run( + inputs = descriptor_set_in, + tools = [ctx.executable.reactive_plugin], + outputs = [gensrcjar], + executable = ctx.executable._protoc, + arguments = [args], + ) + + deps = [java_common.make_non_strict(dep[JavaInfo]) for dep in ctx.attr.deps] + deps += [dep[JavaInfo] for dep in ctx.attr.reactive_deps] + + java_info = java_common.compile( + ctx, + deps = deps, + host_javabase = find_java_runtime_toolchain(ctx, ctx.attr._host_javabase), + java_toolchain = find_java_toolchain(ctx, ctx.attr._java_toolchain), + output = ctx.outputs.jar, + output_source_jar = ctx.outputs.srcjar, + source_jars = [gensrcjar], + ) + + return [java_info] + +_reactive_grpc_library = rule( + attrs = { + "proto": attr.label( + mandatory = True, + providers = [ProtoInfo], + ), + "deps": attr.label_list( + mandatory = True, + allow_empty = False, + providers = [JavaInfo], + ), + "_protoc": attr.label( + default = Label("@com_google_protobuf//:protoc"), + executable = True, + cfg = "host", + ), + "reactive_deps": attr.label_list( + mandatory = True, + allow_empty = False, + providers = [JavaInfo], + ), + "reactive_plugin": attr.label( + mandatory = True, + executable = True, + cfg = "host", + ), + "_java_toolchain": attr.label( + default = Label("@bazel_tools//tools/jdk:current_java_toolchain"), + ), + "_host_javabase": attr.label( + cfg = "host", + default = Label("@bazel_tools//tools/jdk:current_host_java_runtime"), + ), + }, + fragments = ["java"], + outputs = { + "jar": "lib%{name}.jar", + "srcjar": "lib%{name}-src.jar", + }, + provides = [JavaInfo], + implementation = _reactive_grpc_library_impl, +) + +def reactor_grpc_library(**kwargs): + _reactive_grpc_library( + reactive_plugin = "@com_salesforce_servicelibs_reactive_grpc//reactor/reactor-grpc:reactor_grpc_bin", + reactive_deps = [ + "@com_salesforce_servicelibs_reactive_grpc//reactor/reactor-grpc-stub", + "@io_projectreactor_reactor_core", + ], + **kwargs + ) + +def rx_grpc_library(**kwargs): + _reactive_grpc_library( + reactive_plugin = "@com_salesforce_servicelibs_reactive_grpc//rx-java/rxgrpc:rxgrpc_bin", + reactive_deps = [ + "@com_salesforce_servicelibs_reactive_grpc//rx-java/rxgrpc-stub", + "@com_salesforce_servicelibs_reactive_grpc//common/reactive-grpc-common", + "@io_reactivex_rxjava2_rxjava", + ], + **kwargs + ) diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl new file mode 100644 index 00000000..ca61df44 --- /dev/null +++ b/bazel/repositories.bzl @@ -0,0 +1,69 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:java.bzl", "java_import_external") + +def repositories( + omit_org_reactivestreams_reactive_streams = False, + omit_io_projectreactor_reactor_core = False, + omit_io_reactivex_rxjava2_rxjava = False, + omit_io_grpc_grpc_java = False, + omit_com_salesforce_servicelibs_jprotoc = False, + omit_com_github_spullara_mustache_java_compiler = False): + if not omit_org_reactivestreams_reactive_streams: + java_import_external( + name = "org_reactivestreams_reactive_streams", + jar_urls = ["https://repo.maven.apache.org/maven2/org/reactivestreams/reactive-streams/1.0.2/reactive-streams-1.0.2.jar"], + jar_sha256 = "cc09ab0b140e0d0496c2165d4b32ce24f4d6446c0a26c5dc77b06bdf99ee8fae", + srcjar_urls = ["https://repo.maven.apache.org/maven2/org/reactivestreams/reactive-streams/1.0.2/reactive-streams-1.0.2-sources.jar"], + srcjar_sha256 = "963a6480f46a64013d0f144ba41c6c6e63c4d34b655761717a436492886f3667", + licenses = ["notice"], # Apache 2.0 + ) + + if not omit_io_projectreactor_reactor_core: + java_import_external( + name = "io_projectreactor_reactor_core", + jar_urls = ["https://repo.maven.apache.org/maven2/io/projectreactor/reactor-core/3.2.6.RELEASE/reactor-core-3.2.6.RELEASE.jar"], + jar_sha256 = "8962081aa9e0fbe1685cc2746471b232f93f58e269cc89b54efebcb99c65af1a", + srcjar_urls = ["https://repo.maven.apache.org/maven2/io/projectreactor/reactor-core/3.2.6.RELEASE/reactor-core-3.2.6.RELEASE-sources.jar"], + srcjar_sha256 = "b871669ed12aee2af22f20a611bf871c7f848caede85bc19b0ef08ef5f79bc46", + licenses = ["notice"], # Apache 2.0 + ) + + if not omit_io_reactivex_rxjava2_rxjava: + java_import_external( + name = "io_reactivex_rxjava2_rxjava", + jar_urls = ["https://repo.maven.apache.org/maven2/io/reactivex/rxjava2/rxjava/2.2.7/rxjava-2.2.7.jar"], + jar_sha256 = "23798f1b5fecac2aaaa3e224fd0e73f41dc081802c7bd2a6e91030bad36b9013", + srcjar_urls = ["https://repo.maven.apache.org/maven2/io/reactivex/rxjava2/rxjava/2.2.7/rxjava-2.2.7-sources.jar"], + srcjar_sha256 = "b7ee7e2b2ce07eda19755e511757427701f4081a051cace1efd69cf0bfcc8ff2", + licenses = ["notice"], # Apache 2.0 + ) + + if not omit_io_grpc_grpc_java: + io_grpc_grpc_java_version = "v1.21.0" + + http_archive( + name = "io_grpc_grpc_java", + sha256 = "2137a2b568e8266d6c269c995c7ba68db3d8d7d7936087c540fdbfadae577f81", + strip_prefix = "grpc-java-%s" % io_grpc_grpc_java_version[1:], + urls = ["https://github.com/grpc/grpc-java/archive/%s.zip" % io_grpc_grpc_java_version], + ) + + if not omit_com_salesforce_servicelibs_jprotoc: + java_import_external( + name = "com_salesforce_servicelibs_jprotoc", + jar_urls = ["https://repo.maven.apache.org/maven2/com/salesforce/servicelibs/jprotoc/0.9.1/jprotoc-0.9.1.jar"], + jar_sha256 = "55d78aafa930693856055e7d1d63414670beb59a9b253ece5cf546541b4bbd07", + srcjar_urls = ["https://repo.maven.apache.org/maven2/com/salesforce/servicelibs/jprotoc/0.9.1/jprotoc-0.9.1-sources.jar"], + srcjar_sha256 = "ba023a2097874fa7131c277eab69ca748928627bea122a48ef9cb54ca8dafd91", + licenses = ["notice"], # BSD 3-Clause + ) + + if not omit_com_github_spullara_mustache_java_compiler: + java_import_external( + name = "com_github_spullara_mustache_java_compiler", + jar_urls = ["https://repo.maven.apache.org/maven2/com/github/spullara/mustache/java/compiler/0.9.6/compiler-0.9.6.jar"], + jar_sha256 = "c4d697fd3619cb616cc5e22e9530c8a4fd4a8e9a76953c0655ee627cb2d22318", + srcjar_urls = ["https://repo.maven.apache.org/maven2/com/github/spullara/mustache/java/compiler/0.9.6/compiler-0.9.6-sources.jar"], + srcjar_sha256 = "fb3cf89e4daa0aaa4e659aca12a8ddb0d7b605271285f3e108201e0a389b4c7a", + licenses = ["notice"], # Apache 2.0 + ) diff --git a/common/reactive-grpc-benchmarks/pom.xml b/common/reactive-grpc-benchmarks/pom.xml index 90995369..ce883317 100644 --- a/common/reactive-grpc-benchmarks/pom.xml +++ b/common/reactive-grpc-benchmarks/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -30,6 +30,12 @@ reactor-grpc-stub ${project.version} + + io.projectreactor + reactor-core + ${reactor.version} + compile + ${project.groupId} rxgrpc-stub @@ -45,27 +51,25 @@ io.grpc - grpc-stub + grpc-api io.grpc - grpc-protobuf + grpc-stub - io.netty - netty-buffer - 4.1.33.Final - compile + io.grpc + grpc-protobuf org.openjdk.jmh jmh-core - 1.21 + 1.33 org.openjdk.jmh jmh-generator-annprocess - 1.21 + 1.33 org.hdrhistogram @@ -108,16 +112,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${compiler.plugin.version} - - 1.8 - 1.8 - - - org.xolstice.maven.plugins protobuf-maven-plugin @@ -178,4 +172,4 @@ - \ No newline at end of file + diff --git a/common/reactive-grpc-common/BUILD.bazel b/common/reactive-grpc-common/BUILD.bazel new file mode 100644 index 00000000..b20765fe --- /dev/null +++ b/common/reactive-grpc-common/BUILD.bazel @@ -0,0 +1,11 @@ +java_library( + name = "reactive-grpc-common", + srcs = glob(["src/main/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "@com_google_guava_guava", + "@io_grpc_grpc_java//core", + "@io_grpc_grpc_java//stub", + "@org_reactivestreams_reactive_streams", + ], +) diff --git a/common/reactive-grpc-common/pom.xml b/common/reactive-grpc-common/pom.xml index a866031b..841d4b9b 100644 --- a/common/reactive-grpc-common/pom.xml +++ b/common/reactive-grpc-common/pom.xml @@ -12,20 +12,13 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 reactive-grpc-common - - 1.7 - 1.8 - ${java.version} - ${java.version} - - javax.annotation @@ -41,6 +34,7 @@ io.grpc grpc-stub + org.junit.jupiter junit-jupiter-api @@ -78,6 +72,12 @@ 0.1.26 test + + com.github.akarnokd + rxjava3-extensions + 3.0.0 + test + org.awaitility awaitility @@ -97,29 +97,7 @@ - - - org.codehaus.mojo - animal-sniffer-maven-plugin - 1.7 - - - signature-check - verify - - check - - - - - - org.codehaus.mojo.signature - java16 - 1.0 - - - - + org.apache.maven.plugins maven-jar-plugin @@ -133,7 +111,7 @@ org.apache.felix maven-bundle-plugin - 4.1.0 + 5.1.2 bundle-manifest diff --git a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractClientStreamObserverAndPublisher.java b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractClientStreamObserverAndPublisher.java index 3d01f3db..872618af 100644 --- a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractClientStreamObserverAndPublisher.java +++ b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractClientStreamObserverAndPublisher.java @@ -36,6 +36,15 @@ public AbstractClientStreamObserverAndPublisher( super(queue, onSubscribe, onTerminate); } + public AbstractClientStreamObserverAndPublisher( + Queue queue, + Consumer> onSubscribe, + Runnable onTerminate, + int prefetch, + int lowTide) { + super(queue, prefetch, lowTide, onSubscribe, onTerminate); + } + @Override public void beforeStart(ClientCallStreamObserver requestStream) { super.onSubscribe(requestStream); diff --git a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractServerStreamObserverAndPublisher.java b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractServerStreamObserverAndPublisher.java index 41720e49..2273f6e2 100644 --- a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractServerStreamObserverAndPublisher.java +++ b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractServerStreamObserverAndPublisher.java @@ -15,24 +15,36 @@ import io.grpc.stub.ServerCallStreamObserver; /** - * The gRPC server-side implementation of - * {@link AbstractStreamObserverAndPublisher}. + * The gRPC server-side implementation of {@link AbstractStreamObserverAndPublisher}. * - * @param T + * @param + * T */ public abstract class AbstractServerStreamObserverAndPublisher - extends AbstractStreamObserverAndPublisher { + extends AbstractStreamObserverAndPublisher { private volatile boolean abandonDelayedCancel; public AbstractServerStreamObserverAndPublisher( - ServerCallStreamObserver serverCallStreamObserver, - Queue queue, - Consumer> onSubscribe) { + ServerCallStreamObserver serverCallStreamObserver, + Queue queue, + Consumer> onSubscribe + ) { super(queue, onSubscribe); super.onSubscribe(serverCallStreamObserver); } + public AbstractServerStreamObserverAndPublisher( + ServerCallStreamObserver serverCallStreamObserver, + Queue queue, + Consumer> onSubscribe, + int prefetch, + int lowTide + ) { + super(queue, prefetch, lowTide, onSubscribe); + super.onSubscribe(serverCallStreamObserver); + } + @Override public void onError(Throwable throwable) { // This condition is not an error and is safe to ignore. If the client dies unexpectedly, the server calls cancel. @@ -40,7 +52,10 @@ public void onError(Throwable throwable) { // If the cancel happens before a half-close, the ServerCallStreamObserver's cancellation handler // is run, and then a CANCELLED StatusRuntimeException is sent. The StatusRuntimeException can be ignored // because the subscription reactive stream has already been cancelled. - if (throwable instanceof StatusRuntimeException && throwable.getMessage().contains("cancelled before receiving half close")) { + if (throwable instanceof StatusRuntimeException && + (throwable.getMessage().contains("cancelled before receiving half close") || + throwable.getMessage().contains("CANCELLED: client cancelled")) + ) { return; } diff --git a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractStreamObserverAndPublisher.java b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractStreamObserverAndPublisher.java index 499544bf..b2afa64c 100644 --- a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractStreamObserverAndPublisher.java +++ b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractStreamObserverAndPublisher.java @@ -64,8 +64,8 @@ public void request(long n) { }; - protected static final int DEFAULT_CHUNK_SIZE = 512; - protected static final int TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE = DEFAULT_CHUNK_SIZE * 2 / 3; + public static final int DEFAULT_CHUNK_SIZE = 512; + public static final int TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE = DEFAULT_CHUNK_SIZE * 2 / 3; private static final int UNSUBSCRIBED_STATE = 0; private static final int SUBSCRIBED_ONCE_STATE = 1; @@ -85,7 +85,7 @@ public void request(long n) { private volatile boolean done; private Throwable error; - private volatile Subscriber downstream; + protected volatile Subscriber downstream; private volatile boolean cancelled; @@ -226,7 +226,7 @@ private void drainFused(final Subscriber subscriber) { for (;;) { if (cancelled) { - queue.clear(); + discardQueue(queue); downstream = null; return; } @@ -283,7 +283,7 @@ private void drain() { private boolean checkTerminated(boolean d, boolean empty, Subscriber subscriber, Queue q) { if (cancelled) { - q.clear(); + discardQueue(q); downstream = null; return true; } @@ -305,6 +305,7 @@ private boolean checkTerminated(boolean d, boolean empty, Subscriber @Override public void onNext(T t) { if (done || cancelled) { + discardElement(t); return; } @@ -419,7 +420,7 @@ public void cancel() { if (!outputFused) { if (WIP.getAndIncrement(this) == 0) { - queue.clear(); + discardQueue(queue); downstream = null; } } @@ -456,4 +457,10 @@ public boolean isEmpty() { public void clear() { queue.clear(); } + + protected void discardQueue(Queue q) { + q.clear(); + } + + protected void discardElement(T t) { } } \ No newline at end of file diff --git a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducer.java b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducer.java index 281404e4..9bc723f9 100644 --- a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducer.java +++ b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducer.java @@ -109,7 +109,7 @@ public void run() { public void cancel() { Subscription s = SUBSCRIPTION.getAndSet(this, CANCELLED_SUBSCRIPTION); - if (s != CANCELLED_SUBSCRIPTION) { + if (s != null && s != CANCELLED_SUBSCRIPTION) { s.cancel(); if (WIP.getAndIncrement(this) == 0) { @@ -443,7 +443,7 @@ private boolean checkTerminated(boolean d, boolean empty, CallStreamObserver return false; } - private static Throwable prepareError(Throwable throwable) { + protected Throwable prepareError(Throwable throwable) { if (throwable instanceof StatusException || throwable instanceof StatusRuntimeException) { return throwable; } else { diff --git a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndServerProducer.java b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndServerProducer.java index ff869c86..27f77085 100644 --- a/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndServerProducer.java +++ b/common/reactive-grpc-common/src/main/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndServerProducer.java @@ -18,14 +18,20 @@ public abstract class AbstractSubscriberAndServerProducer extends AbstractSubscriberAndProducer { + private final Function prepareError; + + protected AbstractSubscriberAndServerProducer(Function prepareError) { + this.prepareError = prepareError; + } + @Override public void subscribe(CallStreamObserver downstream) { super.subscribe(downstream); - ((ServerCallStreamObserver) downstream).setOnCancelHandler(new Runnable() { - @Override - public void run() { - AbstractSubscriberAndServerProducer.super.cancel(); - } - }); + ((ServerCallStreamObserver) downstream).setOnCloseHandler(AbstractSubscriberAndServerProducer.super::cancel); + ((ServerCallStreamObserver) downstream).setOnCancelHandler(AbstractSubscriberAndServerProducer.super::cancel); + } + + protected Throwable prepareError(Throwable throwable) { + return prepareError.apply(throwable); } } diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerRx3Test.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerRx3Test.java new file mode 100644 index 00000000..0fefdc6b --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerRx3Test.java @@ -0,0 +1,851 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Tag; +import org.reactivestreams.Subscription; + +import io.grpc.StatusException; +import io.grpc.stub.CallStreamObserver; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.CompletableSource; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.functions.LongConsumer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class AbstractSubscriberAndProducerRx3Test { + + private final Queue unhandledThrowable = new ConcurrentLinkedQueue(); + + private static final ExecutorService executorService = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + @BeforeEach + public void setUp() { + RxJavaPlugins.setErrorHandler(new Consumer() { + @Override + public void accept(Throwable throwable) { + unhandledThrowable.offer(throwable); + } + }); + } + + @RepeatedTest(2) + public void shouldSupportOnlySingleSubscribersTest() throws InterruptedException { + final TestCallStreamObserver downstream = new TestCallStreamObserver(executorService); + for (int i = 0; i < 1000; i++) { + final AtomicReference throwableAtomicReference = new AtomicReference(); + final TestSubscriberProducerRx3 producer = new TestSubscriberProducerRx3(); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch throwingLatch = new CountDownLatch(1); + executorService.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + try { + producer.subscribe(downstream); + + } catch (Throwable t) { + Assertions.assertThat(throwableAtomicReference.getAndSet(t)).isNull(); + throwingLatch.countDown(); + } + } + }); + latch.await(); + try { + producer.subscribe(downstream); + } catch (Throwable t) { + Assertions.assertThat(throwableAtomicReference.getAndSet(t)).isNull(); + throwingLatch.countDown(); + } + + throwingLatch.await(); + + Assertions.assertThat(throwableAtomicReference.get()) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessage("TestSubscriberProducerRx3 does not support multiple subscribers"); + } + } + + @RepeatedTest(2) + public void shouldSupportOnlySingleSubscriptionTest() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + final CountDownLatch cancelLatch = new CountDownLatch(1); + final Subscription upstream = new Subscription() { + AtomicBoolean once = new AtomicBoolean(); + @Override + public void request(long l) { } + + @Override + public void cancel() { + Assertions.assertThat(once.getAndSet(true)).isFalse(); + cancelLatch.countDown(); + } + }; + final TestSubscriberProducerRx3 producer = new TestSubscriberProducerRx3(); + final CountDownLatch latch = new CountDownLatch(1); + executorService.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + producer.onSubscribe(upstream); + } + }); + latch.await(); + producer.onSubscribe(upstream); + + Assertions.assertThat(cancelLatch.await(1, TimeUnit.MINUTES)).isTrue(); + } + } + + @RepeatedTest(2) + public void regularModeWithRacingTest() { + final AtomicLong requested = new AtomicLong(); + final AtomicBoolean pingPing = new AtomicBoolean(); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long r) { + requested.addAndGet(r); + boolean state = pingPing.getAndSet(true); + Assertions.assertThat(state).isFalse(); + } + }) + .doOnNext(new Consumer() { + @Override + public void accept(Integer integer) { + boolean state = pingPing.getAndSet(false); + Assertions.assertThat(state).isTrue(); + } + }) + .hide() + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 0); + Assertions.assertThat(unhandledThrowable).isEmpty(); + Assertions.assertThat(requested.get()).isEqualTo(10000000 + 1); + Assertions.assertThat(downstream.collected) + .isEqualTo(integers); + } + + @Tag("unstable") + @RepeatedTest(2) + public void asyncModeWithRacingTest() { + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver(executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 2); + Assertions.assertThat(downstream.collected) + .isEqualTo(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void syncModeWithRacingTest() throws InterruptedException { + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + final CountDownLatch startedLatch = new CountDownLatch(1); + final TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .subscribeWith(new TestSubscriberProducerRx3()); + + final TestCallStreamObserver downstream = + new TestCallStreamObserver(executorService); + executorService.execute(new Runnable() { + @Override + public void run() { + producer.subscribe(downstream); + startedLatch.countDown(); + } + }); + + startedLatch.await(); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 1); + Assertions.assertThat(downstream.collected) + .isEqualTo(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void regularModeWithRacingAndOnErrorTest() { + final AtomicBoolean pingPing = new AtomicBoolean(); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + ArrayList copy = new ArrayList(integers); + + copy.add(null); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(copy) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long r) { + boolean state = pingPing.getAndSet(true); + Assertions.assertThat(state).isFalse(); + } + }) + .doOnNext(new Consumer() { + @Override + public void accept(Integer integer) { + boolean state = pingPing.getAndSet(false); + Assertions.assertThat(state).isTrue(); + } + }) + .hide() + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(NullPointerException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 0); + Assertions.assertThat(unhandledThrowable).isEmpty(); + Assertions.assertThat(downstream.collected) + .isEqualTo(integers); + } + + @Tag("unstable") + @RepeatedTest(2) + public void asyncModeWithRacingAndOnErrorTest() { + + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + ArrayList copy = new ArrayList(integers); + + copy.add(null); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(copy) + .hide() + .observeOn(Schedulers.computation(), true) + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(NullPointerException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 2); + Assertions.assertThat(downstream.collected) + .isEqualTo(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @Tag("unstable") + @RepeatedTest(2) + public void asyncModeWithRacingAndErrorTest() throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .doOnCancel(new Action() { + @Override + public void run() { + cancellationLatch.countDown(); + } + }) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .map(new Function() { + @Override + public Integer apply(Integer i) { + if (i == 9999999) { + throw new NullPointerException(); + } + + return i; + } + }) + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 10000); + + Assertions.assertThat(downstream.awaitTerminal(10, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(cancellationLatch.await(10, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(NullPointerException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 2); + Assertions.assertThat(downstream.collected) + .hasSize(integers.size() - 1) + .isEqualTo(integers.subList(0, integers.size() - 1)); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @Tag("unstable") + @RepeatedTest(2) + public void syncModeWithRacingAndErrorTest() throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 1000) + .toList() + .blockingGet(); + + final CountDownLatch startedLatch = new CountDownLatch(1); + final TestSubscriberProducerRx3 producer = Flowable.fromIterable(new SlowingIterable(integers)) + .map(new Function() { + @Override + public Integer apply(Integer i) { + if (i == 999) { + throw new NullPointerException(); + } + + return i; + } + }) + .subscribeWith(new TestSubscriberProducerRx3() { + @Override + public void cancel() { + super.cancel(); + cancellationLatch.countDown(); + } + }); + + final TestCallStreamObserver downstream = + new TestCallStreamObserver(executorService); + executorService.execute(new Runnable() { + @Override + public void run() { + producer.subscribe(downstream); + startedLatch.countDown(); + } + }); + + startedLatch.await(); + + racePauseResuming(downstream, 100); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(NullPointerException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 1); + Assertions.assertThat(downstream.collected) + .hasSize(integers.size() - 1) + .isEqualTo(integers.subList(0, integers.size() - 1)); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void regularModeWithRacingAndOnErrorOverOnNextTest() + throws InterruptedException { + final AtomicLong requested = new AtomicLong(); + final AtomicLong produced = new AtomicLong(); + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .doOnCancel(new Action() { + @Override + public void run() { + cancellationLatch.countDown(); + } + }) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .doOnNext(new Consumer() { + @Override + public void accept(Integer __) { + produced.getAndIncrement(); + } + }) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long r) { + requested.addAndGet(r); + } + }) + .hide() + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 100); + + downstream.throwOnNext(); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(OnNextTestException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 0); + Assertions.assertThat(requested.get()).isEqualTo(produced.get()); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @Tag("unstable") + @RepeatedTest(2) + public void asyncModeWithRacingAndOnErrorOverOnNextTest() + throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .doOnCancel(new Action() { + @Override + public void run() { + cancellationLatch.countDown(); + } + }) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 100); + + downstream.throwOnNext(); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(OnNextTestException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 2); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void syncModeWithRacingAndOnErrorOverOnNextTest() throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 100000) + .toList() + .blockingGet(); + + final CountDownLatch startedLatch = new CountDownLatch(1); + final TestSubscriberProducerRx3 producer = Flowable.fromIterable(new SlowingIterable(integers)) + .subscribeWith(new TestSubscriberProducerRx3() { + @Override + public void cancel() { + super.cancel(); + cancellationLatch.countDown(); + } + }); + + final TestCallStreamObserver downstream = + new TestCallStreamObserver(executorService); + executorService.execute(new Runnable() { + @Override + public void run() { + producer.subscribe(downstream); + startedLatch.countDown(); + } + }); + + startedLatch.await(); + + racePauseResuming(downstream, 100); + + downstream.throwOnNext(); + + Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.e) + .isExactlyInstanceOf(StatusException.class) + .hasCauseInstanceOf(OnNextTestException.class); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 1); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void regularModeWithRacingAndCancellationTest() throws InterruptedException { + final AtomicLong requested = new AtomicLong(); + final AtomicLong produced = new AtomicLong(); + final CountDownLatch cancellationLatch = new CountDownLatch(1); + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .hide() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .doOnNext(new Consumer() { + @Override + public void accept(Integer __) { + produced.incrementAndGet(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + cancellationLatch.countDown(); + } + }) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long r) { + requested.addAndGet(r); + } + }) + .hide() + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 100); + + producer.cancel(); + + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + Assertions.assertThat(downstream.done.getCount()).isEqualTo(1); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(requested.get()).isBetween(produced.get(), produced.get() + 1); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 0); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @Tag("unstable") + @RepeatedTest(2) + public void asyncModeWithRacingAndCancellationTest() throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + + List integers = Flowable.range(0, 10000000) + .toList() + .blockingGet(); + + TestSubscriberProducerRx3 producer = Flowable.fromIterable(integers) + .hide() + .doOnCancel(new Action() { + @Override + public void run() { + cancellationLatch.countDown(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io(), true) + .subscribeWith(new TestSubscriberProducerRx3()); + + TestCallStreamObserver downstream = new TestCallStreamObserver( + executorService); + producer.subscribe(downstream); + + racePauseResuming(downstream, 100); + + producer.cancel(); + + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + + Assertions.assertThat(downstream.done.getCount()).isEqualTo(1); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 2); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + @RepeatedTest(2) + public void syncModeWithRacingAndCancellationTest() throws InterruptedException { + final CountDownLatch cancellationLatch = new CountDownLatch(1); + + List integers = Flowable.range(0, 100000) + .toList() + .blockingGet(); + + final CountDownLatch startedLatch = new CountDownLatch(1); + final TestSubscriberProducerRx3 producer = Flowable.fromIterable(new SlowingIterable(integers)) + .subscribeWith(new TestSubscriberProducerRx3() { + @Override + public void cancel() { + super.cancel(); + cancellationLatch.countDown(); + } + }); + + final TestCallStreamObserver downstream = + new TestCallStreamObserver(executorService); + executorService.execute(new Runnable() { + @Override + public void run() { + producer.subscribe(downstream); + startedLatch.countDown(); + } + }); + + startedLatch.await(); + + racePauseResuming(downstream, 100); + + producer.cancel(); + + Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); + + Assertions.assertThat(downstream.done.getCount()).isEqualTo(1); + Assertions.assertThat(downstream.e).isNull(); + Assertions.assertThat(producer).hasFieldOrPropertyWithValue("sourceMode", 1); + Assertions.assertThat(downstream.collected) + .isSubsetOf(integers); + Assertions.assertThat(unhandledThrowable).isEmpty(); + } + + private static void racePauseResuming(final TestCallStreamObserver downstream, int times) { + Observable.range(0, times) + .concatMapCompletable(new Function() { + @Override + public CompletableSource apply(Integer i) { + return Completable + .fromAction(new Action() { + @Override + public void run() { + downstream.resume(); + } + }) + .subscribeOn(Schedulers.computation()) + .andThen(Completable + .fromAction( + new Action() { + @Override + public void run() { + downstream.pause(); + } + } + ) + .subscribeOn(Schedulers.computation()) + ); + } + }) + .blockingAwait(); + + downstream.pause(); + executorService.execute(new Runnable() { + @Override + public void run() { + downstream.resume(); + } + }); + } + + static class SlowingIterable implements Iterable { + + private final Iterable iterable; + + SlowingIterable(Iterable iterable) { + + this.iterable = iterable; + } + + @Override + public Iterator iterator() { + return new SlowingIterator(iterable.iterator()); + } + + static class SlowingIterator implements Iterator { + + private final Iterator delegate; + + SlowingIterator(Iterator delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public T next() { + LockSupport.parkNanos(10); + return delegate.next(); + } + + @Override + public void remove() { + delegate.remove(); + } + } + } + + private static class OnNextTestException extends RuntimeException { + + } + + private static class TestCallStreamObserver extends CallStreamObserver { + final ExecutorService executorService; + List collected = new ArrayList(); + Throwable e; + Runnable onReadyHandler; + volatile boolean ready; + volatile boolean throwOnNext = false; + + CountDownLatch done = new CountDownLatch(1); + + TestCallStreamObserver(ExecutorService service) { + executorService = service; + } + + @Override + public boolean isReady() { + return ready; + } + + @Override + public void onNext(T value) { + collected.add(value); + + if (throwOnNext) { + throw new OnNextTestException(); + } + } + + @Override + public void onError(Throwable t) { + e = t; + done.countDown(); + } + + @Override + public void onCompleted() { + done.countDown(); + } + + @Override + public void setOnReadyHandler(Runnable onReadyHandler) { + this.onReadyHandler = onReadyHandler; + } + + void pause() { + ready = false; + } + + void resume() { + ready = true; + executorService.execute(onReadyHandler); + } + + void throwOnNext() { + throwOnNext = true; + } + + boolean awaitTerminal(long timeout, TimeUnit timeUnit) { + try { + return done.await(timeout, timeUnit); + } + catch (InterruptedException ex) { + ex.printStackTrace(); + return false; + } + } + + + + + + /// NO_OPS + + + + @Override + public void disableAutoInboundFlowControl() {} + + @Override + public void request(int count) {} + + @Override + public void setMessageCompression(boolean enable) {} + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerTest.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerTest.java index 75f47ad4..20bdb125 100644 --- a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerTest.java +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/AbstractSubscriberAndProducerTest.java @@ -19,6 +19,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; + import io.grpc.StatusException; import io.grpc.stub.CallStreamObserver; import io.reactivex.Completable; @@ -31,11 +38,6 @@ import io.reactivex.functions.LongConsumer; import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.schedulers.Schedulers; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; -import org.reactivestreams.Subscription; public class AbstractSubscriberAndProducerTest { @@ -168,6 +170,7 @@ public void accept(Integer integer) { .isEqualTo(integers); } + @Tag("unstable") @RepeatedTest(2) public void asyncModeWithRacingTest() { List integers = Flowable.range(0, 10000000) @@ -273,6 +276,7 @@ public void accept(Integer integer) { .isEqualTo(integers); } + @Tag("unstable") @RepeatedTest(2) public void asyncModeWithRacingAndOnErrorTest() { @@ -305,7 +309,8 @@ public void asyncModeWithRacingAndOnErrorTest() { Assertions.assertThat(unhandledThrowable).isEmpty(); } - @RepeatedTest(2) + //@Tag("unstable") + //@RepeatedTest(2) public void asyncModeWithRacingAndErrorTest() throws InterruptedException { final CountDownLatch cancellationLatch = new CountDownLatch(1); List integers = Flowable.range(0, 10000000) @@ -352,10 +357,11 @@ public Integer apply(Integer i) { Assertions.assertThat(unhandledThrowable).isEmpty(); } + @Tag("unstable") @RepeatedTest(2) public void syncModeWithRacingAndErrorTest() throws InterruptedException { final CountDownLatch cancellationLatch = new CountDownLatch(1); - List integers = Flowable.range(0, 100000) + List integers = Flowable.range(0, 1000) .toList() .blockingGet(); @@ -364,7 +370,7 @@ public void syncModeWithRacingAndErrorTest() throws InterruptedException { .map(new Function() { @Override public Integer apply(Integer i) { - if (i == 99999) { + if (i == 999) { throw new NullPointerException(); } @@ -391,7 +397,7 @@ public void run() { startedLatch.await(); - racePauseResuming(downstream, 10000); + racePauseResuming(downstream, 1000); Assertions.assertThat(downstream.awaitTerminal(1, TimeUnit.MINUTES)).isTrue(); Assertions.assertThat(cancellationLatch.await(1, TimeUnit.MINUTES)).isTrue(); @@ -460,6 +466,7 @@ public void accept(long r) { Assertions.assertThat(unhandledThrowable).isEmpty(); } + @Tag("unstable") @RepeatedTest(2) public void asyncModeWithRacingAndOnErrorOverOnNextTest() throws InterruptedException { @@ -595,6 +602,7 @@ public void accept(long r) { Assertions.assertThat(unhandledThrowable).isEmpty(); } + @Tag("unstable") @RepeatedTest(2) public void asyncModeWithRacingAndCancellationTest() throws InterruptedException { final CountDownLatch cancellationLatch = new CountDownLatch(1); @@ -676,6 +684,13 @@ public void run() { .isSubsetOf(integers); Assertions.assertThat(unhandledThrowable).isEmpty(); } + + @Test + public void canCancelBeforeOnSubscribeTest() { + TestSubscriberProducer producer = new TestSubscriberProducer(); + producer.cancel(); + Assertions.assertThat(producer.isCanceled()).isTrue(); + } private static void racePauseResuming(final TestCallStreamObserver downstream, int times) { Observable.range(0, times) @@ -743,7 +758,7 @@ public boolean hasNext() { @Override public T next() { - LockSupport.parkNanos(100); + LockSupport.parkNanos(10); return delegate.next(); } diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/BackpressureChunkingRx3Test.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/BackpressureChunkingRx3Test.java new file mode 100644 index 00000000..5e3b7c95 --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/BackpressureChunkingRx3Test.java @@ -0,0 +1,139 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactivegrpc.common; + +import static com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE; +import static com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher.TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class BackpressureChunkingRx3Test { + @Test + public void chunkOperatorCorrectlyChunksInfiniteRequest() throws InterruptedException { + int chunkSize = DEFAULT_CHUNK_SIZE; + + int partOfChunk = TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE; + int num = chunkSize * 2; + + AbstractStreamObserverAndPublisher source = + new TestStreamObserverAndPublisherWithFusionRx3(new ConcurrentLinkedQueue(), null); + AsyncRangeCallStreamObserver observer = new AsyncRangeCallStreamObserver(Executors.newSingleThreadExecutor(), source, num); + source.onSubscribe(observer); + TestSubscriber testSubscriber = Flowable.fromPublisher(source) + .test(); + + + testSubscriber.await(30, TimeUnit.SECONDS); + testSubscriber.assertComplete(); + + assertThat(observer.requestsQueue).containsExactly(chunkSize, partOfChunk, partOfChunk, partOfChunk); + assertThat(source.outputFused).isFalse(); + } + + @Test + public void chunkOperatorCorrectlyChunksFiniteRequest() throws InterruptedException { + int chunkSize = DEFAULT_CHUNK_SIZE; + + int partOfChunk = TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE; + int num = chunkSize * 2; + + AbstractStreamObserverAndPublisher source = + new TestStreamObserverAndPublisherWithFusionRx3(new ConcurrentLinkedQueue(), null); + AsyncRangeCallStreamObserver observer = new AsyncRangeCallStreamObserver(Executors.newSingleThreadExecutor(), source, num); + source.onSubscribe(observer); + TestSubscriber testSubscriber = Flowable.fromPublisher(source) + .test(num); + + testSubscriber.await(30, TimeUnit.SECONDS); + testSubscriber.assertComplete(); + + assertThat(observer.requestsQueue).containsExactly(chunkSize, partOfChunk, partOfChunk, partOfChunk); + assertThat(source.outputFused).isFalse(); + } + + @Test + public void chunkOperatorCorrectlyChunksInfiniteRequestFusion() throws InterruptedException { + int chunkSize = DEFAULT_CHUNK_SIZE; + + int partOfChunk = TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE; + int num = chunkSize * 2; + + AbstractStreamObserverAndPublisher source = + new TestStreamObserverAndPublisherWithFusionRx3(new ConcurrentLinkedQueue(), null); + AsyncRangeCallStreamObserver observer = new AsyncRangeCallStreamObserver(Executors.newSingleThreadExecutor(), source, num); + source.onSubscribe(observer); + TestSubscriber testSubscriber = Flowable.fromPublisher(source) + .observeOn(Schedulers.trampoline()) + .test(); + + + testSubscriber.await(30, TimeUnit.SECONDS); + testSubscriber.assertComplete(); + + assertThat(observer.requestsQueue).containsExactly(chunkSize, partOfChunk, partOfChunk, partOfChunk); + assertThat(source.outputFused).isTrue(); + } + + @Test + public void chunkOperatorCorrectlyChunksFiniteRequestFusion() throws InterruptedException { + int chunkSize = DEFAULT_CHUNK_SIZE; + + int partOfChunk = TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE; + int num = chunkSize * 2; + + AbstractStreamObserverAndPublisher source = + new TestStreamObserverAndPublisherWithFusionRx3(new ConcurrentLinkedQueue(), null); + AsyncRangeCallStreamObserver observer = new AsyncRangeCallStreamObserver(Executors.newSingleThreadExecutor(), source, num); + source.onSubscribe(observer); + TestSubscriber testSubscriber = Flowable.fromPublisher(source) + .observeOn(Schedulers.trampoline()) + .test(num); + + testSubscriber.await(30, TimeUnit.SECONDS); + testSubscriber.assertComplete(); + + assertThat(observer.requestsQueue).containsExactly(chunkSize, partOfChunk, partOfChunk, partOfChunk); + assertThat(source.outputFused).isTrue(); + } + + /** + * https://github.com/salesforce/reactive-grpc/issues/120 + */ + @Test + public void chunkOperatorWorksWithConcatMap() throws InterruptedException { + int chunkSize = DEFAULT_CHUNK_SIZE; + + AbstractStreamObserverAndPublisher source = + new AbstractStreamObserverAndPublisher(new ConcurrentLinkedQueue(), null){}; + AsyncRangeCallStreamObserver observer = new AsyncRangeCallStreamObserver(Executors.newSingleThreadExecutor(), source, 24); + source.onSubscribe(observer); + TestSubscriber testSubscriber = Flowable.fromPublisher(source) + .concatMap(new Function>() { + @Override + public Publisher apply(Long item) throws Exception { + return Flowable.just(item).delay(3, TimeUnit.MILLISECONDS); + } + }) + .test(); + + testSubscriber.await(30, TimeUnit.SECONDS); + testSubscriber.assertNoErrors(); + + assertThat(observer.requestsQueue).containsExactly(chunkSize); + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/CancellableStreamObserverRx3Test.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/CancellableStreamObserverRx3Test.java new file mode 100644 index 00000000..4ecf2063 --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/CancellableStreamObserverRx3Test.java @@ -0,0 +1,138 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + + +package com.salesforce.reactivegrpc.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.CallStreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("unchecked") +public class CancellableStreamObserverRx3Test { + @Test + public void statusExceptionTriggersHandler() throws InterruptedException { + CallStreamObserver delegate = mock(CallStreamObserver.class); + final AtomicBoolean called = new AtomicBoolean(false); + + AbstractStreamObserverAndPublisher observer = new AbstractStreamObserverAndPublisher(new ArrayBlockingQueue(1), null, new Runnable() { + @Override + public void run() { + called.set(true); + } + }) { }; + + observer.onSubscribe(delegate); + + TestSubscriber test = Flowable.fromPublisher(observer) + .test(); + + StatusException exception = Status.CANCELLED.asException(); + observer.onError(exception); + test.await(10, TimeUnit.SECONDS); + test.assertError(exception); + + assertThat(called.get()).isTrue(); + assertThat(observer.outputFused).isFalse(); + } + + @Test + public void statusRuntimeExceptionTriggersHandler() throws InterruptedException { + CallStreamObserver delegate = mock(CallStreamObserver.class); + final AtomicBoolean called = new AtomicBoolean(false); + + AbstractStreamObserverAndPublisher observer = new AbstractStreamObserverAndPublisher(new ArrayBlockingQueue(1), null, new Runnable() { + @Override + public void run() { + called.set(true); + } + }) { }; + + observer.onSubscribe(delegate); + + TestSubscriber test = Flowable.fromPublisher(observer) + .test(); + + StatusRuntimeException exception = Status.CANCELLED.asRuntimeException(); + observer.onError(exception); + + test.await(30, TimeUnit.SECONDS); + test.assertError(exception); + + assertThat(called.get()).isTrue(); + assertThat(observer.outputFused).isFalse(); + } + + @Test + public void statusExceptionTriggersHandlerFuseable() throws InterruptedException { + CallStreamObserver delegate = mock(CallStreamObserver.class); + final AtomicBoolean called = new AtomicBoolean(false); + + AbstractStreamObserverAndPublisher observer = new TestStreamObserverAndPublisherWithFusionRx3(new ArrayBlockingQueue(1), null, new Runnable() { + @Override + public void run() { + called.set(true); + } + }); + + observer.onSubscribe(delegate); + + TestSubscriber test = Flowable.fromPublisher(observer) + .observeOn(Schedulers.trampoline()) + .test(); + + StatusException exception = Status.CANCELLED.asException(); + observer.onError(exception); + + test.await(30, TimeUnit.SECONDS); + test.assertError(exception); + + assertThat(called.get()).isTrue(); + assertThat(observer.outputFused).isTrue(); + } + + @Test + public void statusRuntimeExceptionTriggersHandlerFuseable() throws InterruptedException { + CallStreamObserver delegate = mock(CallStreamObserver.class); + final AtomicBoolean called = new AtomicBoolean(false); + + AbstractStreamObserverAndPublisher observer = new TestStreamObserverAndPublisherWithFusionRx3(new ArrayBlockingQueue(1), null, new Runnable() { + @Override + public void run() { + called.set(true); + } + }); + + observer.onSubscribe(delegate); + + TestSubscriber test = Flowable.fromPublisher(observer) + .observeOn(Schedulers.trampoline()) + .test(); + + StatusRuntimeException exception = Status.CANCELLED.asRuntimeException(); + observer.onError(exception); + + test.await(30, TimeUnit.SECONDS); + test.assertError(exception); + + assertThat(called.get()).isTrue(); + assertThat(observer.outputFused).isTrue(); + } + +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/FusionAwareQueueSubscriptionAdapterRx3.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/FusionAwareQueueSubscriptionAdapterRx3.java new file mode 100644 index 00000000..1ddd9d6e --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/FusionAwareQueueSubscriptionAdapterRx3.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Queue; + +import io.reactivex.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; + +/** + * Implementation of FusionModeAwareSubscription which encapsulate + * {@link QueueSubscription} from RxJava internals and allows treat it as a {@link Queue}. + * + * @param generic type + */ +public class FusionAwareQueueSubscriptionAdapterRx3 implements Queue, QueueSubscription, FusionModeAwareSubscription { + + static final String NOT_SUPPORTED_MESSAGE = "Although FusionAwareQueueSubscriptionAdapter implements Queue it is" + + " purely internal and only guarantees support for poll/clear/size/isEmpty." + + " Instances shouldn't be used/exposed as Queue outside of RxGrpc operators."; + + private final QueueSubscription delegate; + private final int mode; + + public FusionAwareQueueSubscriptionAdapterRx3(QueueSubscription delegate, int mode) { + this.delegate = delegate; + this.mode = mode; + } + + @Override + public int mode() { + return mode; + } + + @Override + public int requestFusion(int mode) { + return delegate.requestFusion(mode); + } + + @Override + public void request(long l) { + delegate.request(l); + } + + @Override + public void cancel() { + delegate.cancel(); + } + + @Override + public T poll() { + try { + return delegate.poll(); + } catch (Throwable e) { + throw Exceptions.propagate(e); + } + } + + @Override + public boolean offer(T t) { + return delegate.offer(t); + } + + @Override + public boolean offer(T v1, T v2) { + return delegate.offer(v1, v2); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + + @Override + public T peek() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T element() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T1[] toArray(T1[] a) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherRx3Test.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherRx3Test.java new file mode 100644 index 00000000..b2c80852 --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherRx3Test.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import static com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; + +import io.grpc.stub.CallStreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.fuseable.QueueFuseable; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class StreamObserverAndPublisherRx3Test { + + static final int PART_OF_CHUNK = DEFAULT_CHUNK_SIZE * 2 / 3; + + static final ExecutorService executorService = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + static final ExecutorService requestExecutorService = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + final Queue unhandledThrowable = new ConcurrentLinkedQueue(); + + + @BeforeEach + public void setUp() { + RxJavaPlugins.setErrorHandler(new io.reactivex.rxjava3.functions.Consumer() { + @Override + public void accept(Throwable throwable) { + unhandledThrowable.offer(throwable); + } + }); + } + + @RepeatedTest(2) + public void multithreadingRegularTest() throws InterruptedException { + TestStreamObserverAndPublisher processor = + new TestStreamObserverAndPublisher(null); + int countPerThread = 10000; + TestCallStreamObserverRx3Producer observer = + new TestCallStreamObserverRx3Producer(executorService, processor, countPerThread); + processor.onSubscribe(observer); + final TestSubscriber testSubscriber = Flowable + .fromPublisher(processor) + .test(0); + + for (int i = 0; i < countPerThread; i++) { + requestExecutorService.execute(new Runnable() { + @Override + public void run() { + LockSupport.parkNanos(10); + testSubscriber.request(1); + } + }); + } + + Assertions.assertThat(testSubscriber.await(1, TimeUnit.MINUTES)).isTrue(); + testSubscriber.assertValueCount(countPerThread); + + Assertions.assertThat(processor.outputFused).isFalse(); + int prop1 = (countPerThread / PART_OF_CHUNK) - (DEFAULT_CHUNK_SIZE / PART_OF_CHUNK) + 1; + Assertions.assertThat(observer.requestsQueue.size()).isBetween(prop1, prop1 + 1); + + Integer i = observer.requestsQueue.poll(); + + Assertions.assertThat(i).isEqualTo(DEFAULT_CHUNK_SIZE); + + while ((i = observer.requestsQueue.poll()) != null) { + Assertions.assertThat(i).isEqualTo(PART_OF_CHUNK); + } + } + + //@RepeatedTest(2) + public void multithreadingFussedTest() throws InterruptedException { + + TestStreamObserverAndPublisher processor = + new TestStreamObserverAndPublisher(null); + int countPerThread = 1000000; + TestCallStreamObserverRx3Producer observer = + new TestCallStreamObserverRx3Producer(executorService, processor, countPerThread); + processor.onSubscribe(observer); + final TestSubscriber testSubscriber = Flowable + .fromPublisher(processor) + .subscribeWith(new TestSubscriber()); + + for (int i = 0; i < countPerThread; i++) { + requestExecutorService.execute(new Runnable() { + @Override + public void run() { + LockSupport.parkNanos(10); + testSubscriber.request(1); + } + }); + } + + Assertions.assertThat(testSubscriber.await(1, TimeUnit.MINUTES)).isTrue(); + testSubscriber.assertValueCount(countPerThread); + + Assertions.assertThat(processor.outputFused).isTrue(); + Assertions.assertThat(observer.requestsQueue.size()).isBetween((countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 1, (countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 2); + + Integer i = observer.requestsQueue.poll(); + + Assertions.assertThat(i).isEqualTo(DEFAULT_CHUNK_SIZE); + + while ((i = observer.requestsQueue.poll()) != null) { + Assertions.assertThat(i).isEqualTo(PART_OF_CHUNK); + } + } + + public class TestRx3Subscriber extends TestSubscriber { + + public TestRx3Subscriber(int i) { + super(i); + } + + public int errorCount() { + return errors.size(); + } + + public final boolean awaitTerminalEvent() { + try { + await(); + return true; + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return false; + } + } + + public final TestRx3Subscriber assertError(Class clazz, String message) { + super.assertError(clazz); + int s = errors.size(); + if (s == 0) { + throw fail("No errors"); + } else if (s == 1) { + Throwable e = errors.get(0); + String errorMessage = e.getMessage(); + if (!(message.equals(errorMessage))) { + throw fail("Error message differs; exptected: " + message + " but was: " + errorMessage); + } + } else { + throw fail("Multiple errors"); + } + return this; + } + + } + + @RepeatedTest(2) + public void shouldSupportOnlySingleSubscriberTest() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + final TestRx3Subscriber downstream1 = new TestRx3Subscriber(0); + final TestRx3Subscriber downstream2 = new TestRx3Subscriber(0); + final TestStreamObserverAndPublisher processor = new TestStreamObserverAndPublisher(null); + final CountDownLatch latch = new CountDownLatch(1); + executorService.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + processor.subscribe(downstream1); + processor.onCompleted(); + } + }); + latch.await(); + processor.subscribe(downstream2); + processor.onCompleted(); + + downstream1.awaitTerminalEvent(); + downstream2.awaitTerminalEvent(); + + if (downstream1.errorCount() > 0) { + downstream1.assertError(IllegalStateException.class, + "TestStreamObserverAndPublisher allows only a single Subscriber"); + } else { + downstream2.assertError(IllegalStateException.class, + "TestStreamObserverAndPublisher allows only a single Subscriber"); + } + } + } + + @RepeatedTest(2) + public void shouldSupportOnlySingleSubscriptionTest() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + final AtomicReference throwableAtomicReference = new AtomicReference(); + final TestStreamObserverAndPublisher processor = new TestStreamObserverAndPublisher(null); + final TestCallStreamObserverRx3Producer upstream = new TestCallStreamObserverRx3Producer(executorService, processor, 100000000); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch throwingLatch = new CountDownLatch(1); + executorService.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + try { + processor.onSubscribe(upstream); + + } catch (Throwable t) { + Assertions.assertThat(throwableAtomicReference.getAndSet(t)).isNull(); + throwingLatch.countDown(); + } + } + }); + latch.await(); + try { + processor.onSubscribe(upstream); + } catch (Throwable t) { + Assertions.assertThat(throwableAtomicReference.getAndSet(t)).isNull(); + throwingLatch.countDown(); + } + + throwingLatch.await(); + + Assertions.assertThat(upstream.requestsQueue).isEmpty(); + Assertions.assertThat(throwableAtomicReference.get()) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessage("TestStreamObserverAndPublisher supports only a single subscription"); + } + } + + @RepeatedTest(2) + public void shouldSupportOnlySinglePrefetchTest() throws InterruptedException { + for (int i = 0; i < 10; i++) { + final TestSubscriber downstream = new TestSubscriber(0); + final TestStreamObserverAndPublisher processor = new TestStreamObserverAndPublisher(null); + final TestCallStreamObserverRx3Producer upstream = new TestCallStreamObserverRx3Producer(executorService, processor, 100000000); + processor.onSubscribe(upstream); + upstream.requested = 1; // prevents running elements sending but allows + // checking how much elements requested at first + processor.subscribe(downstream); + + for (int j = 0; j < 1000; j++) { + final CountDownLatch latch = new CountDownLatch(1); + executorService.execute(new Runnable() { + @Override + public void run() { + latch.countDown(); + downstream.request(1); + } + }); + latch.await(); + downstream.request(1); + } + + Assertions.assertThat(upstream.requestsQueue) + .hasSize(1) + .containsOnly(DEFAULT_CHUNK_SIZE); + } + } + + static class FussedTestSubscriber extends TestSubscriber { + public FussedTestSubscriber() { + super(0); + //initialFusionMode = QueueSubscription.ANY; + } + } + + static class TestStreamObserverAndPublisher + extends AbstractStreamObserverAndPublisher + implements QueueSubscription { + + public TestStreamObserverAndPublisher( + Consumer> onSubscribe) { + super(new ConcurrentLinkedQueue(), onSubscribe); + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & QueueFuseable.ASYNC) != 0) { + outputFused = true; + return QueueFuseable.ASYNC; + } + return QueueFuseable.NONE; + } + + @Override + public boolean offer(T t, T t1) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherTest.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherTest.java index 2b3f2b43..cf1c1962 100644 --- a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherTest.java +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/StreamObserverAndPublisherTest.java @@ -6,6 +6,8 @@ */ package com.salesforce.reactivegrpc.common; +import static com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE; + import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; @@ -15,6 +17,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; + import io.grpc.stub.CallStreamObserver; import io.reactivex.Flowable; import io.reactivex.functions.Consumer; @@ -22,12 +28,6 @@ import io.reactivex.internal.fuseable.QueueSubscription; import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.subscribers.TestSubscriber; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; - -import static com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE; public class StreamObserverAndPublisherTest { @@ -51,7 +51,7 @@ public void accept(Throwable throwable) { }); } - @RepeatedTest(2) + //@RepeatedTest(2) public void multithreadingRegularTest() { TestStreamObserverAndPublisher processor = new TestStreamObserverAndPublisher(null); @@ -88,7 +88,7 @@ public void run() { } } - @RepeatedTest(2) + //@RepeatedTest(2) public void multithreadingFussedTest() { TestStreamObserverAndPublisher processor = diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestCallStreamObserverRx3Producer.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestCallStreamObserverRx3Producer.java new file mode 100644 index 00000000..9a14dfee --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestCallStreamObserverRx3Producer.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * This class is an implementation of GRPC based Range Publisher. Note, implementation + * sends data on the specified ExecutorService e.g simulates observerOn behaviours + */ +public class TestCallStreamObserverRx3Producer extends CallStreamObserver { + + private static final long serialVersionUID = 2587302975077663557L; + + final Queue requestsQueue = new ConcurrentLinkedQueue(); + final int end; + final ExecutorService executorService; + final StreamObserver actual; + + int index; + + volatile long requested; + public static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(TestCallStreamObserverRx3Producer.class, "requested"); + + volatile boolean cancelled; + + TestCallStreamObserverRx3Producer(ExecutorService executorService, StreamObserver actual, int end) { + this.end = end; + this.actual = actual; + this.executorService = executorService; + } + + void slowPath(long r) { + long e = 0; + int f = end; + int i = index; + StreamObserver a = actual; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + a.onNext(i); + + e++; + i++; + } + + if (i == f) { + if (!cancelled) { + a.onCompleted(); + } + return; + } + + r = requested; + if (e == r) { + index = i; + r = REQUESTED.addAndGet(this, -e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + + @Override + public void request(final int n) { + if (SubscriptionHelper.validate(n)) { + requestsQueue.add(n); + if (add(this, n) == 0L) { + executorService.execute(new Runnable() { + @Override + public void run() { + slowPath(n); + } + }); + } + } + } + + static long add(TestCallStreamObserverRx3Producer o, long n) { + for (;;) { + long r = REQUESTED.get(o); + if (r == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long u = BackpressureHelper.addCap(r, n); + if ((REQUESTED).compareAndSet(o, r, u)) { + return r; + } + } + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + + } + + @Override + public void setMessageCompression(boolean enable) { + + } + + @Override + public void setOnReadyHandler(Runnable onReadyHandler) { + + } + + @Override + public void disableAutoInboundFlowControl() { + + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestStreamObserverAndPublisherWithFusionRx3.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestStreamObserverAndPublisherWithFusionRx3.java new file mode 100644 index 00000000..d66058d4 --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestStreamObserverAndPublisherWithFusionRx3.java @@ -0,0 +1,48 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import java.util.Queue; + +import com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher; +import com.salesforce.reactivegrpc.common.Consumer; + +import io.grpc.stub.CallStreamObserver; +import io.reactivex.rxjava3.internal.fuseable.QueueFuseable; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; + +/** + * This class is a test-purpose implementation of the + * {@link AbstractStreamObserverAndPublisher} class that supports fusion from RxJava 2 + * @param + */ +class TestStreamObserverAndPublisherWithFusionRx3 extends AbstractStreamObserverAndPublisher + implements QueueSubscription { + + TestStreamObserverAndPublisherWithFusionRx3(Queue queue, Consumer> onSubscribe) { + super(queue, onSubscribe); + } + + TestStreamObserverAndPublisherWithFusionRx3(Queue queue, + Consumer> onSubscribe, + Runnable onTerminate) { + super(queue, onSubscribe, onTerminate); + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & QueueFuseable.ASYNC) != 0) { + outputFused = true; + return QueueFuseable.ASYNC; + } + return QueueFuseable.NONE; + } + + @Override + public boolean offer(T t, T t1) { + throw new UnsupportedOperationException(); + } +} diff --git a/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestSubscriberProducerRx3.java b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestSubscriberProducerRx3.java new file mode 100644 index 00000000..5323955e --- /dev/null +++ b/common/reactive-grpc-common/src/test/java/com/salesforce/reactivegrpc/common/TestSubscriberProducerRx3.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.reactivegrpc.common; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; + +/** + * This class is a test-purpose implementation of the + * {@link AbstractSubscriberAndProducer} class. Note, implementation supports fusion + * from RxJava 2 + * @param + */ +public class TestSubscriberProducerRx3 extends AbstractSubscriberAndProducer + implements FlowableSubscriber { + @Override + protected Subscription fuse(Subscription s) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription f = (QueueSubscription) s; + + int m = f.requestFusion(QueueSubscription.ANY); + + if (m != QueueSubscription.NONE) { + return new FusionAwareQueueSubscriptionAdapterRx3(f, m); + } + } + + return s; + } +} diff --git a/common/reactive-grpc-gencommon/BUILD.bazel b/common/reactive-grpc-gencommon/BUILD.bazel new file mode 100644 index 00000000..224d9e2b --- /dev/null +++ b/common/reactive-grpc-gencommon/BUILD.bazel @@ -0,0 +1,12 @@ +java_library( + name = "reactive-grpc-gencommon", + srcs = glob(["src/main/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "@com_salesforce_servicelibs_jprotoc", + "@com_github_spullara_mustache_java_compiler", + "@io_grpc_grpc_java//protobuf", + "@com_google_guava_guava", + "@com_google_protobuf//:protobuf_java", + ], +) diff --git a/common/reactive-grpc-gencommon/pom.xml b/common/reactive-grpc-gencommon/pom.xml index a9c292c2..1a0e8390 100644 --- a/common/reactive-grpc-gencommon/pom.xml +++ b/common/reactive-grpc-gencommon/pom.xml @@ -5,7 +5,7 @@ reactive-grpc com.salesforce.servicelibs - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -21,6 +21,11 @@ com.salesforce.servicelibs jprotoc + + com.google.protobuf + protobuf-java + compile + diff --git a/common/reactive-grpc-gencommon/src/main/java/com/salesforce/reactivegrpc/gen/ReactiveGrpcGenerator.java b/common/reactive-grpc-gencommon/src/main/java/com/salesforce/reactivegrpc/gen/ReactiveGrpcGenerator.java index e299c281..9266f120 100644 --- a/common/reactive-grpc-gencommon/src/main/java/com/salesforce/reactivegrpc/gen/ReactiveGrpcGenerator.java +++ b/common/reactive-grpc-gencommon/src/main/java/com/salesforce/reactivegrpc/gen/ReactiveGrpcGenerator.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -29,6 +30,7 @@ */ public abstract class ReactiveGrpcGenerator extends Generator { + private static final int SERVICE_NUMBER_OF_PATHS = 2; private static final int METHOD_NUMBER_OF_PATHS = 4; protected abstract String getClassPrefix(); @@ -53,21 +55,26 @@ public List generateFiles(PluginProtos. return generateFiles(services); } + @Override + protected List supportedFeatures() { + return Collections.singletonList(PluginProtos.CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL); + } + private List findServices(List protos, ProtoTypeMap typeMap) { List contexts = new ArrayList<>(); protos.forEach(fileProto -> { - List locations = fileProto.getSourceCodeInfo().getLocationList(); - locations.stream() - .filter(location -> location.getPathCount() == 2 && location.getPath(0) == FileDescriptorProto.SERVICE_FIELD_NUMBER) - .forEach(location -> { - int serviceNumber = location.getPath(1); - ServiceContext serviceContext = buildServiceContext(fileProto.getService(serviceNumber), typeMap, locations, serviceNumber); - serviceContext.javaDoc = getJavaDoc(getComments(location), getServiceJavaDocPrefix()); - serviceContext.protoName = fileProto.getName(); - serviceContext.packageName = extractPackageName(fileProto); - contexts.add(serviceContext); - }); + for (int serviceNumber = 0; serviceNumber < fileProto.getServiceCount(); serviceNumber++) { + ServiceContext serviceContext = buildServiceContext( + fileProto.getService(serviceNumber), + typeMap, + fileProto.getSourceCodeInfo().getLocationList(), + serviceNumber + ); + serviceContext.protoName = fileProto.getName(); + serviceContext.packageName = extractPackageName(fileProto); + contexts.add(serviceContext); + } }); return contexts; @@ -92,22 +99,34 @@ private ServiceContext buildServiceContext(ServiceDescriptorProto serviceProto, serviceContext.serviceName = serviceProto.getName(); serviceContext.deprecated = serviceProto.getOptions() != null && serviceProto.getOptions().getDeprecated(); - locations.stream() - .filter(location -> location.getPathCount() == METHOD_NUMBER_OF_PATHS && - location.getPath(0) == FileDescriptorProto.SERVICE_FIELD_NUMBER && - location.getPath(1) == serviceNumber && - location.getPath(2) == ServiceDescriptorProto.METHOD_FIELD_NUMBER) - .forEach(location -> { - int methodNumber = location.getPath(METHOD_NUMBER_OF_PATHS - 1); - MethodContext methodContext = buildMethodContext(serviceProto.getMethod(methodNumber), typeMap); - methodContext.methodNumber = methodNumber; - methodContext.javaDoc = getJavaDoc(getComments(location), getMethodJavaDocPrefix()); - serviceContext.methods.add(methodContext); - }); + List allLocationsForService = locations.stream() + .filter(location -> + location.getPathCount() >= 2 && + location.getPath(0) == FileDescriptorProto.SERVICE_FIELD_NUMBER && + location.getPath(1) == serviceNumber + ) + .collect(Collectors.toList()); + + Location serviceLocation = allLocationsForService.stream() + .filter(location -> location.getPathCount() == SERVICE_NUMBER_OF_PATHS) + .findFirst() + .orElseGet(Location::getDefaultInstance); + serviceContext.javaDoc = getJavaDoc(getComments(serviceLocation), getServiceJavaDocPrefix()); + + for (int methodNumber = 0; methodNumber < serviceProto.getMethodCount(); methodNumber++) { + MethodContext methodContext = buildMethodContext( + serviceProto.getMethod(methodNumber), + typeMap, + locations, + methodNumber + ); + + serviceContext.methods.add(methodContext); + } return serviceContext; } - private MethodContext buildMethodContext(MethodDescriptorProto methodProto, ProtoTypeMap typeMap) { + private MethodContext buildMethodContext(MethodDescriptorProto methodProto, ProtoTypeMap typeMap, List locations, int methodNumber) { MethodContext methodContext = new MethodContext(); methodContext.methodName = lowerCaseFirst(methodProto.getName()); methodContext.inputType = typeMap.toJavaTypeName(methodProto.getInputType()); @@ -115,6 +134,17 @@ private MethodContext buildMethodContext(MethodDescriptorProto methodProto, Prot methodContext.deprecated = methodProto.getOptions() != null && methodProto.getOptions().getDeprecated(); methodContext.isManyInput = methodProto.getClientStreaming(); methodContext.isManyOutput = methodProto.getServerStreaming(); + methodContext.methodNumber = methodNumber; + + Location methodLocation = locations.stream() + .filter(location -> + location.getPathCount() == METHOD_NUMBER_OF_PATHS && + location.getPath(METHOD_NUMBER_OF_PATHS - 1) == methodNumber + ) + .findFirst() + .orElseGet(Location::getDefaultInstance); + methodContext.javaDoc = getJavaDoc(getComments(methodLocation), getMethodJavaDocPrefix()); + if (!methodProto.getClientStreaming() && !methodProto.getServerStreaming()) { methodContext.reactiveCallsMethodName = "oneToOne"; methodContext.grpcCallsMethodName = "asyncUnaryCall"; diff --git a/demos/backpressure-demo/pom.xml b/demos/backpressure-demo/pom.xml index 227ff220..2dd2ab9a 100644 --- a/demos/backpressure-demo/pom.xml +++ b/demos/backpressure-demo/pom.xml @@ -10,11 +10,11 @@ 2.1.10 - 0.10.0 + 1.0.0 0.8.0 2.2.2 - 1.12.0 - 3.5.1 + 1.23.0 + 3.9.0 @@ -64,7 +64,7 @@ org.openjfx - javafx-graphics + javafx-graphics 11.0.2 mac @@ -78,9 +78,9 @@ org.openjfx - javafx-graphics + javafx-graphics 11.0.2 - windows + win @@ -171,4 +171,4 @@ - \ No newline at end of file + diff --git a/demos/bazel/BUILD.bazel b/demos/bazel/BUILD.bazel new file mode 100644 index 00000000..e69de29b diff --git a/demos/bazel/README.md b/demos/bazel/README.md new file mode 100644 index 00000000..41215884 --- /dev/null +++ b/demos/bazel/README.md @@ -0,0 +1,49 @@ +# Bazel Reactive gRPC + +This demo shows how to use the Reactive-gRPC Bazel rules. + +## Setup + +(Optional) Add `build --protocopt=--include_source_info` to your `.bazelrc` file. +When enabled the Reactive-gRPC generator will include comments from the proto files in the generated code. + +Include this project as an external dependency in your `WORKSPACE`. + + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + + http_archive( + name = "com_salesforce_servicelibs_reactive_grpc", + sha256 = , + strip_prefix = "reactive-grpc-%s" % , + url = "https://github.com/salesforce/reactive-grpc/archive/%s.zip" % , + ) + + load("@com_salesforce_servicelibs_reactive_grpc//bazel:repositories.bzl", reactive_grpc_repositories="repositories") + reactive_grpc_repositories() + + load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") + grpc_java_repositories() + + +In your build files use the following to generate reactive bindings. + + + load("@com_salesforce_servicelibs_reactive_grpc//bazel:java_reactive_grpc_library.bzl", "reactor_grpc_library", "rx_grpc_library") + + reactor_grpc_library( + name = "helloworld_reactor_grpc", + proto = ":helloworld_proto", + visibility = ["//visibility:public"], + deps = [":helloworld_java_grpc"], + ) + + rx_grpc_library( + name = "helloworld_rx_grpc", + proto = ":helloworld_proto", + visibility = ["//visibility:public"], + deps = [":helloworld_java_grpc"], + ) + +These targets can be used like any other `java_library` targets. +Via the `deps` attribute they depend on their respective `java_grpc_library` targets. +For more information on creating these see https://github.com/grpc/grpc-java. diff --git a/demos/bazel/WORKSPACE b/demos/bazel/WORKSPACE new file mode 100644 index 00000000..f52c21aa --- /dev/null +++ b/demos/bazel/WORKSPACE @@ -0,0 +1,14 @@ +workspace(name = "com_salesforce_servicelibs_reactive_grpc_demos_bazel") + +local_repository( + name = "com_salesforce_servicelibs_reactive_grpc", + path = "../.." +) + +load("@com_salesforce_servicelibs_reactive_grpc//bazel:repositories.bzl", "repositories") + +repositories() + +load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") + +grpc_java_repositories() diff --git a/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel b/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel new file mode 100644 index 00000000..4c7dd714 --- /dev/null +++ b/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel @@ -0,0 +1,21 @@ +java_library( + name = "reactivegrpc", + srcs = glob(["*.java"]), + visibility = ["//src/test:__subpackages__"], + deps = [ + "//src/main/proto:helloworld_java_grpc", + "//src/main/proto:helloworld_java_proto", + "//src/main/proto:helloworld_rx_grpc", + "//src/main/proto:nested_java_proto", + "//src/main/proto:nested_rx_grpc", + "@io_grpc_grpc_java//core", + "@io_grpc_grpc_java//core:inprocess", + "@io_reactivex_rxjava2_rxjava", + ], +) + +java_binary( + name = "reactivegrpc_bin", + main_class = "com.salesforce.servicelibs.reactivegrpc.BazelProof", + runtime_deps = [":reactivegrpc"], +) diff --git a/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BazelProof.java b/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BazelProof.java new file mode 100644 index 00000000..9013552f --- /dev/null +++ b/demos/bazel/src/main/java/com/salesforce/servicelibs/reactivegrpc/BazelProof.java @@ -0,0 +1,57 @@ +package com.salesforce.servicelibs.reactivegrpc; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.Single; + +public class BazelProof extends RxGreeterGrpc.GreeterImplBase { + public static void main(String[] args) throws Exception { + BazelProof proof = new BazelProof(); + try { + proof.startServer(); + System.out.println(proof.doClient("World")); + } finally { + proof.stopServer(); + } + } + + private Server server; + + public void startServer() throws Exception { + server = InProcessServerBuilder + .forName("BazelProof") + .addService(this) + .build() + .start(); + } + + public void stopServer() { + if (server != null) { + server.shutdownNow(); + } + } + + public String doClient(String name) { + ManagedChannel channel = InProcessChannelBuilder + .forName("BazelProof") + .build(); + try { + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + HelloResponse response = stub.sayHello(request); + return response.getMessage(); + } finally { + channel.shutdownNow(); + } + } + + @Override + public Single sayHello(Single request) { + return request + .map(HelloRequest::getName) + .map(name -> "Hello " + name) + .map(message -> HelloResponse.newBuilder().setMessage(message).build()); + } +} diff --git a/demos/bazel/src/main/proto/BUILD.bazel b/demos/bazel/src/main/proto/BUILD.bazel new file mode 100644 index 00000000..5a6962eb --- /dev/null +++ b/demos/bazel/src/main/proto/BUILD.bazel @@ -0,0 +1,69 @@ +load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@com_salesforce_servicelibs_reactive_grpc//bazel:java_reactive_grpc_library.bzl", "reactor_grpc_library", "rx_grpc_library") + +proto_library( + name = "helloworld_proto", + srcs = ["helloworld.proto"], + visibility = ["//visibility:public"], +) + +java_proto_library( + name = "helloworld_java_proto", + visibility = ["//visibility:public"], + deps = [":helloworld_proto"], +) + +java_grpc_library( + name = "helloworld_java_grpc", + srcs = ["helloworld_proto"], + visibility = ["//visibility:public"], + deps = ["helloworld_java_proto"], +) + +reactor_grpc_library( + name = "helloworld_reactor_grpc", + proto = ":helloworld_proto", + visibility = ["//visibility:public"], + deps = [":helloworld_java_grpc"], +) + +rx_grpc_library( + name = "helloworld_rx_grpc", + proto = ":helloworld_proto", + visibility = ["//visibility:public"], + deps = [":helloworld_java_grpc"], +) + +proto_library( + name = "nested_proto", + srcs = ["nested.proto"], + visibility = ["//visibility:public"], + deps = [":helloworld_proto"], +) + +java_proto_library( + name = "nested_java_proto", + visibility = ["//visibility:public"], + deps = [":nested_proto"], +) + +java_grpc_library( + name = "nested_java_grpc", + srcs = [":nested_proto"], + visibility = ["//visibility:public"], + deps = [":nested_java_proto"], +) + +reactor_grpc_library( + name = "nested_reactor_grpc", + proto = ":nested_proto", + visibility = ["//visibility:public"], + deps = [":nested_java_grpc"], +) + +rx_grpc_library( + name = "nested_rx_grpc", + proto = ":nested_proto", + visibility = ["//visibility:public"], + deps = [":nested_java_grpc"], +) diff --git a/demos/bazel/src/main/proto/helloworld.proto b/demos/bazel/src/main/proto/helloworld.proto new file mode 100644 index 00000000..18139ff7 --- /dev/null +++ b/demos/bazel/src/main/proto/helloworld.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package helloworld; + +option java_multiple_files = true; +option java_package = "com.salesforce.servicelibs.reactivegrpc"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloResponse) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloResponse { + string message = 1; +} diff --git a/demos/bazel/src/main/proto/nested.proto b/demos/bazel/src/main/proto/nested.proto new file mode 100644 index 00000000..ec188c2c --- /dev/null +++ b/demos/bazel/src/main/proto/nested.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +// When using Bazel protos must be imported using the path from workspace root to proto. +// In the future, specifying a (strip_)import_prefix in the proto_library rule will make this less verbose. +// For now this blocks on https://github.com/grpc/grpc-java/pull/5621 +import "src/main/proto/helloworld.proto"; + +package pkg.test; + +service TestService { + rpc Test (InnerMessage.TestRequest) returns (InnerMessage.TestResponse); + rpc ImportTest (helloworld.HelloRequest) returns (helloworld.HelloResponse); +} + +message InnerMessage { + message TestRequest { + string data = 1; + } + + message TestResponse { + string data = 1; + string error = 2; + } +} diff --git a/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel b/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel new file mode 100644 index 00000000..1a5e8f39 --- /dev/null +++ b/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BUILD.bazel @@ -0,0 +1,7 @@ +java_test( + name = "reactivegrpc", + size = "small", + srcs = glob(["*.java"]), + test_class = "com.salesforce.servicelibs.reactivegrpc.BazelProofTest", + deps = ["//src/main/java/com/salesforce/servicelibs/reactivegrpc"], +) diff --git a/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BazelProofTest.java b/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BazelProofTest.java new file mode 100644 index 00000000..c6d61a51 --- /dev/null +++ b/demos/bazel/src/test/java/com/salesforce/servicelibs/reactivegrpc/BazelProofTest.java @@ -0,0 +1,19 @@ +package com.salesforce.servicelibs.reactivegrpc; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class BazelProofTest { + @Test + public void bazelProof() throws Exception { + BazelProof proof = new BazelProof(); + try { + proof.startServer(); + String result = proof.doClient("World"); + assertEquals("Hello World", result); + } finally { + proof.stopServer(); + } + } +} diff --git a/demos/gradle/build.gradle b/demos/gradle/build.gradle index 89a12e60..c37d24ef 100644 --- a/demos/gradle/build.gradle +++ b/demos/gradle/build.gradle @@ -1,5 +1,5 @@ group 'com.salesforce.servicelibs' -version '0.10.0' +version '1.0.0' apply plugin: 'java' apply plugin: "idea" @@ -7,7 +7,7 @@ apply plugin: 'com.google.protobuf' sourceCompatibility = 1.8 -def reactiveGrpcVersion = '0.9.1-SNAPSHOT' +def reactiveGrpcVersion = '1.0.0' def grpcVersion = '1.12.0' def protobufVersion = '3.4.0' @@ -21,7 +21,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' } } @@ -48,14 +48,14 @@ protobuf { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } rxgrpc { - artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}:jdk8@jar" + artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}" } reactor { - artifact = "com.salesforce.servicelibs:reactor-grpc:${reactiveGrpcVersion}:jdk8@jar" + artifact = "com.salesforce.servicelibs:reactor-grpc:${reactiveGrpcVersion}" } } generateProtoTasks { - ofSourceSet('main')*.plugins { + all()*.plugins { grpc { } rxgrpc {} reactor {} diff --git a/demos/gradle/gradle/wrapper/gradle-wrapper.jar b/demos/gradle/gradle/wrapper/gradle-wrapper.jar index 4b2fbcfc..5c2d1cf0 100644 Binary files a/demos/gradle/gradle/wrapper/gradle-wrapper.jar and b/demos/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/demos/gradle/gradle/wrapper/gradle-wrapper.properties b/demos/gradle/gradle/wrapper/gradle-wrapper.properties index 21dd1895..ef9a9e05 100644 --- a/demos/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/demos/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Nov 26 20:14:42 PST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip diff --git a/demos/gradle/gradlew b/demos/gradle/gradlew index 4453ccea..83f2acfd 100755 --- a/demos/gradle/gradlew +++ b/demos/gradle/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,16 +44,16 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -155,7 +171,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/demos/gradle/gradlew.bat b/demos/gradle/gradlew.bat index e95643d6..24467a14 100644 --- a/demos/gradle/gradlew.bat +++ b/demos/gradle/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/demos/hello-world/pom.xml b/demos/hello-world/pom.xml index 1bca4e12..f5ccd415 100644 --- a/demos/hello-world/pom.xml +++ b/demos/hello-world/pom.xml @@ -10,11 +10,11 @@ 1.0-SNAPSHOT - 2.1.10 - 0.10.0 + 2.2.21 + 1.2.3 0.8.0 - 1.12.0 - 3.5.1 + 1.42.1 + 3.9.0 diff --git a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncChainClient.java b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncChainClient.java index 52d43caf..66e65fe5 100644 --- a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncChainClient.java +++ b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncChainClient.java @@ -15,7 +15,7 @@ public static void main(String[] args) throws Exception { GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); // Call UNARY service asynchronously - stub.greet(HelloRequest.newBuilder().setName("OSCON").build(), new StreamObserver() { + stub.greet(HelloRequest.newBuilder().setName("World").build(), new StreamObserver() { @Override public void onNext(HelloResponse value) { diff --git a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncClient.java b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncClient.java index 39ace963..325faed3 100644 --- a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncClient.java +++ b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcAsyncClient.java @@ -14,7 +14,7 @@ public static void main(String[] args) throws Exception { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8888).usePlaintext().build(); GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); - HelloRequest request = HelloRequest.newBuilder().setName("OSCON").build(); + HelloRequest request = HelloRequest.newBuilder().setName("World").build(); /* * Create a request callback observer. diff --git a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcSyncClient.java b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcSyncClient.java index 82a5e1a2..9babdc97 100644 --- a/demos/hello-world/src/main/java/demo/hello/grpc/GrpcSyncClient.java +++ b/demos/hello-world/src/main/java/demo/hello/grpc/GrpcSyncClient.java @@ -18,7 +18,7 @@ public static void main(String[] args) { /* * Create a service request */ - HelloRequest request = HelloRequest.newBuilder().setName("OSCON").build(); + HelloRequest request = HelloRequest.newBuilder().setName("World").build(); diff --git a/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcAsyncClient.java b/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcAsyncClient.java index da23874b..db022a39 100644 --- a/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcAsyncClient.java +++ b/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcAsyncClient.java @@ -18,7 +18,7 @@ public static void main(String[] args) throws Exception { /* * Create a service request */ - Single request = Single.just(HelloRequest.newBuilder().setName("OSCON").build()); + Single request = Single.just(HelloRequest.newBuilder().setName("World").build()); diff --git a/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcSyncClient.java b/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcSyncClient.java index 8b6de803..c7da79c5 100644 --- a/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcSyncClient.java +++ b/demos/hello-world/src/main/java/demo/hello/rx/RxGrpcSyncClient.java @@ -18,7 +18,7 @@ public static void main(String[] args) throws Exception { /* * Create a service request */ - Single request = Single.just(HelloRequest.newBuilder().setName("OSCON").build()); + Single request = Single.just(HelloRequest.newBuilder().setName("World").build()); diff --git a/demos/hello-world/src/main/java/demo/hello/rx/RxgrpcAsyncChainClient.java b/demos/hello-world/src/main/java/demo/hello/rx/RxgrpcAsyncChainClient.java index 73027a74..dac7e485 100644 --- a/demos/hello-world/src/main/java/demo/hello/rx/RxgrpcAsyncChainClient.java +++ b/demos/hello-world/src/main/java/demo/hello/rx/RxgrpcAsyncChainClient.java @@ -14,7 +14,7 @@ public static void main(String[] args) throws Exception { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8888).usePlaintext().build(); RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); - Single.just("OSCON") + Single.just("World") // Call UNARY service asynchronously .map(RxgrpcAsyncChainClient::request) .as(stub::greet) diff --git a/demos/reactive-grpc-chat/reactor-chat-kotlin/ReactorChat-Client-Kt/pom.xml b/demos/reactive-grpc-chat/reactor-chat-kotlin/ReactorChat-Client-Kt/pom.xml index 1aae0478..b2368842 100644 --- a/demos/reactive-grpc-chat/reactor-chat-kotlin/ReactorChat-Client-Kt/pom.xml +++ b/demos/reactive-grpc-chat/reactor-chat-kotlin/ReactorChat-Client-Kt/pom.xml @@ -15,7 +15,7 @@ jline jline - 2.14.4 + 2.14.6 org.jetbrains.kotlin diff --git a/demos/reactive-grpc-chat/reactor-chat-kotlin/pom.xml b/demos/reactive-grpc-chat/reactor-chat-kotlin/pom.xml index b4aeb160..287b5c1e 100644 --- a/demos/reactive-grpc-chat/reactor-chat-kotlin/pom.xml +++ b/demos/reactive-grpc-chat/reactor-chat-kotlin/pom.xml @@ -22,9 +22,9 @@ 1.2.30 - 0.10.0 - 1.12.0 - 3.5.1 + 1.2.3 + 1.42.1 + 3.9.0 0.8.0 3.1.5.RELEASE @@ -40,6 +40,11 @@ grpc-protobuf ${grpc.version} + + io.grpc + grpc-stub + ${grpc.version} + com.salesforce.servicelibs reactor-grpc-stub diff --git a/demos/reactive-grpc-chat/reactor-chat/ReactorChat-Server/pom.xml b/demos/reactive-grpc-chat/reactor-chat/ReactorChat-Server/pom.xml index f2d51ac9..7eee6233 100644 --- a/demos/reactive-grpc-chat/reactor-chat/ReactorChat-Server/pom.xml +++ b/demos/reactive-grpc-chat/reactor-chat/ReactorChat-Server/pom.xml @@ -45,7 +45,7 @@ org.springframework spring-context - 4.2.0.RELEASE + 4.3.30.RELEASE diff --git a/demos/reactive-grpc-chat/reactor-chat/pom.xml b/demos/reactive-grpc-chat/reactor-chat/pom.xml index 7d5bc779..253ccdfd 100644 --- a/demos/reactive-grpc-chat/reactor-chat/pom.xml +++ b/demos/reactive-grpc-chat/reactor-chat/pom.xml @@ -17,9 +17,9 @@ 4.0.0 - 0.10.0 - 1.12.0 - 3.5.1 + 1.2.3 + 1.42.1 + 3.9.0 0.8.0 3.1.5.RELEASE @@ -40,6 +40,11 @@ grpc-protobuf ${grpc.version} + + io.grpc + grpc-stub + ${grpc.version} + com.salesforce.servicelibs reactor-grpc-stub diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/app/build.gradle b/demos/reactive-grpc-chat/rxjava-chat-android/app/build.gradle index a6001955..814dc62b 100644 --- a/demos/reactive-grpc-chat/rxjava-chat-android/app/build.gradle +++ b/demos/reactive-grpc-chat/rxjava-chat-android/app/build.gradle @@ -1,9 +1,9 @@ apply plugin: 'com.android.application' apply plugin: 'com.google.protobuf' -def reactiveGrpcVersion = '0.10.0' -def grpcVersion = '1.13.1' -def protobufVersion = '3.5.1' +def reactiveGrpcVersion = '1.0.0' +def grpcVersion = '1.23.0' +def protobufVersion = '3.9.0' android { compileSdkVersion 28 @@ -28,11 +28,11 @@ android { } protobuf { - protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}-1" } + protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}" } plugins { javalite { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" } grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } - rxgrpc { artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}:jdk8@jar" } + rxgrpc { artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}" } } generateProtoTasks { all().each { task -> diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/build.gradle b/demos/reactive-grpc-chat/rxjava-chat-android/build.gradle index f138e3be..51c6700b 100644 --- a/demos/reactive-grpc-chat/rxjava-chat-android/build.gradle +++ b/demos/reactive-grpc-chat/rxjava-chat-android/build.gradle @@ -10,7 +10,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.1.3' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' // NOTE: Do not place your application dependencies here; they belong diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.jar b/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.jar index 7a3265ee..5c2d1cf0 100644 Binary files a/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.jar and b/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.properties b/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.properties index a30e7346..ef9a9e05 100644 --- a/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.properties +++ b/demos/reactive-grpc-chat/rxjava-chat-android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 22 14:26:25 PDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/gradlew b/demos/reactive-grpc-chat/rxjava-chat-android/gradlew index cccdd3d5..83f2acfd 100755 --- a/demos/reactive-grpc-chat/rxjava-chat-android/gradlew +++ b/demos/reactive-grpc-chat/rxjava-chat-android/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` diff --git a/demos/reactive-grpc-chat/rxjava-chat-android/gradlew.bat b/demos/reactive-grpc-chat/rxjava-chat-android/gradlew.bat index e95643d6..24467a14 100644 --- a/demos/reactive-grpc-chat/rxjava-chat-android/gradlew.bat +++ b/demos/reactive-grpc-chat/rxjava-chat-android/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/demos/reactive-grpc-chat/rxjava-chat/pom.xml b/demos/reactive-grpc-chat/rxjava-chat/pom.xml index 66711aa8..3531739e 100644 --- a/demos/reactive-grpc-chat/rxjava-chat/pom.xml +++ b/demos/reactive-grpc-chat/rxjava-chat/pom.xml @@ -15,11 +15,11 @@ - 2.1.10 - 0.10.0 + 2.2.21 + 1.2.3 0.8.0 - 1.12.0 - 3.5.1 + 1.42.1 + 3.9.0 @@ -38,6 +38,11 @@ grpc-protobuf ${grpc.version} + + io.grpc + grpc-stub + ${grpc.version} + com.salesforce.servicelibs rxgrpc-stub diff --git a/demos/reactive-grpc-examples/pom.xml b/demos/reactive-grpc-examples/pom.xml index 7600b760..44495e40 100644 --- a/demos/reactive-grpc-examples/pom.xml +++ b/demos/reactive-grpc-examples/pom.xml @@ -10,9 +10,9 @@ - 0.10.0 - 1.12.0 - 3.5.1 + 1.0.0 + 1.42.1 + 3.9.0 diff --git a/inerop/README.md b/inerop/README.md new file mode 100644 index 00000000..43e158b7 --- /dev/null +++ b/inerop/README.md @@ -0,0 +1,17 @@ +# Reactive-gRPC Interoperability Tests + +This directory contains three gRPC Hello-World implementations in Java, C#, and Go. They represent the three base +implementations of gRPC - Java, C-Core, and Go. The Java service uses a Reactive-gRPC Reactor stub instead of a basic +gRPC-Java stub. + +These three services excercise the following cases: + +* Reactive-gRPC calling gRPC-C +* gRPC-C calling Reactive-gRPC +* Reactive-gRPC calling gRPC-Go +* gRPC-Go calling Reactive-gRPC + +## Running + +Each directory has a `run.sh` script that builds and starts each service. Start all three services and then follow +the on-screen prompts. diff --git a/inerop/csharp/.gitignore b/inerop/csharp/.gitignore new file mode 100644 index 00000000..cbbd0b5c --- /dev/null +++ b/inerop/csharp/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/inerop/csharp/.vscode/launch.json b/inerop/csharp/.vscode/launch.json new file mode 100644 index 00000000..488a52f0 --- /dev/null +++ b/inerop/csharp/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/example-csharp.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/inerop/csharp/.vscode/tasks.json b/inerop/csharp/.vscode/tasks.json new file mode 100644 index 00000000..66dd09c1 --- /dev/null +++ b/inerop/csharp/.vscode/tasks.json @@ -0,0 +1,36 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/example-csharp.csproj" + ], + "problemMatcher": "$tsc" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/example-csharp.csproj" + ], + "problemMatcher": "$tsc" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/example-csharp.csproj" + ], + "problemMatcher": "$tsc" + } + ] +} \ No newline at end of file diff --git a/inerop/csharp/Program.cs b/inerop/csharp/Program.cs new file mode 100644 index 00000000..c4e50d96 --- /dev/null +++ b/inerop/csharp/Program.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Com.Example; +using Grpc.Core; + +namespace example_csharp +{ + class GreeterImpl : Greeter.GreeterBase + { + public override Task SayHello(HelloRequest request, ServerCallContext context) { + Console.WriteLine("C# service request: " + request.Name); + return Task.FromResult(new HelloResponse { + Message = { + "Hello " + request.Name, + "Aloha " + request.Name, + "Howdy " + request.Name + } + }); + } + + public override async Task SayHelloStream(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { + Console.WriteLine("C# service stream request"); + while (await requestStream.MoveNext(context.CancellationToken)) { + var req = requestStream.Current; + var resp = new HelloResponse { + Message = { + "Hello " + req.Name, + "Aloha " + req.Name, + "Howdy " + req.Name + } + }; + await responseStream.WriteAsync(resp); + } + } + } + + class Program + { + static void Main(string[] args) + { + // Build the server + Console.WriteLine("Starting C# server on port 9002"); + Server server = new Server { + Services = { Greeter.BindService(new GreeterImpl()) }, + Ports = { new ServerPort("localhost", 9002, ServerCredentials.Insecure)} + }; + server.Start(); + + // Call the Java server on port 9000 + Console.WriteLine("Press enter to call the Java server..."); + Console.ReadKey(); + + // Set up gRPC client + Channel channel = new Channel("localhost:9000", ChannelCredentials.Insecure); + var client = new Greeter.GreeterClient(channel); + + // Call the service + var req = new HelloRequest { Name = "C#" }; + var resp = client.SayHello(req); + foreach (string msg in resp.Message) { + Console.WriteLine(msg); + } + + // Block for server termination + Console.ReadKey(); + channel.ShutdownAsync().Wait(); + server.ShutdownAsync().Wait(); + } + } +} diff --git a/inerop/csharp/README.md b/inerop/csharp/README.md new file mode 100644 index 00000000..03365652 --- /dev/null +++ b/inerop/csharp/README.md @@ -0,0 +1,9 @@ +# C# gRPC Demo + +## Prerequisites + +* dotnet core 2.2+ + +## Commands + +* Build and Run: `dotnet run` diff --git a/inerop/csharp/example-csharp.csproj b/inerop/csharp/example-csharp.csproj new file mode 100644 index 00000000..9f1e146c --- /dev/null +++ b/inerop/csharp/example-csharp.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp2.2 + example_csharp + + + + + + + + + + + + + diff --git a/inerop/csharp/protos/greeter.proto b/inerop/csharp/protos/greeter.proto new file mode 100644 index 00000000..0b77ab01 --- /dev/null +++ b/inerop/csharp/protos/greeter.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.example; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloResponse) {} + rpc SayHelloStream (stream HelloRequest) returns (stream HelloResponse) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + repeated string message = 1; +} \ No newline at end of file diff --git a/inerop/csharp/run.sh b/inerop/csharp/run.sh new file mode 100755 index 00000000..e9467c21 --- /dev/null +++ b/inerop/csharp/run.sh @@ -0,0 +1,2 @@ +#! /bin/sh +dotnet run \ No newline at end of file diff --git a/inerop/go/.gitignore b/inerop/go/.gitignore new file mode 100644 index 00000000..28392db2 --- /dev/null +++ b/inerop/go/.gitignore @@ -0,0 +1,2 @@ +example-go +*.pb.go \ No newline at end of file diff --git a/inerop/go/Makefile b/inerop/go/Makefile new file mode 100644 index 00000000..6e30bff6 --- /dev/null +++ b/inerop/go/Makefile @@ -0,0 +1,8 @@ +gen: + protoc -I . --go_out="plugins=grpc:." greeter.proto + +build: + go build + +run: + ./example-go \ No newline at end of file diff --git a/inerop/go/README.md b/inerop/go/README.md new file mode 100644 index 00000000..416e8089 --- /dev/null +++ b/inerop/go/README.md @@ -0,0 +1,13 @@ +# Go gRPC Demo + +## Prerequisites + +* make +* protoc +* go 1.12+ + +## Commands + +* Generate: `make gen` +* Build: `make build` +* Run: `make run` diff --git a/inerop/go/go.mod b/inerop/go/go.mod new file mode 100644 index 00000000..76316955 --- /dev/null +++ b/inerop/go/go.mod @@ -0,0 +1,8 @@ +module github.com/example/example-go + +go 1.12 + +require ( + github.com/golang/protobuf v1.3.1 + google.golang.org/grpc v1.21.1 +) diff --git a/inerop/go/go.sum b/inerop/go/go.sum new file mode 100644 index 00000000..a1960f6f --- /dev/null +++ b/inerop/go/go.sum @@ -0,0 +1,25 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/inerop/go/greeter.proto b/inerop/go/greeter.proto new file mode 100644 index 00000000..717060bd --- /dev/null +++ b/inerop/go/greeter.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package com.example; + +// Must add go_pacakge to override generated go package declaraion +option go_package="main"; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloResponse) {} + rpc SayHelloStream (stream HelloRequest) returns (stream HelloResponse) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + repeated string message = 1; +} \ No newline at end of file diff --git a/inerop/go/main.go b/inerop/go/main.go new file mode 100644 index 00000000..c293794f --- /dev/null +++ b/inerop/go/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + + grpc "google.golang.org/grpc" +) + +type greeter struct{} + +func (g *greeter) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { + fmt.Println("Go service request: " + req.GetName()) + resp := HelloResponse{ + Message: []string{ + "Hello " + req.GetName(), + "Aloha " + req.GetName(), + "Howdy " + req.GetName()}, + } + return &resp, nil +} + +func (g *greeter) SayHelloStream(srv Greeter_SayHelloStreamServer) error { + fmt.Println("Go service stream request") + for { + req, err := srv.Recv() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + resp := HelloResponse{ + Message: []string{ + "Hello " + req.GetName(), + "Aloha " + req.GetName(), + "Howdy " + req.GetName()}, + } + + if err := srv.Send(&resp); err != nil { + return err + } + } +} + +func main() { + // Build the server + listener, err := net.Listen("tcp", ":9001") + if err != nil { + panic(err) + } + server := grpc.NewServer() + RegisterGreeterServer(server, &greeter{}) + + go func() { + fmt.Println("Starting Go server on port 9001") + err = server.Serve(listener) + if err != nil { + panic(err) + } + }() + + // Call the C# server on port 9002 + fmt.Println("Press enter to call the Java server...") + fmt.Scanln() + + // Call the service + conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure()) + if err != nil { + panic(err) + } + defer conn.Close() + + client := NewGreeterClient(conn) + req := HelloRequest{Name: "Golang"} + + resp, err := client.SayHello(context.Background(), &req) + if err != nil { + panic(err) + } + for _, message := range resp.GetMessage() { + fmt.Println(message) + } + + // Wait to exit + fmt.Scanln() +} diff --git a/inerop/go/run.sh b/inerop/go/run.sh new file mode 100755 index 00000000..356ba6bf --- /dev/null +++ b/inerop/go/run.sh @@ -0,0 +1,2 @@ +#! /bin/sh +make gen build run \ No newline at end of file diff --git a/inerop/java/.gitignore b/inerop/java/.gitignore new file mode 100644 index 00000000..eb109446 --- /dev/null +++ b/inerop/java/.gitignore @@ -0,0 +1,5 @@ +.classpath +.project +.settings/ +target +dependency-reduced-pom.xml \ No newline at end of file diff --git a/inerop/java/.vscode/launch.json b/inerop/java/.vscode/launch.json new file mode 100644 index 00000000..b2511f16 --- /dev/null +++ b/inerop/java/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "type": "java", + "name": "CodeLens (Launch) - Main", + "request": "launch", + "mainClass": "com.example.Main", + "projectName": "example-java" + } + ] +} \ No newline at end of file diff --git a/inerop/java/.vscode/settings.json b/inerop/java/.vscode/settings.json new file mode 100644 index 00000000..e0f15db2 --- /dev/null +++ b/inerop/java/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/inerop/java/README.md b/inerop/java/README.md new file mode 100644 index 00000000..29f940f0 --- /dev/null +++ b/inerop/java/README.md @@ -0,0 +1,11 @@ +# Java gRPC Demo + +## Prerequisites + +* Apache Maven +* Java 8+ + +## Commands + +* Build: `mvn package` +* Run: `java -jar target/example-java-1.0-SNAPSHOT.jar` diff --git a/inerop/java/pom.xml b/inerop/java/pom.xml new file mode 100644 index 00000000..870119da --- /dev/null +++ b/inerop/java/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + com.example + example-java + 1.0-SNAPSHOT + + + 1.8 + 1.8 + UTF-8 + + 1.42.1 + 3.7.1 + + + + + + io.grpc + grpc-netty-shaded + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + javax.annotation + javax.annotation-api + 1.3.2 + + + com.salesforce.servicelibs + reactor-grpc-stub + 1.2.3 + + + + + + + kr.motd.maven + os-maven-plugin + 1.5.0.Final + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.5.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + reactor-grpc + com.salesforce.servicelibs + reactor-grpc + 1.0.0 + com.salesforce.reactorgrpc.ReactorGrpcGenerator + + + + + + + compile + compile-custom + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + com.example.Main + + + + + + + + + \ No newline at end of file diff --git a/inerop/java/run.sh b/inerop/java/run.sh new file mode 100755 index 00000000..aa656e51 --- /dev/null +++ b/inerop/java/run.sh @@ -0,0 +1,3 @@ +#! /bin/sh +mvn clean package +java -jar target/example-java-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/inerop/java/src/main/java/com/example/Main.java b/inerop/java/src/main/java/com/example/Main.java new file mode 100644 index 00000000..5dade040 --- /dev/null +++ b/inerop/java/src/main/java/com/example/Main.java @@ -0,0 +1,71 @@ +package com.example; + +import io.grpc.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class Main { + public static class GreeterImpl extends ReactorGreeterGrpc.GreeterImplBase { + @Override + public Mono sayHello(Mono request) { + return request.map(req -> + GreeterOuterClass.HelloResponse.newBuilder() + .addMessage("Hello " + req.getName()) + .addMessage("Aloha " + req.getName()) + .addMessage("Howdy " + req.getName()) + .build() + ); + } + } + + public static void main(String[] args) throws Exception { + // Build the server + System.out.println("Starting Java server on port 9000"); + Server server = ServerBuilder.forPort(9000).addService(new GreeterImpl()).build().start(); + + // Call the Go server on port 9001 + System.out.println("Press enter to call Go server..."); + System.in.read(); + + // Set up gRPC client + ManagedChannel goChannel = ManagedChannelBuilder.forAddress("localhost", 9001).usePlaintext().build(); + ReactorGreeterGrpc.ReactorGreeterStub goStub = ReactorGreeterGrpc.newReactorStub(goChannel); + + // Call the Go service + Mono.just(GreeterOuterClass.HelloRequest.newBuilder().setName("Java").build()) + .as(goStub::sayHello) + .block() + .getMessageList().forEach(msg -> System.out.println("Java " + msg)); + + Flux.range(1, 100) + .map(i -> GreeterOuterClass.HelloRequest.newBuilder().setName("Number " + i).build()) + .as(goStub::sayHelloStream) + .flatMap(resp -> Flux.fromIterable(resp.getMessageList())) + .doOnEach(msg -> System.out.println("Java " + msg)) + .blockLast(); + + // Call the C# server on port 9002 + System.out.println("Press enter to call C# server..."); + System.in.read(); + + // Set up gRPC client + ManagedChannel csharpChannel = ManagedChannelBuilder.forAddress("localhost", 9001).usePlaintext().build(); + ReactorGreeterGrpc.ReactorGreeterStub csharpStub = ReactorGreeterGrpc.newReactorStub(csharpChannel); + + // Call the Go service + Mono.just(GreeterOuterClass.HelloRequest.newBuilder().setName("Java").build()) + .as(csharpStub::sayHello) + .block() + .getMessageList().forEach(msg -> System.out.println("Java " + msg)); + + Flux.range(1, 100) + .map(i -> GreeterOuterClass.HelloRequest.newBuilder().setName("Number " + i).build()) + .as(csharpStub::sayHelloStream) + .flatMap(resp -> Flux.fromIterable(resp.getMessageList())) + .doOnEach(msg -> System.out.println("Java " + msg)) + .blockLast(); + + // Block for server termination + server.awaitTermination(); + } +} diff --git a/inerop/java/src/main/proto/greeter.proto b/inerop/java/src/main/proto/greeter.proto new file mode 100644 index 00000000..0b77ab01 --- /dev/null +++ b/inerop/java/src/main/proto/greeter.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.example; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloResponse) {} + rpc SayHelloStream (stream HelloRequest) returns (stream HelloResponse) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + repeated string message = 1; +} \ No newline at end of file diff --git a/inerop/launch.sh b/inerop/launch.sh new file mode 100755 index 00000000..685c485c --- /dev/null +++ b/inerop/launch.sh @@ -0,0 +1,5 @@ +#! /bin/sh +code java +code go +code csharp + diff --git a/pom.xml b/pom.xml index 21d5ca0b..b4c4d6f5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT pom reactive-grpc @@ -42,12 +42,21 @@ common/reactive-grpc-common common/reactive-grpc-gencommon + rx-java/rxgrpc rx-java/rxgrpc-stub rx-java/rxgrpc-tck rx-java/rxgrpc-test + + rx3-java/rx3grpc + rx3-java/rx3grpc-stub + rx3-java/rx3grpc-tck + rx3-java/rx3grpc-test + reactor/reactor-grpc reactor/reactor-grpc-stub + reactor/reactor-grpc-retry + reactor/reactor-grpc-retry-pre3.3.9 reactor/reactor-grpc-tck reactor/reactor-grpc-test reactor/reactor-grpc-test-32 @@ -55,23 +64,28 @@ - - 1.0.2 - 1.19.0 - 3.6.1 + 0.6.1 3.8.0 - 0.8.1 - 0.9.1 - 2.2.7 - 3.1.15.RELEASE - 3.1.9.RELEASE + 1.1.0 + + + 1.0.4 + 1.54.0 + 3.22.2 + 1.2.2 + + 2.2.21 + 3.5.4 + 3.1.6 + 0.8.1 5.4.0 3.12.2 3.1.6 2.27.0 + 3.1.9.RELEASE UTF-8 1.8 @@ -119,6 +133,11 @@ reactor-grpc-stub ${project.version} + + ${project.groupId} + rx3grpc-stub + ${project.version} + com.salesforce.servicelibs grpc-contrib @@ -129,30 +148,47 @@ jprotoc ${jprotoc.version} + + com.google.protobuf + protobuf-java + ${protoc.version} + provided + io.grpc grpc-netty ${grpc.version} + provided io.grpc grpc-protobuf ${grpc.version} + provided io.grpc grpc-core ${grpc.version} + provided + + + io.grpc + grpc-api + ${grpc.version} + provided io.grpc grpc-context ${grpc.version} + provided io.grpc grpc-stub ${grpc.version} + provided @@ -220,15 +256,38 @@ + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.20 + + + signature-check + verify + + check + + + + + + org.codehaus.mojo.signature + java18 + 1.0 + + + + org.apache.maven.plugins maven-checkstyle-plugin - 3.0.0 + 3.1.0 com.puppycrawl.tools checkstyle - 8.19 + 8.29 @@ -249,23 +308,9 @@ + org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M3 - - - - - - - - - - - - - - -Xmx1024m -XX:MaxPermSize=256m - + 3.0.0 @@ -341,4 +386,4 @@ - \ No newline at end of file + diff --git a/reactor/README.md b/reactor/README.md index bc9e494f..4690c386 100644 --- a/reactor/README.md +++ b/reactor/README.md @@ -36,7 +36,7 @@ protobuf { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } reactor { - artifact = "com.salesforce.servicelibs:reactor-grpc:${reactiveGrpcVersion}:jdk8@jar" + artifact = "com.salesforce.servicelibs:reactor-grpc:${reactiveGrpcVersion}" } } generateProtoTasks { @@ -49,8 +49,38 @@ protobuf { ``` And add the following dependency: `"com.salesforce.servicelibs:reactor-grpc-stub:${reactiveGrpcVersion}"` -*At this time, Reactor-gRPC with Gradle only supports bash-based environments. Windows users will need to build using Windows Subsystem for Linux (win 10), Gitbash, or Cygwin.* +*At this time, Reactor-gRPC with Gradle only supports bash-based environments. Windows users will need to build using +Windows Subsystem for Linux (win 10) or invoke the Maven protobuf plugin with Gradle.* +### Bazel +To use RxGrpc with Bazel, update your `WORKSPACE` file. + +```bazel +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "com_salesforce_servicelibs_reactive_grpc", + strip_prefix = "reactive-grpc-1.0.1", + url = "https://github.com/salesforce/reactive-grpc/archive/v1.0.1.zip", +) + +load("@com_salesforce_servicelibs_reactive_grpc//bazel:repositories.bzl", "repositories") + +repositories() +``` + +Then, add a `rx_grpc_library()` rule to your proto's `BUILD` file, referencing both the `proto_library()` target and +the `java_proto_library()` target. + +```bazel +load("@com_salesforce_servicelibs_reactive_grpc//bazel:java_reactive_grpc_library.bzl", "rx_grpc_library") + +rx_grpc_library( + name = "foo_rx_proto", + proto = ":foo_proto", + deps = [":foo_java_proto"], +) +``` Usage ===== After installing the plugin, Reactor-gRPC service stubs will be generated along with your gRPC service stubs. @@ -90,12 +120,12 @@ After installing the plugin, Reactor-gRPC service stubs will be generated along ## Don't break the chain Used on their own, the generated Reactor stub methods do not cleanly chain with other Reactor operators. -Using the `compose()` and `as()` methods of `Mono` and `Flux` are preferred over direct invocation. +Using the `transform()` and `as()` methods of `Mono` and `Flux` are preferred over direct invocation. #### One→One, Many→Many ```java -Mono monoResponse = monoRequest.compose(stub::sayHello); -Flux fluxResponse = fluxRequest.compose(stub::sayHelloBothStream); +Mono monoResponse = monoRequest.transform(stub::sayHello); +Flux fluxResponse = fluxRequest.transform(stub::sayHelloBothStream); ``` #### One→Many, Many→One @@ -110,11 +140,52 @@ retry, the upstream rx pipeline is re-subscribed to acquire a request message an rx pipeline never sees the error. ```java -Flux fluxResponse = fluxRequest.compose(GrpcRetry.ManyToMany.retry(stub::sayHelloBothStream)); +Flux fluxResponse = fluxRequest.transformDeferred(GrpcRetry.ManyToMany.retry(stub::sayHelloBothStream)); ``` For complex retry scenarios, use the `Retry` builder from Reactor Extras. +Due to breaking changes to the Flux retry API introduced in Reactor 3.3.9 (Semver fail!), the Retry class has been moved +to either the `reactor-grpc-retry` or `reactor-grpc-retry-pre3.3.9` maven modules. You must select the correct module +for your version. + +## gRPC Context propagation +Reactor does not have a convenient mechanism for passing the `ThreadLocal` gRPC `Context` between threads in a reactive +call chain. If you never use `observeOn()` or `subscribeOn()` the gRPC context _should_ propagate correctly. However, +the use of a `Scheduler` will necessitate taking manual control over Context propagation. + +Two context propagation techniques are: + +1. Capture the context in a `Tuple` intermediate type, transforming an indirect static context reference + into an explicit reference. +2. Make use of Reactor's [`subscriberContext()`](https://github.com/reactor/reactor-core/blob/master/docs/asciidoc/advancedFeatures.adoc#adding-a-context-to-a-reactive-sequence) + API to capture the gRPC context in the call chain. + +## Configuration of flow control +Reactor GRPC by default prefetch 512 items on client and server side. When the messages are bigger it +can consume a lot of memory. One can override these default settings using ReactorCallOptions: + +Prefetch on client side (client consumes too slow): + +```java + ReactorMyAPIStub api = ReactorMyAPIGrpc.newReactorStub(channel) + .withOption(ReactorCallOptions.CALL_OPTIONS_PREFETCH, 16) + .withOption(ReactorCallOptions.CALL_OPTIONS_LOW_TIDE, 4); +``` + +Prefetch on server side (server consumes too slow): + +```java + // Override getCallOptions method in your *ImplBase service class. + // One can use methodId to do method specific override + @Override + protected CallOptions getCallOptions(int methodId) { + return CallOptions.DEFAULT + .withOption(ReactorCallOptions.CALL_OPTIONS_PREFETCH, 16) + .withOption(ReactorCallOptions.CALL_OPTIONS_LOW_TIDE, 4); + } +``` + Modules ======= @@ -122,5 +193,7 @@ Reactor-gRPC is broken down into four sub-modules: * _reactor-grpc_ - a protoc generator for generating gRPC bindings for Reactor. * _reactor-grpc-stub_ - stub classes supporting the generated Reactor bindings. +* _reactor-grpc-retry_ - class for retrying requests. +* _reactor-grpc-retry-pre3.3.9_ - class for retrying requests for Reactor versions <= 3.3.8. * _reactor-grpc-test_ - integration tests for Reactor. * _reactor-grpc-tck_ - Reactive Streams TCK compliance tests for Reactor. diff --git a/reactor/reactor-grpc-retry-pre3.3.9/pom.xml b/reactor/reactor-grpc-retry-pre3.3.9/pom.xml new file mode 100644 index 00000000..787f7741 --- /dev/null +++ b/reactor/reactor-grpc-retry-pre3.3.9/pom.xml @@ -0,0 +1,116 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + reactor-grpc-retry-pre3.3.9 + + + 3.3.8.RELEASE + + + + + ${project.groupId} + reactive-grpc-common + ${project.version} + + + io.projectreactor + reactor-core + ${old.reactor.version} + provided + + + + io.projectreactor + reactor-test + ${old.reactor.version} + test + + + io.projectreactor.addons + reactor-extra + ${reactor.extra.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + ../../checkstyle.xml + ../../checkstyle_ignore.xml + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 5.1.2 + + + bundle-manifest + process-classes + + manifest + + + + + + + diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/GrpcRetry.java b/reactor/reactor-grpc-retry-pre3.3.9/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java similarity index 99% rename from reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/GrpcRetry.java rename to reactor/reactor-grpc-retry-pre3.3.9/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java index 249fb0f2..985e47fd 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/GrpcRetry.java +++ b/reactor/reactor-grpc-retry-pre3.3.9/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -package com.salesforce.reactorgrpc; +package com.salesforce.reactorgrpc.retry; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -166,4 +166,4 @@ public static Function, Mono> retryImmediately(final F return retryWhen(operation, errors -> errors); } } -} +} \ No newline at end of file diff --git a/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/GrpcRetryTest.java b/reactor/reactor-grpc-retry-pre3.3.9/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java similarity index 98% rename from reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/GrpcRetryTest.java rename to reactor/reactor-grpc-retry-pre3.3.9/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java index f42cffa5..5590fc66 100644 --- a/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/GrpcRetryTest.java +++ b/reactor/reactor-grpc-retry-pre3.3.9/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java @@ -4,9 +4,8 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -package com.salesforce.reactorgrpc.stub; +package com.salesforce.reactorgrpc.retry; -import com.salesforce.reactorgrpc.GrpcRetry; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -170,4 +169,4 @@ public void manyToOneRetryAfter() { .expectComplete() .verify(Duration.ofSeconds(1)); } -} +} \ No newline at end of file diff --git a/reactor/reactor-grpc-retry/pom.xml b/reactor/reactor-grpc-retry/pom.xml new file mode 100644 index 00000000..cf235790 --- /dev/null +++ b/reactor/reactor-grpc-retry/pom.xml @@ -0,0 +1,112 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + reactor-grpc-retry + + + + ${project.groupId} + reactive-grpc-common + ${project.version} + + + io.projectreactor + reactor-core + ${reactor.version} + provided + + + + io.projectreactor + reactor-test + ${reactor.version} + test + + + io.projectreactor.addons + reactor-extra + ${reactor.extra.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + ../../checkstyle.xml + ../../checkstyle_ignore.xml + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 5.1.2 + + + bundle-manifest + process-classes + + manifest + + + + + + + diff --git a/reactor/reactor-grpc-retry/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java b/reactor/reactor-grpc-retry/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java new file mode 100644 index 00000000..905f3375 --- /dev/null +++ b/reactor/reactor-grpc-retry/src/main/java/com/salesforce/reactorgrpc/retry/GrpcRetry.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactorgrpc.retry; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.function.Function; + +/** + * {@code GrpcRetry} is used to transparently re-establish a streaming gRPC request in the event of a server error. + *

+ * During a retry, the upstream rx pipeline is re-subscribed to acquire a request message and the RPC call re-issued. + * The downstream rx pipeline never sees the error. + */ +public final class GrpcRetry { + private GrpcRetry() { } + + /** + * {@link GrpcRetry} functions for streaming response gRPC operations. + */ + public static final class OneToMany { + private OneToMany() { + } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flux#retryWhen(Retry)}. + * + * For easier use, use the Retry builder from + * Reactor Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param whenFactory receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flux#retryWhen(Retry) + */ + public static Function, Flux> retryWhen(final Function, Flux> operation, final Function, ? extends Publisher> whenFactory) { + return request -> Flux.defer(() -> operation.apply(request)).retryWhen(Retry.withThrowable(whenFactory)); + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param I + * @param O + */ + public static Function, Flux> retryAfter(final Function, Flux> operation, final Duration delay) { + return retryWhen(operation, errors -> errors.delayElements(delay)); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static Function, Flux> retryImmediately(final Function, Flux> operation) { + return retryWhen(operation, errors -> errors); + } + } + + /** + * {@link GrpcRetry} functions for bi-directional streaming gRPC operations. + */ + public static final class ManyToMany { + private ManyToMany() { + } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flux#retryWhen(Retry)}. + * + * For easier use, use the Retry builder from + * Reactor Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param whenFactory receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flux#retryWhen(Retry) + */ + public static Function, ? extends Publisher> retryWhen(final Function, Flux> operation, final Function, ? extends Publisher> whenFactory) { + return request -> Flux.defer(() -> operation.apply(request)).retryWhen(Retry.withThrowable(whenFactory)); + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param I + * @param O + */ + public static Function, ? extends Publisher> retryAfter(final Function, Flux> operation, final Duration delay) { + return retryWhen(operation, errors -> errors.delayElements(delay)); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static Function, ? extends Publisher> retryImmediately(final Function, Flux> operation) { + return retryWhen(operation, errors -> errors); + } + } + + /** + * {@link GrpcRetry} functions for streaming request gRPC operations. + */ + public static final class ManyToOne { + private ManyToOne() { + } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flux#retryWhen(Retry)}. + * + * For easier use, use the Retry builder from + * Reactor Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param whenFactory receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flux#retryWhen(Retry) + */ + public static Function, Mono> retryWhen(final Function, Mono> operation, final Function, ? extends Publisher> whenFactory) { + return request -> Mono.defer(() -> operation.apply(request)).retryWhen(Retry.withThrowable(whenFactory)); + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param I + * @param O + */ + public static Function, Mono> retryAfter(final Function, Mono> operation, final Duration delay) { + return retryWhen(operation, errors -> errors.delayElements(delay)); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static Function, Mono> retryImmediately(final Function, Mono> operation) { + return retryWhen(operation, errors -> errors); + } + } +} diff --git a/reactor/reactor-grpc-retry/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java b/reactor/reactor-grpc-retry/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java new file mode 100644 index 00000000..9d887a8a --- /dev/null +++ b/reactor/reactor-grpc-retry/src/test/java/com/salesforce/reactorgrpc/retry/GrpcRetryTest.java @@ -0,0 +1,172 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactorgrpc.retry; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; +import reactor.retry.Retry; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; + +@SuppressWarnings("Duplicates") +public class GrpcRetryTest { + private Flux newThreeErrorFlux() { + return Flux.create(new Consumer>() { + int count = 3; + @Override + public void accept(FluxSink emitter) { + if (count > 0) { + emitter.error(new Throwable("Not yet!")); + count--; + } else { + emitter.next(0); + emitter.complete(); + } + } + }, FluxSink.OverflowStrategy.BUFFER); + } + + private Mono newThreeErrorMono() { + return Mono.create(new Consumer>() { + int count = 3; + @Override + public void accept(MonoSink emitter){ + if (count > 0) { + emitter.error(new Throwable("Not yet!")); + count--; + } else { + emitter.success(0); + } + } + }); + } + + @Test + public void noRetryMakesErrorFlowabable() { + Flux test = newThreeErrorFlux() + .as(flux -> flux); + + StepVerifier.create(test) + .expectErrorMessage("Not yet!") + .verify(Duration.ofSeconds(1)); + } + + @Test + public void noRetryMakesErrorSingle() { + Mono test = newThreeErrorMono() + .as(mono -> mono); + + StepVerifier.create(test) + .expectErrorMessage("Not yet!") + .verify(Duration.ofSeconds(1)); + } + + @Test + public void oneToManyRetryWhen() { + Flux test = newThreeErrorMono() + .>as(GrpcRetry.OneToMany.retryWhen(Mono::flux, Retry.any().retryMax(4))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void oneToManyRetryImmediately() { + Flux test = newThreeErrorMono() + .>as(GrpcRetry.OneToMany.retryImmediately(Mono::flux)); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void oneToManyRetryAfter() { + Flux test = newThreeErrorMono() + .>as(GrpcRetry.OneToMany.retryAfter(Mono::flux, Duration.ofMillis(10))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToManyRetryWhen() { + Flux test = newThreeErrorFlux() + .transformDeferred(GrpcRetry.ManyToMany.retryWhen(Function.identity(), Retry.any().retryMax(4))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToManyRetryImmediately() { + Flux test = newThreeErrorFlux() + .transformDeferred(GrpcRetry.ManyToMany.retryImmediately(Function.identity())); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToManyRetryAfter() { + Flux test = newThreeErrorFlux() + .transformDeferred(GrpcRetry.ManyToMany.retryAfter(Function.identity(), Duration.ofMillis(10))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToOneRetryWhen() { + Mono test = newThreeErrorFlux() + .>as(GrpcRetry.ManyToOne.retryWhen(Flux::single, Retry.any().retryMax(4))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToOneRetryImmediately() { + Mono test = newThreeErrorFlux() + .>as(GrpcRetry.ManyToOne.retryImmediately(Flux::single)); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + public void manyToOneRetryAfter() { + Mono test = newThreeErrorFlux() + .>as(GrpcRetry.ManyToOne.retryAfter(Flux::single, Duration.ofMillis(10))); + + StepVerifier.create(test) + .expectNext(0) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } +} diff --git a/reactor/reactor-grpc-stub/BUILD.bazel b/reactor/reactor-grpc-stub/BUILD.bazel new file mode 100644 index 00000000..61f6519a --- /dev/null +++ b/reactor/reactor-grpc-stub/BUILD.bazel @@ -0,0 +1,15 @@ +java_library( + name = "reactor-grpc-stub", + srcs = glob(["src/main/**/*.java"]), + # Disable error prone build failure (triggered by unused return) + javacopts = ["-XepDisableAllChecks"], + visibility = ["//visibility:public"], + deps = [ + "//common/reactive-grpc-common", + "@com_google_guava_guava", + "@io_grpc_grpc_java//core", + "@io_grpc_grpc_java//stub", + "@io_projectreactor_reactor_core", + "@org_reactivestreams_reactive_streams", + ], +) diff --git a/reactor/reactor-grpc-stub/pom.xml b/reactor/reactor-grpc-stub/pom.xml index ae789ec0..c06c215b 100644 --- a/reactor/reactor-grpc-stub/pom.xml +++ b/reactor/reactor-grpc-stub/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -25,23 +25,23 @@ reactive-grpc-common ${project.version} + + io.grpc + grpc-stub + io.projectreactor reactor-core ${reactor.version} + provided + io.projectreactor reactor-test ${reactor.version} test - - io.projectreactor.addons - reactor-extra - ${reactor.extra.version} - test - org.junit.jupiter junit-jupiter-api @@ -79,16 +79,8 @@ ../../checkstyle_ignore.xml - - org.apache.maven.plugins - maven-compiler-plugin - ${compiler.plugin.version} - - 1.8 - 1.8 - - + org.apache.maven.plugins maven-jar-plugin @@ -102,7 +94,7 @@ org.apache.felix maven-bundle-plugin - 4.1.0 + 5.1.2 bundle-manifest diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ClientCalls.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ClientCalls.java index 478bf096..54b8d2ec 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ClientCalls.java +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ClientCalls.java @@ -7,11 +7,11 @@ package com.salesforce.reactorgrpc.stub; -import java.util.function.BiConsumer; -import java.util.function.Function; - +import io.grpc.CallOptions; import io.grpc.stub.CallStreamObserver; import io.grpc.stub.StreamObserver; +import java.util.function.BiConsumer; +import java.util.function.Function; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; @@ -31,7 +31,8 @@ private ClientCalls() { */ public static Mono oneToOne( Mono monoSource, - BiConsumer> delegate) { + BiConsumer> delegate, + CallOptions options) { try { return Mono .create(emitter -> monoSource.subscribe( @@ -65,12 +66,17 @@ public void onCompleted() { */ public static Flux oneToMany( Mono monoSource, - BiConsumer> delegate) { + BiConsumer> delegate, + CallOptions options) { try { + + final int prefetch = ReactorCallOptions.getPrefetch(options); + final int lowTide = ReactorCallOptions.getLowTide(options); + return monoSource .flatMapMany(request -> { ReactorClientStreamObserverAndPublisher consumerStreamObserver = - new ReactorClientStreamObserverAndPublisher<>(null); + new ReactorClientStreamObserverAndPublisher<>(null, null, prefetch, lowTide); delegate.accept(request, consumerStreamObserver); @@ -88,7 +94,8 @@ public static Flux oneToMany( @SuppressWarnings("unchecked") public static Mono manyToOne( Flux fluxSource, - Function, StreamObserver> delegate) { + Function, StreamObserver> delegate, + CallOptions options) { try { ReactorSubscriberAndClientProducer subscriberAndGRPCProducer = fluxSource.subscribeWith(new ReactorSubscriberAndClientProducer<>()); @@ -97,10 +104,10 @@ public static Mono manyToOne( s -> subscriberAndGRPCProducer.subscribe((CallStreamObserver) s), subscriberAndGRPCProducer::cancel ); - delegate.apply(observerAndPublisher); return Flux.from(observerAndPublisher) - .singleOrEmpty(); + .doOnSubscribe(s -> delegate.apply(observerAndPublisher)) + .singleOrEmpty(); } catch (Throwable throwable) { return Mono.error(throwable); } @@ -113,18 +120,22 @@ public static Mono manyToOne( @SuppressWarnings("unchecked") public static Flux manyToMany( Flux fluxSource, - Function, StreamObserver> delegate) { + Function, StreamObserver> delegate, + CallOptions options) { try { + + final int prefetch = ReactorCallOptions.getPrefetch(options); + final int lowTide = ReactorCallOptions.getLowTide(options); + ReactorSubscriberAndClientProducer subscriberAndGRPCProducer = fluxSource.subscribeWith(new ReactorSubscriberAndClientProducer<>()); ReactorClientStreamObserverAndPublisher observerAndPublisher = new ReactorClientStreamObserverAndPublisher<>( s -> subscriberAndGRPCProducer.subscribe((CallStreamObserver) s), - subscriberAndGRPCProducer::cancel + subscriberAndGRPCProducer::cancel, prefetch, lowTide ); - delegate.apply(observerAndPublisher); - return Flux.from(observerAndPublisher); + return Flux.from(observerAndPublisher).doOnSubscribe(s -> delegate.apply(observerAndPublisher)); } catch (Throwable throwable) { return Flux.error(throwable); } diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorCallOptions.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorCallOptions.java new file mode 100644 index 00000000..918ffb16 --- /dev/null +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorCallOptions.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactorgrpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher; +import io.grpc.CallOptions; + +/** + * Reactor Call options. + */ +public final class ReactorCallOptions { + + private ReactorCallOptions() { + } + + /** + * Sets Prefetch size of queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_PREFETCH = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.PREFETCH", + Integer.valueOf(AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE)); + + /** + * Sets Low Tide of prefetch queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_LOW_TIDE = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.LOW_TIDE", + Integer.valueOf(AbstractStreamObserverAndPublisher.TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE)); + + + /** + * Utility function to get prefetch option. + */ + public static int getPrefetch(final CallOptions options) { + return options == null ? CALL_OPTIONS_PREFETCH.getDefault() : options.getOption(CALL_OPTIONS_PREFETCH); + } + + /** + * Utility function to get low tide option together with validation. + */ + public static int getLowTide(final CallOptions options) { + int prefetch = getPrefetch(options); + int lowTide = options == null ? CALL_OPTIONS_LOW_TIDE.getDefault() : options.getOption(CALL_OPTIONS_LOW_TIDE); + if (lowTide >= prefetch) { + throw new IllegalArgumentException(CALL_OPTIONS_LOW_TIDE + " must be less than " + CALL_OPTIONS_PREFETCH); + } + return lowTide; + } + +} diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisher.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisher.java index d91971de..12fb1c3c 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisher.java +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisher.java @@ -10,9 +10,13 @@ import com.salesforce.reactivegrpc.common.AbstractClientStreamObserverAndPublisher; import com.salesforce.reactivegrpc.common.Consumer; import io.grpc.stub.CallStreamObserver; +import reactor.core.CoreSubscriber; import reactor.core.Fuseable; +import reactor.core.publisher.Operators; import reactor.util.concurrent.Queues; +import java.util.Queue; + /** * TODO: Explain what this class does. * @param T @@ -30,6 +34,14 @@ class ReactorClientStreamObserverAndPublisher super(Queues.get(DEFAULT_CHUNK_SIZE).get(), onSubscribe, onTerminate); } + ReactorClientStreamObserverAndPublisher( + Consumer> onSubscribe, + Runnable onTerminate, + int prefetch, + int lowTide) { + super(Queues.get(DEFAULT_CHUNK_SIZE).get(), onSubscribe, onTerminate, prefetch, lowTide); + } + @Override public int requestFusion(int requestedMode) { if ((requestedMode & Fuseable.ASYNC) != 0) { @@ -38,4 +50,20 @@ public int requestFusion(int requestedMode) { } return Fuseable.NONE; } + + @Override + protected void discardQueue(Queue q) { + if (downstream instanceof CoreSubscriber) { + Operators.onDiscardQueueWithClear(q, ((CoreSubscriber) downstream).currentContext(), null); + } else { + q.clear(); + } + } + + @Override + protected void discardElement(T t) { + if (downstream instanceof CoreSubscriber) { + Operators.onDiscard(t, ((CoreSubscriber) downstream).currentContext()); + } + } } diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisher.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisher.java index da1451bf..33bac861 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisher.java +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisher.java @@ -11,9 +11,13 @@ import com.salesforce.reactivegrpc.common.Consumer; import io.grpc.stub.CallStreamObserver; import io.grpc.stub.ServerCallStreamObserver; +import reactor.core.CoreSubscriber; import reactor.core.Fuseable; +import reactor.core.publisher.Operators; import reactor.util.concurrent.Queues; +import java.util.Queue; + /** * TODO: Explain what this class does. * @param T @@ -24,8 +28,10 @@ class ReactorServerStreamObserverAndPublisher ReactorServerStreamObserverAndPublisher( ServerCallStreamObserver serverCallStreamObserver, - Consumer> onSubscribe) { - super(serverCallStreamObserver, Queues.get(DEFAULT_CHUNK_SIZE).get(), onSubscribe); + Consumer> onSubscribe, + int prefetch, + int lowTide) { + super(serverCallStreamObserver, Queues.get(DEFAULT_CHUNK_SIZE).get(), onSubscribe, prefetch, lowTide); } @Override @@ -36,4 +42,20 @@ public int requestFusion(int requestedMode) { } return Fuseable.NONE; } + + @Override + protected void discardQueue(Queue q) { + if (downstream instanceof CoreSubscriber) { + Operators.onDiscardQueueWithClear(q, ((CoreSubscriber) downstream).currentContext(), null); + } else { + q.clear(); + } + } + + @Override + protected void discardElement(T t) { + if (downstream instanceof CoreSubscriber) { + Operators.onDiscard(t, ((CoreSubscriber) downstream).currentContext()); + } + } } \ No newline at end of file diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorSubscriberAndServerProducer.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorSubscriberAndServerProducer.java index 14e136a0..cd3a6ff6 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorSubscriberAndServerProducer.java +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ReactorSubscriberAndServerProducer.java @@ -8,6 +8,7 @@ package com.salesforce.reactorgrpc.stub; import com.salesforce.reactivegrpc.common.AbstractSubscriberAndServerProducer; +import com.salesforce.reactivegrpc.common.Function; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -21,6 +22,10 @@ public class ReactorSubscriberAndServerProducer extends AbstractSubscriberAndServerProducer implements CoreSubscriber { + public ReactorSubscriberAndServerProducer(Function prepareError) { + super(prepareError); + } + @Override protected Subscription fuse(Subscription s) { if (s instanceof Fuseable.QueueSubscription) { diff --git a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ServerCalls.java b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ServerCalls.java index 3f19dc85..fe153042 100644 --- a/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ServerCalls.java +++ b/reactor/reactor-grpc-stub/src/main/java/com/salesforce/reactorgrpc/stub/ServerCalls.java @@ -10,11 +10,13 @@ import java.util.function.Function; import com.google.common.base.Preconditions; +import io.grpc.CallOptions; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,23 +35,24 @@ private ServerCalls() { */ public static void oneToOne( TRequest request, StreamObserver responseObserver, - Function, Mono> delegate) { + Function> delegate, + Function prepareError) { try { - Mono rxRequest = Mono.just(request); - - Mono rxResponse = Preconditions.checkNotNull(delegate.apply(rxRequest)); - rxResponse.subscribe( + Mono rxResponse = Preconditions.checkNotNull(delegate.apply(request)); + Disposable subscription = rxResponse.subscribe( value -> { // Don't try to respond if the server has already canceled the request if (responseObserver instanceof ServerCallStreamObserver && ((ServerCallStreamObserver) responseObserver).isCancelled()) { return; } responseObserver.onNext(value); - responseObserver.onCompleted(); }, - throwable -> responseObserver.onError(prepareError(throwable))); + throwable -> responseObserver.onError(prepareError.apply(throwable)), + responseObserver::onCompleted + ); + cancelSubscriptionOnCallEnd(subscription, (ServerCallStreamObserver) responseObserver); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } } @@ -59,15 +62,14 @@ public static void oneToOne( */ public static void oneToMany( TRequest request, StreamObserver responseObserver, - Function, Flux> delegate) { + Function> delegate, + Function prepareError) { try { - Mono rxRequest = Mono.just(request); - - Flux rxResponse = Preconditions.checkNotNull(delegate.apply(rxRequest)); - ReactorSubscriberAndServerProducer server = rxResponse.subscribeWith(new ReactorSubscriberAndServerProducer<>()); + Flux rxResponse = Preconditions.checkNotNull(delegate.apply(request)); + ReactorSubscriberAndServerProducer server = rxResponse.subscribeWith(new ReactorSubscriberAndServerProducer<>(prepareError::apply)); server.subscribe((ServerCallStreamObserver) responseObserver); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } } @@ -77,30 +79,37 @@ public static void oneToMany( */ public static StreamObserver manyToOne( StreamObserver responseObserver, - Function, Mono> delegate) { + Function, Mono> delegate, + Function prepareError, + CallOptions options) { + + final int prefetch = ReactorCallOptions.getPrefetch(options); + final int lowTide = ReactorCallOptions.getLowTide(options); + ReactorServerStreamObserverAndPublisher streamObserverPublisher = - new ReactorServerStreamObserverAndPublisher<>((ServerCallStreamObserver) responseObserver, null); + new ReactorServerStreamObserverAndPublisher<>((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); try { Mono rxResponse = Preconditions.checkNotNull(delegate.apply(Flux.from(streamObserverPublisher))); - rxResponse.subscribe( + Disposable subscription = rxResponse.subscribe( value -> { // Don't try to respond if the server has already canceled the request if (!streamObserverPublisher.isCancelled()) { responseObserver.onNext(value); - responseObserver.onCompleted(); } }, throwable -> { // Don't try to respond if the server has already canceled the request if (!streamObserverPublisher.isCancelled()) { streamObserverPublisher.abortPendingCancel(); - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } - } + }, + responseObserver::onCompleted ); + cancelSubscriptionOnCallEnd(subscription, (ServerCallStreamObserver) responseObserver); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } return streamObserverPublisher; @@ -112,28 +121,41 @@ public static StreamObserver manyToOne( */ public static StreamObserver manyToMany( StreamObserver responseObserver, - Function, Flux> delegate) { - ReactorServerStreamObserverAndPublisher streamObserverPublisher = - new ReactorServerStreamObserverAndPublisher<>((ServerCallStreamObserver) responseObserver, null); + Function, Flux> delegate, + Function prepareError, + CallOptions options) { + final int prefetch = ReactorCallOptions.getPrefetch(options); + final int lowTide = ReactorCallOptions.getLowTide(options); + + ReactorServerStreamObserverAndPublisher streamObserverPublisher = + new ReactorServerStreamObserverAndPublisher<>((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); try { Flux rxResponse = Preconditions.checkNotNull(delegate.apply(Flux.from(streamObserverPublisher))); - ReactorSubscriberAndServerProducer subscriber = new ReactorSubscriberAndServerProducer<>(); + ReactorSubscriberAndServerProducer subscriber = new ReactorSubscriberAndServerProducer<>(prepareError::apply); subscriber.subscribe((ServerCallStreamObserver) responseObserver); // Don't try to respond if the server has already canceled the request rxResponse.subscribe(subscriber); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } return streamObserverPublisher; } - private static Throwable prepareError(Throwable throwable) { + /** + * Implements default error mapping. + */ + public static Throwable prepareError(Throwable throwable) { if (throwable instanceof StatusException || throwable instanceof StatusRuntimeException) { return throwable; } else { return Status.fromThrowable(throwable).asException(); } } + + private static void cancelSubscriptionOnCallEnd(Disposable subscription, ServerCallStreamObserver responseObserver) { + responseObserver.setOnCancelHandler(subscription::dispose); + responseObserver.setOnCloseHandler(subscription::dispose); + } } diff --git a/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisherTest.java b/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisherTest.java index dd44527c..9455e2bd 100644 --- a/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisherTest.java +++ b/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorClientStreamObserverAndPublisherTest.java @@ -6,15 +6,18 @@ package com.salesforce.reactorgrpc.stub; -import java.util.concurrent.ForkJoinPool; - -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; + public class ReactorClientStreamObserverAndPublisherTest { @@ -24,7 +27,7 @@ public class ReactorClientStreamObserverAndPublisherTest { @Test public void multiThreadedProducerTest() { ReactorClientStreamObserverAndPublisher processor = - new ReactorClientStreamObserverAndPublisher<>(null); + new ReactorClientStreamObserverAndPublisher<>(null); int countPerThread = 100000; TestCallStreamObserverProducer observer = new TestCallStreamObserverProducer(ForkJoinPool.commonPool(), processor, countPerThread); @@ -34,21 +37,70 @@ public void multiThreadedProducerTest() { .expectNextCount(countPerThread) .verifyComplete(); - Assertions.assertThat(observer.requestsQueue.size()).isBetween((countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 1, (countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 3); + assertThat(observer.requestsQueue.size()).isBetween((countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 1, + (countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 3); } @Test public void producerFusedTest() { ReactorClientStreamObserverAndPublisher processor = - new ReactorClientStreamObserverAndPublisher<>(null); + new ReactorClientStreamObserverAndPublisher<>(null); int countPerThread = 100000; - TestCallStreamObserverProducer observer = new TestCallStreamObserverProducer(ForkJoinPool.commonPool(), processor, countPerThread); + TestCallStreamObserverProducer observer = new TestCallStreamObserverProducer(ForkJoinPool.commonPool(), + processor, countPerThread); processor.beforeStart(observer); StepVerifier.create(Flux.from(processor)) .expectFusion(Fuseable.ANY, Fuseable.ASYNC) .expectNextCount(countPerThread) .verifyComplete(); - Assertions.assertThat(observer.requestsQueue.size()).isBetween((countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 1, (countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 3); + assertThat(observer.requestsQueue.size()).isBetween((countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 1, + (countPerThread - DEFAULT_CHUNK_SIZE) / PART_OF_CHUNK + 3); + } + + @Test + public void discardQueueTest() { + ReactorClientStreamObserverAndPublisher processor = + new ReactorClientStreamObserverAndPublisher<>(null); + int countPerThread = 5; + TestCallStreamObserverProducer observer = new TestCallStreamObserverProducer(ForkJoinPool.commonPool(), + processor, countPerThread); + processor.beforeStart(observer); + + ConcurrentLinkedQueue discardedByObserverAndPublisher = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue discardedByPublishOn = new ConcurrentLinkedQueue<>(); + + AtomicBoolean firstHandled = new AtomicBoolean(); + Flux consumer = + Flux.from(processor) + .doOnDiscard(Integer.class, discardedByObserverAndPublisher::add) + .log("processor") + .limitRate(1) + .publishOn(Schedulers.parallel()) + .limitRate(1) + .doOnDiscard(Integer.class, discardedByPublishOn::add) + .handle((i, sink) -> { + if (firstHandled.compareAndSet(false, true)) { + try { + Thread.sleep(100); + } catch (Exception e) { + // noop + } + sink.next(i); + } else { + sink.complete(); + } + }) + .log("handled"); + + StepVerifier.create(consumer) + .expectNext(0) + .verifyComplete(); + + // 1 is dropped in handle without invoking the discard hook, + assertThat(discardedByObserverAndPublisher).containsExactly(3, 4); + // impl details: processor is able to schedule 2 before it's cancelled + // also, discard hooks are cumulative, so not using containsExactly + assertThat(discardedByPublishOn).contains(2); } } \ No newline at end of file diff --git a/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisherTest.java b/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisherTest.java new file mode 100644 index 00000000..d5799eec --- /dev/null +++ b/reactor/reactor-grpc-stub/src/test/java/com/salesforce/reactorgrpc/stub/ReactorServerStreamObserverAndPublisherTest.java @@ -0,0 +1,39 @@ +package com.salesforce.reactorgrpc.stub; + +import static org.mockito.Answers.RETURNS_DEFAULTS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Duration; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.stub.ServerCallStreamObserver; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +class ReactorServerStreamObserverAndPublisherTest { + + @SuppressWarnings("unchecked") + @Test + void noErrorsOnCancelBeforeHalfClose() { + // ServerCalls.manyToMany(mock(StreamObserver.class), f -> f, CallOptions.DEFAULT).; + io.grpc.stub.ServerCalls.BidiStreamingMethod method = mock(io.grpc.stub.ServerCalls.BidiStreamingMethod.class); + ServerCallStreamObserver upstream = mock(ServerCallStreamObserver.class, RETURNS_DEFAULTS); + ReactorServerStreamObserverAndPublisher observer = new ReactorServerStreamObserverAndPublisher<>(upstream, null, 42, 42); + when(method.invoke(any())).thenReturn(observer); + + ServerCallHandler serverCallHandler = io.grpc.stub.ServerCalls.asyncBidiStreamingCall(method); + ServerCall.Listener callListener = serverCallHandler.startCall(mock(ServerCall.class), new Metadata()); + + StepVerifier.create(observer) + .expectSubscription() + .then(callListener::onCancel) + .expectNoEvent(Duration.ofMillis(1)) + .thenCancel() + .verifyThenAssertThat() + .hasNotDroppedErrors(); + } +} \ No newline at end of file diff --git a/reactor/reactor-grpc-tck/pom.xml b/reactor/reactor-grpc-tck/pom.xml index 0e63bb26..d23d4ac8 100644 --- a/reactor/reactor-grpc-tck/pom.xml +++ b/reactor/reactor-grpc-tck/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -26,6 +26,12 @@ ${project.version} test + + io.projectreactor + reactor-core + ${reactor.version} + test + io.grpc grpc-netty @@ -36,6 +42,11 @@ grpc-core test + + io.grpc + grpc-api + test + io.grpc grpc-stub @@ -55,7 +66,7 @@ io.netty netty-buffer - 4.1.33.Final + 4.1.70.Final compile @@ -79,16 +90,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${compiler.plugin.version} - - 1.8 - 1.8 - - - org.xolstice.maven.plugins protobuf-maven-plugin diff --git a/reactor/reactor-grpc-test-32/pom.xml b/reactor/reactor-grpc-test-32/pom.xml index 7f0c454c..0d109112 100644 --- a/reactor/reactor-grpc-test-32/pom.xml +++ b/reactor/reactor-grpc-test-32/pom.xml @@ -5,7 +5,7 @@ reactive-grpc com.salesforce.servicelibs - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -18,18 +18,24 @@ reactor-grpc-stub ${project.version} test - - - io.projectreactor - reactor-core - - + + + + + + io.projectreactor reactor-core - 3.2.5.RELEASE + ${reactor.version} + test + + + + + io.projectreactor reactor-test @@ -46,6 +52,11 @@ grpc-core test + + io.grpc + grpc-api + test + io.grpc grpc-stub @@ -102,16 +113,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${compiler.plugin.version} - - 1.8 - 1.8 - - - org.xolstice.maven.plugins protobuf-maven-plugin diff --git a/reactor/reactor-grpc-test/pom.xml b/reactor/reactor-grpc-test/pom.xml index 1355f881..1417e38c 100644 --- a/reactor/reactor-grpc-test/pom.xml +++ b/reactor/reactor-grpc-test/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -26,6 +26,12 @@ ${project.version} test + + io.projectreactor + reactor-core + ${reactor.version} + test + io.projectreactor reactor-test @@ -47,6 +53,11 @@ grpc-core test + + io.grpc + grpc-api + test + io.grpc grpc-stub @@ -123,16 +134,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${compiler.plugin.version} - - 1.8 - 1.8 - - - org.xolstice.maven.plugins protobuf-maven-plugin diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/BackpressureIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/BackpressureIntegrationTest.java index bc27f4f2..991b64ad 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/BackpressureIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/BackpressureIntegrationTest.java @@ -245,7 +245,7 @@ public Flux twoWayRequestPressure(Flux r public Flux twoWayResponsePressure(Flux request) { return Flux.merge( request.then(Mono.empty()), - responsePressure(null) + responsePressure((Empty) null) ); } } @@ -278,7 +278,7 @@ public Flux twoWayRequestPressure(Flux r @Override public Flux twoWayResponsePressure(Flux request) { request.subscribe(); - return responsePressure(null); + return responsePressure((Empty) null); } } } diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ConcurrentRequestIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ConcurrentRequestIntegrationTest.java index 0d0b8bb3..ed2e0b1b 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ConcurrentRequestIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ConcurrentRequestIntegrationTest.java @@ -81,7 +81,7 @@ public void fourKindsOfRequestAtOnce() throws Exception { // == MAKE REQUESTS == // One to One Mono req1 = Mono.just(HelloRequest.newBuilder().setName("reactorjava").build()); - Mono resp1 = req1.compose(stub::sayHello); + Mono resp1 = req1.transform(stub::sayHello); // One to Many Mono req2 = Mono.just(HelloRequest.newBuilder().setName("reactorjava").build()); diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ContextPropagationIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ContextPropagationIntegrationTest.java index 1370e0e5..6875a351 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ContextPropagationIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ContextPropagationIntegrationTest.java @@ -137,7 +137,7 @@ public void ClientSendsContext() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); Context.current() .withValue(ctxKey, "ClientSendsContext") - .run(() -> StepVerifier.create(worldReq.compose(stub::sayHello).map(HelloResponse::getMessage)) + .run(() -> StepVerifier.create(worldReq.transform(stub::sayHello).map(HelloResponse::getMessage)) .expectNext("Hello World") .verifyComplete()); @@ -148,7 +148,7 @@ public void ClientSendsContext() { public void ClientGetsContext() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); - Mono test = worldReq.compose(stub::sayHello) + Mono test = worldReq.transform(stub::sayHello) .doOnSuccess(resp -> { Context ctx = Context.current(); assertThat(ctxKey.get(ctx)).isEqualTo("ClientGetsContext"); @@ -163,7 +163,7 @@ public void ClientGetsContext() { public void ServerAcceptsContext() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); - StepVerifier.create(worldReq.compose(stub::sayHello).map(HelloResponse::getMessage)) + StepVerifier.create(worldReq.transform(stub::sayHello).map(HelloResponse::getMessage)) .expectNext("Hello World") .verifyComplete(); assertThat(svc.getReceivedCtxValue()).isEqualTo("ServerAcceptsContext"); diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/DoNotCallUntilSubscribeIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/DoNotCallUntilSubscribeIntegrationTest.java new file mode 100644 index 00000000..8144870c --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/DoNotCallUntilSubscribeIntegrationTest.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactorgrpc; + +import io.grpc.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Duplicates") +@RunWith(Parameterized.class) +public class DoNotCallUntilSubscribeIntegrationTest { + private Server server; + private ManagedChannel channel; + private WasCalledInterceptor interceptor; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { new TestService(), false }, + { new FusedTestService(), true } + }); + } + + private final ReactorGreeterGrpc.GreeterImplBase service; + private final boolean expectFusion; + + public DoNotCallUntilSubscribeIntegrationTest(ReactorGreeterGrpc.GreeterImplBase service, boolean expectFusion) { + this.service = service; + this.expectFusion = expectFusion; + } + + private static class WasCalledInterceptor implements ServerInterceptor { + private boolean wasCalled = false; + private boolean didRespond = false; + + public boolean wasCalled() { + return wasCalled; + } + + public boolean didRespond() { + return didRespond; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener( + next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void sendMessage(RespT message) { + didRespond = true; + super.sendMessage(message); + } + }, headers)) { + @Override + public void onMessage(ReqT message) { + wasCalled = true; + super.onMessage(message); + } + }; + } + } + + @Before + public void setupServer() throws Exception { + interceptor = new WasCalledInterceptor(); + server = ServerBuilder.forPort(9000).addService(service).intercept(interceptor).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @After + public void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws Exception { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Mono req = Mono.just(HelloRequest.newBuilder().setName("reactorjava").build()); + Mono resp = req.transform(stub::sayHello); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void oneToMany() throws Exception { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Mono req = Mono.just(HelloRequest.newBuilder().setName("reactorjava").build()); + Flux resp = req.as(stub::sayHelloRespStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToOne() throws Exception { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Flux req = Flux.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + if (!expectFusion) { + req = req.hide(); + } + + Mono resp = req.as(stub::sayHelloReqStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToMany() throws Exception { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Flux req = Flux.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + if (!expectFusion) { + req = req.hide(); + } + + Flux resp = req.transform(stub::sayHelloBothStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + static class TestService extends ReactorGreeterGrpc.GreeterImplBase { + + @Override + public Mono sayHello(HelloRequest protoRequest) { + return Mono.fromCallable(() -> greet("Hello", protoRequest)); + } + + @Override + public Flux sayHelloRespStream(HelloRequest protoRequest) { + return Flux.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest)); + } + + @Override + public Mono sayHelloReqStream(Flux reactorRequest) { + return reactorRequest + .hide() + .map(HelloRequest::getName) + .collectList() + .map(names -> greet("Hello", String.join(" and ", names))) + .hide(); + } + + @Override + public Flux sayHelloBothStream(Flux reactorRequest) { + return reactorRequest + .hide() + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))) + .hide(); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + } + + static class FusedTestService extends ReactorGreeterGrpc.GreeterImplBase { + + @Override + public Mono sayHello(Mono reactorRequest) { + return reactorRequest.map(protoRequest -> greet("Hello", protoRequest)); + } + + @Override + public Flux sayHelloRespStream(Mono reactorRequest) { + return reactorRequest.flatMapMany(protoRequest -> Flux.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest))); + } + + @Override + public Mono sayHelloReqStream(Flux reactorRequest) { + return reactorRequest + .map(HelloRequest::getName) + .collectList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flux sayHelloBothStream(Flux reactorRequest) { + return reactorRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + } +} diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/EndToEndIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/EndToEndIntegrationTest.java index 468e03f5..7cebadc5 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/EndToEndIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/EndToEndIntegrationTest.java @@ -68,7 +68,7 @@ public void stopServer() throws InterruptedException { public void oneToOne() throws IOException { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); Mono req = Mono.just(HelloRequest.newBuilder().setName("reactorjava").build()); - Mono resp = req.compose(stub::sayHello); + Mono resp = req.transform(stub::sayHello); StepVerifier.create(resp.map(HelloResponse::getMessage)) .expectNext("Hello reactorjava") @@ -143,19 +143,16 @@ public void manyToMany() throws Exception { static class TestService extends ReactorGreeterGrpc.GreeterImplBase { @Override - public Mono sayHello(Mono reactorRequest) { - return reactorRequest.hide() - .map(protoRequest -> greet("Hello", protoRequest)); + public Mono sayHello(HelloRequest protoRequest) { + return Mono.fromCallable(() -> greet("Hello", protoRequest)); } @Override - public Flux sayHelloRespStream(Mono reactorRequest) { - return reactorRequest - .hide() - .flatMapMany(protoRequest -> Flux.just( + public Flux sayHelloRespStream(HelloRequest protoRequest) { + return Flux.just( greet("Hello", protoRequest), greet("Hi", protoRequest), - greet("Greetings", protoRequest))); + greet("Greetings", protoRequest)); } @Override diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/JvmFatalServerErrorIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/JvmFatalServerErrorIntegrationTest.java new file mode 100644 index 00000000..d32b5d5f --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/JvmFatalServerErrorIntegrationTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.reactorgrpc; + +import io.grpc.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.concurrent.Executors; + +public class JvmFatalServerErrorIntegrationTest { + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + ReactorGreeterGrpc.GreeterImplBase svc = new ReactorGreeterGrpc.GreeterImplBase() { + @Override + public Mono sayHello(Mono reactorRequest) { + return reactorRequest.map(this::map); + } + + @Override + public Flux sayHelloRespStream(Mono reactorRequest) { + return reactorRequest.map(this::map).flux(); + } + + @Override + public Mono sayHelloReqStream(Flux reactorRequest) { + return reactorRequest.map(this::map).single(); + } + + @Override + public Flux sayHelloBothStream(Flux reactorRequest) { + return reactorRequest.map(this::map); + } + + private HelloResponse map(HelloRequest request) { + throw new NoSuchMethodError("Fatal!"); + } + + @Override + protected Throwable onErrorMap(Throwable throwable) { + if (throwable instanceof NoSuchMethodError) { + return Status.INTERNAL.withDescription("NoSuchMethod:" + throwable.getMessage()).asRuntimeException(); + } + return super.onErrorMap(throwable); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @Before + public void init() { + StepVerifier.setDefaultTimeout(Duration.ofSeconds(3)); + } + + @AfterClass + public static void stopServer() { + server.shutdown(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Mono resp = Mono.just(HelloRequest.getDefaultInstance()).transform(stub::sayHello); + + StepVerifier.create(resp) + .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void oneToMany() { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Flux resp = Mono.just(HelloRequest.getDefaultInstance()).as(stub::sayHelloRespStream); + Flux test = resp + .doOnNext(System.out::println) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")); + + StepVerifier.create(resp) + .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToOne() { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel) + .withExecutor(Executors.newSingleThreadExecutor()); + Flux req = Flux.just(HelloRequest.getDefaultInstance()); + Mono resp = req.as(stub::sayHelloReqStream); + + StepVerifier.create(resp) + .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToMany() { + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); + Flux req = Flux.just(HelloRequest.getDefaultInstance()); + Flux resp = req.transform(stub::sayHelloBothStream); + + StepVerifier.create(resp) + .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } +} diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ReactiveClientStandardServerInteropTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ReactiveClientStandardServerInteropTest.java index 8481c717..b4de9ce4 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ReactiveClientStandardServerInteropTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ReactiveClientStandardServerInteropTest.java @@ -122,7 +122,7 @@ public static void stopServer() throws InterruptedException { public void oneToOne() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); Mono reactorRequest = Mono.just("World"); - Mono reactorResponse = reactorRequest.map(this::toRequest).compose(stub::sayHello).map(this::fromResponse); + Mono reactorResponse = reactorRequest.map(this::toRequest).transform(stub::sayHello).map(this::fromResponse); StepVerifier.create(reactorResponse) .expectNext("Hello World") @@ -155,7 +155,7 @@ public void manyToOne() { public void manyToMany() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); Flux reactorRequest = Flux.just("A", "B", "C", "D"); - Flux reactorResponse = reactorRequest.map(this::toRequest).compose(stub::sayHelloBothStream).map(this::fromResponse); + Flux reactorResponse = reactorRequest.map(this::toRequest).transform(stub::sayHelloBothStream).map(this::fromResponse); StepVerifier.create(reactorResponse) .expectNext("Hello A and B", "Hello C and D") diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerCancellationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerCancellationTest.java new file mode 100644 index 00000000..c24242fa --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerCancellationTest.java @@ -0,0 +1,114 @@ +package com.salesforce.reactorgrpc; + +import java.io.IOException; +import java.time.Duration; + +import io.grpc.ForwardingServerCallListener; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.testing.GrpcCleanupRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +public class ServerCancellationTest { + @Rule + public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private final TestService testService = new TestService(); + private static ReactorGreeterGrpc.ReactorGreeterStub greetings; + + @Before + public void setupServer() throws IOException { + String serverName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(serverName) + .addService(testService) + .intercept(new CallClosingInterceptor()) + .build() + .start() + ); + ManagedChannel channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).build()); + greetings = ReactorGreeterGrpc.newReactorStub(channel); + } + + @Test + public void cancelsSubscriptionOnInterceptorClose() { + assertCancelsReactorSubscription(greetings.sayHello(HelloRequest.getDefaultInstance())); + } + + @Test + public void cancelsSubscriptionOnInterceptorCloseClientStream() { + assertCancelsReactorSubscription(greetings.sayHelloReqStream(Flux.just(HelloRequest.getDefaultInstance()))); + } + + @Test + public void cancelsSubscriptionOnInterceptorCloseServerStream() { + assertCancelsReactorSubscription(greetings.sayHelloRespStream(HelloRequest.getDefaultInstance())); + } + + @Test + public void cancelsSubscriptionOnInterceptorCloseBidiStream() { + assertCancelsReactorSubscription(greetings.sayHelloBothStream(Flux.just(HelloRequest.getDefaultInstance()))); + } + + private void assertCancelsReactorSubscription(Publisher request) { + StepVerifier.create(request) + .expectErrorMatches(error -> + error instanceof StatusRuntimeException && + ((StatusRuntimeException) error).getStatus().getCode() == Status.Code.ABORTED + ) + .verify(Duration.ofSeconds(2)); + testService.testPublisher.assertNoSubscribers(); + } + + private static class CallClosingInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next + ) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onMessage(ReqT message) { + super.onMessage(message); + call.close(Status.ABORTED, new Metadata()); + } + }; + } + } + + private static class TestService extends ReactorGreeterGrpc.GreeterImplBase { + protected final TestPublisher testPublisher = TestPublisher.create(); + + @Override public Mono sayHello(HelloRequest request) { + return testPublisher.mono(); + } + + @Override public Flux sayHelloRespStream(HelloRequest request) { + return testPublisher.flux(); + } + + @Override public Mono sayHelloReqStream(Flux request) { + request.subscribe(); + return testPublisher.mono(); + } + + @Override public Flux sayHelloBothStream(Flux request) { + request.subscribe(); + return testPublisher.flux(); + } + } +} diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerErrorIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerErrorIntegrationTest.java index 2f593179..122d4cd5 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerErrorIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/ServerErrorIntegrationTest.java @@ -67,7 +67,7 @@ public void stopServer() { @Test public void oneToOne() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); - Mono resp = Mono.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + Mono resp = Mono.just(HelloRequest.getDefaultInstance()).transform(stub::sayHello); StepVerifier.create(resp) .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException)t).getStatus() == Status.INTERNAL); diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnaryZeroMessageResponseIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnaryZeroMessageResponseIntegrationTest.java index 466ef1dc..29975352 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnaryZeroMessageResponseIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnaryZeroMessageResponseIntegrationTest.java @@ -47,13 +47,25 @@ public void onCompleted() { } } + private static class ReactorMissingUnaryResponseService extends ReactorGreeterGrpc.GreeterImplBase { + @Override + public Mono sayHello(Mono request) { + return Mono.empty(); + } + + @Override + public Mono sayHelloReqStream(Flux request) { + return Mono.empty(); + } + } + @Test public void zeroMessageResponseOneToOne() { serverRule.getServiceRegistry().addService(new MissingUnaryResponseService()); ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(serverRule.getChannel()); Mono req = Mono.just(HelloRequest.newBuilder().setName("reactor").build()); - Mono resp = req.compose(stub::sayHello); + Mono resp = req.transform(stub::sayHello); StepVerifier.create(resp).verifyErrorMatches(t -> t instanceof StatusRuntimeException && @@ -76,4 +88,34 @@ public void zeroMessageResponseManyToOne() { t instanceof StatusRuntimeException && ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.CANCELLED); } + + @Test + public void reactorZeroMessageResponseOneToOne() { + serverRule.getServiceRegistry().addService(new ReactorMissingUnaryResponseService()); + + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(serverRule.getChannel()); + Mono req = Mono.just(HelloRequest.newBuilder().setName("reactor").build()); + Mono resp = req.transform(stub::sayHello); + + StepVerifier.create(resp).verifyErrorMatches(t -> + t instanceof StatusRuntimeException && + ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.CANCELLED); + } + + @Test + public void reactorZeroMessageResponseManyToOne() { + serverRule.getServiceRegistry().addService(new ReactorMissingUnaryResponseService()); + + ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(serverRule.getChannel()); + Flux req = Flux.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Mono resp = req.as(stub::sayHelloReqStream); + + StepVerifier.create(resp).verifyErrorMatches(t -> + t instanceof StatusRuntimeException && + ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.CANCELLED); + } } diff --git a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnexpectedServerErrorIntegrationTest.java b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnexpectedServerErrorIntegrationTest.java index ac220a2e..2abecbf4 100644 --- a/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnexpectedServerErrorIntegrationTest.java +++ b/reactor/reactor-grpc-test/src/test/java/com/salesforce/reactorgrpc/UnexpectedServerErrorIntegrationTest.java @@ -77,7 +77,7 @@ public static void stopServer() { @Test public void oneToOne() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); - Mono resp = Mono.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + Mono resp = Mono.just(HelloRequest.getDefaultInstance()).transform(stub::sayHello); StepVerifier.create(resp) .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); @@ -111,7 +111,7 @@ public void manyToOne() { public void manyToMany() { ReactorGreeterGrpc.ReactorGreeterStub stub = ReactorGreeterGrpc.newReactorStub(channel); Flux req = Flux.just(HelloRequest.getDefaultInstance()); - Flux resp = req.compose(stub::sayHelloBothStream); + Flux resp = req.transform(stub::sayHelloBothStream); StepVerifier.create(resp) .verifyErrorMatches(t -> t instanceof StatusRuntimeException && ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); diff --git a/reactor/reactor-grpc-test/src/test/proto/com/example/v1/frontend.proto b/reactor/reactor-grpc-test/src/test/proto/com/example/v1/frontend.proto new file mode 100644 index 00000000..efc6cf1c --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/proto/com/example/v1/frontend.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package com.example.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service Frontend { + rpc Heartbeat(HeartbeatRequest) returns (google.protobuf.Empty); +} +message HeartbeatRequest { + google.protobuf.Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/reactor/reactor-grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto b/reactor/reactor-grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto new file mode 100644 index 00000000..e5601d3f --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package com.example.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service Settings { + rpc SettingsGet_Classic_1 (google.protobuf.Empty) returns (Settings_Classic_1); + rpc SettingsGetClassic2 (google.protobuf.Empty) returns (Settings_Classic_1); +} + +message Settings_Classic_1 { + google.protobuf.Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/reactor/reactor-grpc-test/src/test/proto/helloworld_optional.proto b/reactor/reactor-grpc-test/src/test/proto/helloworld_optional.proto new file mode 100644 index 00000000..5bfc3e2a --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/proto/helloworld_optional.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package optional_helloworld; + +option java_package = "com.salesforce.rxgrpc"; +option java_outer_classname = "HelloWorldOptionalProto"; + +// The greeting service definition. +service OptionalGreeter { + // Sends a greeting + rpc SayHello (OptionalHelloRequest) returns (OptionalHelloResponse) {} +} + +// The request message containing the user's name. +message OptionalHelloRequest { + optional string name = 1; +} + +// The response message containing the greetings +message OptionalHelloResponse { + optional string message = 1; +} \ No newline at end of file diff --git a/reactor/reactor-grpc-test/src/test/proto/some_parameter.proto b/reactor/reactor-grpc-test/src/test/proto/some_parameter.proto new file mode 100644 index 00000000..2a639487 --- /dev/null +++ b/reactor/reactor-grpc-test/src/test/proto/some_parameter.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +// Test file for https://github.com/salesforce/reactive-grpc/issues/26 +// If this proto compiles, then the test passes. + +package my.someparameters; + +message SomeParameter { + string id = 1; +} + +service WithParameter { + rpc SayGoodbye (SomeParameter) returns (SomeParameter) {} +} \ No newline at end of file diff --git a/reactor/reactor-grpc/BUILD.bazel b/reactor/reactor-grpc/BUILD.bazel new file mode 100644 index 00000000..67866520 --- /dev/null +++ b/reactor/reactor-grpc/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "reactor_grpc", + srcs = glob(["src/main/**/*.java"]), + resources = ["src/main/resources/ReactorStub.mustache"], + deps = [ + "//common/reactive-grpc-gencommon", + "@com_salesforce_servicelibs_jprotoc", + ], +) + +java_binary( + name = "reactor_grpc_bin", + main_class = "com.salesforce.reactorgrpc.ReactorGrpcGenerator", + visibility = ["//visibility:public"], + runtime_deps = [":reactor_grpc"], +) diff --git a/reactor/reactor-grpc/pom.xml b/reactor/reactor-grpc/pom.xml index d0896e57..ce0ccb5b 100644 --- a/reactor/reactor-grpc/pom.xml +++ b/reactor/reactor-grpc/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -39,23 +39,85 @@ - org.springframework.boot - spring-boot-maven-plugin - 1.5.8.RELEASE + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + package - repackage + shade - com.salesforce.reactorgrpc.ReactorGrpcGenerator - JAR - jdk8 - true + + + android.annotation + com.salesforce.servicelibs.android.annotation + + + com.github.mustachejava + com.salesforce.servicelibs.com.github.mustachejava + + + com.google + com.salesforce.servicelibs.com.google + + + google.protobuf + com.salesforce.servicelibs.google.protobuf + + + io.grpc + com.salesforce.servicelibs.io.grpc + + + io.perfmark + com.salesforce.servicelibs.io.perfmark + + + javax.annotation + com.salesforce.servicelibs.javax.annotation + + + org.checkerframework + com.salesforce.servicelibs.org.checkerframework + + + org.codehaus + com.salesforce.servicelibs.org.codehaus + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + com.salesforce.reactorgrpc.ReactorGrpcGenerator + + + + + com.salesforce.servicelibs + canteen-maven-plugin + ${canteen.plugin.version} + + + + bootstrap + + + + - \ No newline at end of file + diff --git a/reactor/reactor-grpc/src/main/resources/ReactorStub.mustache b/reactor/reactor-grpc/src/main/resources/ReactorStub.mustache index 6fe725cd..6066e458 100644 --- a/reactor/reactor-grpc/src/main/resources/ReactorStub.mustache +++ b/reactor/reactor-grpc/src/main/resources/ReactorStub.mustache @@ -51,7 +51,7 @@ public final class {{className}} { @java.lang.Deprecated {{/deprecated}} public {{#isManyOutput}}reactor.core.publisher.Flux{{/isManyOutput}}{{^isManyOutput}}reactor.core.publisher.Mono{{/isManyOutput}}<{{outputType}}> {{methodName}}({{#isManyInput}}reactor.core.publisher.Flux{{/isManyInput}}{{^isManyInput}}reactor.core.publisher.Mono{{/isManyInput}}<{{inputType}}> reactorRequest) { - return com.salesforce.reactorgrpc.stub.ClientCalls.{{reactiveCallsMethodName}}(reactorRequest, delegateStub::{{methodName}}); + return com.salesforce.reactorgrpc.stub.ClientCalls.{{reactiveCallsMethodName}}(reactorRequest, delegateStub::{{methodNameCamelCase}}, getCallOptions()); } {{/methods}} @@ -63,7 +63,7 @@ public final class {{className}} { @java.lang.Deprecated {{/deprecated}} public {{#isManyOutput}}reactor.core.publisher.Flux{{/isManyOutput}}{{^isManyOutput}}reactor.core.publisher.Mono{{/isManyOutput}}<{{outputType}}> {{methodName}}({{inputType}} reactorRequest) { - return com.salesforce.reactorgrpc.stub.ClientCalls.{{reactiveCallsMethodName}}(reactor.core.publisher.Mono.just(reactorRequest), delegateStub::{{methodName}}); + return com.salesforce.reactorgrpc.stub.ClientCalls.{{reactiveCallsMethodName}}(reactor.core.publisher.Mono.just(reactorRequest), delegateStub::{{methodNameCamelCase}}, getCallOptions()); } {{/unaryRequestMethods}} @@ -75,13 +75,25 @@ public final class {{className}} { public static abstract class {{serviceName}}ImplBase implements io.grpc.BindableService { {{#methods}} + {{^isManyInput}} {{#javaDoc}} {{{javaDoc}}} {{/javaDoc}} {{#deprecated}} @java.lang.Deprecated {{/deprecated}} - public {{#isManyOutput}}reactor.core.publisher.Flux{{/isManyOutput}}{{^isManyOutput}}reactor.core.publisher.Mono{{/isManyOutput}}<{{outputType}}> {{methodName}}({{#isManyInput}}reactor.core.publisher.Flux{{/isManyInput}}{{^isManyInput}}reactor.core.publisher.Mono{{/isManyInput}}<{{inputType}}> request) { + public {{#isManyOutput}}reactor.core.publisher.Flux{{/isManyOutput}}{{^isManyOutput}}reactor.core.publisher.Mono{{/isManyOutput}}<{{outputType}}> {{methodNameCamelCase}}({{inputType}} request) { + return {{methodNameCamelCase}}(reactor.core.publisher.Mono.just(request)); + } + {{/isManyInput}} + + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}reactor.core.publisher.Flux{{/isManyOutput}}{{^isManyOutput}}reactor.core.publisher.Mono{{/isManyOutput}}<{{outputType}}> {{methodNameCamelCase}}({{#isManyInput}}reactor.core.publisher.Flux{{/isManyInput}}{{^isManyInput}}reactor.core.publisher.Mono{{/isManyInput}}<{{inputType}}> request) { throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNIMPLEMENTED); } @@ -99,10 +111,18 @@ public final class {{className}} { {{/methods}} .build(); } + + protected io.grpc.CallOptions getCallOptions(int methodId) { + return null; + } + + protected Throwable onErrorMap(Throwable throwable) { + return com.salesforce.reactorgrpc.stub.ServerCalls.prepareError(throwable); + } } {{#methods}} - private static final int METHODID_{{methodNameUpperUnderscore}} = {{methodNumber}}; + public static final int METHODID_{{methodNameUpperUnderscore}} = {{methodNumber}}; {{/methods}} private static final class MethodHandlers implements @@ -127,7 +147,7 @@ public final class {{className}} { case METHODID_{{methodNameUpperUnderscore}}: com.salesforce.reactorgrpc.stub.ServerCalls.{{reactiveCallsMethodName}}(({{inputType}}) request, (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, - serviceImpl::{{methodName}}); + serviceImpl::{{methodNameCamelCase}}, serviceImpl::onErrorMap); break; {{/isManyInput}} {{/methods}} @@ -145,7 +165,7 @@ public final class {{className}} { case METHODID_{{methodNameUpperUnderscore}}: return (io.grpc.stub.StreamObserver) com.salesforce.reactorgrpc.stub.ServerCalls.{{reactiveCallsMethodName}}( (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, - serviceImpl::{{methodName}}); + serviceImpl::{{methodNameCamelCase}}, serviceImpl::onErrorMap, serviceImpl.getCallOptions(methodId)); {{/isManyInput}} {{/methods}} default: diff --git a/rx-java/README.md b/rx-java/README.md index 0156f726..0cf3c294 100644 --- a/rx-java/README.md +++ b/rx-java/README.md @@ -36,7 +36,7 @@ protobuf { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } rxgrpc { - artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}:jdk8@jar" + artifact = "com.salesforce.servicelibs:rxgrpc:${reactiveGrpcVersion}" } } generateProtoTasks { @@ -47,8 +47,38 @@ protobuf { } } ``` -*At this time, RxGrpc with Gradle only supports bash-based environments. Windows users will need to build using Windows Subsystem for Linux (win 10), Gitbash, or Cygwin.* +*At this time, RxGrpc with Gradle only supports bash-based environments. Windows users will need to build using Windows +Subsystem for Linux (win 10) or invoke the Maven protobuf plugin with Gradle.* +### Bazel +To use RxGrpc with Bazel, update your `WORKSPACE` file. + +```bazel +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "com_salesforce_servicelibs_reactive_grpc", + strip_prefix = "reactive-grpc-1.0.1", + url = "https://github.com/salesforce/reactive-grpc/archive/v1.0.1.zip", +) + +load("@com_salesforce_servicelibs_reactive_grpc//bazel:repositories.bzl", "repositories") + +repositories() +``` + +Then, add a `rx_grpc_library()` rule to your proto's `BUILD` file, referencing both the `proto_library()` target and +the `java_proto_library()` target. + +```bazel +load("@com_salesforce_servicelibs_reactive_grpc//bazel:java_reactive_grpc_library.bzl", "rx_grpc_library") + +rx_grpc_library( + name = "foo_rx_proto", + proto = ":foo_proto", + deps = [":foo_java_proto"], +) +``` Usage ===== After installing the plugin, RxGrpc service stubs will be generated along with your gRPC service stubs. @@ -138,6 +168,31 @@ public class RxContextPropagator { } } ``` + +## Configuration of flow control +RX GRPC by default prefetch 512 items on client and server side. When the messages are bigger it +can consume a lot of memory. One can override these default settings using RxCallOptions: + +Prefetch on client side (client consumes too slow): + +```java + RxMyAPIStub api = RxMyAPIGrpc.newRxStub(channel) + .withOption(RxCallOptions.CALL_OPTIONS_PREFETCH, 16) + .withOption(RxCallOptions.CALL_OPTIONS_LOW_TIDE, 4); +``` + +Prefetch on server side (server consumes too slow): + +```java + // Override getCallOptions method in your *ImplBase service class. + // One can use methodId to do method specific override + @Override + protected CallOptions getCallOptions(int methodId) { + return CallOptions.DEFAULT + .withOption(RxCallOptions.CALL_OPTIONS_PREFETCH, 16) + .withOption(RxCallOptions.CALL_OPTIONS_LOW_TIDE, 4); + } +``` Modules ======= diff --git a/rx-java/rxgrpc-stub/BUILD.bazel b/rx-java/rxgrpc-stub/BUILD.bazel new file mode 100644 index 00000000..ec22a8d3 --- /dev/null +++ b/rx-java/rxgrpc-stub/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "rxgrpc-stub", + srcs = glob(["src/main/**/*.java"]), + # Disable error prone build failure (triggered by unused return) + javacopts = ["-XepDisableAllChecks"], + visibility = ["//visibility:public"], + deps = [ + "//common/reactive-grpc-common", + "@com_google_guava_guava", + "@io_grpc_grpc_java//context", + "@io_grpc_grpc_java//core", + "@io_grpc_grpc_java//stub", + "@io_reactivex_rxjava2_rxjava", + "@org_reactivestreams_reactive_streams", + ], +) diff --git a/rx-java/rxgrpc-stub/pom.xml b/rx-java/rxgrpc-stub/pom.xml index 621f8505..43340fad 100644 --- a/rx-java/rxgrpc-stub/pom.xml +++ b/rx-java/rxgrpc-stub/pom.xml @@ -12,30 +12,31 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 rxgrpc-stub - - 1.7 - ${java.version} - ${java.version} - - ${project.groupId} reactive-grpc-common ${project.version} + + io.grpc + grpc-stub + ${grpc.version} + io.reactivex.rxjava2 rxjava ${rxjava.version} + provided + com.github.davidmoten rxjava2-extras @@ -86,28 +87,7 @@ - - - org.codehaus.mojo - animal-sniffer-maven-plugin - 1.7 - - - signature-check - verify - - check - - - - - - org.codehaus.mojo.signature - java16 - 1.0 - - - + org.apache.maven.plugins maven-jar-plugin @@ -121,7 +101,7 @@ org.apache.felix maven-bundle-plugin - 4.1.0 + 5.1.2 bundle-manifest diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ClientCalls.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ClientCalls.java index fd80dc3b..76f916a0 100644 --- a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ClientCalls.java +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ClientCalls.java @@ -9,6 +9,7 @@ import com.salesforce.reactivegrpc.common.BiConsumer; import com.salesforce.reactivegrpc.common.Function; +import io.grpc.CallOptions; import io.grpc.stub.CallStreamObserver; import io.grpc.stub.StreamObserver; import io.reactivex.Flowable; @@ -33,7 +34,8 @@ private ClientCalls() { */ public static Single oneToOne( final Single rxRequest, - final BiConsumer> delegate) { + final BiConsumer> delegate, + final CallOptions options) { try { return Single .create(new SingleOnSubscribe() { @@ -82,14 +84,19 @@ public void accept(Throwable t) { */ public static Flowable oneToMany( final Single rxRequest, - final BiConsumer> delegate) { + final BiConsumer> delegate, + final CallOptions options) { try { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + return rxRequest .flatMapPublisher(new io.reactivex.functions.Function>() { @Override public Publisher apply(TRequest request) { final RxClientStreamObserverAndPublisher consumerStreamObserver = - new RxClientStreamObserverAndPublisher(null); + new RxClientStreamObserverAndPublisher(null, null, prefetch, lowTide); delegate.accept(request, consumerStreamObserver); @@ -108,7 +115,8 @@ public Publisher apply(TRequest request) { @SuppressWarnings("unchecked") public static Single manyToOne( final Flowable flowableSource, - final Function, StreamObserver> delegate) { + final Function, StreamObserver> delegate, + final CallOptions options) { try { final RxSubscriberAndClientProducer subscriberAndGRPCProducer = flowableSource.subscribeWith(new RxSubscriberAndClientProducer()); @@ -127,10 +135,10 @@ public void run() { } } ); - delegate.apply(observerAndPublisher); return Flowable.fromPublisher(observerAndPublisher) - .singleOrError(); + .doOnSubscribe(s -> delegate.apply(observerAndPublisher)) + .singleOrError(); } catch (Throwable throwable) { return Single.error(throwable); } @@ -143,7 +151,12 @@ public void run() { @SuppressWarnings("unchecked") public static Flowable manyToMany( final Flowable flowableSource, - final Function, StreamObserver> delegate) { + final Function, StreamObserver> delegate, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + try { final RxSubscriberAndClientProducer subscriberAndGRPCProducer = flowableSource.subscribeWith(new RxSubscriberAndClientProducer()); @@ -160,13 +173,13 @@ public void accept(CallStreamObserver observer) { public void run() { subscriberAndGRPCProducer.cancel(); } - } - ); - delegate.apply(observerAndPublisher); + }, + prefetch, lowTide); - return Flowable.fromPublisher(observerAndPublisher); + return Flowable.fromPublisher(observerAndPublisher).doOnSubscribe(s -> delegate.apply(observerAndPublisher)); } catch (Throwable throwable) { return Flowable.error(throwable); } } + } diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxCallOptions.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxCallOptions.java new file mode 100644 index 00000000..7df97546 --- /dev/null +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxCallOptions.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rxgrpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher; +import io.grpc.CallOptions; + +/** + * RX Call options. + */ +public final class RxCallOptions { + + private RxCallOptions() { + } + + /** + * Sets Prefetch size of queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_PREFETCH = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.PREFETCH", + Integer.valueOf(AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE)); + + /** + * Sets Low Tide of prefetch queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_LOW_TIDE = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.LOW_TIDE", + Integer.valueOf(AbstractStreamObserverAndPublisher.TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE)); + + + /** + * Utility function to get prefetch option. + */ + public static int getPrefetch(final CallOptions options) { + return options == null ? CALL_OPTIONS_PREFETCH.getDefault() : options.getOption(CALL_OPTIONS_PREFETCH); + } + + /** + * Utility function to get low tide option together with validation. + */ + public static int getLowTide(final CallOptions options) { + int prefetch = getPrefetch(options); + int lowTide = options == null ? CALL_OPTIONS_LOW_TIDE.getDefault() : options.getOption(CALL_OPTIONS_LOW_TIDE); + if (lowTide >= prefetch) { + throw new IllegalArgumentException(CALL_OPTIONS_LOW_TIDE + " must be less than " + CALL_OPTIONS_PREFETCH); + } + return lowTide; + } + +} diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxClientStreamObserverAndPublisher.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxClientStreamObserverAndPublisher.java index 668dc876..e5862c73 100644 --- a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxClientStreamObserverAndPublisher.java +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxClientStreamObserverAndPublisher.java @@ -22,16 +22,20 @@ class RxClientStreamObserverAndPublisher extends AbstractClientStreamObserverAndPublisher implements QueueSubscription { - RxClientStreamObserverAndPublisher(Consumer> onSubscribe) { - super(new SimpleQueueAdapter(new SpscArrayQueue(DEFAULT_CHUNK_SIZE)), onSubscribe); - } - RxClientStreamObserverAndPublisher( Consumer> onSubscribe, Runnable onTerminate) { super(new SimpleQueueAdapter(new SpscArrayQueue(DEFAULT_CHUNK_SIZE)), onSubscribe, onTerminate); } + RxClientStreamObserverAndPublisher( + Consumer> onSubscribe, + Runnable onTerminate, + int prefetch, + int lowTide) { + super(new SimpleQueueAdapter(new SpscArrayQueue(prefetch)), onSubscribe, onTerminate, prefetch, lowTide); + } + @Override public int requestFusion(int requestedMode) { if ((requestedMode & QueueFuseable.ASYNC) != 0) { diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxServerStreamObserverAndPublisher.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxServerStreamObserverAndPublisher.java index fb1ebb8a..4719a548 100644 --- a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxServerStreamObserverAndPublisher.java +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxServerStreamObserverAndPublisher.java @@ -25,8 +25,10 @@ class RxServerStreamObserverAndPublisher RxServerStreamObserverAndPublisher( ServerCallStreamObserver serverCallStreamObserver, - Consumer> onSubscribe) { - super(serverCallStreamObserver, new SimpleQueueAdapter(new SpscArrayQueue(DEFAULT_CHUNK_SIZE)), onSubscribe); + Consumer> onSubscribe, + int prefetch, + int lowTide) { + super(serverCallStreamObserver, new SimpleQueueAdapter(new SpscArrayQueue(prefetch)), onSubscribe, prefetch, lowTide); } @Override diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxSubscriberAndServerProducer.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxSubscriberAndServerProducer.java index 7f157aad..89532d60 100644 --- a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxSubscriberAndServerProducer.java +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/RxSubscriberAndServerProducer.java @@ -8,6 +8,7 @@ package com.salesforce.rxgrpc.stub; import com.salesforce.reactivegrpc.common.AbstractSubscriberAndServerProducer; +import com.salesforce.reactivegrpc.common.Function; import io.reactivex.FlowableSubscriber; import io.reactivex.internal.fuseable.QueueSubscription; import org.reactivestreams.Subscription; @@ -21,6 +22,10 @@ public class RxSubscriberAndServerProducer extends AbstractSubscriberAndServerProducer implements FlowableSubscriber { + public RxSubscriberAndServerProducer(Function prepareError) { + super(prepareError); + } + @Override protected Subscription fuse(Subscription s) { if (s instanceof QueueSubscription) { diff --git a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ServerCalls.java b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ServerCalls.java index 2cdffd2f..7a1c6118 100644 --- a/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ServerCalls.java +++ b/rx-java/rxgrpc-stub/src/main/java/com/salesforce/rxgrpc/stub/ServerCalls.java @@ -9,6 +9,7 @@ import com.google.common.base.Preconditions; import com.salesforce.reactivegrpc.common.Function; +import io.grpc.CallOptions; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; @@ -34,11 +35,10 @@ private ServerCalls() { public static void oneToOne( final TRequest request, final StreamObserver responseObserver, - final Function, Single> delegate) { + final Function> delegate, + final Function prepareError) { try { - final Single rxRequest = Single.just(request); - - final Single rxResponse = Preconditions.checkNotNull(delegate.apply(rxRequest)); + final Single rxResponse = Preconditions.checkNotNull(delegate.apply(request)); rxResponse.subscribe( new Consumer() { @Override @@ -54,11 +54,11 @@ public void accept(TResponse value) { new Consumer() { @Override public void accept(Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } }); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } } @@ -69,16 +69,15 @@ public void accept(Throwable throwable) { public static void oneToMany( final TRequest request, final StreamObserver responseObserver, - final Function, Flowable> delegate) { + final Function> delegate, + final Function prepareError) { try { - final Single rxRequest = Single.just(request); - - final Flowable rxResponse = Preconditions.checkNotNull(delegate.apply(rxRequest)); + final Flowable rxResponse = Preconditions.checkNotNull(delegate.apply(request)); final RxSubscriberAndServerProducer serverProducer = - rxResponse.subscribeWith(new RxSubscriberAndServerProducer()); + rxResponse.subscribeWith(new RxSubscriberAndServerProducer(prepareError::apply)); serverProducer.subscribe((ServerCallStreamObserver) responseObserver); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } } @@ -88,9 +87,15 @@ public static void oneToMany( */ public static StreamObserver manyToOne( final StreamObserver responseObserver, - final Function, Single> delegate) { + final Function, Single> delegate, + final Function prepareError, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + final RxServerStreamObserverAndPublisher streamObserverPublisher = - new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null); + new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); try { final Single rxResponse = Preconditions.checkNotNull(delegate.apply(Flowable.fromPublisher(streamObserverPublisher))); @@ -111,13 +116,13 @@ public void accept(Throwable throwable) { // Don't try to respond if the server has already canceled the request if (!streamObserverPublisher.isCancelled()) { streamObserverPublisher.abortPendingCancel(); - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } } } ); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } return streamObserverPublisher; @@ -129,24 +134,33 @@ public void accept(Throwable throwable) { */ public static StreamObserver manyToMany( final StreamObserver responseObserver, - final Function, Flowable> delegate) { + final Function, Flowable> delegate, + final Function prepareError, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + final RxServerStreamObserverAndPublisher streamObserverPublisher = - new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null); + new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); try { final Flowable rxResponse = Preconditions.checkNotNull(delegate.apply(Flowable.fromPublisher(streamObserverPublisher))); - final RxSubscriberAndServerProducer subscriber = new RxSubscriberAndServerProducer(); + final RxSubscriberAndServerProducer subscriber = new RxSubscriberAndServerProducer(prepareError::apply); subscriber.subscribe((ServerCallStreamObserver) responseObserver); // Don't try to respond if the server has already canceled the request rxResponse.subscribe(subscriber); } catch (Throwable throwable) { - responseObserver.onError(prepareError(throwable)); + responseObserver.onError(prepareError.apply(throwable)); } return streamObserverPublisher; } - private static Throwable prepareError(Throwable throwable) { + /** + * Implements default error mapping. + */ + public static Throwable prepareError(Throwable throwable) { if (throwable instanceof StatusException || throwable instanceof StatusRuntimeException) { return throwable; } else { diff --git a/rx-java/rxgrpc-tck/pom.xml b/rx-java/rxgrpc-tck/pom.xml index e58108ad..4e60a56e 100644 --- a/rx-java/rxgrpc-tck/pom.xml +++ b/rx-java/rxgrpc-tck/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -26,6 +26,12 @@ ${project.version} test + + io.reactivex.rxjava2 + rxjava + ${rxjava.version} + test + io.grpc grpc-netty @@ -36,6 +42,11 @@ grpc-core test + + io.grpc + grpc-api + test + io.grpc grpc-stub diff --git a/rx-java/rxgrpc-test/pom.xml b/rx-java/rxgrpc-test/pom.xml index 926a1e95..07dab9d8 100644 --- a/rx-java/rxgrpc-test/pom.xml +++ b/rx-java/rxgrpc-test/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -26,6 +26,12 @@ ${project.version} test + + io.reactivex.rxjava2 + rxjava + ${rxjava.version} + test + io.grpc grpc-netty @@ -36,6 +42,11 @@ grpc-core test + + io.grpc + grpc-api + test + io.grpc grpc-stub diff --git a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/BackpressureIntegrationTest.java b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/BackpressureIntegrationTest.java index 183c26fb..4e9169d7 100644 --- a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/BackpressureIntegrationTest.java +++ b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/BackpressureIntegrationTest.java @@ -69,7 +69,7 @@ public Flowable twoWayRequestPressure(Flowable twoWayResponsePressure(Flowable request) { request.subscribe(); - return responsePressure(null); + return responsePressure((Empty) null); } } diff --git a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/DoNotCallUntilSubscribeIntegrationTest.java b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/DoNotCallUntilSubscribeIntegrationTest.java new file mode 100644 index 00000000..dfd0f8a8 --- /dev/null +++ b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/DoNotCallUntilSubscribeIntegrationTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rxgrpc; + +import io.grpc.*; +import io.reactivex.Flowable; +import io.reactivex.Single; +import org.junit.*; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This test ensures that server calls aren't made if subscribe() isn't called. EndToEndIntegrationTest verifies + * that server calls are made when subscribe() is called. + */ +@SuppressWarnings("Duplicates") +public class DoNotCallUntilSubscribeIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private Server server; + private ManagedChannel channel; + private WasCalledInterceptor interceptor; + + private static class WasCalledInterceptor implements ServerInterceptor { + private boolean wasCalled = false; + private boolean didRespond = false; + + public boolean wasCalled() { + return wasCalled; + } + + public boolean didRespond() { + return didRespond; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener( + next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void sendMessage(RespT message) { + didRespond = true; + super.sendMessage(message); + } + }, headers)) { + @Override + public void onMessage(ReqT message) { + wasCalled = true; + super.onMessage(message); + } + }; + } + } + + @Before + public void setupServer() throws Exception { + RxGreeterGrpc.GreeterImplBase svc = new RxGreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(HelloRequest protoRequest) { + return Single.fromCallable(() -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloRespStream(HelloRequest protoRequest) { + return Flowable.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest)); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .toList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + interceptor = new WasCalledInterceptor(); + server = ServerBuilder.forPort(9000).addService(svc).intercept(interceptor).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @After + public void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws Exception { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp = req.compose(stub::sayHello); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void oneToMany() throws Exception { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Flowable resp = req.as(stub::sayHelloRespStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToOne() throws Exception { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Single resp = req.as(stub::sayHelloReqStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToMany() throws Exception { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + Flowable resp = req.compose(stub::sayHelloBothStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } +} diff --git a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/EndToEndIntegrationTest.java b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/EndToEndIntegrationTest.java index 532636d5..47e1fb25 100644 --- a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/EndToEndIntegrationTest.java +++ b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/EndToEndIntegrationTest.java @@ -35,16 +35,16 @@ public static void setupServer() throws Exception { RxGreeterGrpc.GreeterImplBase svc = new RxGreeterGrpc.GreeterImplBase() { @Override - public Single sayHello(Single rxRequest) { - return rxRequest.map(protoRequest -> greet("Hello", protoRequest)); + public Single sayHello(HelloRequest protoRequest) { + return Single.fromCallable(() -> greet("Hello", protoRequest)); } @Override - public Flowable sayHelloRespStream(Single rxRequest) { - return rxRequest.flatMapPublisher(protoRequest -> Flowable.just( + public Flowable sayHelloRespStream(HelloRequest protoRequest) { + return Flowable.just( greet("Hello", protoRequest), greet("Hi", protoRequest), - greet("Greetings", protoRequest))); + greet("Greetings", protoRequest)); } @Override diff --git a/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/JvmFatalServerErrorIntegrationTest.java b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/JvmFatalServerErrorIntegrationTest.java new file mode 100644 index 00000000..2f16b581 --- /dev/null +++ b/rx-java/rxgrpc-test/src/test/java/com/salesforce/rxgrpc/JvmFatalServerErrorIntegrationTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rxgrpc; + +import io.grpc.*; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.observers.TestObserver; +import io.reactivex.subscribers.TestSubscriber; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unchecked") +public class JvmFatalServerErrorIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + RxGreeterGrpc.GreeterImplBase svc = new RxGreeterGrpc.GreeterImplBase() { + @Override + public Single sayHello(Single rxRequest) { + return rxRequest.map(this::kaboom); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest.map(this::kaboom).toFlowable(); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom).firstOrError(); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom); + } + + private HelloResponse kaboom(HelloRequest request) { + throw new NoSuchMethodError("Fatal!"); + } + + @Override + protected Throwable onErrorMap(Throwable throwable) { + if (throwable instanceof NoSuchMethodError) { + return Status.INTERNAL.withDescription("NoSuchMethod:" + throwable.getMessage()).asRuntimeException(); + } + return super.onErrorMap(throwable); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() { + server.shutdown(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Single resp = Single.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + TestObserver test = resp.test(); + + test.awaitTerminalEvent(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void oneToMany() { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Flowable resp = Single.just(HelloRequest.getDefaultInstance()).as(stub::sayHelloRespStream); + TestSubscriber test = resp + .doOnNext(System.out::println) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .test(); + + test.awaitTerminalEvent(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToOne() { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Single resp = req.as(stub::sayHelloReqStream); + TestObserver test = resp.test(); + + test.awaitTerminalEvent(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + // Flowable requests get canceled when unexpected errors happen + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToMany() { + RxGreeterGrpc.RxGreeterStub stub = RxGreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Flowable resp = req.compose(stub::sayHelloBothStream); + TestSubscriber test = resp.test(); + + test.awaitTerminalEvent(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } +} diff --git a/rx-java/rxgrpc-test/src/test/proto/helloworld_optional.proto b/rx-java/rxgrpc-test/src/test/proto/helloworld_optional.proto new file mode 100644 index 00000000..5bfc3e2a --- /dev/null +++ b/rx-java/rxgrpc-test/src/test/proto/helloworld_optional.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package optional_helloworld; + +option java_package = "com.salesforce.rxgrpc"; +option java_outer_classname = "HelloWorldOptionalProto"; + +// The greeting service definition. +service OptionalGreeter { + // Sends a greeting + rpc SayHello (OptionalHelloRequest) returns (OptionalHelloResponse) {} +} + +// The request message containing the user's name. +message OptionalHelloRequest { + optional string name = 1; +} + +// The response message containing the greetings +message OptionalHelloResponse { + optional string message = 1; +} \ No newline at end of file diff --git a/rx-java/rxgrpc/BUILD.bazel b/rx-java/rxgrpc/BUILD.bazel new file mode 100644 index 00000000..75c51936 --- /dev/null +++ b/rx-java/rxgrpc/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "rxgrpc", + srcs = glob(["src/main/**/*.java"]), + resources = ["src/main/resources/RxStub.mustache"], + deps = [ + "//common/reactive-grpc-gencommon", + "@com_salesforce_servicelibs_jprotoc", + ], +) + +java_binary( + name = "rxgrpc_bin", + main_class = "com.salesforce.rxgrpc.RxGrpcGenerator", + visibility = ["//visibility:public"], + runtime_deps = [":rxgrpc"], +) diff --git a/rx-java/rxgrpc/pom.xml b/rx-java/rxgrpc/pom.xml index c7d7218d..b53938b3 100644 --- a/rx-java/rxgrpc/pom.xml +++ b/rx-java/rxgrpc/pom.xml @@ -12,7 +12,7 @@ com.salesforce.servicelibs reactive-grpc - 0.11.0-SNAPSHOT + 1.2.5-SNAPSHOT ../../pom.xml 4.0.0 @@ -39,23 +39,85 @@ - org.springframework.boot - spring-boot-maven-plugin - 1.5.8.RELEASE + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + package - repackage + shade - com.salesforce.rxgrpc.RxGrpcGenerator - JAR - jdk8 - true + + + android.annotation + com.salesforce.servicelibs.android.annotation + + + com.github.mustachejava + com.salesforce.servicelibs.com.github.mustachejava + + + com.google + com.salesforce.servicelibs.com.google + + + google.protobuf + com.salesforce.servicelibs.google.protobuf + + + io.grpc + com.salesforce.servicelibs.io.grpc + + + io.perfmark + com.salesforce.servicelibs.io.perfmark + + + javax.annotation + com.salesforce.servicelibs.javax.annotation + + + org.checkerframework + com.salesforce.servicelibs.org.checkerframework + + + org.codehaus + com.salesforce.servicelibs.org.codehaus + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + com.salesforce.rxgrpc.RxGrpcGenerator + + + + + com.salesforce.servicelibs + canteen-maven-plugin + ${canteen.plugin.version} + + + + bootstrap + + + + - \ No newline at end of file + diff --git a/rx-java/rxgrpc/src/main/resources/RxStub.mustache b/rx-java/rxgrpc/src/main/resources/RxStub.mustache index 45285e83..b741c1b4 100644 --- a/rx-java/rxgrpc/src/main/resources/RxStub.mustache +++ b/rx-java/rxgrpc/src/main/resources/RxStub.mustache @@ -58,7 +58,7 @@ public final class {{className}} { public void accept({{inputType}} request, io.grpc.stub.StreamObserver<{{outputType}}> observer) { delegateStub.{{methodNameCamelCase}}(request, observer); } - }); + }, getCallOptions()); {{/isManyInput}} {{#isManyInput}} new com.salesforce.reactivegrpc.common.Function, io.grpc.stub.StreamObserver<{{inputType}}>>() { @@ -66,7 +66,7 @@ public final class {{className}} { public io.grpc.stub.StreamObserver<{{inputType}}> apply(io.grpc.stub.StreamObserver<{{outputType}}> observer) { return delegateStub.{{methodNameCamelCase}}(observer); } - }); + }, getCallOptions()); {{/isManyInput}} } @@ -85,7 +85,7 @@ public final class {{className}} { public void accept({{inputType}} request, io.grpc.stub.StreamObserver<{{outputType}}> observer) { delegateStub.{{methodNameCamelCase}}(request, observer); } - }); + }, getCallOptions()); } {{/unaryRequestMethods}} @@ -97,6 +97,18 @@ public final class {{className}} { public static abstract class {{serviceName}}ImplBase implements io.grpc.BindableService { {{#methods}} + {{^isManyInput}} + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}io.reactivex.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.Single{{/isManyOutput}}<{{outputType}}> {{methodNameCamelCase}}({{inputType}} request) { + return {{methodNameCamelCase}}(io.reactivex.Single.just(request)); + } + {{/isManyInput}} + {{#javaDoc}} {{{javaDoc}}} {{/javaDoc}} @@ -121,10 +133,19 @@ public final class {{className}} { {{/methods}} .build(); } + + protected io.grpc.CallOptions getCallOptions(int methodId) { + return null; + } + + protected Throwable onErrorMap(Throwable throwable) { + return com.salesforce.rxgrpc.stub.ServerCalls.prepareError(throwable); + } + } {{#methods}} - private static final int METHODID_{{methodNameUpperUnderscore}} = {{methodNumber}}; + public static final int METHODID_{{methodNameUpperUnderscore}} = {{methodNumber}}; {{/methods}} private static final class MethodHandlers implements @@ -149,12 +170,12 @@ public final class {{className}} { case METHODID_{{methodNameUpperUnderscore}}: com.salesforce.rxgrpc.stub.ServerCalls.{{reactiveCallsMethodName}}(({{inputType}}) request, (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, - new com.salesforce.reactivegrpc.common.Function<{{#isManyInput}}io.reactivex.Flowable{{/isManyInput}}{{^isManyInput}}io.reactivex.Single{{/isManyInput}}<{{inputType}}>, {{#isManyOutput}}io.reactivex.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.Single{{/isManyOutput}}<{{outputType}}>>() { + new com.salesforce.reactivegrpc.common.Function<{{#isManyInput}}io.reactivex.Flowable<{{inputType}}>{{/isManyInput}}{{^isManyInput}}{{inputType}}{{/isManyInput}}, {{#isManyOutput}}io.reactivex.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.Single{{/isManyOutput}}<{{outputType}}>>() { @java.lang.Override - public {{#isManyOutput}}io.reactivex.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.Single{{/isManyOutput}}<{{outputType}}> apply({{#isManyInput}}io.reactivex.Flowable{{/isManyInput}}{{^isManyInput}}io.reactivex.Single{{/isManyInput}}<{{inputType}}> single) { + public {{#isManyOutput}}io.reactivex.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.Single{{/isManyOutput}}<{{outputType}}> apply({{#isManyInput}}io.reactivex.Flowable<{{inputType}}>{{/isManyInput}}{{^isManyInput}}{{inputType}}{{/isManyInput}} single) { return serviceImpl.{{methodNameCamelCase}}(single); } - }); + }, serviceImpl::onErrorMap); break; {{/isManyInput}} {{/methods}} @@ -172,7 +193,7 @@ public final class {{className}} { case METHODID_{{methodNameUpperUnderscore}}: return (io.grpc.stub.StreamObserver) com.salesforce.rxgrpc.stub.ServerCalls.{{reactiveCallsMethodName}}( (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, - serviceImpl::{{methodNameCamelCase}}); + serviceImpl::{{methodNameCamelCase}}, serviceImpl::onErrorMap, serviceImpl.getCallOptions(methodId)); {{/isManyInput}} {{/methods}} default: diff --git a/rx3-java/rx3grpc-stub/BUILD.bazel b/rx3-java/rx3grpc-stub/BUILD.bazel new file mode 100644 index 00000000..ec22a8d3 --- /dev/null +++ b/rx3-java/rx3grpc-stub/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "rxgrpc-stub", + srcs = glob(["src/main/**/*.java"]), + # Disable error prone build failure (triggered by unused return) + javacopts = ["-XepDisableAllChecks"], + visibility = ["//visibility:public"], + deps = [ + "//common/reactive-grpc-common", + "@com_google_guava_guava", + "@io_grpc_grpc_java//context", + "@io_grpc_grpc_java//core", + "@io_grpc_grpc_java//stub", + "@io_reactivex_rxjava2_rxjava", + "@org_reactivestreams_reactive_streams", + ], +) diff --git a/rx3-java/rx3grpc-stub/README.md b/rx3-java/rx3grpc-stub/README.md new file mode 100644 index 00000000..3c4f3d16 --- /dev/null +++ b/rx3-java/rx3grpc-stub/README.md @@ -0,0 +1,18 @@ +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.salesforce.servicelibs/rxgrpc-stub/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.salesforce.servicelibs/rxgrpc-stub) + +Usage +===== +```xml + + + ... + + + + com.salesforce.servicelibs + rxgrpc-stub + [VERSION] + + + +``` \ No newline at end of file diff --git a/rx3-java/rx3grpc-stub/pom.xml b/rx3-java/rx3grpc-stub/pom.xml new file mode 100644 index 00000000..38e038a1 --- /dev/null +++ b/rx3-java/rx3grpc-stub/pom.xml @@ -0,0 +1,135 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + rx3grpc-stub + + + ${java.version} + ${java.version} + + + + + ${project.groupId} + reactive-grpc-common + ${project.version} + + + io.grpc + grpc-stub + + + io.reactivex.rxjava3 + rxjava + ${rx3java.version} + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + ../../checkstyle.xml + ../../checkstyle_ignore.xml + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.20 + + + signature-check + verify + + check + + + + + + org.codehaus.mojo.signature + java16 + 1.0 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 5.1.2 + + + bundle-manifest + process-classes + + manifest + + + + + + + diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcContextOnScheduleHook.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcContextOnScheduleHook.java new file mode 100644 index 00000000..2c4dfa27 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcContextOnScheduleHook.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import io.grpc.Context; +import io.reactivex.rxjava3.functions.Function; + +/** + * {@code GrpcContextOnScheduleHook} is a RxJava scheduler handler hook implementation for transferring the gRPC + * {@code Context} between RxJava Schedulers. + *

+ * To install the hook, call {@code RxJavaPlugins.setScheduleHandler(new GrpcContextOnScheduleHook());} somewhere in + * your application startup. + */ +public class GrpcContextOnScheduleHook implements Function { + @Override + public Runnable apply(Runnable runnable) { + return Context.current().wrap(runnable); + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcRetry.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcRetry.java new file mode 100644 index 00000000..72df0189 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/GrpcRetry.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.FlowableConverter; +import io.reactivex.rxjava3.core.FlowableTransformer; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleConverter; +import io.reactivex.rxjava3.core.SingleSource; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.functions.Supplier; + +/** + * {@code GrpcRetry} is used to transparently re-establish a streaming gRPC request in the event of a server error. + *

+ * During a retry, the upstream rx pipeline is re-subscribed to acquire a request message and the RPC call re-issued. + * The downstream rx pipeline never sees the error. + */ +public final class GrpcRetry { + private GrpcRetry() { } + + /** + * {@link GrpcRetry} functions for streaming response gRPC operations. + */ + public static final class OneToMany { + private OneToMany() { } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flowable#retryWhen(Function)}. + * + * For easier use, use the RetryWhen builder from + * RxJava2 Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param handler receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flowable#retryWhen(Function) + */ + public static SingleConverter> retryWhen(final Function, Flowable> operation, final Function, ? extends Publisher> handler) { + return new SingleConverter>() { + @Override + public Flowable apply(final Single request) { + return Flowable.defer(new Supplier>() { + @Override + public Publisher get() throws Throwable { + return operation.apply(request); + } + }).retryWhen(handler); + } + }; + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param unit the units to use for {@code delay} + * @param I + * @param O + */ + public static SingleConverter> retryAfter(final Function, Flowable> operation, final int delay, final TimeUnit unit) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors.flatMap(new Function>() { + @Override + public Publisher apply(Throwable error) { + return Flowable.timer(delay, unit); + } + }); + } + }); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static SingleConverter> retryImmediately(final Function, Flowable> operation) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors; + } + }); + } + } + + /** + * {@link GrpcRetry} functions for bi-directional streaming gRPC operations. + */ + public static final class ManyToMany { + private ManyToMany() { } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flowable#retryWhen(Function)}. + * + * For easier use, use the RetryWhen builder from + * RxJava2 Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param handler receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flowable#retryWhen(Function) + */ + public static FlowableTransformer retryWhen(final Function, Flowable> operation, final Function, ? extends Publisher> handler) { + return new FlowableTransformer() { + @Override + public Flowable apply(final Flowable request) { + return Flowable.defer(new Supplier>() { + @Override + public Publisher get() throws Throwable { + return operation.apply(request); + } + }).retryWhen(handler); + } + }; + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param unit the units to use for {@code delay} + * @param I + * @param O + */ + public static FlowableTransformer retryAfter(final Function, Flowable> operation, final int delay, final TimeUnit unit) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors.flatMap(new Function>() { + @Override + public Publisher apply(Throwable error) { + return Flowable.timer(delay, unit); + } + }); + } + }); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static FlowableTransformer retryImmediately(final Function, Flowable> operation) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors; + } + }); + } + } + + /** + * {@link GrpcRetry} functions for streaming request gRPC operations. + */ + public static final class ManyToOne { + private ManyToOne() { } + + /** + * Retries a streaming gRPC call, using the same semantics as {@link Flowable#retryWhen(Function)}. + * + * For easier use, use the RetryWhen builder from + * RxJava2 Extras. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param handler receives a Publisher of notifications with which a user can complete or error, aborting the retry + * @param I + * @param O + * + * @see Flowable#retryWhen(Function) + */ + public static FlowableConverter> retryWhen(final Function, Single> operation, final Function, ? extends Publisher> handler) { + return new FlowableConverter>() { + @Override + public Single apply(final Flowable request) { + return Single.defer(new Supplier>() { + @Override + public SingleSource get() throws Throwable { + return operation.apply(request); + } + }).retryWhen(handler); + } + }; + } + + /** + * Retries a streaming gRPC call with a fixed delay between retries. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param delay the delay between retries + * @param unit the units to use for {@code delay} + * @param I + * @param O + */ + public static FlowableConverter> retryAfter(final Function, Single> operation, final int delay, final TimeUnit unit) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors.flatMap(new Function>() { + @Override + public Publisher apply(Throwable error) { + return Flowable.timer(delay, unit); + } + }); + } + }); + } + + /** + * Retries a streaming gRPC call immediately. + * + * @param operation the gRPC operation to retry, typically from a generated reactive-grpc stub class + * @param I + * @param O + */ + public static FlowableConverter> retryImmediately(final Function, Single> operation) { + return retryWhen(operation, new Function, Publisher>() { + @Override + public Publisher apply(Flowable errors) { + return errors; + } + }); + } + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ClientCalls.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ClientCalls.java new file mode 100644 index 00000000..171e7cbf --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ClientCalls.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import org.reactivestreams.Publisher; + +import com.salesforce.reactivegrpc.common.BiConsumer; +import com.salesforce.reactivegrpc.common.Function; + +import io.grpc.CallOptions; +import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleEmitter; +import io.reactivex.rxjava3.core.SingleOnSubscribe; +import io.reactivex.rxjava3.functions.Consumer; + +/** + * Utility functions for processing different client call idioms. We have one-to-one correspondence + * between utilities in this class and the potential signatures in a generated stub client class so + * that the runtime can vary behavior without requiring regeneration of the stub. + */ +public final class ClientCalls { + private ClientCalls() { + + } + + /** + * Implements a unary → unary call using {@link Single} → {@link Single}. + */ + public static Single oneToOne( + final Single rxRequest, + final BiConsumer> delegate, + final CallOptions options) { + try { + return Single + .create(new SingleOnSubscribe() { + @Override + public void subscribe(final SingleEmitter emitter) { + rxRequest.subscribe( + new Consumer() { + @Override + public void accept(TRequest request) { + delegate.accept(request, new StreamObserver() { + @Override + public void onNext(TResponse tResponse) { + emitter.onSuccess(tResponse); + } + + @Override + public void onError(Throwable throwable) { + emitter.onError(throwable); + } + + @Override + public void onCompleted() { + // Do nothing + } + }); + } + }, + new Consumer() { + @Override + public void accept(Throwable t) { + emitter.onError(t); + } + } + ); + } + }) + .lift(new SubscribeOnlyOnceSingleOperator()); + } catch (Throwable throwable) { + return Single.error(throwable); + } + } + + /** + * Implements a unary → stream call as {@link Single} → {@link Flowable}, where the server responds with a + * stream of messages. + */ + public static Flowable oneToMany( + final Single rxRequest, + final BiConsumer> delegate, + final CallOptions options) { + try { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + + return rxRequest + .flatMapPublisher(new io.reactivex.rxjava3.functions.Function>() { + @Override + public Publisher apply(TRequest request) { + final RxClientStreamObserverAndPublisher consumerStreamObserver = + new RxClientStreamObserverAndPublisher(null, null, prefetch, lowTide); + + delegate.accept(request, consumerStreamObserver); + + return consumerStreamObserver; + } + }); + } catch (Throwable throwable) { + return Flowable.error(throwable); + } + } + + /** + * Implements a stream → unary call as {@link Flowable} → {@link Single}, where the client transits a stream of + * messages. + */ + @SuppressWarnings("unchecked") + public static Single manyToOne( + final Flowable flowableSource, + final Function, StreamObserver> delegate, + final CallOptions options) { + try { + final RxSubscriberAndClientProducer subscriberAndGRPCProducer = + flowableSource.subscribeWith(new RxSubscriberAndClientProducer()); + final RxClientStreamObserverAndPublisher observerAndPublisher = + new RxClientStreamObserverAndPublisher( + new com.salesforce.reactivegrpc.common.Consumer>() { + @Override + public void accept(CallStreamObserver observer) { + subscriberAndGRPCProducer.subscribe((CallStreamObserver) observer); + } + }, + new Runnable() { + @Override + public void run() { + subscriberAndGRPCProducer.cancel(); + } + } + ); + + return Flowable.fromPublisher(observerAndPublisher) + .doOnSubscribe(s -> delegate.apply(observerAndPublisher)) + .singleOrError(); + } catch (Throwable throwable) { + return Single.error(throwable); + } + } + + /** + * Implements a bidirectional stream → stream call as {@link Flowable} → {@link Flowable}, where both the client + * and the server independently stream to each other. + */ + @SuppressWarnings("unchecked") + public static Flowable manyToMany( + final Flowable flowableSource, + final Function, StreamObserver> delegate, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + + try { + final RxSubscriberAndClientProducer subscriberAndGRPCProducer = + flowableSource.subscribeWith(new RxSubscriberAndClientProducer()); + final RxClientStreamObserverAndPublisher observerAndPublisher = + new RxClientStreamObserverAndPublisher( + new com.salesforce.reactivegrpc.common.Consumer>() { + @Override + public void accept(CallStreamObserver observer) { + subscriberAndGRPCProducer.subscribe((CallStreamObserver) observer); + } + }, + new Runnable() { + @Override + public void run() { + subscriberAndGRPCProducer.cancel(); + } + }, + prefetch, lowTide); + + return Flowable.fromPublisher(observerAndPublisher).doOnSubscribe(s -> delegate.apply(observerAndPublisher)); + } catch (Throwable throwable) { + return Flowable.error(throwable); + } + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/FusionAwareQueueSubscriptionAdapter.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/FusionAwareQueueSubscriptionAdapter.java new file mode 100644 index 00000000..5bddcbd4 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/FusionAwareQueueSubscriptionAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractUnimplementedQueue; +import com.salesforce.reactivegrpc.common.FusionModeAwareSubscription; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * Implementation of FusionModeAwareSubscription which encapsulate + * {@link QueueSubscription} from RxJava internals and allows treat it as a {@link java.util.Queue}. + * + * @param T + */ +class FusionAwareQueueSubscriptionAdapter extends AbstractUnimplementedQueue implements QueueSubscription, FusionModeAwareSubscription { + + private final QueueSubscription delegate; + private final int mode; + + FusionAwareQueueSubscriptionAdapter(QueueSubscription delegate, int mode) { + this.delegate = delegate; + this.mode = mode; + } + + @Override + public int mode() { + return mode; + } + + @Override + public int requestFusion(int mode) { + return delegate.requestFusion(mode); + } + + @Override + public void request(long l) { + delegate.request(l); + } + + @Override + public void cancel() { + delegate.cancel(); + } + + @Override + public T poll() { + try { + return delegate.poll(); + } catch (Throwable e) { + throw Exceptions.propagate(e); + } + } + + @Override + public boolean offer(T t) { + return delegate.offer(t); + } + + @Override + public boolean offer(T v1, T v2) { + return delegate.offer(v1, v2); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public void clear() { + delegate.clear(); + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxCallOptions.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxCallOptions.java new file mode 100644 index 00000000..6ce57267 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxCallOptions.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractStreamObserverAndPublisher; + +import io.grpc.CallOptions; + +/** + * RX Call options. + */ +public final class RxCallOptions { + + private RxCallOptions() { + } + + /** + * Sets Prefetch size of queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_PREFETCH = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.PREFETCH", + Integer.valueOf(AbstractStreamObserverAndPublisher.DEFAULT_CHUNK_SIZE)); + + /** + * Sets Low Tide of prefetch queue. + */ + public static final io.grpc.CallOptions.Key CALL_OPTIONS_LOW_TIDE = + io.grpc.CallOptions.Key.createWithDefault("reactivegrpc.internal.LOW_TIDE", + Integer.valueOf(AbstractStreamObserverAndPublisher.TWO_THIRDS_OF_DEFAULT_CHUNK_SIZE)); + + + /** + * Utility function to get prefetch option. + */ + public static int getPrefetch(final CallOptions options) { + return options == null ? CALL_OPTIONS_PREFETCH.getDefault() : options.getOption(CALL_OPTIONS_PREFETCH); + } + + /** + * Utility function to get low tide option together with validation. + */ + public static int getLowTide(final CallOptions options) { + int prefetch = getPrefetch(options); + int lowTide = options == null ? CALL_OPTIONS_LOW_TIDE.getDefault() : options.getOption(CALL_OPTIONS_LOW_TIDE); + if (lowTide >= prefetch) { + throw new IllegalArgumentException(CALL_OPTIONS_LOW_TIDE + " must be less than " + CALL_OPTIONS_PREFETCH); + } + return lowTide; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxClientStreamObserverAndPublisher.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxClientStreamObserverAndPublisher.java new file mode 100644 index 00000000..4787b8b6 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxClientStreamObserverAndPublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractClientStreamObserverAndPublisher; +import com.salesforce.reactivegrpc.common.Consumer; + +import io.grpc.stub.CallStreamObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +/** + * TODO: Explain what this class does. + * + * @param T + */ +class RxClientStreamObserverAndPublisher + extends AbstractClientStreamObserverAndPublisher + implements QueueSubscription { + + RxClientStreamObserverAndPublisher( + Consumer> onSubscribe, + Runnable onTerminate) { + super(new SimpleQueueAdapter(new SpscArrayQueue(DEFAULT_CHUNK_SIZE)), onSubscribe, onTerminate); + } + + RxClientStreamObserverAndPublisher( + Consumer> onSubscribe, + Runnable onTerminate, + int prefetch, + int lowTide) { + super(new SimpleQueueAdapter(new SpscArrayQueue(prefetch)), onSubscribe, onTerminate, prefetch, lowTide); + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & QueueFuseable.ASYNC) != 0) { + outputFused = true; + return QueueFuseable.ASYNC; + } + return QueueFuseable.NONE; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxServerStreamObserverAndPublisher.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxServerStreamObserverAndPublisher.java new file mode 100644 index 00000000..be311624 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxServerStreamObserverAndPublisher.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractServerStreamObserverAndPublisher; +import com.salesforce.reactivegrpc.common.Consumer; + +import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.ServerCallStreamObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +/** + * TODO: Explain what this class does. + * @param T + */ +class RxServerStreamObserverAndPublisher + extends AbstractServerStreamObserverAndPublisher + implements QueueSubscription { + + RxServerStreamObserverAndPublisher( + ServerCallStreamObserver serverCallStreamObserver, + Consumer> onSubscribe, + int prefetch, + int lowTide) { + super(serverCallStreamObserver, new SimpleQueueAdapter(new SpscArrayQueue(prefetch)), onSubscribe, prefetch, lowTide); + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & QueueFuseable.ASYNC) != 0) { + outputFused = true; + return QueueFuseable.ASYNC; + } + return QueueFuseable.NONE; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndClientProducer.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndClientProducer.java new file mode 100644 index 00000000..fe2b04be --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndClientProducer.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import org.reactivestreams.Subscription; + +import com.salesforce.reactivegrpc.common.AbstractSubscriberAndClientProducer; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * The gRPC client-side implementation of {@link com.salesforce.reactivegrpc.common.AbstractSubscriberAndProducer}. + * + * @param T + */ +public class RxSubscriberAndClientProducer + extends AbstractSubscriberAndClientProducer + implements FlowableSubscriber { + + @Override + protected Subscription fuse(Subscription s) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription f = (QueueSubscription) s; + + int m = f.requestFusion(QueueSubscription.ANY); + + if (m != QueueSubscription.NONE) { + return new FusionAwareQueueSubscriptionAdapter(f, m); + } + } + + return s; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndServerProducer.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndServerProducer.java new file mode 100644 index 00000000..f25498bf --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/RxSubscriberAndServerProducer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.Function; +import org.reactivestreams.Subscription; + +import com.salesforce.reactivegrpc.common.AbstractSubscriberAndServerProducer; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * The gRPC server-side implementation of {@link com.salesforce.reactivegrpc.common.AbstractSubscriberAndProducer}. + * + * @param T + */ +public class RxSubscriberAndServerProducer + extends AbstractSubscriberAndServerProducer + implements FlowableSubscriber { + + public RxSubscriberAndServerProducer(Function prepareError) { + super(prepareError); + } + + @Override + protected Subscription fuse(Subscription s) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription f = (QueueSubscription) s; + + int m = f.requestFusion(QueueSubscription.ANY); + + if (m != QueueSubscription.NONE) { + return new FusionAwareQueueSubscriptionAdapter(f, m); + } + } + + return s; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ServerCalls.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ServerCalls.java new file mode 100644 index 00000000..4cc116a0 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/ServerCalls.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import com.google.common.base.Preconditions; +import com.salesforce.reactivegrpc.common.Function; + +import io.grpc.CallOptions; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ServerCallStreamObserver; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.functions.Consumer; + +/** + * Utility functions for processing different server call idioms. We have one-to-one correspondence + * between utilities in this class and the potential signatures in a generated server stub class so + * that the runtime can vary behavior without requiring regeneration of the stub. + */ +public final class ServerCalls { + private ServerCalls() { + + } + + /** + * Implements a unary → unary call using {@link Single} → {@link Single}. + */ + public static void oneToOne( + final TRequest request, + final StreamObserver responseObserver, + final Function> delegate, + final Function prepareError) { + try { + final Single rxResponse = Preconditions.checkNotNull(delegate.apply(request)); + rxResponse.subscribe( + new Consumer() { + @Override + public void accept(TResponse value) { + // Don't try to respond if the server has already canceled the request + if (responseObserver instanceof ServerCallStreamObserver && ((ServerCallStreamObserver) responseObserver).isCancelled()) { + return; + } + responseObserver.onNext(value); + responseObserver.onCompleted(); + } + }, + new Consumer() { + @Override + public void accept(Throwable throwable) { + responseObserver.onError(prepareError.apply(throwable)); + } + }); + } catch (Throwable throwable) { + responseObserver.onError(prepareError.apply(throwable)); + } + } + + /** + * Implements a unary → stream call as {@link Single} → {@link Flowable}, where the server responds with a + * stream of messages. + */ + public static void oneToMany( + final TRequest request, + final StreamObserver responseObserver, + final Function> delegate, + final Function prepareError) { + try { + final Flowable rxResponse = Preconditions.checkNotNull(delegate.apply(request)); + final RxSubscriberAndServerProducer serverProducer = + rxResponse.subscribeWith(new RxSubscriberAndServerProducer(prepareError)); + serverProducer.subscribe((ServerCallStreamObserver) responseObserver); + } catch (Throwable throwable) { + responseObserver.onError(prepareError.apply(throwable)); + } + } + + /** + * Implements a stream → unary call as {@link Flowable} → {@link Single}, where the client transits a stream of + * messages. + */ + public static StreamObserver manyToOne( + final StreamObserver responseObserver, + final Function, Single> delegate, + final Function prepareError, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + + final RxServerStreamObserverAndPublisher streamObserverPublisher = + new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); + + try { + final Single rxResponse = Preconditions.checkNotNull(delegate.apply(Flowable.fromPublisher(streamObserverPublisher))); + rxResponse.subscribe( + new Consumer() { + @Override + public void accept(TResponse value) { + // Don't try to respond if the server has already canceled the request + if (!streamObserverPublisher.isCancelled()) { + responseObserver.onNext(value); + responseObserver.onCompleted(); + } + } + }, + new Consumer() { + @Override + public void accept(Throwable throwable) { + // Don't try to respond if the server has already canceled the request + if (!streamObserverPublisher.isCancelled()) { + streamObserverPublisher.abortPendingCancel(); + responseObserver.onError(prepareError.apply(throwable)); + } + } + } + ); + } catch (Throwable throwable) { + responseObserver.onError(prepareError.apply(throwable)); + } + + return streamObserverPublisher; + } + + /** + * Implements a bidirectional stream → stream call as {@link Flowable} → {@link Flowable}, where both the client + * and the server independently stream to each other. + */ + public static StreamObserver manyToMany( + final StreamObserver responseObserver, + final Function, Flowable> delegate, + final Function prepareError, + final CallOptions options) { + + final int prefetch = RxCallOptions.getPrefetch(options); + final int lowTide = RxCallOptions.getLowTide(options); + + final RxServerStreamObserverAndPublisher streamObserverPublisher = + new RxServerStreamObserverAndPublisher((ServerCallStreamObserver) responseObserver, null, prefetch, lowTide); + + try { + final Flowable rxResponse = Preconditions.checkNotNull(delegate.apply(Flowable.fromPublisher(streamObserverPublisher))); + final RxSubscriberAndServerProducer subscriber = new RxSubscriberAndServerProducer(prepareError); + subscriber.subscribe((ServerCallStreamObserver) responseObserver); + // Don't try to respond if the server has already canceled the request + rxResponse.subscribe(subscriber); + } catch (Throwable throwable) { + responseObserver.onError(prepareError.apply(throwable)); + } + + return streamObserverPublisher; + } + + /** + * Implements default error mapping. + */ + public static Throwable prepareError(Throwable throwable) { + if (throwable instanceof StatusException || throwable instanceof StatusRuntimeException) { + return throwable; + } else { + return Status.fromThrowable(throwable).asException(); + } + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SimpleQueueAdapter.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SimpleQueueAdapter.java new file mode 100644 index 00000000..e50b354c --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SimpleQueueAdapter.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +package com.salesforce.rx3grpc.stub; + +import com.salesforce.reactivegrpc.common.AbstractUnimplementedQueue; + +import io.reactivex.rxjava3.operators.SimplePlainQueue; + +/** + * Adapts the RxJava {@code SimpleQueue} interface to a common java {@link java.util.Queue}. + * @param T + */ +final class SimpleQueueAdapter extends AbstractUnimplementedQueue implements SimplePlainQueue { + + private final SimplePlainQueue simpleQueue; + + SimpleQueueAdapter(SimplePlainQueue queue) { + simpleQueue = queue; + } + + @Override + public T poll() { + return simpleQueue.poll(); + } + + @Override + public boolean isEmpty() { + return simpleQueue.isEmpty(); + } + + @Override + public void clear() { + simpleQueue.clear(); + } + + @Override + public boolean offer(T t) { + return simpleQueue.offer(t); + } + + @Override + public boolean offer(T t1, T t2) { + return simpleQueue.offer(t1, t2); + } + + @Override + public boolean equals(Object o) { + return simpleQueue.equals(o); + } + + @Override + public int hashCode() { + return simpleQueue.hashCode(); + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceFlowableOperator.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceFlowableOperator.java new file mode 100644 index 00000000..671c3cd8 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceFlowableOperator.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableOperator; + +/** + * SubscribeOnlyOnceFlowableOperator throws an exception if a user attempts to subscribe more than once to a + * {@link io.reactivex.rxjava3.core.Flowable}. + * + * @param T + */ +public class SubscribeOnlyOnceFlowableOperator implements FlowableOperator { + private AtomicBoolean subscribedOnce = new AtomicBoolean(false); + + @Override + public Subscriber apply(final Subscriber observer) { + return new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + if (subscribedOnce.getAndSet(true)) { + throw new NullPointerException("You cannot directly subscribe to a gRPC service multiple times " + + "concurrently. Use Flowable.share() instead."); + } else { + observer.onSubscribe(subscription); + } + } + + @Override + public void onNext(T t) { + observer.onNext(t); + } + + @Override + public void onError(Throwable throwable) { + observer.onError(throwable); + } + + @Override + public void onComplete() { + observer.onComplete(); + } + }; + } +} diff --git a/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceSingleOperator.java b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceSingleOperator.java new file mode 100644 index 00000000..89803a6d --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/main/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceSingleOperator.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.core.SingleOperator; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * SubscribeOnlyOnceSingleOperator throws an exception if a user attempts to subscribe more than once to a + * {@link io.reactivex.rxjava3.core.Single}. + * + * @param T + */ +public class SubscribeOnlyOnceSingleOperator implements SingleOperator { + private AtomicBoolean subscribedOnce = new AtomicBoolean(false); + + @Override + public SingleObserver apply(final SingleObserver observer) { + return new SingleObserver() { + @Override + public void onSubscribe(Disposable d) { + if (subscribedOnce.getAndSet(true)) { + throw new NullPointerException("You cannot directly subscribe to a gRPC service multiple times " + + "concurrently. Use Flowable.share() instead."); + } else { + observer.onSubscribe(d); + } + } + + @Override + public void onSuccess(T t) { + observer.onSuccess(t); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + }; + } +} diff --git a/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/Consumers.java b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/Consumers.java new file mode 100644 index 00000000..42f28565 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/Consumers.java @@ -0,0 +1,161 @@ +package com.salesforce.rx3grpc.stub; + +import java.io.Closeable; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.functions.LongConsumer; + +public final class Consumers { + + private Consumers() { + // prevent instantiation + } + + public static LongConsumer addLongTo(final List list) { + return new LongConsumer() { + + @Override + public void accept(long t) throws Exception { + list.add(t); + } + + }; + } + + @SuppressWarnings("unchecked") + public static Consumer close() { + return (Consumer) CloseHolder.INSTANCE; + } + + private static final class CloseHolder { + final static Consumer INSTANCE = new Consumer() { + @Override + public void accept(Closeable t) throws Exception { + t.close(); + } + + }; + } + + public static Consumer increment(final AtomicInteger value) { + return new Consumer() { + @Override + public void accept(Object t) throws Exception { + value.incrementAndGet(); + } + }; + } + + public static Consumer printStackTrace() { + // TODO make holder + return new Consumer() { + @Override + public void accept(Throwable e) throws Exception { + e.printStackTrace(); + } + }; + } + + @SuppressWarnings("unchecked") + public static Consumer doNothing() { + return (Consumer) DoNothingHolder.INSTANCE; + } + + private static final class DoNothingHolder { + static final Consumer INSTANCE = new Consumer() { + + @Override + public void accept(Object t) throws Exception { + // do nothing + } + }; + } + + public static Consumer set(final AtomicReference value) { + return new Consumer() { + + @Override + public void accept(T t) throws Exception { + value.set(t); + } + }; + } + + public static Consumer set(final AtomicInteger value) { + return new Consumer() { + @Override + public void accept(Integer t) throws Throwable { + value.set(t); + } + }; + } + + public static Consumer decrement(final AtomicInteger value) { + return new Consumer() { + @Override + public void accept(Object t) throws Throwable { + value.decrementAndGet(); + } + }; + } + + @SuppressWarnings("unchecked") + public static Consumer setToTrue(final AtomicBoolean value) { + return new Consumer() { + @Override + public void accept(T t) throws Throwable { + value.set(true); + } + }; + } + + public static Consumer addTo(final List list) { + return new Consumer() { + @Override + public void accept(T t) throws Throwable { + list.add(t); + } + }; + } + + @SuppressWarnings("unchecked") + public static Consumer println() { + return (Consumer) PrintlnHolder.INSTANCE; + } + + private static final class PrintlnHolder { + static final Consumer INSTANCE = new Consumer() { + @Override + public void accept(Object t) throws Throwable { + System.out.println(t); + } + }; + } + + public static Consumer assertBytesEquals(final byte[] expected) { + // TODO make holder + return new Consumer() { + @Override + public void accept(byte[] array) throws Throwable { + if (!Arrays.equals(expected, array)) { + // TODO use custom exception + throw new Exception("arrays not equal: expected=" + Arrays.toString(expected) + ",actual=" + Arrays.toString(array)); + } + } + }; + } + + public static LongConsumer printLong(final String prefix) { + return new LongConsumer() { + @Override + public void accept(long t) throws Throwable { + System.out.println(prefix + t); + } + }; + } +} diff --git a/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcContextOnScheduleHookTest.java b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcContextOnScheduleHookTest.java new file mode 100644 index 00000000..52844364 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcContextOnScheduleHookTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.awaitility.Duration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.salesforce.rx3grpc.GrpcContextOnScheduleHook; + +import io.grpc.Context; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class GrpcContextOnScheduleHookTest { + @BeforeEach + public void before() { + RxJavaPlugins.setScheduleHandler(new GrpcContextOnScheduleHook()); + } + + @AfterEach + public void after() { + RxJavaPlugins.setScheduleHandler(null); + } + + @Test + public void GrpcContextPropagatesAcrossSchedulers() { + final Context.Key contextKey = Context.key("key"); + + final AtomicBoolean done = new AtomicBoolean(); + + Context.current().withValue(contextKey, "foo").wrap(new Runnable() { + @Override + public void run() { + Observable.just(1, 2, 3) + .observeOn(Schedulers.computation()) + .subscribeOn(Schedulers.io()) + .subscribe( + new Consumer() { + @Override + public void accept(Integer i) throws Exception { + System.out.println(i); + assertThat(contextKey.get()).isEqualTo("foo"); + } + }, + new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + + } + }, + new Action() { + @Override + public void run() throws Exception { + done.set(true); + } + }); + } + }).run(); + + await().atMost(Duration.FIVE_HUNDRED_MILLISECONDS).untilTrue(done); + } +} diff --git a/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcRetryTest.java b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcRetryTest.java new file mode 100644 index 00000000..267aff00 --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/GrpcRetryTest.java @@ -0,0 +1,254 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.salesforce.rx3grpc.GrpcRetry; + +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.FlowableConverter; +import io.reactivex.rxjava3.core.FlowableEmitter; +import io.reactivex.rxjava3.core.FlowableOnSubscribe; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleConverter; +import io.reactivex.rxjava3.core.SingleEmitter; +import io.reactivex.rxjava3.core.SingleOnSubscribe; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("Duplicates") +public class GrpcRetryTest { + private Flowable newThreeErrorFlowable() { + return Flowable.create(new FlowableOnSubscribe() { + int count = 3; + @Override + public void subscribe(FlowableEmitter emitter) throws Exception { + if (count > 0) { + emitter.onError(new Throwable("Not yet!")); + count--; + } else { + emitter.onNext(0); + emitter.onComplete(); + } + } + }, BackpressureStrategy.BUFFER); + } + + private Single newThreeErrorSingle() { + return Single.create(new SingleOnSubscribe() { + int count = 3; + @Override + public void subscribe(SingleEmitter emitter) throws Exception { + if (count > 0) { + emitter.onError(new Throwable("Not yet!")); + count--; + } else { + emitter.onSuccess(0); + } + } + }); + } + + @Test + public void noRetryMakesErrorFlowabable() throws InterruptedException { + TestSubscriber test = newThreeErrorFlowable() + .to(new FlowableConverter>() { + @Override + public Flowable apply(Flowable flowable) { + return flowable; + } + }) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertError(new Predicate() { + @Override + public boolean test(Throwable t) throws Throwable { + return t.getMessage().equals("Not yet!"); + } + }); + } + + @Test + public void noRetryMakesErrorSingle() throws InterruptedException { + TestObserver test = newThreeErrorSingle() + .to(new SingleConverter>() { + @Override + public Single apply(Single single) { + return single; + } + }) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertError(new Predicate() { + @Override + public boolean test(Throwable t) throws Throwable { + return t.getMessage().equals("Not yet!"); + } + }); + } + + @Test + public void oneToManyRetryWhen() throws InterruptedException { + TestSubscriber test = newThreeErrorSingle() + .to(GrpcRetry.OneToMany.retryWhen(new Function, Flowable>() { + @Override + public Flowable apply(Single single) { + return single.toFlowable(); + } + }, RetryWhen.maxRetries(3).build())) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void oneToManyRetryImmediately() throws InterruptedException { + TestSubscriber test = newThreeErrorSingle() + .to(GrpcRetry.OneToMany.retryImmediately(new Function, Flowable>() { + @Override + public Flowable apply(Single single) { + return single.toFlowable(); + } + })) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void oneToManyRetryAfter() throws InterruptedException { + TestSubscriber test = newThreeErrorSingle() + .to(GrpcRetry.OneToMany.retryAfter(new Function, Flowable>() { + @Override + public Flowable apply(Single single) { + return single.toFlowable(); + } + }, 10, TimeUnit.MILLISECONDS)) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToManyRetryWhen() throws InterruptedException { + TestSubscriber test = newThreeErrorFlowable() + .compose(GrpcRetry.ManyToMany.retryWhen(new Function, Flowable>() { + @Override + public Flowable apply(Flowable flowable) { + return flowable; + } + }, RetryWhen.maxRetries(3).build())) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToManyRetryImmediately() throws InterruptedException { + TestSubscriber test = newThreeErrorFlowable() + .compose(GrpcRetry.ManyToMany.retryImmediately(new Function, Flowable>() { + @Override + public Flowable apply(Flowable flowable) { + return flowable; + } + })) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToManyRetryAfter() throws InterruptedException { + TestSubscriber test = newThreeErrorFlowable() + .compose(GrpcRetry.ManyToMany.retryAfter(new Function, Flowable>() { + @Override + public Flowable apply(Flowable flowable) { + return flowable; + } + }, 10, TimeUnit.MILLISECONDS)) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToOneRetryWhen() throws InterruptedException { + TestObserver test = newThreeErrorFlowable() + .to(GrpcRetry.ManyToOne.retryWhen(new Function, Single>() { + @Override + public Single apply(Flowable flowable) { + return flowable.singleOrError(); + } + }, RetryWhen.maxRetries(3).build())) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToOneRetryImmediately() throws InterruptedException { + TestObserver test = newThreeErrorFlowable() + .to(GrpcRetry.ManyToOne.retryImmediately(new Function, Single>() { + @Override + public Single apply(Flowable flowable) { + return flowable.singleOrError(); + } + })) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } + + @Test + public void manyToOneRetryAfter() throws InterruptedException { + TestObserver test = newThreeErrorFlowable() + .to(GrpcRetry.ManyToOne.retryAfter(new Function, Single>() { + @Override + public Single apply(Flowable flowable) { + return flowable.singleOrError(); + } + }, 10, TimeUnit.MILLISECONDS)) + .test(); + + test.await(1, TimeUnit.SECONDS); + test.assertValues(0); + test.assertNoErrors(); + test.assertComplete(); + } +} diff --git a/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/RetryWhen.java b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/RetryWhen.java new file mode 100644 index 00000000..79d5d3df --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/RetryWhen.java @@ -0,0 +1,344 @@ +package com.salesforce.rx3grpc.stub; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Provides builder for the {@link Function} parameter of + * {@link Flowable#retryWhen(Function)}. For example: + * + *
+ * o.retryWhen(RetryWhen.maxRetries(4).delay(10, TimeUnit.SECONDS).action(log).build());
+ * 
+ * + *

+ * or + *

+ * + *
+ * o.retryWhen(RetryWhen.exponentialBackoff(100, TimeUnit.MILLISECONDS).maxRetries(10).build());
+ * 
+ */ +public final class RetryWhen { + + private RetryWhen() { + // prevent instantiation + } + + private static final long NO_MORE_DELAYS = -1; + + private static Function, Flowable> notificationHandler( + final Flowable delays, final Scheduler scheduler, final Consumer action, + final List> retryExceptions, + final List> failExceptions, + final Predicate exceptionPredicate) { + + final Function> checkExceptions = createExceptionChecker( + retryExceptions, failExceptions, exceptionPredicate); + + return createNotificationHandler(delays, scheduler, action, checkExceptions); + } + + private static Function, Flowable> createNotificationHandler( + final Flowable delays, final Scheduler scheduler, final Consumer action, + final Function> checkExceptions) { + return new Function, Flowable>() { + + @SuppressWarnings("unchecked") + @Override + public Flowable apply(Flowable errors) { + // TODO remove this cast when rxjava 2.0.3 released because + // signature of retryWhen + // will be fixed + return (Flowable) (Flowable) errors + // zip with delays, use -1 to signal completion + .zipWith(delays.concatWith(Flowable.just(NO_MORE_DELAYS)), TO_ERROR_AND_DURATION) + // check retry and non-retry exceptions + .flatMap(checkExceptions) + // perform user action (for example log that a + // delay is happening) + .doOnNext(callActionExceptForLast(action)) + // delay the time in ErrorAndDuration + .flatMap(delay(scheduler)); + } + }; + } + + private static Consumer callActionExceptForLast(final Consumer action) { + return new Consumer() { + @Override + public void accept(ErrorAndDuration e) throws Throwable { + if (e.durationMs() != NO_MORE_DELAYS) { + action.accept(e); + } + } + }; + } + + // TODO unit test + private static Function> createExceptionChecker( + final List> retryExceptions, + final List> failExceptions, + final Predicate exceptionPredicate) { + return new Function>() { + + @Override + public Flowable apply(ErrorAndDuration e) throws Throwable { + if (!exceptionPredicate.test(e.throwable())) { + return Flowable.error(e.throwable()); + } + for (Class cls : failExceptions) { + if (cls.isAssignableFrom(e.throwable().getClass())) { + return Flowable.error(e.throwable()); + } + } + if (retryExceptions.size() > 0) { + for (Class cls : retryExceptions) { + if (cls.isAssignableFrom(e.throwable().getClass())) { + return Flowable.just(e); + } + } + return Flowable.error(e.throwable()); + } else { + return Flowable.just(e); + } + } + }; + } + + private static BiFunction TO_ERROR_AND_DURATION = new BiFunction() { + @Override + public ErrorAndDuration apply(Throwable throwable, Long aLong) { + return new ErrorAndDuration(throwable, aLong); + } + }; + + private static Function> delay(final Scheduler scheduler) { + return new Function>() { + @Override + public Flowable apply(ErrorAndDuration e) throws Throwable { + if (e.durationMs() == NO_MORE_DELAYS) { + return Flowable.error(e.throwable()); + } else { + return Flowable.timer(e.durationMs(), TimeUnit.MILLISECONDS, scheduler) + .map(constant(e)); + } + } + }; + } + + private static Function constant(final T value) { + return new Function() { + @Override + public T apply(Object t) throws Throwable { + return value; + } + }; + } + + // Builder factory methods + + public static Builder retryWhenInstanceOf(Class... classes) { + return new Builder().retryWhenInstanceOf(classes); + } + + public static Builder failWhenInstanceOf(Class... classes) { + return new Builder().failWhenInstanceOf(classes); + } + + public static Builder retryIf(Predicate predicate) { + return new Builder().retryIf(predicate); + } + + public static Builder delays(Flowable delays, TimeUnit unit) { + return new Builder().delays(delays, unit); + } + + public static Builder delaysInt(Flowable delays, TimeUnit unit) { + return new Builder().delaysInt(delays, unit); + } + + public static Builder delay(long delay, final TimeUnit unit) { + return new Builder().delay(delay, unit); + } + + public static Builder maxRetries(int maxRetries) { + return new Builder().maxRetries(maxRetries); + } + + public static Builder scheduler(Scheduler scheduler) { + return new Builder().scheduler(scheduler); + } + + public static Builder action(Consumer action) { + return new Builder().action(action); + } + + public static Builder exponentialBackoff(final long firstDelay, final TimeUnit unit, final double factor) { + return new Builder().exponentialBackoff(firstDelay, unit, factor); + } + + public static Builder exponentialBackoff(long firstDelay, TimeUnit unit) { + return new Builder().exponentialBackoff(firstDelay, unit); + } + + public static final class Builder { + + static final class TruePredicate implements Predicate { + @Override + public boolean test(Object o) { + return true; + } + } + private final List> retryExceptions = new ArrayList>(); + private final List> failExceptions = new ArrayList>(); + private Predicate exceptionPredicate = new TruePredicate(); + + private Flowable delays = Flowable.just(0L).repeat(); + private Optional maxRetries = Optional.absent(); + private Optional scheduler = Optional.of(Schedulers.computation()); + private Consumer action = Consumers.doNothing(); + + private Builder() { + // must use static factory method to instantiate + } + + public Builder retryWhenInstanceOf(Class... classes) { + retryExceptions.addAll(Arrays.asList(classes)); + return this; + } + + public Builder failWhenInstanceOf(Class... classes) { + failExceptions.addAll(Arrays.asList(classes)); + return this; + } + + public Builder retryIf(Predicate predicate) { + this.exceptionPredicate = predicate; + return this; + } + + public Builder delays(Flowable delays, TimeUnit unit) { + this.delays = delays.map(toMillis(unit)); + return this; + } + + private static class ToLongHolder { + static final Function INSTANCE = new Function() { + @Override + public Long apply(Integer n) { + if (n == null) { + return null; + } else { + return n.longValue(); + } + } + }; + } + + public Builder delaysInt(Flowable delays, TimeUnit unit) { + return delays(delays.map(ToLongHolder.INSTANCE), unit); + } + + public Builder delay(Long delay, final TimeUnit unit) { + this.delays = Flowable.just(delay).map(toMillis(unit)).repeat(); + return this; + } + + private static Function toMillis(final TimeUnit unit) { + return new Function() { + + @Override + public Long apply(Long t) { + return unit.toMillis(t); + } + }; + } + + public Builder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + public Builder scheduler(Scheduler scheduler) { + this.scheduler = Optional.of(scheduler); + return this; + } + + public Builder action(Consumer action) { + this.action = action; + return this; + } + + public Builder exponentialBackoff(final long firstDelay, final long maxDelay, final TimeUnit unit, + final double factor) { + + delays = Flowable.range(1, Integer.MAX_VALUE) + // make exponential + .map(new Function() { + @Override + public Long apply(Integer n) { + long delayMs = Math.round(Math.pow(factor, n - 1) * unit.toMillis(firstDelay)); + if (maxDelay == -1) { + return delayMs; + } else { + long maxDelayMs = unit.toMillis(maxDelay); + return Math.min(maxDelayMs, delayMs); + } + } + }); + return this; + } + + public Builder exponentialBackoff(final long firstDelay, final TimeUnit unit, final double factor) { + return exponentialBackoff(firstDelay, -1, unit, factor); + } + + public Builder exponentialBackoff(long firstDelay, TimeUnit unit) { + return exponentialBackoff(firstDelay, unit, 2); + } + + public Function, Flowable> build() { + Preconditions.checkNotNull(delays); + if (maxRetries.isPresent()) { + delays = delays.take(maxRetries.get()); + } + return notificationHandler(delays, scheduler.get(), action, retryExceptions, failExceptions, + exceptionPredicate); + } + + } + + public static final class ErrorAndDuration { + + private final Throwable throwable; + private final long durationMs; + + public ErrorAndDuration(Throwable throwable, long durationMs) { + this.throwable = throwable; + this.durationMs = durationMs; + } + + public Throwable throwable() { + return throwable; + } + + public long durationMs() { + return durationMs; + } + + } +} diff --git a/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceTest.java b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceTest.java new file mode 100644 index 00000000..6d2320ac --- /dev/null +++ b/rx3-java/rx3grpc-stub/src/test/java/com/salesforce/rx3grpc/stub/SubscribeOnlyOnceTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.stub; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; + +@SuppressWarnings("unchecked") +public class SubscribeOnlyOnceTest { + @Test + public void subscribeOnlyOnceFlowableOperatorErrorsWhenMultipleSubscribe() { + SubscribeOnlyOnceFlowableOperator op = new SubscribeOnlyOnceFlowableOperator(); + Subscriber innerSub = mock(Subscriber.class); + final Subscription subscription = mock(Subscription.class); + + final Subscriber outerSub = op.apply(innerSub); + + outerSub.onSubscribe(subscription); + assertThatThrownBy(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() { + outerSub.onSubscribe(subscription); + } + }) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("cannot directly subscribe to a gRPC service multiple times"); + + verify(innerSub, times(1)).onSubscribe(subscription); + } + + @Test + public void subscribeOnlyOnceSingleOperatorErrorsWhenMultipleSubscribe() { + SubscribeOnlyOnceSingleOperator op = new SubscribeOnlyOnceSingleOperator(); + SingleObserver innerSub = mock(SingleObserver.class); + final Disposable disposable = mock(Disposable.class); + + final SingleObserver outerSub = op.apply(innerSub); + + outerSub.onSubscribe(disposable); + assertThatThrownBy(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() { + outerSub.onSubscribe(disposable); + } + }) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("cannot directly subscribe to a gRPC service multiple times"); + + verify(innerSub, times(1)).onSubscribe(disposable); + } +} diff --git a/rx3-java/rx3grpc-tck/README.md b/rx3-java/rx3grpc-tck/README.md new file mode 100644 index 00000000..38db3a75 --- /dev/null +++ b/rx3-java/rx3grpc-tck/README.md @@ -0,0 +1,3 @@ +This module contains tests from the Reactive Streams Technology Compatibility Kit for RxGrpc. + +https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck \ No newline at end of file diff --git a/rx3-java/rx3grpc-tck/pom.xml b/rx3-java/rx3grpc-tck/pom.xml new file mode 100644 index 00000000..c2141375 --- /dev/null +++ b/rx3-java/rx3grpc-tck/pom.xml @@ -0,0 +1,108 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + rx3grpc-tck + + + + ${project.groupId} + rx3grpc-stub + ${project.version} + test + + + io.grpc + grpc-netty + test + + + io.grpc + grpc-core + test + + + io.grpc + grpc-stub + test + + + io.grpc + grpc-protobuf + test + + + org.reactivestreams + reactive-streams-tck + ${reactive.streams.version} + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.0 + + + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + true + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf.plugin.version} + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + test-compile + test-compile-custom + + + + + rx3grpc + ${project.groupId} + rx3grpc + ${project.version} + com.salesforce.rx3grpc.Rx3GrpcGenerator + + + + + + + + + + diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/FusedTckService.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/FusedTckService.java new file mode 100644 index 00000000..c04bd5a2 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/FusedTckService.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; + +public class FusedTckService extends Rx3TckGrpc.TckImplBase { + public static final int KABOOM = -1; + + @Override + public Single oneToOne(Single request) { + return request.map(this::maybeExplode); + } + + @Override + public Flowable oneToMany(Single request) { + return request + .map(this::maybeExplode) + .toFlowable() + // send back no more than 10 responses + .flatMap(message -> Flowable.range(1, Math.min(message.getNumber(), 10)) + ,false, 1, 1) + .map(this::toMessage); + } + + @Override + public Single manyToOne(Flowable request) { + return request.map(this::maybeExplode).last(Message.newBuilder().setNumber(0).build()); + } + + @Override + public Flowable manyToMany(Flowable request) { + return request.map(this::maybeExplode); + } + + private Message maybeExplode(Message req) throws Exception { + if (req.getNumber() < 0) { + throw new Exception("Kaboom!"); + } else { + return req; + } + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyFusedVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyFusedVerificationTest.java new file mode 100644 index 00000000..79de0c06 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyFusedVerificationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToManyFusedVerificationTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToManyFusedVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToManyVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherManyToManyVerificationTest").addService(new FusedTckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToManyVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Publisher publisher = request.compose(stub::manyToMany); + + return publisher; + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Publisher publisher = request.compose(stub::manyToMany); + + return publisher; + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyHalfFusedVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyHalfFusedVerificationTest.java new file mode 100644 index 00000000..c08a594e --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyHalfFusedVerificationTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToManyHalfFusedVerificationTest + extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToManyHalfFusedVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToManyVerificationTest"); + server = InProcessServerBuilder.forName( + "RxGrpcPublisherManyToManyVerificationTest").addService(new FusedTckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToManyVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Publisher publisher = request.hide().compose(stub::manyToMany); + + return publisher; + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Publisher publisher = request.hide().compose(stub::manyToMany); + + return publisher; + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyVerificationTest.java new file mode 100644 index 00000000..5351fdd2 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToManyVerificationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToManyVerificationTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToManyVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToManyVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherManyToManyVerificationTest").addService(new TckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToManyVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Publisher publisher = request.hide().compose(stub::manyToMany).hide(); + + return publisher; + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Publisher publisher = request.hide().compose(stub::manyToMany).hide(); + + return publisher; + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationFusedTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationFusedTest.java new file mode 100644 index 00000000..f29ab215 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationFusedTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToOneVerificationFusedTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToOneVerificationFusedTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToOneVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").addService(new FusedTckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Single publisher = request.to(stub::manyToOne); + + return publisher.toFlowable(); + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Single publisher = request.to(stub::manyToOne); + + return publisher.toFlowable(); + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationHalfFusedTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationHalfFusedTest.java new file mode 100644 index 00000000..d7f88ecf --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationHalfFusedTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToOneVerificationHalfFusedTest + extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToOneVerificationHalfFusedTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToOneVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").addService(new FusedTckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Single publisher = request.hide().to(stub::manyToOne); + + return publisher.toFlowable(); + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Single publisher = request.hide().to(stub::manyToOne); + + return publisher.toFlowable(); + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationTest.java new file mode 100644 index 00000000..75d3f6ea --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherManyToOneVerificationTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherManyToOneVerificationTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherManyToOneVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherManyToOneVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").addService(new TckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherManyToOneVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.range(0, (int)elements).map(this::toMessage); + Single publisher = request.hide().to(stub::manyToOne).hide(); + + return publisher.toFlowable(); + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Flowable request = Flowable.just(toMessage(TckService.KABOOM)); + Single publisher = request.hide().to(stub::manyToOne).hide(); + + return publisher.toFlowable(); + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationFussedTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationFussedTest.java new file mode 100644 index 00000000..b15ecaf8 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationFussedTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Single; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherOneToManyVerificationFussedTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherOneToManyVerificationFussedTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherOneToManyVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherOneToManyVerificationTest").addService(new FusedTckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherOneToManyVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage((int) elements)); + Publisher publisher = request.to(stub::oneToMany); + + return publisher; + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage(TckService.KABOOM)); + Publisher publisher = request.to(stub::oneToMany); + + return publisher; + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationTest.java new file mode 100644 index 00000000..32af4453 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToManyVerificationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Single; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherOneToManyVerificationTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherOneToManyVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherOneToManyVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherOneToManyVerificationTest").addService(new TckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherOneToManyVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage((int) elements)); + Publisher publisher = request.hide().to(stub::oneToMany); + + return publisher; + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage(TckService.KABOOM)); + Publisher publisher = request.hide().to(stub::oneToMany); + + return publisher; + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToOneVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToOneVerificationTest.java new file mode 100644 index 00000000..63e9b517 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcPublisherOneToOneVerificationTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.reactivex.rxjava3.core.Single; + +/** + * Publisher tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcPublisherOneToOneVerificationTest extends PublisherVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + public RxGrpcPublisherOneToOneVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setup() throws Exception { + System.out.println("RxGrpcPublisherOneToOneVerificationTest"); + server = InProcessServerBuilder.forName("RxGrpcPublisherOneToOneVerificationTest").addService(new TckService()).build().start(); + channel = InProcessChannelBuilder.forName("RxGrpcPublisherOneToOneVerificationTest").usePlaintext().build(); + } + + @AfterClass + public static void tearDown() throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + + server = null; + channel = null; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } + + @Override + public Publisher createPublisher(long elements) { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage((int) elements)); + Single publisher = request.compose(stub::oneToOne); + + return publisher.toFlowable(); + } + + @Override + public Publisher createFailedPublisher() { + Rx3TckGrpc.RxTckStub stub = Rx3TckGrpc.newRxStub(channel); + Single request = Single.just(toMessage(TckService.KABOOM)); + Single publisher = request.compose(stub::oneToOne); + + return publisher.toFlowable(); + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcSubscriberWhiteboxVerificationTest.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcSubscriberWhiteboxVerificationTest.java new file mode 100644 index 00000000..516fa316 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/RxGrpcSubscriberWhiteboxVerificationTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import javax.annotation.Nullable; + +import com.salesforce.rx3grpc.stub.RxSubscriberAndClientProducer; +import io.grpc.stub.ClientCallStreamObserver; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.SubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Subscriber tests from the Reactive Streams Technology Compatibility Kit. + * https://github.com/reactive-streams/reactive-streams-jvm/tree/master/tck + */ +@SuppressWarnings("Duplicates") +@Test(timeOut = 3000) +public class RxGrpcSubscriberWhiteboxVerificationTest extends SubscriberWhiteboxVerification { + public static final long DEFAULT_TIMEOUT_MILLIS = 500L; + + public RxGrpcSubscriberWhiteboxVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS)); + } + + @BeforeClass + public static void setup() { + System.out.println("RxGrpcSubscriberWhiteboxVerificationTest"); + } + + @Override + public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { + RxSubscriberAndClientProducer producer = + new RxSubscriberAndClientProducer() { + @Override + public void onSubscribe(final Subscription s) { + super.onSubscribe(s); + + // register a successful Subscription, and create a Puppet, + // for the WhiteboxVerification to be able to drive its tests: + probe.registerOnSubscribe(new SubscriberPuppet() { + + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + + super.run(); + } + + @Override + public void onNext(Message element) { + // in addition to normal Subscriber work that you're testing, register onNext with the probe + super.onNext(element); + probe.registerOnNext(element); + } + + @Override + public void onError(Throwable cause) { + // in addition to normal Subscriber work that you're testing, register onError with the probe + super.onError(cause); + probe.registerOnError(cause); + } + + @Override + public void onComplete() { + // in addition to normal Subscriber work that you're testing, register onComplete with the probe + super.onComplete(); + probe.registerOnComplete(); + } + }; + + producer.subscribe(new StubServerCallStreamObserver()); + + return producer; + } + + @Override + public Message createElement(int i) { + return Message.newBuilder().setNumber(i).build(); + } + + private final class StubServerCallStreamObserver extends ClientCallStreamObserver { + @Override + public boolean isReady() { + return true; + } + + @Override + public void setOnReadyHandler(Runnable onReadyHandler) { + + } + + @Override + public void disableAutoInboundFlowControl() { + + } + + @Override + public void request(int count) { + System.out.println("Request " + count); + } + + @Override + public void setMessageCompression(boolean enable) { + + } + + @Override + public void onNext(Message value) { + System.out.println(value.getNumber()); + } + + @Override + public void onError(Throwable t) { + System.out.println(t.getMessage()); + } + + @Override + public void onCompleted() { + System.out.println("Completed"); + } + + @Override + public void cancel(@Nullable String s, @Nullable Throwable throwable) { + + } + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/TckService.java b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/TckService.java new file mode 100644 index 00000000..fa30cdb8 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/java/com/salesforce/rx3grpc/tck/TckService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc.tck; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; + +public class TckService extends Rx3TckGrpc.TckImplBase { + public static final int KABOOM = -1; + + @Override + public Single oneToOne(Single request) { + return request.hide().map(this::maybeExplode); + } + + @Override + public Flowable oneToMany(Single request) { + return request + .hide() + .map(this::maybeExplode) + .toFlowable() + // send back no more than 10 responses + .flatMap(message -> Flowable.range(1, Math.min(message.getNumber(), 10)) + ,false, 1, 1) + .map(this::toMessage) + .hide(); + } + + @Override + public Single manyToOne(Flowable request) { + return request.hide().map(this::maybeExplode).last(Message.newBuilder().setNumber(0).build()).hide(); + } + + @Override + public Flowable manyToMany(Flowable request) { + return request.hide().map(this::maybeExplode); + } + + private Message maybeExplode(Message req) throws Exception { + if (req.getNumber() < 0) { + throw new Exception("Kaboom!"); + } else { + return req; + } + } + + private Message toMessage(int i) { + return Message.newBuilder().setNumber(i).build(); + } +} diff --git a/rx3-java/rx3grpc-tck/src/test/proto/tck.proto b/rx3-java/rx3grpc-tck/src/test/proto/tck.proto new file mode 100644 index 00000000..14168176 --- /dev/null +++ b/rx3-java/rx3grpc-tck/src/test/proto/tck.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package tck; + +option java_multiple_files = true; +option java_package = "com.salesforce.rx3grpc.tck"; +option java_outer_classname = "TckProto"; + +service Tck { + rpc OneToOne (Message) returns (Message) {} + rpc OneToMany (Message) returns (stream Message) {} + rpc ManyToOne (stream Message) returns (Message) {} + rpc ManyToMany (stream Message) returns (stream Message) {} +} + +message Message { + int32 number = 1; +} diff --git a/rx3-java/rx3grpc-test/README.md b/rx3-java/rx3grpc-test/README.md new file mode 100644 index 00000000..a559387b --- /dev/null +++ b/rx3-java/rx3grpc-test/README.md @@ -0,0 +1 @@ +This module contains integration tests for RxGrpc. \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/pom.xml b/rx3-java/rx3grpc-test/pom.xml new file mode 100644 index 00000000..24ec9490 --- /dev/null +++ b/rx3-java/rx3grpc-test/pom.xml @@ -0,0 +1,158 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + rx3grpc-test + + + + ${project.groupId} + rx3grpc-stub + ${project.version} + test + + + io.grpc + grpc-netty + test + + + io.grpc + grpc-core + test + + + io.grpc + grpc-stub + test + + + io.grpc + grpc-protobuf + test + + + io.grpc + grpc-context + test + + + io.grpc + grpc-testing + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.awaitility + awaitility + test + + + com.salesforce.servicelibs + grpc-contrib + test + + + com.salesforce.servicelibs + grpc-testing-contrib + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.0 + + + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + true + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf.plugin.version} + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + test-compile + test-compile-custom + + + + + rx3grpc + ${project.groupId} + rx3grpc + ${project.version} + com.salesforce.rx3grpc.Rx3GrpcGenerator + + + dump + com.salesforce.servicelibs + jprotoc + ${jprotoc.version} + com.salesforce.jprotoc.dump.DumpGenerator + + + + + + + + + diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/AbstractStubTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/AbstractStubTest.java new file mode 100644 index 00000000..5b2c68a4 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/AbstractStubTest.java @@ -0,0 +1,44 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.Deadline; +import io.grpc.ManagedChannel; +import io.grpc.testing.GrpcServerRule; + +public class AbstractStubTest { + @Rule + public GrpcServerRule serverRule = new GrpcServerRule(); + + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + @Test + public void getChannelWorks() { + ManagedChannel channel = serverRule.getChannel(); + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + + assertThat(stub.getChannel()).isEqualTo(channel); + } + + @Test + public void settingCallOptionsWorks() { + ManagedChannel channel = serverRule.getChannel(); + Deadline deadline = Deadline.after(42, TimeUnit.SECONDS); + + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel).withDeadline(deadline); + + assertThat(stub.getCallOptions().getDeadline()).isEqualTo(deadline); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/BackpressureIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/BackpressureIntegrationTest.java new file mode 100644 index 00000000..5771d46c --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/BackpressureIntegrationTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import com.google.protobuf.Empty; +import com.salesforce.servicelibs.NumberProto; +import com.salesforce.servicelibs.NumberProto.Number; +import com.salesforce.servicelibs.Rx3NumbersGrpc; +import com.salesforce.servicelibs.Rx3NumbersGrpc.RxNumbersStub; + +import io.grpc.testing.GrpcServerRule; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("Duplicates") +@Ignore +public class BackpressureIntegrationTest { + @Rule + public GrpcServerRule serverRule = new GrpcServerRule(); + + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static final int NUMBER_OF_STREAM_ELEMENTS = 512 * 12; + + private static AtomicLong lastValueTime; + private static AtomicLong numberOfWaits; + + private static class TestService extends Rx3NumbersGrpc.NumbersImplBase { + @Override + public Single requestPressure(Flowable request) { + return request + .map(proto -> proto.getNumber(0)) + .doOnNext(i -> System.out.println(" --> " + i)) + .doOnNext(i -> waitIfValuesAreEqual(i, 3)) + .last(-1) + .map(BackpressureIntegrationTest::protoNum); + } + + @Override + public Flowable responsePressure(Single request) { + return Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .doOnNext(i -> System.out.println(" <-- " + i)) + .doOnNext(i -> updateNumberOfWaits(lastValueTime, numberOfWaits)) + .map(BackpressureIntegrationTest::protoNum); + } + + @Override + public Flowable twoWayRequestPressure(Flowable request) { + return requestPressure(request).toFlowable(); + } + + @Override + public Flowable twoWayResponsePressure(Flowable request) { + request.subscribe(); + return responsePressure((Empty) null); + } + } + + @Before + public void resetServerStats() { + lastValueTime = new AtomicLong(0); + numberOfWaits = new AtomicLong(0); + } + + @Test + public void clientToServerBackpressure() throws InterruptedException { + serverRule.getServiceRegistry().addService(new TestService()); + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + Flowable rxRequest = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .doOnNext(i -> System.out.println(i + " --> ")) + .doOnNext(i -> updateNumberOfWaits(lastValueTime, numberOfWaits)) + .map(BackpressureIntegrationTest::protoNum); + + TestObserver rxResponse = rxRequest.to(stub::requestPressure).test(); + + rxResponse.await(15, TimeUnit.SECONDS); + rxResponse.assertComplete() + .assertValue(v -> v.getNumber(0) == NUMBER_OF_STREAM_ELEMENTS - 1); + + assertThat(numberOfWaits.get()).isGreaterThan(0L); + } + + @Test + public void serverToClientBackpressure() throws InterruptedException { + serverRule.getServiceRegistry().addService(new TestService()); + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + Single rxRequest = Single.just(Empty.getDefaultInstance()); + + TestSubscriber rxResponse = rxRequest.to(stub::responsePressure) + .doOnNext(n -> System.out.println(n.getNumber(0) + " <--")) + .doOnNext(n -> waitIfValuesAreEqual(n.getNumber(0), 3)) + .test(); + + rxResponse.await(15, TimeUnit.SECONDS); + rxResponse.assertComplete() + .assertValueCount(NUMBER_OF_STREAM_ELEMENTS); + + assertThat(numberOfWaits.get()).isGreaterThan(0L); + } + + @Test + public void bidiResponseBackpressure() throws InterruptedException { + serverRule.getServiceRegistry().addService(new TestService()); + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + TestSubscriber rxResponse = Flowable.empty() + .compose(stub::twoWayResponsePressure) + .doOnNext(n -> System.out.println(n.getNumber(0) + " <--")) + .doOnNext(n -> waitIfValuesAreEqual(n.getNumber(0), 3)) + .test(); + + rxResponse.await(15, TimeUnit.SECONDS); + rxResponse.assertComplete() + .assertValueCount(NUMBER_OF_STREAM_ELEMENTS); + + assertThat(numberOfWaits.get()).isGreaterThan(0L); + } + + @Test + public void bidiRequestBackpressure() throws InterruptedException { + serverRule.getServiceRegistry().addService(new TestService()); + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + Flowable rxRequest = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .doOnNext(i -> System.out.println(i + " --> ")) + .doOnNext(i -> updateNumberOfWaits(lastValueTime, numberOfWaits)) + .map(BackpressureIntegrationTest::protoNum); + + TestSubscriber rxResponse = rxRequest.compose(stub::twoWayRequestPressure).test(); + + rxResponse.await(15, TimeUnit.SECONDS); + rxResponse.assertComplete() + .assertValue(v -> v.getNumber(0) == NUMBER_OF_STREAM_ELEMENTS - 1); + + assertThat(numberOfWaits.get()).isGreaterThan(0L); + } + + + private static void updateNumberOfWaits(AtomicLong start, AtomicLong maxTime) { + Long now = System.currentTimeMillis(); + Long startValue = start.get(); + if (startValue != 0 && now - startValue > 1000) { + maxTime.incrementAndGet(); + } + start.set(now); + } + + private static void waitIfValuesAreEqual(int value, int other) { + if (value == other) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + } + } + + private static NumberProto.Number protoNum(int i) { + Integer[] ints = new Integer[32 * 1024]; + Arrays.setAll(ints, operand -> i); + + return NumberProto.Number.newBuilder().addAllNumber(Arrays.asList(ints)).build(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/CancellationPropagationIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/CancellationPropagationIntegrationTest.java new file mode 100644 index 00000000..ce817e32 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/CancellationPropagationIntegrationTest.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import org.awaitility.Duration; +import org.junit.Rule; +import org.junit.Test; + +import com.google.protobuf.Empty; +import com.salesforce.grpc.testing.contrib.NettyGrpcServerRule; +import com.salesforce.servicelibs.NumberProto; +import com.salesforce.servicelibs.NumberProto.Number; +import com.salesforce.servicelibs.Rx3NumbersGrpc; +import com.salesforce.servicelibs.Rx3NumbersGrpc.RxNumbersStub; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings({"unchecked", "Duplicates"}) +public class CancellationPropagationIntegrationTest { + private static final int NUMBER_OF_STREAM_ELEMENTS = 10000; + + @Rule + public NettyGrpcServerRule serverRule = new NettyGrpcServerRule(); + + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule(); + + private static class TestService extends Rx3NumbersGrpc.NumbersImplBase { + private AtomicInteger lastNumberProduced = new AtomicInteger(Integer.MIN_VALUE); + private AtomicBoolean wasCanceled = new AtomicBoolean(false); + private AtomicBoolean explicitCancel = new AtomicBoolean(false); + + int getLastNumberProduced() { + return lastNumberProduced.get(); + } + + boolean wasCanceled() { + return wasCanceled.get(); + } + + void setExplicitCancel(boolean explicitCancel) { + this.explicitCancel.set(explicitCancel); + } + + @Override + public Flowable responsePressure(Single request) { + // Produce a very long sequence + return Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .delay(10, TimeUnit.MILLISECONDS) + .doOnNext(i -> lastNumberProduced.set(i)) + .map(CancellationPropagationIntegrationTest::protoNum) + .doOnCancel(() -> { + wasCanceled.set(true); + System.out.println("Server canceled"); + }); + } + + @Override + public Single requestPressure(Flowable request) { + if (explicitCancel.get()) { + // Process a very long sequence + Disposable subscription = request.subscribe(n -> System.out.println("S: " + n.getNumber(0))); + return Single + .just(protoNum(-1)) + .delay(250, TimeUnit.MILLISECONDS) + // Explicitly cancel by disposing the subscription + .doOnSuccess(x -> subscription.dispose()); + } else { + // Process some of a very long sequence and cancel implicitly with a take(10) + return request.map(req -> req.getNumber(0)) + .doOnNext(System.out::println) + .take(10) + .last(-1) + .map(CancellationPropagationIntegrationTest::protoNum); + } + } + + @Override + public Flowable twoWayPressure(Flowable request) { + return requestPressure(request).toFlowable(); + } + } + + @Test + public void clientCanCancelServerStreamExplicitly() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + TestSubscriber subscription = Single.just(Empty.getDefaultInstance()) + .to(stub::responsePressure) + .doOnNext(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .test(); + + Thread.sleep(250); + subscription.cancel(); + Thread.sleep(250); + + subscription.await(3, TimeUnit.SECONDS); + // Cancellation may or may not deliver the last generated message due to delays in the gRPC processing thread + assertThat(Math.abs(subscription.values().size() - svc.getLastNumberProduced())).isLessThanOrEqualTo(3); + assertThat(svc.wasCanceled()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void clientCanCancelServerStreamImplicitly() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + TestSubscriber subscription = Single.just(Empty.getDefaultInstance()) + .to(stub::responsePressure) + .doOnNext(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .take(10) + .test(); + + // Consume some work + Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + subscription.cancel(); + + subscription.await(3, TimeUnit.SECONDS); + subscription.assertValueCount(10); + subscription.assertComplete(); + assertThat(svc.wasCanceled()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void serverCanCancelClientStreamImplicitly() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + svc.setExplicitCancel(false); + + AtomicBoolean requestWasCanceled = new AtomicBoolean(false); + AtomicBoolean requestDidProduce = new AtomicBoolean(false); + + Flowable request = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .delay(10, TimeUnit.MILLISECONDS) + .map(CancellationPropagationIntegrationTest::protoNum) + .doOnNext(x -> { + requestDidProduce.set(true); + System.out.println("Produced: " + x.getNumber(0)); + }) + .doOnCancel(() -> { + requestWasCanceled.set(true); + System.out.println("Client canceled"); + }); + + TestObserver observer = request + .to(stub::requestPressure) + .doOnSuccess(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .test(); + + observer.await(3, TimeUnit.SECONDS); + observer.assertComplete(); + + await().atMost(Duration.FIVE_HUNDRED_MILLISECONDS).untilTrue(requestWasCanceled); + + assertThat(requestWasCanceled.get()).isTrue(); + assertThat(requestDidProduce.get()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void serverCanCancelClientStreamExplicitly() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + svc.setExplicitCancel(true); + + AtomicBoolean requestWasCanceled = new AtomicBoolean(false); + AtomicBoolean requestDidProduce = new AtomicBoolean(false); + + Flowable request = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .delay(10, TimeUnit.MILLISECONDS) + .map(CancellationPropagationIntegrationTest::protoNum) + .doOnNext(n -> { + requestDidProduce.set(true); + System.out.println("P: " + n.getNumber(0)); + }) + .doOnCancel(() -> { + requestWasCanceled.set(true); + System.out.println("Client canceled"); + }); + + TestObserver observer = request + .to(stub::requestPressure) + .doOnSuccess(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .test(); + + observer.await(30, TimeUnit.SECONDS); + observer.assertComplete(); + + await().atMost(Duration.FIVE_HUNDRED_MILLISECONDS).untilTrue(requestWasCanceled); + + assertThat(requestWasCanceled.get()).isTrue(); + assertThat(requestDidProduce.get()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void serverCanCancelClientStreamImplicitlyBidi() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + svc.setExplicitCancel(false); + + AtomicBoolean requestWasCanceled = new AtomicBoolean(false); + AtomicBoolean requestDidProduce = new AtomicBoolean(false); + + Flowable request = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .delay(10, TimeUnit.MILLISECONDS) + .map(CancellationPropagationIntegrationTest::protoNum) + .doOnNext(x -> { + requestDidProduce.set(true); + System.out.println("Produced: " + x.getNumber(0)); + }) + .doOnCancel(() -> { + requestWasCanceled.set(true); + System.out.println("Client canceled"); + }); + + TestSubscriber observer = request + .compose(stub::twoWayPressure) + .doOnNext(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .test(); + + observer.await(3, TimeUnit.SECONDS); + assertThat(requestWasCanceled.get()).isTrue(); + assertThat(requestDidProduce.get()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void serverCanCancelClientStreamExplicitlyBidi() throws InterruptedException { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + svc.setExplicitCancel(true); + + AtomicBoolean requestWasCanceled = new AtomicBoolean(false); + AtomicBoolean requestDidProduce = new AtomicBoolean(false); + + Flowable request = Flowable + .fromIterable(IntStream.range(0, NUMBER_OF_STREAM_ELEMENTS)::iterator) + .delay(10, TimeUnit.MILLISECONDS) + .map(CancellationPropagationIntegrationTest::protoNum) + .doOnNext(n -> { + requestDidProduce.set(true); + System.out.println("P: " + n.getNumber(0)); + }) + .doOnCancel(() -> { + requestWasCanceled.set(true); + System.out.println("Client canceled"); + }); + + TestSubscriber observer = request + .compose(stub::twoWayPressure) + .doOnNext(number -> System.out.println(number.getNumber(0))) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .test(); + + observer.await(30, TimeUnit.SECONDS); + assertThat(requestWasCanceled.get()).isTrue(); + assertThat(requestDidProduce.get()).isTrue(); + + errorRule.verifyNoError(); + } + + @Test + public void prematureResponseStreamDisposalShouldNotThrowUnhandledException() throws Exception { + TestService svc = new TestService(); + serverRule.getServiceRegistry().addService(svc); + + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + // slowly process the response stream + Disposable subscription = stub.responsePressure(Empty.getDefaultInstance()).subscribe(n -> { + Thread.sleep(1000); + }); + + subscription.dispose(); + + Thread.sleep(200); + errorRule.verifyNoError(); + } + + private static NumberProto.Number protoNum(int i) { + Integer[] ints = {i}; + return NumberProto.Number.newBuilder().addAllNumber(Arrays.asList(ints)).build(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ChainedCallIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ChainedCallIntegrationTest.java new file mode 100644 index 00000000..4c004542 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ChainedCallIntegrationTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; + +@SuppressWarnings("Duplicates") +public class ChainedCallIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private Server server; + private ManagedChannel channel; + + @Before + public void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(Single rxRequest) { + return rxRequest.map(protoRequest -> response("[" + protoRequest.getName() + "]")); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .flatMapPublisher(name -> Flowable.just( + response("{" + name + "}"), + response("/" + name + "/"), + response("\\" + name + "\\"), + response("(" + name + ")")) + ); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .reduce((l, r) -> l + " :: " + r) + .defaultIfEmpty("EMPTY") + .map(ChainedCallIntegrationTest::response); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .map(name -> "<" + name + ">") + .map(ChainedCallIntegrationTest::response); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @After + public void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void servicesCanCallOtherServices() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + + Single chain = Single.just(request("X")) + // one -> one + .compose(stub::sayHello) + .map(ChainedCallIntegrationTest::bridge) + .doOnSuccess(System.out::println) + // one -> many + .to(stub::sayHelloRespStream) + .map(ChainedCallIntegrationTest::bridge) + .doOnNext(System.out::println) + // many -> many + .compose(stub::sayHelloBothStream) + .map(ChainedCallIntegrationTest::bridge) + .doOnNext(System.out::println) + // many -> one + .to(stub::sayHelloReqStream) + .map(ChainedCallIntegrationTest::bridge) + .doOnSuccess(System.out::println) + // one -> one + .compose(stub::sayHello) + .map(HelloResponse::getMessage) + .doOnSuccess(System.out::println); + + + TestObserver test = chain.test(); + + test.await(2, TimeUnit.SECONDS); + test.assertComplete(); + test.assertValue("[<{[X]}> :: :: <\\[X]\\> :: <([X])>]"); + } + + private static HelloRequest bridge(HelloResponse response) { + return request(response.getMessage()); + } + + private static HelloRequest request(String text) { + return HelloRequest.newBuilder().setName(text).build(); + } + + private static HelloResponse response(String text) { + return HelloResponse.newBuilder().setMessage(text).build(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ClientThreadIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ClientThreadIntegrationTest.java new file mode 100644 index 00000000..f2ff8e8f --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ClientThreadIntegrationTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +/** + * This test verifies that the thread pools passed to gRPC are the same thread pools used by downstream reactive code. + */ +@SuppressWarnings("Duplicates") +public class ClientThreadIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private Server server; + private ManagedChannel channel; + + private AtomicReference serverThreadName = new AtomicReference<>(); + + @Before + public void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(Single rxRequest) { + serverThreadName.set(Thread.currentThread().getName()); + return rxRequest.map(protoRequest -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + serverThreadName.set(Thread.currentThread().getName()); + return rxRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + server = ServerBuilder + .forPort(0) + .addService(svc) + .executor(Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("TheGrpcServer").build())) + .build() + .start(); + channel = ManagedChannelBuilder + .forAddress("localhost", server.getPort()) + .usePlaintext() + .executor(Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("TheGrpcClient").build())) + .build(); + } + + @After + public void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp = req.compose(stub::sayHello); + + AtomicReference clientThreadName = new AtomicReference<>(); + + TestObserver testObserver = resp + .map(HelloResponse::getMessage) + .doOnSuccess(x -> clientThreadName.set(Thread.currentThread().getName())) + .test(); + testObserver.await(3, TimeUnit.SECONDS); + + assertThat(clientThreadName.get()).isEqualTo("TheGrpcClient"); + assertThat(serverThreadName.get()).isEqualTo("TheGrpcServer"); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + AtomicReference clientThreadName = new AtomicReference<>(); + + Flowable resp = req.compose(stub::sayHelloBothStream); + + TestSubscriber testSubscriber = resp + .map(HelloResponse::getMessage) + .doOnNext(x -> clientThreadName.set(Thread.currentThread().getName())) + .test(); + testSubscriber.await(3, TimeUnit.SECONDS); + testSubscriber.assertComplete(); + + assertThat(clientThreadName.get()).isEqualTo("TheGrpcClient"); + assertThat(serverThreadName.get()).isEqualTo("TheGrpcServer"); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ConcurrentRequestIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ConcurrentRequestIntegrationTest.java new file mode 100644 index 00000000..5d785b96 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ConcurrentRequestIntegrationTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("Duplicates") +public class ConcurrentRequestIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(Single rxRequest) { + return rxRequest + .doOnSuccess(System.out::println) + .map(protoRequest -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest + .doOnSuccess(System.out::println) + .flatMapPublisher(protoRequest -> Flowable.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest))); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .doOnNext(System.out::println) + .map(HelloRequest::getName) + .toList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .doOnNext(System.out::println) + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void fourKindsOfRequestAtOnce() throws Exception { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + + // == MAKE REQUESTS == + // One to One + Single req1 = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp1 = req1.compose(stub::sayHello); + + // One to Many + Single req2 = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Flowable resp2 = req2.to(stub::sayHelloRespStream); + + // Many to One + Flowable req3 = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Single resp3 = req3.to(stub::sayHelloReqStream); + + // Many to Many + Flowable req4 = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + Flowable resp4 = req4.compose(stub::sayHelloBothStream); + + // == VERIFY RESPONSES == + ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + + // Run all four verifications in parallel + try { + // One to One + ListenableFuture oneToOne = executorService.submit(() -> { + TestObserver testObserver1 = resp1.map(HelloResponse::getMessage).test(); + testObserver1.await(1, TimeUnit.SECONDS); + testObserver1.assertValue("Hello rxjava"); + return true; + }); + + // One to Many + ListenableFuture oneToMany = executorService.submit(() -> { + TestSubscriber testSubscriber1 = resp2.map(HelloResponse::getMessage).test(); + testSubscriber1.await(1, TimeUnit.SECONDS); + testSubscriber1.assertValues("Hello rxjava", "Hi rxjava", "Greetings rxjava"); + return true; + }); + + // Many to One + ListenableFuture manyToOne = executorService.submit(() -> { + TestObserver testObserver2 = resp3.map(HelloResponse::getMessage).test(); + testObserver2.await(1, TimeUnit.SECONDS); + testObserver2.assertValue("Hello a and b and c"); + return true; + }); + + // Many to Many + ListenableFuture manyToMany = executorService.submit(() -> { + TestSubscriber testSubscriber2 = resp4.map(HelloResponse::getMessage).test(); + testSubscriber2.await(1, TimeUnit.SECONDS); + testSubscriber2.assertValues("Hello a and b", "Hello c and d", "Hello e"); + testSubscriber2.assertComplete(); + return true; + }); + + @SuppressWarnings("unchecked") + ListenableFuture> allFutures = Futures.allAsList(Lists.newArrayList(oneToOne, oneToMany, manyToOne, manyToMany)); + // Block for response + List results = allFutures.get(3, TimeUnit.SECONDS); + assertThat(results).containsExactly(true, true, true, true); + + } finally { + executorService.shutdown(); + } + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ContextPropagationIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ContextPropagationIntegrationTest.java new file mode 100644 index 00000000..ab15f1c6 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ContextPropagationIntegrationTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; + +@SuppressWarnings("Duplicates") +public class ContextPropagationIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + private static Context.Key ctxKey = Context.key("ctxKey"); + private static Single worldReq = Single.just(HelloRequest.newBuilder().setName("World").build()); + + private static TestService svc = new TestService(); + private static TestClientInterceptor clientInterceptor = new TestClientInterceptor(); + private static TestServerInterceptor serverInterceptor = new TestServerInterceptor(); + + private static class TestClientInterceptor implements ClientInterceptor { + private String sendMessageCtxValue; + + public void reset() { + sendMessageCtxValue = null; + } + + public String getSendMessageCtxValue() { + return sendMessageCtxValue; + } + + @Override + public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener){ + @Override + public void onMessage(RespT message) { + Context.current().withValue(ctxKey, "ClientGetsContext").run(() -> super.onMessage(message)); + } + }, headers); + } + + @Override + public void sendMessage(ReqT message) { + sendMessageCtxValue = ctxKey.get(); + super.sendMessage(message); + } + }; + } + } + + private static class TestServerInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + Context ctx = Context.current().withValue(ctxKey, "ServerAcceptsContext"); + return Contexts.interceptCall(ctx, call, headers, next); + } + } + + private static class TestService extends Rx3GreeterGrpc.GreeterImplBase { + private String receivedCtxValue; + + public String getReceivedCtxValue() { + return receivedCtxValue; + } + + private void reset() { + receivedCtxValue = null; + } + + @Override + public Single sayHello(Single request) { + return request + .doOnSuccess(x -> receivedCtxValue = ctxKey.get()) + .map(HelloRequest::getName) + .map(name -> HelloResponse.newBuilder().setMessage("Hello " + name).build()); + } + } + + @BeforeClass + public static void setupServer() throws Exception { + server = ServerBuilder.forPort(9000).addService(ServerInterceptors.intercept(svc, serverInterceptor)).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().intercept(clientInterceptor).build(); + } + + @Before + public void resetServerStats() { + svc.reset(); + clientInterceptor.reset(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void ClientSendsContext() { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Context.current() + .withValue(ctxKey, "ClientSendsContext") + .run(() -> { + try { + worldReq.compose(stub::sayHello).test().await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + + assertThat(clientInterceptor.getSendMessageCtxValue()).isEqualTo("ClientSendsContext"); + } + + @Test + public void ClientGetsContext() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + + TestObserver testObserver = worldReq + .compose(stub::sayHello) + .doOnSuccess(resp -> { + Context ctx = Context.current(); + assertThat(ctxKey.get(ctx)).isEqualTo("ClientGetsContext"); + }) + .test(); + + testObserver.await(1, TimeUnit.SECONDS); + testObserver.assertComplete(); + } + + @Test + public void ServerAcceptsContext() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + + worldReq.compose(stub::sayHello).test().await(1, TimeUnit.SECONDS); + + assertThat(svc.getReceivedCtxValue()).isEqualTo("ServerAcceptsContext"); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/DoNotCallUntilSubscribeIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/DoNotCallUntilSubscribeIntegrationTest.java new file mode 100644 index 00000000..00b3f6c4 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/DoNotCallUntilSubscribeIntegrationTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import io.grpc.*; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import org.junit.*; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("Duplicates") +public class DoNotCallUntilSubscribeIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private Server server; + private ManagedChannel channel; + private WasCalledInterceptor interceptor; + + private static class WasCalledInterceptor implements ServerInterceptor { + private boolean wasCalled = false; + private boolean didRespond = false; + + public boolean wasCalled() { + return wasCalled; + } + + public boolean didRespond() { + return didRespond; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener( + next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void sendMessage(RespT message) { + didRespond = true; + super.sendMessage(message); + } + }, headers)) { + @Override + public void onMessage(ReqT message) { + wasCalled = true; + super.onMessage(message); + } + }; + } + } + + @Before + public void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(HelloRequest protoRequest) { + return Single.fromCallable(() -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloRespStream(HelloRequest protoRequest) { + return Flowable.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest)); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .toList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + interceptor = new WasCalledInterceptor(); + server = ServerBuilder.forPort(9000).addService(svc).intercept(interceptor).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @After + public void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp = req.compose(stub::sayHello); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Flowable resp = req.to(stub::sayHelloRespStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Single resp = req.to(stub::sayHelloReqStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + Flowable resp = req.compose(stub::sayHelloBothStream); + + Thread.sleep(100); + assertThat(interceptor.wasCalled()).isFalse(); + assertThat(interceptor.didRespond()).isFalse(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/EndToEndIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/EndToEndIntegrationTest.java new file mode 100644 index 00000000..b39a795f --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/EndToEndIntegrationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("Duplicates") +public class EndToEndIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(HelloRequest protoRequest) { + return Single.fromCallable(() -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloRespStream(HelloRequest protoRequest) { + return Flowable.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest)); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .toList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp = req.compose(stub::sayHello); + + TestObserver testObserver = resp.map(HelloResponse::getMessage).test(); + testObserver.await(3, TimeUnit.SECONDS); + testObserver.assertValue("Hello rxjava"); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Flowable resp = req.to(stub::sayHelloRespStream); + + TestSubscriber testSubscriber = resp.map(HelloResponse::getMessage).test(); + testSubscriber.await(3, TimeUnit.SECONDS); + testSubscriber.assertValues("Hello rxjava", "Hi rxjava", "Greetings rxjava"); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Single resp = req.to(stub::sayHelloReqStream); + + TestObserver testObserver = resp.map(HelloResponse::getMessage).test(); + testObserver.await(3, TimeUnit.SECONDS); + testObserver.assertValue("Hello a and b and c"); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build(), + HelloRequest.newBuilder().setName("d").build(), + HelloRequest.newBuilder().setName("e").build()); + + Flowable resp = req.compose(stub::sayHelloBothStream); + + TestSubscriber testSubscriber = resp.map(HelloResponse::getMessage).test(); + testSubscriber.await(3, TimeUnit.SECONDS); + testSubscriber.assertValues("Hello a and b", "Hello c and d", "Hello e"); + testSubscriber.assertComplete(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/JvmFatalServerErrorIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/JvmFatalServerErrorIntegrationTest.java new file mode 100644 index 00000000..0ac9d53b --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/JvmFatalServerErrorIntegrationTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import io.grpc.*; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unchecked") +public class JvmFatalServerErrorIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Single sayHello(Single rxRequest) { + return rxRequest.map(this::kaboom); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest.map(this::kaboom).toFlowable(); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom).firstOrError(); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom); + } + + private HelloResponse kaboom(HelloRequest request) { + throw new NoSuchMethodError("Fatal!"); + } + + @Override + protected Throwable onErrorMap(Throwable throwable) { + if (throwable instanceof NoSuchMethodError) { + return Status.INTERNAL.withDescription("NoSuchMethod:" + throwable.getMessage()).asRuntimeException(); + } + return super.onErrorMap(throwable); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() { + server.shutdown(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single resp = Single.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable resp = Single.just(HelloRequest.getDefaultInstance()).to(stub::sayHelloRespStream); + TestSubscriber test = resp + .doOnNext(System.out::println) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Single resp = req.to(stub::sayHelloReqStream); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + // Flowable requests get canceled when unexpected errors happen + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Flowable resp = req.compose(stub::sayHelloBothStream); + TestSubscriber test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.Code.INTERNAL); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ReactiveClientStandardServerInteropTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ReactiveClientStandardServerInteropTest.java new file mode 100644 index 00000000..79c8fa68 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ReactiveClientStandardServerInteropTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("Duplicates") +public class ReactiveClientStandardServerInteropTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + GreeterGrpc.GreeterImplBase svc = new GreeterGrpc.GreeterImplBase() { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onCompleted(); + } + + @Override + public void sayHelloRespStream(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Hi " + request.getName()).build()); + responseObserver.onNext(HelloResponse.newBuilder().setMessage("Greetings " + request.getName()).build()); + responseObserver.onCompleted(); + } + + @Override + public StreamObserver sayHelloReqStream(StreamObserver responseObserver) { + return new StreamObserver() { + List names = new ArrayList<>(); + + @Override + public void onNext(HelloRequest request) { + names.add(request.getName()); + } + + @Override + public void onError(Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + String message = "Hello " + String.join(" and ", names); + responseObserver.onNext(HelloResponse.newBuilder().setMessage(message).build()); + responseObserver.onCompleted(); + } + }; + } + + @Override + public StreamObserver sayHelloBothStream(StreamObserver responseObserver) { + return new StreamObserver() { + List names = new ArrayList<>(); + + @Override + public void onNext(HelloRequest request) { + names.add(request.getName()); + } + + @Override + public void onError(Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + // Will fail for odd number of names, but that's not what is being tested, so ¯\_(ツ)_/¯ + for (int i = 0; i < names.size(); i += 2) { + String message = "Hello " + names.get(i) + " and " + names.get(i+1); + responseObserver.onNext(HelloResponse.newBuilder().setMessage(message).build()); + } + responseObserver.onCompleted(); + } + }; + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single rxRequest = Single.just("World"); + Single rxResponse = rxRequest + .map(this::toRequest) + .compose(stub::sayHello) + .map(this::fromResponse); + + TestObserver test = rxResponse.test(); + test.await(1, TimeUnit.SECONDS); + + test.assertNoErrors(); + test.assertValue("Hello World"); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single rxRequest = Single.just("World"); + Flowable rxResponse = rxRequest + .map(this::toRequest) + .to(stub::sayHelloRespStream) + .map(this::fromResponse); + + TestSubscriber test = rxResponse.test(); + test.await(1, TimeUnit.SECONDS); + + test.assertNoErrors(); + test.assertValues("Hello World", "Hi World", "Greetings World"); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable rxRequest = Flowable.just("A", "B", "C"); + Single rxResponse = rxRequest + .map(this::toRequest) + .to(stub::sayHelloReqStream) + .map(this::fromResponse); + + TestObserver test = rxResponse.test(); + test.await(1, TimeUnit.SECONDS); + + test.assertNoErrors(); + test.assertValue("Hello A and B and C"); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable rxRequest = Flowable.just("A", "B", "C", "D"); + Flowable rxResponse = rxRequest + .map(this::toRequest) + .compose(stub::sayHelloBothStream) + .map(this::fromResponse); + + TestSubscriber test = rxResponse.test(); + test.await(1, TimeUnit.SECONDS); + + test.assertNoErrors(); + test.assertValues("Hello A and B", "Hello C and D"); + } + + private HelloRequest toRequest(String name) { + return HelloRequest.newBuilder().setName(name).build(); + } + + private String fromResponse(HelloResponse response) { + return response.getMessage(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorIntegrationTest.java new file mode 100644 index 00000000..fe19e6da --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorIntegrationTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("unchecked") +public class ServerErrorIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Single sayHello(Single rxRequest) { + return Single.error(new StatusRuntimeException(Status.INTERNAL)); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return Flowable.error(new StatusRuntimeException(Status.INTERNAL)); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return Single.error(new StatusRuntimeException(Status.INTERNAL)); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return Flowable.error(new StatusRuntimeException(Status.INTERNAL)); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() { + server.shutdown(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single resp = Single.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus() == Status.INTERNAL); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable resp = Single.just(HelloRequest.getDefaultInstance()).to(stub::sayHelloRespStream); + TestSubscriber test = resp + .doOnNext(System.out::println) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus() == Status.INTERNAL); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single resp = Flowable.just(HelloRequest.getDefaultInstance()).to(stub::sayHelloReqStream); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus() == Status.INTERNAL); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable resp = Flowable.just(HelloRequest.getDefaultInstance()).compose(stub::sayHelloBothStream); + TestSubscriber test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus() == Status.INTERNAL); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorUpstreamCancellationIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorUpstreamCancellationIntegrationTest.java new file mode 100644 index 00000000..7c4dfb26 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ServerErrorUpstreamCancellationIntegrationTest.java @@ -0,0 +1,105 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Rule; +import org.junit.Test; + +import com.google.protobuf.Empty; +import com.salesforce.grpc.testing.contrib.NettyGrpcServerRule; +import com.salesforce.servicelibs.NumberProto; +import com.salesforce.servicelibs.NumberProto.Number; +import com.salesforce.servicelibs.Rx3NumbersGrpc; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class ServerErrorUpstreamCancellationIntegrationTest { + @Rule + public NettyGrpcServerRule serverRule = new NettyGrpcServerRule(); + + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static class ExplodeAfterFiveService extends Rx3NumbersGrpc.NumbersImplBase { + @Override + public Flowable twoWayPressure(Flowable request) { + return request.map(x -> kaboom()); + } + + @Override + public Single requestPressure(Flowable request) { + return request.map(x -> kaboom()).firstOrError(); + } + + @Override + public Flowable responsePressure(Single request) { + return request.map(x -> kaboom()).toFlowable(); + } + + private NumberProto.Number kaboom() { + throw Status.FAILED_PRECONDITION.asRuntimeException(); + } + } + + @Test + public void serverErrorSignalsUpstreamCancellationManyToOne() throws InterruptedException { + serverRule.getServiceRegistry().addService(new ExplodeAfterFiveService()); + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + AtomicBoolean upstreamCancel = new AtomicBoolean(false); + AtomicReference throwable = new AtomicReference<>(); + + TestObserver observer = Flowable.range(0, Integer.MAX_VALUE) + .map(this::protoNum) + .doOnCancel(() -> upstreamCancel.set(true)) + .to(stub::requestPressure) + .doOnError(throwable::set) + .doOnSuccess(i -> System.out.println(i.getNumber(0))) + .test(); + + observer.await(3, TimeUnit.SECONDS); + observer.assertError(StatusRuntimeException.class); + assertThat(upstreamCancel.get()).isTrue(); + assertThat(((StatusRuntimeException) throwable.get()).getStatus()).isEqualTo(Status.FAILED_PRECONDITION); + } + + @Test + public void serverErrorSignalsUpstreamCancellationBidi() throws InterruptedException { + serverRule.getServiceRegistry().addService(new ExplodeAfterFiveService()); + Rx3NumbersGrpc.RxNumbersStub stub = Rx3NumbersGrpc.newRxStub(serverRule.getChannel()); + + AtomicBoolean upstreamCancel = new AtomicBoolean(false); + + TestSubscriber subscriber = Flowable.range(0, Integer.MAX_VALUE) + .map(this::protoNum) + .doOnCancel(() -> upstreamCancel.set(true)) + .compose(stub::twoWayPressure) + .doOnNext(i -> System.out.println(i.getNumber(0))) + .test(); + + subscriber.await(3, TimeUnit.SECONDS); + subscriber.assertError(StatusRuntimeException.class); + assertThat(upstreamCancel.get()).isTrue(); + } + + private NumberProto.Number protoNum(int i) { + Integer[] ints = {i}; + return NumberProto.Number.newBuilder().addAllNumber(Arrays.asList(ints)).build(); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ShareIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ShareIntegrationTest.java new file mode 100644 index 00000000..5edee89b --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/ShareIntegrationTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Rule; +import org.junit.Test; + +import com.google.common.collect.Lists; +import com.salesforce.grpc.testing.contrib.NettyGrpcServerRule; + +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; + +/** + * Test to demonstrate splitting the output of an RxGrpc stream in RxJava. + * See: https://github.com/salesforce/reactive-grpc/issues/131 + */ +public class ShareIntegrationTest { + @Rule + public NettyGrpcServerRule serverRule = new NettyGrpcServerRule(); + + @Test + public void serverPublishShouldWork() throws InterruptedException { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + // a function that can use the multicasted source sequence as many times as needed, without causing + // multiple subscriptions to the source sequence. Subscribers to the given source will receive all + // notifications of the source from the time of the subscription forward. + .publish(shared -> { + Single first = shared.firstOrError(); + Flowable rest = shared.skip(0); + return first + .flatMap(firstVal -> rest + .map(HelloRequest::getName) + .toList() + .map(names -> { + ArrayList strings = Lists.newArrayList(firstVal.getName()); + strings.addAll(names); + Thread.sleep(1000); + return HelloResponse.newBuilder().setMessage("Hello " + String.join(" and ", strings)).build(); + } + ).doOnError(System.out::println)) + .toFlowable(); + }) + .singleOrError(); + } + }; + + serverRule.getServiceRegistry().addService(svc); + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + + TestObserver resp = Flowable.just("Alpha", "Bravo", "Charlie") + .map(s -> HelloRequest.newBuilder().setName(s).build()) + .to(stub::sayHelloReqStream) + .map(HelloResponse::getMessage) + .test(); + + resp.await(5, TimeUnit.SECONDS); + resp.assertComplete(); + resp.assertValue("Hello Alpha and Bravo and Charlie"); + } + + @Test + public void clientPublishShouldWork() throws InterruptedException { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Flowable sayHelloRespStream(Single request) { + return request.flatMapObservable(x -> Observable.just("Alpha", "Bravo", "Charlie")) + .map(name -> HelloResponse.newBuilder().setMessage(name).build()) + .toFlowable(BackpressureStrategy.BUFFER); + } + }; + + serverRule.getServiceRegistry().addService(svc); + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + + TestObserver resp = stub.sayHelloRespStream(HelloRequest.getDefaultInstance()) + // a function that can use the multicasted source sequence as many times as needed, without causing + // multiple subscriptions to the source sequence. Subscribers to the given source will receive all + // notifications of the source from the time of the subscription forward. + .publish(shared -> { + Single first = shared.firstOrError(); + Flowable rest = shared.skip(0); + return first + .flatMap(firstVal -> rest + .map(HelloResponse::getMessage) + .toList() + .map(names -> { + ArrayList strings = Lists.newArrayList(firstVal.getMessage()); + strings.addAll(names); + Thread.sleep(1000); + return HelloResponse.newBuilder().setMessage("Hello " + String.join(" and ", strings)).build(); + }) + .doOnError(System.out::println) + ) + .map(HelloResponse::getMessage) + .toFlowable(); + }) + .singleOrError() + .test(); + + resp.await(5, TimeUnit.SECONDS); + resp.assertComplete(); + resp.assertValue("Hello Alpha and Bravo and Charlie"); + } + + @Test + public void serverShareShouldWork() throws InterruptedException { + AtomicReference other = new AtomicReference<>(); + + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Single sayHelloReqStream(Flowable request) { + Flowable share = request.share(); + + // Let's make a side effect in a different stream! + share + .map(HelloRequest::getName) + .reduce("", (l, r) -> l + "+" + r) + .subscribe(other::set); + + return share + .map(HelloRequest::getName) + .reduce("", (l, r) -> l + "&" + r) + .map(m -> HelloResponse.newBuilder().setMessage(m).build()); + } + }; + + serverRule.getServiceRegistry().addService(svc); + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + + TestObserver resp = Flowable.just("Alpha", "Bravo", "Charlie") + .map(n -> HelloRequest.newBuilder().setName(n).build()) + .to(stub::sayHelloReqStream) + .map(HelloResponse::getMessage) + .test(); + + resp.await(1, TimeUnit.SECONDS); + resp.assertComplete(); + resp.assertValue("&Alpha&Bravo&Charlie"); + + assertThat(other.get()).isEqualTo("+Alpha+Bravo+Charlie"); + } + + @Test + public void clientShareShouldWork() throws InterruptedException { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Flowable sayHelloRespStream(Single request) { + return request + // Always return Alpha, Bravo, Charlie + .flatMapPublisher(x -> { + System.out.println("Flatten : " + x); + return Flowable.just("Alpha", "Bravo", "Charlie"); + }) + .map(n -> HelloResponse.newBuilder().setMessage(n).build()); + } + }; + + serverRule.getServiceRegistry().addService(svc); + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + + Flowable share = Single.just(HelloRequest.getDefaultInstance()) + .to(stub::sayHelloRespStream) + .share(); + + // Split the response stream! + TestObserver resp1 = share + .map(HelloResponse::getMessage) + .reduce("", (l, r) -> l + "+" + r) + .test(); + + TestObserver resp2 = share + .map(HelloResponse::getMessage) + .reduce("", (l, r) -> l + "&" + r) + .test(); + + resp1.await(1, TimeUnit.SECONDS); + resp1.assertComplete(); + resp1.assertValue("+Alpha+Bravo+Charlie"); + + resp2.await(1, TimeUnit.SECONDS); + resp2.assertComplete(); + resp2.assertValue("&Alpha&Bravo&Charlie"); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/StandardClientReactiveServerInteropTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/StandardClientReactiveServerInteropTest.java new file mode 100644 index 00000000..0895f3eb --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/StandardClientReactiveServerInteropTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import com.salesforce.grpc.contrib.LambdaStreamObserver; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; + +@SuppressWarnings("Duplicates") +public class StandardClientReactiveServerInteropTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + + @Override + public Single sayHello(Single rxRequest) { + return rxRequest.map(protoRequest -> greet("Hello", protoRequest)); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest.flatMapPublisher(protoRequest -> Flowable.just( + greet("Hello", protoRequest), + greet("Hi", protoRequest), + greet("Greetings", protoRequest))); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .toList() + .map(names -> greet("Hello", String.join(" and ", names))); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest + .map(HelloRequest::getName) + .buffer(2) + .map(names -> greet("Hello", String.join(" and ", names))); + } + + private HelloResponse greet(String greeting, HelloRequest request) { + return greet(greeting, request.getName()); + } + + private HelloResponse greet(String greeting, String name) { + return HelloResponse.newBuilder().setMessage(greeting + " " + name).build(); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() { + AtomicBoolean called = new AtomicBoolean(false); + GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); + + HelloRequest request = HelloRequest.newBuilder().setName("World").build(); + stub.sayHello(request, new LambdaStreamObserver<>( + response -> { + assertThat(response.getMessage()).isEqualTo("Hello World"); + called.set(true); + } + )); + + await().atMost(1, TimeUnit.SECONDS).untilTrue(called); + } + + @Test + public void oneToMany() { + AtomicInteger called = new AtomicInteger(0); + GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); + + HelloRequest request = HelloRequest.newBuilder().setName("World").build(); + stub.sayHelloRespStream(request, new LambdaStreamObserver<>( + response -> { + assertThat(response.getMessage()).isIn("Hello World", "Hi World", "Greetings World"); + called.incrementAndGet(); + } + )); + + await().atMost(1, TimeUnit.SECONDS).untilAtomic(called, equalTo(3)); + } + + @Test + public void manyToOne() { + AtomicBoolean called = new AtomicBoolean(false); + GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); + + StreamObserver requestStream = stub.sayHelloReqStream(new LambdaStreamObserver<>( + response -> { + assertThat(response.getMessage()).isEqualTo("Hello A and B and C"); + called.set(true); + } + )); + + requestStream.onNext(HelloRequest.newBuilder().setName("A").build()); + requestStream.onNext(HelloRequest.newBuilder().setName("B").build()); + requestStream.onNext(HelloRequest.newBuilder().setName("C").build()); + requestStream.onCompleted(); + + await().atMost(1, TimeUnit.SECONDS).untilTrue(called); + } + + @Test + public void manyToMany() { + AtomicInteger called = new AtomicInteger(0); + GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); + + StreamObserver requestStream = stub.sayHelloBothStream(new LambdaStreamObserver<>( + response -> { + assertThat(response.getMessage()).isIn("Hello A and B", "Hello C and D"); + called.incrementAndGet(); + } + )); + + requestStream.onNext(HelloRequest.newBuilder().setName("A").build()); + requestStream.onNext(HelloRequest.newBuilder().setName("B").build()); + requestStream.onNext(HelloRequest.newBuilder().setName("C").build()); + requestStream.onNext(HelloRequest.newBuilder().setName("D").build()); + requestStream.onCompleted(); + + await().atMost(1, TimeUnit.SECONDS).untilAtomic(called, equalTo(2)); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnaryZeroMessageResponseIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnaryZeroMessageResponseIntegrationTest.java new file mode 100644 index 00000000..920a245b --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnaryZeroMessageResponseIntegrationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.junit.Rule; +import org.junit.Test; + +import com.salesforce.grpc.testing.contrib.NettyGrpcServerRule; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; + +@SuppressWarnings("Duplicates") +public class UnaryZeroMessageResponseIntegrationTest { + @Rule + public NettyGrpcServerRule serverRule = new NettyGrpcServerRule(); + + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static class MissingUnaryResponseService extends GreeterGrpc.GreeterImplBase { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onCompleted(); + } + + @Override + public StreamObserver sayHelloReqStream(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(HelloRequest helloRequest) { + responseObserver.onCompleted(); + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onCompleted() { + + } + }; + } + } + + @Test + public void zeroMessageResponseOneToOne() throws InterruptedException { + serverRule.getServiceRegistry().addService(new MissingUnaryResponseService()); + + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + Single req = Single.just(HelloRequest.newBuilder().setName("rxjava").build()); + Single resp = req.compose(stub::sayHello); + + TestObserver testObserver = resp.map(HelloResponse::getMessage).test(); + testObserver.await(3, TimeUnit.SECONDS); + testObserver.assertError(StatusRuntimeException.class); + testObserver.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.CANCELLED.getCode()); + } + + @Test + public void zeroMessageResponseManyToOne() throws InterruptedException { + serverRule.getServiceRegistry().addService(new MissingUnaryResponseService()); + + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(serverRule.getChannel()); + Flowable req = Flowable.just( + HelloRequest.newBuilder().setName("a").build(), + HelloRequest.newBuilder().setName("b").build(), + HelloRequest.newBuilder().setName("c").build()); + + Single resp = req.to(stub::sayHelloReqStream); + + TestObserver testObserver = resp.map(HelloResponse::getMessage).test(); + testObserver.await(3, TimeUnit.SECONDS); + testObserver.assertError(StatusRuntimeException.class); + testObserver.assertError(t -> ((StatusRuntimeException) t).getStatus().getCode() == Status.CANCELLED.getCode()); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnexpectedServerErrorIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnexpectedServerErrorIntegrationTest.java new file mode 100644 index 00000000..929f9ef3 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnexpectedServerErrorIntegrationTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +@SuppressWarnings("unchecked") +public class UnexpectedServerErrorIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + @Override + public Single sayHello(Single rxRequest) { + return rxRequest.map(this::kaboom); + } + + @Override + public Flowable sayHelloRespStream(Single rxRequest) { + return rxRequest.map(this::kaboom).toFlowable(); + } + + @Override + public Single sayHelloReqStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom).firstOrError(); + } + + @Override + public Flowable sayHelloBothStream(Flowable rxRequest) { + return rxRequest.map(this::kaboom); + } + + private HelloResponse kaboom(HelloRequest request) throws Exception{ + throw Status.INTERNAL.withDescription("Kaboom!").asException(); + } + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() { + server.shutdown(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void oneToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Single resp = Single.just(HelloRequest.getDefaultInstance()).compose(stub::sayHello); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void oneToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable resp = Single.just(HelloRequest.getDefaultInstance()).to(stub::sayHelloRespStream); + TestSubscriber test = resp + .doOnNext(System.out::println) + .doOnError(throwable -> System.out.println(throwable.getMessage())) + .doOnComplete(() -> System.out.println("Completed")) + .doOnCancel(() -> System.out.println("Client canceled")) + .test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToOne() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Single resp = req.to(stub::sayHelloReqStream); + TestObserver test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + // Flowable requests get canceled when unexpected errors happen + test.assertError(t -> ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); + } + + @Test + public void manyToMany() throws InterruptedException { + Rx3GreeterGrpc.RxGreeterStub stub = Rx3GreeterGrpc.newRxStub(channel); + Flowable req = Flowable.just(HelloRequest.getDefaultInstance()); + Flowable resp = req.compose(stub::sayHelloBothStream); + TestSubscriber test = resp.test(); + + test.await(3, TimeUnit.SECONDS); + test.assertError(t -> t instanceof StatusRuntimeException); + test.assertError(t -> ((StatusRuntimeException)t).getStatus().getCode() == Status.Code.INTERNAL); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnhandledRxJavaErrorRule.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnhandledRxJavaErrorRule.java new file mode 100644 index 00000000..0dc62d15 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnhandledRxJavaErrorRule.java @@ -0,0 +1,53 @@ +/* Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import java.util.function.Predicate; + +import org.junit.Assert; +import org.junit.rules.ExternalResource; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * {@code UnhandledRxJavaErrorRule} is a JUnit rule that captures unhandled RxJava exceptions.` + */ +public class UnhandledRxJavaErrorRule extends ExternalResource { + private Throwable unhandledThrowable; + private boolean autoverify; + + @Override + protected void before() throws Throwable { + RxJavaPlugins.setErrorHandler(throwable -> unhandledThrowable = throwable); + } + + @Override + protected void after() { + RxJavaPlugins.setErrorHandler(null); + if (autoverify) { + verifyNoError(); + } + } + + public UnhandledRxJavaErrorRule autoVerifyNoError() { + autoverify = true; + return this; + } + + public void verifyNoError() { + if (unhandledThrowable != null) { + unhandledThrowable.printStackTrace(); + Assert.fail("Unhandled RxJava error\n" + unhandledThrowable.toString()); + } + } + + public void verify(Predicate test) { + if (! test.test(unhandledThrowable)) { + Assert.fail("Unhandled RxJava error was not as expected"); + } + } +} diff --git a/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnimplementedMethodIntegrationTest.java b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnimplementedMethodIntegrationTest.java new file mode 100644 index 00000000..b8a35a47 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/java/com/salesforce/rx3grpc/UnimplementedMethodIntegrationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.StatusRuntimeException; + +@SuppressWarnings("Duplicates") +public class UnimplementedMethodIntegrationTest { + @Rule + public UnhandledRxJavaErrorRule errorRule = new UnhandledRxJavaErrorRule().autoVerifyNoError(); + + private static Server server; + private static ManagedChannel channel; + + @BeforeClass + public static void setupServer() throws Exception { + Rx3GreeterGrpc.GreeterImplBase svc = new Rx3GreeterGrpc.GreeterImplBase() { + // Don't implement anything + }; + + server = ServerBuilder.forPort(9000).addService(svc).build().start(); + channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + channel.shutdown(); + + server = null; + channel = null; + } + + @Test + public void unimplementedMethodShouldFail() { + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel); + + assertThatThrownBy(() -> stub.sayHello(HelloRequest.newBuilder().setName("World").build())) + .isInstanceOf(StatusRuntimeException.class) + .hasMessageContaining("UNIMPLEMENTED"); + } +} diff --git a/rx3-java/rx3grpc-test/src/test/proto/backpressure.proto b/rx3-java/rx3grpc-test/src/test/proto/backpressure.proto new file mode 100644 index 00000000..50978dba --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/backpressure.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package com.salesforce.servicelibs; + +option java_package = "com.salesforce.servicelibs"; +option java_outer_classname = "NumberProto"; + +import "google/protobuf/empty.proto"; + +service Numbers { + rpc RequestPressure (stream Number) returns (Number) {} + rpc ResponsePressure (google.protobuf.Empty) returns (stream Number) {} + rpc TwoWayPressure (stream Number) returns (stream Number) {} + rpc TwoWayRequestPressure (stream Number) returns (stream Number) {} + rpc TwoWayResponsePressure (stream Number) returns (stream Number) {} +} + +message Number { + repeated int32 number = 1; +} diff --git a/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/frontend.proto b/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/frontend.proto new file mode 100644 index 00000000..efc6cf1c --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/frontend.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package com.example.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service Frontend { + rpc Heartbeat(HeartbeatRequest) returns (google.protobuf.Empty); +} +message HeartbeatRequest { + google.protobuf.Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto b/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto new file mode 100644 index 00000000..e5601d3f --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/com/example/v1/settingsgetclassic.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package com.example.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service Settings { + rpc SettingsGet_Classic_1 (google.protobuf.Empty) returns (Settings_Classic_1); + rpc SettingsGetClassic2 (google.protobuf.Empty) returns (Settings_Classic_1); +} + +message Settings_Classic_1 { + google.protobuf.Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/helloworld.proto b/rx3-java/rx3grpc-test/src/test/proto/helloworld.proto new file mode 100644 index 00000000..8b6cf0df --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/helloworld.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package helloworld; + +option java_multiple_files = true; +option java_package = "com.salesforce.rx3grpc"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloResponse) {} + rpc SayHelloRespStream (HelloRequest) returns (stream HelloResponse) {} + rpc SayHelloReqStream (stream HelloRequest) returns (HelloResponse) {} + rpc SayHelloBothStream (stream HelloRequest) returns (stream HelloResponse) {} +} + +service Dismisser { + rpc SayGoodbye (HelloRequest) returns (HelloResponse) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloResponse { + string message = 1; +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/nested.proto b/rx3-java/rx3grpc-test/src/test/proto/nested.proto new file mode 100644 index 00000000..c655fa9e --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/nested.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package nested; + +message Outer { // Level 0 + enum FooEnum { + FOO = 0; + BAR = 1; + CHEESE = 2; + } + message MiddleAA { // Level 1 + + message Inner { // Level 2 + int64 ival = 1; + bool booly = 2; + Outer.FooEnum enum = 3; + } + } + message MiddleBB { // Level 1 + message Inner { // Level 2 + int32 ival = 1; + bool booly = 2; + Outer.FooEnum enum = 3; + } + } +} + +service Nested { + rpc doNested (Outer.MiddleAA.Inner) returns (Outer.MiddleBB.Inner) {} +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/nested_enum_same_name.proto b/rx3-java/rx3grpc-test/src/test/proto/nested_enum_same_name.proto new file mode 100644 index 00000000..db41a4d5 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/nested_enum_same_name.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package nested_overlap; + +option java_multiple_files = true; + +message Outer { // Level 0 + enum Foo { + FOO = 0; + BAR = 1; + CHEESE = 2; + } + Outer.Foo e = 1; +} + +message Foo { + string bar = 1; +} + +service Nested { + rpc doNested (Foo) returns (Outer) {} +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/some_parameter.proto b/rx3-java/rx3grpc-test/src/test/proto/some_parameter.proto new file mode 100644 index 00000000..2a639487 --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/some_parameter.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +// Test file for https://github.com/salesforce/reactive-grpc/issues/26 +// If this proto compiles, then the test passes. + +package my.someparameters; + +message SomeParameter { + string id = 1; +} + +service WithParameter { + rpc SayGoodbye (SomeParameter) returns (SomeParameter) {} +} \ No newline at end of file diff --git a/rx3-java/rx3grpc-test/src/test/proto/weyland-yutani.proto b/rx3-java/rx3grpc-test/src/test/proto/weyland-yutani.proto new file mode 100644 index 00000000..ba1451ca --- /dev/null +++ b/rx3-java/rx3grpc-test/src/test/proto/weyland-yutani.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package com.salesforce.invalid.dash; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +// The time - as a service. +service CurrentTime { + // Sends the current time + rpc SayTime (google.protobuf.Empty) returns (TimeResponse) {} +} + +// The response message containing the time +message TimeResponse { + google.protobuf.Timestamp time = 1; +} \ No newline at end of file diff --git a/rx3-java/rx3grpc/BUILD.bazel b/rx3-java/rx3grpc/BUILD.bazel new file mode 100644 index 00000000..cefb4495 --- /dev/null +++ b/rx3-java/rx3grpc/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "rxgrpc", + srcs = glob(["src/main/**/*.java"]), + resources = ["src/main/resources/RxStub.mustache"], + deps = [ + "//common/reactive-grpc-gencommon", + "@com_salesforce_servicelibs_jprotoc", + ], +) + +java_binary( + name = "rxgrpc_bin", + main_class = "com.salesforce.rx3grpc.RxGrpcGenerator", + visibility = ["//visibility:public"], + runtime_deps = [":rxgrpc"], +) diff --git a/rx3-java/rx3grpc/README.md b/rx3-java/rx3grpc/README.md new file mode 100644 index 00000000..6241e894 --- /dev/null +++ b/rx3-java/rx3grpc/README.md @@ -0,0 +1,53 @@ +[![Javadocs](https://javadoc.io/badge/com.salesforce.servicelibs/rxgrpc-stub.svg)](https://javadoc.io/doc/com.salesforce.servicelibs/rxgrpc-stub) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.salesforce.servicelibs/rxgrpc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.salesforce.servicelibs/rxgrpc) + + +Usage +===== +```xml + + + ... + + + + + kr.motd.maven + os-maven-plugin + 1.4.1.Final + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.5.0 + + com.google.protobuf:protoc:3.0.2:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.2.0:exe:${os.detected.classifier} + + + + + test-compile + test-compile-custom + + + + + rxgrpc + com.salesforce.servicelibs + rxgrpc + [VERSION] + com.salesforce.rx3grpc.RxGrpcGenerator + + + + + + + + + +``` \ No newline at end of file diff --git a/rx3-java/rx3grpc/pom.xml b/rx3-java/rx3grpc/pom.xml new file mode 100644 index 00000000..18f8bf41 --- /dev/null +++ b/rx3-java/rx3grpc/pom.xml @@ -0,0 +1,83 @@ + + + + + + com.salesforce.servicelibs + reactive-grpc + 1.2.5-SNAPSHOT + ../../pom.xml + + 4.0.0 + + rx3grpc + + + + ${project.groupId} + reactive-grpc-gencommon + ${project.version} + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + ../../checkstyle.xml + ../../checkstyle_ignore.xml + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + com.salesforce.rx3grpc.Rx3GrpcGenerator + + + + + + + com.salesforce.servicelibs + canteen-maven-plugin + ${canteen.plugin.version} + + + + bootstrap + + + + + + + diff --git a/rx3-java/rx3grpc/src/main/java/com/salesforce/rx3grpc/Rx3GrpcGenerator.java b/rx3-java/rx3grpc/src/main/java/com/salesforce/rx3grpc/Rx3GrpcGenerator.java new file mode 100644 index 00000000..b70d51e3 --- /dev/null +++ b/rx3-java/rx3grpc/src/main/java/com/salesforce/rx3grpc/Rx3GrpcGenerator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, Salesforce.com, Inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +package com.salesforce.rx3grpc; + +import com.salesforce.jprotoc.ProtocPlugin; +import com.salesforce.reactivegrpc.gen.ReactiveGrpcGenerator; + +/** + * A protoc generator for generating ReactiveX 3.0 bindings for gRPC. + */ +public class Rx3GrpcGenerator extends ReactiveGrpcGenerator { + + @Override + protected String getClassPrefix() { + return "Rx3"; + } + + public static void main(String[] args) { + if (args.length == 0) { + ProtocPlugin.generate(new Rx3GrpcGenerator()); + } else { + ProtocPlugin.debug(new Rx3GrpcGenerator(), args[0]); + } + } +} diff --git a/rx3-java/rx3grpc/src/main/resources/Rx3Stub.mustache b/rx3-java/rx3grpc/src/main/resources/Rx3Stub.mustache new file mode 100644 index 00000000..6b1d44a6 --- /dev/null +++ b/rx3-java/rx3grpc/src/main/resources/Rx3Stub.mustache @@ -0,0 +1,205 @@ +{{#packageName}} +package {{packageName}}; +{{/packageName}} + +import static {{packageName}}.{{serviceName}}Grpc.getServiceDescriptor; +import static io.grpc.stub.ServerCalls.asyncUnaryCall; +import static io.grpc.stub.ServerCalls.asyncServerStreamingCall; +import static io.grpc.stub.ServerCalls.asyncClientStreamingCall; +import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall; + + +{{#deprecated}} +@java.lang.Deprecated +{{/deprecated}} +@javax.annotation.Generated( +value = "by RxGrpc generator", +comments = "Source: {{protoName}}") +public final class {{className}} { + private {{className}}() {} + + public static Rx{{serviceName}}Stub newRxStub(io.grpc.Channel channel) { + return new Rx{{serviceName}}Stub(channel); + } + + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + public static final class Rx{{serviceName}}Stub extends io.grpc.stub.AbstractStub { + private {{serviceName}}Grpc.{{serviceName}}Stub delegateStub; + + private Rx{{serviceName}}Stub(io.grpc.Channel channel) { + super(channel); + delegateStub = {{serviceName}}Grpc.newStub(channel); + } + + private Rx{{serviceName}}Stub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + delegateStub = {{serviceName}}Grpc.newStub(channel).build(channel, callOptions); + } + + @java.lang.Override + protected Rx{{serviceName}}Stub build(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new Rx{{serviceName}}Stub(channel, callOptions); + } + + {{#methods}} + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}> {{methodName}}({{#isManyInput}}io.reactivex.rxjava3.core.Flowable{{/isManyInput}}{{^isManyInput}}io.reactivex.rxjava3.core.Single{{/isManyInput}}<{{inputType}}> rxRequest) { + return com.salesforce.rx3grpc.stub.ClientCalls.{{reactiveCallsMethodName}}(rxRequest, + {{^isManyInput}} + new com.salesforce.reactivegrpc.common.BiConsumer<{{inputType}}, io.grpc.stub.StreamObserver<{{outputType}}>>() { + @java.lang.Override + public void accept({{inputType}} request, io.grpc.stub.StreamObserver<{{outputType}}> observer) { + delegateStub.{{methodNameCamelCase}}(request, observer); + } + }, getCallOptions()); + {{/isManyInput}} + {{#isManyInput}} + new com.salesforce.reactivegrpc.common.Function, io.grpc.stub.StreamObserver<{{inputType}}>>() { + @java.lang.Override + public io.grpc.stub.StreamObserver<{{inputType}}> apply(io.grpc.stub.StreamObserver<{{outputType}}> observer) { + return delegateStub.{{methodNameCamelCase}}(observer); + } + }, getCallOptions()); + {{/isManyInput}} + } + + {{/methods}} + {{#unaryRequestMethods}} + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}> {{methodName}}({{inputType}} rxRequest) { + return com.salesforce.rx3grpc.stub.ClientCalls.{{reactiveCallsMethodName}}(io.reactivex.rxjava3.core.Single.just(rxRequest), + new com.salesforce.reactivegrpc.common.BiConsumer<{{inputType}}, io.grpc.stub.StreamObserver<{{outputType}}>>() { + @java.lang.Override + public void accept({{inputType}} request, io.grpc.stub.StreamObserver<{{outputType}}> observer) { + delegateStub.{{methodNameCamelCase}}(request, observer); + } + }, getCallOptions()); + } + + {{/unaryRequestMethods}} + } + + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + public static abstract class {{serviceName}}ImplBase implements io.grpc.BindableService { + + {{#methods}} + {{^isManyInput}} + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}> {{methodNameCamelCase}}({{inputType}} request) { + return {{methodNameCamelCase}}(io.reactivex.rxjava3.core.Single.just(request)); + } + {{/isManyInput}} + + {{#javaDoc}} + {{{javaDoc}}} + {{/javaDoc}} + {{#deprecated}} + @java.lang.Deprecated + {{/deprecated}} + public {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}> {{methodNameCamelCase}}({{#isManyInput}}io.reactivex.rxjava3.core.Flowable{{/isManyInput}}{{^isManyInput}}io.reactivex.rxjava3.core.Single{{/isManyInput}}<{{inputType}}> request) { + throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNIMPLEMENTED); + } + + {{/methods}} + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + {{#methods}} + .addMethod( + {{packageName}}.{{serviceName}}Grpc.get{{methodNamePascalCase}}Method(), + {{grpcCallsMethodName}}( + new MethodHandlers< + {{inputType}}, + {{outputType}}>( + this, METHODID_{{methodNameUpperUnderscore}}))) + {{/methods}} + .build(); + } + + protected io.grpc.CallOptions getCallOptions(int methodId) { + return null; + } + + protected Throwable onErrorMap(Throwable throwable) { + return com.salesforce.rx3grpc.stub.ServerCalls.prepareError(throwable); + } + + } + + {{#methods}} + public static final int METHODID_{{methodNameUpperUnderscore}} = {{methodNumber}}; + {{/methods}} + + private static final class MethodHandlers implements + io.grpc.stub.ServerCalls.UnaryMethod, + io.grpc.stub.ServerCalls.ServerStreamingMethod, + io.grpc.stub.ServerCalls.ClientStreamingMethod, + io.grpc.stub.ServerCalls.BidiStreamingMethod { + private final {{serviceName}}ImplBase serviceImpl; + private final int methodId; + + MethodHandlers({{serviceName}}ImplBase serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + {{#methods}} + {{^isManyInput}} + case METHODID_{{methodNameUpperUnderscore}}: + com.salesforce.rx3grpc.stub.ServerCalls.{{reactiveCallsMethodName}}(({{inputType}}) request, + (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, + new com.salesforce.reactivegrpc.common.Function<{{#isManyInput}}io.reactivex.rxjava3.core.Flowable<{{inputType}}>{{/isManyInput}}{{^isManyInput}}{{inputType}}{{/isManyInput}}, {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}>>() { + @java.lang.Override + public {{#isManyOutput}}io.reactivex.rxjava3.core.Flowable{{/isManyOutput}}{{^isManyOutput}}io.reactivex.rxjava3.core.Single{{/isManyOutput}}<{{outputType}}> apply({{#isManyInput}}io.reactivex.rxjava3.core.Flowable<{{inputType}}>{{/isManyInput}}{{^isManyInput}}{{inputType}}{{/isManyInput}} single) { + return serviceImpl.{{methodNameCamelCase}}(single); + } + }, serviceImpl::onErrorMap); + break; + {{/isManyInput}} + {{/methods}} + default: + throw new java.lang.AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke(io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + {{#methods}} + {{#isManyInput}} + case METHODID_{{methodNameUpperUnderscore}}: + return (io.grpc.stub.StreamObserver) com.salesforce.rx3grpc.stub.ServerCalls.{{reactiveCallsMethodName}}( + (io.grpc.stub.StreamObserver<{{outputType}}>) responseObserver, + serviceImpl::{{methodNameCamelCase}}, serviceImpl::onErrorMap, serviceImpl.getCallOptions(methodId)); + {{/isManyInput}} + {{/methods}} + default: + throw new java.lang.AssertionError(); + } + } + } + +}