Skip to content

Commit df57dd2

Browse files
committed
Refine event mesh application workflows
1 parent 2e1e61c commit df57dd2

40 files changed

Lines changed: 799 additions & 449 deletions

eventmesh-sdks/eventmesh-sdk-rust/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ async fn main() -> eventmesh::Result<()> {
4141
let client = GrpcClient::new(GrpcConfig::new(Endpoint::new("127.0.0.1", 10_205)?))?;
4242
let producer = client.producer(ProducerOptions::new("orders-producer"))?;
4343
let receipt = producer
44-
.publish(Message::from(EventMeshMessage::new("orders.created", r#"{"id": 42}"#)))
44+
.publish(Message::from(EventMeshMessage::new(
45+
"orders.created",
46+
r#"{"id": 42}"#,
47+
)?))
4548
.await?;
4649
println!("accepted with code {}", receipt.code);
4750
Ok(())
@@ -73,11 +76,17 @@ See the runnable transport-specific consumer programs in [examples/README.md](ex
7376

7477
`HttpClient::consumer` binds its callback socket before registering subscriptions, then owns the axum server, heartbeat, and registration lifecycle. For an application-owned endpoint, use `HttpClient::webhook_registration` with `eventmesh::http::codec::{parse_push_body, WebhookReply}`. TCP unsubscribe is session-wide, so its API is `unsubscribe_all()`.
7578

79+
All consumers use the same local lifecycle contract: `shutdown()` only signals
80+
background work to stop, while `join().await` waits for it and reports task or
81+
transport failures. HTTP consumers and webhook registrations additionally
82+
provide `close().await`, which unregisters remote subscriptions before
83+
signalling shutdown and joining.
84+
7685
HTTP request/reply is not exposed because the current SDK and stock Runtime do not provide a complete HTTP responder path. Use gRPC or TCP for request/reply.
7786

7887
`Message` is a public dialect envelope, not a wire format. The selected transport owns protobuf, HTTP form, or TCP frame serialization. With `cloud_events`, CloudEvents remain CloudEvents; `Message::into_event_mesh()` does not silently flatten them into the native EventMesh model.
7988

80-
`EventMeshMessage` is likewise a business model rather than a stable serde JSON contract. gRPC, HTTP, and TCP convert it into private transport-specific wire DTOs. Native messages require non-blank topics and content; an explicit `ttl` must be a positive millisecond value no greater than `i32::MAX`. EventMesh does not define a never-expire TTL sentinel.
89+
`EventMeshMessage` is likewise a business model rather than a stable serde JSON contract. gRPC, HTTP, and TCP convert it into private transport-specific wire DTOs. A topic must be non-blank and content must be present, but empty content is accepted for Java SDK interoperability. Inbound TTL metadata is preserved as received; each transport applies its own outbound content and TTL limits when publishing.
8190

8291
## Configuration and errors
8392

eventmesh-sdks/eventmesh-sdk-rust/examples/grpc/producer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async fn main() -> eventmesh::Result<()> {
2929
.publish(Message::from(EventMeshMessage::new(
3030
"test-topic-rust-sdk",
3131
"hello from rust",
32-
)))
32+
)?))
3333
.await?;
3434
println!("published: {receipt:?}");
3535
Ok(())

eventmesh-sdks/eventmesh-sdk-rust/examples/http/producer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async fn main() -> eventmesh::Result<()> {
3030
.publish(Message::from(EventMeshMessage::new(
3131
"test-topic-rust-sdk",
3232
"hello from rust",
33-
)))
33+
)?))
3434
.await?;
3535
println!("published: {receipt:?}");
3636
Ok(())

eventmesh-sdks/eventmesh-sdk-rust/examples/tcp/producer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async fn main() -> eventmesh::Result<()> {
3131
.publish(Message::from(EventMeshMessage::new(
3232
"test-topic-rust-sdk",
3333
"hello from rust",
34-
)))
34+
)?))
3535
.await?;
3636
println!("published: {receipt:?}");
3737
Ok(())

eventmesh-sdks/eventmesh-sdk-rust/src/config/client.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,11 @@ pub struct ConsumerOptions {
326326
}
327327

