Skip to content

Commit

Permalink
chore: merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tqwewe committed Jan 1, 2022
2 parents 1555a71 + 18b2820 commit 9d44cfb
Show file tree
Hide file tree
Showing 25 changed files with 679 additions and 202 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<a href="http://thalo.rs" target="_blank" rel="noopener noreferrer"><img width="124" src="logo.png" alt="Thalo logo"></a>
<a href="http://thalo.rs" target="_blank" rel="noopener noreferrer"><img width="124" src="https://raw.githubusercontent.com/thalo-rs/thalo/dev/logo.png" alt="Thalo logo"></a>
</p>

<h1 align="center">Thalo</h1>
Expand All @@ -11,7 +11,7 @@
<img src="https://img.shields.io/crates/l/thalo?style=flat-square" alt="License">
<a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="Pull Requests Welcome"></a>
<a href="https://github.com/thalo-rs/thalo/stargazers"><img src="https://img.shields.io/github/stars/thalo-rs/thalo?style=flat-square" alt="Stargazers"></a>
<a href="https://github.com/thalo-rs/thalo/commits"><img src="https://img.shields.io/github/last-commit/thalo-rs/thalo?style=flat-square" alt="Last Commit"></a>
<a href="https://github.com/thalo-rs/thalo/commits"><img src="https://img.shields.io/github/last-commit/thalo-rs/thalo/dev?style=flat-square" alt="Last Commit"></a>
<a href="https://discord.gg/4Cq8NnPYPA"><img src="https://img.shields.io/discord/913402468895965264?color=%23414EED&label=Discord&logo=Discord&logoColor=%23FFFFFF&style=flat-square" alt="Discord"></a>
</p>

Expand Down
24 changes: 12 additions & 12 deletions examples/protobuf/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use std::sync::Arc;

use thalo::{
aggregate::Aggregate,
event::{AggregateEventEnvelope, IntoEvents},
event_store::EventStore,
aggregate::Aggregate, event::AggregateEventEnvelope, event_store::EventStore,
tests_cfg::bank_account::BankAccount,
};
use thalo_inmemory::InMemoryEventStore;
Expand Down Expand Up @@ -47,17 +45,17 @@ impl bank_account_server::BankAccount for BankAccountService {
}

let (bank_account, event) = BankAccount::open_account(command.id, command.initial_balance)?;
let events = event.into_events();
let events = &[event];

let event_ids = self
.event_store
.save_events::<BankAccount>(bank_account.id(), &events)
.save_events::<BankAccount>(bank_account.id(), events)
.await
.map_err(|err| Status::internal(err.to_string()))?;

broadcast_events(&self.event_store, &self.event_stream, &event_ids).await?;

let events_json = serde_json::to_string(&events)
let events_json = serde_json::to_string(events)
.map_err(|_| Status::internal("failed to serialize events"))?;

