feat: with tx on all
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
e774529b04
commit
bc28451f8d
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
.cuddle/
|
.cuddle/
|
||||||
|
local.env
|
||||||
|
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -725,6 +725,8 @@ dependencies = [
|
|||||||
"crunch-envelope",
|
"crunch-envelope",
|
||||||
"crunch-traits",
|
"crunch-traits",
|
||||||
"futures",
|
"futures",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -2449,6 +2451,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2531,6 +2534,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2571,6 +2575,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2595,6 +2600,7 @@ dependencies = [
|
|||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -28,6 +28,7 @@ nats = "0.24.0"
|
|||||||
clap = {version = "4.4.5", features = ["derive"]}
|
clap = {version = "4.4.5", features = ["derive"]}
|
||||||
toml_edit = {version = "0.20.0",features = ["serde"]}
|
toml_edit = {version = "0.20.0",features = ["serde"]}
|
||||||
serde = {version = "1.0.188", features = ["derive"]}
|
serde = {version = "1.0.188", features = ["derive"]}
|
||||||
|
serde_json = {version = "1.0.107"}
|
||||||
prost = {version = "0.12"}
|
prost = {version = "0.12"}
|
||||||
prost-types = {version = "0.12"}
|
prost-types = {version = "0.12"}
|
||||||
prost-build = "0.12"
|
prost-build = "0.12"
|
||||||
@ -37,6 +38,6 @@ genco = {version = "0.17.6"}
|
|||||||
walkdir = {version = "2.4.0"}
|
walkdir = {version = "2.4.0"}
|
||||||
regex = {version = "1.9.5"}
|
regex = {version = "1.9.5"}
|
||||||
inquire = {version = "0.6.2"}
|
inquire = {version = "0.6.2"}
|
||||||
sqlx = {version = "0.7.2", default-features = false, features = ["migrate", "macros", "postgres", "runtime-tokio", "tls-rustls", "chrono", "json" ]}
|
sqlx = {version = "0.7.2", default-features = false, features = ["migrate", "macros", "postgres", "runtime-tokio", "tls-rustls", "chrono", "json", "uuid" ]}
|
||||||
|
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
@ -306,24 +306,24 @@ impl Default for Codegen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// #[cfg(test)]
|
||||||
mod tests {
|
// mod tests {
|
||||||
use super::*;
|
// use super::*;
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_node() {
|
// fn test_node() {
|
||||||
let mut root = Node::new("root".into(), None, None);
|
// let mut root = Node::new("root".into(), None, None);
|
||||||
|
|
||||||
root.insert("basic.my_event.rs", vec!["One".into(), "Two".into()]);
|
// root.insert("basic.my_event.rs", vec!["One".into(), "Two".into()]);
|
||||||
root.insert("basic.includes.includes.rs", vec!["Three".into()]);
|
// root.insert("basic.includes.includes.rs", vec!["Three".into()]);
|
||||||
root.insert("basic.includes.includes-two.rs", Vec::new());
|
// root.insert("basic.includes.includes-two.rs", Vec::new());
|
||||||
|
|
||||||
let res = root
|
// let res = root
|
||||||
.traverse()
|
// .traverse()
|
||||||
.to_file_string()
|
// .to_file_string()
|
||||||
.expect("to generate rust code");
|
// .expect("to generate rust code");
|
||||||
|
|
||||||
pretty_assertions::assert_eq!(res, r#""#);
|
// pretty_assertions::assert_eq!(res, r#""#);
|
||||||
|
|
||||||
panic!();
|
// panic!();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
@ -151,6 +151,7 @@ output-path = "src/crunch"
|
|||||||
[[publish]]
|
[[publish]]
|
||||||
schema-path = "some-schema"
|
schema-path = "some-schema"
|
||||||
output-path = "some-output"
|
output-path = "some-output"
|
||||||
|
entities = []
|
||||||
"#;
|
"#;
|
||||||
let mut config = File::parse(raw).await?;
|
let mut config = File::parse(raw).await?;
|
||||||
let config = config.add_publish("some-schema", "some-output", &[]);
|
let config = config.add_publish("some-schema", "some-output", &[]);
|
||||||
@ -176,6 +177,7 @@ codegen = ["rust"]
|
|||||||
[[publish]]
|
[[publish]]
|
||||||
schema-path = "some-schema"
|
schema-path = "some-schema"
|
||||||
output-path = "some-output"
|
output-path = "some-output"
|
||||||
|
entities = []
|
||||||
"#;
|
"#;
|
||||||
let mut config = File::parse(raw).await?;
|
let mut config = File::parse(raw).await?;
|
||||||
let config = config.add_publish("some-schema", "some-output", &[]);
|
let config = config.add_publish("some-schema", "some-output", &[]);
|
||||||
@ -221,6 +223,7 @@ codegen = ["rust"]
|
|||||||
[[publish]]
|
[[publish]]
|
||||||
schema-path = "some-schema"
|
schema-path = "some-schema"
|
||||||
output-path = "some-output"
|
output-path = "some-output"
|
||||||
|
entities = []
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let config = File::parse(raw).await?.get_config()?;
|
let config = File::parse(raw).await?.get_config()?;
|
||||||
|
@ -21,6 +21,10 @@ pub struct Msg {
|
|||||||
state: MsgState,
|
state: MsgState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct InMemoryTx {}
|
||||||
|
|
||||||
|
impl crunch_traits::Tx for InMemoryTx {}
|
||||||
|
|
||||||
pub struct InMemoryPersistence {
|
pub struct InMemoryPersistence {
|
||||||
pub outbox: Arc<RwLock<VecDeque<Msg>>>,
|
pub outbox: Arc<RwLock<VecDeque<Msg>>>,
|
||||||
pub store: Arc<RwLock<BTreeMap<String, Msg>>>,
|
pub store: Arc<RwLock<BTreeMap<String, Msg>>>,
|
||||||
@ -49,9 +53,11 @@ impl crunch_traits::Persistence for InMemoryPersistence {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next(&self) -> Option<String> {
|
async fn next(&self) -> Result<Option<(String, crunch_traits::DynTx)>, PersistenceError> {
|
||||||
let mut outbox = self.outbox.write().await;
|
let mut outbox = self.outbox.write().await;
|
||||||
outbox.pop_front().map(|i| i.id)
|
Ok(outbox
|
||||||
|
.pop_front()
|
||||||
|
.map(|i| (i.id, Box::new(InMemoryTx {}) as crunch_traits::DynTx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError> {
|
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError> {
|
||||||
|
@ -16,4 +16,6 @@ async-trait.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
tokio-stream = {workspace = true, features = ["sync"]}
|
tokio-stream = {workspace = true, features = ["sync"]}
|
@ -4,5 +4,5 @@ CREATE TABLE outbox (
|
|||||||
metadata JSONB NOT NULL,
|
metadata JSONB NOT NULL,
|
||||||
content BYTEA NOT NULL,
|
content BYTEA NOT NULL,
|
||||||
inserted_time TIMESTAMPTZ NOT NULL DEFAULT now(),
|
inserted_time TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
state VARCHAR NOT NULL,
|
state VARCHAR NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use crunch_traits::{errors::PersistenceError, EventInfo};
|
use crunch_traits::{errors::PersistenceError, EventInfo};
|
||||||
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{postgres::PgPoolOptions, types::Json, Pool, Postgres};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct PostgresTx {}
|
||||||
|
|
||||||
|
impl crunch_traits::Tx for PostgresTx {}
|
||||||
|
|
||||||
pub struct PostgresPersistence {
|
pub struct PostgresPersistence {
|
||||||
pool: Pool<Postgres>,
|
pool: Pool<Postgres>,
|
||||||
@ -14,15 +20,85 @@ impl PostgresPersistence {
|
|||||||
|
|
||||||
Ok(Self { pool })
|
Ok(Self { pool })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn new_from_env() -> anyhow::Result<Self> {
|
||||||
|
let dsn = std::env::var("DATABASE_URL")
|
||||||
|
.map_err(|e| anyhow::anyhow!("DATABASE_URL is not set: {e}"))?;
|
||||||
|
|
||||||
|
Self::new(&dsn).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct InsertResp {
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
struct PgEventInfo {
|
||||||
|
domain: &'static str,
|
||||||
|
entity_type: &'static str,
|
||||||
|
event_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&EventInfo> for PgEventInfo {
|
||||||
|
fn from(value: &EventInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
domain: value.domain,
|
||||||
|
entity_type: value.entity_type,
|
||||||
|
event_name: value.event_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl crunch_traits::Persistence for PostgresPersistence {
|
impl crunch_traits::Persistence for PostgresPersistence {
|
||||||
|
// FIXME: Need some sort of concurrency control mechanism. If the insert fails or is done twice, then that user may receive multiple requests.
|
||||||
|
// This should be solved by adding transactions, event streams and sequence numbers
|
||||||
async fn insert(&self, event_info: &EventInfo, content: Vec<u8>) -> anyhow::Result<()> {
|
async fn insert(&self, event_info: &EventInfo, content: Vec<u8>) -> anyhow::Result<()> {
|
||||||
todo!()
|
let event_info: PgEventInfo = event_info.into();
|
||||||
|
sqlx::query_as::<_, InsertResp>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO outbox (id, metadata, content, state)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
'inserted'
|
||||||
|
)
|
||||||
|
RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(uuid::Uuid::new_v4())
|
||||||
|
.bind(Json(&event_info))
|
||||||
|
.bind(content)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn next(&self) -> Option<String> {
|
async fn next(&self) -> Result<Option<(String, crunch_traits::DynTx)>, PersistenceError> {
|
||||||
todo!()
|
let resp = sqlx::query_as::<_, InsertResp>(
|
||||||
|
r#"
|
||||||
|
SELECT id
|
||||||
|
FROM outbox
|
||||||
|
WHERE state = 'inserted'
|
||||||
|
ORDER BY inserted_time ASC
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!(e))
|
||||||
|
.map_err(PersistenceError::AnyErr)?;
|
||||||
|
|
||||||
|
let id = match resp {
|
||||||
|
Some(InsertResp { id }) => Some(id.to_string()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(id.map(|id| (id, Box::new(PostgresTx {}) as crunch_traits::DynTx)))
|
||||||
}
|
}
|
||||||
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError> {
|
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError> {
|
||||||
todo!()
|
todo!()
|
||||||
|
8
crates/crunch-postgres/tests/new_test.rs
Normal file
8
crates/crunch-postgres/tests/new_test.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use crunch_postgres::PostgresPersistence;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_new_from_env() -> anyhow::Result<()> {
|
||||||
|
PostgresPersistence::new_from_env().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
63
crates/crunch-postgres/tests/persistence_test.rs
Normal file
63
crates/crunch-postgres/tests/persistence_test.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use crunch_postgres::PostgresPersistence;
|
||||||
|
use crunch_traits::{EventInfo, Persistence};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_persistence_insert() -> anyhow::Result<()> {
|
||||||
|
let persistence = PostgresPersistence::new_from_env().await?;
|
||||||
|
|
||||||
|
persistence
|
||||||
|
.insert(
|
||||||
|
&EventInfo {
|
||||||
|
domain: "some-domain",
|
||||||
|
entity_type: "some-entity-type",
|
||||||
|
event_name: "some-event-name",
|
||||||
|
},
|
||||||
|
b"some-strange-and-cruncy-content".to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
persistence
|
||||||
|
.insert(
|
||||||
|
&EventInfo {
|
||||||
|
domain: "some-domain",
|
||||||
|
entity_type: "some-entity-type",
|
||||||
|
event_name: "some-event-name",
|
||||||
|
},
|
||||||
|
b"some-strange-and-cruncy-content".to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_persistence_next() -> anyhow::Result<()> {
|
||||||
|
let persistence = PostgresPersistence::new_from_env().await?;
|
||||||
|
|
||||||
|
persistence
|
||||||
|
.insert(
|
||||||
|
&EventInfo {
|
||||||
|
domain: "some-domain",
|
||||||
|
entity_type: "some-entity-type",
|
||||||
|
event_name: "some-event-name",
|
||||||
|
},
|
||||||
|
b"some-strange-and-cruncy-content".to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
persistence
|
||||||
|
.insert(
|
||||||
|
&EventInfo {
|
||||||
|
domain: "some-domain",
|
||||||
|
entity_type: "some-entity-type",
|
||||||
|
event_name: "some-event-name",
|
||||||
|
},
|
||||||
|
b"some-strange-and-cruncy-content".to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(persistence.next().await?.is_some());
|
||||||
|
assert!(persistence.next().await?.is_some());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -54,6 +54,9 @@ pub enum PersistenceError {
|
|||||||
|
|
||||||
#[error("failed to publish item {0}")]
|
#[error("failed to publish item {0}")]
|
||||||
UpdatePublished(anyhow::Error),
|
UpdatePublished(anyhow::Error),
|
||||||
|
|
||||||
|
#[error("database query failed {0}")]
|
||||||
|
AnyErr(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -3,10 +3,14 @@ use std::fmt::Display;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use errors::{DeserializeError, PersistenceError, SerializeError};
|
use errors::{DeserializeError, PersistenceError, SerializeError};
|
||||||
|
|
||||||
|
pub trait Tx: Send + Sync {}
|
||||||
|
|
||||||
|
pub type DynTx = Box<dyn Tx>;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Persistence {
|
pub trait Persistence {
|
||||||
async fn insert(&self, event_info: &EventInfo, content: Vec<u8>) -> anyhow::Result<()>;
|
async fn insert(&self, event_info: &EventInfo, content: Vec<u8>) -> anyhow::Result<()>;
|
||||||
async fn next(&self) -> Option<String>;
|
async fn next(&self) -> Result<Option<(String, DynTx)>, PersistenceError>;
|
||||||
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError>;
|
async fn get(&self, event_id: &str) -> Result<Option<(EventInfo, Vec<u8>)>, PersistenceError>;
|
||||||
async fn update_published(&self, event_id: &str) -> Result<(), PersistenceError>;
|
async fn update_published(&self, event_id: &str) -> Result<(), PersistenceError>;
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,8 @@ impl OutboxHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_messages(p: &Persistence, t: &Transport) -> anyhow::Result<Option<()>> {
|
async fn handle_messages(p: &Persistence, t: &Transport) -> anyhow::Result<Option<()>> {
|
||||||
match p.next().await {
|
match p.next().await? {
|
||||||
Some(item) => match p.get(&item).await? {
|
Some((item, _)) => match p.get(&item).await? {
|
||||||
Some((info, content)) => {
|
Some((info, content)) => {
|
||||||
t.publish(&info, content).await?;
|
t.publish(&info, content).await?;
|
||||||
p.update_published(&item).await?;
|
p.update_published(&item).await?;
|
||||||
|
@ -5,3 +5,11 @@ base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
|
|||||||
vars:
|
vars:
|
||||||
service: "crunch"
|
service: "crunch"
|
||||||
registry: kasperhermansen
|
registry: kasperhermansen
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
local_up:
|
||||||
|
type: shell
|
||||||
|
local_down:
|
||||||
|
type: shell
|
||||||
|
"db:shell":
|
||||||
|
type: shell
|
||||||
|
4
scripts/db:shell.sh
Executable file
4
scripts/db:shell.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PGPASSWORD="cuddle"
|
||||||
|
psql -h localhost -d cuddle -U cuddle
|
19
scripts/local_down.sh
Executable file
19
scripts/local_down.sh
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
docker_compose_content=$(cat <<EOF
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: "postgres:latest"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: cuddle
|
||||||
|
POSTGRES_PASSWORD: cuddle
|
||||||
|
POSTGRES_DB: cuddle
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
EOF)
|
||||||
|
|
||||||
|
docker-compose -p cuddle_local -f <(echo "$docker_compose_content") down --remove-orphans --volumes
|
24
scripts/local_up.sh
Executable file
24
scripts/local_up.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
docker_compose_content=$(cat <<EOF
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: "postgres:latest"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: cuddle
|
||||||
|
POSTGRES_PASSWORD: cuddle
|
||||||
|
POSTGRES_DB: cuddle
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
EOF)
|
||||||
|
|
||||||
|
docker-compose -p cuddle_local -f <(echo "$docker_compose_content") up -d --remove-orphans
|
||||||
|
|
||||||
|
cat <<EOF > local.env
|
||||||
|
DATABASE_URL=postgres://cuddle:cuddle@localhost:5432/cuddle
|
||||||
|
EOF
|
||||||
|
|
Loading…
Reference in New Issue
Block a user