328328
impl ConsumerOptions {
329-
/// Create options for `group` with serial delivery by default.
329+
/// Create options for `group`.
330+
///
331+
/// Delivery concurrency is transport-specific. HTTP webhook requests may
332+
/// run concurrently, TCP delivery is serial, and gRPC stream consumers use
333+
/// [`GrpcConsumerOptions`] to configure handler concurrency.
330334
pub fn new(group: impl Into<String>) -> Self {
331335
Self {
332336
group: group.into(),

eventmesh-sdks/eventmesh-sdk-rust/src/error.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,24 @@ pub enum Error {
4949
#[error("grpc transport error: {0}")]
5050
GrpcTransport(String),
5151

52-
/// An HTTP transport error (Phase 2).
52+
/// An HTTP transport error.
5353
#[error("http error: status {status}: {message}")]
54-
Http { status: u16, message: String },
54+
Http {
55+
/// HTTP response status code.
56+
status: u16,
57+
/// Error description returned by the HTTP transport or peer.
58+
message: String,
59+
},
5560

56-
/// A TCP transport error (Phase 3).
61+
/// A TCP transport error.
5762
#[error("tcp error: {0}")]
5863
Tcp(String),
5964

6065
/// Serialization / deserialization failure.
6166
#[error("codec error: {0}")]
6267
Codec(#[from] serde_json::Error),
6368

64-
/// A message failed validation before it was sent on the wire.
69+
/// A message failed validation during construction, decoding, or sending.
6570
#[error("invalid message: {0}")]
6671
InvalidMessage(String),
6772

@@ -80,7 +85,12 @@ pub enum Error {
8085

8186
/// The EventMesh server returned a non-success response code.
8287
#[error("server error: code={code} message={message}")]
83-
Server { code: i32, message: String },
88+
Server {
89+
/// EventMesh server response code.
90+
code: i32,
91+
/// Error description returned by the EventMesh server.
92+
message: String,
93+
},
8494

8595
/// Low-level I/O error.
8696
#[error("io error: {0}")]

eventmesh-sdks/eventmesh-sdk-rust/src/grpc.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,14 @@ impl GrpcWebhookConsumer {
154154
.map(|_| ())
155155
}
156156

157-
/// Stop the heartbeat task.
158-
pub async fn shutdown(&self) {
159-
self.inner.shutdown().await;
157+
/// Signal the heartbeat task to stop.
158+
pub fn shutdown(&self) {
159+
self.inner.request_shutdown();
160160
}
161161

162-
/// Wait for the heartbeat task to stop.
163-
pub async fn join(&self) {
164-
self.inner.wait_for_shutdown().await;
162+
/// Wait for the heartbeat task to stop and report task failure.
163+
pub async fn join(&self) -> Result<()> {
164+
self.inner.wait_for_shutdown().await
165165
}
166166
}
167167

@@ -179,9 +179,9 @@ impl<H: MessageHandler> GrpcConsumer<H> {
179179
.map(|_| ())
180180
}
181181

182-
/// Request graceful stream shutdown.
183-
pub async fn shutdown(&self) {
184-
self.inner.shutdown().await;
182+
/// Signal graceful stream shutdown.
183+
pub fn shutdown(&self) {
184+
self.inner.request_shutdown();
185185
}
186186

187187
/// Wait for stream shutdown.

eventmesh-sdks/eventmesh-sdk-rust/src/http.rs

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,15 @@ impl HttpClient {
113113
.await
114114
{
115115
lifecycle.cancel();
116-
inner.shutdown().await;
116+
let _ = inner.shutdown().await;
117117
let _ = server_handle.await;
118118
return Err(error);
119119
}
120120

121121
if server_handle.is_finished() {
122122
lifecycle.cancel();
123123
let _ = inner.unsubscribe_all().await;
124-
inner.shutdown().await;
124+
let _ = inner.shutdown().await;
125125
return match server_handle.await {
126126
Ok(Err(error)) => Err(error),
127127
Ok(Ok(())) => Err(EventMeshError::ChannelClosed(
@@ -206,20 +206,29 @@ impl HttpConsumer {
206206
&self.webhook_url
207207
}
208208

209-
/// Unregister all subscriptions and stop heartbeat and callback serving.
210-
pub async fn shutdown(&self) -> Result<()> {
211-
let unregister_result = self.inner.unsubscribe_all().await;
212-
self.inner.shutdown().await;
209+
/// Signal heartbeat and callback serving to stop.
210+
pub fn shutdown(&self) {
211+
self.inner.request_shutdown();
213212
self.lifecycle.cancel();
214-
let server_result = self.wait_for_server().await;
215-
unregister_result.and(server_result)
216213
}
217214

218-
/// Wait until the callback server exits and all background work stops.
215+
/// Wait until the callback server exits and report background task failure.
219216
pub async fn join(&self) -> Result<()> {
220-
self.lifecycle.cancelled().await;
221-
self.inner.wait_for_shutdown().await;
222-
self.wait_for_server().await
217+
let server_result = self.wait_for_server().await;
218+
// A panicked server task cannot cancel the lifecycle token itself.
219+
// Cancel it here so heartbeat shutdown is guaranteed before returning.
220+
self.lifecycle.cancel();
221+
self.inner.request_shutdown();
222+
let heartbeat_result = self.inner.wait_for_shutdown().await;
223+
server_result.and(heartbeat_result)
224+
}
225+
226+
/// Unregister all subscriptions, signal shutdown, and join background work.
227+
pub async fn close(&self) -> Result<()> {
228+
let unregister_result = self.inner.unsubscribe_all().await;
229+
self.shutdown();
230+
let join_result = self.join().await;
231+
unregister_result.and(join_result)
223232
}
224233

225234
async fn wait_for_server(&self) -> Result<()> {
@@ -272,16 +281,22 @@ impl WebhookRegistration {
272281
.map(|_| ())
273282
}
274283

275-
/// Unregister every tracked subscription and stop the heartbeat task.
276-
pub async fn shutdown(&self) -> Result<()> {
277-
let unregister_result = self.inner.unsubscribe_all().await;
278-
self.inner.shutdown().await;
279-
unregister_result
284+
/// Signal the heartbeat task to stop.
285+
pub fn shutdown(&self) {
286+
self.inner.request_shutdown();
280287
}
281288

282-
/// Wait for the heartbeat task to stop.
283-
pub async fn join(&self) {
284-
self.inner.wait_for_shutdown().await;
289+
/// Wait for the heartbeat task to stop and report task failure.
290+
pub async fn join(&self) -> Result<()> {
291+
self.inner.wait_for_shutdown().await
292+
}
293+
294+
/// Unregister every tracked subscription, shut down, and join.
295+
pub async fn close(&self) -> Result<()> {
296+
let unregister_result = self.inner.unsubscribe_all().await;
297+
self.shutdown();
298+
let join_result = self.join().await;
299+
unregister_result.and(join_result)
285300
}
286301
}
287302

@@ -399,7 +414,7 @@ mod tests {
399414
}
400415

401416
#[tokio::test]
402-
async fn managed_consumer_serves_and_unregisters_on_shutdown() {
417+
async fn managed_consumer_serves_and_unregisters_on_close() {
403418
let (client, codes, runtime) = mock_runtime(false).await;
404419
let (tx, mut rx) = mpsc::unbounded_channel();
405420
let consumer = client
@@ -430,18 +445,40 @@ mod tests {
430445
.await
431446
.unwrap()
432447
.unwrap();
433-
assert_eq!(
434-
received.as_event_mesh().unwrap().content.as_deref(),
435-
Some("created")
436-
);
448+
assert_eq!(received.as_event_mesh().unwrap().content(), "created");
437449

438-
consumer.shutdown().await.unwrap();
450+
consumer.close().await.unwrap();
439451
let codes = codes.lock().await.clone();
440452
assert!(codes.contains(&crate::common::status_code::RequestCode::SUBSCRIBE));
441453
assert!(codes.contains(&crate::common::status_code::RequestCode::UNSUBSCRIBE));
442454
runtime.abort();
443455
}
444456

457+
#[tokio::test]
458+
async fn managed_consumer_shutdown_only_signals_local_tasks() {
459+
let (client, codes, runtime) = mock_runtime(false).await;
460+
let consumer = client
461+
.consumer(
462+
ConsumerOptions::new("group"),
463+
WebhookOptions::new("127.0.0.1:0".parse().unwrap()),
464+
[Subscription::new("orders")],
465+
|_message| async { Ok(None) },
466+
)
467+
.await
468+
.unwrap();
469+
470+
consumer.shutdown();
471+
consumer.join().await.unwrap();
472+
473+
let codes = codes.lock().await.clone();
474+
assert!(codes.contains(&crate::common::status_code::RequestCode::SUBSCRIBE));
475+
assert!(
476+
!codes.contains(&crate::common::status_code::RequestCode::UNSUBSCRIBE),
477+
"shutdown must not perform remote cleanup; close owns that operation"
478+
);
479+
runtime.abort();
480+
}
481+
445482
#[tokio::test]
446483
async fn registration_failure_stops_managed_server() {
447484
let (client, _codes, runtime) = mock_runtime(true).await;

eventmesh-sdks/eventmesh-sdk-rust/src/lib.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@
4141
//!
4242
//! A [`MessageHandler`] returns `Ok(None)` to acknowledge an asynchronous
4343
//! delivery, `Ok(Some(reply))` to reply to a synchronous delivery, or `Err(_)`
44-
//! to report application failure. Long-lived consumers expose `shutdown` and
45-
//! `join`; a managed HTTP consumer also owns its callback server, registration,
46-
//! and heartbeat lifecycle.
44+
//! to report application failure. Long-lived consumers use a two-step
45+
//! lifecycle: `shutdown` only signals cancellation, while `join` waits for
46+
//! background work and reports failures. Managed HTTP consumers and webhook
47+
//! registrations additionally expose `close` to unregister remotely before
48+
//! shutting down and joining.
4749
48-
#![deny(unsafe_code)]
50+
#![deny(missing_docs, unsafe_code)]
4951

5052
// These modules are wire adapters retained behind the v2 public façade. Some
5153
// protocol-specific compatibility paths are intentionally feature-dependent,
@@ -82,7 +84,9 @@ pub mod tcp;
8284

8385
pub use error::{Error, Result};
8486
pub use handler::MessageHandler;
85-
pub use message::{EventMeshMessage, Message, MessageKind, PublishReceipt};
87+
pub use message::{
88+
EventMeshMessage, EventMeshMessageBuilder, Message, MessageKind, PublishReceipt,
89+
};
8690
pub use subscription::{DeliveryMode, DeliveryType, Subscription};
8791

8892
#[cfg(feature = "grpc")]

eventmesh-sdks/eventmesh-sdk-rust/src/message.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
use crate::error::EventMeshError;
2828
use crate::error::Result;
2929

30-
pub use crate::model::EventMeshMessage;
30+
pub use crate::model::{EventMeshMessage, EventMeshMessageBuilder};
3131

3232
/// Which public event dialect a [`Message`] contains.
3333
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

0 commit comments

Comments
 (0)