Ok(tonic::Response::new(Response {
Expand All @@ -78,17 +76,18 @@ impl bank_account_server::BankAccount for BankAccountService {
.map_err(|err| Status::internal(err.to_string()))?
.ok_or_else(|| Status::not_found("account does not exist"))?;

let events = bank_account.deposit_funds(command.amount)?.into_events();
let event = bank_account.deposit_funds(command.amount)?;
let events = &[event];

let event_ids = self
.event_store
.save_events::<BankAccount>(bank_account.id(), &events)
.save_events::<BankAccount>(bank_account.id(), events)
.await
.map_err(|err| Status::internal(err.to_string()))?;

broadcast_events(&self.event_store, &self.event_stream, &event_ids).await?;

let events_json = serde_json::to_string(&events)
let events_json = serde_json::to_string(events)
.map_err(|_| Status::internal("failed to serialize events"))?;

Ok(tonic::Response::new(Response {
Expand All @@ -109,17 +108,18 @@ impl bank_account_server::BankAccount for BankAccountService {
.map_err(|err| Status::internal(err.to_string()))?
.ok_or_else(|| Status::not_found("account does not exist"))?;

let events = bank_account.withdraw_funds(command.amount)?.into_events();
let event = bank_account.withdraw_funds(command.amount)?;
let events = &[event];

let event_ids = self
.event_store
.save_events::<BankAccount>(bank_account.id(), &events)
.save_events::<BankAccount>(bank_account.id(), events)
.await
.map_err(|err| Status::internal(err.to_string()))?;

broadcast_events(&self.event_store, &self.event_stream, &event_ids).await?;

let events_json = serde_json::to_string(&events)
let events_json = serde_json::to_string(events)
.map_err(|_| Status::internal("failed to serialize events"))?;

Ok(tonic::Response::new(Response {
Expand Down
33 changes: 0 additions & 33 deletions outbox-relay.Dockerfile

This file was deleted.

1 change: 1 addition & 0 deletions thalo-kafka/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ categories = [
]

[dependencies]
async-trait = "0.1"
async-stream = "0.3"
futures-util = "0.3"
rdkafka = "0.28"
Expand Down
3 changes: 3 additions & 0 deletions thalo-kafka/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use thiserror::Error;
/// Error enum.
#[derive(Debug, Error)]
pub enum Error {
/// Failed to create stream.
#[error("failed to create stream")]
CreateStreamError(rdkafka::error::KafkaError),
/// Message had an empty payload.
#[error("empty message payload")]
EmptyPayloadError(rdkafka::message::OwnedMessage),
Expand Down
234 changes: 234 additions & 0 deletions thalo-kafka/src/event_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
use std::fmt;

use async_trait::async_trait;
use futures_util::{stream::BoxStream, StreamExt};
use rdkafka::{
consumer::{Consumer, ConsumerContext, StreamConsumer},
message::OwnedMessage,
Message,
};
use thalo::event::EventHandler;
use tracing::{trace, warn};

use crate::{event_stream::KafkaEventMessage, Error, KafkaClientConfig, KafkaEventStream};

/// Watch an event handler and apply incoming events.
#[async_trait]
pub trait WatchEventHandler<Event>
where
Self: EventHandler<Event>,
Event: Clone + fmt::Debug + Send,
<Self as EventHandler<Event>>::Error: 'static + fmt::Display + Send,
{
/// List of kafka topics to subscribe to.
///
/// # Example
///
/// ```
/// fn topics() -> Vec<&'static str> {
/// vec!["user-events"]
/// }
/// ```
fn topics() -> Vec<&'static str>;

/// An event stream for receiving aggregate events.
///
/// Typically, this will be an event stream from a single aggregate,
/// but can be a merged stream to handle events from multiple aggregates.
///
/// # Examples
///
/// Listen to events from single aggregate.
///
/// ```
/// fn event_stream(
/// kafka_event_stream: &KafkaEventStream,
/// ) -> Result<
/// BoxStream<'_, Result<KafkaEventMessage<AuthEvent>, thalo_kafka::Error>>,
/// thalo_kafka::Error,
/// > {
/// kafka_event_stream.listen_events::<BankAccount>()
/// }
/// ```
///
/// Listen to events from multiple aggregates.
/// ```
/// fn event_stream(
/// kafka_event_stream: &KafkaEventStream,
/// ) -> Result<
/// BoxStream<'_, Result<KafkaEventMessage<AuthEvent>, thalo_kafka::Error>>,
/// thalo_kafka::Error,
/// > {
/// let stream = tokio_stream::StreamExt::merge(
/// kafka_event_stream.listen_events::<Auth>()?,
/// kafka_event_stream.listen_events::<Auth>()?,
/// );
///
/// Ok(stream.boxed())
/// }
/// ```
fn event_stream(
kafka_event_stream: &KafkaEventStream,
) -> Result<BoxStream<'_, Result<KafkaEventMessage<Event>, Error>>, Error>;

/// Watch an event handler for incoming events and handle each event with [`EventHandler::handle`].
///
/// Topics should be handled by the [`WatchEventHandler::event_stream`] method.
/// If you use [`KafkaEventStream::listen_events`] method, topics will be subscibed to automatically
/// using the topics returned by [`WatchEventHandler::topics`] in your implementation.
///
/// If your event handler returns an error, then the kafka offset will not be saved,
/// and Kafka will re-send the event upon reconnection.
///
/// # Examples
///
/// Watch single event handlers.
///
/// ```
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let kafka_host = std::env::var("KAFKA_HOST").expect("missing kafka_host env var");
/// let database_url = std::env::var("DATABASE_URL").expect("missing database_url env var");
///
/// let db = Database::connect(&database_url).await?;
///
/// let projection = BankAccountProjection::new(db);
///
/// projection.watch(&kafka_host, "bank-account").await?;
///
/// Ok(())
/// }
/// ```
///
/// Watch multiple event handlers.
///
/// ```
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let kafka_host = std::env::var("KAFKA_HOST").expect("missing kafka_host env var");
/// let database_url = std::env::var("DATABASE_URL").expect("missing database_url env var");
///
/// let db = Database::connect(&database_url).await?;
///
/// let projections = [
/// ("bank-account", BankAccountProjection::new(db.clone())),
/// ("transactions", TransactionsProjection::new(db)),
/// ];
///
/// let handles: Vec<_> = projections
/// .into_iter()
/// .map(|(group_id, projection)| {
/// tokio::spawn(async move { projection.watch(&redpanda_host, group_id).await })
/// })
/// .collect();
///
/// for handle in handles {
/// handle.await??;
/// }
///
/// Ok(())
/// }
/// ```
async fn watch(&self, brokers: &str, group_id: &str) -> Result<(), Error> {
let consumer: StreamConsumer = KafkaClientConfig::new_recommended(group_id, brokers)
.into_inner()
.create()
.map_err(Error::CreateStreamError)?;

let kafka_event_stream = KafkaEventStream::new(&Self::topics(), consumer);
let consumer = kafka_event_stream.consumer();

let mut event_stream = Self::event_stream(&kafka_event_stream)?;
while let Some(result) = event_stream.next().await {
match result {
Ok(msg) => match self.handle(msg.event).await {
Ok(_) => {
trace!(
topic = msg.message.topic(),
partition = msg.message.partition(),
offset = msg.message.offset(),
"handled event"
);

if let Err(err) = consumer.store_offset(
msg.message.topic(),
msg.message.partition(),
msg.message.offset(),
) {
warn!("error while storing offset: {}", err);
}
}
Err(err) => {
warn!(
topic = msg.message.topic(),
partition = msg.message.partition(),
offset = msg.message.offset(),
"event handler error: {}",
err
);
}
},
Err(err) => {
err.log();
err.store_offset(&consumer);
}
}
}

Ok(())
}
}

trait StreamError: fmt::Display + Sized {
fn get_message(&self) -> Option<&OwnedMessage>;

fn store_offset<C, R>(&self, consumer: &StreamConsumer<C, R>)
where
C: ConsumerContext;

fn log(&self) {
if let Some(message) = self.get_message() {
warn!(
topic = message.topic(),
partition = message.partition(),
offset = message.offset(),
"message error: {}",
self
);
} else {
warn!("message error: {}", self);
}
}
}

impl StreamError for Error {
fn get_message(&self) -> Option<&OwnedMessage> {
use Error::*;

match &self {
EmptyPayloadError(message) | MessageJsonDeserializeError { message, .. } => {
Some(message)
}
_ => None,
}
}

fn store_offset<C, R>(&self, consumer: &StreamConsumer<C, R>)
where
C: ConsumerContext,
{
if let Some(message) = self.get_message() {
if let Err(err) =
consumer.store_offset(message.topic(), message.partition(), message.offset())
{
warn!(
topic = message.topic(),
partition = message.partition(),
offset = message.offset(),
"error while storing offset: {}",
err
);
}
}
}
}
Loading

0 comments on commit 9d44cfb

Please sign in to comment.