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