Java SDK with all features implemented for milestone 0.4 (#154)

* Basic Dapr HTTP Client for Actors.

* ActorTypeInformation with parsing logic + some stubs.

* Fixing license.

* Adding MethodContext + StateSerializer.

* Add ActorRuntime (#44)

* Add ActorRuntime

* split DaprAsyncClient hierarchy into 2 hierarchies for the different directions of communication

* Rename DaprClientBase to AbstractDaprClient

* Add ActorId

* Encapsulating async call in AbstractDaprClient as a single method. (#46)

* Add ActorMethodInfoMap (#47)

* Add ActorMethodInfoMap

* Add ConverterUtils class (#49)

* Add ConverterUtils + unit test

* Improved async call to okhttp. Former solution just wrapped a blocking call, now we using asynch call and collect the results via callbacks. Mono's are created based on the result. This make more efficient use of the resources since nothing is blocking now. Reformatted according new checkstyle (#48)

* More reformatting to 2 spaces indentation. (#50)

* Add ActorTimer and related (#51)

* Add ActorTimer and related

* cr

* ActorService + DaprStateProvider + some more. (#53)

* Faster JSON building + separate auto-gen jar + tests. (#55)

* closed the response objects and returning a string (#56)

* Add ReminderInfo (#54)

* Add ReminderInfo

* remove extra var

* More Actors Stuff (#57)

* ActorManager

* More work done.

* Adding example for actor runtime service.

* Implements ActorStateManager + fixes + javadocs.

* ActorProxy + some refactoring (#62)

* ActorManager

* More work done.

* Adding example for actor runtime service.

* Implements ActorStateManager + fixes + javadocs.

* Fix OrderManager example in order to process http 201
Change the implement the calls to DAPR using the class AbstractDaprClient because before the change the class always return Mono.Empty
Implementation of the ActorAsyncProxy
Change the name of the Actor Dapr Http Async Client

* Update code with the changes proposed by Artur in the code review

* Changes to support ActorProxy + Fixes.

Co-authored-by: Juan Jose Herrera <35985447+JuanJose-Herrera@users.noreply.github.com>

* Adding new methods for Http Client (#60)

* Adding new methods for Http Client

* Finishing implementation for HttpClient

* Adding JavaDoc and Changing return Type to new Methods

* Using ObjectSerializer and changing Constants values

* Adding gRPC client adapter, to encapsulate gRPC logic from the user (#61)

* Adding a common adapter to be exposed to users, for calling either the gRPC or the HTTP Clients.
Implementing gRPC adapter based on interface.
Moving ActorStateSerializer and renamed to be used as a generic utility for Serializing objects

* Creating a builder for the GrpcClient
Abstracting generic serialize and deserialize methods into a generic base class while leaving Actor specific stuff into the ActorStateSerializer class
Adding Javadocs

* Unit tests for ActorManager + fixes for Timer and Reminder. (#63)

* Add UnitTest to ActorProxyImpl and JaCoCo (coverage tool) implementation (#67)

* Change from unwrapMethodResponse to deserialize, because the response from an actor method is not wrapped in "Data" object
Add Unit test for the ActorProxy class

* #24 Implement more testing to the ActorProxyImpl and refactor how we manage the errors with Mono
#20 Add the coverage tool JaCoCo, the rules are commented at this moment in order to allow to execute the examples at this moment, if we enable the rules, the compilation fails.

* Enable rules for unit test coverage with a minimum of 0 in order to allow the developers to success compile, the minimum should be set to .8 before the projects ends

* Use the unwrapMethodResponse of the serilizer in order to deserilize actor responses.

* Reverting ActorProxyImpl.java

There is not need to change ActorProxyImpl since the previous change to not wrap it with the "data" structure was incorrect.

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Starting package to handle SpringBoot integration. (#69)

* Request body must be null on get for okhttp, empty body does not work… (#70)

* Request body must be null on get for okhttp, empty body does not work.  Also, expose the ActorStateManager methods that ought to be exposed to user.

* Make ctor package private and use equals for string compare

* Refactoring HTTP Client and creating Client HTTP Adapter (#68)

* Refactoring HTTP Client to follow same pattern as the GRPC Client.
Creating DaprClientHttpAdapter to be exposed to the users.
Refactor all uses of the http Client to use the adapter instead following composition rather than inheritance.
Dealing with conflicts

* Renaming AppToDaprHttpAsync to AppToDaprHttpAsyncClient changed previously by mistake

* Refactor Adapters to centralize all generic and actor specific functionality in the same place, having a single entry point for all communications to DAPR.
Leaving GRPC Adapter implemented, but without the possibility to create an instance of it.
Fixing Test cases

* Addressing PR comments

* PubSub + related fixes. (#71)

* Fixing issue regarding GET and DELETE method (#76)

* Unit tests for PubSub + JavaDocs. (#75)

* Invoke service + unit tests. (#78)

* Java sdk wip (#77)

* Fixing issue regarding GET and DELETE method

* Adding unit test for DaprHttp.java

* Adding test scope

* Renaming a property and fixing conflict to merge.

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* pubsub sample needs update for listener interface (#80)

* Fix bindings + add samples (#81)

* Fix binding and add samples

* Fix sample command line

* update comment for name change

* lowercase d matches runtime, though it looks like it is case insensitive

* Spliting Actor client and runtime into a separate jar. (#83)

* Unit testing + bug fixing in GRPC Adapter (#79)

* Adding mockito plugin to be able to mock final classes
Increasing test coverage for ObjectSerializer
Fixing bug in GRPC Adapter while creating the envelopes, found during unit testing.

* First step into returning Http Headers as part of the response for the DaprClientHttpAdapter
Updating State object to match the API.
Fixing Broken Unit Tests and increasing coverage for DaprClientGrpcAdapter

* Adding documentation, fixing typos and renaming support method to be more descriptive.

* Addressing PR comments
Increasing test coverage
Fixing Merge conflicts

* Addressing PR comments

* Addressing comments regarding where to build the query parameters forthe HTTP Client calls. (#84)

* Addressing comments regarding where to build the query parameters for the HTTP Client calls.

* Making map immutable while also preventing a NPE

* Fixing DaprHttp and its tests. (#93)

* Increasing test coverage for DaprClientGrpcAdapter (#94)

* Upload jacoco test coverage report to artifact storage (#95)

* set jacoco report output dir

* upload test report

* add dapr version info

* #26 Integretion Testing Initial Example (#74)

* #26 Add Hello World Integration Testing working on Windows, need work to work on MAC and Linux

* #26 Add new Integration Test to test DAPR state functionality

* #26 Add Hello World Integration Testing working on Windows, need work to work on MAC and Linux

* Update Integration Testing getting free ports automatically

* #26 Refractor to use a base class for all the integration tests

* #26 Make StateOptions as optional in order to not throw a null pointer exception

* #26 Remove empty lines and correct the ident

* Adding license to DaprIntegrationTestingRunner

Co-authored-by: Young Bu Park <youngp@microsoft.com>
Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Fixing and finishing unit tests (#97)

* Fixing and finishing unit tests

* Fixing styling Issues and adding new test

* Unit tests for stateful actors + fixes. (#104)

* Receiving the StateOptions on save as part of the State, to comply with the DAPR API (#105)

* Receiving the StateOptions on save as part of the State, to comply with the DAPR API

* #26 Add integration test for Concurrency funtionality in the states module

Co-authored-by: Juan Jose Herrera <35985447+JuanJose-Herrera@users.noreply.github.com>
Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Deploy Jar packages to Nexus via CI (#106)

* Add SNAPSHOT suffix to version

* set deployment setup

* add ossrh  repostiory

* OSSRH setting

* add deployment steps

* exclude examples jar

* remove condition

* exclude spring boot pkg

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* deactivate should not call save (#108)

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Fixing bug, where call should be deferred instead of async (#102)

* Fixing bug, where call should be deferred instead of async

* Removing unused pool property
Returning null as response body if empty response is found.
Adding Test case for validating callback is executed when expected.

* Fixing assertion based on previous changes

* Adding test case to verify that the call to grpc only happens when the requestor block the thread, instead of in parallel immediately after calling the methods in the adapter.
Documenting complex test case

* Fixing merge conflicts

* Documenting complex test case

* Disable GPG sign by default (#111)

* [CD] Fix gpg private key import (#113)

* fix gpg key import process

* add GPG_TTY env setting

* revert

* rename dapr-sdk to dapr-actors

* fix

* Fix GPG plugin

* [CD] Conditional SNAPSHOT and final release (#115)

* Release pkg based on tag and branch

* Update build.yml

* Update build.yml

* Creating unit tests for HttpAdapter + Adding junit 5 dependency + Reformating code (#112)

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Removing springboot jar and adding some unit tests for ActorRuntime. (#114)

* Release SNAPSHOT pkg for every build (#118)

* Release SNAPSHOT build every build

* fix

* Fix sample (#119)

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Unit tests for actors, no state. (#117)

* actor unit tests, no state

clean up a bit

* Rename class

* Fix merge

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Moving io.dapr.runtime to test + fixing examples accordingly. (#121)

* mvn integration-test doesn't work w/o this plugin (#122)

* Resolving conflict for PR #110 (#130)

* Renaming State domain object.
Removing duplicate use of State Options.
Adding custom serialize and deserialize classes for StateOptions objects.

* #26 Refractor StateOptions to clean unused code

* Fixing merge issues.

* Fixing unit test for DaprClientHttpAdapterTest after refactoring.

Co-authored-by: Juan Jose Herrera <35985447+JuanJose-Herrera@users.noreply.github.com>

* Refactor StateOptions and add IT consistency testing (#110)

* Renaming State domain object.
Removing duplicate use of State Options.
Adding custom serialize and deserialize classes for StateOptions objects.

* #26 Refractor StateOptions to clean unused code

* Fixing merge issues.

* #26 Add integration test for states using Retry policies

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Fix IT issues, to run the integration test rember to add the parameter skipITs=false (#131)

* Addresses multiple items from design review (#124)

* Use byte[] for customer data and serialized data.
Fixes for actor demo.

* Simplifying ObjectSerializer + handling CloudEvent properly.

* Split DaprClient into 3 parts to hide actor APIs.

* Remove Actor "dummy" interface.

* Supports custom serializer.

* Change seriaizer to receive Object directly.

* Handling custom serialization in State APIs + enhancements to the state APIs.

* Addressing small comment on Mono chain in ActorManager.

* Make serializer mandatory, throwing exception when null.

* Fix arg parsing problem -Dexec.args= seems to not allow hyphens inside (#136)

* Fix bug in Actor due to timer serialization. (#139)

* Missing block (#140)

* Install local test kafka before running build and tests (#142)

* local test kafka docker-compose yaml

* add kafka install step

* Separate serializer for state. (#135)

* Add gRPC State Integration Test (#138)

* Add GRP State Integration Test

* Explain ignored test cases for GRPC

* Explain ignored test cases for GRPC

* Update DaprClientTestBuilder.java

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Actor Activation / Deactivation Integartion Test (#144)

* Adding common setup for starting dapr applications in Integration Tests.
Adding integration test for Actor Activation and Reactivation.

* Changing the way parameters are sent to the application running under Dapr in ITs to fix issue found in Ubuntu about wrong parsing.
Moving logic of the ActivationClient into the IT class

* Fixing compilation issues in test due to rebase from upstream.

* Addressing PR comments

* Adding more unit test to reach 80% Coverage (#149)

* Adding checkstyle + fix style in sdk-actos. (#146)

* Update debug instructions since LB is not needed. (#150)

* Add E2E testing for bindings (#148)

* Add GRP State Integration Test

* Explain ignored test cases for GRPC

* Explain ignored test cases for GRPC

* Update DaprClientTestBuilder.java

* Add binding E2E testing.

* Add fix binding E2E testing after merge with new changes

* Remove example comments

Co-authored-by: Artur Souza <artursouza.ms@outlook.com>

* Adding documentation for examples (#153)

Co-authored-by: Leon Mai <lemai@microsoft.com>
Co-authored-by: ji11er <57505038+ji11er@users.noreply.github.com>
Co-authored-by: Juan Jose Herrera <35985447+JuanJose-Herrera@users.noreply.github.com>
Co-authored-by: mestizoLopez <ing.javierlg@gmail.com>
Co-authored-by: Andres Robles <15348598+AndresRoblesMX@users.noreply.github.com>
Co-authored-by: Young Bu Park <youngp@microsoft.com>
Co-authored-by: Marcos Reyes <59033058+marcosreyes05@users.noreply.github.com>
This commit is contained in:
Artur Souza 2020-01-24 12:49:56 -08:00 committed by GitHub
parent b63fa48954
commit cf68f24289
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 17252 additions and 25173 deletions

View File

@ -21,19 +21,27 @@ jobs:
env:
JDK_VER: 13.0.x
DAPR_RUNTIME_VER: 0.3.0
OSSRH_USER_TOKEN: ${{ secrets.OSSRH_USER_TOKEN }}
OSSRH_PWD_TOKEN: ${{ secrets.OSSRH_PWD_TOKEN }}
GPG_KEY: ${{ secrets.GPG_KEY }}
GPG_PWD: ${{ secrets.GPG_PWD }}
steps:
- uses: actions/checkout@v1
- name: Set up ${{ env.JDK_VER }}
- name: Set up OpenJDK ${{ env.JDK_VER }}
uses: actions/setup-java@v1
with:
java-version: ${{ env.JDK_VER }}
- name: Set up Dapr CLI
run: wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
- name: Initialize Dapr runtime as a standalone mode
- name: Initialize Dapr runtime ${{ env.DAPR_RUNTIME_VER }}
run: |
sudo dapr init --runtime-version ${{ env.DAPR_RUNTIME_VER }}
echo "Showing dapr version..."
dapr --version
- name: Install Local kafka using docker-compose
run: |
docker-compose -f ./sdk/src/test/java/io/dapr/it/deploy/local-test-kafka.yml up -d
docker ps
- name: Clean up files
run: mvn clean
- name: Build sdk
@ -42,3 +50,32 @@ jobs:
run: mvn test
- name: Integration-test
run: mvn integration-test
- name: Upload test report for sdk
uses: actions/upload-artifact@master
with:
name: report-dapr-java-sdk
path: sdk/target/jacoco-report/
- name: Upload test report for sdk-actors
uses: actions/upload-artifact@master
with:
name: report-dapr-java-sdk-actors
path: sdk-actors/target/jacoco-report/
- name: Packaging jars
run: mvn package
- name: Get pom parent version
run: |
PARENT_VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
echo "##[set-env name=PARENT_VERSION;]$PARENT_VERSION"
- name: Is SNAPSHOT release ?
if: contains(github.ref, 'master') && contains(env.PARENT_VERSION, '-SNAPSHOT')
run: echo "##[set-env name=DEPLOY_OSSRH;]true"
- name: Is Release or RC version ?
if: startswith(github.ref, 'refs/tags/v') && !contains(env.PARENT_VERSION, '-SNAPSHOT')
run: echo "##[set-env name=DEPLOY_OSSRH;]true"
- name: Publish to ossrh
if: env.DEPLOY_OSSRH == 'true'
run: |
echo ${{ secrets.GPG_PRIVATE_KEY }} | base64 -d > private-key.gpg
export GPG_TTY=$(tty)
gpg --batch --import private-key.gpg
mvn -V -B -Dgpg.skip=false -s settings.xml deploy

1
.gitignore vendored
View File

@ -13,6 +13,7 @@
# Log file
*.log
/syslog.txt
# BlueJ files
*.ctxt

View File

@ -48,27 +48,28 @@ mvn versions:commit
### Debug Java application or Dapr's Java SDK
If you have a Java application or an issue on this SDK that needs to be debugged, follow the steps below:
If you have a Java application or an issue on this SDK that needs to be debugged, run Dapr using a dummy command and start the application from your IDE (IntelliJ, for example).
For Linux and MacOS:
Install [Pen Load Balancer](https://sourceforge.net/projects/penloadbalancer/) or your preferred load balancer utility:
```sh
sudo apt-get install pen
```
Note: Pen is also available on Windows in the link above. For MacOS, you will need to [build it from source code](https://github.com/UlricE/pen/wiki/Building-Pen-from-Git).
Then run Dapr with the load balancer process listening on port 3001 and forwarding to port 3000. For Pen Load Balancer, it would be:
```sh
dapr run --app-id testapp --app-port 3001 --port 3500 -- pen -b 99999999 -f localhost:3001 localhost:3000
dapr run --app-id testapp --app-port 3000 --port 3500 --grpc-port 5001-- cat
```
The command below will start a load balancer listening on port `3001` that forwards connections to port `3000`, while Dapr's app identifier is `testapp` and listening port is `3500`. If you try to make a HTTP call to any URL on `localhost:3001`, it will fail until you have an application listening on `localhost:3000`.
For Windows:
```sh
dapr run --app-id testapp --app-port 3000 --port 3500 --grpc-port 5001 -- waitfor FOREVER
```
When running your Java application from IDE, make sure the following environment variables are set, so the Java SDK knows how to connect to Dapr's sidecar:
```
DAPR_HTTP_PORT=3500
DAPR_GRPC_PORT=5001
```
Now you can go to your IDE (like IntelliJ, for example) and debug your Java application, using port `3500` to call Dapr while also listening to port `3000` to expose Dapr's callback endpoint.
Calls to Dapr's APIs on `http://localhost:3500/*` should work now and trigger breakpoints in your code.
**If your application needs to suscribe to topics or register Actors in Dapr, for example, then start debugging your app first and run dapr with load balancer last.**
Reminder: for Dapr, your application is listening to port `3001` and not `3000` since it can only see the load balancer's port.
**If your application needs to suscribe to topics or register Actors in Dapr, for example, then start debugging your app first and run dapr with dummy command last.**
**If using Visual Studio Code, also consider [this solution as well](https://github.com/dapr/docs/tree/master/howto/vscode-debugging-daprd).**

268
checkstyle.xml Normal file
View File

@ -0,0 +1,268 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.sf.net (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module>
<module name="MethodParamPad"/>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="scope" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="public"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="ignoreInlineTags" value="false"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation"/>
</module>
</module>

View File

@ -0,0 +1,20 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: sample123
spec:
type: bindings.kafka
metadata:
# Kafka broker connection setting
- name: brokers
value: localhost:9092
# consumer configuration: topic and consumer group
- name: topics
value: sample
- name: consumerGroup
value: group1
# publisher configuration: topic
- name: publishTopic
value: sample
- name: authRequired
value: "false"

View File

@ -7,17 +7,22 @@
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>0.3.0-alpha</version>
<version>0.2.0-SNAPSHOT</version>
</parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-examples</artifactId>
<packaging>jar</packaging>
<version>0.3.0-alpha</version>
<version>0.2.0-SNAPSHOT</version>
<name>dapr-sdk-examples</name>
<properties>
<protobuf.output.directory>${project.build.directory}/generated-sources</protobuf.output.directory>
<protobuf.input.directory>${project.parent.basedir}/proto</protobuf.input.directory>
<protobuf.input.directory>${project.basedir}/proto</protobuf.input.directory>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
@ -34,18 +39,27 @@
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<version>${grpc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -58,10 +72,25 @@
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>3.10.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>0.3.0-alpha</version>
<version>${project.version}</version>
</dependency>
</dependencies>
@ -83,7 +112,7 @@
<includeMavenTypes>direct</includeMavenTypes>
<includeStdTypes>true</includeStdTypes>
<inputDirectories>
<include>${protobuf.input.directory}/examples</include>
<include>${protobuf.input.directory}</include>
</inputDirectories>
<outputTargets>
<outputTarget>
@ -100,6 +129,14 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.actors.http;
import reactor.core.publisher.Mono;
/**
* Example of implementation of an Actor.
*/
public interface DemoActor {
void registerReminder();
String say(String something);
void clock(String message);
Mono<Integer> incrementAndGet(int delta);
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.actors.http;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyBuilder;
import io.dapr.serializer.DefaultObjectSerializer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Client for Actor runtime.
* 1. Build and install jars:
* mvn clean install
* 2. Run the client:
* dapr run --app-id demoactorclient --port 3006 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.actors.http.DemoActorClient
*/
public class DemoActorClient {
private static final int NUM_ACTORS = 3;
private static final int NUM_MESSAGES_PER_ACTOR = 10;
private static final String METHOD_NAME = "say";
private static final ExecutorService POOL = Executors.newFixedThreadPool(NUM_ACTORS);
public static void main(String[] args) throws Exception {
ActorProxyBuilder builder = new ActorProxyBuilder("DemoActor", new DefaultObjectSerializer());
List<CompletableFuture<Void>> futures = new ArrayList<>(NUM_ACTORS);
for (int i = 0; i < NUM_ACTORS; i++) {
ActorProxy actor = builder.build(ActorId.createRandom());
futures.add(callActorNTimes(actor));
}
futures.forEach(CompletableFuture::join);
POOL.shutdown();
POOL.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Done.");
}
private static final CompletableFuture<Void> callActorNTimes(ActorProxy actor) {
return CompletableFuture.runAsync(() -> {
actor.invokeActorMethod("registerReminder").block();
for (int i = 0; i < NUM_MESSAGES_PER_ACTOR; i++) {
actor.invokeActorMethod("incrementAndGet", 1).block();
String result = actor.invokeActorMethod(METHOD_NAME,
String.format("Actor %s said message #%d", actor.getActorId().toString(), i), String.class).block();
System.out.println(String.format("Actor %s got a reply: %s", actor.getActorId().toString(), result));
try {
Thread.sleep((long)(1000 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return;
}
}
System.out.println(
"Messages sent: " + actor.invokeActorMethod("incrementAndGet", 0, int.class).block());
}, POOL);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.actors.http;
import io.dapr.actors.ActorId;
import io.dapr.actors.runtime.AbstractActor;
import io.dapr.actors.runtime.ActorRuntimeContext;
import io.dapr.actors.runtime.ActorType;
import io.dapr.actors.runtime.Remindable;
import reactor.core.publisher.Mono;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Calendar;
import java.util.TimeZone;
/**
* Implementation of the DemoActor for the server side.
*/
@ActorType(name = "DemoActor")
public class DemoActorImpl extends AbstractActor implements DemoActor, Remindable<Integer> {
/**
* Format to output date and time.
*/
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
public DemoActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
super.registerActorTimer(
null,
"clock",
"ping!",
Duration.ofSeconds(2),
Duration.ofSeconds(1)).block();
}
@Override
public void registerReminder() {
super.registerReminder(
"myremind",
(int)(Integer.MAX_VALUE * Math.random()),
Duration.ofSeconds(5),
Duration.ofSeconds(2)).block();
}
@Override
public String say(String something) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println("Server say method for actor " +
super.getId() + ": " +
(something == null ? "" : something + " @ " + utcNowAsString));
super.getActorStateManager().set("lastmessage", something).block();
// Now respond with current timestamp.
return utcNowAsString;
}
@Override
public Mono<Integer> incrementAndGet(int delta) {
return super.getActorStateManager().contains("counter")
.flatMap(exists -> exists ? super.getActorStateManager().get("counter", int.class) : Mono.just(0))
.map(c -> c + delta)
.flatMap(c -> super.getActorStateManager().set("counter", c).thenReturn(c));
}
@Override
public void clock(String message) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println("Server timer for actor " +
super.getId() + ": " +
(message == null ? "" : message + " @ " + utcNowAsString));
}
@Override
public Class<Integer> getStateType() {
return Integer.class;
}
@Override
public Mono<Void> receiveReminder(String reminderName, Integer state, Duration dueTime, Duration period) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println(String.format(
"Server reminded actor %s of: %s for %d @ %s",
this.getId(), reminderName, state, utcNowAsString));
return Mono.empty();
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.actors.http;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.springboot.DaprApplication;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Service for Actor runtime.
* 1. Build and install jars:
* mvn clean install
* 2. Run the server:
* dapr run --app-id demoactorservice --app-port 3000 --port 3005 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.actors.http.DemoActorService -Dexec.args="-p 3000"
*/
@SpringBootApplication
public class DemoActorService {
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port Dapr will listen to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
// Register the Actor class.
ActorRuntime.getInstance().registerActor(
DemoActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
// Start Dapr's callback endpoint.
DaprApplication.start(port);
// Start application's endpoint.
SpringApplication.run(DemoActorService.class);
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.actors.http;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Greetings from your Spring Boot Application!";
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.bindings;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
/**
* SpringBoot Controller to handle input binding.
*/
@RestController
public class InputBindingController {
@PostMapping(path = "/bindingSample")
public Mono<Void> handleInputBinding(@RequestBody(required = false) byte[] body) {
return Mono.fromRunnable(() ->
System.out.println("Received message through binding: " + (body == null ? "" : new String(body))));
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.bindings;
import io.dapr.springboot.DaprApplication;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
/**
* Service for input binding example.
* 1. From your repo root, build and install jars:
* mvn clean install
* 2. cd to [repo-root]/examples
* 3. Run :
* dapr run --app-id inputbinding --app-port 3000 --port 3005 -- mvn exec:java -D exec.mainClass=io.dapr.examples.bindings.InputBindingExample -D exec.args="-p 3000"
*/
public class InputBindingExample {
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port Dapr will listen to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
// Start Dapr's callback endpoint.
DaprApplication.start(port);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.bindings;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.serializer.DefaultObjectSerializer;
/**
* Service for output binding example.
* 1. From your repo root, build and install jars:
* mvn clean install
* 2. cd to [repo-root]/examples
* 3. Run the program:
* dapr run --app-id outputbinding --port 3006 -- mvn exec:java -D exec.mainClass=io.dapr.examples.bindings.OutputBindingExample
*/
public class OutputBindingExample {
public static class MyClass {
public MyClass(){}
public String message;
}
public static void main(String[] args) {
DaprClient client = new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer()).build();
final String BINDING_NAME = "bindingSample";
// This is an example of sending data in a user-defined object. The input binding will receive:
// {"message":"hello"}
MyClass myClass = new MyClass();
myClass.message = "hello";
System.out.println("sending a class with message: " + myClass.message);
client.invokeBinding(BINDING_NAME, myClass).block();
// This is an example of sending a plain string. The input binding will receive:
// "cat"
final String m = "cat";
System.out.println("sending a plain string: " + m);
client.invokeBinding(BINDING_NAME, m).block();
try {
Thread.sleep((long) (10000 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return;
}
System.out.println("Done.");
}
}

View File

@ -0,0 +1,142 @@
# Dapr Bindings Sample
In this sample, we'll create two java applications: an output binding application and an input binding application, using Dapr Java SDK.
This sample includes two applications:
* InputBindingExample (Initializes the Dapr Spring boot application client)
* OutputBindingExample (pushes the event message)
Visit [this](https://github.com/dapr/docs/tree/master/concepts/bindings) link for more information about Dapr and bindings concepts.
## Binding sample using the Java-SDK
In this example, the component used is Kafka but others are also available.
Visit [this](https://github.com/dapr/components-contrib/tree/master/bindings) link for more information about binding implementations.
## Pre-requisites
* [Dapr and Dapr Cli](https://github.com/dapr/docs/blob/master/getting-started/environment-setup.md#environment-setup).
* Java JDK 11 (or greater): [Oracle JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11) or [OpenJDK](https://jdk.java.net/13/).
* [Apache Maven](https://maven.apache.org/install.html) version 3.x.
### Checking out the code
Clone this repository:
```sh
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
```
Then build the Maven project:
```sh
# make sure you are in the `java-sdk` directory.
mvn install
```
### Setting Kafka locally
Before getting into the application code, follow these steps in order to setup a local instance of Kafka. This is needed for the local instances. Steps are:
1. navigate to the [repo-root]/examples/src/main/java/io/dapr/examples/bindings
2. Run `docker-compose -f ./docker-compose-single-kafka.yml up -d` to run the container locally
3. Run `docker ps` to see the container running locally:
```bash
342d3522ca14 kafka-docker_kafka "start-kafka.sh" 14 hours ago Up About
a minute 0.0.0.0:9092->9092/tcp kafka-docker_kafka_1
0cd69dbe5e65 wurstmeister/zookeeper "/bin/sh -c '/usr/sb…" 8 days ago Up About
a minute 22/tcp, 2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp kafka-docker_zookeeper_1
```
Click [here](https://github.com/wurstmeister/kafka-docker) for more information about the kafka broker server.
### Running the Input binding sample
The input binding sample uses the Spring Boot´s DaprApplication class for initializing the `InputBindingController`. In `InputBindingExample.java` file, you will find the `InputBindingExample` class and the `main` method. See the code snippet below:
```java
public class InputBindingExample {
public static void main(String[] args) throws Exception {
///..
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
// Start Dapr's callback endpoint.
DaprApplication.start(port);
}
///...
}
```
`DaprApplication.start()` Method will run an Spring Boot application that registers the `InputBindingController`, which exposes the actual handling of the event message as a POST request. The Dapr's sidecar is the one that performs the actual call to this controller, based on the binding features and the output binding action.
```java
@RestController
public class InputBindingController {
@PostMapping(path = "/bindingSample")
public Mono<Void> handleInputBinding(@RequestBody(required = false) byte[] body) {
return Mono.fromRunnable(() ->
System.out.println("Received message through binding: " + (body == null ? "" : new String(body))));
}
}
```
Execute the follow script in order to run the Input Binding example:
```sh
cd to [repo-root]/examples
dapr run --app-id inputbinding --app-port 3000 --port 3005 -- mvn exec:java -D exec.mainClass=io.dapr.examples.bindings.InputBindingExample -D exec.args="-p 3000"
```
### Running the Output binding sample
The output binding application is a simple java class with a main method that uses the Dapr Client to invoke binding.
In the `OutputBindingExample.java` file, you will find the `OutputBindingExample` class, containing the main method. The main method declares a Dapr Client using the `DaprClientBuilder` class. Notice that this builder gets two serializer implementations in the constructor: One is for Dapr's sent and recieved objects, and second is for objects to be persisted. The client publishes events using `invokeBinding` method. See the code snippet below:
```java
public class OutputBindingExample {
///...
public static void main(String[] args) throws Exception {
DaprClient client = new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer()).build();
final String BINDING_NAME = "bindingSample";
///...
MyClass myClass = new MyClass();
myClass.message = "hello";
System.out.println("sending an object instance with message: " + myClass.message);
client.invokeBinding(BINDING_NAME, myClass); //Binding a data object
///..
final String m = "cat";
System.out.println("sending a plain string: " + m);
client.invokeBinding(BINDING_NAME, m); //Binding a plain string text
}
///...
}
```
This example binds two events: A user-defined data object (using the `myClass` object as parameter) and a simple string using the same `invokeBinding` method.
Use the follow command to execute the Output Binding example:
```sh
cd to [repo-root]/examples
dapr run --app-id outputbinding --port 3006 -- mvn exec:java -D exec.mainClass=io.dapr.examples.bindings.OutputBindingExample
```
Once running, the OutputBindingExample should print the output as follows:
![publisheroutput](../../../../../resources/img/outputbinding.png)
Events have been sent.
Once running, the InputBindingExample should print the output as follows:
![publisheroutput](../../../../../resources/img/inputbinding.png)
Events have been retrieved from the binding.
For more details on Dapr Spring Boot integration, please refer to [Dapr Spring Boot](../../springboot/DaprApplication.java) Application implementation.

View File

@ -0,0 +1,14 @@
version: '2'
services:
zookeeper:
image: wurstmeister/zookeeper:latest
ports:
- "2181:2181"
kafka:
image: wurstmeister/kafka:latest
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_CREATE_TOPICS: "sample:1:1"
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181

View File

@ -23,105 +23,108 @@ import static io.dapr.examples.DaprExamplesProtos.SayResponse;
/**
* 1. Build and install jars:
* mvn clean install
* mvn clean install
* 2. Send messages to the server:
* dapr run --protocol grpc --grpc-port 50001 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.invoke.grpc.HelloWorldClient -Dexec.args="-p 50001 'message one' 'message two'"
* dapr run --protocol grpc --grpc-port 50001 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.invoke.grpc.HelloWorldClient -Dexec.args="-p 50001 'message one' 'message two'"
*/
public class HelloWorldClient {
/**
* Client mode: class representing a client-side logic for calling HelloWorld over Dapr.
*/
private static class GrpcHelloWorldDaprClient {
/**
* Client mode: class representing a client-side logic for calling HelloWorld over Dapr.
* Client communication channel: host, port and tls(on/off)
*/
private static class GrpcHelloWorldDaprClient {
private final ManagedChannel channel;
/**
* Client communication channel: host, port and tls(on/off)
*/
private final ManagedChannel channel;
/**
* Calls will be done asynchronously.
*/
private final DaprGrpc.DaprFutureStub client;
/**
* Creates a Grpc client for the DaprGrpc service.
* @param host host for the remote service endpoint
* @param port port for the remote service endpoint
*/
public GrpcHelloWorldDaprClient(String host, int port) {
this(ManagedChannelBuilder
.forAddress("localhost", port)
.usePlaintext() // SSL/TLS is default, we turn it off just because this is a sample and not prod.
.build());
}
/**
* Helper constructor to build client from channel.
* @param channel
*/
private GrpcHelloWorldDaprClient(ManagedChannel channel) {
this.channel = channel;
this.client = DaprGrpc.newFutureStub(channel);
}
/**
* Client mode: sends messages, one per second.
* @param messages
*/
private void sendMessages(String... messages) throws ExecutionException, InterruptedException, InvalidProtocolBufferException {
List<ListenableFuture<InvokeServiceResponseEnvelope>> futureResponses = new ArrayList<>();
for (String message : messages)
{
SayRequest request = SayRequest
.newBuilder()
.setMessage(message)
.build();
// Now, wrap the request with Dapr's envelope.
InvokeServiceEnvelope requestEnvelope = InvokeServiceEnvelope
.newBuilder()
.setId("hellogrpc") // Service's identifier.
.setData(Any.pack(request))
.setMethod("say") // The service's method to be invoked by Dapr.
.build();
futureResponses.add(client.invokeService(requestEnvelope));
System.out.println("Client: sent => " + message);
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
}
for (ListenableFuture<InvokeServiceResponseEnvelope> future : futureResponses) {
Any data = future.get().getData(); // Blocks waiting for response.
// IMPORTANT: do not use Any.unpack(), use Type.ParseFrom() instead.
SayResponse response = SayResponse.parseFrom(data.getValue());
System.out.println("Client: got response => " + response.getTimestamp());
}
}
/**
* Client mode: gracefully shutdown client within 1 min, otherwise force it.
* @throws InterruptedException Propagated interrupted exception.
*/
private void shutdown() throws InterruptedException {
this.channel.shutdown().awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Client: Bye.");
}
/**
* Calls will be done asynchronously.
*/
private final DaprGrpc.DaprFutureStub client;
/**
* Creates a Grpc client for the DaprGrpc service.
*
* @param host host for the remote service endpoint
* @param port port for the remote service endpoint
*/
public GrpcHelloWorldDaprClient(String host, int port) {
this(ManagedChannelBuilder
.forAddress("localhost", port)
.usePlaintext() // SSL/TLS is default, we turn it off just because this is a sample and not prod.
.build());
}
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port to listen or send event to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
GrpcHelloWorldDaprClient helloWorldClient = new GrpcHelloWorldDaprClient("localhost", port);
helloWorldClient.sendMessages(cmd.getArgs());
helloWorldClient.shutdown();
/**
* Helper constructor to build client from channel.
*
* @param channel
*/
private GrpcHelloWorldDaprClient(ManagedChannel channel) {
this.channel = channel;
this.client = DaprGrpc.newFutureStub(channel);
}
/**
* Client mode: sends messages, one per second.
*
* @param messages
*/
private void sendMessages(String... messages) throws ExecutionException, InterruptedException, InvalidProtocolBufferException {
List<ListenableFuture<InvokeServiceResponseEnvelope>> futureResponses = new ArrayList<>();
for (String message : messages) {
SayRequest request = SayRequest
.newBuilder()
.setMessage(message)
.build();
// Now, wrap the request with Dapr's envelope.
InvokeServiceEnvelope requestEnvelope = InvokeServiceEnvelope
.newBuilder()
.setId("hellogrpc") // Service's identifier.
.setData(Any.pack(request))
.setMethod("say") // The service's method to be invoked by Dapr.
.build();
futureResponses.add(client.invokeService(requestEnvelope));
System.out.println("Client: sent => " + message);
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
}
for (ListenableFuture<InvokeServiceResponseEnvelope> future : futureResponses) {
Any data = future.get().getData(); // Blocks waiting for response.
// IMPORTANT: do not use Any.unpack(), use Type.ParseFrom() instead.
SayResponse response = SayResponse.parseFrom(data.getValue());
System.out.println("Client: got response => " + response.getTimestamp());
}
}
/**
* Client mode: gracefully shutdown client within 1 min, otherwise force it.
*
* @throws InterruptedException Propagated interrupted exception.
*/
private void shutdown() throws InterruptedException {
this.channel.shutdown().awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Client: Bye.");
}
}
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port to listen or send event to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
GrpcHelloWorldDaprClient helloWorldClient = new GrpcHelloWorldDaprClient("localhost", port);
helloWorldClient.sendMessages(cmd.getArgs());
helloWorldClient.shutdown();
}
}

View File

@ -25,116 +25,116 @@ import static io.dapr.examples.DaprExamplesProtos.SayResponse;
* 1. Build and install jars:
* mvn clean install
* 2. Run in server mode:
* dapr run --app-id hellogrpc --app-port 5000 --protocol grpc -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.invoke.grpc.HelloWorldService -Dexec.args="-p 5000"
* dapr run --app-id hellogrpc --app-port 5000 --protocol grpc -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.invoke.grpc.HelloWorldService -Dexec.args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5009"
*/
public class HelloWorldService {
/**
* Server mode: class that encapsulates all server-side logic for Grpc.
*/
private static class GrpcHelloWorldDaprService extends DaprClientGrpc.DaprClientImplBase {
/**
* Server mode: class that encapsulates all server-side logic for Grpc.
* Format to output date and time.
*/
private static class GrpcHelloWorldDaprService extends DaprClientGrpc.DaprClientImplBase {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
/**
* Format to output date and time.
*/
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
/**
* Server mode: Grpc server.
*/
private Server server;
/**
* Server mode: Grpc server.
*/
private Server server;
/**
* Server mode: starts listening on given port.
*
* @param port Port to listen on.
* @throws IOException Errors while trying to start service.
*/
private void start(int port) throws IOException {
this.server = ServerBuilder
.forPort(port)
.addService(this)
.build()
.start();
System.out.printf("Server: started listening on port %d\n", port);
/**
* Server mode: starts listening on given port.
*
* @param port Port to listen on.
* @throws IOException Errors while trying to start service.
*/
private void start(int port) throws IOException {
this.server = ServerBuilder
.forPort(port)
.addService(this)
.build()
.start();
System.out.printf("Server: started listening on port %d\n", port);
// Now we handle ctrl+c (or any other JVM shutdown)
Runtime.getRuntime().addShutdownHook(new Thread() {
// Now we handle ctrl+c (or any other JVM shutdown)
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Server: shutting down gracefully ...");
GrpcHelloWorldDaprService.this.server.shutdown();
System.out.println("Server: Bye.");
}
});
}
/**
* Server mode: waits for shutdown trigger.
*
* @throws InterruptedException Propagated interrupted exception.
*/
private void awaitTermination() throws InterruptedException {
if (this.server != null) {
this.server.awaitTermination();
}
}
/**
* Server mode: this is the Dapr method to receive Invoke operations via Grpc.
*
* @param request Dapr envelope request,
* @param responseObserver Dapr envelope response.
*/
@Override
public void onInvoke(DaprClientProtos.InvokeEnvelope request, StreamObserver<Any> responseObserver) {
try {
if ("say".equals(request.getMethod())) {
// IMPORTANT: do not use Any.unpack(), use Type.ParseFrom() instead.
SayRequest sayRequest = SayRequest.parseFrom(request.getData().getValue());
SayResponse sayResponse = this.say(sayRequest);
responseObserver.onNext(Any.pack(sayResponse));
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
responseObserver.onError(e);
} finally {
responseObserver.onCompleted();
}
}
/**
* Handling of the 'say' method.
*
* @param request Request to say something.
* @return Response with when it was said.
*/
public SayResponse say(SayRequest request) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println("Server: " + request.getMessage() + " @ " + utcNowAsString);
// Now respond with current timestamp.
SayResponse.Builder responseBuilder = SayResponse.newBuilder();
return responseBuilder.setTimestamp(utcNowAsString).build();
public void run() {
System.out.println("Server: shutting down gracefully ...");
GrpcHelloWorldDaprService.this.server.shutdown();
System.out.println("Server: Bye.");
}
});
}
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port to listen or send event to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
final GrpcHelloWorldDaprService service = new GrpcHelloWorldDaprService();
service.start(port);
service.awaitTermination();
/**
* Server mode: waits for shutdown trigger.
*
* @throws InterruptedException Propagated interrupted exception.
*/
private void awaitTermination() throws InterruptedException {
if (this.server != null) {
this.server.awaitTermination();
}
}
/**
* Server mode: this is the Dapr method to receive Invoke operations via Grpc.
*
* @param request Dapr envelope request,
* @param responseObserver Dapr envelope response.
*/
@Override
public void onInvoke(DaprClientProtos.InvokeEnvelope request, StreamObserver<Any> responseObserver) {
try {
if ("say".equals(request.getMethod())) {
// IMPORTANT: do not use Any.unpack(), use Type.ParseFrom() instead.
SayRequest sayRequest = SayRequest.parseFrom(request.getData().getValue());
SayResponse sayResponse = this.say(sayRequest);
responseObserver.onNext(Any.pack(sayResponse));
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
responseObserver.onError(e);
} finally {
responseObserver.onCompleted();
}
}
/**
* Handling of the 'say' method.
*
* @param request Request to say something.
* @return Response with when it was said.
*/
public SayResponse say(SayRequest request) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println("Server: " + request.getMessage() + " @ " + utcNowAsString);
// Now respond with current timestamp.
SayResponse.Builder responseBuilder = SayResponse.newBuilder();
return responseBuilder.setTimestamp(utcNowAsString).build();
}
}
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port to listen to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
final GrpcHelloWorldDaprService service = new GrpcHelloWorldDaprService();
service.start(port);
service.awaitTermination();
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.invoke.http;
import io.dapr.springboot.DaprApplication;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
/**
* 1. Build and install jars:
* mvn clean install
* 2. Run in server mode:
* dapr run --app-id invokedemo --app-port 3000 --port 3005 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.invoke.http.DemoService -D exec.args="-p 3000"
*/
public class DemoService {
/**
* Starts the service.
* @param args Expects the port: -p PORT
* @throws Exception If cannot start service.
*/
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port to listen to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
DaprApplication.start(port);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.invoke.http;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Map;
import java.util.TimeZone;
/**
* SpringBoot Controller to handle input binding.
*/
@RestController
public class DemoServiceController {
/**
* Json serializer/deserializer.
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Format to output date and time.
*/
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@PostMapping(path = "/say")
public Mono<String> handleMethod(@RequestBody(required = false) byte[] body,
@RequestHeader Map<String, String> headers) {
return Mono.fromSupplier(() -> {
try {
String message = body == null ? "" : new String(body, StandardCharsets.UTF_8);
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
String metadataString = headers == null ? "" : OBJECT_MAPPER.writeValueAsString(headers);
// Handles the request by printing message.
System.out.println(
"Server: " + message + " @ " + utcNowAsString + " and metadata: " + metadataString);
return utcNowAsString;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.invoke.http;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.client.domain.Verb;
/**
* 1. Build and install jars:
* mvn clean install
* 2. Send messages to the server:
* dapr run --port 3006 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.invoke.http.InvokeClient -Dexec.args="'message one' 'message two'"
*/
public class InvokeClient {
/**
* Identifier in Dapr for the service this client will invoke.
*/
private static final String SERVICE_APP_ID = "invokedemo";
/**
* Starts the invoke client.
* @param args Messages to be sent as request for the invoke API.
*/
public static void main(String[] args) {
DaprClient client = (new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer())).build();
for (String message : args) {
client.invokeService(Verb.POST, SERVICE_APP_ID, "say", message, null, String.class).block();
}
}
}

View File

@ -0,0 +1,130 @@
# Method invocation Sample
In this sample, we'll create a two java applications: An exposer service application which exposes a method and a client application which will invoke the method from demo service using Dapr.
This sample includes:
* DemoService (Exposes the method to be remotely accessed)
* InvokeClient (Invokes the exposed method from DemoService)
Visit [this](https://github.com/dapr/docs/blob/master/concepts/service-invocation/service-invocation.md) link for more information about Dapr and service invocation.
## Remote invocation using the Java-SDK
This sample uses the Client provided in Dapr Java SDK invoking a remote method. Exposing the method
## Pre-requisites
* [Dapr and Dapr Cli](https://github.com/dapr/docs/blob/master/getting-started/environment-setup.md#environment-setup).
* Java JDK 11 (or greater): [Oracle JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11) or [OpenJDK](https://jdk.java.net/13/).
* [Apache Maven](https://maven.apache.org/install.html) version 3.x.
### Checking out the code
Clone this repository:
```sh
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
```
Then build the Maven project:
```sh
# make sure you are in the `java-sdk` directory.
mvn install
```
### Running the Demo service sample
The Demo service application is meant to expose a method that can be remotely invoked. In this example, the exposer feature has two parts:
In the `DemoService.java` file, you will find the `DemoService` class, containing the main method. The main method uses the Spring Boot´s DaprApplication class for initializing the `ExposerServiceController`. See the code snippet below:
```java
public class DemoService {
///...
public static void main(String[] args) throws Exception {
///...
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
DaprApplication.start(port);
}
}
```
`DaprApplication.start()` Method will run an Spring Boot application that registers the `DemoServiceController`, which exposes the invoking action as a POST request. The Dapr's sidecar is the one that performs the actual call to the controller, based on the binding features and the remote invocation action.
This Spring Controller exposes the `say` method. The method retrieves metadata from the headers and prints them along with the current date in console. The actual response from method is the formatted current date. See the code snippet below:
```java
@RestController
public class DemoServiceController {
///...
@PostMapping(path = "/say")
public Mono<String> handleMethod(@RequestBody(required = false) byte[] body,
@RequestHeader Map<String, String> headers) {
return Mono.fromSupplier(() -> {
try {
String message = body == null ? "" : new String(body, StandardCharsets.UTF_8);
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
String metadataString = headers == null ? "" : OBJECT_MAPPER.writeValueAsString(headers);
// Handles the request by printing message.
System.out.println(
"Server: " + message + " @ " + utcNowAsString + " and metadata: " + metadataString);
return utcNowAsString;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
```
Use the follow command to execute the exposer example:
```sh
dapr run --app-id invokedemo --app-port 3000 --port 3005 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.invoke.http.DemoService -D exec.args="-p 3000"
```
Once running, the ExposerService is now ready to be invoked by Dapr.
### Running the InvokeClient sample
The Invoke client sample uses the Dapr SDK for invoking the remote method. The main method declares a Dapr Client using the DaprClientBuilder class. Notice that this builder gets two serializer implementations in the constructor: One is for Dapr's sent and recieved objects, and second is for objects to be persisted. It needs to know the method name to invoke as well as the application id for the remote application. In `InvokeClient.java` file, you will find the `InvokeClient` class and the `main` method. See the code snippet below:
```java
public class InvokeClient {
private static final String SERVICE_APP_ID = "invokedemo";
///...
public static void main(String[] args) {
DaprClient client = (new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer())).build();
for (String message : args) {
client.invokeService(Verb.POST, SERVICE_APP_ID, "say", message, null, String.class).block();
}
}
///...
}
```
The class knows the app id for the remote application. It uses the the static `Dapr.getInstance().invokeService` method to invoke the remote method defining the parameters: The verb, application id, method name, and proper data and metadata, as well as the type of the expected retun data.
Execute the follow script in order to run the InvokeClient example, passing two messages for the remote method:
```sh
dapr run --port 3006 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.invoke.http.InvokeClient -D exec.args="'message one' 'message two'"
```
Once running, the output should display the messages sent from invoker in the exposer service output as follows:
![exposeroutput](../../../../../../resources/img/exposer-service.png)
Method have been remotely invoked and displaying the remote messages.
For more details on Dapr Spring Boot integration, please refer to [Dapr Spring Boot](../../../springboot/DaprApplication.java) Application implementation.

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.pubsub.http;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.serializer.DefaultObjectSerializer;
import java.util.Collections;
/**
* Message publisher.
* 1. Build and install jars:
* mvn clean install
* 2. Run the program:
* dapr run --app-id publisher --port 3006 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.pubsub.http.Publisher
*/
public class Publisher {
//Number of messages to be sent: 10
private static final int NUM_MESSAGES = 10;
//The title of the topic to be used for publishing
private static final String TOPIC_NAME = "testingtopic";
public static void main(String[] args) throws Exception {
//Creating the DaprClient: Using the default builder client produces an HTTP Dapr Client
DaprClient client = new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer()).build();
for (int i = 0; i < NUM_MESSAGES; i++) {
String message = String.format("This is message #%d", i);
//Publishing messages
client.publishEvent(TOPIC_NAME, message).block();
System.out.println("Published message: " + message);
try {
Thread.sleep((long)(1000 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return;
}
}
//Publishing a single bite: Example of non-string based content published
client.publishEvent(
TOPIC_NAME,
new byte[] { 1 },
Collections.singletonMap("content-type", "application/octet-stream")).block();
System.out.println("Published one byte.");
System.out.println("Done.");
}
}

View File

@ -0,0 +1,140 @@
# Dapr Pub-Sub Sample
In this sample, we'll create a publisher and a subscriber java applications using Dapr, based on the publish-subcribe pattern. The publisher will generate messages of a specific topic, while subscriber will listen for messages of specific topic. See [Why Pub-Sub](#why-pub-sub) to understand when this pattern might be a good choice for your software architecture.
Visit [this](https://github.com/dapr/docs/tree/master/concepts/publish-subscribe-messaging) link for more information about Dapr and Pub-Sub.
## Pub-Sub Sample using the Java-SDK
This sample uses the HTTP Client provided in Dapr Java SDK for subscribing, and Dapr Spring Boot integration for publishing. This example uses Redis Streams (enabled in Redis versions => 5).
## Pre-requisites
* [Dapr and Dapr Cli](https://github.com/dapr/docs/blob/master/getting-started/environment-setup.md#environment-setup).
* Java JDK 11 (or greater): [Oracle JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11) or [OpenJDK](https://jdk.java.net/13/).
* [Apache Maven](https://maven.apache.org/install.html) version 3.x.
### Checking out the code
Clone this repository:
```sh
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
```
Then build the Maven project:
```sh
# make sure you are in the `java-sdk` directory.
mvn install
```
### Running the subscriber
The first is the subscriber. It will subscribe to the topic to be used by the publisher and read the messages published. The Subscriber uses the Spring Boot´s DaprApplication class for initializing the `SubscriberController`. In `Subscriber.java` file, you will find the `Subscriber` class and the `main` method. See the code snippet below:
```java
public class Subscriber {
public static void main(String[] args) throws Exception {
///...
// Start Dapr's callback endpoint.
DaprApplication.start(port);
}
}
```
`DaprApplication.start()` Method will run an Spring Boot application that registers the `SubscriberController`, which exposes the message retrieval as a POST request. The Dapr's sidecar is the one that performs the actual call to the controller, based on the pubsub features.
This Spring Controller handles the message endpoint, Printing the recieved message which is recieved as the POST body. See the code snippet below:
```java
@RestController
public class SubscriberController {
///...
@GetMapping("/dapr/subscribe")
public byte[] daprConfig() throws Exception {
return SERIALIZER.serialize(new String[] { "message" });
}
@PostMapping(path = "/message")
public Mono<Void> handleMessage(@RequestBody(required = false) byte[] body,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
try {
// Dapr's event is compliant to CloudEvent.
CloudEventEnvelope envelope = SERIALIZER.deserialize(body, CloudEventEnvelope.class);
String message = envelope.getData() == null ? "" : new String(envelope.getData());
System.out.println("Subscriber got message: " + message);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
```
Execute the follow script in order to run the Subscriber example:
```sh
dapr run --app-id subscriber --app-port 3000 --port 3005 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.pubsub.http.Subscriber -D exec.args="-p 3000"
```
### Running the publisher
The other component is the publisher. It is a simple java application with a main method that uses the Dapr HTTP Client to publish 10 messages to an specific topic.
In the `Publisher.java` file, you will find the `Publisher` class, containing the main method. The main method declares a Dapr Client using the `DaprClientBuilder` class. Notice that this builder gets two serializer implementations in the constructor: One is for Dapr's sent and recieved objects, and second is for objects to be persisted. The client publishes messages using `publishEvent` method. See the code snippet below:
```java
public class Publisher {
private static final int NUM_MESSAGES = 10;
private static final String TOPIC_NAME = "testingtopic";
///...
public static void main(String[] args) throws Exception {
DaprClient client = new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer()).build();
for (int i = 0; i < NUM_MESSAGES; i++) {
String message = String.format("This is message #%d", i);
client.publishEvent(TOPIC_NAME, message).block();
System.out.println("Published message: " + message);
//..
}
}
///...
}
```
This example also pushes a non-string content event, the follow code in same `Publisher` main method publishes a bite:
```java
public class Publisher {
///...
public static void main(String[] args) throws Exception {
///...
//Publishing a single bite: Example of non-string based content published
client.publishEvent(
TOPIC_NAME,
new byte[] { 1 },
Collections.singletonMap("content-type", "application/octet-stream")).block();
System.out.println("Published one byte.");
System.out.println("Done.");
}
///...
}
```
Use the follow command to execute the Publisher example:
```sh
dapr run --app-id publisher --port 3006 -- mvn exec:java -pl=examples -D exec.mainClass=io.dapr.examples.pubsub.http.Publisher
```
Once running, the Publisher should print the output as follows:
![publisheroutput](../../../../../../resources/img/publisher.png)
Messages have been published in the topic.
Once running, the Subscriber should print the output as follows:
![publisheroutput](../../../../../../resources/img/subscriber.png)
Messages have been retrieved from the topic.
For more details on Dapr Spring Boot integration, please refer to [Dapr Spring Boot](../../../springboot/DaprApplication.java) Application implementation.

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.pubsub.http;
import io.dapr.springboot.DaprApplication;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
/**
* Service for subscriber.
* 1. Build and install jars:
* mvn clean install
* 2. Run the server:
* dapr run --app-id subscriber --app-port 3000 --port 3005 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.pubsub.http.Subscriber -Dexec.args="-p 3000"
*/
public class Subscriber {
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addRequiredOption("p", "port", true, "Port Dapr will listen to.");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
// If port string is not valid, it will throw an exception.
int port = Integer.parseInt(cmd.getOptionValue("port"));
// Start Dapr's callback endpoint.
DaprApplication.start(port);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.examples.pubsub.http;
import io.dapr.client.domain.CloudEvent;
import io.dapr.serializer.DefaultObjectSerializer;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* SpringBoot Controller to handle input binding.
*/
@RestController
public class SubscriberController {
/**
* Dapr's default serializer/deserializer.
*/
private static final DefaultObjectSerializer SERIALIZER = new DefaultObjectSerializer ();
@GetMapping("/dapr/subscribe")
public byte[] daprConfig() throws Exception {
return SERIALIZER.serialize(new String[] { "testingtopic" });
}
@PostMapping(path = "/testingtopic")
public Mono<Void> handleMessage(@RequestBody(required = false) byte[] body,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
try {
// Dapr's event is compliant to CloudEvent.
CloudEvent envelope = CloudEvent.deserialize(body);
String message = envelope.getData() == null ? "" : new String(envelope.getData());
System.out.println("Subscriber got message: " + message);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}

View File

@ -16,46 +16,46 @@ import java.util.UUID;
* dapr run --grpc-port 50001 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.Example
*/
public class Example {
public static void main(String[] args) {
ManagedChannel channel =
ManagedChannelBuilder.forAddress("localhost", 50001).usePlaintext().build();
DaprBlockingStub client = DaprGrpc.newBlockingStub(channel);
public static void main(String[] args) {
ManagedChannel channel =
ManagedChannelBuilder.forAddress("localhost", 50001).usePlaintext().build();
DaprBlockingStub client = DaprGrpc.newBlockingStub(channel);
String key = "mykey";
// First, write key-value pair.
{
String value = UUID.randomUUID().toString();
StateRequest req = StateRequest
.newBuilder()
.setKey(key)
.setValue(Any.newBuilder().setValue(ByteString.copyFromUtf8(value)).build())
.build();
SaveStateEnvelope state = SaveStateEnvelope.newBuilder()
.addRequests(req)
.build();
client.saveState(state);
System.out.println("Saved!");
}
// Now, read it back.
{
GetStateEnvelope req = GetStateEnvelope
.newBuilder()
.setKey(key)
.build();
GetStateResponseEnvelope response = client.getState(req);
String value = response.getData().getValue().toStringUtf8();
System.out.println("Got: " + value);
}
// Then, delete it.
{
DeleteStateEnvelope req = DeleteStateEnvelope
.newBuilder()
.setKey(key)
.build();
client.deleteState(req);
System.out.println("Deleted!");
}
String key = "mykey";
// First, write key-value pair.
{
String value = UUID.randomUUID().toString();
StateRequest req = StateRequest
.newBuilder()
.setKey(key)
.setValue(Any.newBuilder().setValue(ByteString.copyFromUtf8(value)).build())
.build();
SaveStateEnvelope state = SaveStateEnvelope.newBuilder()
.addRequests(req)
.build();
client.saveState(state);
System.out.println("Saved!");
}
// Now, read it back.
{
GetStateEnvelope req = GetStateEnvelope
.newBuilder()
.setKey(key)
.build();
GetStateResponseEnvelope response = client.getState(req);
String value = response.getData().getValue().toStringUtf8();
System.out.println("Got: " + value);
}
// Then, delete it.
{
DeleteStateEnvelope req = DeleteStateEnvelope
.newBuilder()
.setKey(key)
.build();
client.deleteState(req);
System.out.println("Deleted!");
}
}
}

View File

@ -2,6 +2,9 @@ package io.dapr.examples.state.http;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.serializer.DefaultObjectSerializer;
import org.json.JSONArray;
import org.json.JSONObject;
@ -20,6 +23,8 @@ import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@ -27,117 +32,84 @@ import static java.lang.System.out;
/**
* OrderManager web app.
*
* <p>
* Based on the helloworld Node.js example in https://github.com/dapr/samples/blob/master/1.hello-world/app.js
*
* <p>
* To install jars into your local maven repo:
* mvn clean install
*
* mvn clean install
* <p>
* To run (after step above):
* dapr run --app-id orderapp --app-port 3000 --port 3500 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.state.http.OrderManager
*
* dapr run --app-id orderapp --app-port 3000 --port 3500 -- mvn exec:java -pl=examples -Dexec.mainClass=io.dapr.examples.state.http.OrderManager
* <p>
* If this class changes, run this before running it again:
* mvn compile
* mvn compile
*/
public class OrderManager {
static HttpClient httpClient;
public static void main(String[] args) throws IOException {
int httpPort = 3001;
HttpServer httpServer = HttpServer.create(new InetSocketAddress(httpPort), 0);
public static void main(String[] args) throws IOException {
int httpPort = 3000;
String daprPort = Optional.ofNullable(System.getenv("DAPR_HTTP_PORT")).orElse("3500");
String stateUrl = String.format("http://localhost:%s/v1.0/state", daprPort);
HttpServer httpServer = HttpServer.create(new InetSocketAddress(httpPort), 0);
DaprClient daprClient =
(new DaprClientBuilder(new DefaultObjectSerializer(), new DefaultObjectSerializer())).build();
httpClient = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(2)).build();
httpServer.createContext("/order").setHandler(e -> {
out.println("Fetching order!");
fetch(stateUrl + "/order").thenAccept(response -> {
int resCode = response.statusCode() == 200 ? 200 : 500;
String body = response.statusCode() == 200 ? response.body() : "Could not get state.";
try {
e.sendResponseHeaders(resCode, body.getBytes().length);
OutputStream os = e.getResponseBody();
try {
os.write(body.getBytes());
} finally {
os.close();
}
} catch (IOException ioerror) {
out.println(ioerror);
}
});
});
httpServer.createContext("/neworder").setHandler(e -> {
try {
out.println("Received new order ...");
String json = readBody(e);
JSONObject jsonObject = new JSONObject(json);
JSONObject data = jsonObject.getJSONObject("data");
String orderId = data.getString("orderId");
out.printf("Got a new order! Order ID: %s\n", orderId);
JSONObject item = new JSONObject();
item.put("key", "order");
item.put("value", data);
JSONArray state = new JSONArray();
state.put(item);
out.printf("Writing to state: %s\n", state.toString());
post(stateUrl, state.toString()).thenAccept(response -> {
int resCode = response.statusCode() == 200 ? 200 : 500;
String body = response.body();
try {
e.sendResponseHeaders(resCode, body.getBytes().length);
OutputStream os = e.getResponseBody();
try {
os.write(body.getBytes());
} finally {
os.close();
}
} catch (IOException ioerror) {
out.println(ioerror);
}
});
} catch (IOException ioerror) {
out.println(ioerror);
}
});
httpServer.start();
out.printf("Java App listening on port %s.", httpPort);
}
private static CompletableFuture<HttpResponse<String>> fetch(String url) {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
return httpClient.sendAsync(request, BodyHandlers.ofString());
}
private static CompletableFuture<HttpResponse<String>> post(String url, String body) {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
.header("Content-Type", "application/json; charset=UTF-8").POST(BodyPublishers.ofString(body)).build();
return httpClient.sendAsync(request, BodyHandlers.ofString());
}
private static String readBody(HttpExchange t) throws IOException {
// retrieve the request json data
InputStream is = t.getRequestBody();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
httpServer.createContext("/order").setHandler(e -> {
out.println("Fetching order!");
try {
while ((len = is.read(buffer)) > 0)
bos.write(buffer, 0, len);
} catch (IOException e) {
e.printStackTrace();
} finally {
bos.close();
byte[] data = daprClient.getState("order", String.class).block().getValue().getBytes();
e.getResponseHeaders().set("content-type", "application/json");
e.sendResponseHeaders(200, data.length);
e.getResponseBody().write(data);
e.getResponseBody().close();
} catch (IOException ioerror) {
out.println(ioerror);
e.sendResponseHeaders(500, ioerror.getMessage().getBytes().length);
e.getResponseBody().write(ioerror.getMessage().getBytes());
e.getResponseBody().close();
}
return new String(bos.toByteArray(), Charset.forName("UTF-8"));
});
httpServer.createContext("/neworder").setHandler(e -> {
try {
out.println("Received new order ...");
String json = readBody(e);
JSONObject jsonObject = new JSONObject(json);
JSONObject data = jsonObject.getJSONObject("data");
String orderId = data.getString("orderId");
out.printf("Got a new order! Order ID: %s\n", orderId);
daprClient.saveState("order", data.toString()).block();
out.printf("Saved state: %s\n", data.toString());
e.sendResponseHeaders(200, 0);
e.getResponseBody().write(new byte[0]);
e.getResponseBody().close();
} catch (IOException ioerror) {
out.println(ioerror);
e.sendResponseHeaders(500, ioerror.getMessage().getBytes().length);
e.getResponseBody().write(ioerror.getMessage().getBytes());
e.getResponseBody().close();
}
});
httpServer.start();
out.printf("Java App listening on port %s.", httpPort);
}
private static String readBody(HttpExchange t) throws IOException {
// retrieve the request json data
InputStream is = t.getRequestBody();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = is.read(buffer)) > 0)
bos.write(buffer, 0, len);
} catch (IOException e) {
e.printStackTrace();
} finally {
bos.close();
}
return new String(bos.toByteArray(), Charset.forName("UTF-8"));
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Dapr's HTTP callback implementation via SpringBoot.
*/
@SpringBootApplication(scanBasePackages = {"io.dapr.springboot", "io.dapr.examples"})
public class DaprApplication {
/**
* Starts Dapr's callback in a given port.
* @param port Port to listen to.
*/
public static void start(int port) {
SpringApplication app = new SpringApplication(DaprApplication.class);
app.run(String.format("--server.port=%d", port));
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.springboot;
import io.dapr.actors.runtime.ActorRuntime;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
/**
* SpringBoot Controller to handle callback APIs for Dapr.
*/
@RestController
public class DaprController {
@GetMapping("/")
public String index() {
return "Greetings from Dapr!";
}
@GetMapping("/dapr/config")
public byte[] daprConfig() throws Exception {
return ActorRuntime.getInstance().serializeConfig();
}
@PostMapping(path = "/actors/{type}/{id}")
public Mono<Void> activateActor(@PathVariable("type") String type,
@PathVariable("id") String id) throws Exception {
return ActorRuntime.getInstance().activate(type, id);
}
@DeleteMapping(path = "/actors/{type}/{id}")
public Mono<Void> deactivateActor(@PathVariable("type") String type,
@PathVariable("id") String id) throws Exception {
return ActorRuntime.getInstance().deactivate(type, id);
}
@PutMapping(path = "/actors/{type}/{id}/method/{method}")
public Mono<byte[]> invokeActorMethod(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("method") String method,
@RequestBody(required = false) byte[] body) {
return ActorRuntime.getInstance().invoke(type, id, method, body);
}
@PutMapping(path = "/actors/{type}/{id}/method/timer/{timer}")
public Mono<Void> invokeActorTimer(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("timer") String timer) {
return ActorRuntime.getInstance().invokeTimer(type, id, timer);
}
@PutMapping(path = "/actors/{type}/{id}/method/remind/{reminder}")
public Mono<Void> invokeActorReminder(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("reminder") String reminder,
@RequestBody(required = false) byte[] body) {
return ActorRuntime.getInstance().invokeReminder(type, id, reminder, body);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

111
pom.xml
View File

@ -7,7 +7,7 @@
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<packaging>pom</packaging>
<version>0.3.0-alpha</version>
<version>0.2.0-SNAPSHOT</version>
<name>dapr-sdk-parent</name>
<description>SDK for Dapr.</description>
<url>https://dapr.io</url>
@ -22,8 +22,23 @@
<maven-antrun-plugin.version>1.8</maven-antrun-plugin.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.deploy.skip>true</maven.deploy.skip>
<gpg.skip>true</gpg.skip>
</properties>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<!-- Use default repository -->
<!-- <repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository> -->
</distributionManagement>
<dependencyManagement>
<dependencies>
<dependency>
@ -33,6 +48,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
@ -54,6 +74,87 @@
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<gpgArguments>
<arg>--batch</arg>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>failsafe-maven-plugin</artifactId>
<version>2.4.3-alpha-1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<skip>${skipITs}</skip>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError>
<linkXRef>false</linkXRef>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.27</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<licenses>
<license>
<name>MIT License</name>
@ -71,13 +172,15 @@
</developers>
<scm>
<connection>scm:git:git://github.com/dapr/java-sdk.git</connection>
<developerConnection>scm:git:ssh://github.com:dapr/java-sdk.git</developerConnection>
<url>http://github.com/dapr/java-sdk</url>
<url>https://github.com/dapr/java-sdk</url>
<connection>scm:git:https://github.com/dapr/java-sdk.git</connection>
<tag>HEAD</tag>
</scm>
<modules>
<module>sdk-autogen</module>
<module>sdk</module>
<module>sdk-actors</module>
<module>examples</module>
</modules>

158
sdk-actors/pom.xml Normal file
View File

@ -0,0 +1,158 @@
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>0.2.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-sdk-actors</artifactId>
<packaging>jar</packaging>
<version>0.2.0-SNAPSHOT</version>
<name>dapr-sdk-actors</name>
<description>SDK for Actors on Dapr</description>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>libs-release</name>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<properties>
<skipITs>true</skipITs>
<maven.deploy.skip>false</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.gmazzo</groupId>
<artifactId>okhttp-mock</artifactId>
<version>1.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>target/jacoco-report/</outputDirectory>
</configuration>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>80%</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors;
import java.util.UUID;
/**
* The ActorId represents the identity of an actor within an actor service.
*/
public class ActorId extends Object implements Comparable<ActorId> {
/**
* The ID of the actor as a String.
*/
private final String stringId;
/**
* An error message for an invalid constructor arg.
*/
private final String errorMsg = "actor needs to be initialized with an id!";
/**
* Initializes a new instance of the ActorId class with the id passed in.
*
* @param id Value for actor id
*/
public ActorId(String id) {
if (id != null) {
this.stringId = id;
} else {
throw new IllegalArgumentException(errorMsg);
}
}
/**
* Returns the String representation of this Actor's identifier.
*
* @return The String representation of this ActorId
*/
@Override
public String toString() {
return this.stringId;
}
/**
* Compares this instance with a specified {link #ActorId} object and
* indicates whether this instance precedes, follows, or appears in the same
* position in the sort order as the specified actorId.
* The comparison is done based on the id if both the instances.
*
* @param other The actorId to compare with this instance.
* @return A 32-bit signed integer that indicates whether this instance
* precedes, follows, or appears in the same position in the sort order as the
* other parameter.
*/
@Override
public int compareTo(ActorId other) {
return (other == null) ? 1
: compareContent(this, other);
}
/**
* Calculates the hash code for this ActorId.
*
* @return The hash code of this ActorId.
*/
@Override
public int hashCode() {
return this.stringId.hashCode();
}
/**
* Compare if the content of two ids are the same.
*
* @param id1 One identifier.
* @param id2 Another identifier.
* @return -1, 0, or 1 depending on the compare result of the stringId member.
*/
private int compareContent(ActorId id1, ActorId id2) {
return id1.stringId.compareTo(id2.stringId);
}
/**
* Checks if this instance is equals to the other instance.
*
* @return true if the 2 ActorId's are equal.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return hasEqualContent(this, (ActorId) obj);
}
/**
* Creates a new ActorId with a random id.
*
* @return A new ActorId with a random id.
*/
public static ActorId createRandom() {
UUID id = UUID.randomUUID();
return new ActorId(id.toString());
}
/**
* Compares if two actors have the same content.
*
* @param id1 One identifier.
* @param id2 Another identifier.
* @return true if the two ActorId's are equal
*/
private static boolean hasEqualContent(ActorId id1, ActorId id2) {
return id1.stringId.equals(id2.stringId);
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors;
import java.util.logging.Level;
import java.util.logging.Logger;
// TODO: Implement distributed tracing.
// TODO: Make this generic to the SDK and not only for Actors.
/**
* Class to emit trace log messages.
*/
public final class ActorTrace {
/**
* Gets the default Logger.
*/
private static final Logger LOGGER = Logger.getLogger(ActorTrace.class.getName());
/**
* Writes an information trace log.
*
* @param type Type of log.
* @param id Instance identifier.
* @param msgFormat Message or message format (with type and id input as well).
* @param params Params for the message.
*/
public void writeInfo(String type, String id, String msgFormat, Object... params) {
this.write(Level.INFO, type, id, msgFormat, params);
}
/**
* Writes an warning trace log.
*
* @param type Type of log.
* @param id Instance identifier.
* @param msgFormat Message or message format (with type and id input as well).
* @param params Params for the message.
*/
public void writeWarning(String type, String id, String msgFormat, Object... params) {
this.write(Level.WARNING, type, id, msgFormat, params);
}
/**
* Writes an error trace log.
*
* @param type Type of log.
* @param id Instance identifier.
* @param msgFormat Message or message format (with type and id input as well).
* @param params Params for the message.
*/
public void writeError(String type, String id, String msgFormat, Object... params) {
this.write(Level.SEVERE, type, id, msgFormat, params);
}
/**
* Writes a trace log.
*
* @param level Severity level of the log.
* @param type Type of log.
* @param id Instance identifier.
* @param msgFormat Message or message format (with type and id input as well).
* @param params Params for the message.
*/
private void write(Level level, String type, String id, String msgFormat, Object... params) {
String formatString = String.format("%s:%s %s", emptyIfNul(type), emptyIfNul(id), emptyIfNul(msgFormat));
if ((params == null) || (params.length == 0)) {
LOGGER.log(level, formatString);
} else {
LOGGER.log(level, String.format(formatString, params));
}
}
/**
* Utility method that returns empty if String is null.
*
* @param s String to be checked.
* @return String (if not null) or empty (if null).
*/
private static String emptyIfNul(String s) {
if (s == null) {
return "";
}
return s;
}
}

View File

@ -0,0 +1,63 @@
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import reactor.core.publisher.Mono;
/**
* Proxy to communicate to a given Actor instance in Dapr.
*/
public interface ActorProxy {
/**
* Returns the ActorId associated with the proxy object.
*
* @return An ActorId object.
*/
ActorId getActorId();
/**
* Returns actor implementation type of the actor associated with the proxy object.
*
* @return Actor's type name.
*/
String getActorType();
/**
* Invokes an Actor method on Dapr.
*
* @param methodName Method name to invoke.
* @param clazz The type of the return class.
* @param <T> The type to be returned.
* @return Asynchronous result with the Actor's response.
*/
<T> Mono<T> invokeActorMethod(String methodName, Class<T> clazz);
/**
* Invokes an Actor method on Dapr.
*
* @param methodName Method name to invoke.
* @param data Object with the data.
* @param clazz The type of the return class.
* @param <T> The type to be returned.
* @return Asynchronous result with the Actor's response.
*/
<T> Mono<T> invokeActorMethod(String methodName, Object data, Class<T> clazz);
/**
* Invokes an Actor method on Dapr.
*
* @param methodName Method name to invoke.
* @return Asynchronous result with the Actor's response.
*/
Mono<Void> invokeActorMethod(String methodName);
/**
* Invokes an Actor method on Dapr.
*
* @param methodName Method name to invoke.
* @param data Object with the data.
* @return Asynchronous result with the Actor's response.
*/
Mono<Void> invokeActorMethod(String methodName, Object data);
}

View File

@ -0,0 +1,63 @@
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import io.dapr.client.DaprHttpBuilder;
import io.dapr.serializer.DaprObjectSerializer;
/**
* Builder to generate an ActorProxy instance. Builder can be reused for multiple instances.
*/
public class ActorProxyBuilder {
/**
* Builder for Dapr's raw http client.
*/
private final DaprHttpBuilder daprHttpBuilder = new DaprHttpBuilder();
/**
* Actor's type.
*/
private final String actorType;
/**
* Dapr's object serializer.
*/
private final DaprObjectSerializer objectSerializer;
/**
* Instantiates a new builder for a given Actor type.
*
* @param actorType Actor's type.
* @param objectSerializer Serializer for objects sent/received.
*/
public ActorProxyBuilder(String actorType, DaprObjectSerializer objectSerializer) {
if ((actorType == null) || actorType.isEmpty()) {
throw new IllegalArgumentException("ActorType is required.");
}
if (objectSerializer == null) {
throw new IllegalArgumentException("Serializer is required.");
}
this.actorType = actorType;
this.objectSerializer = objectSerializer;
}
/**
* Instantiates a new ActorProxy.
*
* @param actorId Actor's identifier.
* @return New instance of ActorProxy.
*/
public ActorProxy build(ActorId actorId) {
if (actorId == null) {
throw new IllegalArgumentException("Cannot instantiate an Actor without Id.");
}
return new ActorProxyImpl(
this.actorType,
actorId,
this.objectSerializer,
new DaprHttpClient(this.daprHttpBuilder.build()));
}
}

View File

@ -0,0 +1,136 @@
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import io.dapr.actors.runtime.ObjectSerializer;
import io.dapr.serializer.DaprObjectSerializer;
import java.io.IOException;
import reactor.core.publisher.Mono;
/**
* Implements a proxy client for an Actor's instance.
*/
class ActorProxyImpl implements ActorProxy {
/**
* Serializer used for internal objects.
*/
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
/**
* Actor's identifier for this Actor instance.
*/
private final ActorId actorId;
/**
* Actor's type for this Actor instance.
*/
private final String actorType;
/**
* Serializer/deserialzier to exchange message for Actors.
*/
private final DaprObjectSerializer serializer;
/**
* Client to talk to the Dapr's API.
*/
private final DaprClient daprClient;
/**
* Creates a new instance of {@link ActorProxyImpl}.
*
* @param actorType actor implementation type of the actor associated with the proxy object.
* @param actorId The actorId associated with the proxy
* @param serializer Serializer and deserializer for method calls.
* @param daprClient Dapr client.
*/
ActorProxyImpl(String actorType, ActorId actorId, DaprObjectSerializer serializer, DaprClient daprClient) {
this.actorType = actorType;
this.actorId = actorId;
this.daprClient = daprClient;
this.serializer = serializer;
}
/**
* {@inheritDoc}
*/
public ActorId getActorId() {
return actorId;
}
/**
* {@inheritDoc}
*/
public String getActorType() {
return actorType;
}
/**
* {@inheritDoc}
*/
@Override
public <T> Mono<T> invokeActorMethod(String methodName, Object data, Class<T> clazz) {
return this.daprClient.invokeActorMethod(actorType, actorId.toString(), methodName, this.wrap(data))
.filter(s -> s.length > 0)
.map(s -> unwrap(s, clazz));
}
/**
* {@inheritDoc}
*/
@Override
public <T> Mono<T> invokeActorMethod(String methodName, Class<T> clazz) {
return this.daprClient.invokeActorMethod(actorType, actorId.toString(), methodName, null)
.filter(s -> s.length > 0)
.map(s -> unwrap(s, clazz));
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> invokeActorMethod(String methodName) {
return this.daprClient.invokeActorMethod(actorType, actorId.toString(), methodName, null).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> invokeActorMethod(String methodName, Object data) {
return this.daprClient.invokeActorMethod(actorType, actorId.toString(), methodName, this.wrap(data)).then();
}
/**
* Extracts the response object from the Actor's method result.
*
* @param response response returned by API.
* @param clazz Expected response class.
* @param <T> Expected response type.
* @return Response object or null.
* @throws RuntimeException In case it cannot generate Object.
*/
private <T> T unwrap(final byte[] response, Class<T> clazz) {
try {
return this.serializer.deserialize(INTERNAL_SERIALIZER.unwrapData(response), clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Builds the request to invoke an API for Actors.
*
* @param request Request object for the original Actor's method.
* @return Payload to be sent to Dapr's API.
* @throws RuntimeException In case it cannot generate payload.
*/
private byte[] wrap(final Object request) {
try {
return INTERNAL_SERIALIZER.wrapData(this.serializer.serialize(request));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.client;
import reactor.core.publisher.Mono;
/**
* Generic Client Adapter to be used regardless of the GRPC or the HTTP Client implementation required.
*/
interface DaprClient {
/**
* Invokes an Actor method on Dapr.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param methodName Method name to invoke.
* @param jsonPayload Serialized body.
* @return Asynchronous result with the Actor's response.
*/
Mono<byte[]> invokeActorMethod(String actorType, String actorId, String methodName, byte[] jsonPayload);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.client;
import io.dapr.client.DaprHttp;
import io.dapr.utils.Constants;
import reactor.core.publisher.Mono;
/**
* DaprClient over HTTP for actor client.
*
* @see DaprHttp
*/
class DaprHttpClient implements DaprClient {
/**
* The HTTP client to be used.
*
* @see DaprHttp
*/
private final DaprHttp client;
/**
* Instantiates a new Dapr Http Client to invoke Actors.
*
* @param client Dapr's http client.
*/
DaprHttpClient(DaprHttp client) {
this.client = client;
}
/**
* {@inheritDoc}
*/
@Override
public Mono<byte[]> invokeActorMethod(String actorType, String actorId, String methodName, byte[] jsonPayload) {
String url = String.format(Constants.ACTOR_METHOD_RELATIVE_URL_FORMAT, actorType, actorId, methodName);
Mono<DaprHttp.Response> responseMono =
this.client.invokeAPI(DaprHttp.HttpMethods.POST.name(), url, null, jsonPayload, null);
return responseMono.map(r -> r.getBody());
}
}

View File

@ -0,0 +1,349 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.ActorTrace;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import reactor.core.publisher.Mono;
/**
* Represents the base class for actors.
* The base type for actors, that provides the common functionality for actors.
* The state is preserved across actor garbage collections and fail-overs.
*/
public abstract class AbstractActor {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
/**
* Type of tracing messages.
*/
private static final String TRACE_TYPE = "Actor";
/**
* Context for the Actor runtime.
*/
private final ActorRuntimeContext<?> actorRuntimeContext;
/**
* Actor identifier.
*/
private final ActorId id;
/**
* Emits trace messages for Actors.
*/
private final ActorTrace actorTrace;
/**
* Registered timers for this Actor.
*/
private final Map<String, ActorTimer> timers;
/**
* Manager for the states in Actors.
*/
private final ActorStateManager actorStateManager;
/**
* Internal control to assert method invocation on start and finish in this SDK.
*/
private boolean started;
/**
* Instantiates a new Actor.
*
* @param runtimeContext Context for the runtime.
* @param id Actor identifier.
*/
protected AbstractActor(ActorRuntimeContext runtimeContext, ActorId id) {
this.actorRuntimeContext = runtimeContext;
this.id = id;
this.actorStateManager = new ActorStateManager(
runtimeContext.getStateProvider(),
runtimeContext.getActorTypeInformation().getName(),
id);
this.actorTrace = runtimeContext.getActorTrace();
this.timers = Collections.synchronizedMap(new HashMap<>());
this.started = false;
}
/**
* Returns the id of the actor.
*
* @return Actor id.
*/
protected ActorId getId() {
return this.id;
}
/**
* Returns the state store manager for this Actor.
*
* @return State store manager for this Actor
*/
protected ActorStateManager getActorStateManager() {
return this.actorStateManager;
}
/**
* Registers a reminder for this Actor.
*
* @param reminderName Name of the reminder.
* @param state State to be send along with reminder triggers.
* @param dueTime Due time for the first trigger.
* @param period Frequency for the triggers.
* @param <T> Type of the state object.
* @return Asynchronous void response.
*/
protected <T> Mono<Void> registerReminder(
String reminderName,
T state,
Duration dueTime,
Duration period) {
try {
byte[] data = this.actorRuntimeContext.getObjectSerializer().serialize(state);
ActorReminderParams params = new ActorReminderParams(data, dueTime, period);
byte[] serialized = INTERNAL_SERIALIZER.serialize(params);
return this.actorRuntimeContext.getDaprClient().registerActorReminder(
this.actorRuntimeContext.getActorTypeInformation().getName(),
this.id.toString(),
reminderName,
serialized);
} catch (IOException e) {
return Mono.error(e);
}
}
/**
* Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it.
*
* @param timerName Name of the timer, unique per Actor (auto-generated if null).
* @param callback Name of the method to be called.
* @param state State to be passed it to the method when timer triggers.
* @param dueTime The amount of time to delay before the async callback is first invoked.
* Specify negative one (-1) milliseconds to prevent the timer from starting.
* Specify zero (0) to start the timer immediately.
* @param period The time interval between invocations of the async callback.
* Specify negative one (-1) milliseconds to disable periodic signaling.
* @param <T> Type for the state to be passed in to timer.
* @return Asynchronous result.
*/
protected <T> Mono<Void> registerActorTimer(
String timerName,
String callback,
T state,
Duration dueTime,
Duration period) {
return Mono.fromSupplier(() -> {
if ((callback == null) || callback.isEmpty()) {
throw new IllegalArgumentException("Timer requires a callback function.");
}
String name = timerName;
if ((timerName == null) || (timerName.isEmpty())) {
name = String.format("%s_Timer_%d", this.id.toString(), this.timers.size() + 1);
}
ActorTimer actorTimer = new ActorTimer(this, name, callback, state, dueTime, period);
this.timers.put(name, actorTimer);
return actorTimer;
}).flatMap(actorTimer -> {
try {
return this.actorRuntimeContext.getDaprClient().registerActorTimer(
this.actorRuntimeContext.getActorTypeInformation().getName(),
this.id.toString(),
actorTimer.getName(),
INTERNAL_SERIALIZER.serialize(actorTimer));
} catch (Exception e) {
return Mono.error(e);
}
});
}
/**
* Unregisters an Actor timer.
*
* @param timerName Name of Timer to be unregistered.
* @return Asynchronous void response.
*/
protected Mono<Void> unregisterTimer(String timerName) {
return Mono.fromSupplier(() -> getActorTimer(timerName))
.flatMap(actorTimer -> this.actorRuntimeContext.getDaprClient().unregisterActorTimer(
this.actorRuntimeContext.getActorTypeInformation().getName(),
this.id.toString(),
timerName))
.then(Mono.fromRunnable(() -> this.timers.remove(timerName)));
}
/**
* Unregisters a Reminder.
*
* @param reminderName Name of Reminder to be unregistered.
* @return Asynchronous void response.
*/
protected Mono<Void> unregisterReminder(String reminderName) {
return this.actorRuntimeContext.getDaprClient().unregisterActorReminder(
this.actorRuntimeContext.getActorTypeInformation().getName(),
this.id.toString(),
reminderName);
}
/**
* Callback function invoked after an Actor has been activated.
*
* @return Asynchronous void response.
*/
protected Mono<Void> onActivate() {
return Mono.empty();
}
/**
* Callback function invoked after an Actor has been deactivated.
*
* @return Asynchronous void response.
*/
protected Mono<Void> onDeactivate() {
return Mono.empty();
}
/**
* Callback function invoked before method is invoked.
*
* @param actorMethodContext Method context.
* @return Asynchronous void response.
*/
protected Mono<Void> onPreActorMethod(ActorMethodContext actorMethodContext) {
return Mono.empty();
}
/**
* Callback function invoked after method is invoked.
*
* @param actorMethodContext Method context.
* @return Asynchronous void response.
*/
protected Mono<Void> onPostActorMethod(ActorMethodContext actorMethodContext) {
return Mono.empty();
}
/**
* Saves the state of this Actor.
*
* @return Asynchronous void response.
*/
protected Mono<Void> saveState() {
return this.actorStateManager.save();
}
/**
* Resets the cached state of this Actor.
*/
void rollback() {
if (!this.started) {
throw new IllegalStateException("Cannot reset state before starting call.");
}
this.resetState();
this.started = false;
}
/**
* Resets the cached state of this Actor.
*/
void resetState() {
this.actorStateManager.clear();
}
/**
* Gets a given timer by name.
*
* @param timerName Timer name.
* @return Asynchronous void response.
*/
ActorTimer getActorTimer(String timerName) {
return timers.getOrDefault(timerName, null);
}
/**
* Internal callback when an Actor is activated.
*
* @return Asynchronous void response.
*/
Mono<Void> onActivateInternal() {
return Mono.fromRunnable(() -> {
this.actorTrace.writeInfo(TRACE_TYPE, this.id.toString(), "Activating ...");
this.resetState();
}).then(this.onActivate())
.then(this.doWriteInfo(TRACE_TYPE, this.id.toString(), "Activated"))
.then(this.saveState());
}
/**
* Internal callback when an Actor is deactivated.
*
* @return Asynchronous void response.
*/
Mono<Void> onDeactivateInternal() {
this.actorTrace.writeInfo(TRACE_TYPE, this.id.toString(), "Deactivating ...");
return Mono.fromRunnable(() -> this.resetState())
.then(this.onDeactivate())
.then(this.doWriteInfo(TRACE_TYPE, this.id.toString(), "Deactivated"));
}
/**
* Internal callback prior to method be invoked.
*
* @param actorMethodContext Method context.
* @return Asynchronous void response.
*/
Mono<Void> onPreActorMethodInternal(ActorMethodContext actorMethodContext) {
return Mono.fromRunnable(() -> {
if (this.started) {
throw new IllegalStateException("Cannot invoke a method before completing previous call.");
}
this.started = true;
}).then(this.onPreActorMethod(actorMethodContext));
}
/**
* Internal callback after method is invoked.
*
* @param actorMethodContext Method context.
* @return Asynchronous void response.
*/
Mono<Void> onPostActorMethodInternal(ActorMethodContext actorMethodContext) {
return Mono.fromRunnable(() -> {
if (!this.started) {
throw new IllegalStateException("Cannot complete a method before starting a call.");
}
}).then(this.onPostActorMethod(actorMethodContext))
.then(this.saveState())
.then(Mono.fromRunnable(() -> {
this.started = false;
}));
}
/**
* Internal method to emit a trace message.
*
* @param type Type of trace message.
* @param id Identifier of entity relevant for the trace message.
* @param message Message to be logged.
* @return Asynchronous void response.
*/
private Mono<Void> doWriteInfo(String type, String id, String message) {
return Mono.fromRunnable(() -> this.actorTrace.writeInfo(type, id, message));
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
/**
* Represents the call-type associated with the method invoked by actor runtime.
*/
enum ActorCallType {
/**
* Specifies that the method invoked is an actor interface method for a given
* client request.
*/
ACTOR_INTERFACE_METHOD,
/**
* Specifies that the method invoked is a timer callback method.
*/
TIMER_METHOD,
/**
* Specifies that the method is when a reminder fires.
*/
REMINDER_METHOD
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
/**
* Creates an actor of a given type.
*
* @param <T> Actor Type to be created.
*/
@FunctionalInterface
public interface ActorFactory<T extends AbstractActor> {
/**
* Creates an Actor.
*
* @param actorRuntimeContext Actor type's context in the runtime.
* @param actorId Actor Id.
* @return Actor or null it failed.
*/
T createActor(ActorRuntimeContext<T> actorRuntimeContext, ActorId actorId);
}

View File

@ -0,0 +1,342 @@
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import reactor.core.publisher.Mono;
/**
* Manages actors of a specific type.
*/
class ActorManager<T extends AbstractActor> {
/**
* Serializer for internal Dapr objects.
*/
private static final ObjectSerializer OBJECT_SERIALIZER = new ObjectSerializer();
/**
* Context for the Actor runtime.
*/
private final ActorRuntimeContext<T> runtimeContext;
/**
* Methods found in Actors.
*/
private final ActorMethodInfoMap actorMethods;
/**
* Active Actor instances.
*/
private final Map<ActorId, T> activeActors;
/**
* Instantiates a new manager for a given actor referenced in the runtimeContext.
*
* @param runtimeContext Runtime context for the Actor.
*/
ActorManager(ActorRuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext;
this.actorMethods = new ActorMethodInfoMap(runtimeContext.getActorTypeInformation().getInterfaces());
this.activeActors = Collections.synchronizedMap(new HashMap<>());
}
/**
* Activates an Actor.
*
* @param actorId Actor identifier.
* @return Asynchronous void response.
*/
Mono<Void> activateActor(ActorId actorId) {
return Mono.fromSupplier(() -> this.runtimeContext.getActorFactory().createActor(runtimeContext, actorId))
.flatMap(actor -> actor.onActivateInternal().then(this.onActivatedActor(actorId, actor)));
}
/**
* Deactivates an Actor.
*
* @param actorId Actor identifier.
* @return Asynchronous void response.
*/
Mono<Void> deactivateActor(ActorId actorId) {
return Mono.fromSupplier(() -> this.activeActors.remove(actorId)).flatMap(actor -> actor.onDeactivateInternal());
}
/**
* Invokes reminder for Actor.
*
* @param actorId Identifier for Actor being invoked.
* @param reminderName Name of reminder being invoked.
* @param params Parameters for the reminder.
* @return Asynchronous void response.
*/
Mono<Void> invokeReminder(ActorId actorId, String reminderName, byte[] params) {
return Mono.fromSupplier(() -> {
if (!this.runtimeContext.getActorTypeInformation().isRemindable()) {
return null;
}
try {
return OBJECT_SERIALIZER.deserialize(params, ActorReminderParams.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).flatMap(p ->
invoke(actorId,
ActorMethodContext.createForReminder(reminderName),
actor -> doReminderInvokation((Remindable) actor, reminderName, p))).then();
}
/**
* Invokes a timer for a given Actor.
*
* @param actorId Identifier for Actor.
* @param timerName Name of timer being invoked.
* @return Asynchronous void response.
*/
Mono<Void> invokeTimer(ActorId actorId, String timerName) {
return Mono.fromSupplier(() -> {
AbstractActor actor = this.activeActors.getOrDefault(actorId, null);
if (actor == null) {
throw new IllegalArgumentException(
String.format("Could not find actor %s of type %s.",
actorId.toString(),
this.runtimeContext.getActorTypeInformation().getName()));
}
ActorTimer actorTimer = actor.getActorTimer(timerName);
if (actorTimer == null) {
throw new IllegalStateException(
String.format("Could not find timer %s for actor %s.",
timerName,
this.runtimeContext.getActorTypeInformation().getName()));
}
return actorTimer;
}).flatMap(actorTimer -> invokeMethod(
actorId,
ActorMethodContext.createForTimer(actorTimer.getName()),
actorTimer.getCallback(),
actorTimer.getState()))
.then();
}
/**
* Internal callback for when Actor is activated.
*
* @param actorId Actor identifier.
* @param actor Actor's instance.
* @return Asynchronous void response.
*/
private Mono<Void> onActivatedActor(ActorId actorId, T actor) {
return Mono.fromRunnable(() -> this.activeActors.put(actorId, actor));
}
/**
* Internal method to actually invoke a reminder.
*
* @param actor Actor that owns the reminder.
* @param reminderName Name of the reminder.
* @param reminderParams Params for the reminder.
* @return Asynchronous void response.
*/
private Mono<Boolean> doReminderInvokation(
Remindable actor,
String reminderName,
ActorReminderParams reminderParams) {
return Mono.fromSupplier(() -> {
if (actor == null) {
throw new IllegalArgumentException("actor is mandatory.");
}
if (reminderName == null) {
throw new IllegalArgumentException("reminderName is mandatory.");
}
if (reminderParams == null) {
throw new IllegalArgumentException("reminderParams is mandatory.");
}
return true;
}).flatMap(x -> {
try {
Object data = this.runtimeContext.getObjectSerializer().deserialize(
reminderParams.getData(),
actor.getStateType());
return actor.receiveReminder(
reminderName,
data,
reminderParams.getDueTime(),
reminderParams.getPeriod());
} catch (Exception e) {
return Mono.error(e);
}
}).thenReturn(true);
}
/**
* Invokes a given method in the Actor.
*
* @param actorId Identifier for Actor being invoked.
* @param methodName Name of method being invoked.
* @param request Input object for the method being invoked.
* @return Asynchronous void response.
*/
Mono<byte[]> invokeMethod(ActorId actorId, String methodName, byte[] request) {
return invokeMethod(actorId, null, methodName, request);
}
/**
* Internal method to actually invoke Actor's timer method.
*
* @param actorId Identifier for the Actor.
* @param context Method context to be invoked.
* @param methodName Method name to be invoked.
* @param input Input object to be passed in to the invoked method.
* @return Asynchronous void response.
*/
private Mono<Object> invokeMethod(ActorId actorId, ActorMethodContext context, String methodName, Object input) {
ActorMethodContext actorMethodContext = context;
if (actorMethodContext == null) {
actorMethodContext = ActorMethodContext.createForActor(methodName);
}
return this.invoke(actorId, actorMethodContext, actor -> {
try {
// Finds the actor method with the given name and 1 or no parameter.
Method method = this.actorMethods.get(methodName);
if (method.getReturnType().equals(Mono.class)) {
return invokeMonoMethod(actor, method, input);
}
return invokeMethod(actor, method, input);
} catch (Exception e) {
return Mono.error(e);
}
});
}
/**
* Internal method to actually invoke Actor's method.
*
* @param actorId Identifier for the Actor.
* @param context Method context to be invoked.
* @param methodName Method name to be invoked.
* @param request Input object to be passed in to the invoked method.
* @return Asynchronous serialized response.
*/
private Mono<byte[]> invokeMethod(ActorId actorId, ActorMethodContext context, String methodName, byte[] request) {
ActorMethodContext actorMethodContext = context;
if (actorMethodContext == null) {
actorMethodContext = ActorMethodContext.createForActor(methodName);
}
return this.invoke(actorId, actorMethodContext, actor -> {
try {
// Finds the actor method with the given name and 1 or no parameter.
Method method = this.actorMethods.get(methodName);
Object input = null;
if (method.getParameterCount() == 1) {
// Actor methods must have a one or no parameter, which is guaranteed at this point.
Class<?> inputClass = method.getParameterTypes()[0];
input = this.runtimeContext.getObjectSerializer().deserialize(request, inputClass);
}
if (method.getReturnType().equals(Mono.class)) {
return invokeMonoMethod(actor, method, input);
}
return invokeMethod(actor, method, input);
} catch (Exception e) {
return Mono.error(e);
}
}).map(r -> {
try {
return this.runtimeContext.getObjectSerializer().serialize(r);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* Invokes a method that returns a plain object (not Mono).
*
* @param actor Actor to be invoked.
* @param method Method to be invoked.
* @param input Input object for the method (or null).
* @return Asynchronous object response.
*/
private Mono<Object> invokeMethod(AbstractActor actor, Method method, Object input) {
return Mono.fromSupplier(() -> {
try {
if (method.getParameterCount() == 0) {
return method.invoke(actor);
} else {
// Actor methods must have a one or no parameter, which is guaranteed at this point.
return method.invoke(actor, input);
}
} catch (Exception e) {
return Mono.error(e);
}
});
}
/**
* Invokes a method that returns Mono.
*
* @param actor Actor to be invoked.
* @param method Method to be invoked.
* @param input Input object for the method (or null).
* @return Asynchronous object response.
*/
private Mono<Object> invokeMonoMethod(AbstractActor actor, Method method, Object input) {
try {
if (method.getParameterCount() == 0) {
return (Mono<Object>) method.invoke(actor);
} else {
// Actor methods must have a one or no parameter, which is guaranteed at this point.
return (Mono<Object>) method.invoke(actor, input);
}
} catch (Exception e) {
return Mono.error(e);
}
}
/**
* Internal call to invoke a method, timer or reminder for an Actor.
*
* @param actorId Actor identifier.
* @param context Context for the method/timer/reminder call.
* @param func Function to perform the method call.
* @param <T> Expected return type for the function call.
* @return Asynchronous response for the returned object.
*/
private <T> Mono<T> invoke(ActorId actorId, ActorMethodContext context, Function<AbstractActor, Mono<T>> func) {
try {
AbstractActor actor = this.activeActors.getOrDefault(actorId, null);
if (actor == null) {
throw new IllegalArgumentException(
String.format("Could not find actor %s of type %s.",
actorId.toString(),
this.runtimeContext.getActorTypeInformation().getName()));
}
return actor.onPreActorMethodInternal(context)
.then((Mono<Object>) func.apply(actor))
.switchIfEmpty(
actor.onPostActorMethodInternal(context))
.flatMap(r -> actor.onPostActorMethodInternal(context).thenReturn(r))
.onErrorMap(throwable -> {
actor.rollback();
return throwable;
})
.map(o -> (T) o);
} catch (Exception e) {
return Mono.error(e);
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
/**
* Contains information about the method that is invoked by actor runtime.
*/
public class ActorMethodContext {
/**
* Method name to be invoked.
*/
private final String methodName;
/**
* Call type to be used.
*/
private final ActorCallType callType;
/**
* Constructs a new instance of {@link ActorMethodContext}, representing a call for an Actor.
*
* @param methodName Method name to be invoked.
* @param callType Call type to be used.
*/
private ActorMethodContext(String methodName, ActorCallType callType) {
this.methodName = methodName;
this.callType = callType;
}
/**
* Gets the name of the method invoked by actor runtime.
*
* @return The method name.
*/
public String getMethodName() {
return this.methodName;
}
/**
* Gets the call type to be used.
*
* @return Call type.
*/
public ActorCallType getCallType() {
return this.callType;
}
/**
* Creates a context to invoke an Actor's method.
*
* @param methodName THe method to be invoked.
* @return Context of the method call as {@link ActorMethodContext}
*/
static ActorMethodContext createForActor(String methodName) {
return new ActorMethodContext(methodName, ActorCallType.ACTOR_INTERFACE_METHOD);
}
/**
* Creates a context to invoke an Actor's timer.
*
* @param methodName THe method to be invoked.
* @return Context of the method call as {@link ActorMethodContext}
*/
static ActorMethodContext createForTimer(String methodName) {
return new ActorMethodContext(methodName, ActorCallType.TIMER_METHOD);
}
/**
* Creates a context to invoke an Actor's reminder.
*
* @param methodName THe method to be invoked.
* @return Context of the method call as {@link ActorMethodContext}
*/
static ActorMethodContext createForReminder(String methodName) {
return new ActorMethodContext(methodName, ActorCallType.REMINDER_METHOD);
}
}

View File

@ -0,0 +1,56 @@
package io.dapr.actors.runtime;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Actor method dispatcher map. Holds method_name -> Method for methods defined in Actor interfaces.
*/
class ActorMethodInfoMap {
/**
* Map for methods based on name.
*/
private final Map<String, Method> methods;
/**
* Instantiates a given Actor map based on the interfaces found in the class.
*
* @param interfaceTypes Interfaces found in the Actor class.
*/
ActorMethodInfoMap(Collection<Class<?>> interfaceTypes) {
Map<String, Method> methods = new HashMap<>();
// Find methods which are defined in Actor interface.
for (Class<?> actorInterface : interfaceTypes) {
for (Method methodInfo : actorInterface.getMethods()) {
// Only support methods with 1 or 0 argument.
if (methodInfo.getParameterCount() <= 1) {
// If Actor class uses overloading, then one will win.
// Document this behavior, so users know how to write their code.
methods.put(methodInfo.getName(), methodInfo);
}
}
}
this.methods = Collections.unmodifiableMap(methods);
}
/**
* Gets the Actor's method by name.
*
* @param methodName Name of the method.
* @return Method.
* @throws NoSuchMethodException If method is not found.
*/
Method get(String methodName) throws NoSuchMethodException {
Method method = this.methods.get(methodName);
if (method == null) {
throw new NoSuchMethodException(String.format("Could not find method %s.", methodName));
}
return method;
}
}

View File

@ -0,0 +1,104 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package io.dapr.actors.runtime;
import java.time.Duration;
/**
* Parameters for Actor Reminder.
*/
final class ActorReminderParams {
/**
* Minimum duration for period.
*/
private static final Duration MIN_TIME_PERIOD = Duration.ofMillis(-1);
/**
* Data to be passed in as part of the reminder trigger.
*/
private final byte[] data;
/**
* Time the reminder is due for the 1st time.
*/
private final Duration dueTime;
/**
* Interval between triggers.
*/
private final Duration period;
/**
* Instantiates a new instance for the params of a reminder.
*
* @param data Data to be passed in as part of the reminder trigger.
* @param dueTime Time the reminder is due for the 1st time.
* @param period Interval between triggers.
*/
ActorReminderParams(byte[] data, Duration dueTime, Duration period) {
validateDueTime("DueTime", dueTime);
validatePeriod("Period", period);
this.data = data;
this.dueTime = dueTime;
this.period = period;
}
/**
* Gets the time the reminder is due for the 1st time.
*
* @return Time the reminder is due for the 1st time.
*/
Duration getDueTime() {
return dueTime;
}
/**
* Gets the interval between triggers.
*
* @return Interval between triggers.
*/
Duration getPeriod() {
return period;
}
/**
* Gets the data to be passed in as part of the reminder trigger.
*
* @return Data to be passed in as part of the reminder trigger.
*/
byte[] getData() {
return data;
}
/**
* Validates due time is valid, throws {@link IllegalArgumentException}.
*
* @param argName Name of the argument passed in.
* @param value Vale being checked.
*/
private static void validateDueTime(String argName, Duration value) {
if (value.compareTo(Duration.ZERO) < 0) {
String message = String.format(
"argName: %s - Duration toMillis() - specified value must be greater than %s", argName, Duration.ZERO);
throw new IllegalArgumentException(message);
}
}
/**
* Validates reminder period is valid, throws {@link IllegalArgumentException}.
*
* @param argName Name of the argument passed in.
* @param value Vale being checked.
*/
private static void validatePeriod(String argName, Duration value) throws IllegalArgumentException {
if (value.compareTo(MIN_TIME_PERIOD) < 0) {
String message = String.format(
"argName: %s - Duration toMillis() - specified value must be greater than %s", argName, MIN_TIME_PERIOD);
throw new IllegalArgumentException(message);
}
}
}

View File

@ -0,0 +1,279 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.ActorTrace;
import io.dapr.client.DaprHttpBuilder;
import io.dapr.serializer.DaprObjectSerializer;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import reactor.core.publisher.Mono;
/**
* Contains methods to register actor types. Registering the types allows the
* runtime to create instances of the actor.
*/
public class ActorRuntime {
/**
* Serializer for internal Dapr objects.
*/
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
/**
* A trace type used when logging.
*/
private static final String TRACE_TYPE = "ActorRuntime";
/**
* Tracing errors, warnings and info logs.
*/
private static final ActorTrace ACTOR_TRACE = new ActorTrace();
/**
* Gets an instance to the ActorRuntime. There is only 1.
*/
private static volatile ActorRuntime instance;
/**
* Configuration for the Actor runtime.
*/
private final ActorRuntimeConfig config;
/**
* A client used to communicate from the actor to the Dapr runtime.
*/
private final DaprClient daprClient;
/**
* Map of ActorType --> ActorManager.
*/
private final Map<String, ActorManager> actorManagers;
/**
* The default constructor. This should not be called directly.
*
* @throws IllegalStateException If cannot instantiate Runtime.
*/
private ActorRuntime() throws IllegalStateException {
this(new DaprHttpClient(new DaprHttpBuilder().build()));
}
/**
* Constructor with dependency injection, useful for testing. This should not be called directly.
*
* @param daprClient Client to communicate with Dapr.
* @throws IllegalStateException If class has one instance already.
*/
private ActorRuntime(DaprClient daprClient) throws IllegalStateException {
if (instance != null) {
throw new IllegalStateException("ActorRuntime should only be constructed once");
}
this.config = new ActorRuntimeConfig();
this.actorManagers = Collections.synchronizedMap(new HashMap<>());
this.daprClient = daprClient;
}
/**
* Returns an ActorRuntime object.
*
* @return An ActorRuntime object.
*/
public static ActorRuntime getInstance() {
if (instance == null) {
synchronized (ActorRuntime.class) {
if (instance == null) {
instance = new ActorRuntime();
}
}
}
return instance;
}
/**
* Gets the Actor configuration for this runtime.
*
* @return Actor configuration serialized.
* @throws IOException If cannot serialize config.
*/
public byte[] serializeConfig() throws IOException {
return this.INTERNAL_SERIALIZER.serialize(this.config);
}
/**
* Registers an actor with the runtime.
*
* @param clazz The type of actor.
* @param objectSerializer Serializer for Actor's request and response objects.
* @param stateSerializer Serializer for Actor's state objects.
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(
Class<T> clazz, DaprObjectSerializer objectSerializer, DaprObjectSerializer stateSerializer) {
registerActor(clazz, null, objectSerializer, stateSerializer);
}
/**
* Registers an actor with the runtime.
*
* @param clazz The type of actor.
* @param actorFactory An optional factory to create actors. This can be used for dependency injection.
* @param objectSerializer Serializer for Actor's request and response objects.
* @param stateSerializer Serializer for Actor's state objects.
* @param <T> Actor class type.
*/
public <T extends AbstractActor> void registerActor(
Class<T> clazz, ActorFactory<T> actorFactory,
DaprObjectSerializer objectSerializer,
DaprObjectSerializer stateSerializer) {
if (clazz == null) {
throw new IllegalArgumentException("Class is required.");
}
if (objectSerializer == null) {
throw new IllegalArgumentException("Serializer is required.");
}
if (stateSerializer == null) {
throw new IllegalArgumentException("State objectSerializer is required.");
}
ActorTypeInformation<T> actorTypeInfo = ActorTypeInformation.create(clazz);
ActorFactory<T> actualActorFactory = actorFactory != null ? actorFactory : new DefaultActorFactory<T>();
ActorRuntimeContext<T> context = new ActorRuntimeContext<>(
this,
objectSerializer,
actualActorFactory,
actorTypeInfo,
this.daprClient,
new DaprStateAsyncProvider(this.daprClient, stateSerializer));
// Create ActorManagers, override existing entry if registered again.
this.actorManagers.put(actorTypeInfo.getName(), new ActorManager<T>(context));
this.config.addRegisteredActorType(actorTypeInfo.getName());
}
/**
* Activates an actor for an actor type with given actor id.
*
* @param actorTypeName Actor type name to activate the actor for.
* @param actorId Actor id for the actor to be activated.
* @return Async void task.
*/
public Mono<Void> activate(String actorTypeName, String actorId) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.activateActor(new ActorId(actorId)));
}
/**
* Deactivates an actor for an actor type with given actor id.
*
* @param actorTypeName Actor type name to deactivate the actor for.
* @param actorId Actor id for the actor to be deactivated.
* @return Async void task.
*/
public Mono<Void> deactivate(String actorTypeName, String actorId) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.deactivateActor(new ActorId(actorId)));
}
/**
* Invokes the specified method for the actor, this is mainly used for cross
* language invocation.
*
* @param actorTypeName Actor type name to invoke the method for.
* @param actorId Actor id for the actor for which method will be invoked.
* @param actorMethodName Method name on actor type which will be invoked.
* @param payload RAW payload for the actor method.
* @return Response for the actor method.
*/
public Mono<byte[]> invoke(String actorTypeName, String actorId, String actorMethodName, byte[] payload) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.invokeMethod(new ActorId(actorId), actorMethodName, unwrap(payload)))
.map(response -> wrap((byte[]) response));
}
/**
* Fires a reminder for the Actor.
*
* @param actorTypeName Actor type name to invoke the method for.
* @param actorId Actor id for the actor for which method will be invoked.
* @param reminderName The name of reminder provided during registration.
* @param params Params for the reminder.
* @return Async void task.
*/
public Mono<Void> invokeReminder(String actorTypeName, String actorId, String reminderName, byte[] params) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.invokeReminder(new ActorId(actorId), reminderName, params));
}
/**
* Fires a timer for the Actor.
*
* @param actorTypeName Actor type name to invoke the method for.
* @param actorId Actor id for the actor for which method will be invoked.
* @param timerName The name of timer provided during registration.
* @return Async void task.
*/
public Mono<Void> invokeTimer(String actorTypeName, String actorId, String timerName) {
return Mono.fromSupplier(() -> this.getActorManager(actorTypeName))
.flatMap(m -> m.invokeTimer(new ActorId(actorId), timerName));
}
/**
* Finds the actor manager or errors out.
*
* @param actorTypeName Actor type for the actor manager to be found.
* @return Actor manager instance, never null.
* @throws IllegalStateException if cannot find actor's manager.
*/
private ActorManager getActorManager(String actorTypeName) {
ActorManager actorManager = this.actorManagers.get(actorTypeName);
if (actorManager == null) {
String errorMsg = String.format("Actor type %s is not registered with Actor runtime.", actorTypeName);
ACTOR_TRACE.writeError(TRACE_TYPE, actorTypeName, "Actor type is not registered with runtime.");
throw new IllegalStateException(errorMsg);
}
return actorManager;
}
/**
* Extracts the data as String from the Actor's method result.
*
* @param payload String returned by API.
* @return data or null.
* @throws RuntimeException In case it cannot extract data.
*/
private byte[] unwrap(final byte[] payload) {
try {
return INTERNAL_SERIALIZER.unwrapData(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Builds the request to invoke an API for Actors.
*
* @param data Data to be wrapped in the request.
* @return Payload to be sent to Dapr's API.
* @throws RuntimeException In case it cannot generate payload.
*/
private byte[] wrap(final byte[] data) {
try {
return INTERNAL_SERIALIZER.wrapData(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
class ActorRuntimeConfig {
private Collection<String> registeredActorTypes = new ArrayList<>();
ActorRuntimeConfig addRegisteredActorType(String actorTypeName) {
if (actorTypeName == null) {
throw new IllegalArgumentException("Registered actor must have a type name.");
}
this.registeredActorTypes.add(actorTypeName);
return this;
}
Collection<String> getRegisteredActorTypes() {
return Collections.unmodifiableCollection(registeredActorTypes);
}
ActorRuntimeConfig setRegisteredActorTypes(Collection<String> registeredActorTypes) {
this.registeredActorTypes = registeredActorTypes;
return this;
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorTrace;
import io.dapr.serializer.DaprObjectSerializer;
/**
* Provides the context for the Actor's runtime.
*
* @param <T> Actor's type for the context.
*/
public class ActorRuntimeContext<T extends AbstractActor> {
/**
* Runtime.
*/
private final ActorRuntime actorRuntime;
/**
* Serializer for transient objects.
*/
private final DaprObjectSerializer objectSerializer;
/**
* Actor factory.
*/
private final ActorFactory<T> actorFactory;
/**
* Information of the Actor's type.
*/
private final ActorTypeInformation<T> actorTypeInformation;
/**
* Trace for Actor logs.
*/
private final ActorTrace actorTrace;
/**
* Client to communicate to Dapr's API.
*/
private final DaprClient daprClient;
/**
* State provider for given Actor Type.
*/
private final DaprStateAsyncProvider stateProvider;
/**
* Instantiates a new runtime context for the Actor type.
*
* @param actorRuntime Runtime.
* @param objectSerializer Serializer for transient objects.
* @param actorFactory Factory for Actors.
* @param actorTypeInformation Information for Actor's type.
* @param daprClient Client to communicate to Dapr.
* @param stateProvider State provider for given Actor's type.
*/
ActorRuntimeContext(ActorRuntime actorRuntime,
DaprObjectSerializer objectSerializer,
ActorFactory<T> actorFactory,
ActorTypeInformation<T> actorTypeInformation,
DaprClient daprClient,
DaprStateAsyncProvider stateProvider) {
this.actorRuntime = actorRuntime;
this.objectSerializer = objectSerializer;
this.actorFactory = actorFactory;
this.actorTypeInformation = actorTypeInformation;
this.actorTrace = new ActorTrace();
this.daprClient = daprClient;
this.stateProvider = stateProvider;
}
/**
* Gets the Actor's runtime.
*
* @return Actor's runtime.
*/
ActorRuntime getActorRuntime() {
return this.actorRuntime;
}
/**
* Gets the Actor's serializer for transient objects.
*
* @return Actor's serializer for transient objects.
*/
DaprObjectSerializer getObjectSerializer() {
return this.objectSerializer;
}
/**
* Gets the Actor's serializer.
*
* @return Actor's serializer.
*/
ActorFactory<T> getActorFactory() {
return this.actorFactory;
}
/**
* Gets the information about the Actor's type.
*
* @return Information about the Actor's type.
*/
ActorTypeInformation<T> getActorTypeInformation() {
return this.actorTypeInformation;
}
/**
* Gets the trace for Actor logs.
*
* @return Trace for Actor logs.
*/
ActorTrace getActorTrace() {
return this.actorTrace;
}
/**
* Gets the client to communicate to Dapr's API.
*
* @return Client to communicate to Dapr's API.
*/
DaprClient getDaprClient() {
return this.daprClient;
}
/**
* Gets the state provider for given Actor's type.
*
* @return State provider for given Actor's type.
*/
DaprStateAsyncProvider getStateProvider() {
return stateProvider;
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
/**
* Represents a state change for an actor.
*/
public final class ActorStateChange {
/**
* Name of the state being changed.
*/
private final String stateName;
/**
* New value for the state being changed.
*/
private final Object value;
/**
* Type of change {@link ActorStateChangeKind}.
*/
private final ActorStateChangeKind changeKind;
/**
* Creates an actor state change.
*
* @param stateName Name of the state being changed.
* @param value New value for the state being changed.
* @param changeKind Kind of change.
*/
ActorStateChange(String stateName, Object value, ActorStateChangeKind changeKind) {
this.stateName = stateName;
this.value = value;
this.changeKind = changeKind;
}
/**
* Gets the name of the state being changed.
*
* @return Name of the state.
*/
String getStateName() {
return stateName;
}
/**
* Gets the new value of the state being changed.
*
* @return New value.
*/
Object getValue() {
return value;
}
/**
* Gets the kind of change.
*
* @return Kind of change.
*/
ActorStateChangeKind getChangeKind() {
return changeKind;
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
/**
* Represents an actor's state change.
*/
public enum ActorStateChangeKind {
/**
* No change in state.
*/
NONE(""),
/**
* State needs to be added.
*/
ADD("upsert"),
/**
* State needs to be updated.
*/
UPDATE("upsert"),
/**
* State needs to be removed.
*/
REMOVE("delete");
/**
* Operation name in Dapr's state management.
*/
private final String daprStateChangeOperation;
/**
* Creates a kind of actor state change.
*
* @param daprStateChangeOperation Equivalent operation name Dapr's state management
*/
ActorStateChangeKind(String daprStateChangeOperation) {
this.daprStateChangeOperation = daprStateChangeOperation;
}
/**
* Gets equivalent operation name Dapr's state management.
*
* @return Equivalent operation name Dapr's state management
*/
String getDaprStateChangeOperation() {
return daprStateChangeOperation;
}
}

View File

@ -0,0 +1,311 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import reactor.core.publisher.Mono;
/**
* Manages state changes of a given Actor instance.
* All changes are cached in-memory until save() is called.
*/
public class ActorStateManager {
/**
* Provides states using a state store.
*/
private final DaprStateAsyncProvider stateProvider;
/**
* Name of the Actor's type.
*/
private final String actorTypeName;
/**
* Actor's identifier.
*/
private final ActorId actorId;
/**
* Cache of state changes in this Actor's instance.
*/
private final Map<String, StateChangeMetadata> stateChangeTracker;
/**
* Instantiates a new state manager for the given Actor's instance.
*
* @param stateProvider State store provider.
* @param actorTypeName Name of Actor's type.
* @param actorId Actor's identifier.
*/
ActorStateManager(DaprStateAsyncProvider stateProvider, String actorTypeName, ActorId actorId) {
this.stateProvider = stateProvider;
this.actorTypeName = actorTypeName;
this.actorId = actorId;
this.stateChangeTracker = new HashMap<>();
}
/**
* Adds a given key/value to the Actor's state store's cache.
*
* @param stateName Name of the state being added.
* @param value Value to be added.
* @param <T> Type of the object being added.
* @return Asynchronous void operation.
*/
public <T> Mono<Void> add(String stateName, T value) {
return Mono.fromSupplier(() -> {
if (stateName == null) {
throw new IllegalArgumentException("State's name cannot be null.");
}
return null;
}).then(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName)
.map(exists -> {
if (this.stateChangeTracker.containsKey(stateName)) {
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
if (metadata.kind == ActorStateChangeKind.REMOVE) {
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.UPDATE, value));
return true;
}
throw new IllegalStateException("Duplicate cached state: " + stateName);
}
if (exists) {
throw new IllegalStateException("Duplicate state: " + stateName);
}
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.ADD, value));
return true;
}))
.then();
}
/**
* Fetches the most recent value for the given state, including cached value.
*
* @param stateName Name of the state.
* @param clazz Class type for the value being fetched.
* @param <T> Type being fetched.
* @return Asynchronous response with fetched object.
*/
public <T> Mono<T> get(String stateName, Class<T> clazz) {
return Mono.fromSupplier(() -> {
if (stateName == null) {
throw new IllegalArgumentException("State's name cannot be null.");
}
if (this.stateChangeTracker.containsKey(stateName)) {
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
if (metadata.kind == ActorStateChangeKind.REMOVE) {
throw new NoSuchElementException("State is marked for removal: " + stateName);
}
return (T) metadata.value;
}
return (T) null;
}).switchIfEmpty(
this.stateProvider.load(this.actorTypeName, this.actorId, stateName, clazz)
.switchIfEmpty(Mono.error(new NoSuchElementException("State not found: " + stateName)))
.map(v -> {
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.NONE, v));
return (T) v;
}));
}
/**
* Updates a given key/value pair in the state store's cache.
*
* @param stateName Name of the state being updated.
* @param value Value to be set for given state.
* @param <T> Type of the value being set.
* @return Asynchronous void result.
*/
public <T> Mono<Void> set(String stateName, T value) {
return Mono.fromSupplier(() -> {
if (stateName == null) {
throw new IllegalArgumentException("State's name cannot be null.");
}
if (this.stateChangeTracker.containsKey(stateName)) {
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
ActorStateChangeKind kind = metadata.kind;
if ((kind == ActorStateChangeKind.NONE) || (kind == ActorStateChangeKind.REMOVE)) {
kind = ActorStateChangeKind.UPDATE;
}
this.stateChangeTracker.put(stateName, new StateChangeMetadata(kind, value));
return true;
}
return false;
}).filter(x -> x)
.switchIfEmpty(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName)
.map(exists -> {
this.stateChangeTracker.put(stateName,
new StateChangeMetadata(exists ? ActorStateChangeKind.UPDATE : ActorStateChangeKind.ADD, value));
return exists;
}))
.then();
}
/**
* Removes a given state from state store's cache.
*
* @param stateName State being stored.
* @return Asynchronous void result.
*/
public Mono<Void> remove(String stateName) {
return Mono.fromSupplier(() -> {
if (stateName == null) {
throw new IllegalArgumentException("State's name cannot be null.");
}
if (this.stateChangeTracker.containsKey(stateName)) {
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
if (metadata.kind == ActorStateChangeKind.REMOVE) {
return true;
}
if (metadata.kind == ActorStateChangeKind.ADD) {
this.stateChangeTracker.remove(stateName);
return true;
}
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null));
return true;
}
return false;
})
.filter(x -> x)
.switchIfEmpty(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName))
.filter(exists -> exists)
.map(exists -> {
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null));
return exists;
})
.then();
}
/**
* Checks if a given state exists in state store or cache.
*
* @param stateName State being checked.
* @return Asynchronous boolean result indicating whether state is present.
*/
public Mono<Boolean> contains(String stateName) {
return Mono.fromSupplier(() -> {
if (stateName == null) {
throw new IllegalArgumentException("State's name cannot be null.");
}
if (this.stateChangeTracker.containsKey(stateName)) {
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
if (metadata.kind == ActorStateChangeKind.REMOVE) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}
return null;
}
).switchIfEmpty(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName));
}
/**
* Saves all changes to state store.
*
* @return Asynchronous void result.
*/
public Mono<Void> save() {
return Mono.fromSupplier(() -> {
if (this.stateChangeTracker.isEmpty()) {
return null;
}
List<ActorStateChange> changes = new ArrayList<>();
List<String> removed = new ArrayList<>();
for (Map.Entry<String, StateChangeMetadata> tuple : this.stateChangeTracker.entrySet()) {
if (tuple.getValue().kind == ActorStateChangeKind.NONE) {
continue;
}
if (tuple.getValue().kind == ActorStateChangeKind.REMOVE) {
removed.add(tuple.getKey());
}
changes.add(new ActorStateChange(tuple.getKey(), tuple.getValue().value, tuple.getValue().kind));
}
return changes.toArray(new ActorStateChange[0]);
}).flatMap(changes -> this.stateProvider.apply(this.actorTypeName, this.actorId, changes))
.then(Mono.fromRunnable(() -> this.flush()));
}
/**
* Clears all changes not yet saved to state store.
*/
public void clear() {
this.stateChangeTracker.clear();
}
/**
* Commits the current cached values after successful save.
*/
private void flush() {
for (Map.Entry<String, StateChangeMetadata> tuple : this.stateChangeTracker.entrySet()) {
String stateName = tuple.getKey();
if (tuple.getValue().kind == ActorStateChangeKind.REMOVE) {
this.stateChangeTracker.remove(stateName);
} else {
StateChangeMetadata metadata = new StateChangeMetadata(ActorStateChangeKind.NONE, tuple.getValue().value);
this.stateChangeTracker.put(stateName, metadata);
}
}
}
/**
* Internal class to represent value and change kind.
*/
private static final class StateChangeMetadata {
/**
* Kind of change cached.
*/
private final ActorStateChangeKind kind;
/**
* Value cached.
*/
private final Object value;
/**
* Creates a new instance of the metadata on state change.
*
* @param kind Kind of change.
* @param value Value to be set.
*/
private StateChangeMetadata(ActorStateChangeKind kind, Object value) {
this.kind = kind;
this.value = value;
}
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.time.Duration;
/**
* Represents the timer set on an Actor, to be called once after due time and then every period.
*
* @param <T> State type.
*/
final class ActorTimer<T> {
/**
* Actor that owns this timer.
*/
private final AbstractActor owner;
/**
* Name of this timer.
*/
private String name;
/**
* Name of the method to be called for this timer.
*/
private String callback;
/**
* State to be sent in the timer.
*/
private T state;
/**
* Due time for the timer's first trigger.
*/
private Duration dueTime;
/**
* Period at which the timer will be triggered.
*/
private Duration period;
/**
* Instantiates a new Actor Timer.
*
* @param owner The Actor that owns this timer. The timer callback will be fired for this Actor.
* @param timerName The name of the timer.
* @param callback The name of the method to be called for this timer.
* @param state information to be used by the callback method
* @param dueTime the time when timer is first due.
* @param period the periodic time when timer will be invoked.
*/
ActorTimer(AbstractActor owner,
String timerName,
String callback,
T state,
Duration dueTime,
Duration period) {
this.owner = owner;
this.name = timerName;
this.callback = callback;
this.state = state;
this.dueTime = dueTime;
this.period = period;
}
/**
* Gets the name of the Timer. The name is unique per actor.
*
* @return The name of the timer.
*/
public String getName() {
return this.name;
}
/**
* Gets the name of the method for this Timer.
*
* @return The name of the method for this timer.
*/
public String getCallback() {
return this.callback;
}
/**
* Gets the time when timer is first due.
*
* @return Time as Duration when timer is first due.
*/
public Duration getDueTime() {
return this.dueTime;
}
/**
* Gets the periodic time when timer will be invoked.
*
* @return Periodic time as Duration when timer will be invoked.
*/
public Duration getPeriod() {
return this.period;
}
/**
* Gets state containing information to be used by the callback method, or null.
*
* @return State containing information to be used by the callback method, or null.
*/
public T getState() {
return this.state;
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to define Actor class.
*/
@Documented
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActorType {
/**
* Overrides Actor's name.
*
* @return Actor's name.
*/
String name();
}

View File

@ -0,0 +1,152 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* Contains the information about the class implementing an actor.
*/
final class ActorTypeInformation<T> {
/**
* Actor type's name.
*/
private final String name;
/**
* Actor's implementation class.
*/
private final Class<T> implementationClass;
/**
* Actor's immediate interfaces.
*/
private final Collection<Class<?>> interfaces;
/**
* Whether Actor type is abstract.
*/
private final boolean abstractClass;
/**
* Whether Actor type is remindable.
*/
private final boolean remindable;
/**
* Instantiates a new {@link ActorTypeInformation}.
*
* @param name Actor type's name.
* @param implementationClass Actor's implementation class.
* @param interfaces Actor's immediate interfaces.
* @param abstractClass Whether Actor type is abstract.
* @param remindable Whether Actor type is remindable.
*/
private ActorTypeInformation(String name,
Class<T> implementationClass,
Collection<Class<?>> interfaces,
boolean abstractClass,
boolean remindable) {
this.name = name;
this.implementationClass = implementationClass;
this.interfaces = interfaces;
this.abstractClass = abstractClass;
this.remindable = remindable;
}
/**
* Returns the name of this ActorType.
*
* @return ActorType's name.
*/
public String getName() {
return this.name;
}
/**
* Gets the type of the class implementing the actor.
*
* @return The {@link Class} of implementing the actor.
*/
public Class<T> getImplementationClass() {
return this.implementationClass;
}
/**
* Gets the actor interfaces that are implemented by actor class.
*
* @return Collection of actor interfaces.
*/
public Collection<Class<?>> getInterfaces() {
return Collections.unmodifiableCollection(this.interfaces);
}
/**
* Gets a value indicating whether the class implementing actor is abstract.
*
* @return true if the class implementing actor is abstract, otherwise false.
*/
public boolean isAbstractClass() {
return this.abstractClass;
}
/**
* Gets a value indicating whether the actor class implements
* {@link Remindable}.
*
* @return true if the actor class implements {@link Remindable}.
*/
public boolean isRemindable() {
return this.remindable;
}
/**
* Creates the {@link ActorTypeInformation} from given Class.
*
* @param actorClass The type of class implementing the actor to create
* ActorTypeInformation for.
* @return ActorTypeInformation if successfully created for actorType or null.
*/
public static <T> ActorTypeInformation<T> tryCreate(Class<T> actorClass) {
try {
return create(actorClass);
} catch (IllegalArgumentException e) {
return null;
}
}
/**
* Creates an {@link #ActorTypeInformation} from actorType.
*
* @param actorClass The class implementing the actor to create
* ActorTypeInformation for.
* @return {@link #ActorTypeInformation} created from actorType.
*/
public static <T> ActorTypeInformation<T> create(Class<T> actorClass) {
if (!ActorTypeUtilities.isActor(actorClass)) {
throw new IllegalArgumentException(
String.format(
"The type '%s' is not an Actor. An actor class must inherit from '%s'.",
actorClass == null ? "" : actorClass.getCanonicalName(),
AbstractActor.class.getCanonicalName()));
}
// get all actor interfaces
Class<?>[] actorInterfaces = actorClass.getInterfaces();
boolean isAbstract = Modifier.isAbstract(actorClass.getModifiers());
boolean isRemindable = ActorTypeUtilities.isRemindableActor(actorClass);
ActorType actorTypeAnnotation = actorClass.getAnnotation(ActorType.class);
String typeName = actorTypeAnnotation != null ? actorTypeAnnotation.name() : actorClass.getSimpleName();
return new ActorTypeInformation(typeName, actorClass, Arrays.asList(actorInterfaces), isAbstract, isRemindable);
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.util.Arrays;
/**
* Utility class to extract information on Actor type.
*/
final class ActorTypeUtilities {
/**
* Gets all interfaces that extend Actor.
*
* @param clazz Actor class.
* @return Array of Actor interfaces.
*/
public static Class[] getActorInterfaces(Class clazz) {
if (clazz == null) {
return new Class[0];
}
return Arrays.stream(clazz.getInterfaces())
.filter(t -> AbstractActor.class.isAssignableFrom(t))
.filter(t -> getNonActorParentClass(t) == null)
.toArray(Class[]::new);
}
/**
* Determines if given class is an Actor interface.
*
* @param clazz Actor interface candidate.
* @return Whether this is an Actor interface.
*/
public static boolean isActorInterface(Class clazz) {
return (clazz != null) && clazz.isInterface() && (getNonActorParentClass(clazz) == null);
}
/**
* Determines whether this is an Actor class.
*
* @param clazz Actor class candidate.
* @return Whether this is an Actor class.
*/
public static boolean isActor(Class clazz) {
if (clazz == null) {
return false;
}
return AbstractActor.class.isAssignableFrom(clazz);
}
/**
* Determines whether this is an remindable Actor.
*
* @param clazz Actor class.
* @return Whether this is an remindable Actor.
*/
public static boolean isRemindableActor(Class clazz) {
return (clazz != null)
&& isActor(clazz)
&& (Arrays.stream(clazz.getInterfaces()).filter(t -> t.equals(Remindable.class)).count() > 0);
}
/**
* Returns the parent class if it is not the {@link AbstractActor} parent
* class.
*
* @param clazz Actor class.
* @return Parent class or null if it is {@link AbstractActor}.
*/
public static Class getNonActorParentClass(Class clazz) {
if (clazz == null) {
return null;
}
Class[] items = Arrays.stream(clazz.getInterfaces())
.filter(t -> !t.equals(AbstractActor.class))
.toArray(Class[]::new);
if (items.length == 0) {
return clazz;
}
for (Class c : items) {
Class nonActorParent = getNonActorParentClass(c);
if (nonActorParent != null) {
return nonActorParent;
}
}
return null;
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import reactor.core.publisher.Mono;
/**
* Generic Client Adapter to be used regardless of the GRPC or the HTTP Client implementation required.
*/
interface DaprClient {
/**
* Gets a state from Dapr's Actor.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param keyName State name.
* @return Asynchronous result with current state value.
*/
Mono<byte[]> getActorState(String actorType, String actorId, String keyName);
/**
* Saves state batch to Dapr.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param data State to be saved.
* @return Asynchronous void result.
*/
Mono<Void> saveActorStateTransactionally(String actorType, String actorId, byte[] data);
/**
* Register a reminder.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param reminderName Name of reminder to be registered.
* @param data JSON reminder data as per Dapr's spec.
* @return Asynchronous void result.
*/
Mono<Void> registerActorReminder(String actorType, String actorId, String reminderName, byte[] data);
/**
* Unregisters a reminder.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param reminderName Name of reminder to be unregistered.
* @return Asynchronous void result.
*/
Mono<Void> unregisterActorReminder(String actorType, String actorId, String reminderName);
/**
* Registers a timer.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param timerName Name of timer to be registered.
* @param data JSON reminder data as per Dapr's spec.
* @return Asynchronous void result.
*/
Mono<Void> registerActorTimer(String actorType, String actorId, String timerName, byte[] data);
/**
* Unregisters a timer.
*
* @param actorType Type of actor.
* @param actorId Actor Identifier.
* @param timerName Name of timer to be unregistered.
* @return Asynchronous void result.
*/
Mono<Void> unregisterActorTimer(String actorType, String actorId, String timerName);
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.client.DaprHttp;
import io.dapr.utils.Constants;
import reactor.core.publisher.Mono;
/**
* A DaprClient over HTTP for Actor's runtime.
*/
class DaprHttpClient implements DaprClient {
/**
* The HTTP client to be used.
*
* @see DaprHttp
*/
private final DaprHttp client;
/**
* Internal constructor.
*
* @param client Dapr's http client.
*/
DaprHttpClient(DaprHttp client) {
this.client = client;
}
/**
* {@inheritDoc}
*/
@Override
public Mono<byte[]> getActorState(String actorType, String actorId, String keyName) {
String url = String.format(Constants.ACTOR_STATE_KEY_RELATIVE_URL_FORMAT, actorType, actorId, keyName);
Mono<DaprHttp.Response> responseMono = this.client.invokeAPI(DaprHttp.HttpMethods.GET.name(), url, null, "", null);
return responseMono.map(r -> r.getBody());
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> saveActorStateTransactionally(String actorType, String actorId, byte[] data) {
String url = String.format(Constants.ACTOR_STATE_RELATIVE_URL_FORMAT, actorType, actorId);
return this.client.invokeAPI(DaprHttp.HttpMethods.PUT.name(), url, null, data, null).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> registerActorReminder(String actorType, String actorId, String reminderName, byte[] data) {
String url = String.format(Constants.ACTOR_REMINDER_RELATIVE_URL_FORMAT, actorType, actorId, reminderName);
return this.client.invokeAPI(DaprHttp.HttpMethods.PUT.name(), url, null, data, null).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> unregisterActorReminder(String actorType, String actorId, String reminderName) {
String url = String.format(Constants.ACTOR_REMINDER_RELATIVE_URL_FORMAT, actorType, actorId, reminderName);
return this.client.invokeAPI(DaprHttp.HttpMethods.DELETE.name(), url, null, null).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> registerActorTimer(String actorType, String actorId, String timerName, byte[] data) {
String url = String.format(Constants.ACTOR_TIMER_RELATIVE_URL_FORMAT, actorType, actorId, timerName);
return this.client.invokeAPI(DaprHttp.HttpMethods.PUT.name(), url, null, data, null).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Void> unregisterActorTimer(String actorType, String actorId, String timerName) {
String url = String.format(Constants.ACTOR_TIMER_RELATIVE_URL_FORMAT, actorType, actorId, timerName);
return this.client.invokeAPI(DaprHttp.HttpMethods.DELETE.name(), url, null, null).then();
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.serializer.StringContentType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import reactor.core.publisher.Mono;
/**
* State Provider to interact with Dapr runtime to handle state.
*/
class DaprStateAsyncProvider {
/**
* Shared Json Factory as per Jackson's documentation, used only for this class.
*/
private static final JsonFactory JSON_FACTORY = new JsonFactory();
/**
* Dapr's client for Actor runtime.
*/
private final DaprClient daprClient;
/**
* Serializer for state objects.
*/
private final DaprObjectSerializer stateSerializer;
/**
* Flag determining if serializer's input and output contains a valid String.
*/
private final boolean isStateString;
/**
* Instantiates a new Actor's state provider.
*
* @param daprClient Dapr client for Actor runtime.
* @param stateSerializer Serializer for state objects.
*/
DaprStateAsyncProvider(DaprClient daprClient, DaprObjectSerializer stateSerializer) {
this.daprClient = daprClient;
this.stateSerializer = stateSerializer;
this.isStateString = stateSerializer.getClass().getAnnotation(StringContentType.class) != null;
}
<T> Mono<T> load(String actorType, ActorId actorId, String stateName, Class<T> clazz) {
Mono<byte[]> result = this.daprClient.getActorState(actorType, actorId.toString(), stateName);
return result.flatMap(s -> {
try {
T response = this.stateSerializer.deserialize(s, clazz);
if (response == null) {
return Mono.empty();
}
return Mono.just(response);
} catch (IOException e) {
return Mono.error(new RuntimeException(e));
}
});
}
Mono<Boolean> contains(String actorType, ActorId actorId, String stateName) {
Mono<byte[]> result = this.daprClient.getActorState(actorType, actorId.toString(), stateName);
return result.map(s -> true).defaultIfEmpty(false);
}
/**
* Saves state changes transactionally.
* [
* {
* "operation": "upsert",
* "request": {
* "key": "key1",
* "value": "myData"
* }
* },
* {
* "operation": "delete",
* "request": {
* "key": "key2"
* }
* }
* ]
*
* @param actorType Name of the actor being changed.
* @param actorId Identifier of the actor being changed.
* @param stateChanges Collection of changes to be performed transactionally.
* @return Void.
*/
Mono<Void> apply(String actorType, ActorId actorId, ActorStateChange... stateChanges) {
if ((stateChanges == null) || stateChanges.length == 0) {
return Mono.empty();
}
int count = 0;
// Constructing the JSON via a stream API to avoid creating transient objects to be instantiated.
byte[] payload = null;
try (ByteArrayOutputStream writer = new ByteArrayOutputStream()) {
JsonGenerator generator = JSON_FACTORY.createGenerator(writer);
// Start array
generator.writeStartArray();
for (ActorStateChange stateChange : stateChanges) {
if ((stateChange == null) || (stateChange.getChangeKind() == null)) {
continue;
}
String operationName = stateChange.getChangeKind().getDaprStateChangeOperation();
if ((operationName == null) || (operationName.length() == 0)) {
continue;
}
count++;
// Start operation object.
generator.writeStartObject();
generator.writeStringField("operation", operationName);
// Start request object.
generator.writeObjectFieldStart("request");
generator.writeStringField("key", stateChange.getStateName());
if ((stateChange.getChangeKind() == ActorStateChangeKind.UPDATE)
|| (stateChange.getChangeKind() == ActorStateChangeKind.ADD)) {
byte[] data = this.stateSerializer.serialize(stateChange.getValue());
if (data != null) {
if (this.isStateString) {
generator.writeStringField("value", new String(data));
} else {
generator.writeBinaryField("value", data);
}
}
}
// End request object.
generator.writeEndObject();
// End operation object.
generator.writeEndObject();
}
// End array
generator.writeEndArray();
generator.close();
writer.flush();
payload = writer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return Mono.error(e);
}
if (count == 0) {
// No-op since there is no operation to be performed.
Mono.empty();
}
return this.daprClient.saveActorStateTransactionally(actorType, actorId.toString(), payload);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.ActorTrace;
import java.lang.reflect.Constructor;
/**
* Instantiates actors by calling their constructor with {@link ActorRuntimeContext} and {@link ActorId}.
*
* @param <T> Actor Type to be created.
*/
class DefaultActorFactory<T extends AbstractActor> implements ActorFactory<T> {
/**
* Tracing errors, warnings and info logs.
*/
private static final ActorTrace ACTOR_TRACE = new ActorTrace();
/**
* {@inheritDoc}
*/
@Override
public T createActor(ActorRuntimeContext<T> actorRuntimeContext, ActorId actorId) {
try {
if (actorRuntimeContext == null) {
return null;
}
Constructor<T> constructor = actorRuntimeContext
.getActorTypeInformation()
.getImplementationClass()
.getConstructor(ActorRuntimeContext.class, ActorId.class);
return constructor.newInstance(actorRuntimeContext, actorId);
} catch (Exception e) {
ACTOR_TRACE.writeError(
actorRuntimeContext.getActorTypeInformation().getName(),
actorId.toString(),
"Failed to create actor instance.");
}
return null;
}
}

View File

@ -0,0 +1,210 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import io.dapr.utils.DurationUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
/**
* Serializes and deserializes internal objects.
*/
public class ObjectSerializer extends io.dapr.client.ObjectSerializer {
/**
* Shared Json Factory as per Jackson's documentation.
*/
private static final JsonFactory JSON_FACTORY = new JsonFactory();
/**
* {@inheritDoc}
*/
@Override
public byte[] serialize(Object state) throws IOException {
if (state == null) {
return null;
}
if (state.getClass() == ActorTimer.class) {
// Special serializer for this internal classes.
return serialize((ActorTimer) state);
}
if (state.getClass() == ActorReminderParams.class) {
// Special serializer for this internal classes.
return serialize((ActorReminderParams) state);
}
if (state.getClass() == ActorRuntimeConfig.class) {
// Special serializer for this internal classes.
return serialize((ActorRuntimeConfig) state);
}
// Is not an special case.
return super.serialize(state);
}
/**
* Faster serialization for params of Actor's timer.
*
* @param timer Timer's to be serialized.
* @return JSON String.
* @throws IOException If cannot generate JSON.
*/
private byte[] serialize(ActorTimer<?> timer) throws IOException {
if (timer == null) {
return null;
}
try (ByteArrayOutputStream writer = new ByteArrayOutputStream()) {
JsonGenerator generator = JSON_FACTORY.createGenerator(writer);
generator.writeStartObject();
generator.writeStringField("dueTime", DurationUtils.ConvertDurationToDaprFormat(timer.getDueTime()));
generator.writeStringField("period", DurationUtils.ConvertDurationToDaprFormat(timer.getPeriod()));
generator.writeStringField("callback", timer.getCallback());
if (timer.getState() != null) {
generator.writeBinaryField("data", this.serialize(timer.getState()));
}
generator.writeEndObject();
generator.close();
writer.flush();
return writer.toByteArray();
}
}
/**
* Faster serialization for Actor's reminder.
*
* @param reminder Reminder to be serialized.
* @return JSON String.
* @throws IOException If cannot generate JSON.
*/
private byte[] serialize(ActorReminderParams reminder) throws IOException {
try (ByteArrayOutputStream writer = new ByteArrayOutputStream()) {
JsonGenerator generator = JSON_FACTORY.createGenerator(writer);
generator.writeStartObject();
generator.writeStringField("dueTime", DurationUtils.ConvertDurationToDaprFormat(reminder.getDueTime()));
generator.writeStringField("period", DurationUtils.ConvertDurationToDaprFormat(reminder.getPeriod()));
if (reminder.getData() != null) {
generator.writeBinaryField("data", reminder.getData());
}
generator.writeEndObject();
generator.close();
writer.flush();
return writer.toByteArray();
}
}
/**
* Faster serialization for Actor's runtime configuration.
*
* @param config Configuration for Dapr's actor runtime.
* @return JSON String.
* @throws IOException If cannot generate JSON.
*/
private byte[] serialize(ActorRuntimeConfig config) throws IOException {
try (ByteArrayOutputStream writer = new ByteArrayOutputStream()) {
JsonGenerator generator = JSON_FACTORY.createGenerator(writer);
generator.writeStartObject();
generator.writeArrayFieldStart("entities");
for (String actorClass : config.getRegisteredActorTypes()) {
generator.writeString(actorClass);
}
generator.writeEndArray();
// TODO: handle configuration.
generator.writeEndObject();
generator.close();
writer.flush();
return writer.toByteArray();
}
}
/**
* {@inheritDoc}
*/
@Override
public <T> T deserialize(byte[] content, Class<T> clazz) throws IOException {
if (clazz == ActorReminderParams.class) {
// Special serializer for this internal classes.
return (T) deserializeActorReminder(content);
}
// Is not one of the special cases.
return super.deserialize(content, clazz);
}
/**
* Extracts the response data from a JSON Payload where data is in "data" attribute.
*
* @param payload JSON payload containing "data".
* @return byte[] instance, null.
* @throws IOException In case it cannot generate String.
*/
public byte[] unwrapData(final byte[] payload) throws IOException {
if (payload == null) {
return null;
}
JsonNode root = OBJECT_MAPPER.readTree(payload);
if (root == null) {
return null;
}
JsonNode dataNode = root.get("data");
if (dataNode == null) {
return null;
}
return dataNode.binaryValue();
}
/**
* Wraps data in the "data" attribute in a JSON object.
*
* @param data bytes to be wrapped into the "data" attribute in a JSON object.
* @return String to be sent to Dapr's API.
* @throws RuntimeException In case it cannot generate String.
*/
public byte[] wrapData(final byte[] data) throws IOException {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
JsonGenerator generator = JSON_FACTORY.createGenerator(output);
generator.writeStartObject();
if (data != null) {
generator.writeBinaryField("data", data);
}
generator.writeEndObject();
generator.close();
output.flush();
return output.toByteArray();
}
}
/**
* Deserializes an Actor Reminder.
*
* @param value Content to be deserialized.
* @return Actor Reminder.
* @throws IOException If cannot parse JSON.
*/
private ActorReminderParams deserializeActorReminder(byte[] value) throws IOException {
if (value == null) {
return null;
}
JsonNode node = OBJECT_MAPPER.readTree(value);
Duration dueTime = DurationUtils.ConvertDurationFromDaprFormat(node.get("dueTime").asText());
Duration period = DurationUtils.ConvertDurationFromDaprFormat(node.get("period").asText());
byte[] data = node.get("data") != null ? node.get("data").binaryValue() : null;
return new ActorReminderParams(data, dueTime, period);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import java.time.Duration;
import reactor.core.publisher.Mono;
/**
* Interface that actors must implement to consume reminders registered using RegisterReminderAsync.
*/
public interface Remindable<T> {
/**
* Gets the class for state object.
*
* @return Class for state object.
*/
Class<T> getStateType();
/**
* The reminder call back invoked when an actor reminder is triggered.
* The state of this actor is saved by the actor runtime upon completion of the task returned by this method.
* If an error occurs while saving the state, then all state cached by this actor's {@link ActorStateManager} will
* be discarded and reloaded from previously saved state when the next actor method or reminder invocation occurs.
*
* @param reminderName The name of reminder provided during registration.
* @param state The user state provided during registration.
* @param dueTime The invocation due time provided during registration.
* @param period The invocation period provided during registration.
* @return A task that represents the asynchronous operation performed by this callback.
*/
Mono<Void> receiveReminder(String reminderName, T state, Duration dueTime, Duration period);
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors;
import java.util.*;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for ActorId.
*/
public class ActorIdTest {
@Test(expected = IllegalArgumentException.class)
public void initializeNewActorIdObjectWithNullId() {
ActorId actorId = new ActorId(null);
}
@Test
public void getId() {
String id = "123";
ActorId actorId = new ActorId(id);
Assert.assertEquals(id, actorId.toString());
}
@Test
public void verifyToString() {
String id = "123";
ActorId actorId = new ActorId(id);
Assert.assertEquals(id, actorId.toString());
}
@Test
public void verifyEqualsByObject() {
List<Wrapper> values = createEqualsTestValues();
for (Wrapper w : values) {
Assert.assertEquals(w.expectedResult, w.item1.equals(w.item2));
}
}
@Test
public void verifyEqualsByActorId() {
List<Wrapper> values = createEqualsTestValues();
for (Wrapper w : values) {
ActorId a1 = (ActorId) w.item1;
Object a2 = w.item2;
Assert.assertEquals(w.expectedResult, a1.equals(a2));
}
}
@Test
public void verifyCompareTo() {
List<Wrapper> values = createComparesToTestValues();
for (Wrapper w : values) {
ActorId a1 = (ActorId) w.item1;
ActorId a2 = (ActorId) w.item2;
Assert.assertEquals(w.expectedResult, a1.compareTo(a2));
}
}
private List<Wrapper> createEqualsTestValues() {
List<Wrapper> list = new ArrayList<Wrapper>();
list.add(new Wrapper(new ActorId("1"), null, false));
list.add(new Wrapper(new ActorId("1"), new ActorId("1"), true));
list.add(new Wrapper(new ActorId("1"), new Object(), false));
list.add(new Wrapper(new ActorId("1"), new ActorId("2"), false));
return list;
}
private List<Wrapper> createComparesToTestValues() {
List<Wrapper> list = new ArrayList<Wrapper>();
list.add(new Wrapper(new ActorId("1"), null, 1));
list.add(new Wrapper(new ActorId("1"), new ActorId("1"), 0));
list.add(new Wrapper(new ActorId("1"), new ActorId("2"), -1));
list.add(new Wrapper(new ActorId("2"), new ActorId("1"), 1));
return list;
}
class Wrapper<T> {
public Object item1;
public Object item2;
public T expectedResult;
public Wrapper(Object i, Object j, T e) {
this.item1 = i;
this.item2 = j;
this.expectedResult = e;
}
}
}

View File

@ -0,0 +1,48 @@
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
public class ActorProxyBuilderTest {
@Test(expected = IllegalArgumentException.class)
public void buildWithNullActorId() {
new ActorProxyBuilder("test", new DefaultObjectSerializer())
.build(null);
}
@Test(expected = IllegalArgumentException.class)
public void buildWithEmptyActorType() {
new ActorProxyBuilder("", new DefaultObjectSerializer())
.build(new ActorId("100"));
}
@Test(expected = IllegalArgumentException.class)
public void buildWithNullActorType() {
new ActorProxyBuilder(null, new DefaultObjectSerializer())
.build(new ActorId("100"));
}
@Test(expected = IllegalArgumentException.class)
public void buildWithNullSerializer() {
new ActorProxyBuilder("MyActor", null)
.build(new ActorId("100"));
}
@Test()
public void build() {
ActorProxyBuilder builder = new ActorProxyBuilder("test", new DefaultObjectSerializer());
ActorProxy actorProxy = builder.build(new ActorId("100"));
Assert.assertNotNull(actorProxy);
Assert.assertEquals("test", actorProxy.getActorType());
Assert.assertEquals("100", actorProxy.getActorId().toString());
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DaprObjectSerializer;
public class ActorProxyForTestsImpl extends ActorProxyImpl {
public ActorProxyForTestsImpl(String actorType, ActorId actorId, DaprObjectSerializer serializer, DaprClient daprClient) {
super(actorType, actorId, serializer, daprClient);
}
}

View File

@ -0,0 +1,293 @@
package io.dapr.actors.client;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.serializer.DaprObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ActorProxyImplTest {
@Test()
public void constructorActorProxyTest() {
final DaprClient daprClient = mock(DaprClient.class);
final DaprObjectSerializer serializer = mock(DaprObjectSerializer.class);
final ActorProxyImpl actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
serializer,
daprClient);
Assert.assertEquals(actorProxy.getActorId().toString(), "100");
Assert.assertEquals(actorProxy.getActorType(), "myActorType");
}
@Test()
public void invokeActorMethodWithoutDataWithReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
Mono<byte[]> daprResponse =
Mono.just("{\n\t\"data\": \"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"\n}"
.getBytes());
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNull()))
.thenReturn(daprResponse);
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<MyData> result = actorProxy.invokeActorMethod("getData", MyData.class);
MyData myData = result.block();
Assert.assertNotNull(myData);
Assert.assertEquals("valueA", myData.getPropertyA());
Assert.assertEquals("valueB", myData.getPropertyB());// propertyB=null
}
@Test()
public void invokeActorMethodWithoutDataWithEmptyReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNull()))
.thenReturn(Mono.just("".getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<MyData> result = actorProxy.invokeActorMethod("getData", MyData.class);
MyData myData = result.block();
Assert.assertNull(myData);
}
@Test(expected = RuntimeException.class)
public void invokeActorMethodWithIncorrectReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNull()))
.thenReturn(Mono.just("{test}".getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<MyData> result = actorProxy.invokeActorMethod("getData", MyData.class);
result.doOnSuccess(x ->
Assert.fail("Not exception was throw"))
.doOnError(Throwable::printStackTrace
).block();
}
@Test()
public void invokeActorMethodSavingDataWithReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(
Mono.just("{\n\t\"data\": \"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"\n}"
.getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
Mono<MyData> result = actorProxy.invokeActorMethod("getData", saveData, MyData.class);
MyData myData = result.block();
Assert.assertNotNull(myData);
Assert.assertEquals("valueA", myData.getPropertyA());
Assert.assertEquals("valueB", myData.getPropertyB());//propertyB=null
}
@Test(expected = RuntimeException.class)
public void invokeActorMethodSavingDataWithIncorrectReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(Mono.just("{test}".getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
Mono<MyData> result = actorProxy.invokeActorMethod("getData", saveData, MyData.class);
result.doOnSuccess(x ->
Assert.fail("Not exception was throw"))
.doOnError(Throwable::printStackTrace
).block();
}
@Test()
public void invokeActorMethodSavingDataWithEmptyReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(Mono.just("".getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
Mono<MyData> result = actorProxy.invokeActorMethod("getData", saveData, MyData.class);
MyData myData = result.block();
Assert.assertNull(myData);
}
@Test(expected = RuntimeException.class)
public void invokeActorMethodSavingDataWithIncorrectInputType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(Mono.just("{test}".getBytes()));
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
saveData.setMyData(saveData);
Mono<MyData> result = actorProxy.invokeActorMethod("getData", saveData, MyData.class);
result.doOnSuccess(x ->
Assert.fail("Not exception was throw"))
.doOnError(Throwable::printStackTrace
).block();
}
@Test()
public void invokeActorMethodWithDataWithVoidReturnType() {
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(Mono.empty());
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<Void> result = actorProxy.invokeActorMethod("getData", saveData);
Void emptyResponse = result.block();
Assert.assertNull(emptyResponse);
}
@Test(expected = RuntimeException.class)
public void invokeActorMethodWithDataWithVoidIncorrectInputType() {
MyData saveData = new MyData();
saveData.setPropertyA("valueA");
saveData.setPropertyB("valueB");
saveData.setMyData(saveData);
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNotNull()))
.thenReturn(Mono.empty());
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<Void> result = actorProxy.invokeActorMethod("getData", saveData);
Void emptyResponse = result.doOnError(Throwable::printStackTrace).block();
Assert.assertNull(emptyResponse);
}
@Test()
public void invokeActorMethodWithoutDataWithVoidReturnType() {
final DaprClient daprClient = mock(DaprClient.class);
when(daprClient.invokeActorMethod(anyString(), anyString(), anyString(), Mockito.isNull()))
.thenReturn(Mono.empty());
final ActorProxy actorProxy = new ActorProxyImpl(
"myActorType",
new ActorId("100"),
new DefaultObjectSerializer(),
daprClient);
Mono<Void> result = actorProxy.invokeActorMethod("getData");
Void emptyResponse = result.block();
Assert.assertNull(emptyResponse);
}
static class MyData {
/// Gets or sets the value for PropertyA.
private String propertyA;
/// Gets or sets the value for PropertyB.
private String propertyB;
private MyData myData;
public String getPropertyB() {
return propertyB;
}
public void setPropertyB(String propertyB) {
this.propertyB = propertyB;
}
public String getPropertyA() {
return propertyA;
}
public void setPropertyA(String propertyA) {
this.propertyA = propertyA;
}
@Override
public String toString() {
return "MyData{" +
"propertyA='" + propertyA + '\'' +
", propertyB='" + propertyB + '\'' +
'}';
}
public MyData getMyData() {
return myData;
}
public void setMyData(MyData myData) {
this.myData = myData;
}
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.client;
import reactor.core.publisher.Mono;
public class DaprClientStub implements DaprClient {
@Override
public Mono<byte[]> invokeActorMethod(String actorType, String actorId, String methodName, byte[] jsonPayload) {
return Mono.just(new byte[0]);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.client;
import io.dapr.client.DaprHttp;
import io.dapr.client.DaprHttpProxy;
import okhttp3.OkHttpClient;
import okhttp3.mock.Behavior;
import okhttp3.mock.MockInterceptor;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
public class DaprHttpClientTest {
private DaprHttpClient DaprHttpClient;
private OkHttpClient okHttpClient;
private MockInterceptor mockInterceptor;
private final String EXPECTED_RESULT = "{\"data\":\"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"}";
@Before
public void setUp() {
mockInterceptor = new MockInterceptor(Behavior.UNORDERED);
okHttpClient = new OkHttpClient.Builder().addInterceptor(mockInterceptor).build();
}
@Test
public void invokeActorMethod() {
DaprHttp daprHttpMock = mock(DaprHttp.class);
mockInterceptor.addRule()
.post("http://localhost:3000/v1.0/actors/DemoActor/1/method/Payment")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<byte[]> mono =
DaprHttpClient.invokeActorMethod("DemoActor", "1", "Payment", "".getBytes());
assertEquals(new String(mono.block()), EXPECTED_RESULT);
}
}

View File

@ -0,0 +1,64 @@
package io.dapr.actors.it;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import java.util.Optional;
public class BaseIT {
protected static DaprIntegrationTestingRunner daprIntegrationTestingRunner;
public static DaprIntegrationTestingRunner createDaprIntegrationTestingRunner(String successMessage, Class serviceClass, Boolean useAppPort, Boolean useGrpcPort, Boolean useHttpPort, int sleepTime, boolean isClient) {
return new DaprIntegrationTestingRunner(successMessage, serviceClass, useAppPort, useGrpcPort, useHttpPort, sleepTime, isClient);
}
@AfterClass
public static void cleanUp() {
Optional.ofNullable(daprIntegrationTestingRunner).ifPresent(daprRunner -> daprRunner.destroyDapr());
}
public static class MyData {
/// Gets or sets the value for PropertyA.
private String propertyA;
/// Gets or sets the value for PropertyB.
private String propertyB;
private MyData myData;
public String getPropertyB() {
return propertyB;
}
public void setPropertyB(String propertyB) {
this.propertyB = propertyB;
}
public String getPropertyA() {
return propertyA;
}
public void setPropertyA(String propertyA) {
this.propertyA = propertyA;
}
@Override
public String toString() {
return "MyData{" +
"propertyA='" + propertyA + '\'' +
", propertyB='" + propertyB + '\'' +
'}';
}
public MyData getMyData() {
return myData;
}
public void setMyData(MyData myData) {
this.myData = myData;
}
}
}

View File

@ -0,0 +1,168 @@
package io.dapr.actors.it;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class DaprIntegrationTestingRunner {
private static AtomicInteger appGeneratorId = new AtomicInteger();
@Rule
public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
public DaprIntegrationTestingRunner.DaprFreePorts DAPR_FREEPORTS;
private Runtime rt = Runtime.getRuntime();
private Process proc;
private String successMessage;
private Class serviceClass;
private Boolean useAppPort;
private Boolean useGrpcPort;
private Boolean useHttpPort;
private int sleepTime;
private String appName;
private boolean appRanOK = Boolean.FALSE;
private boolean isClient = false;
DaprIntegrationTestingRunner(String successMessage, Class serviceClass, Boolean useAppPort, Boolean useGrpcPort, Boolean useHttpPort, int sleepTime, boolean isClient) {
this.successMessage = successMessage;
this.serviceClass = serviceClass;
this.useAppPort = useAppPort;
this.useGrpcPort = useGrpcPort;
this.useHttpPort = useHttpPort;
this.sleepTime = sleepTime;
this.isClient = isClient;
this.generateAppName();
try {
DAPR_FREEPORTS = new DaprIntegrationTestingRunner.DaprFreePorts().initPorts();
environmentVariables.set("DAPR_HTTP_PORT", String.valueOf(DAPR_FREEPORTS.getHttpPort()));
} catch (Exception e) {
e.printStackTrace();
}
}
public DaprFreePorts initializeDapr() throws Exception {
String daprCommand=this.buildDaprCommand();
System.out.println(daprCommand);
proc= rt.exec(daprCommand);
final Thread stuffToDo = new Thread(() -> {
try {
try (InputStream stdin = proc.getInputStream()) {
try(InputStreamReader isr = new InputStreamReader(stdin)) {
try (BufferedReader br = new BufferedReader(isr)){
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
if (line.contains(successMessage)) {
this.appRanOK = true;
}
}
}
}
}
} catch (IOException ex) {
Assert.fail(ex.getMessage());
}
});
stuffToDo.start();
Thread.sleep(sleepTime);
return DAPR_FREEPORTS;
}
private static final String DAPR_RUN = "dapr run --app-id %s ";
/**
* The args in -Dexec.args are the App name, and if needed the app's port.
* The args are passed as a CSV due to conflict of parsing a space separated list in different OS
*/
private static final String DAPR_COMMAND = " -- mvn exec:java -Dexec.mainClass=%s -Dexec.classpathScope=test -Dexec.args=\"%s,%s\"";
private String buildDaprCommand(){
StringBuilder stringBuilder= new StringBuilder(String.format(DAPR_RUN, this.appName))
.append(this.useAppPort ? "--app-port " + this.DAPR_FREEPORTS.appPort : "")
.append(this.useGrpcPort ? " --grpc-port " + this.DAPR_FREEPORTS.grpcPort : "")
.append(this.useHttpPort ? " --port " + this.DAPR_FREEPORTS.httpPort : "")
.append(String.format(DAPR_COMMAND, this.serviceClass.getCanonicalName(), this.appName, buildPortsParamCommands()));
return stringBuilder.toString();
}
private String buildPortsParamCommands() {
StringBuilder ports = new StringBuilder();
if (this.useAppPort) {
ports.append(this.DAPR_FREEPORTS.appPort);
}
return ports.toString();
}
private void generateAppName(){
this.appName="DAPRapp" + appGeneratorId.incrementAndGet();
}
public boolean isAppRanOK() {
return appRanOK;
}
public String getAppName() {
return appName;
}
private static Integer findRandomOpenPortOnAllLocalInterfaces() throws Exception {
try (
ServerSocket socket = new ServerSocket(0)
) {
return socket.getLocalPort();
}
}
public void destroyDapr() {
Optional.ofNullable(rt).ifPresent( runtime -> {
try {
runtime.exec("dapr stop --app-id " + this.appName);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Optional.ofNullable(proc).ifPresent(process -> process.destroy());
}
public static class DaprFreePorts {
private int grpcPort;
private int httpPort;
private int appPort;
public DaprFreePorts initPorts() throws Exception {
this.appPort = findRandomOpenPortOnAllLocalInterfaces();
this.grpcPort = findRandomOpenPortOnAllLocalInterfaces();
this.httpPort = findRandomOpenPortOnAllLocalInterfaces();
return this;
}
public int getGrpcPort() {
return grpcPort;
}
public int getHttpPort() {
return httpPort;
}
public int getAppPort() {
return appPort;
}
}
}

View File

@ -0,0 +1,105 @@
package io.dapr.actors.it.actors;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyBuilder;
import io.dapr.actors.it.BaseIT;
import io.dapr.actors.it.DaprIntegrationTestingRunner;
import io.dapr.actors.it.services.springboot.ActorService;
import io.dapr.actors.it.services.springboot.EmptyService;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Verb;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.*;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
public class ActivationDeactiviationIT extends BaseIT {
private static Logger logger = LoggerFactory.getLogger(ActivationDeactiviationIT.class);
private static final AtomicInteger atomicInteger = new AtomicInteger(1);
private final String BASE_URL = "actors/%s/%s";
private final DefaultObjectSerializer serializer = new DefaultObjectSerializer();
private DaprIntegrationTestingRunner clientDaprIntegrationTestingRunner;
@Rule
public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
@After
public void cleanUpTestCase() {
Optional.ofNullable(clientDaprIntegrationTestingRunner).ifPresent(daprRunner -> daprRunner.destroyDapr());
}
@BeforeClass
public static void init() throws Exception {
daprIntegrationTestingRunner =
createDaprIntegrationTestingRunner(
"dapr initialized",
ActorService.class,
true,
true,
true,
30000,
false
);
daprIntegrationTestingRunner.initializeDapr();
}
@Test
public void testThatWhenInvokingMethodActorActivatesItselfAndDeactivesIteselfAfterElepsedTime() throws Exception {
Thread.sleep(20000);
assertTrue("Service App did not started sucessfully", daprIntegrationTestingRunner.isAppRanOK());
clientDaprIntegrationTestingRunner =
createDaprIntegrationTestingRunner(
"BUILD SUCCESS",
EmptyService.class,
false,
false,
true,
20000,
true
);
clientDaprIntegrationTestingRunner.initializeDapr();
environmentVariables.set("DAPR_HTTP_PORT", String.valueOf(clientDaprIntegrationTestingRunner.DAPR_FREEPORTS.getHttpPort()));
final AtomicInteger atomicInteger = new AtomicInteger(1);
String actorType = "DemoActorTest";
DefaultObjectSerializer serializer = new DefaultObjectSerializer();
logger.debug("Creating proxy builder");
ActorProxyBuilder proxyBuilder = new ActorProxyBuilder(actorType, serializer);
logger.debug("Creating actorId");
ActorId actorId1 = new ActorId(Integer.toString(atomicInteger.getAndIncrement()));
logger.debug("Building proxy");
ActorProxy proxy = proxyBuilder.build(actorId1);
logger.debug("Invoking Say from Proxy");
String sayResponse = proxy.invokeActorMethod("say", "message", String.class).block();
logger.debug("asserting not null response: [" + sayResponse + "]");
assertNotNull(sayResponse);
logger.debug("Retrieving active Actors");
List<String> activeActors = proxy.invokeActorMethod("retrieveActiveActors", null, List.class).block();
logger.debug("Active actors: [" + activeActors.toString() + "]");
assertTrue("Expecting actorId:[" + actorId1.toString() + "]", activeActors.contains(actorId1.toString()));
logger.debug("Waitng for 15 seconds so actor deactives itself");
Thread.sleep(15000);
ActorId actorId2 = new ActorId(Integer.toString(atomicInteger.getAndIncrement()));
ActorProxy proxy2 = proxyBuilder.build(actorId2);
List<String> activeActorsSecondtry = proxy2.invokeActorMethod("retrieveActiveActors", null, List.class).block();
logger.debug("Active actors: [" + activeActorsSecondtry.toString() + "]");
assertFalse("NOT Expecting actorId:[" + actorId1.toString() + "]", activeActorsSecondtry.contains(actorId1.toString()));
}
}

View File

@ -0,0 +1,26 @@
package io.dapr.actors.it.services.springboot;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.runtime.ActorRuntime;
import io.dapr.serializer.DefaultObjectSerializer;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
public class ActorService {
/**
* Starts the service.
* @param args Expects the port: -p PORT
* @throws Exception If cannot start service.
*/
public static void main(String[] args) throws Exception {
// If port string is not valid, it will throw an exception.
long port = Long.parseLong(args[0].split(",")[1]);
ActorRuntime.getInstance().registerActor(DemoActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
DaprApplication.start(port);
}
}

View File

@ -0,0 +1,21 @@
package io.dapr.actors.it.services.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Dapr's HTTP callback implementation via SpringBoot.
*/
@SpringBootApplication(scanBasePackages = {"io.dapr.actors.it.services.springboot"})
public class DaprApplication {
/**
* Starts Dapr's callback in a given port.
* @param port Port to listen to.
*/
public static void start(long port) {
SpringApplication app = new SpringApplication(DaprApplication.class);
app.run(String.format("--server.port=%d", port));
}
}

View File

@ -0,0 +1,68 @@
package io.dapr.actors.it.services.springboot;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.actors.runtime.ActorManagerTest;
import io.dapr.actors.runtime.ActorRuntime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Map;
import java.util.TimeZone;
/**
* SpringBoot Controller to handle callback APIs for Dapr.
*/
@RestController
public class DaprController {
@GetMapping("/")
public String index() {
return "Greetings from Dapr!";
}
@GetMapping("/dapr/config")
public String daprConfig() throws Exception {
return "{\"actorIdleTimeout\":\"5s\",\"actorScanInterval\":\"2s\",\"drainOngoingCallTimeout\":\"1s\",\"drainBalancedActors\":true,\"entities\":[\"DemoActorTest\"]}";
}
@PostMapping(path = "/actors/{type}/{id}")
public Mono<Void> activateActor(@PathVariable("type") String type,
@PathVariable("id") String id) throws Exception {
return ActorRuntime.getInstance().activate(type, id);
}
@DeleteMapping(path = "/actors/{type}/{id}")
public Mono<Void> deactivateActor(@PathVariable("type") String type,
@PathVariable("id") String id) throws Exception {
return ActorRuntime.getInstance().deactivate(type, id);
}
@PutMapping(path = "/actors/{type}/{id}/method/{method}")
public Mono<byte[]> invokeActorMethod(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("method") String method,
@RequestBody(required = false) byte[] body) {
return ActorRuntime.getInstance().invoke(type, id, method, body);
}
@PutMapping(path = "/actors/{type}/{id}/method/timer/{timer}")
public Mono<Void> invokeActorTimer(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("timer") String timer) {
return ActorRuntime.getInstance().invokeTimer(type, id, timer);
}
@PutMapping(path = "/actors/{type}/{id}/method/remind/{reminder}")
public Mono<Void> invokeActorReminder(@PathVariable("type") String type,
@PathVariable("id") String id,
@PathVariable("reminder") String reminder,
@RequestBody(required = false) byte[] body) {
return ActorRuntime.getInstance().invokeReminder(type, id, reminder, body);
}
}

View File

@ -0,0 +1,9 @@
package io.dapr.actors.it.services.springboot;
import java.util.List;
public interface DemoActor {
String say(String something);
List<String> retrieveActiveActors();
}

View File

@ -0,0 +1,56 @@
package io.dapr.actors.it.services.springboot;
import io.dapr.actors.ActorId;
import io.dapr.actors.runtime.AbstractActor;
import io.dapr.actors.runtime.ActorRuntimeContext;
import io.dapr.actors.runtime.ActorType;
import reactor.core.publisher.Mono;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;
@ActorType(name = "DemoActorTest")
public class DemoActorImpl extends AbstractActor implements DemoActor {
public static final List<String> ACTIVE_ACTOR = new ArrayList<>();
public DemoActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
}
/**
* Format to output date and time.
*/
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public String say(String something) {
Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());
// Handles the request by printing message.
System.out.println("Server say method for actor " +
super.getId() + ": " +
(something == null ? "" : something + " @ " + utcNowAsString));
// Now respond with current timestamp.
return utcNowAsString;
}
@Override
public List<String> retrieveActiveActors() {
return Collections.unmodifiableList(ACTIVE_ACTOR);
}
@Override
protected Mono<Void> onActivate() {
return Mono.fromRunnable(() -> ACTIVE_ACTOR.add(super.getId().toString())).then(super.onActivate());
}
@Override
protected Mono<Void> onDeactivate() {
return Mono.fromRunnable(() -> ACTIVE_ACTOR.remove(super.getId().toString())).then(super.onDeactivate());
}
}

View File

@ -0,0 +1,14 @@
package io.dapr.actors.it.services.springboot;
/**
* Use this class in order to run DAPR with any needed services, like states.
*
* To run manually, from repo root:
* 1. mvn clean install
* 2. dapr run --grpc-port 41707 --port 32851 -- mvn exec:java -Dexec.mainClass=io.dapr.it.services.EmptyService -Dexec.classpathScope="test" -Dexec.args="-p 44511 -grpcPort 41707 -httpPort 32851" -pl=sdk
*/
public class EmptyService {
public static void main(String[] args) {
System.out.println("Hello from EmptyService");
}
}

View File

@ -0,0 +1,176 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyForTestsImpl;
import io.dapr.actors.client.DaprClientStub;
import io.dapr.serializer.DaprObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ActorCustomSerializerTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final DaprObjectSerializer CUSTOM_SERIALIZER = new JavaSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
private final ActorRuntimeContext context = createContext();
private ActorManager<ActorImpl> manager = new ActorManager<>(context);
public interface MyActor {
Mono<Integer> intInIntOut(int input);
Mono<String> stringInStringOut(String input);
Mono<MyData> classInClassOut(MyData input);
}
@ActorType(name = "MyActor")
public static class ActorImpl extends AbstractActor implements MyActor {
//public MyActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
public ActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
}
@Override
public Mono<Integer> intInIntOut(int input) {
return Mono.fromSupplier(() -> input + input);
}
@Override
public Mono<String> stringInStringOut(String input) {
return Mono.fromSupplier(() -> input + input);
}
@Override
public Mono<MyData> classInClassOut(MyData input) {
return Mono.fromSupplier(() -> new MyData(
input.getName() + input.getName(),
input.getNum() + input.getNum())
);
}
}
static class MyData implements Serializable {
private String name;
private int num;
public MyData() {
this.name = "";
this.num = 0;
}
public MyData(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return this.name;
}
public int getNum() {
return this.num;
}
}
@Test
public void classInClassOut() {
ActorProxy actorProxy = createActorProxy();
MyData d = new MyData("hi", 3);
MyData response = actorProxy.invokeActorMethod("classInClassOut", d, MyData.class).block();
Assert.assertEquals("hihi", response.getName());
Assert.assertEquals(6, response.getNum());
}
@Test
public void stringInStringOut() {
ActorProxy actorProxy = createActorProxy();
String response = actorProxy.invokeActorMethod("stringInStringOut", "oi", String.class).block();
Assert.assertEquals("oioi", response);
}
@Test
public void intInIntOut() {
ActorProxy actorProxy = createActorProxy();
int response = actorProxy.invokeActorMethod("intInIntOut", 2, int.class).block();
Assert.assertEquals(4, response);
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private ActorProxy createActorProxy() {
ActorId actorId = newActorId();
// Mock daprClient for ActorProxy only, not for runtime.
DaprClientStub daprClient = mock(DaprClientStub.class);
when(daprClient.invokeActorMethod(
eq(context.getActorTypeInformation().getName()),
eq(actorId.toString()),
any(),
any()))
.thenAnswer(invocationOnMock ->
this.manager.invokeMethod(
new ActorId(invocationOnMock.getArgument(1, String.class)),
invocationOnMock.getArgument(2, String.class),
INTERNAL_SERIALIZER.unwrapData(invocationOnMock.getArgument(3, byte[].class)))
.map(s -> {
try {
return INTERNAL_SERIALIZER.wrapData(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
this.manager.activateActor(actorId).block();
return new ActorProxyForTestsImpl(
context.getActorTypeInformation().getName(),
actorId,
CUSTOM_SERIALIZER,
daprClient);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
CUSTOM_SERIALIZER,
new DefaultActorFactory<T>(),
ActorTypeInformation.create(ActorImpl.class),
daprClient,
mock(DaprStateAsyncProvider.class)
);
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for Actor Manager
*/
public class ActorManagerTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
interface MyActor {
String say(String something);
int getCount();
void incrementCount(int delta);
}
public static class NotRemindableActor extends AbstractActor {
public NotRemindableActor(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
}
}
@ActorType(name = "MyActor")
public static class MyActorImpl extends AbstractActor implements MyActor, Remindable<String> {
private int timeCount = 0;
@Override
public String say(String something) {
return executeSayMethod(something);
}
@Override
public int getCount() {
return this.timeCount;
}
@Override
public void incrementCount(int delta) {
this.timeCount = timeCount + delta;
}
public MyActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
super.registerActorTimer(
"count",
"incrementCount",
2,
Duration.ofSeconds(1),
Duration.ofSeconds(1)
).block();
}
@Override
public Class<String> getStateType() {
return String.class;
}
@Override
public Mono<Void> receiveReminder(String reminderName, String state, Duration dueTime, Duration period) {
return Mono.empty();
}
}
private ActorRuntimeContext<MyActorImpl> context = createContext(MyActorImpl.class);
private ActorManager<MyActorImpl> manager = new ActorManager<>(context);
@Test(expected = IllegalArgumentException.class)
public void invokeBeforeActivate() throws Exception {
ActorId actorId = newActorId();
String message = "something";
this.manager.invokeMethod(actorId, "say", message.getBytes()).block();
}
@Test
public void activateThenInvoke() throws Exception {
ActorId actorId = newActorId();
byte[] message = this.context.getObjectSerializer().serialize("something");
this.manager.activateActor(actorId).block();
byte[] response = this.manager.invokeMethod(actorId, "say", message).block();
Assert.assertEquals(executeSayMethod(
this.context.getObjectSerializer().deserialize(message, String.class)),
this.context.getObjectSerializer().deserialize(response, String.class));
}
@Test(expected = IllegalArgumentException.class)
public void activateInvokeDeactivateThenInvoke() throws Exception {
ActorId actorId = newActorId();
byte[] message = this.context.getObjectSerializer().serialize("something");
this.manager.activateActor(actorId).block();
byte[] response = this.manager.invokeMethod(actorId, "say", message).block();
Assert.assertEquals(executeSayMethod(
this.context.getObjectSerializer().deserialize(message, String.class)),
this.context.getObjectSerializer().deserialize(response, String.class));
this.manager.deactivateActor(actorId).block();
this.manager.invokeMethod(actorId, "say", message).block();
}
@Test
public void invokeReminderNotRemindable() throws Exception {
ActorId actorId = newActorId();
ActorRuntimeContext<NotRemindableActor> context = createContext(NotRemindableActor.class);
ActorManager<NotRemindableActor> manager = new ActorManager<>(context);
manager.invokeReminder(actorId, "myremind", createReminderParams("hello")).block();
}
@Test(expected = IllegalArgumentException.class)
public void invokeReminderBeforeActivate() throws Exception {
ActorId actorId = newActorId();
this.manager.invokeReminder(actorId, "myremind", createReminderParams("hello")).block();
}
@Test
public void activateThenInvokeReminder() throws Exception {
ActorId actorId = newActorId();
this.manager.activateActor(actorId).block();
this.manager.invokeReminder(actorId, "myremind", createReminderParams("hello")).block();
}
@Test(expected = IllegalArgumentException.class)
public void activateDeactivateThenInvokeReminder() throws Exception {
ActorId actorId = newActorId();
this.manager.activateActor(actorId).block();
this.manager.deactivateActor(actorId).block();;
this.manager.invokeReminder(actorId, "myremind", createReminderParams("hello")).block();
}
@Test(expected = IllegalArgumentException.class)
public void invokeTimerBeforeActivate() {
ActorId actorId = newActorId();
this.manager.invokeTimer(actorId, "count").block();
}
@Test(expected = IllegalStateException.class)
public void activateThenInvokeTimerBeforeRegister() {
ActorId actorId = newActorId();
this.manager.activateActor(actorId).block();
this.manager.invokeTimer(actorId, "unknown").block();
}
@Test
public void activateThenInvokeTimer() {
ActorId actorId = newActorId();
this.manager.activateActor(actorId).block();
this.manager.invokeTimer(actorId, "count").block();
byte[] response = this.manager.invokeMethod(actorId, "getCount", null).block();
Assert.assertEquals("2", new String(response));
}
@Test(expected = IllegalArgumentException.class)
public void activateInvokeTimerDeactivateThenInvokeTimer() {
ActorId actorId = newActorId();
this.manager.activateActor(actorId).block();
this.manager.invokeTimer(actorId, "count").block();
byte[] response = this.manager.invokeMethod(actorId, "getCount", null).block();
Assert.assertEquals("2", new String(response));
this.manager.deactivateActor(actorId).block();
this.manager.invokeTimer(actorId, "count").block();
}
private byte[] createReminderParams(String data) throws IOException {
byte[] serializedData = this.context.getObjectSerializer().serialize(data);
ActorReminderParams params = new ActorReminderParams(serializedData, Duration.ofSeconds(1), Duration.ofSeconds(1));
return INTERNAL_SERIALIZER.serialize(params);
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private static String executeSayMethod(String something) {
return "Said: " + (something == null ? "" : something);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext(Class<T> clazz) {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
new DefaultObjectSerializer(),
new DefaultActorFactory<T>(),
ActorTypeInformation.create(clazz),
daprClient,
mock(DaprStateAsyncProvider.class)
);
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import org.junit.Assert;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
/**
* Unit tests for ActorMethodInfoMap.
*/
public class ActorMethodInfoMapTest {
@Test
public void normalUsage() {
ArrayList<Class<?>> interfaceTypes = new ArrayList<>();
interfaceTypes.add(TestActor.class);
ActorMethodInfoMap m = new ActorMethodInfoMap(interfaceTypes);
try {
Method m1 = m.get("getData");
Assert.assertEquals("getData", m1.getName());
Class c = m1.getReturnType();
Assert.assertEquals(c.getClass(), String.class.getClass());
Parameter[] p = m1.getParameters();
Assert.assertEquals(p[0].getType().getClass(), String.class.getClass());
} catch (Exception e) {
Assert.fail("Exception not expected.");
}
}
@Test(expected = NoSuchMethodException.class)
public void lookUpNonExistingMethod() throws NoSuchMethodException {
ArrayList<Class<?>> interfaceTypes = new ArrayList<>();
interfaceTypes.add(TestActor.class);
ActorMethodInfoMap m = new ActorMethodInfoMap(interfaceTypes);
m.get("thisMethodDoesNotExist");
}
/**
* Only used for this test.
*/
public interface TestActor {
String getData(String key);
}
}

View File

@ -0,0 +1,219 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyForTestsImpl;
import io.dapr.actors.client.DaprClientStub;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ActorNoStateTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
private final ActorRuntimeContext context = createContext();
private ActorManager<ActorImpl> manager = new ActorManager<>(context);
public interface MyActor {
// The test will only call the versions of this in a derived class to the user code base class.
// The user code base class version will throw.
Mono<String> stringInStringOut(String input);
Mono<Boolean> stringInBooleanOut(String input);
Mono<Void> stringInVoidOutIntentionallyThrows(String input);
Mono<MyData> classInClassOut(MyData input);
}
@ActorType(name = "MyActor")
public static class ActorImpl extends AbstractActor implements MyActor {
private final ActorId id;
private boolean activated;
private boolean methodReturningVoidInvoked;
//public MyActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
public ActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
this.methodReturningVoidInvoked = false;
}
@Override
public Mono<String> stringInStringOut(String s) {
return Mono.fromSupplier(() -> {
return s + s;
}
);
}
@Override
public Mono<Boolean> stringInBooleanOut(String s) {
return Mono.fromSupplier(() -> {
if (s.equals("true")) {
return true;
} else {
return false;
}
});
}
@Override
public Mono<Void> stringInVoidOutIntentionallyThrows(String input) {
return Mono.fromRunnable(() -> {
// IllegalMonitorStateException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new IllegalMonitorStateException("IntentionalException");
});
}
@Override
public Mono<MyData> classInClassOut(MyData input) {
return Mono.fromSupplier(() -> {
return new MyData(
input.getName() + input.getName(),
input.getNum() + input.getNum());
});
}
}
static class MyData {
private String name;
private int num;
public MyData() {
this.name = "";
this.num = 0;
}
public MyData(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return this.name;
}
public int getNum() {
return this.num;
}
}
@Test
public void stringInStringOut() {
ActorProxy proxy = createActorProxy();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
"abcabc",
proxy.invokeActorMethod("stringInStringOut", "abc", String.class).block());
}
@Test
public void stringInBooleanOut() {
ActorProxy proxy = createActorProxy();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
false,
proxy.invokeActorMethod("stringInBooleanOut", "hello world", Boolean.class).block());
Assert.assertEquals(
true,
proxy.invokeActorMethod("stringInBooleanOut", "true", Boolean.class).block());
}
@Test(expected = IllegalMonitorStateException.class)
public void stringInVoidOutIntentionallyThrows() {
ActorProxy actorProxy = createActorProxy();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
actorProxy.invokeActorMethod("stringInVoidOutIntentionallyThrows", "hello world").block();
}
@Test
public void classInClassOut() {
ActorProxy actorProxy = createActorProxy();
MyData d = new MyData("hi", 3);
// this should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
MyData response = actorProxy.invokeActorMethod("classInClassOut", d, MyData.class).block();
Assert.assertEquals(
"hihi",
response.getName());
Assert.assertEquals(
6,
response.getNum());
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private ActorProxy createActorProxy() {
ActorId actorId = newActorId();
// Mock daprClient for ActorProxy only, not for runtime.
DaprClientStub daprClient = mock(DaprClientStub.class);
when(daprClient.invokeActorMethod(
eq(context.getActorTypeInformation().getName()),
eq(actorId.toString()),
any(),
any()))
.thenAnswer(invocationOnMock ->
this.manager.invokeMethod(
new ActorId(invocationOnMock.getArgument(1, String.class)),
invocationOnMock.getArgument(2, String.class),
INTERNAL_SERIALIZER.unwrapData(invocationOnMock.getArgument(3, byte[].class)))
.map(s -> {
try {
return INTERNAL_SERIALIZER.wrapData(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
this.manager.activateActor(actorId).block();
return new ActorProxyForTestsImpl(
context.getActorTypeInformation().getName(),
actorId,
new DefaultObjectSerializer(),
daprClient);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
new DefaultObjectSerializer(),
new DefaultActorFactory<T>(),
ActorTypeInformation.create(ActorImpl.class),
daprClient,
mock(DaprStateAsyncProvider.class)
);
}
}

View File

@ -0,0 +1,63 @@
package io.dapr.actors.runtime;
import org.junit.Assert;
import org.junit.Test;
import java.time.Duration;
public class ActorReminderParamsTest {
private static final ObjectSerializer SERIALIZER = new ObjectSerializer();
@Test(expected = IllegalArgumentException.class)
public void outOfRangeDueTime() {
ActorReminderParams info = new ActorReminderParams(null, Duration.ZERO.plusSeconds(-10), Duration.ZERO.plusMinutes(1));
}
@Test
public void negativePeriod() {
// this is ok
ActorReminderParams info = new ActorReminderParams(null, Duration.ZERO.plusMinutes(1), Duration.ZERO.plusMillis(-1));
}
@Test(expected = IllegalArgumentException.class)
public void outOfRangePeriod() {
ActorReminderParams info = new ActorReminderParams(null, Duration.ZERO.plusMinutes(1), Duration.ZERO.plusMinutes(-10));
}
@Test
public void noState() {
ActorReminderParams original = new ActorReminderParams(null, Duration.ZERO.plusMinutes(2), Duration.ZERO.plusMinutes((5)));
ActorReminderParams recreated = null;
try {
byte[] serialized = SERIALIZER.serialize(original);
recreated = SERIALIZER.deserialize(serialized, ActorReminderParams.class);
}
catch(Exception e) {
System.out.println("The error is: " + e);
Assert.fail();
}
Assert.assertArrayEquals(original.getData(), recreated.getData());
Assert.assertEquals(original.getDueTime(), recreated.getDueTime());
Assert.assertEquals(original.getPeriod(), recreated.getPeriod());
}
@Test
public void withState() {
ActorReminderParams original = new ActorReminderParams("maru".getBytes(), Duration.ZERO.plusMinutes(2), Duration.ZERO.plusMinutes((5)));
ActorReminderParams recreated = null;
try {
byte[] serialized = SERIALIZER.serialize(original);
recreated = SERIALIZER.deserialize(serialized, ActorReminderParams.class);
}
catch(Exception e) {
System.out.println("The error is: " + e);
Assert.fail();
}
Assert.assertArrayEquals(original.getData(), recreated.getData());
Assert.assertEquals(original.getDueTime(), recreated.getDueTime());
Assert.assertEquals(original.getPeriod(), recreated.getPeriod());
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.UUID;
import static org.mockito.Mockito.mock;
public class ActorRuntimeTest {
private static final String ACTOR_NAME = "MyGreatActor";
public interface MyActor {
String say();
}
@ActorType(name = ACTOR_NAME)
public static class MyActorImpl extends AbstractActor implements MyActor {
public MyActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
}
public String say() {
return "Nothing to say.";
}
}
private static final ObjectSerializer ACTOR_STATE_SERIALIZER = new ObjectSerializer();
private static Constructor<ActorRuntime> constructor;
private DaprClient mockDaprClient;
private ActorRuntime runtime;
@BeforeClass
public static void beforeAll() throws Exception {
constructor = (Constructor<ActorRuntime>) Arrays.stream(ActorRuntime.class.getDeclaredConstructors())
.filter(c -> c.getParameters().length == 1)
.map(c -> {
c.setAccessible(true);
return c;
})
.findFirst()
.get();
}
@Before
public void setup() throws Exception {
this.mockDaprClient = mock(DaprClient.class);
this.runtime = constructor.newInstance(this.mockDaprClient);
}
@Test
public void registerActor() throws Exception {
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
Assert.assertTrue(new String(this.runtime.serializeConfig()).contains(ACTOR_NAME));
}
@Test
public void activateActor() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.activate(ACTOR_NAME, actorId).block();
}
@Test
public void invokeActor() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.activate(ACTOR_NAME, actorId).block();
byte[] response = this.runtime.invoke(ACTOR_NAME, actorId, "say", null).block();
String message = ACTOR_STATE_SERIALIZER.deserialize(ACTOR_STATE_SERIALIZER.unwrapData(response), String.class);
Assert.assertEquals("Nothing to say.", message);
}
@Test
public void activateThendeactivateActor() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.activate(ACTOR_NAME, actorId).block();
this.runtime.deactivate(ACTOR_NAME, actorId).block();
}
@Test
public void deactivateActor() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.deactivate(ACTOR_NAME, actorId).block();
}
@Test
public void lazyActivate() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.activate(ACTOR_NAME, actorId).block();
this.runtime.invoke(ACTOR_NAME, actorId, "say", null)
.doOnError(e -> Assert.assertTrue(e.getMessage().contains("Could not find actor")))
.doOnSuccess(s -> Assert.fail())
.onErrorReturn("".getBytes())
.block();
}
@Test
public void lazyDeactivate() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
this.runtime.activate(ACTOR_NAME, actorId).block();
Mono<Void> deacticateCall = this.runtime.deactivate(ACTOR_NAME, actorId);
this.runtime.invoke(ACTOR_NAME, actorId, "say", null).block();
deacticateCall.block();
this.runtime.invoke(ACTOR_NAME, actorId, "say", null)
.doOnError(e -> Assert.assertTrue(e.getMessage().contains("Could not find actor")))
.doOnSuccess(s -> Assert.fail())
.onErrorReturn("".getBytes())
.block();
}
@Test
public void lazyInvoke() throws Exception {
String actorId = UUID.randomUUID().toString();
this.runtime.registerActor(MyActorImpl.class, new DefaultObjectSerializer(), new DefaultObjectSerializer());
Mono<byte[]> invokeCall = this.runtime.invoke(ACTOR_NAME, actorId, "say", null);
this.runtime.activate(ACTOR_NAME, actorId).block();
invokeCall.block();
}
}

View File

@ -0,0 +1,665 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyForTestsImpl;
import io.dapr.actors.client.DaprClientStub;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.IllegalCharsetNameException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ActorStatefulTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
private static final Collection<String> DEACTIVATED_ACTOR_IDS = Collections.synchronizedList(new ArrayList<>());
private final ActorRuntimeContext context = createContext();
private ActorManager<MyActorImpl> manager = new ActorManager<>(context);
public interface MyActor {
Mono<Boolean> isActive();
MyMethodContext getPreCallMethodContext();
MyMethodContext getPostCallMethodContext();
Mono<Void> unregisterTimerAndReminder();
Mono<Integer> incrementAndGetCount(int increment) throws Exception;
Mono<Integer> getCountButThrowsException();
Mono<Void> addMessage(String message);
Mono<String> setMessage(String message);
Mono<String> getMessage();
Mono<Boolean> hasMessage();
Mono<Void> deleteMessage();
Mono<Void> forceDuplicateException();
Mono<Void> forcePartialChange();
Mono<Void> throwsWithoutSaving();
Mono<Void> setMethodContext(MyMethodContext context);
Mono<MyMethodContext> getMethodContext();
String getIdString();
}
@ActorType(name = "MyActor")
public static class MyActorImpl extends AbstractActor implements MyActor, Remindable<String> {
private final ActorId id;
private boolean activated;
private MyMethodContext preMethodCalled;
private MyMethodContext postMethodCalled;
public MyActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
}
@Override
public Mono<Boolean> isActive() {
return Mono.fromSupplier(() -> this.activated);
}
@Override
public Mono<Void> onActivate() {
return Mono
.fromRunnable(() -> this.activated = true)
.then(super.registerActorTimer(
"mytimer",
"hasMessage",
null,
Duration.ofSeconds(1),
Duration.ofSeconds(1)))
.then(super.registerReminder(
"myreminder",
null,
Duration.ofSeconds(1),
Duration.ofSeconds(1)
));
}
@Override
public Mono<Void> onDeactivate() {
return Mono.fromRunnable(() -> DEACTIVATED_ACTOR_IDS.add(this.id.toString()));
}
@Override
public Mono<Void> onPreActorMethod(ActorMethodContext context) {
// Only keep the first one to make sure we can validate it via another method invocation.
return Mono.fromRunnable(() -> {
this.preMethodCalled = this.preMethodCalled != null ? this.preMethodCalled : new MyMethodContext()
.setName(context.getMethodName())
.setType(context.getCallType().toString());
});
}
@Override
public Mono<Void> onPostActorMethod(ActorMethodContext context) {
// Only keep the first one to make sure we can validate it via another method invocation.
return Mono.fromRunnable(() -> {
this.postMethodCalled = this.postMethodCalled != null ? this.postMethodCalled : new MyMethodContext()
.setName(context.getMethodName())
.setType(context.getCallType().toString());
});
}
@Override
public MyMethodContext getPreCallMethodContext() {
return this.preMethodCalled;
}
@Override
public MyMethodContext getPostCallMethodContext() {
return this.postMethodCalled;
}
@Override
public Mono<Void> unregisterTimerAndReminder() {
return super.unregisterReminder("UnknownReminder")
.then(super.unregisterTimer("UnknownTimer"))
.then(super.unregisterReminder("myreminder"))
.then(super.unregisterTimer("mytimer"));
}
@Override
public Mono<Integer> incrementAndGetCount(int increment) {
return Mono.fromRunnable(() -> {
if (increment == 0) {
// Artificial exception case for testing.
throw new NumberFormatException("increment cannot be zero.");
}
})
.then(super.getActorStateManager().contains("counter"))
.flatMap(contains -> {
if (!contains) {
return Mono.just(0);
}
return super.getActorStateManager().get("counter", int.class);
})
.map(count -> count + increment)
.flatMap(count -> super.getActorStateManager().set("counter", count).thenReturn(count));
}
@Override
public Mono<Integer> getCountButThrowsException() {
return super.getActorStateManager().get("counter_WRONG_NAME", int.class);
}
@Override
public Mono<Void> addMessage(String message) {
return super.getActorStateManager().add("message", message);
}
@Override
public Mono<String> setMessage(String message) {
return super.getActorStateManager().set("message", message).thenReturn(executeSayMethod(message));
}
@Override
public Mono<String> getMessage() {
return super.getActorStateManager().get("message", String.class);
}
@Override
public Mono<Boolean> hasMessage() {
return super.getActorStateManager().contains("message");
}
@Override
public Mono<Void> deleteMessage() {
return super.getActorStateManager().remove("message");
}
@Override
public Mono<Void> forceDuplicateException() {
// Second add should throw exception.
return super.getActorStateManager().add("message", "anything")
.then(super.getActorStateManager().add("message", "something else"));
}
@Override
public Mono<Void> forcePartialChange() {
return super.getActorStateManager().add("message", "first message")
.then(super.saveState())
.then(super.getActorStateManager().add("message", "second message"));
}
@Override
public Mono<Void> throwsWithoutSaving() {
return super.getActorStateManager().add("message", "first message")
.then(Mono.error(new IllegalCharsetNameException("random")));
}
@Override
public Mono<Void> setMethodContext(MyMethodContext context) {
return super.getActorStateManager().set("context", context);
}
@Override
public Mono<MyMethodContext> getMethodContext() {
return super.getActorStateManager().get("context", MyMethodContext.class);
}
// Blocking methods are also supported for Actors. Mono is not required.
@Override
public String getIdString() {
return this.id.toString();
}
@Override
public Class<String> getStateType() {
// Remindable type.
return String.class;
}
@Override
public Mono<Void> receiveReminder(String reminderName, String state, Duration dueTime, Duration period) {
return Mono.empty();
}
}
// Class used to validate serialization/deserialization
public static class MyMethodContext implements Serializable {
private String type;
private String name;
public String getType() {
return type;
}
public MyMethodContext setType(String type) {
this.type = type;
return this;
}
public String getName() {
return name;
}
public MyMethodContext setName(String name) {
this.name = name;
return this;
}
}
@Test
public void happyGetSetDeleteContains() {
ActorProxy proxy = newActorProxy();
Assert.assertEquals(
proxy.getActorId().toString(), proxy.invokeActorMethod("getIdString", String.class).block());
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("setMessage", "hello world").block();
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
Assert.assertEquals(
"hello world", proxy.invokeActorMethod("getMessage", String.class).block());
Assert.assertEquals(
executeSayMethod("hello world"),
proxy.invokeActorMethod("setMessage", "hello world", String.class).block());
proxy.invokeActorMethod("deleteMessage").block();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test(expected = IllegalStateException.class)
public void lazyGet() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("setMessage", "first message").block();
// Creates the mono plan but does not call it yet.
Mono<String> getMessageCall = proxy.invokeActorMethod("getMessage", String.class);
proxy.invokeActorMethod("deleteMessage").block();
// Call should fail because the message was deleted.
getMessageCall.block();
}
@Test
public void lazySet() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Creates the mono plan but does not call it yet.
Mono<Void> setMessageCall = proxy.invokeActorMethod("setMessage", "first message");
// No call executed yet, so message should not be set.
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
setMessageCall.block();
// Now the message has been set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test
public void lazyContains() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Creates the mono plan but does not call it yet.
Mono<Boolean> hasMessageCall = proxy.invokeActorMethod("hasMessage", Boolean.class);
// Sets the message.
proxy.invokeActorMethod("setMessage", "hello world").block();
// Now we check if message is set.
hasMessageCall.block();
// Now the message should be set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test
public void lazyDelete() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("setMessage", "first message").block();
// Message is set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Created the mono plan but does not execute it yet.
Mono<Void> deleteMessageCall = proxy.invokeActorMethod("deleteMessage");
// Message is still set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
deleteMessageCall.block();
// Now message is not set.
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test
public void lazyAdd() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("setMessage", "first message").block();
// Message is set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Created the mono plan but does not execute it yet.
Mono<Void> addMessageCall = proxy.invokeActorMethod("addMessage", "second message");
// Message is still set.
Assert.assertEquals("first message",
proxy.invokeActorMethod("getMessage", String.class).block());
// Delete message
proxy.invokeActorMethod("deleteMessage").block();
// Should work since previous message was deleted.
addMessageCall.block();
// New message is still set.
Assert.assertEquals("second message",
proxy.invokeActorMethod("getMessage", String.class).block());
}
@Test
public void onActivateAndOnDeactivate() {
ActorProxy proxy = newActorProxy();
Assert.assertTrue(proxy.invokeActorMethod("isActive", Boolean.class).block());
Assert.assertFalse(DEACTIVATED_ACTOR_IDS.contains(proxy.getActorId().toString()));
proxy.invokeActorMethod("hasMessage", Boolean.class).block();
this.manager.deactivateActor(proxy.getActorId()).block();
Assert.assertTrue(DEACTIVATED_ACTOR_IDS.contains(proxy.getActorId().toString()));
}
@Test
public void onPreMethodAndOnPostMethod() {
ActorProxy proxy = newActorProxy();
proxy.invokeActorMethod("hasMessage", Boolean.class).block();
MyMethodContext preContext =
proxy.invokeActorMethod("getPreCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("hasMessage", preContext.getName());
Assert.assertEquals(ActorCallType.ACTOR_INTERFACE_METHOD.toString(), preContext.getType());
MyMethodContext postContext =
proxy.invokeActorMethod("getPostCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("hasMessage", postContext.getName());
Assert.assertEquals(ActorCallType.ACTOR_INTERFACE_METHOD.toString(), postContext.getType());
}
@Test
public void invokeTimer() {
ActorProxy proxy = newActorProxy();
this.manager.invokeTimer(proxy.getActorId(), "mytimer").block();
MyMethodContext preContext =
proxy.invokeActorMethod("getPreCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("mytimer", preContext.getName());
Assert.assertEquals(ActorCallType.TIMER_METHOD.toString(), preContext.getType());
MyMethodContext postContext =
proxy.invokeActorMethod("getPostCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("mytimer", postContext.getName());
Assert.assertEquals(ActorCallType.TIMER_METHOD.toString(), postContext.getType());
}
@Test(expected = IllegalArgumentException.class)
public void invokeTimerAfterDeactivate() {
ActorProxy proxy = newActorProxy();
this.manager.deactivateActor(proxy.getActorId()).block();
this.manager.invokeTimer(proxy.getActorId(), "mytimer").block();
}
@Test(expected = IllegalStateException.class)
public void invokeTimerAfterUnregister() {
ActorProxy proxy = newActorProxy();
proxy.invokeActorMethod("unregisterTimerAndReminder").block();
this.manager.invokeTimer(proxy.getActorId(), "mytimer").block();
}
@Test(expected = IllegalStateException.class)
public void invokeUnknownTimer() {
ActorProxy proxy = newActorProxy();
this.manager.invokeTimer(proxy.getActorId(), "unknown").block();
}
@Test
public void invokeReminder() throws Exception {
ActorProxy proxy = newActorProxy();
byte[] params = createReminderParams("anything");
this.manager.invokeReminder(proxy.getActorId(), "myreminder", params).block();
MyMethodContext preContext =
proxy.invokeActorMethod("getPreCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("myreminder", preContext.getName());
Assert.assertEquals(ActorCallType.REMINDER_METHOD.toString(), preContext.getType());
MyMethodContext postContext =
proxy.invokeActorMethod("getPostCallMethodContext", MyMethodContext.class).block();
Assert.assertEquals("myreminder", postContext.getName());
Assert.assertEquals(ActorCallType.REMINDER_METHOD.toString(), postContext.getType());
}
@Test(expected = IllegalArgumentException.class)
public void invokeReminderAfterDeactivate() throws Exception {
ActorProxy proxy = newActorProxy();
this.manager.deactivateActor(proxy.getActorId()).block();
byte[] params = createReminderParams("anything");
this.manager.invokeReminder(proxy.getActorId(), "myreminder", params).block();
}
@Test
public void classTypeRequestResponseInStateStore() {
ActorProxy proxy = newActorProxy();
MyMethodContext expectedContext = new MyMethodContext().setName("MyName").setType("MyType");
proxy.invokeActorMethod("setMethodContext", expectedContext).block();
MyMethodContext context = proxy.invokeActorMethod("getMethodContext", MyMethodContext.class).block();
Assert.assertEquals(expectedContext.getName(), context.getName());
Assert.assertEquals(expectedContext.getType(), context.getType());
}
@Test
public void intTypeRequestResponseInStateStore() {
ActorProxy proxy = newActorProxy();
Assert.assertEquals(1, (int)proxy.invokeActorMethod("incrementAndGetCount", 1, int.class).block());
Assert.assertEquals(6, (int)proxy.invokeActorMethod("incrementAndGetCount", 5, int.class).block());
}
@Test(expected = NumberFormatException.class)
public void intTypeWithMethodException() {
ActorProxy proxy = newActorProxy();
// Zero is a magic input that will make method throw an exception.
proxy.invokeActorMethod("incrementAndGetCount", 0, int.class).block();
}
@Test(expected = IllegalStateException.class)
public void intTypeWithRuntimeException() {
ActorProxy proxy = newActorProxy();
proxy.invokeActorMethod("getCountButThrowsException", int.class).block();
}
@Test(expected = IllegalStateException.class)
public void actorRuntimeException() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("forceDuplicateException").block();
}
@Test(expected = IllegalCharsetNameException.class)
public void actorMethodException() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
proxy.invokeActorMethod("throwsWithoutSaving").block();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test
public void rollbackChanges() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Runs a method that will add one message but fail because tries to add a second one.
proxy.invokeActorMethod("forceDuplicateException")
.onErrorResume(throwable -> Mono.empty())
.block();
// No message is set
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
}
@Test
public void partialChanges() {
ActorProxy proxy = newActorProxy();
Assert.assertFalse(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// Runs a method that will add one message, commit but fail because tries to add a second one.
proxy.invokeActorMethod("forcePartialChange")
.onErrorResume(throwable -> Mono.empty())
.block();
// Message is set.
Assert.assertTrue(proxy.invokeActorMethod("hasMessage", Boolean.class).block());
// It is first message and not the second due to a save() in the middle but an exception in the end.
Assert.assertEquals("first message",
proxy.invokeActorMethod("getMessage", String.class).block());
}
private ActorProxy newActorProxy() {
ActorId actorId = newActorId();
// Mock daprClient for ActorProxy only, not for runtime.
DaprClientStub daprClient = mock(DaprClientStub.class);
when(daprClient.invokeActorMethod(
eq(context.getActorTypeInformation().getName()),
eq(actorId.toString()),
any(),
any()))
.thenAnswer(invocationOnMock ->
this.manager.invokeMethod(
new ActorId(invocationOnMock.getArgument(1, String.class)),
invocationOnMock.getArgument(2, String.class),
INTERNAL_SERIALIZER.unwrapData(
invocationOnMock.getArgument(3, byte[].class)))
.map(s -> {
try {
return INTERNAL_SERIALIZER.wrapData(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
this.manager.activateActor(actorId).block();
return new ActorProxyForTestsImpl(
context.getActorTypeInformation().getName(),
actorId,
new DefaultObjectSerializer(),
daprClient);
}
private byte[] createReminderParams(String data) throws IOException {
byte[] serialized = this.context.getObjectSerializer().serialize(data);
ActorReminderParams params = new ActorReminderParams(serialized, Duration.ofSeconds(1), Duration.ofSeconds(1));
return INTERNAL_SERIALIZER.serialize(params);
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private static String executeSayMethod(String something) {
return "Said: " + (something == null ? "" : something);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
new DefaultObjectSerializer(),
new DefaultActorFactory<T>(),
ActorTypeInformation.create(MyActorImpl.class),
daprClient,
new DaprInMemoryStateProvider(new JavaSerializer())
);
}
}

View File

@ -0,0 +1,63 @@
package io.dapr.actors.runtime;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.time.Duration;
public class ActorTimerTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Test
public void serialize() throws IOException {
Duration dueTime = Duration.ZERO
.plusMinutes(7)
.plusSeconds(17);
Duration period = Duration.ZERO
.plusHours(1)
.plusSeconds(3);
ActorTimer timer = new ActorTimer(
null,
"testTimer",
"myfunction",
null,
dueTime,
period);
byte[] s = new ObjectSerializer().serialize(timer);
String expected = "{\"period\":\"1h0m3s0ms\",\"dueTime\":\"0h7m17s0ms\", \"callback\": \"myfunction\"}";
// Deep comparison via JsonNode.equals method.
Assert.assertEquals(OBJECT_MAPPER.readTree(expected), OBJECT_MAPPER.readTree(s));
}
@Test
public void serializeWithOneTimePeriod() throws IOException {
Duration dueTime = Duration.ZERO
.plusMinutes(7)
.plusSeconds(17);
// this is intentionally negative
Duration period = Duration.ZERO
.minusHours(1)
.minusMinutes(3);
ActorTimer timer = new ActorTimer(
null,
"testTimer",
"myfunction",
null,
dueTime,
period);
byte[] s = new ObjectSerializer().serialize(timer);
// A negative period will be serialized to an empty string which is interpreted by Dapr to mean fire once only.
String expected = "{\"period\":\"\",\"dueTime\":\"0h7m17s0ms\", \"callback\": \"myfunction\"}";
// Deep comparison via JsonNode.equals method.
Assert.assertEquals(OBJECT_MAPPER.readTree(expected), OBJECT_MAPPER.readTree(s));
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* Unit tests for ActorTypeInformation.
*/
public class ActorTypeInformationTest {
/**
* Actor interfaced used in this test only.
*/
private interface MyActor {
}
/**
* Checks information for a non-remindable actor.
*/
@Test
public void notRemindable() {
class A extends AbstractActor implements MyActor {
A() {
super(null, null);
}
}
ActorTypeInformation info = ActorTypeInformation.create(A.class);
Assert.assertNotNull(info);
Assert.assertEquals("A", info.getName());
Assert.assertEquals(A.class, info.getImplementationClass());
Assert.assertFalse(info.isAbstractClass());
Assert.assertFalse(info.isRemindable());
Assert.assertEquals(1, info.getInterfaces().size());
Assert.assertTrue(info.getInterfaces().contains(MyActor.class));
}
/**
* Checks information for a remindable actor.
*/
@Test
public void remindable() {
class A extends AbstractActor implements MyActor, Remindable {
A() {
super(null, null);
}
@Override
public Class getStateType() {
return null;
}
@Override
public Mono<Void> receiveReminder(String reminderName, Object state, Duration dueTime, Duration period) {
return null;
}
}
ActorTypeInformation info = ActorTypeInformation.create(A.class);
Assert.assertNotNull(info);
Assert.assertEquals("A", info.getName());
Assert.assertEquals(A.class, info.getImplementationClass());
Assert.assertFalse(info.isAbstractClass());
Assert.assertTrue(info.isRemindable());
Assert.assertEquals(2, info.getInterfaces().size());
Assert.assertTrue(info.getInterfaces().contains(Remindable.class));
Assert.assertTrue(info.getInterfaces().contains(MyActor.class));
}
/**
* Checks information for an actor renamed via annotation.
*/
@Test
public void renamedWithAnnotation() {
@ActorType(name = "B")
class A extends AbstractActor implements MyActor {
A() {
super(null, null);
}
}
ActorTypeInformation info = ActorTypeInformation.create(A.class);
Assert.assertNotNull(info);
Assert.assertEquals("B", info.getName());
Assert.assertEquals(A.class, info.getImplementationClass());
Assert.assertFalse(info.isAbstractClass());
Assert.assertFalse(info.isRemindable());
Assert.assertEquals(1, info.getInterfaces().size());
Assert.assertTrue(info.getInterfaces().contains(MyActor.class));
}
/**
* Checks information for an actor is invalid due to an non-actor parent.
*/
@Test
public void nonActorParentClass() {
abstract class MyAbstractClass implements MyActor {
}
class A extends MyAbstractClass {
}
ActorTypeInformation info = ActorTypeInformation.tryCreate(A.class);
Assert.assertNull(info);
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.client.DaprHttp;
import io.dapr.client.DaprHttpProxy;
import okhttp3.OkHttpClient;
import okhttp3.mock.Behavior;
import okhttp3.mock.MockInterceptor;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class DaprHttpClientTest {
private DaprHttpClient DaprHttpClient;
private OkHttpClient okHttpClient;
private MockInterceptor mockInterceptor;
private final String EXPECTED_RESULT = "{\"data\":\"ewoJCSJwcm9wZXJ0eUEiOiAidmFsdWVBIiwKCQkicHJvcGVydHlCIjogInZhbHVlQiIKCX0=\"}";
@Before
public void setUp() throws Exception {
mockInterceptor = new MockInterceptor(Behavior.UNORDERED);
okHttpClient = new OkHttpClient.Builder().addInterceptor(mockInterceptor).build();
}
@Test
public void getActorState() {
mockInterceptor.addRule()
.get("http://localhost:3000/v1.0/actors/DemoActor/1/state/order")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<byte[]> mono = DaprHttpClient.getActorState("DemoActor", "1", "order");
assertEquals(new String(mono.block()), EXPECTED_RESULT);
}
@Test
public void saveActorStateTransactionally() {
mockInterceptor.addRule()
.put("http://localhost:3000/v1.0/actors/DemoActor/1/state")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<Void> mono =
DaprHttpClient.saveActorStateTransactionally("DemoActor", "1", "".getBytes());
assertNull(mono.block());
}
@Test
public void registerActorReminder() {
mockInterceptor.addRule()
.put("http://localhost:3000/v1.0/actors/DemoActor/1/reminders/reminder")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<Void> mono =
DaprHttpClient.registerActorReminder("DemoActor", "1", "reminder", "".getBytes());
assertNull(mono.block());
}
@Test
public void unregisterActorReminder() {
mockInterceptor.addRule()
.delete("http://localhost:3000/v1.0/actors/DemoActor/1/reminders/reminder")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<Void> mono = DaprHttpClient.unregisterActorReminder("DemoActor", "1", "reminder");
assertNull(mono.block());
}
@Test
public void registerActorTimer() {
mockInterceptor.addRule()
.put("http://localhost:3000/v1.0/actors/DemoActor/1/timers/timer")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<Void> mono =
DaprHttpClient.registerActorTimer("DemoActor", "1", "timer", "".getBytes());
assertNull(mono.block());
}
@Test
public void unregisterActorTimer() {
mockInterceptor.addRule()
.delete("http://localhost:3000/v1.0/actors/DemoActor/1/timers/timer")
.respond(EXPECTED_RESULT);
DaprHttp daprHttp = new DaprHttpProxy(3000, okHttpClient);
DaprHttpClient = new DaprHttpClient(daprHttp);
Mono<Void> mono = DaprHttpClient.unregisterActorTimer("DemoActor", "1", "timer");
assertNull(mono.block());
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DaprObjectSerializer;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Fake state provider for tests in Actors - data is kept in memory only.
*/
public class DaprInMemoryStateProvider extends DaprStateAsyncProvider {
private static final Map<String, byte[]> stateStore = new HashMap<>();
private final DaprObjectSerializer serializer;
DaprInMemoryStateProvider(DaprObjectSerializer serializer) {
super(null, serializer /* just to avoid NPE */);
this.serializer = serializer;
}
@Override
<T> Mono<T> load(String actorType, ActorId actorId, String stateName, Class<T> clazz) {
return Mono.fromSupplier(() -> {
try {
String stateId = this.buildId(actorType, actorId, stateName);
if (!stateStore.containsKey(stateId)) {
throw new IllegalStateException("State not found.");
}
return this.serializer.deserialize(this.stateStore.get(stateId), clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
@Override
Mono<Boolean> contains(String actorType, ActorId actorId, String stateName) {
return Mono.fromSupplier(() -> stateStore.containsKey(this.buildId(actorType, actorId, stateName)));
}
@Override
Mono<Void> apply(String actorType, ActorId actorId, ActorStateChange... stateChanges) {
return Mono.fromRunnable(() -> {
try {
for (ActorStateChange stateChange : stateChanges) {
String stateId = buildId(actorType, actorId, stateChange.getStateName());
switch (stateChange.getChangeKind()) {
case REMOVE:
stateStore.remove(stateId);
break;
case ADD:
case UPDATE:
byte[] raw = this.serializer.serialize(stateChange.getValue());
stateStore.put(stateId, raw);
break;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private static final String buildId(String actorType, ActorId actorId, String stateName) {
return String.format("%s||%s||%s", actorType, actorId.toString(), stateName);
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests for the state store facade.
*/
public class DaprStateAsyncProviderTest {
private static final DaprObjectSerializer SERIALIZER = new DefaultObjectSerializer();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final double EPSILON = 1e-10;
/**
* Class used to test JSON serialization.
*/
public static final class Customer {
private int id;
private String name;
public int getId() {
return id;
}
public Customer setId(int id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public Customer setName(String name) {
this.name = name;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id == customer.id &&
Objects.equals(name, customer.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
@Test
public void happyCaseApply() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient
.saveActorStateTransactionally(
eq("MyActor"),
eq("123"),
argThat(s -> {
try {
JsonNode node = OBJECT_MAPPER.readTree(s);
if (node == null) {
return false;
}
if (node.size() != 3) {
return false;
}
boolean foundInsertName = false;
boolean foundUpdateZipcode = false;
boolean foundDeleteFlag = false;
for (JsonNode operation : node) {
if (operation.get("operation") == null) {
return false;
}
if (operation.get("request") == null) {
return false;
}
String opName = operation.get("operation").asText();
String key = operation.get("request").get("key").asText();
JsonNode valueNode = operation.get("request").get("value");
byte[] value = (valueNode == null) ? null : valueNode.textValue().getBytes();
foundInsertName |= "upsert".equals(opName) &&
"name".equals(key) &&
Arrays.equals(SERIALIZER.serialize("Jon Doe"), value);
foundUpdateZipcode |= "upsert".equals(opName) &&
"zipcode".equals(key) &&
Arrays.equals(SERIALIZER.serialize(98011), value);
foundDeleteFlag |= "delete".equals(opName) &&
"flag".equals(key) &&
(value == null);
}
return foundInsertName && foundUpdateZipcode && foundDeleteFlag;
} catch (IOException e) {
e.printStackTrace();
return false;
}
})))
.thenReturn(Mono.empty());
DaprStateAsyncProvider provider = new DaprStateAsyncProvider(daprClient, SERIALIZER);
provider.apply("MyActor",
new ActorId("123"),
createInsertChange("name", "Jon Doe"),
createUpdateChange("zipcode", 98011),
createDeleteChange("flag"))
.block();
verify(daprClient).saveActorStateTransactionally(eq("MyActor"), eq("123"), any());
}
@Test
public void happyCaseLoad() throws Exception {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient
.getActorState(any(), any(), eq("name")))
.thenReturn(Mono.just(SERIALIZER.serialize("Jon Doe")));
when(daprClient
.getActorState(any(), any(), eq("zipcode")))
.thenReturn(Mono.just(SERIALIZER.serialize(98021)));
when(daprClient
.getActorState(any(), any(), eq("goals")))
.thenReturn(Mono.just(SERIALIZER.serialize(98)));
when(daprClient
.getActorState(any(), any(), eq("balance")))
.thenReturn(Mono.just(SERIALIZER.serialize(46.55)));
when(daprClient
.getActorState(any(), any(), eq("active")))
.thenReturn(Mono.just(SERIALIZER.serialize(true)));
when(daprClient
.getActorState(any(), any(), eq("customer")))
.thenReturn(Mono.just("{ \"id\": 1000, \"name\": \"Roxane\"}".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("anotherCustomer")))
.thenReturn(Mono.just("{ \"id\": 2000, \"name\": \"Max\"}".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("nullCustomer")))
.thenReturn(Mono.empty());
DaprStateAsyncProvider provider = new DaprStateAsyncProvider(daprClient, SERIALIZER);
Assert.assertEquals("Jon Doe",
provider.load("MyActor", new ActorId("123"), "name", String.class).block());
Assert.assertEquals(98021,
(int)provider.load("MyActor", new ActorId("123"), "zipcode", int.class).block());
Assert.assertEquals(98,
(int) provider.load("MyActor", new ActorId("123"), "goals", int.class).block());
Assert.assertEquals(98,
(int) provider.load("MyActor", new ActorId("123"), "goals", int.class).block());
Assert.assertEquals(46.55,
(double) provider.load("MyActor", new ActorId("123"), "balance", double.class).block(),
EPSILON);
Assert.assertEquals(true,
(boolean) provider.load("MyActor", new ActorId("123"), "active", boolean.class).block());
Assert.assertEquals(new Customer().setId(1000).setName("Roxane"),
provider.load("MyActor", new ActorId("123"), "customer", Customer.class).block());
Assert.assertNotEquals(new Customer().setId(1000).setName("Roxane"),
provider.load("MyActor", new ActorId("123"), "anotherCustomer", Customer.class).block());
Assert.assertNull(provider.load("MyActor", new ActorId("123"), "nullCustomer", Customer.class).block());
}
@Test
public void happyCaseContains() {
DaprClient daprClient = mock(DaprClient.class);
// Keys that exists.
when(daprClient
.getActorState(any(), any(), eq("name")))
.thenReturn(Mono.just("Jon Doe".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("zipcode")))
.thenReturn(Mono.just("98021".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("goals")))
.thenReturn(Mono.just("98".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("balance")))
.thenReturn(Mono.just("46.55".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("active")))
.thenReturn(Mono.just("true".getBytes()));
when(daprClient
.getActorState(any(), any(), eq("customer")))
.thenReturn(Mono.just("{ \"id\": \"3000\", \"name\": \"Ely\" }".getBytes()));
// Keys that do not exist.
when(daprClient
.getActorState(any(), any(), eq("Does not exist")))
.thenReturn(Mono.empty());
when(daprClient
.getActorState(any(), any(), eq("NAME")))
.thenReturn(Mono.empty());
when(daprClient
.getActorState(any(), any(), eq(null)))
.thenReturn(Mono.empty());
DaprStateAsyncProvider provider = new DaprStateAsyncProvider(daprClient, SERIALIZER);
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "name").block());
Assert.assertFalse(provider.contains("MyActor", new ActorId("123"), "NAME").block());
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "zipcode").block());
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "goals").block());
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "balance").block());
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "active").block());
Assert.assertTrue(provider.contains("MyActor", new ActorId("123"), "customer").block());
Assert.assertFalse(provider.contains("MyActor", new ActorId("123"), "Does not exist").block());
Assert.assertFalse(provider.contains("MyActor", new ActorId("123"), null).block());
}
private final <T> ActorStateChange createInsertChange(String name, T value) {
return new ActorStateChange(name, value, ActorStateChangeKind.ADD);
}
private final <T> ActorStateChange createUpdateChange(String name, T value) {
return new ActorStateChange(name, value, ActorStateChangeKind.UPDATE);
}
private final ActorStateChange createDeleteChange(String name) {
return new ActorStateChange(name, null, ActorStateChangeKind.REMOVE);
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.serializer.DaprObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.mock;
/**
* Testing the default constructor of an Actor.
*/
public class DefaultActorFactoryTest {
/**
* A compliant implementation of Actor to be used in the tests below.
*/
static class MyActor extends AbstractActor {
ActorRuntimeContext<MyActor> context;
ActorId actorId;
public MyActor(ActorRuntimeContext<MyActor> context, ActorId actorId) {
super(context, actorId);
this.context = context;
this.actorId = actorId;
}
}
/**
* A non-compliant implementation of Actor to be used in the tests below.
*/
static class InvalidActor extends AbstractActor {
InvalidActor() {
super(null, null);
}
}
/**
* Happy case.
*/
@Test
public void happyActor() {
DefaultActorFactory<MyActor> factory = new DefaultActorFactory<>();
ActorId actorId = ActorId.createRandom();
MyActor actor = factory.createActor(createActorRuntimeContext(MyActor.class), actorId);
Assert.assertEquals(actorId, actor.actorId);
Assert.assertNotNull(actor.context);
}
/**
* Class is not an actor.
*/
@Test
public void noValidConstructor() {
DefaultActorFactory<InvalidActor> factory = new DefaultActorFactory<>();
ActorId actorId = ActorId.createRandom();
InvalidActor actor = factory.createActor(createActorRuntimeContext(InvalidActor.class), actorId);
Assert.assertNull(actor);
}
private static <T extends AbstractActor> ActorRuntimeContext<T> createActorRuntimeContext(Class<T> clazz) {
return new ActorRuntimeContext(
mock(ActorRuntime.class),
mock(DaprObjectSerializer.class),
mock(ActorFactory.class),
ActorTypeInformation.create(clazz),
mock(DaprClient.class),
mock(DaprStateAsyncProvider.class));
}
}

View File

@ -0,0 +1,374 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyForTestsImpl;
import io.dapr.actors.client.DaprClientStub;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DerivedActorTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
private final ActorRuntimeContext context = createContext();
private ActorManager<ActorChild> manager = new ActorManager<>(context);
public interface MyActor {
// These 4 will be implemented in the user code class that extends AbstractActor, but it
// will not be implemented in another class that will inherit that.
Mono<String> onlyImplementedInParentStringInStringOut(String input);
Mono<Boolean> onlyImplementedInParentStringInBooleanOut(String input);
Mono<Void> onlyImplementedInParentStringInVoidOut(String input);
Mono<MyData> onlyImplementedInParentClassInClassOut(MyData input);
// used to validate onlyImplementedInParentStringInVoidOut() was called
boolean methodReturningVoidInvoked();
// The test will only call the versions of this in a derived class to the user code base class.
// The user code base class version will throw.
Mono<String> stringInStringOut(String input);
Mono<Boolean> stringInBooleanOut(String input);
Mono<Void> stringInVoidOut(String input);
Mono<Void> stringInVoidOutIntentionallyThrows(String input);
Mono<MyData> classInClassOut(MyData input);
}
@ActorType(name = "MyActor")
public static class ActorParent extends AbstractActor implements MyActor {
private final ActorId id;
private boolean activated;
private boolean methodReturningVoidInvoked;
public ActorParent(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
this.methodReturningVoidInvoked = false;
}
@Override
public Mono<String> onlyImplementedInParentStringInStringOut(String input) {
return Mono.fromSupplier(() -> {
return input + input + input;
});
}
@Override
public Mono<Boolean> onlyImplementedInParentStringInBooleanOut(String input) {
return Mono.fromSupplier(() -> {
if (input.equals("icecream")) {
return true;
} else {
return false;
}
});
}
@Override
public Mono<Void> onlyImplementedInParentStringInVoidOut(String input) {
return Mono.fromRunnable(() -> {
this.methodReturningVoidInvoked = true;
System.out.println("Received " + input);
});
}
@Override
public Mono<MyData> onlyImplementedInParentClassInClassOut(MyData input) {
return Mono.fromSupplier(() -> {
return new MyData(
input.getName() + input.getName() + input.getName(),
input.getNum() + input.getNum() + input.getNum());
});
}
@Override
public boolean methodReturningVoidInvoked() {
return this.methodReturningVoidInvoked;
}
@Override
public Mono<String> stringInStringOut(String s) {
return Mono.fromSupplier(() -> {
// In the cases below we intentionally only call the derived version of this.
// ArithmeticException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new ArithmeticException("This method should not have been called");
}
);
}
@Override
public Mono<Boolean> stringInBooleanOut(String s) {
return Mono.fromSupplier(() -> {
// In the cases below we intentionally only call the derived version of this.
// ArithmeticException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new ArithmeticException("This method should not have been called");
});
}
@Override
public Mono<Void> stringInVoidOut(String input) {
return Mono.fromRunnable(() -> {
this.methodReturningVoidInvoked = true;
System.out.println("Received " + input);
});
}
@Override
public Mono<Void> stringInVoidOutIntentionallyThrows(String input) {
return Mono.fromRunnable(() -> {
// IllegalMonitorStateException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new IllegalMonitorStateException("IntentionalException");
});
}
@Override
public Mono<MyData> classInClassOut(MyData input) {
return Mono.fromSupplier(() -> {
// In the cases below we intentionally only call the derived version of this.
// ArithmeticException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new ArithmeticException("This method should not have been called");
});
}
}
public static class ActorChild extends ActorParent implements MyActor {
private final ActorId id;
private boolean activated;
public ActorChild(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
}
@Override
public Mono<String> stringInStringOut(String s) {
return Mono.fromSupplier(() -> {
return s + s;
}
);
}
@Override
public Mono<Boolean> stringInBooleanOut(String s) {
return Mono.fromSupplier(() -> {
if (s.equals("true")) {
return true;
} else {
return false;
}
});
}
@Override
public Mono<MyData> classInClassOut(MyData input) {
return Mono.fromSupplier(() -> {
return new MyData(
input.getName() + input.getName(),
input.getNum() + input.getNum());
});
}
}
static class MyData {
private String name;
private int num;
public MyData() {
this.name = "";
this.num = 0;
}
public MyData(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return this.name;
}
public int getNum() {
return this.num;
}
}
@Test
public void stringInStringOut() {
ActorProxy proxy = createActorProxyForActorChild();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
"abcabc",
proxy.invokeActorMethod("stringInStringOut", "abc", String.class).block());
}
@Test
public void stringInBooleanOut() {
ActorProxy proxy = createActorProxyForActorChild();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
false,
proxy.invokeActorMethod("stringInBooleanOut", "hello world", Boolean.class).block());
Assert.assertEquals(
true,
proxy.invokeActorMethod("stringInBooleanOut", "true", Boolean.class).block());
}
@Test
public void stringInVoidOut() {
ActorProxy actorProxy = createActorProxyForActorChild();
// stringInVoidOut() has not been invoked so this is false
Assert.assertEquals(
false,
actorProxy.invokeActorMethod("methodReturningVoidInvoked", Boolean.class).block());
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
actorProxy.invokeActorMethod("stringInVoidOut", "hello world").block();
Assert.assertEquals(
true,
actorProxy.invokeActorMethod("methodReturningVoidInvoked", Boolean.class).block());
}
@Test(expected = IllegalMonitorStateException.class)
public void stringInVoidOutIntentionallyThrows() {
ActorProxy actorProxy = createActorProxyForActorChild();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
actorProxy.invokeActorMethod("stringInVoidOutIntentionallyThrows", "hello world").block();
}
@Test
public void classInClassOut() {
ActorProxy actorProxy = createActorProxyForActorChild();
MyData d = new MyData("hi", 3);
// this should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
MyData response = actorProxy.invokeActorMethod("classInClassOut", d, MyData.class).block();
Assert.assertEquals(
"hihi",
response.getName());
Assert.assertEquals(
6,
response.getNum());
}
// The actor methods this test invokes are all implemented in ActorParent only. We're asserting it's callable when the actor proxy is for an ActorChild.
@Test
public void testInheritedActorMethods() {
ActorProxy actorProxy = createActorProxyForActorChild();
Assert.assertEquals(
"www",
actorProxy.invokeActorMethod("onlyImplementedInParentStringInStringOut", "w", String.class).block());
Assert.assertEquals(
true,
actorProxy.invokeActorMethod("onlyImplementedInParentStringInBooleanOut", "icecream", Boolean.class).block());
// onlyImplementedInParentStringInVoidOut() has not been invoked so this is false
Assert.assertEquals(
false,
actorProxy.invokeActorMethod("methodReturningVoidInvoked", Boolean.class).block());
actorProxy.invokeActorMethod("onlyImplementedInParentStringInVoidOut", "icecream", Boolean.class).block();
// now it should return true.
Assert.assertEquals(
true,
actorProxy.invokeActorMethod("methodReturningVoidInvoked", Boolean.class).block());
MyData d = new MyData("hi", 3);
MyData response = actorProxy.invokeActorMethod("onlyImplementedInParentClassInClassOut", d, MyData.class).block();
Assert.assertEquals(
"hihihi",
response.getName());
Assert.assertEquals(
9,
response.getNum());
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private ActorProxy createActorProxyForActorChild() {
ActorId actorId = newActorId();
// Mock daprClient for ActorProxy only, not for runtime.
DaprClientStub daprClient = mock(DaprClientStub.class);
when(daprClient.invokeActorMethod(
eq(context.getActorTypeInformation().getName()),
eq(actorId.toString()),
any(),
any()))
.thenAnswer(invocationOnMock ->
this.manager.invokeMethod(
new ActorId(invocationOnMock.getArgument(1, String.class)),
invocationOnMock.getArgument(2, String.class),
INTERNAL_SERIALIZER.unwrapData(
invocationOnMock.getArgument(3, byte[].class)))
.map(s -> {
try {
return INTERNAL_SERIALIZER.wrapData(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
this.manager.activateActor(actorId).block();
return new ActorProxyForTestsImpl(
context.getActorTypeInformation().getName(),
actorId,
new DefaultObjectSerializer(),
daprClient);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
new DefaultObjectSerializer(),
new DefaultActorFactory<T>(),
ActorTypeInformation.create(ActorChild.class),
daprClient,
mock(DaprStateAsyncProvider.class)
);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.serializer.DaprObjectSerializer;
import java.io.*;
/**
* Class used to test different serializer implementations.
*/
public class JavaSerializer implements DaprObjectSerializer {
/**
* {@inheritDoc}
*/
@Override
public byte[] serialize(Object o) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(o);
oos.flush();
return bos.toByteArray();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public <T> T deserialize(byte[] data, Class<T> clazz) throws IOException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) {
try (ObjectInputStream ois = new ObjectInputStream(bis)) {
try {
return (T) ois.readObject();
} catch (Exception e) {
throw new IOException("Could not deserialize Java object.", e);
}
}
}
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.actors.runtime;
import io.dapr.actors.ActorId;
import io.dapr.actors.client.ActorProxy;
import io.dapr.actors.client.ActorProxyForTestsImpl;
import io.dapr.actors.client.DaprClientStub;
import io.dapr.serializer.DefaultObjectSerializer;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ThrowFromPreAndPostActorMethodsTest {
private static final ObjectSerializer INTERNAL_SERIALIZER = new ObjectSerializer();
private static final AtomicInteger ACTOR_ID_COUNT = new AtomicInteger();
private final ActorRuntimeContext context = createContext();
private ActorManager<ActorChild> manager = new ActorManager<>(context);
public interface MyActor {
Mono<Boolean> stringInBooleanOut(String input);
}
@ActorType(name = "MyActor")
public static class ActorParent extends AbstractActor implements MyActor {
private final ActorId id;
private boolean activated;
private boolean methodReturningVoidInvoked;
public ActorParent(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
this.methodReturningVoidInvoked = false;
}
@Override
public Mono<Void> onPreActorMethodInternal(ActorMethodContext actorMethodContext) {
// IllegalMonitorStateException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new IllegalMonitorStateException("Intentional throw from onPreActorMethodInternal");
}
@Override
public Mono<Boolean> stringInBooleanOut(String s) {
return Mono.fromSupplier(() -> {
// In the cases below we intentionally only call the derived version of this.
// ArithmeticException is being thrown only because it's un unusual exception so it's unlikely
// to collide with something else.
throw new ArithmeticException("This method should not have been called");
});
}
}
public static class ActorChild extends ActorParent implements MyActor {
private final ActorId id;
private boolean activated;
public ActorChild(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
this.id = id;
this.activated = true;
}
@Override
public Mono<Boolean> stringInBooleanOut(String s) {
return Mono.fromSupplier(() -> {
if (s.equals("true")) {
return true;
} else {
return false;
}
});
}
}
static class MyData {
private String name;
private int num;
public MyData() {
this.name = "";
this.num = 0;
}
public MyData(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return this.name;
}
public int getNum() {
return this.num;
}
}
// IllegalMonitorStateException should be intentionally thrown. This type was chosen for this test just because
// it is unlikely to collide.
@Test(expected = IllegalMonitorStateException.class)
public void stringInBooleanOut1() {
ActorProxy proxy = createActorProxyForActorChild();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
false,
proxy.invokeActorMethod("stringInBooleanOut", "hello world", Boolean.class).block());
}
// IllegalMonitorStateException should be intentionally thrown. This type was chosen for this test just because
// it is unlikely to collide.
@Test(expected = IllegalMonitorStateException.class)
public void stringInBooleanOut2() {
ActorProxy proxy = createActorProxyForActorChild();
// these should only call the actor methods for ActorChild. The implementations in ActorParent will throw.
Assert.assertEquals(
true,
proxy.invokeActorMethod("stringInBooleanOut", "true", Boolean.class).block());
}
private static ActorId newActorId() {
return new ActorId(Integer.toString(ACTOR_ID_COUNT.incrementAndGet()));
}
private ActorProxy createActorProxyForActorChild() {
ActorId actorId = newActorId();
// Mock daprClient for ActorProxy only, not for runtime.
DaprClientStub daprClient = mock(DaprClientStub.class);
when(daprClient.invokeActorMethod(
eq(context.getActorTypeInformation().getName()),
eq(actorId.toString()),
any(),
any()))
.thenAnswer(invocationOnMock ->
this.manager.invokeMethod(
new ActorId(invocationOnMock.getArgument(1, String.class)),
invocationOnMock.getArgument(2, String.class),
INTERNAL_SERIALIZER.unwrapData(
invocationOnMock.getArgument(3, byte[].class)))
.map(s -> {
try {
return INTERNAL_SERIALIZER.wrapData(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
this.manager.activateActor(actorId).block();
return new ActorProxyForTestsImpl(
context.getActorTypeInformation().getName(),
actorId,
new DefaultObjectSerializer(),
daprClient);
}
private static <T extends AbstractActor> ActorRuntimeContext createContext() {
DaprClient daprClient = mock(DaprClient.class);
when(daprClient.registerActorTimer(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.registerActorReminder(any(), any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorTimer(any(), any(), any())).thenReturn(Mono.empty());
when(daprClient.unregisterActorReminder(any(), any(), any())).thenReturn(Mono.empty());
return new ActorRuntimeContext(
mock(ActorRuntime.class),
new DefaultObjectSerializer(),
new DefaultActorFactory<T>(),
ActorTypeInformation.create(ActorChild.class),
daprClient,
mock(DaprStateAsyncProvider.class)
);
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.client;
import okhttp3.OkHttpClient;
public class DaprHttpProxy extends io.dapr.client.DaprHttp {
public DaprHttpProxy(int port, OkHttpClient httpClient) {
super(port, httpClient);
}
}

Some files were not shown because too many files have changed in this diff Show More