photon_backend_fluvio/
port.rs1use std::pin::Pin;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use chrono::Utc;
8use futures::stream::Stream;
9use photon_backend::models::Event;
10use photon_backend::{PhotonError, Result, StorageCapabilities, StoragePort};
11use serde_json::Value;
12use uuid::Uuid;
13
14use crate::checkpoint::CheckpointStore;
15use crate::config::{FluvioConfig, FluvioStoragePortBuilder, ReplayCursor, ENDPOINT_ENV};
16use crate::connect::{connect_fluvio, SharedClient};
17use crate::consumer::subscribe_stream;
18use crate::publish::PublishPipeline;
19use crate::stream_shard::{composite_seq, fluvio_topic_for, pick_shard, publish_routing_key};
20use crate::topic::{ensure_checkpoint_topic, ensure_data_topic, warn_replication_settings};
21
22pub fn fluvio_endpoint_from_env() -> Result<String> {
28 std::env::var(ENDPOINT_ENV).map_err(|_| {
29 PhotonError::Internal(format!("{ENDPOINT_ENV} not set for fluvio storage adapter"))
30 })
31}
32
33pub struct FluvioStoragePort {
35 client: SharedClient,
36 config: FluvioConfig,
37 pipeline: PublishPipeline,
38 checkpoint_store: CheckpointStore,
39 ensured_topics: Arc<dashmap::DashSet<String>>,
40}
41
42impl FluvioStoragePort {
43 #[must_use]
45 pub fn builder() -> FluvioStoragePortBuilder {
46 FluvioStoragePortBuilder::new()
47 }
48
49 pub async fn from_env() -> Result<Self> {
55 Self::builder().from_env_defaults().build().await
56 }
57
58 #[must_use]
60 pub const fn config(&self) -> &FluvioConfig {
61 &self.config
62 }
63
64 async fn connect_with_config(config: FluvioConfig) -> Result<Self> {
65 warn_replication_settings(&config);
66 let client = connect_fluvio(&config).await?;
67 ensure_checkpoint_topic(&client, &config).await?;
68 let checkpoint_store = CheckpointStore::connect(Arc::clone(&client), &config).await?;
69 let pipeline = PublishPipeline::new(Arc::clone(&client), &config);
70 Ok(Self {
71 client,
72 config,
73 pipeline,
74 checkpoint_store,
75 ensured_topics: Arc::new(dashmap::DashSet::new()),
76 })
77 }
78
79 async fn ensure_topic_once(&self, topic: &str) -> Result<()> {
80 if self.ensured_topics.contains(topic) {
81 return Ok(());
82 }
83 ensure_data_topic(&self.client, &self.config, topic).await?;
84 self.ensured_topics.insert(topic.to_string());
85 Ok(())
86 }
87}
88
89impl FluvioStoragePortBuilder {
90 pub async fn build(self) -> Result<FluvioStoragePort> {
96 let config = self.resolve()?;
97 FluvioStoragePort::connect_with_config(config).await
98 }
99}
100
101#[async_trait]
102impl StoragePort for FluvioStoragePort {
103 fn capabilities(&self) -> StorageCapabilities {
104 StorageCapabilities::broker("fluvio")
105 }
106
107 async fn append(
108 &self,
109 topic_name: &str,
110 topic_key: Option<&str>,
111 actor_json: Value,
112 payload_json: Value,
113 ) -> Result<Event> {
114 let _ = self.config.crypto.encrypt(&actor_json, &payload_json)?;
115 let mut event = Event {
116 event_id: Uuid::new_v4().to_string(),
117 topic_name: topic_name.to_string(),
118 topic_key: topic_key.map(String::from),
119 seq: 0,
120 actor_json,
121 payload_json,
122 created_at: Utc::now(),
123 };
124
125 let routing = publish_routing_key(topic_key, &event.event_id);
126 let shard = pick_shard(&routing, self.config.topic_shards);
127 let fluvio_topic = fluvio_topic_for(&self.config, shard, topic_name);
128 self.ensure_topic_once(&fluvio_topic).await?;
129
130 let offset_seq = self.pipeline.publish(&fluvio_topic, &event).await?;
131
132 if self.config.replay_cursor == ReplayCursor::StreamSeq {
133 if let Some(seq) = offset_seq {
134 event.seq = if self.config.is_sharded() {
135 composite_seq(shard, u64::try_from(seq.max(0)).unwrap_or(0))
136 } else {
137 seq
138 };
139 }
140 }
141
142 Ok(event)
143 }
144
145 fn subscribe(
146 &self,
147 topic_name: String,
148 topic_key_filter: Option<String>,
149 after_seq: Option<i64>,
150 ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
151 let effective_after = if self.config.replay_cursor == ReplayCursor::TailOnly {
152 None
153 } else {
154 after_seq
155 };
156 subscribe_stream(
157 Arc::clone(&self.client),
158 self.config.clone(),
159 self.checkpoint_store.clone(),
160 Arc::clone(&self.ensured_topics),
161 topic_name,
162 topic_key_filter,
163 effective_after,
164 )
165 }
166
167 async fn get_event(&self, _event_id: &str) -> Result<Option<Event>> {
168 Ok(None)
169 }
170
171 #[allow(clippy::unused_async)] async fn load_checkpoint(
173 &self,
174 subscription_name: &str,
175 topic_name: &str,
176 topic_key: Option<&str>,
177 ) -> Result<Option<i64>> {
178 if self.config.replay_cursor == ReplayCursor::TailOnly {
179 return Ok(None);
180 }
181 self.checkpoint_store
182 .load(subscription_name, topic_name, topic_key)
183 }
184
185 async fn commit_checkpoint(
186 &self,
187 subscription_name: &str,
188 topic_name: &str,
189 topic_key: Option<&str>,
190 last_seq: i64,
191 ) -> Result<()> {
192 if self.config.replay_cursor == ReplayCursor::TailOnly {
193 return Ok(());
194 }
195 self.checkpoint_store
196 .commit(subscription_name, topic_name, topic_key, last_seq)
197 .await
198 }
199}