Skip to main content

carbon_core/
datasource.rs

1//! Ingestion layer of the pipeline: defines upstream data sources and the
2//! normalized update types they emit.
3//!
4//! # Components
5//!
6//! - [`Datasource`] — async producer that streams `(Update, DatasourceId)` into
7//!   the pipeline.
8//! - [`DatasourceId`] — identity used for routing, filtering, and metrics.
9//! - [`Update`] — unified payload consumed by downstream pipeline stages.
10//! - [`UpdateType`] — declared set of update variants a datasource may emit.
11//!
12//! # Flow
13//!
14//! Each datasource runs in a dedicated Tokio task spawned by `Pipeline::run`.
15//! It emits `(Update, DatasourceId)` pairs through an MPSC channel and
16//! terminates when the `CancellationToken` is triggered or the channel is
17//! closed.
18
19use {
20    crate::error::CarbonResult,
21    async_trait::async_trait,
22    chrono::{DateTime, Utc},
23    solana_account::Account,
24    solana_clock::Slot,
25    solana_hash::Hash,
26    solana_pubkey::Pubkey,
27    solana_signature::Signature,
28    solana_transaction::versioned::VersionedTransaction,
29    solana_transaction_status::{Rewards, TransactionStatusMeta},
30    tokio_util::sync::CancellationToken,
31};
32
33/// Metadata describing a datasource disconnection event.
34///
35/// Used for observability and gap detection in streaming sources.
36#[derive(Debug, Clone)]
37pub struct DatasourceDisconnection {
38    pub source: String,
39    pub disconnect_time: DateTime<Utc>,
40    pub last_slot_before_disconnect: Slot,
41    pub first_slot_after_reconnect: Slot,
42    /// Number of slots missed during downtime.
43    pub missed_slots: u64,
44}
45
46/// Async producer trait implemented by all upstream data sources.
47///
48/// Runs as a dedicated task and streams `(Update, DatasourceId)` into the
49/// pipeline. Implementations must respect the provided `CancellationToken` and
50/// exit on shutdown. `update_types` declares which `Update` variants may be
51/// emitted.
52#[async_trait]
53pub trait Datasource: Send + Sync {
54    async fn consume(
55        &self,
56        id: DatasourceId,
57        sender: tokio::sync::mpsc::Sender<(Update, DatasourceId)>,
58        cancellation_token: CancellationToken,
59    ) -> CarbonResult<()>;
60
61    fn update_types(&self) -> Vec<UpdateType>;
62}
63
64/// Unique identifier for a datasource instance.
65///
66/// Used for filtering, routing, and per-source metrics aggregation.
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct DatasourceId(String);
69
70impl DatasourceId {
71    pub fn new_unique() -> Self {
72        Self(uuid::Uuid::new_v4().to_string())
73    }
74
75    pub fn new_named(name: &str) -> Self {
76        Self(name.to_string())
77    }
78}
79
80/// Unified payload emitted by datasources into the pipeline.
81#[derive(Debug, Clone)]
82pub enum Update {
83    Account(AccountUpdate),
84    Transaction(Box<TransactionUpdate>),
85    AccountDeletion(AccountDeletion),
86    BlockDetails(BlockDetails),
87}
88
89/// Declared set of update variants a datasource may emit.
90///
91/// Used by the pipeline to validate that emitted updates match expectations.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum UpdateType {
94    AccountUpdate,
95    Transaction,
96    AccountDeletion,
97    BlockDetails,
98}
99
100impl Update {
101    pub fn update_type(&self) -> UpdateType {
102        match self {
103            Update::Account(_) => UpdateType::AccountUpdate,
104            Update::Transaction(_) => UpdateType::Transaction,
105            Update::AccountDeletion(_) => UpdateType::AccountDeletion,
106            Update::BlockDetails(_) => UpdateType::BlockDetails,
107        }
108    }
109}
110
111/// Account state update emitted by streaming or snapshot sources.
112#[derive(Debug, Clone)]
113pub struct AccountUpdate {
114    pub pubkey: Pubkey,
115    pub account: Account,
116    pub slot: u64,
117    pub transaction_signature: Option<Signature>,
118}
119
120impl AccountUpdate {
121    /// Converts zero-lamport account updates into deletion events.
122    pub fn into_update(self) -> Update {
123        let Self {
124            pubkey,
125            account,
126            slot,
127            transaction_signature,
128        } = self;
129
130        if account.lamports == 0 {
131            Update::AccountDeletion(AccountDeletion {
132                pubkey,
133                account,
134                slot,
135                transaction_signature,
136            })
137        } else {
138            Update::Account(Self {
139                pubkey,
140                account,
141                slot,
142                transaction_signature,
143            })
144        }
145    }
146}
147
148/// Full transaction payload with execution metadata.
149#[derive(Debug, Clone)]
150pub struct TransactionUpdate {
151    pub signature: Signature,
152    pub transaction: VersionedTransaction,
153    pub meta: TransactionStatusMeta,
154    pub is_vote: bool,
155    pub slot: u64,
156    pub index: Option<u64>,
157    pub block_time: Option<i64>,
158    pub block_hash: Option<Hash>,
159}
160
161/// Account closure event with the source's final account state.
162#[derive(Debug, Clone)]
163pub struct AccountDeletion {
164    pub pubkey: Pubkey,
165    pub account: Account,
166    pub slot: u64,
167    pub transaction_signature: Option<Signature>,
168}
169
170/// Block-level metadata emitted by block-aware datasources.
171#[derive(Debug, Clone)]
172pub struct BlockDetails {
173    pub slot: u64,
174    pub block_hash: Option<Hash>,
175    pub previous_block_hash: Option<Hash>,
176    pub rewards: Option<Rewards>,
177    pub num_reward_partitions: Option<u64>,
178    pub block_time: Option<i64>,
179    pub block_height: Option<u64>,
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn converts_zero_lamport_updates_to_deletions() {
188        let pubkey = Pubkey::new_unique();
189        let owner = Pubkey::new_unique();
190        let transaction_signature = Some(Signature::new_unique());
191        let account = Account {
192            lamports: 0,
193            data: vec![1, 2, 3],
194            owner,
195            executable: false,
196            rent_epoch: 42,
197        };
198
199        let update = AccountUpdate {
200            pubkey,
201            account,
202            slot: 7,
203            transaction_signature,
204        }
205        .into_update();
206
207        let Update::AccountDeletion(deletion) = update else {
208            panic!("expected account deletion");
209        };
210        assert_eq!(deletion.pubkey, pubkey);
211        assert_eq!(deletion.account.owner, owner);
212        assert_eq!(deletion.account.data, vec![1, 2, 3]);
213        assert_eq!(deletion.slot, 7);
214        assert_eq!(deletion.transaction_signature, transaction_signature);
215    }
216
217    #[test]
218    fn keeps_funded_account_updates() {
219        let update = AccountUpdate {
220            pubkey: Pubkey::new_unique(),
221            account: Account {
222                lamports: 1,
223                ..Account::default()
224            },
225            slot: 7,
226            transaction_signature: None,
227        }
228        .into_update();
229
230        assert!(matches!(update, Update::Account(_)));
231    }
232}