mirror of https://github.com/dapr/rust-sdk.git
Add example for Input and output Bindings (#57)
* Add example for Input and output Bindings * fix: add example tag * chore: lint and fix Signed-off-by: mikeee <hey@mike.ee> * feat: implement validation * chore: lint Signed-off-by: mikeee <hey@mike.ee> * chore: bump timeout Signed-off-by: mikeee <hey@mike.ee> * chore: ignore return code for kafka setup Signed-off-by: mikeee <hey@mike.ee> * chore: fix kafka step Signed-off-by: mikeee <hey@mike.ee> --------- Signed-off-by: Mike Nguyen <hey@mike.ee> Signed-off-by: mikeee <hey@mike.ee> Co-authored-by: Mike Nguyen <hey@mike.ee>
This commit is contained in:
parent
ea644546fb
commit
5764c6cf3d
|
@ -144,7 +144,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
examples:
|
examples:
|
||||||
[ "actors", "client", "configuration", "crypto", "invoke/grpc", "invoke/grpc-proxying", "pubsub", "query_state", "secrets-bulk" ]
|
[ "actors", "bindings", "client", "configuration", "crypto", "invoke/grpc", "invoke/grpc-proxying", "pubsub", "query_state", "secrets-bulk" ]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
|
@ -82,6 +82,14 @@ path = "examples/pubsub/publisher.rs"
|
||||||
name = "subscriber"
|
name = "subscriber"
|
||||||
path = "examples/pubsub/subscriber.rs"
|
path = "examples/pubsub/subscriber.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "output-bindings"
|
||||||
|
path = "examples/bindings/output.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "input-bindings"
|
||||||
|
path = "examples/bindings/input.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "query_state_q1"
|
name = "query_state_q1"
|
||||||
path = "examples/query_state/query1.rs"
|
path = "examples/query_state/query1.rs"
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Input and Output Bindings Example
|
||||||
|
|
||||||
|
This is a simple example that demonstrates Dapr's binding capabilities. To implement input bindings in your rust application, you need to implement `AppCallback` server for subscribing to events. Specifically, the following two methods need to be implemented for input bindings to work:
|
||||||
|
|
||||||
|
1. `list_input_bindings` - Dapr runtime calls this method to get list of bindings the application is subscribed to.
|
||||||
|
2. `on_binding_event` - Defines how the application handles the input binding event.
|
||||||
|
|
||||||
|
> **Note:** Make sure to use latest version of proto bindings.
|
||||||
|
|
||||||
|
In order to have both examples working with the same binding configuration ServiceBus was used here. If you don't have it available you can change to a binding that works for both Input and Output from [this list](https://docs.dapr.io/reference/components-reference/supported-bindings/)
|
||||||
|
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
To run this example:
|
||||||
|
|
||||||
|
1. Run a kafka container
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Run kafka instance
|
||||||
|
background: true
|
||||||
|
sleep: 60
|
||||||
|
timeout_seconds: 120
|
||||||
|
expected_return_code:
|
||||||
|
expected_stderr_lines:
|
||||||
|
-->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 9092:9092 apache/kafka:3.7.1
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
2. Run the multi-app run template (`dapr.yaml`)
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Run Multi-app Run
|
||||||
|
output_match_mode: substring
|
||||||
|
match_order: sequential
|
||||||
|
expected_stdout_lines:
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 0 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 1 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 2 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 3 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 4 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 5 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 6 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 7 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 8 => hello from rust!'
|
||||||
|
- '== APP - rust-input-b == Binding Name: binding-example'
|
||||||
|
- '== APP - rust-input-b == Message: 9 => hello from rust!'
|
||||||
|
background: true
|
||||||
|
sleep: 30
|
||||||
|
timeout_seconds: 90
|
||||||
|
-->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dapr run -f .
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: binding-example
|
||||||
|
spec:
|
||||||
|
type: bindings.kafka
|
||||||
|
metadata:
|
||||||
|
- name: direction
|
||||||
|
value: "input, output"
|
||||||
|
# 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: authType
|
||||||
|
value: "none"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: 1
|
||||||
|
common:
|
||||||
|
resourcesPath: ./components/
|
||||||
|
daprdLogDestination: console
|
||||||
|
apps:
|
||||||
|
- appID: rust-input-b
|
||||||
|
appDirPath: ./
|
||||||
|
appProtocol: grpc
|
||||||
|
appPort: 50051
|
||||||
|
logLevel: debug
|
||||||
|
command: ["cargo", "run", "--example", "input-bindings"]
|
||||||
|
- appID: rust-output-b
|
||||||
|
appDirPath: ./
|
||||||
|
appProtocol: grpc
|
||||||
|
logLevel: debug
|
||||||
|
command: ["cargo", "run", "--example", "output-bindings"]
|
|
@ -0,0 +1,87 @@
|
||||||
|
use tonic::{transport::Server, Request, Response, Status};
|
||||||
|
|
||||||
|
use dapr::dapr::dapr::proto::common::v1::{InvokeRequest, InvokeResponse};
|
||||||
|
use dapr::dapr::dapr::proto::runtime::v1::app_callback_server::{AppCallback, AppCallbackServer};
|
||||||
|
use dapr::dapr::dapr::proto::runtime::v1::{
|
||||||
|
BindingEventRequest, BindingEventResponse, ListInputBindingsResponse,
|
||||||
|
ListTopicSubscriptionsResponse, TopicEventRequest, TopicEventResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AppCallbackService {}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl AppCallback for AppCallbackService {
|
||||||
|
/// Invokes service method with InvokeRequest.
|
||||||
|
async fn on_invoke(
|
||||||
|
&self,
|
||||||
|
_request: Request<InvokeRequest>,
|
||||||
|
) -> Result<Response<InvokeResponse>, Status> {
|
||||||
|
Ok(Response::new(InvokeResponse::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all topics subscribed by this app.
|
||||||
|
async fn list_topic_subscriptions(
|
||||||
|
&self,
|
||||||
|
_request: Request<()>,
|
||||||
|
) -> Result<Response<ListTopicSubscriptionsResponse>, Status> {
|
||||||
|
Ok(Response::new(ListTopicSubscriptionsResponse::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes events from Pubsub.
|
||||||
|
async fn on_topic_event(
|
||||||
|
&self,
|
||||||
|
_request: Request<TopicEventRequest>,
|
||||||
|
) -> Result<Response<TopicEventResponse>, Status> {
|
||||||
|
Ok(Response::new(TopicEventResponse::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all input bindings subscribed by this app.
|
||||||
|
/// NOTE: Dapr runtime will call this method to get
|
||||||
|
/// the list of bindings the app wants to subscribe to.
|
||||||
|
/// In this example, the app is subscribing to a local pubsub binding named "binding-example"
|
||||||
|
|
||||||
|
async fn list_input_bindings(
|
||||||
|
&self,
|
||||||
|
_request: Request<()>,
|
||||||
|
) -> Result<Response<ListInputBindingsResponse>, Status> {
|
||||||
|
let list_bindings = ListInputBindingsResponse {
|
||||||
|
bindings: vec![String::from("binding-example")],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::new(list_bindings))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listens events from the input bindings.
|
||||||
|
async fn on_binding_event(
|
||||||
|
&self,
|
||||||
|
request: Request<BindingEventRequest>,
|
||||||
|
) -> Result<Response<BindingEventResponse>, Status> {
|
||||||
|
let r = request.into_inner();
|
||||||
|
let name = &r.name;
|
||||||
|
let data = &r.data;
|
||||||
|
|
||||||
|
let message = String::from_utf8_lossy(&data);
|
||||||
|
println!("Binding Name: {}", &name);
|
||||||
|
println!("Message: {}", &message);
|
||||||
|
|
||||||
|
Ok(Response::new(BindingEventResponse::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let addr = "[::]:50051".parse().unwrap();
|
||||||
|
|
||||||
|
let callback_service = AppCallbackService::default();
|
||||||
|
|
||||||
|
println!("AppCallback server listening on: {}", addr);
|
||||||
|
|
||||||
|
// Create a gRPC server with the callback_service.
|
||||||
|
Server::builder()
|
||||||
|
.add_service(AppCallbackServer::new(callback_service))
|
||||||
|
.serve(addr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
use std::{collections::HashMap, thread, time::Duration};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// TODO: Handle this issue in the sdk
|
||||||
|
// Introduce delay so that dapr grpc port is assigned before app tries to connect
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Get the Dapr port and create a connection
|
||||||
|
let addr = "https://127.0.0.1".to_string();
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
let mut client = dapr::Client::<dapr::client::TonicClient>::connect(addr).await?;
|
||||||
|
|
||||||
|
// name of the component
|
||||||
|
let binding_name = "binding-example";
|
||||||
|
|
||||||
|
for count in 0..10 {
|
||||||
|
// message metadata
|
||||||
|
let mut metadata = HashMap::<String, String>::new();
|
||||||
|
metadata.insert("count".to_string(), count.to_string());
|
||||||
|
|
||||||
|
// message
|
||||||
|
let message = format!("{} => hello from rust!", &count).into_bytes();
|
||||||
|
|
||||||
|
client
|
||||||
|
.invoke_binding(binding_name, message, "create", Some(metadata))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// sleep for 500ms to simulate delay b/w two events
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -66,6 +66,14 @@ impl TopicSubscription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ListInputBindingsResponse {
|
||||||
|
pub fn binding(binding_name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
bindings: vec![binding_name],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppCallbackService {
|
pub struct AppCallbackService {
|
||||||
handlers: Vec<Handler>,
|
handlers: Vec<Handler>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,15 +70,23 @@ impl<T: DaprInterface> Client<T> {
|
||||||
&mut self,
|
&mut self,
|
||||||
name: S,
|
name: S,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
|
operation: S,
|
||||||
|
metadata: Option<HashMap<String, String>>,
|
||||||
) -> Result<InvokeBindingResponse, Error>
|
) -> Result<InvokeBindingResponse, Error>
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
{
|
{
|
||||||
|
let mut mdata = HashMap::<String, String>::new();
|
||||||
|
if let Some(m) = metadata {
|
||||||
|
mdata = m;
|
||||||
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.invoke_binding(InvokeBindingRequest {
|
.invoke_binding(InvokeBindingRequest {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
data,
|
data,
|
||||||
..Default::default()
|
operation: operation.into(),
|
||||||
|
metadata: mdata,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,7 @@ impl ActorTypeRegistration {
|
||||||
pub fn register_method<T>(
|
pub fn register_method<T>(
|
||||||
mut self,
|
mut self,
|
||||||
method_name: &str,
|
method_name: &str,
|
||||||
handler: impl Handler<T, ActorState> + Send + Sync,
|
handler: impl Handler<T, ActorState> + Sync,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
|
|
Loading…
Reference in New Issue