iota_data_ingestion/workers/
kv_store.rs1use std::{
6 borrow::Borrow,
7 collections::{HashMap, HashSet, VecDeque},
8 iter::repeat,
9 sync::Arc,
10 time::Duration,
11};
12
13use anyhow::Result;
14use async_trait::async_trait;
15use aws_config::{BehaviorVersion, timeout::TimeoutConfig};
16use aws_sdk_dynamodb::{
17 Client,
18 config::{Credentials, Region},
19 primitives::Blob,
20 types::{AttributeValue, PutRequest, WriteRequest},
21};
22use backoff::{ExponentialBackoff, backoff::Backoff};
23use bytes::Bytes;
24use iota_config::object_storage_config::ObjectStoreConfig;
25use iota_data_ingestion_core::Worker;
26use iota_storage::http_key_value_store::{ItemType, TaggedKey};
27use iota_types::{full_checkpoint_content::CheckpointData, storage::ObjectKey};
28use object_store::{DynObjectStore, ObjectStoreExt, path::Path};
29use serde::{Deserialize, Serialize};
30use tracing::{error, info, warn};
31
32use crate::common;
33
34#[derive(Serialize, Deserialize, Clone, Debug)]
35#[serde(rename_all = "kebab-case")]
36pub struct KVStoreTaskConfig {
37 pub object_store_config: ObjectStoreConfig,
38 pub dynamodb_config: DynamoDBConfig,
39}
40
41#[derive(Serialize, Deserialize, Clone, Debug)]
42#[serde(rename_all = "kebab-case")]
43pub struct DynamoDBConfig {
44 pub aws_access_key_id: String,
45 pub aws_secret_access_key: String,
46 pub aws_region: String,
47 pub aws_endpoint: Option<String>,
48 pub table_name: String,
49}
50
51#[derive(Clone)]
52pub struct KVStoreWorker {
53 dynamo_client: Client,
54 remote_store: Arc<DynObjectStore>,
55 table_name: String,
56}
57
58impl KVStoreWorker {
59 pub async fn new(config: KVStoreTaskConfig) -> anyhow::Result<Self> {
60 let credentials = Credentials::new(
61 &config.dynamodb_config.aws_access_key_id,
62 &config.dynamodb_config.aws_secret_access_key,
63 None,
64 None,
65 "dynamodb",
66 );
67 let timeout_config = TimeoutConfig::builder()
68 .operation_timeout(Duration::from_secs(3))
69 .operation_attempt_timeout(Duration::from_secs(10))
70 .connect_timeout(Duration::from_secs(3))
71 .build();
72
73 let http_client = aws_smithy_http_client::Builder::new()
74 .tls_provider(aws_smithy_http_client::tls::Provider::Rustls(
75 aws_smithy_http_client::tls::rustls_provider::CryptoMode::Ring,
76 ))
77 .build_https();
78 let mut aws_config_loader = aws_config::defaults(BehaviorVersion::latest())
79 .http_client(http_client)
80 .credentials_provider(credentials)
81 .region(Region::new(config.dynamodb_config.aws_region))
82 .timeout_config(timeout_config);
83
84 if let Some(url) = config.dynamodb_config.aws_endpoint {
85 aws_config_loader = aws_config_loader.endpoint_url(url);
86 }
87 let aws_config = aws_config_loader.load().await;
88 Ok(Self {
89 dynamo_client: Client::new(&aws_config),
90 remote_store: config.object_store_config.make()?,
91 table_name: config.dynamodb_config.table_name,
92 })
93 }
94
95 async fn multi_set<V: Serialize>(
96 &self,
97 item_type: ItemType,
98 values: impl IntoIterator<Item = (Vec<u8>, V)> + std::marker::Send,
99 ) -> anyhow::Result<()> {
100 let mut items = vec![];
101 let mut seen = HashSet::new();
102 for (digest, value) in values {
103 if seen.contains(&digest) {
104 continue;
105 }
106 seen.insert(digest.clone());
107 let item = WriteRequest::builder()
108 .set_put_request(Some(
109 PutRequest::builder()
110 .item("digest", AttributeValue::B(Blob::new(digest)))
111 .item("type", AttributeValue::S(item_type.to_string()))
112 .item(
113 "bcs",
114 AttributeValue::B(Blob::new(bcs::to_bytes(value.borrow())?)),
115 )
116 .build()?,
117 ))
118 .build();
119 items.push(item);
120 }
121 if items.is_empty() {
122 return Ok(());
123 }
124 let mut backoff = ExponentialBackoff::default();
125 let mut queue: VecDeque<Vec<_>> = items.chunks(25).map(|ck| ck.to_vec()).collect();
126 while let Some(chunk) = queue.pop_front() {
127 let response = self
128 .dynamo_client
129 .batch_write_item()
130 .set_request_items(Some(HashMap::from([(
131 self.table_name.clone(),
132 chunk.to_vec(),
133 )])))
134 .send()
135 .await
136 .inspect_err(|sdk_err| {
137 error!(
138 "{:?}",
139 sdk_err.as_service_error().map(|e| e.meta().to_string())
140 )
141 })?;
142 if let Some(response) = response.unprocessed_items {
143 if let Some(unprocessed) = response.into_iter().next() {
144 if !unprocessed.1.is_empty() {
145 if queue.is_empty() {
146 if let Some(duration) = backoff.next_backoff() {
147 tokio::time::sleep(duration).await;
148 }
149 }
150 queue.push_back(unprocessed.1);
151 }
152 }
153 }
154 }
155 Ok(())
156 }
157
158 async fn upload_checkpoint_contents<V: Serialize + std::marker::Send>(
165 &self,
166 key: Vec<u8>,
167 value: V,
168 ) -> anyhow::Result<()> {
169 let bcs_bytes = bcs::to_bytes(value.borrow())?;
170
171 let attributes = HashMap::from([
172 (
173 "digest".to_string(),
174 AttributeValue::B(Blob::new(key.clone())),
175 ),
176 (
177 "type".to_string(),
178 AttributeValue::S(ItemType::CheckpointContents.to_string()),
179 ),
180 (
181 "bcs".to_string(),
182 AttributeValue::B(Blob::new(bcs_bytes.clone())),
183 ),
184 ]);
185
186 let res = self
187 .dynamo_client
188 .put_item()
189 .table_name(&self.table_name)
190 .set_item(Some(attributes))
191 .send()
192 .await
193 .inspect_err(|err| warn!("dynamodb error: {err}"));
194
195 if res.is_err() {
196 info!("attempt to store checkpoint contents on S3");
197 let location = Path::from(base64_url::encode(&key));
198 self.remote_store
199 .put(&location, Bytes::from(bcs_bytes).into())
200 .await?;
201 }
202
203 Ok(())
204 }
205}
206
207#[async_trait]
208impl Worker for KVStoreWorker {
209 type Message = ();
210 type Error = anyhow::Error;
211
212 async fn process_checkpoint(
213 &self,
214 checkpoint: Arc<CheckpointData>,
215 ) -> Result<Self::Message, Self::Error> {
216 let mut transactions = vec![];
217 let mut effects = vec![];
218 let mut events = vec![];
219 let mut objects = vec![];
220 let mut transactions_to_checkpoint = vec![];
221 let checkpoint_number = checkpoint.checkpoint_summary.sequence_number;
222
223 for transaction in &checkpoint.transactions {
224 let transaction_digest = transaction.transaction.digest().into_bytes().to_vec();
225 effects.push((transaction_digest.clone(), transaction.effects.clone()));
226 transactions_to_checkpoint.push((transaction_digest.clone(), checkpoint_number));
227 transactions.push((transaction_digest.clone(), transaction.transaction.clone()));
228
229 if let Some(tx_events) = &transaction.events {
230 events.push((tx_events.digest().into_bytes().to_vec(), tx_events));
231 }
232 for object in &transaction.output_objects {
233 let object_key = ObjectKey(object.id(), object.version());
234 objects.push((bcs::to_bytes(&object_key)?, object));
235 }
236 }
237 self.multi_set(ItemType::Transaction, transactions).await?;
238 self.multi_set(ItemType::TransactionEffects, effects)
239 .await?;
240 self.multi_set(ItemType::EventTransactionDigest, events)
241 .await?;
242 self.multi_set(ItemType::Object, objects).await?;
243 self.multi_set(
244 ItemType::TransactionToCheckpoint,
245 transactions_to_checkpoint,
246 )
247 .await?;
248
249 let serialized_checkpoint_number =
250 bcs::to_bytes(&TaggedKey::CheckpointSequenceNumber(checkpoint_number))?;
251 let checkpoint_summary = &checkpoint.checkpoint_summary;
252
253 self.upload_checkpoint_contents(
254 serialized_checkpoint_number.clone(),
255 checkpoint.checkpoint_contents.clone(),
256 )
257 .await?;
258
259 self.multi_set(
260 ItemType::CheckpointSummary,
261 [
262 serialized_checkpoint_number,
263 checkpoint_summary.digest().into_bytes().to_vec(),
264 ]
265 .into_iter()
266 .zip(repeat(checkpoint_summary)),
267 )
268 .await?;
269
270 if checkpoint_number.is_multiple_of(common::PROGRESS_LOG_INTERVAL) {
271 info!("stored checkpoint {checkpoint_number} in the KV store");
272 }
273
274 Ok(())
275 }
276}