1use core::result::Result::Ok;
6use std::{any::Any as StdAny, collections::HashMap, time::Duration};
7
8use async_trait::async_trait;
9use diesel::{
10 ExpressionMethods, OptionalExtension, PgConnection, QueryDsl, RunQueryDsl,
11 dsl::{max, min, sql},
12 sql_types::{Array, BigInt, Bytea, Nullable, SmallInt, Text},
13 upsert::excluded,
14};
15use downcast::Any;
16use iota_protocol_config::ProtocolConfig;
17use iota_sdk_types::{CheckpointDigest, ObjectId};
18use iota_types::digests::ChainIdentifier;
19use itertools::Itertools;
20use strum::IntoEnumIterator;
21use tap::TapFallible;
22use tracing::info;
23
24use super::pg_partition_manager::{EpochPartitionData, PgPartitionManager};
25use crate::{
26 db::ConnectionPool,
27 errors::{Context, IndexerError},
28 ingestion::{
29 common::{
30 persist::CommitterWatermark,
31 prepare::{
32 CheckpointObjectChanges, LiveObject, RemovedObject,
33 retain_latest_objects_from_checkpoint_batch,
34 },
35 },
36 primary::persist::{EpochToCommit, TransactionObjectChangesToCommit},
37 },
38 insert_or_ignore_into,
39 metrics::IndexerMetrics,
40 models::{
41 checkpoints::{StoredChainIdentifier, StoredCheckpoint, StoredCpTx},
42 display::StoredDisplay,
43 epoch::{StoredEpochInfo, StoredFeatureFlag, StoredProtocolConfig},
44 events::StoredEvent,
45 obj_indices::StoredObjectVersion,
46 objects::{
47 StoredBackwardHistoryObject, StoredCheckpointedObject, StoredDeletedObject,
48 StoredObject, StoredObjects,
49 },
50 packages::StoredPackage,
51 transactions::{OptimisticTransaction, StoredTransaction, TxGlobalOrder},
52 tx_indices::TxIndexSplit,
53 watermarks::StoredWatermark,
54 },
55 on_conflict_do_update, on_conflict_do_update_with_condition, persist_chunk_into_table,
56 persist_chunk_into_table_in_existing_connection,
57 pruning::pruner::PrunableTable,
58 read_only_blocking, run_query_with_retry,
59 schema::{
60 chain_identifier, checkpointed_objects, checkpoints, display, epochs, event_emit_module,
61 event_emit_package, event_senders, event_struct_instantiation, event_struct_module,
62 event_struct_name, event_struct_package, events, feature_flags, objects,
63 objects_backward_history, objects_version, optimistic_transactions, packages,
64 protocol_configs, pruner_cp_watermark, transactions, tx_calls_fun, tx_calls_mod,
65 tx_calls_pkg, tx_changed_objects, tx_global_order, tx_input_objects, tx_kinds,
66 tx_recipients, tx_senders, tx_wrapped_or_deleted_objects, watermarks,
67 },
68 store::{IndexerStore, diesel_macro},
69 transactional_blocking_with_retry,
70 types::{
71 EventIndex, IndexedCheckpoint, IndexedDeletedObject, IndexedEvent, IndexedObject,
72 IndexedTransaction, TxIndex,
73 },
74};
75
76struct EpochLowerBounds {
78 first_checkpoint_id: u64,
79 first_tx_sequence_number: u64,
80 first_optimistic_sequence_number: u64,
81}
82
83#[macro_export]
84macro_rules! chunk {
85 ($data: expr, $size: expr) => {{
86 $data
87 .into_iter()
88 .chunks($size)
89 .into_iter()
90 .map(|c| c.collect())
91 .collect::<Vec<Vec<_>>>()
92 }};
93}
94
95macro_rules! prune_tx_or_event_indice_table {
96 ($table:ident, $conn:expr, $min_tx:expr, $max_tx:expr, $context_msg:expr) => {
97 diesel::delete($table::table.filter($table::tx_sequence_number.between($min_tx, $max_tx)))
98 .execute($conn)
99 .map_err(IndexerError::from)
100 .context($context_msg)?;
101 };
102}
103
104const PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX: usize = 1000;
109const PG_COMMIT_PARALLEL_CHUNK_SIZE: usize = 100;
111const PG_COMMIT_OBJECTS_PARALLEL_CHUNK_SIZE: usize = 500;
115const PG_DB_COMMIT_SLEEP_DURATION: Duration = Duration::from_secs(3600);
116
117#[derive(Clone)]
118pub struct PgIndexerStoreConfig {
119 pub parallel_chunk_size: usize,
120 pub parallel_objects_chunk_size: usize,
121}
122
123pub struct PgIndexerStore {
124 blocking_cp: ConnectionPool,
125 metrics: IndexerMetrics,
126 partition_manager: PgPartitionManager,
127 pub(crate) config: PgIndexerStoreConfig,
128}
129
130impl Clone for PgIndexerStore {
131 fn clone(&self) -> PgIndexerStore {
132 Self {
133 blocking_cp: self.blocking_cp.clone(),
134 metrics: self.metrics.clone(),
135 partition_manager: self.partition_manager.clone(),
136 config: self.config.clone(),
137 }
138 }
139}
140
141impl PgIndexerStore {
142 pub fn new(blocking_cp: ConnectionPool, metrics: IndexerMetrics) -> Self {
143 let parallel_chunk_size = std::env::var("PG_COMMIT_PARALLEL_CHUNK_SIZE")
144 .unwrap_or_else(|_e| PG_COMMIT_PARALLEL_CHUNK_SIZE.to_string())
145 .parse::<usize>()
146 .unwrap();
147 let parallel_objects_chunk_size = std::env::var("PG_COMMIT_OBJECTS_PARALLEL_CHUNK_SIZE")
148 .unwrap_or_else(|_e| PG_COMMIT_OBJECTS_PARALLEL_CHUNK_SIZE.to_string())
149 .parse::<usize>()
150 .unwrap();
151 let partition_manager = PgPartitionManager::new(blocking_cp.clone());
152 let config = PgIndexerStoreConfig {
153 parallel_chunk_size,
154 parallel_objects_chunk_size,
155 };
156
157 Self {
158 blocking_cp,
159 metrics,
160 partition_manager,
161 config,
162 }
163 }
164
165 pub fn get_metrics(&self) -> IndexerMetrics {
166 self.metrics.clone()
167 }
168
169 pub fn blocking_cp(&self) -> ConnectionPool {
170 self.blocking_cp.clone()
171 }
172
173 pub fn get_protocol_version_index_range(&self) -> Result<(i64, i64), IndexerError> {
175 let start = read_only_blocking!(&self.blocking_cp, |conn| {
178 protocol_configs::dsl::protocol_configs
179 .select(max(protocol_configs::protocol_version))
180 .first::<Option<i64>>(conn)
181 })
182 .context("Failed reading latest protocol version from PostgresDB")?
183 .map_or(1, |v| v + 1);
184
185 let end = read_only_blocking!(&self.blocking_cp, |conn| {
188 epochs::dsl::epochs
189 .select(max(epochs::protocol_version))
190 .first::<Option<i64>>(conn)
191 })
192 .context("Failed reading latest epoch protocol version from PostgresDB")?
193 .unwrap_or(1);
194 Ok((start, end))
195 }
196
197 pub fn get_chain_identifier(&self) -> Result<Option<Vec<u8>>, IndexerError> {
198 read_only_blocking!(&self.blocking_cp, |conn| {
199 chain_identifier::dsl::chain_identifier
200 .select(chain_identifier::checkpoint_digest)
201 .first::<Vec<u8>>(conn)
202 .optional()
203 })
204 .context("Failed reading chain id from PostgresDB")
205 }
206
207 fn get_latest_checkpoint_sequence_number(&self) -> Result<Option<u64>, IndexerError> {
208 read_only_blocking!(&self.blocking_cp, |conn| {
209 checkpoints::dsl::checkpoints
210 .select(max(checkpoints::sequence_number))
211 .first::<Option<i64>>(conn)
212 .map(|v| v.map(|v| v as u64))
213 })
214 .context("Failed reading latest checkpoint sequence number from PostgresDB")
215 }
216
217 fn get_available_checkpoint_range(&self) -> Result<(u64, u64), IndexerError> {
218 read_only_blocking!(&self.blocking_cp, |conn| {
219 checkpoints::dsl::checkpoints
220 .select((
221 min(checkpoints::sequence_number),
222 max(checkpoints::sequence_number),
223 ))
224 .first::<(Option<i64>, Option<i64>)>(conn)
225 .map(|(min, max)| {
226 (
227 min.unwrap_or_default() as u64,
228 max.unwrap_or_default() as u64,
229 )
230 })
231 })
232 .context("Failed reading min and max checkpoint sequence numbers from PostgresDB")
233 }
234
235 fn get_prunable_epoch_range(&self) -> Result<(u64, u64), IndexerError> {
236 read_only_blocking!(&self.blocking_cp, |conn| {
237 epochs::dsl::epochs
238 .select((min(epochs::epoch), max(epochs::epoch)))
239 .first::<(Option<i64>, Option<i64>)>(conn)
240 .map(|(min, max)| {
241 (
242 min.unwrap_or_default() as u64,
243 max.unwrap_or_default() as u64,
244 )
245 })
246 })
247 .context("Failed reading min and max epoch numbers from PostgresDB")
248 }
249
250 fn persist_displays_chunk(&self, displays: Vec<StoredDisplay>) -> Result<(), IndexerError> {
251 transactional_blocking_with_retry!(
252 &self.blocking_cp,
253 |conn| self.persist_displays_chunk_in_existing_transaction(conn, &displays),
254 PG_DB_COMMIT_SLEEP_DURATION
255 )?;
256
257 Ok(())
258 }
259
260 pub(crate) fn persist_live_objects(
261 &self,
262 objects: Vec<LiveObject>,
263 ) -> Result<(), IndexerError> {
264 let guard = self
265 .metrics
266 .checkpoint_db_commit_latency_objects_chunks
267 .start_timer();
268 let len = objects.len();
269 let raw_query = r#"
270 INSERT INTO objects (
271 object_id,
272 object_version,
273 object_digest,
274 owner_type,
275 owner_id,
276 object_type,
277 object_type_package,
278 object_type_module,
279 object_type_name,
280 serialized_object,
281 coin_type,
282 coin_balance,
283 df_kind,
284 finalized_in_cp
285 )
286 SELECT
287 u.object_id,
288 u.object_version,
289 u.object_digest,
290 u.owner_type,
291 u.owner_id,
292 u.object_type,
293 u.object_type_package,
294 u.object_type_module,
295 u.object_type_name,
296 u.serialized_object,
297 u.coin_type,
298 u.coin_balance,
299 u.df_kind,
300 u.finalized_in_cp
301 FROM UNNEST(
302 $1::BYTEA[],
303 $2::BIGINT[],
304 $3::BYTEA[],
305 $4::SMALLINT[],
306 $5::BYTEA[],
307 $6::TEXT[],
308 $7::BYTEA[],
309 $8::TEXT[],
310 $9::TEXT[],
311 $10::BYTEA[],
312 $11::TEXT[],
313 $12::BIGINT[],
314 $13::SMALLINT[],
315 $14::BYTEA[],
316 $15::BIGINT[]
317 ) AS u(object_id, object_version, object_digest, owner_type, owner_id, object_type, object_type_package, object_type_module, object_type_name, serialized_object, coin_type, coin_balance, df_kind, tx_digest, finalized_in_cp)
318 LEFT JOIN tx_global_order o ON o.tx_digest = u.tx_digest
319 WHERE o.optimistic_sequence_number IS NULL OR o.optimistic_sequence_number = -1
320 ON CONFLICT (object_id) DO UPDATE
321 SET
322 object_version = EXCLUDED.object_version,
323 object_digest = EXCLUDED.object_digest,
324 owner_type = EXCLUDED.owner_type,
325 owner_id = EXCLUDED.owner_id,
326 object_type = EXCLUDED.object_type,
327 object_type_package = EXCLUDED.object_type_package,
328 object_type_module = EXCLUDED.object_type_module,
329 object_type_name = EXCLUDED.object_type_name,
330 serialized_object = EXCLUDED.serialized_object,
331 coin_type = EXCLUDED.coin_type,
332 coin_balance = EXCLUDED.coin_balance,
333 df_kind = EXCLUDED.df_kind,
334 finalized_in_cp = EXCLUDED.finalized_in_cp
335 WHERE EXCLUDED.object_version > objects.object_version
336 "#;
337 let (objects, tx_digests): (StoredObjects, Vec<_>) = objects
338 .into_iter()
339 .map(LiveObject::split)
340 .map(|(indexed_object, tx_digest)| {
341 (
342 StoredObject::from(indexed_object),
343 tx_digest.into_bytes().to_vec(),
344 )
345 })
346 .unzip();
347 let query = diesel::sql_query(raw_query)
348 .bind::<Array<Bytea>, _>(objects.object_ids)
349 .bind::<Array<BigInt>, _>(objects.object_versions)
350 .bind::<Array<Bytea>, _>(objects.object_digests)
351 .bind::<Array<SmallInt>, _>(objects.owner_types)
352 .bind::<Array<Nullable<Bytea>>, _>(objects.owner_ids)
353 .bind::<Array<Nullable<Text>>, _>(objects.object_types)
354 .bind::<Array<Nullable<Bytea>>, _>(objects.object_type_packages)
355 .bind::<Array<Nullable<Text>>, _>(objects.object_type_modules)
356 .bind::<Array<Nullable<Text>>, _>(objects.object_type_names)
357 .bind::<Array<Bytea>, _>(objects.serialized_objects)
358 .bind::<Array<Nullable<Text>>, _>(objects.coin_types)
359 .bind::<Array<Nullable<BigInt>>, _>(objects.coin_balances)
360 .bind::<Array<Nullable<SmallInt>>, _>(objects.df_kinds)
361 .bind::<Array<Bytea>, _>(tx_digests)
362 .bind::<Array<Nullable<BigInt>>, _>(objects.finalized_in_cps);
363 transactional_blocking_with_retry!(
364 &self.blocking_cp,
365 |conn| {
366 query.clone().execute(conn)?;
367 Ok::<(), IndexerError>(())
368 },
369 PG_DB_COMMIT_SLEEP_DURATION
370 )
371 .tap_ok(|_| {
372 let elapsed = guard.stop_and_record();
373 info!(elapsed, "Persisted {len} chunked objects");
374 })
375 .tap_err(|e| {
376 tracing::error!("failed to persist object mutations with error: {e}");
377 })
378 }
379
380 fn persist_removed_objects(&self, objects: Vec<RemovedObject>) -> Result<(), IndexerError> {
381 let guard = self
382 .metrics
383 .checkpoint_db_commit_latency_objects_chunks
384 .start_timer();
385 let len = objects.len();
386 let raw_query = r#"
387 DELETE FROM objects
388 USING (
389 SELECT u.object_id, u.object_version
390 FROM UNNEST(
391 $1::BYTEA[],
392 $2::BIGINT[],
393 $3::BYTEA[]
394 ) AS u(object_id, object_version, tx_digest)
395 LEFT JOIN tx_global_order o ON o.tx_digest = u.tx_digest
396 WHERE o.optimistic_sequence_number IS NULL OR o.optimistic_sequence_number = -1
397 ) AS to_delete
398 WHERE objects.object_id = to_delete.object_id
399 AND objects.object_version < to_delete.object_version
400 "#;
401 let (object_ids, versions, tx_digests): (Vec<_>, Vec<_>, Vec<_>) = objects
402 .into_iter()
403 .map(|removed_object| {
404 (
405 removed_object.object_id().as_bytes().to_vec(),
406 removed_object.version() as i64,
407 removed_object.transaction_digest.into_bytes().to_vec(),
408 )
409 })
410 .multiunzip();
411 let query = diesel::sql_query(raw_query)
412 .bind::<Array<Bytea>, _>(object_ids)
413 .bind::<Array<BigInt>, _>(versions)
414 .bind::<Array<Bytea>, _>(tx_digests);
415 transactional_blocking_with_retry!(
416 &self.blocking_cp,
417 |conn| {
418 query.clone().execute(conn)?;
419 Ok::<(), IndexerError>(())
420 },
421 PG_DB_COMMIT_SLEEP_DURATION
422 )
423 .tap_ok(|_| {
424 let elapsed = guard.stop_and_record();
425 info!(elapsed, "Deleted {len} chunked objects");
426 })
427 .tap_err(|e| {
428 tracing::error!("failed to persist object deletions with error: {e}");
429 })
430 }
431
432 pub(crate) fn persist_checkpointed_objects_chunk(
433 &self,
434 objects: Vec<StoredCheckpointedObject>,
435 ) -> Result<(), IndexerError> {
436 use diesel::upsert::excluded;
437
438 let len = objects.len();
439 transactional_blocking_with_retry!(
440 &self.blocking_cp,
441 |conn| {
442 for chunk in objects.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
443 on_conflict_do_update!(
444 checkpointed_objects::table,
445 chunk,
446 checkpointed_objects::object_id,
447 (
448 checkpointed_objects::object_version
449 .eq(excluded(checkpointed_objects::object_version)),
450 checkpointed_objects::object_status
451 .eq(excluded(checkpointed_objects::object_status)),
452 checkpointed_objects::object_digest
453 .eq(excluded(checkpointed_objects::object_digest)),
454 checkpointed_objects::checkpoint_sequence_number
455 .eq(excluded(checkpointed_objects::checkpoint_sequence_number)),
456 checkpointed_objects::owner_type
457 .eq(excluded(checkpointed_objects::owner_type)),
458 checkpointed_objects::owner_id
459 .eq(excluded(checkpointed_objects::owner_id)),
460 checkpointed_objects::object_type
461 .eq(excluded(checkpointed_objects::object_type)),
462 checkpointed_objects::object_type_package
463 .eq(excluded(checkpointed_objects::object_type_package)),
464 checkpointed_objects::object_type_module
465 .eq(excluded(checkpointed_objects::object_type_module)),
466 checkpointed_objects::object_type_name
467 .eq(excluded(checkpointed_objects::object_type_name)),
468 checkpointed_objects::serialized_object
469 .eq(excluded(checkpointed_objects::serialized_object)),
470 checkpointed_objects::coin_type
471 .eq(excluded(checkpointed_objects::coin_type)),
472 checkpointed_objects::coin_balance
473 .eq(excluded(checkpointed_objects::coin_balance)),
474 checkpointed_objects::df_kind
475 .eq(excluded(checkpointed_objects::df_kind)),
476 ),
477 conn
478 );
479 }
480 Ok::<(), IndexerError>(())
481 },
482 PG_DB_COMMIT_SLEEP_DURATION
483 )
484 .tap_ok(|_| {
485 info!("Persisted {len} checkpointed objects");
486 })
487 .tap_err(|e| {
488 tracing::error!("failed to persist checkpointed objects: {e}");
489 })
490 }
491
492 fn persist_object_mutation_chunk_in_existing_transaction(
493 &self,
494 conn: &mut PgConnection,
495 mutated_object_mutation_chunk: Vec<StoredObject>,
496 ) -> Result<(), IndexerError> {
497 on_conflict_do_update_with_condition!(
498 objects::table,
499 mutated_object_mutation_chunk,
500 objects::object_id,
501 (
502 objects::object_id.eq(excluded(objects::object_id)),
503 objects::object_version.eq(excluded(objects::object_version)),
504 objects::object_digest.eq(excluded(objects::object_digest)),
505 objects::owner_type.eq(excluded(objects::owner_type)),
506 objects::owner_id.eq(excluded(objects::owner_id)),
507 objects::object_type.eq(excluded(objects::object_type)),
508 objects::serialized_object.eq(excluded(objects::serialized_object)),
509 objects::coin_type.eq(excluded(objects::coin_type)),
510 objects::coin_balance.eq(excluded(objects::coin_balance)),
511 objects::df_kind.eq(excluded(objects::df_kind)),
512 objects::finalized_in_cp.eq(excluded(objects::finalized_in_cp)),
513 ),
514 excluded(objects::object_version).gt(objects::object_version),
515 conn
516 );
517 Ok::<(), IndexerError>(())
518 }
519
520 fn persist_object_deletion_chunk_in_existing_transaction(
521 &self,
522 conn: &mut PgConnection,
523 deleted_objects_chunk: Vec<StoredDeletedObject>,
524 ) -> Result<(), IndexerError> {
525 let (object_ids, object_versions): (Vec<_>, Vec<_>) = deleted_objects_chunk
526 .iter()
527 .map(|o| (o.object_id.clone(), o.object_version))
528 .unzip();
529 let raw_query = r#"
530 DELETE FROM objects
531 USING UNNEST($1::BYTEA[], $2::BIGINT[]) AS to_delete(object_id, object_version)
532 WHERE objects.object_id = to_delete.object_id
533 AND objects.object_version < to_delete.object_version
534 "#;
535 diesel::sql_query(raw_query)
536 .bind::<Array<Bytea>, _>(object_ids)
537 .bind::<Array<BigInt>, _>(object_versions)
538 .execute(conn)
539 .map_err(IndexerError::from)
540 .context("Failed to write object deletion to PostgresDB")?;
541 Ok::<(), IndexerError>(())
542 }
543
544 fn persist_objects_backward_history_chunk(
545 &self,
546 stored: Vec<StoredBackwardHistoryObject>,
547 ) -> Result<(), IndexerError> {
548 persist_chunk_into_table!(objects_backward_history::table, stored, &self.blocking_cp)
549 }
550
551 fn persist_object_version_chunk(
552 &self,
553 object_versions: Vec<StoredObjectVersion>,
554 ) -> Result<(), IndexerError> {
555 let guard = self
556 .metrics
557 .checkpoint_db_commit_latency_objects_version_chunks
558 .start_timer();
559
560 transactional_blocking_with_retry!(
561 &self.blocking_cp,
562 |conn| {
563 for object_version_chunk in object_versions.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX)
564 {
565 insert_or_ignore_into!(objects_version::table, object_version_chunk, conn);
566 }
567 Ok::<(), IndexerError>(())
568 },
569 PG_DB_COMMIT_SLEEP_DURATION
570 )
571 .tap_ok(|_| {
572 let elapsed = guard.stop_and_record();
573 info!(
574 elapsed,
575 "Persisted {} chunked object versions",
576 object_versions.len(),
577 );
578 })
579 .tap_err(|e| {
580 tracing::error!("failed to persist object versions with error: {e}");
581 })
582 }
583
584 pub(crate) fn persist_chain_identifier(
585 &self,
586 chain_identifier: StoredChainIdentifier,
587 ) -> Result<(), IndexerError> {
588 transactional_blocking_with_retry!(
589 &self.blocking_cp,
590 |conn| {
591 insert_or_ignore_into!(chain_identifier::table, &chain_identifier, conn);
592 Ok::<(), IndexerError>(())
593 },
594 PG_DB_COMMIT_SLEEP_DURATION
595 )
596 }
597
598 fn persist_checkpoints(&self, checkpoints: Vec<IndexedCheckpoint>) -> Result<(), IndexerError> {
599 let Some(first_checkpoint) = checkpoints.first() else {
600 return Ok(());
601 };
602
603 if first_checkpoint.sequence_number == 0 {
606 let checkpoint_digest = first_checkpoint.checkpoint_digest.into_bytes().to_vec();
607 self.persist_protocol_configs_and_feature_flags(checkpoint_digest.clone())?;
608 self.persist_chain_identifier(StoredChainIdentifier { checkpoint_digest })?;
609 }
610 let guard = self
611 .metrics
612 .checkpoint_db_commit_latency_checkpoints
613 .start_timer();
614
615 let stored_cp_txs = checkpoints.iter().map(StoredCpTx::from).collect::<Vec<_>>();
616 transactional_blocking_with_retry!(
617 &self.blocking_cp,
618 |conn| {
619 for stored_cp_tx_chunk in stored_cp_txs.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
620 insert_or_ignore_into!(pruner_cp_watermark::table, stored_cp_tx_chunk, conn);
621 }
622 Ok::<(), IndexerError>(())
623 },
624 PG_DB_COMMIT_SLEEP_DURATION
625 )
626 .tap_ok(|_| {
627 info!(
628 "Persisted {} pruner_cp_watermark rows.",
629 stored_cp_txs.len(),
630 );
631 })
632 .tap_err(|e| {
633 tracing::error!("failed to persist pruner_cp_watermark with error: {e}");
634 })?;
635
636 let stored_checkpoints = checkpoints
637 .iter()
638 .map(StoredCheckpoint::from)
639 .collect::<Vec<_>>();
640 transactional_blocking_with_retry!(
641 &self.blocking_cp,
642 |conn| {
643 for stored_checkpoint_chunk in
644 stored_checkpoints.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX)
645 {
646 insert_or_ignore_into!(checkpoints::table, stored_checkpoint_chunk, conn);
647 let time_now_ms = chrono::Utc::now().timestamp_millis();
648 for stored_checkpoint in stored_checkpoint_chunk {
649 self.metrics
650 .db_commit_lag_ms
651 .set(time_now_ms - stored_checkpoint.timestamp_ms);
652 self.metrics.max_committed_checkpoint_sequence_number.set(
653 stored_checkpoint.sequence_number,
654 );
655 self.metrics.committed_checkpoint_timestamp_ms.set(
656 stored_checkpoint.timestamp_ms,
657 );
658 }
659 for stored_checkpoint in stored_checkpoint_chunk {
660 info!("Indexer lag: persisted checkpoint {} with time now {} and checkpoint time {}", stored_checkpoint.sequence_number, time_now_ms, stored_checkpoint.timestamp_ms);
661 }
662 }
663 Ok::<(), IndexerError>(())
664 },
665 PG_DB_COMMIT_SLEEP_DURATION
666 )
667 .tap_ok(|_| {
668 let elapsed = guard.stop_and_record();
669 info!(
670 elapsed,
671 "Persisted {} checkpoints",
672 stored_checkpoints.len()
673 );
674 })
675 .tap_err(|e| {
676 tracing::error!("failed to persist checkpoints with error: {e}");
677 })
678 }
679
680 fn persist_transactions_chunk(
681 &self,
682 transactions: Vec<IndexedTransaction>,
683 ) -> Result<(), IndexerError> {
684 let guard = self
685 .metrics
686 .checkpoint_db_commit_latency_transactions_chunks
687 .start_timer();
688 let transformation_guard = self
689 .metrics
690 .checkpoint_db_commit_latency_transactions_chunks_transformation
691 .start_timer();
692 let transactions = transactions
693 .iter()
694 .map(StoredTransaction::from)
695 .collect::<Vec<_>>();
696 drop(transformation_guard);
697
698 transactional_blocking_with_retry!(
699 &self.blocking_cp,
700 |conn| {
701 for transaction_chunk in transactions.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
702 insert_or_ignore_into!(transactions::table, transaction_chunk, conn);
703 }
704 Ok::<(), IndexerError>(())
705 },
706 PG_DB_COMMIT_SLEEP_DURATION
707 )
708 .tap_ok(|_| {
709 let elapsed = guard.stop_and_record();
710 info!(
711 elapsed,
712 "Persisted {} chunked transactions",
713 transactions.len()
714 );
715 })
716 .tap_err(|e| {
717 tracing::error!("failed to persist transactions with error: {e}");
718 })
719 }
720
721 fn persist_tx_global_order_chunk(
722 &self,
723 tx_order: Vec<TxGlobalOrder>,
724 ) -> Result<(), IndexerError> {
725 let guard = self
726 .metrics
727 .checkpoint_db_commit_latency_tx_insertion_order_chunks
728 .start_timer();
729
730 transactional_blocking_with_retry!(
731 &self.blocking_cp,
732 |conn| {
733 for tx_order_chunk in tx_order.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
734 on_conflict_do_update_with_condition!(
739 tx_global_order::table,
740 tx_order_chunk,
741 tx_global_order::tx_digest,
742 tx_global_order::tx_sequence_number
743 .eq(excluded(tx_global_order::tx_sequence_number)),
744 tx_global_order::tx_sequence_number.is_null(),
745 conn
746 );
747 }
748 Ok::<(), IndexerError>(())
749 },
750 PG_DB_COMMIT_SLEEP_DURATION
751 )
752 .tap_ok(|_| {
753 let elapsed = guard.stop_and_record();
754 info!(
755 elapsed,
756 "Persisted {} chunked txs insertion order",
757 tx_order.len()
758 );
759 })
760 .tap_err(|e| {
761 tracing::error!("failed to persist txs insertion order with error: {e}");
762 })
763 }
764
765 fn persist_events_chunk(&self, events: Vec<IndexedEvent>) -> Result<(), IndexerError> {
766 let guard = self
767 .metrics
768 .checkpoint_db_commit_latency_events_chunks
769 .start_timer();
770 let len = events.len();
771 let events = events
772 .into_iter()
773 .map(StoredEvent::from)
774 .collect::<Vec<_>>();
775
776 transactional_blocking_with_retry!(
777 &self.blocking_cp,
778 |conn| {
779 for event_chunk in events.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
780 insert_or_ignore_into!(events::table, event_chunk, conn);
781 }
782 Ok::<(), IndexerError>(())
783 },
784 PG_DB_COMMIT_SLEEP_DURATION
785 )
786 .tap_ok(|_| {
787 let elapsed = guard.stop_and_record();
788 info!(elapsed, "Persisted {} chunked events", len);
789 })
790 .tap_err(|e| {
791 tracing::error!("failed to persist events with error: {e}");
792 })
793 }
794
795 async fn persist_packages_in_chunks(
796 &self,
797 packages: Vec<StoredPackage>,
798 ) -> Result<(), IndexerError> {
799 let chunks = chunk!(packages, self.config.parallel_objects_chunk_size);
800 let persist_tasks = chunks
801 .into_iter()
802 .map(|c| self.spawn_blocking_task(move |this| this.persist_packages(&c)));
803 futures::future::try_join_all(persist_tasks)
804 .await
805 .inspect_err(|e| tracing::error!("failed to join persist_packages futures: {e}"))?
806 .into_iter()
807 .collect()
808 }
809
810 fn persist_packages(&self, packages: &[StoredPackage]) -> Result<(), IndexerError> {
811 transactional_blocking_with_retry!(
812 &self.blocking_cp,
813 |conn| {
814 for packages_chunk in packages.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
815 on_conflict_do_update!(
816 packages::table,
817 packages_chunk,
818 packages::package_id,
819 (
820 packages::package_id.eq(excluded(packages::package_id)),
821 packages::move_package.eq(excluded(packages::move_package)),
822 ),
823 conn
824 );
825 }
826 Ok::<(), IndexerError>(())
827 },
828 PG_DB_COMMIT_SLEEP_DURATION
829 )
830 }
831
832 async fn persist_event_indices_chunk(
833 &self,
834 indices: Vec<EventIndex>,
835 ) -> Result<(), IndexerError> {
836 let guard = self
837 .metrics
838 .checkpoint_db_commit_latency_event_indices_chunks
839 .start_timer();
840 let len = indices.len();
841 let (
842 event_emit_packages,
843 event_emit_modules,
844 event_senders,
845 event_struct_packages,
846 event_struct_modules,
847 event_struct_names,
848 event_struct_instantiations,
849 ) = indices.into_iter().map(|i| i.split()).fold(
850 (
851 Vec::new(),
852 Vec::new(),
853 Vec::new(),
854 Vec::new(),
855 Vec::new(),
856 Vec::new(),
857 Vec::new(),
858 ),
859 |(
860 mut event_emit_packages,
861 mut event_emit_modules,
862 mut event_senders,
863 mut event_struct_packages,
864 mut event_struct_modules,
865 mut event_struct_names,
866 mut event_struct_instantiations,
867 ),
868 index| {
869 event_emit_packages.push(index.0);
870 event_emit_modules.push(index.1);
871 event_senders.push(index.2);
872 event_struct_packages.push(index.3);
873 event_struct_modules.push(index.4);
874 event_struct_names.push(index.5);
875 event_struct_instantiations.push(index.6);
876 (
877 event_emit_packages,
878 event_emit_modules,
879 event_senders,
880 event_struct_packages,
881 event_struct_modules,
882 event_struct_names,
883 event_struct_instantiations,
884 )
885 },
886 );
887
888 let mut futures = vec![];
890 futures.push(self.spawn_blocking_task(move |this| {
891 persist_chunk_into_table!(
892 event_emit_package::table,
893 event_emit_packages,
894 &this.blocking_cp
895 )
896 }));
897
898 futures.push(self.spawn_blocking_task(move |this| {
899 persist_chunk_into_table!(
900 event_emit_module::table,
901 event_emit_modules,
902 &this.blocking_cp
903 )
904 }));
905
906 futures.push(self.spawn_blocking_task(move |this| {
907 persist_chunk_into_table!(event_senders::table, event_senders, &this.blocking_cp)
908 }));
909
910 futures.push(self.spawn_blocking_task(move |this| {
911 persist_chunk_into_table!(
912 event_struct_package::table,
913 event_struct_packages,
914 &this.blocking_cp
915 )
916 }));
917
918 futures.push(self.spawn_blocking_task(move |this| {
919 persist_chunk_into_table!(
920 event_struct_module::table,
921 event_struct_modules,
922 &this.blocking_cp
923 )
924 }));
925
926 futures.push(self.spawn_blocking_task(move |this| {
927 persist_chunk_into_table!(
928 event_struct_name::table,
929 event_struct_names,
930 &this.blocking_cp
931 )
932 }));
933
934 futures.push(self.spawn_blocking_task(move |this| {
935 persist_chunk_into_table!(
936 event_struct_instantiation::table,
937 event_struct_instantiations,
938 &this.blocking_cp
939 )
940 }));
941
942 futures::future::try_join_all(futures)
943 .await
944 .map_err(|e| {
945 tracing::error!("failed to join event indices futures in a chunk: {e}");
946 IndexerError::from(e)
947 })?
948 .into_iter()
949 .collect::<Result<Vec<_>, _>>()
950 .map_err(|e| {
951 tracing::error!("failed to persist all event indices in a chunk: {e:?}");
952 IndexerError::PostgresWrite
953 })?;
954 let elapsed = guard.stop_and_record();
955 info!(elapsed, "Persisted {} chunked event indices", len);
956 Ok(())
957 }
958
959 async fn persist_tx_indices_chunk_v2(&self, indices: Vec<TxIndex>) -> Result<(), IndexerError> {
960 let guard = self
961 .metrics
962 .checkpoint_db_commit_latency_tx_indices_chunks
963 .start_timer();
964 let len = indices.len();
965
966 let splits: Vec<TxIndexSplit> = indices.into_iter().map(Into::into).collect();
967
968 let senders: Vec<_> = splits.iter().flat_map(|ix| ix.tx_senders.clone()).collect();
969 let recipients: Vec<_> = splits
970 .iter()
971 .flat_map(|ix| ix.tx_recipients.clone())
972 .collect();
973 let input_objects: Vec<_> = splits
974 .iter()
975 .flat_map(|ix| ix.tx_input_objects.clone())
976 .collect();
977 let changed_objects: Vec<_> = splits
978 .iter()
979 .flat_map(|ix| ix.tx_changed_objects.clone())
980 .collect();
981 let wrapped_or_deleted_objects: Vec<_> = splits
982 .iter()
983 .flat_map(|ix| ix.tx_wrapped_or_deleted_objects.clone())
984 .collect();
985 let pkgs: Vec<_> = splits.iter().flat_map(|ix| ix.tx_pkgs.clone()).collect();
986 let mods: Vec<_> = splits.iter().flat_map(|ix| ix.tx_mods.clone()).collect();
987 let funs: Vec<_> = splits.iter().flat_map(|ix| ix.tx_funs.clone()).collect();
988 let kinds: Vec<_> = splits.iter().flat_map(|ix| ix.tx_kinds.clone()).collect();
989
990 let futures = [
991 self.spawn_blocking_task(move |this| {
992 persist_chunk_into_table!(tx_senders::table, senders, &this.blocking_cp)
993 }),
994 self.spawn_blocking_task(move |this| {
995 persist_chunk_into_table!(tx_recipients::table, recipients, &this.blocking_cp)
996 }),
997 self.spawn_blocking_task(move |this| {
998 persist_chunk_into_table!(tx_input_objects::table, input_objects, &this.blocking_cp)
999 }),
1000 self.spawn_blocking_task(move |this| {
1001 persist_chunk_into_table!(
1002 tx_changed_objects::table,
1003 changed_objects,
1004 &this.blocking_cp
1005 )
1006 }),
1007 self.spawn_blocking_task(move |this| {
1008 persist_chunk_into_table!(
1009 tx_wrapped_or_deleted_objects::table,
1010 wrapped_or_deleted_objects,
1011 &this.blocking_cp
1012 )
1013 }),
1014 self.spawn_blocking_task(move |this| {
1015 persist_chunk_into_table!(tx_calls_pkg::table, pkgs, &this.blocking_cp)
1016 }),
1017 self.spawn_blocking_task(move |this| {
1018 persist_chunk_into_table!(tx_calls_mod::table, mods, &this.blocking_cp)
1019 }),
1020 self.spawn_blocking_task(move |this| {
1021 persist_chunk_into_table!(tx_calls_fun::table, funs, &this.blocking_cp)
1022 }),
1023 self.spawn_blocking_task(move |this| {
1024 persist_chunk_into_table!(tx_kinds::table, kinds, &this.blocking_cp)
1025 }),
1026 ];
1027
1028 futures::future::try_join_all(futures)
1029 .await
1030 .map_err(|e| {
1031 tracing::error!("failed to join tx indices futures in a chunk: {e}");
1032 IndexerError::from(e)
1033 })?
1034 .into_iter()
1035 .collect::<Result<Vec<_>, _>>()
1036 .map_err(|e| {
1037 tracing::error!("failed to persist all tx indices in a chunk: {e:?}");
1038 IndexerError::PostgresWrite
1039 })?;
1040 let elapsed = guard.stop_and_record();
1041 info!(elapsed, "Persisted {} chunked tx_indices", len);
1042 Ok(())
1043 }
1044
1045 pub(crate) fn persist_epochs(&self, epochs: Vec<EpochToCommit>) -> Result<(), IndexerError> {
1046 transactional_blocking_with_retry!(
1047 &self.blocking_cp,
1048 |conn| {
1049 for epoch in &epochs {
1050 if let Some(last_epoch) = &epoch.last_epoch {
1051 info!(last_epoch.epoch, "Persisting epoch end data.");
1052 diesel::update(epochs::table.filter(epochs::epoch.eq(last_epoch.epoch)))
1053 .set(last_epoch)
1054 .execute(conn)?;
1055 }
1056
1057 info!(epoch.new_epoch.epoch, "Persisting epoch beginning info");
1058 insert_or_ignore_into!(epochs::table, &epoch.new_epoch, conn);
1059 }
1060 Ok::<(), IndexerError>(())
1061 },
1062 PG_DB_COMMIT_SLEEP_DURATION
1063 )
1064 }
1065
1066 fn persist_epoch(&self, epoch: EpochToCommit) -> Result<(), IndexerError> {
1067 let guard = self
1068 .metrics
1069 .checkpoint_db_commit_latency_epoch
1070 .start_timer();
1071 let epoch_id = epoch.new_epoch.epoch;
1072
1073 self.persist_epochs(vec![epoch])
1074 .tap_ok(|_| {
1075 let elapsed = guard.stop_and_record();
1076 info!(elapsed, epoch_id, "Persisted epoch beginning info");
1077 })
1078 .tap_err(|e| {
1079 tracing::error!("failed to persist epoch with error: {e}");
1080 })
1081 }
1082
1083 fn advance_epoch(&self, epoch_to_commit: EpochToCommit) -> Result<(), IndexerError> {
1084 let last_epoch_id = epoch_to_commit.last_epoch.as_ref().map(|e| e.epoch);
1085 if let Some(last_epoch_id) = last_epoch_id {
1087 let last_db_epoch: Option<StoredEpochInfo> =
1088 read_only_blocking!(&self.blocking_cp, |conn| {
1089 epochs::table
1090 .filter(epochs::epoch.eq(last_epoch_id))
1091 .first::<StoredEpochInfo>(conn)
1092 .optional()
1093 })
1094 .context("Failed to read last epoch from PostgresDB")?;
1095 if let Some(last_epoch) = last_db_epoch {
1096 let epoch_partition_data =
1097 EpochPartitionData::compose_data(epoch_to_commit, last_epoch);
1098 let table_partitions = self.partition_manager.get_table_partitions()?;
1099 for (table, (_, last_partition)) in table_partitions {
1100 if !self
1103 .partition_manager
1104 .get_strategy(&table)
1105 .is_epoch_partitioned()
1106 {
1107 continue;
1108 }
1109 let guard = self.metrics.advance_epoch_latency.start_timer();
1110 self.partition_manager.advance_epoch(
1111 table.clone(),
1112 last_partition,
1113 &epoch_partition_data,
1114 )?;
1115 let elapsed = guard.stop_and_record();
1116 info!(
1117 elapsed,
1118 "Advanced epoch partition {} for table {}",
1119 last_partition,
1120 table.clone()
1121 );
1122 }
1123 } else {
1124 tracing::error!("last epoch: {last_epoch_id} from PostgresDB is None.");
1125 }
1126 }
1127
1128 Ok(())
1129 }
1130
1131 fn prune_checkpoints_table_by_range(
1132 &self,
1133 min_cp: u64,
1134 max_cp: u64,
1135 ) -> Result<(), IndexerError> {
1136 transactional_blocking_with_retry!(
1137 &self.blocking_cp,
1138 |conn| {
1139 diesel::delete(
1140 checkpoints::table
1141 .filter(checkpoints::sequence_number.between(min_cp as i64, max_cp as i64)),
1142 )
1143 .execute(conn)
1144 .map_err(IndexerError::from)
1145 .context("Failed to prune checkpoints table by range")?;
1146
1147 Ok::<(), IndexerError>(())
1148 },
1149 PG_DB_COMMIT_SLEEP_DURATION
1150 )
1151 }
1152
1153 fn prune_tx_global_order(
1156 &self,
1157 conn: &mut PgConnection,
1158 min_tx: i64,
1159 max_tx: i64,
1160 ) -> Result<(), IndexerError> {
1161 diesel::delete(
1162 tx_global_order::table
1163 .filter(tx_global_order::tx_sequence_number.between(min_tx, max_tx)),
1164 )
1165 .execute(conn)
1166 .map_err(IndexerError::from)
1167 .context("Failed to prune tx_global_order table")
1168 .map(|_| ())
1169 }
1170
1171 fn prune_single_tx_or_event_table(
1173 &self,
1174 table: &crate::pruning::pruner::PrunableTable,
1175 min_tx: u64,
1176 max_tx: u64,
1177 ) -> Result<(), IndexerError> {
1178 use crate::pruning::pruner::PrunableTable;
1179
1180 let (min_tx, max_tx) = (min_tx as i64, max_tx as i64);
1181
1182 transactional_blocking_with_retry!(
1183 &self.blocking_cp,
1184 |conn| {
1185 match table {
1186 PrunableTable::EventEmitModule => {
1188 prune_tx_or_event_indice_table!(
1189 event_emit_module,
1190 conn,
1191 min_tx,
1192 max_tx,
1193 "Failed to prune event_emit_module table"
1194 );
1195 }
1196 PrunableTable::EventEmitPackage => {
1197 prune_tx_or_event_indice_table!(
1198 event_emit_package,
1199 conn,
1200 min_tx,
1201 max_tx,
1202 "Failed to prune event_emit_package table"
1203 );
1204 }
1205 PrunableTable::EventSenders => {
1206 prune_tx_or_event_indice_table!(
1207 event_senders,
1208 conn,
1209 min_tx,
1210 max_tx,
1211 "Failed to prune event_senders table"
1212 );
1213 }
1214 PrunableTable::EventStructInstantiation => {
1215 prune_tx_or_event_indice_table!(
1216 event_struct_instantiation,
1217 conn,
1218 min_tx,
1219 max_tx,
1220 "Failed to prune event_struct_instantiation table"
1221 );
1222 }
1223 PrunableTable::EventStructModule => {
1224 prune_tx_or_event_indice_table!(
1225 event_struct_module,
1226 conn,
1227 min_tx,
1228 max_tx,
1229 "Failed to prune event_struct_module table"
1230 );
1231 }
1232 PrunableTable::EventStructName => {
1233 prune_tx_or_event_indice_table!(
1234 event_struct_name,
1235 conn,
1236 min_tx,
1237 max_tx,
1238 "Failed to prune event_struct_name table"
1239 );
1240 }
1241 PrunableTable::EventStructPackage => {
1242 prune_tx_or_event_indice_table!(
1243 event_struct_package,
1244 conn,
1245 min_tx,
1246 max_tx,
1247 "Failed to prune event_struct_package table"
1248 );
1249 }
1250
1251 PrunableTable::TxSenders => {
1253 prune_tx_or_event_indice_table!(
1254 tx_senders,
1255 conn,
1256 min_tx,
1257 max_tx,
1258 "Failed to prune tx_senders table"
1259 );
1260 }
1261 PrunableTable::TxRecipients => {
1262 prune_tx_or_event_indice_table!(
1263 tx_recipients,
1264 conn,
1265 min_tx,
1266 max_tx,
1267 "Failed to prune tx_recipients table"
1268 );
1269 }
1270 PrunableTable::TxInputObjects => {
1271 prune_tx_or_event_indice_table!(
1272 tx_input_objects,
1273 conn,
1274 min_tx,
1275 max_tx,
1276 "Failed to prune tx_input_objects table"
1277 );
1278 }
1279 PrunableTable::TxChangedObjects => {
1280 prune_tx_or_event_indice_table!(
1281 tx_changed_objects,
1282 conn,
1283 min_tx,
1284 max_tx,
1285 "Failed to prune tx_changed_objects table"
1286 );
1287 }
1288 PrunableTable::TxWrappedOrDeletedObjects => {
1289 prune_tx_or_event_indice_table!(
1290 tx_wrapped_or_deleted_objects,
1291 conn,
1292 min_tx,
1293 max_tx,
1294 "Failed to prune tx_wrapped_or_deleted_objects table"
1295 );
1296 }
1297 PrunableTable::TxCallsPkg => {
1298 prune_tx_or_event_indice_table!(
1299 tx_calls_pkg,
1300 conn,
1301 min_tx,
1302 max_tx,
1303 "Failed to prune tx_calls_pkg table"
1304 );
1305 }
1306 PrunableTable::TxCallsMod => {
1307 prune_tx_or_event_indice_table!(
1308 tx_calls_mod,
1309 conn,
1310 min_tx,
1311 max_tx,
1312 "Failed to prune tx_calls_mod table"
1313 );
1314 }
1315 PrunableTable::TxCallsFun => {
1316 prune_tx_or_event_indice_table!(
1317 tx_calls_fun,
1318 conn,
1319 min_tx,
1320 max_tx,
1321 "Failed to prune tx_calls_fun table"
1322 );
1323 }
1324 PrunableTable::TxKinds => {
1325 prune_tx_or_event_indice_table!(
1326 tx_kinds,
1327 conn,
1328 min_tx,
1329 max_tx,
1330 "Failed to prune tx_kinds table"
1331 );
1332 }
1333 PrunableTable::TxGlobalOrder => {
1334 self.prune_tx_global_order(conn, min_tx, max_tx)?;
1335 }
1336 _ => {
1337 return Err(IndexerError::InvalidArgument(format!(
1338 "table {} is not a transaction or event index table",
1339 table.as_ref()
1340 )));
1341 }
1342 }
1343 Ok::<(), IndexerError>(())
1344 },
1345 PG_DB_COMMIT_SLEEP_DURATION
1346 )
1347 }
1348
1349 fn prune_optimistic_tx_by_optimistic_seq(
1352 &self,
1353 start: u64,
1354 end: u64,
1355 ) -> Result<(), IndexerError> {
1356 use diesel::prelude::*;
1357
1358 transactional_blocking_with_retry!(
1359 &self.blocking_cp,
1360 |conn| {
1361 diesel::delete(optimistic_transactions::table.filter(
1362 optimistic_transactions::optimistic_sequence_number
1363 .between(start as i64, end as i64),
1364 ))
1365 .execute(conn)
1366 .map_err(IndexerError::from)
1367 .context(
1368 format!(
1369 "failed to prune optimistic_transactions table by optimistic_sequence_number range [{start}..={end}]"
1370 )
1371 .as_str(),
1372 )
1373 },
1374 PG_DB_COMMIT_SLEEP_DURATION
1375 )?;
1376 Ok(())
1377 }
1378
1379 fn prune_backward_history_by_checkpoint_with_limit(
1380 &self,
1381 start: u64,
1382 end: u64,
1383 limit: i64,
1384 ) -> Result<usize, IndexerError> {
1385 use diesel::prelude::*;
1386
1387 transactional_blocking_with_retry!(
1388 &self.blocking_cp,
1389 |conn| {
1390 let sql = r#"
1391 WITH to_delete AS (
1392 SELECT superseded_at_checkpoint, object_id, object_version
1393 FROM objects_backward_history
1394 WHERE superseded_at_checkpoint BETWEEN $1 AND $2
1395 ORDER BY superseded_at_checkpoint, object_id, object_version
1396 FOR UPDATE
1397 LIMIT $3
1398 )
1399 DELETE FROM objects_backward_history bh
1400 USING to_delete
1401 WHERE (bh.superseded_at_checkpoint, bh.object_id, bh.object_version) =
1402 (to_delete.superseded_at_checkpoint, to_delete.object_id, to_delete.object_version)
1403 "#;
1404 diesel::sql_query(sql)
1405 .bind::<diesel::sql_types::BigInt, _>(start as i64)
1406 .bind::<diesel::sql_types::BigInt, _>(end as i64)
1407 .bind::<diesel::sql_types::BigInt, _>(limit)
1408 .execute(conn)
1409 .map_err(IndexerError::from)
1410 .context(
1411 format!(
1412 "failed to prune objects_backward_history by checkpoint range [{start}..={end}] with limit {limit}"
1413 )
1414 .as_str(),
1415 )
1416 },
1417 PG_DB_COMMIT_SLEEP_DURATION
1418 )
1419 }
1420
1421 fn prune_cp_tx_table_by_range(&self, min_cp: u64, max_cp: u64) -> Result<(), IndexerError> {
1422 transactional_blocking_with_retry!(
1423 &self.blocking_cp,
1424 |conn| {
1425 diesel::delete(
1426 pruner_cp_watermark::table.filter(
1427 pruner_cp_watermark::checkpoint_sequence_number
1428 .between(min_cp as i64, max_cp as i64),
1429 ),
1430 )
1431 .execute(conn)
1432 .map_err(IndexerError::from)
1433 .context("Failed to prune pruner_cp_watermark table by range")?;
1434 Ok::<(), IndexerError>(())
1435 },
1436 PG_DB_COMMIT_SLEEP_DURATION
1437 )
1438 }
1439
1440 fn prune_table_by_checkpoint_range(
1441 &self,
1442 table: &crate::pruning::pruner::PrunableTable,
1443 min_checkpoint: u64,
1444 max_checkpoint: u64,
1445 ) -> Result<(), IndexerError> {
1446 use crate::pruning::pruner::PrunableTable;
1447
1448 match table {
1449 PrunableTable::Checkpoints => {
1450 self.prune_checkpoints_table_by_range(min_checkpoint, max_checkpoint)
1451 }
1452 PrunableTable::PrunerCpWatermark => {
1453 self.prune_cp_tx_table_by_range(min_checkpoint, max_checkpoint)
1454 }
1455 _ => Err(IndexerError::InvalidArgument(format!(
1456 "table {} is not pruned by checkpoint",
1457 table.as_ref()
1458 ))),
1459 }
1460 }
1461
1462 fn get_network_total_transactions_by_end_of_epoch(
1463 &self,
1464 epoch: u64,
1465 ) -> Result<Option<u64>, IndexerError> {
1466 read_only_blocking!(&self.blocking_cp, |conn| {
1467 epochs::table
1468 .filter(epochs::epoch.eq(epoch as i64))
1469 .select(epochs::network_total_transactions)
1470 .get_result::<Option<i64>>(conn)
1471 })
1472 .context(format!("failed to get network total transactions in epoch {epoch}").as_str())
1473 .map(|option| option.map(|v| v as u64))
1474 }
1475
1476 fn refresh_participation_metrics(&self) -> Result<(), IndexerError> {
1477 transactional_blocking_with_retry!(
1478 &self.blocking_cp,
1479 |conn| {
1480 diesel::sql_query("REFRESH MATERIALIZED VIEW participation_metrics")
1481 .execute(conn)?;
1482 Ok::<(), IndexerError>(())
1483 },
1484 PG_DB_COMMIT_SLEEP_DURATION
1485 )
1486 .tap_ok(|_| {
1487 info!("Successfully refreshed participation_metrics");
1488 })
1489 .tap_err(|e| {
1490 tracing::error!("failed to refresh participation_metrics: {e}");
1491 })
1492 }
1493
1494 fn update_watermarks_upper_bound<E: IntoEnumIterator>(
1495 &self,
1496 watermark: CommitterWatermark,
1497 ) -> Result<(), IndexerError>
1498 where
1499 E::Iterator: Iterator<Item: AsRef<str>>,
1500 {
1501 use diesel::query_dsl::methods::FilterDsl;
1502
1503 let guard = self
1504 .metrics
1505 .checkpoint_db_commit_latency_watermarks
1506 .start_timer();
1507
1508 let upper_bound_updates = E::iter()
1509 .map(|table| StoredWatermark::from_upper_bound_update(table.as_ref(), watermark))
1510 .collect::<Vec<_>>();
1511
1512 transactional_blocking_with_retry!(
1513 &self.blocking_cp,
1514 |conn| {
1515 diesel::insert_into(watermarks::table)
1516 .values(&upper_bound_updates)
1517 .on_conflict(watermarks::entity)
1518 .do_update()
1519 .set((
1520 watermarks::current_epoch.eq(excluded(watermarks::current_epoch)),
1521 watermarks::max_committed_cp.eq(excluded(watermarks::max_committed_cp)),
1522 watermarks::max_committed_tx.eq(excluded(watermarks::max_committed_tx)),
1523 ))
1524 .filter(excluded(watermarks::max_committed_cp).ge(watermarks::max_committed_cp))
1525 .filter(excluded(watermarks::max_committed_tx).ge(watermarks::max_committed_tx))
1526 .filter(excluded(watermarks::current_epoch).ge(watermarks::current_epoch))
1527 .execute(conn)
1528 .map_err(IndexerError::from)
1529 .context("Failed to update watermarks upper bound")?;
1530 Ok::<(), IndexerError>(())
1531 },
1532 PG_DB_COMMIT_SLEEP_DURATION
1533 )
1534 .tap_ok(|_| {
1535 let elapsed = guard.stop_and_record();
1536 info!(elapsed, "Persisted watermarks");
1537 })
1538 .tap_err(|e| {
1539 tracing::error!("Failed to persist watermarks with error: {}", e);
1540 })
1541 }
1542
1543 fn map_epochs_to_low_bounds(
1544 &self,
1545 epochs: &[u64],
1546 ) -> Result<HashMap<u64, EpochLowerBounds>, IndexerError> {
1547 let pool = &self.blocking_cp;
1548 let results: Vec<(i64, i64, i64, i64)> = read_only_blocking!(pool, move |conn| {
1549 epochs::table
1550 .filter(epochs::epoch.eq_any(epochs.iter().map(|&e| e as i64)))
1551 .select((
1552 epochs::epoch,
1553 epochs::first_checkpoint_id,
1554 epochs::first_tx_sequence_number,
1555 epochs::first_optimistic_sequence_number,
1556 ))
1557 .load::<(i64, i64, i64, i64)>(conn)
1558 })
1559 .context("Failed to fetch first checkpoint and tx seq num for epochs")?;
1560
1561 Ok(results
1562 .into_iter()
1563 .map(|(epoch, checkpoint, tx, optimistic_seq)| {
1564 (
1565 epoch as u64,
1566 EpochLowerBounds {
1567 first_checkpoint_id: checkpoint as u64,
1568 first_tx_sequence_number: tx as u64,
1569 first_optimistic_sequence_number: optimistic_seq as u64,
1570 },
1571 )
1572 })
1573 .collect())
1574 }
1575
1576 fn update_watermarks_lower_bound<Table: AsRef<str>>(
1577 &self,
1578 watermarks: Vec<(Table, u64)>,
1579 ) -> Result<(), IndexerError> {
1580 use diesel::query_dsl::methods::FilterDsl;
1581
1582 let epochs: Vec<u64> = watermarks.iter().map(|(_table, epoch)| *epoch).collect();
1583 let epoch_mapping = self.map_epochs_to_low_bounds(&epochs)?;
1584 let lookups: Result<Vec<StoredWatermark>, IndexerError> = watermarks
1585 .into_iter()
1586 .map(|(table, epoch)| {
1587 let bounds = epoch_mapping.get(&epoch).ok_or_else(|| {
1588 IndexerError::PersistentStorageDataCorruption(format!(
1589 "epoch {epoch} not found in epoch mapping",
1590 ))
1591 })?;
1592
1593 let min_available_tx =
1596 if table.as_ref() == PrunableTable::OptimisticTransactions.as_ref() {
1597 bounds.first_optimistic_sequence_number
1598 } else {
1599 bounds.first_tx_sequence_number
1600 };
1601
1602 Ok(StoredWatermark::from_lower_bound_update(
1603 table.as_ref(),
1604 epoch,
1605 bounds.first_checkpoint_id,
1606 min_available_tx,
1607 ))
1608 })
1609 .collect();
1610 let lower_bound_updates = lookups?;
1611 let guard = self
1612 .metrics
1613 .checkpoint_db_commit_latency_watermarks
1614 .start_timer();
1615 transactional_blocking_with_retry!(
1616 &self.blocking_cp,
1617 |conn| {
1618 diesel::insert_into(watermarks::table)
1619 .values(&lower_bound_updates)
1620 .on_conflict(watermarks::entity)
1621 .do_update()
1622 .set((
1623 watermarks::min_available_cp.eq(excluded(watermarks::min_available_cp)),
1624 watermarks::min_available_tx.eq(excluded(watermarks::min_available_tx)),
1625 watermarks::min_available_epoch
1626 .eq(excluded(watermarks::min_available_epoch)),
1627 watermarks::min_bounds_updated_at_timestamp_ms.eq(sql::<
1628 diesel::sql_types::BigInt,
1629 >(
1630 "(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::bigint",
1631 )),
1632 ))
1633 .filter(excluded(watermarks::min_available_cp).gt(watermarks::min_available_cp))
1634 .filter(excluded(watermarks::min_available_tx).gt(watermarks::min_available_tx))
1635 .filter(
1636 excluded(watermarks::min_available_epoch)
1637 .gt(watermarks::min_available_epoch),
1638 )
1639 .filter(
1640 diesel::dsl::sql::<diesel::sql_types::BigInt>(
1641 "(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::bigint",
1642 )
1643 .gt(watermarks::min_bounds_updated_at_timestamp_ms),
1644 )
1645 .execute(conn)
1646 },
1647 PG_DB_COMMIT_SLEEP_DURATION
1648 )
1649 .tap_ok(|_| {
1650 let elapsed = guard.stop_and_record();
1651 tracing::info!(elapsed, "Persisted watermarks lower bounds");
1652 })
1653 .tap_err(|e| {
1654 tracing::error!("Failed to persist watermarks with error: {}", e);
1655 })?;
1656 Ok(())
1657 }
1658
1659 fn get_watermarks(&self) -> Result<(Vec<StoredWatermark>, i64), IndexerError> {
1660 run_query_with_retry!(
1663 &self.blocking_cp,
1664 |conn| {
1665 let stored = watermarks::table
1666 .load::<StoredWatermark>(conn)
1667 .map_err(Into::into)
1668 .context("Failed reading watermarks from PostgresDB")?;
1669 let timestamp = diesel::select(diesel::dsl::sql::<diesel::sql_types::BigInt>(
1670 "(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::bigint",
1671 ))
1672 .get_result(conn)
1673 .map_err(Into::into)
1674 .context("Failed reading current timestamp from PostgresDB")?;
1675 Ok::<_, IndexerError>((stored, timestamp))
1676 },
1677 PG_DB_COMMIT_SLEEP_DURATION
1678 )
1679 }
1680
1681 fn update_watermark_lowest_unpruned_keys<Table: AsRef<str>>(
1682 &self,
1683 unpruned_keys: &[(Table, u64)],
1684 ) -> Result<(), IndexerError> {
1685 transactional_blocking_with_retry!(
1686 &self.blocking_cp,
1687 |conn| {
1688 for (table, lowest_unpruned_key) in unpruned_keys {
1689 diesel::update(watermarks::table.filter(watermarks::entity.eq(table.as_ref())))
1690 .set(watermarks::lowest_unpruned_key.eq(*lowest_unpruned_key as i64))
1691 .execute(conn)
1692 .map_err(IndexerError::from)
1693 .context("failed to update watermark lowest_unpruned_key")?;
1694 }
1695 Ok::<(), IndexerError>(())
1696 },
1697 PG_DB_COMMIT_SLEEP_DURATION
1698 )
1699 }
1700
1701 fn get_watermark_by_entity(
1702 &self,
1703 entity: &str,
1704 ) -> Result<Option<StoredWatermark>, IndexerError> {
1705 run_query_with_retry!(
1706 &self.blocking_cp,
1707 |conn| {
1708 watermarks::table
1709 .filter(watermarks::entity.eq(entity))
1710 .first::<StoredWatermark>(conn)
1711 .optional()
1712 .map_err(Into::into)
1713 .context("failed reading watermark by entity from PostgresDB")
1714 },
1715 PG_DB_COMMIT_SLEEP_DURATION
1716 )
1717 }
1718
1719 pub(crate) async fn execute_in_blocking_worker<F, R>(&self, f: F) -> Result<R, IndexerError>
1720 where
1721 F: FnOnce(Self) -> Result<R, IndexerError> + Send + 'static,
1722 R: Send + 'static,
1723 {
1724 let this = self.clone();
1725 diesel_macro::spawn_blocking_task(move || f(this))
1726 .await
1727 .map_err(Into::into)
1728 .and_then(std::convert::identity)
1729 }
1730
1731 pub(crate) fn spawn_blocking_task<F, R>(
1732 &self,
1733 f: F,
1734 ) -> tokio::task::JoinHandle<std::result::Result<R, IndexerError>>
1735 where
1736 F: FnOnce(Self) -> Result<R, IndexerError> + Send + 'static,
1737 R: Send + 'static,
1738 {
1739 let this = self.clone();
1740 let guard = self.metrics.tokio_blocking_task_wait_latency.start_timer();
1741 diesel_macro::spawn_blocking_task(move || {
1742 let _elapsed = guard.stop_and_record();
1743 f(this)
1744 })
1745 }
1746
1747 fn spawn_task<F, Fut, R>(&self, f: F) -> tokio::task::JoinHandle<Result<R, IndexerError>>
1748 where
1749 F: FnOnce(Self) -> Fut + Send + 'static,
1750 Fut: std::future::Future<Output = Result<R, IndexerError>> + Send + 'static,
1751 R: Send + 'static,
1752 {
1753 let this = self.clone();
1754 tokio::task::spawn(async move { f(this).await })
1755 }
1756}
1757
1758#[async_trait]
1759impl IndexerStore for PgIndexerStore {
1760 async fn get_latest_checkpoint_sequence_number(&self) -> Result<Option<u64>, IndexerError> {
1761 self.execute_in_blocking_worker(|this| this.get_latest_checkpoint_sequence_number())
1762 .await
1763 }
1764
1765 async fn get_available_epoch_range(&self) -> Result<(u64, u64), IndexerError> {
1766 self.execute_in_blocking_worker(|this| this.get_prunable_epoch_range())
1767 .await
1768 }
1769
1770 async fn get_available_checkpoint_range(&self) -> Result<(u64, u64), IndexerError> {
1771 self.execute_in_blocking_worker(|this| this.get_available_checkpoint_range())
1772 .await
1773 }
1774
1775 async fn get_chain_identifier(&self) -> Result<Option<Vec<u8>>, IndexerError> {
1776 self.execute_in_blocking_worker(|this| this.get_chain_identifier())
1777 .await
1778 }
1779
1780 fn persist_objects_in_existing_transaction(
1781 &self,
1782 conn: &mut PgConnection,
1783 object_changes: Vec<TransactionObjectChangesToCommit>,
1784 ) -> Result<(), IndexerError> {
1785 if object_changes.is_empty() {
1786 return Ok(());
1787 }
1788
1789 let (indexed_mutations, indexed_deletions) = retain_latest_indexed_objects(object_changes);
1790 let object_mutations = indexed_mutations
1791 .into_iter()
1792 .map(StoredObject::from)
1793 .collect::<Vec<_>>();
1794 let object_deletions = indexed_deletions
1795 .into_iter()
1796 .map(StoredDeletedObject::from)
1797 .collect::<Vec<_>>();
1798
1799 self.persist_object_mutation_chunk_in_existing_transaction(conn, object_mutations)?;
1800 self.persist_object_deletion_chunk_in_existing_transaction(conn, object_deletions)?;
1801
1802 Ok(())
1803 }
1804
1805 async fn persist_object_versions(
1806 &self,
1807 object_versions: Vec<StoredObjectVersion>,
1808 ) -> Result<(), IndexerError> {
1809 if object_versions.is_empty() {
1810 return Ok(());
1811 }
1812
1813 let guard = self
1814 .metrics
1815 .checkpoint_db_commit_latency_objects_version
1816 .start_timer();
1817
1818 let object_versions_count = object_versions.len();
1819
1820 let chunks = chunk!(object_versions, self.config.parallel_objects_chunk_size);
1821 let futures = chunks
1822 .into_iter()
1823 .map(|c| self.spawn_blocking_task(move |this| this.persist_object_version_chunk(c)))
1824 .collect::<Vec<_>>();
1825
1826 futures::future::try_join_all(futures)
1827 .await
1828 .map_err(|e| {
1829 tracing::error!("failed to join persist_object_version_chunk futures: {e}");
1830 IndexerError::from(e)
1831 })?
1832 .into_iter()
1833 .collect::<Result<Vec<_>, _>>()
1834 .map_err(|e| {
1835 tracing::error!("failed to persist all objects version chunks: {e:?}");
1836 IndexerError::PostgresWrite
1837 })?;
1838 let elapsed = guard.stop_and_record();
1839 info!(elapsed, "Persisted {object_versions_count} object versions");
1840 Ok(())
1841 }
1842
1843 async fn persist_checkpoints(
1844 &self,
1845 checkpoints: Vec<IndexedCheckpoint>,
1846 ) -> Result<(), IndexerError> {
1847 self.execute_in_blocking_worker(move |this| this.persist_checkpoints(checkpoints))
1848 .await
1849 }
1850
1851 async fn persist_transactions(
1852 &self,
1853 transactions: Vec<IndexedTransaction>,
1854 ) -> Result<(), IndexerError> {
1855 let guard = self
1856 .metrics
1857 .checkpoint_db_commit_latency_transactions
1858 .start_timer();
1859 let len = transactions.len();
1860
1861 let chunks = chunk!(transactions, self.config.parallel_chunk_size);
1862 let futures = chunks
1863 .into_iter()
1864 .map(|c| self.spawn_blocking_task(move |this| this.persist_transactions_chunk(c)));
1865
1866 futures::future::try_join_all(futures)
1867 .await
1868 .map_err(|e| {
1869 tracing::error!("failed to join persist_transactions_chunk futures: {e}");
1870 IndexerError::from(e)
1871 })?
1872 .into_iter()
1873 .collect::<Result<Vec<_>, _>>()
1874 .map_err(|e| {
1875 tracing::error!("failed to persist all transactions chunks: {e:?}");
1876 IndexerError::PostgresWrite
1877 })?;
1878 let elapsed = guard.stop_and_record();
1879 info!(elapsed, "Persisted {} transactions", len);
1880 Ok(())
1881 }
1882
1883 fn persist_optimistic_transaction_in_existing_transaction(
1884 &self,
1885 conn: &mut PgConnection,
1886 transaction: OptimisticTransaction,
1887 ) -> Result<(), IndexerError> {
1888 insert_or_ignore_into!(optimistic_transactions::table, &transaction, conn);
1889 Ok(())
1890 }
1891
1892 async fn persist_events(&self, events: Vec<IndexedEvent>) -> Result<(), IndexerError> {
1893 if events.is_empty() {
1894 return Ok(());
1895 }
1896 let len = events.len();
1897 let guard = self
1898 .metrics
1899 .checkpoint_db_commit_latency_events
1900 .start_timer();
1901 let chunks = chunk!(events, self.config.parallel_chunk_size);
1902 let futures = chunks
1903 .into_iter()
1904 .map(|c| self.spawn_blocking_task(move |this| this.persist_events_chunk(c)));
1905
1906 futures::future::try_join_all(futures)
1907 .await
1908 .map_err(|e| {
1909 tracing::error!("failed to join persist_events_chunk futures: {e}");
1910 IndexerError::from(e)
1911 })?
1912 .into_iter()
1913 .collect::<Result<Vec<_>, _>>()
1914 .map_err(|e| {
1915 tracing::error!("failed to persist all events chunks: {e:?}");
1916 IndexerError::PostgresWrite
1917 })?;
1918 let elapsed = guard.stop_and_record();
1919 info!(elapsed, "Persisted {} events", len);
1920 Ok(())
1921 }
1922
1923 async fn persist_displays(&self, displays: Vec<StoredDisplay>) -> Result<(), IndexerError> {
1924 if displays.is_empty() {
1925 return Ok(());
1926 }
1927
1928 if displays.len() < self.config.parallel_objects_chunk_size {
1929 self.spawn_blocking_task(move |this| this.persist_displays_chunk(displays))
1930 .await??;
1931 return Ok(());
1932 }
1933 let chunks = chunk!(displays, self.config.parallel_objects_chunk_size);
1934 let persist_tasks = chunks
1935 .into_iter()
1936 .map(|c| self.spawn_blocking_task(move |this| this.persist_displays_chunk(c)));
1937 futures::future::try_join_all(persist_tasks)
1938 .await
1939 .map_err(|e| {
1940 tracing::error!("failed to join futures for persisting displays: {e}");
1941 IndexerError::from(e)
1942 })?
1943 .into_iter()
1944 .collect::<Result<Vec<_>, _>>()
1945 .map_err(|e| {
1946 tracing::error!("failed to persist all displays chunks: {e:?}");
1947 IndexerError::PostgresWrite
1948 })?;
1949 Ok(())
1950 }
1951
1952 fn persist_displays_chunk_in_existing_transaction(
1953 &self,
1954 conn: &mut PgConnection,
1955 displays: &[StoredDisplay],
1956 ) -> Result<(), IndexerError> {
1957 if displays.is_empty() {
1958 return Ok(());
1959 }
1960
1961 on_conflict_do_update_with_condition!(
1962 display::table,
1963 displays,
1964 display::object_type,
1965 (
1966 display::id.eq(excluded(display::id)),
1967 display::version.eq(excluded(display::version)),
1968 display::bcs.eq(excluded(display::bcs)),
1969 display::bcs_kind.eq(excluded(display::bcs_kind)),
1970 ),
1971 excluded(display::version).gt(display::version),
1972 conn
1973 );
1974
1975 Ok(())
1976 }
1977
1978 async fn persist_packages(&self, packages: Vec<StoredPackage>) -> Result<(), IndexerError> {
1979 if packages.is_empty() {
1980 return Ok(());
1981 }
1982 let len = packages.len();
1983 let guard = self
1984 .metrics
1985 .checkpoint_db_commit_latency_packages
1986 .start_timer();
1987 let persist_result = if len <= self.config.parallel_objects_chunk_size {
1988 self.execute_in_blocking_worker(move |this| this.persist_packages(&packages))
1989 .await
1990 } else {
1991 self.persist_packages_in_chunks(packages)
1992 .await
1993 .map_err(|e| {
1994 tracing::error!("failed to persist all packages chunks: {e:?}");
1995 IndexerError::PostgresWrite
1996 })
1997 };
1998 persist_result
1999 .tap_ok(|_| {
2000 let elapsed = guard.stop_and_record();
2001 info!(elapsed, "Persisted {len} packages");
2002 })
2003 .tap_err(|e| {
2004 tracing::error!("failed to persist packages with error: {e}");
2005 })
2006 }
2007
2008 async fn persist_event_indices(&self, indices: Vec<EventIndex>) -> Result<(), IndexerError> {
2009 if indices.is_empty() {
2010 return Ok(());
2011 }
2012 let len = indices.len();
2013 let guard = self
2014 .metrics
2015 .checkpoint_db_commit_latency_event_indices
2016 .start_timer();
2017 let chunks = chunk!(indices, self.config.parallel_chunk_size);
2018
2019 let futures = chunks.into_iter().map(|chunk| {
2020 self.spawn_task(move |this: Self| async move {
2021 this.persist_event_indices_chunk(chunk).await
2022 })
2023 });
2024
2025 futures::future::try_join_all(futures)
2026 .await
2027 .map_err(|e| {
2028 tracing::error!("failed to join persist_event_indices_chunk futures: {e}");
2029 IndexerError::from(e)
2030 })?
2031 .into_iter()
2032 .collect::<Result<Vec<_>, _>>()
2033 .map_err(|e| {
2034 tracing::error!("failed to persist all event_indices chunks: {e:?}");
2035 IndexerError::PostgresWrite
2036 })?;
2037 let elapsed = guard.stop_and_record();
2038 info!(elapsed, "Persisted {} event_indices chunks", len);
2039 Ok(())
2040 }
2041
2042 async fn persist_epoch(&self, epoch: EpochToCommit) -> Result<(), IndexerError> {
2043 self.execute_in_blocking_worker(move |this| this.persist_epoch(epoch))
2044 .await
2045 }
2046
2047 async fn advance_epoch(&self, epoch: EpochToCommit) -> Result<(), IndexerError> {
2048 self.execute_in_blocking_worker(move |this| this.advance_epoch(epoch))
2049 .await
2050 }
2051
2052 async fn get_network_total_transactions_by_end_of_epoch(
2053 &self,
2054 epoch: u64,
2055 ) -> Result<Option<u64>, IndexerError> {
2056 self.execute_in_blocking_worker(move |this| {
2057 this.get_network_total_transactions_by_end_of_epoch(epoch)
2058 })
2059 .await
2060 }
2061
2062 async fn refresh_participation_metrics(&self) -> Result<(), IndexerError> {
2063 self.execute_in_blocking_worker(move |this| this.refresh_participation_metrics())
2064 .await
2065 }
2066
2067 async fn update_watermarks_upper_bound<E: IntoEnumIterator>(
2068 &self,
2069 watermark: CommitterWatermark,
2070 ) -> Result<(), IndexerError>
2071 where
2072 E::Iterator: Iterator<Item: AsRef<str>>,
2073 {
2074 self.execute_in_blocking_worker(move |this| {
2075 this.update_watermarks_upper_bound::<E>(watermark)
2076 })
2077 .await
2078 }
2079
2080 fn as_any(&self) -> &dyn StdAny {
2081 self
2082 }
2083
2084 fn persist_protocol_configs_and_feature_flags(
2087 &self,
2088 chain_id: Vec<u8>,
2089 ) -> Result<(), IndexerError> {
2090 let chain_id = ChainIdentifier::from(
2091 CheckpointDigest::from_bytes(chain_id).expect("unable to convert chain id"),
2092 );
2093
2094 let mut all_configs = vec![];
2095 let mut all_flags = vec![];
2096
2097 let (start_version, end_version) = self.get_protocol_version_index_range()?;
2098 info!(
2099 "Persisting protocol configs with start_version: {}, end_version: {}",
2100 start_version, end_version
2101 );
2102
2103 for version in start_version..=end_version {
2106 let protocol_configs = ProtocolConfig::get_for_version_if_supported(
2107 (version as u64).into(),
2108 chain_id.chain(),
2109 )
2110 .ok_or(IndexerError::Generic(format!(
2111 "Unable to fetch protocol version {} and chain {:?}",
2112 version,
2113 chain_id.chain()
2114 )))?;
2115 let configs_vec = protocol_configs
2116 .attr_map()
2117 .into_iter()
2118 .map(|(k, v)| StoredProtocolConfig {
2119 protocol_version: version,
2120 config_name: k,
2121 config_value: v.map(|v| v.to_string()),
2122 })
2123 .collect::<Vec<_>>();
2124 all_configs.extend(configs_vec);
2125
2126 let feature_flags = protocol_configs
2127 .feature_map()
2128 .into_iter()
2129 .map(|(k, v)| StoredFeatureFlag {
2130 protocol_version: version,
2131 flag_name: k,
2132 flag_value: v,
2133 })
2134 .collect::<Vec<_>>();
2135 all_flags.extend(feature_flags);
2136 }
2137
2138 transactional_blocking_with_retry!(
2139 &self.blocking_cp,
2140 |conn| {
2141 for config_chunk in all_configs.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
2142 insert_or_ignore_into!(protocol_configs::table, config_chunk, conn);
2143 }
2144 for flag_chunk in all_flags.chunks(PG_COMMIT_CHUNK_SIZE_INTRA_DB_TX) {
2145 insert_or_ignore_into!(feature_flags::table, flag_chunk, conn);
2146 }
2147 Ok::<(), IndexerError>(())
2148 },
2149 PG_DB_COMMIT_SLEEP_DURATION
2150 )?;
2151 Ok(())
2152 }
2153
2154 async fn persist_tx_indices(&self, indices: Vec<TxIndex>) -> Result<(), IndexerError> {
2155 if indices.is_empty() {
2156 return Ok(());
2157 }
2158 let len = indices.len();
2159 let guard = self
2160 .metrics
2161 .checkpoint_db_commit_latency_tx_indices
2162 .start_timer();
2163 let chunks = chunk!(indices, self.config.parallel_chunk_size);
2164
2165 let futures = chunks.into_iter().map(|chunk| {
2166 self.spawn_task(move |this: Self| async move {
2167 this.persist_tx_indices_chunk_v2(chunk).await
2168 })
2169 });
2170 futures::future::try_join_all(futures)
2171 .await
2172 .map_err(|e| {
2173 tracing::error!("failed to join persist_tx_indices_chunk futures: {e}");
2174 IndexerError::from(e)
2175 })?
2176 .into_iter()
2177 .collect::<Result<Vec<_>, _>>()
2178 .map_err(|e| {
2179 tracing::error!("failed to persist all tx_indices chunks: {e:?}");
2180 IndexerError::PostgresWrite
2181 })?;
2182 let elapsed = guard.stop_and_record();
2183 info!(elapsed, "Persisted {} tx_indices chunks", len);
2184 Ok(())
2185 }
2186
2187 async fn persist_objects(
2188 &self,
2189 objects: Vec<CheckpointObjectChanges>,
2190 ) -> Result<(), IndexerError> {
2191 if objects.is_empty() {
2192 return Ok(());
2193 }
2194 let guard = self
2195 .metrics
2196 .checkpoint_db_commit_latency_objects
2197 .start_timer();
2198 let CheckpointObjectChanges {
2199 changed_objects: mutations,
2200 deleted_objects: deletions,
2201 } = retain_latest_objects_from_checkpoint_batch(objects);
2202 let mutation_len = mutations.len();
2203 let deletion_len = deletions.len();
2204
2205 let mutation_chunks = chunk!(mutations, self.config.parallel_objects_chunk_size);
2206 let deletion_chunks = chunk!(deletions, self.config.parallel_objects_chunk_size);
2207 let mutation_futures = mutation_chunks
2208 .into_iter()
2209 .map(|c| self.spawn_blocking_task(move |this| this.persist_live_objects(c)));
2210 let deletion_futures = deletion_chunks
2211 .into_iter()
2212 .map(|c| self.spawn_blocking_task(move |this| this.persist_removed_objects(c)));
2213 futures::future::try_join_all(mutation_futures.chain(deletion_futures))
2214 .await
2215 .map_err(|e| {
2216 tracing::error!("failed to join futures for persisting objects: {e}");
2217 IndexerError::from(e)
2218 })?
2219 .into_iter()
2220 .collect::<Result<Vec<_>, _>>()
2221 .map_err(|e| {
2222 tracing::error!("failed to persist all object chunks: {e:?}");
2223 IndexerError::PostgresWrite
2224 })?;
2225
2226 let elapsed = guard.stop_and_record();
2227 info!(
2228 elapsed,
2229 "Persisted objects with {mutation_len} mutations and {deletion_len} deletions",
2230 );
2231 Ok(())
2232 }
2233
2234 async fn persist_object_backward_history(
2235 &self,
2236 objects: Vec<StoredBackwardHistoryObject>,
2237 ) -> Result<(), IndexerError> {
2238 if objects.is_empty() {
2239 return Ok(());
2240 }
2241 let len = objects.len();
2242 let chunks = chunk!(objects, self.config.parallel_objects_chunk_size);
2243 let futures = chunks.into_iter().map(|c| {
2244 self.spawn_blocking_task(move |this| this.persist_objects_backward_history_chunk(c))
2245 });
2246
2247 futures::future::try_join_all(futures)
2248 .await
2249 .map_err(|e| {
2250 tracing::error!(
2251 "failed to join persist_objects_backward_history_chunk futures: {e}"
2252 );
2253 IndexerError::from(e)
2254 })?
2255 .into_iter()
2256 .collect::<Result<Vec<_>, _>>()
2257 .map_err(|e| {
2258 tracing::error!("failed to persist all objects backward history chunks: {e:?}");
2259 IndexerError::PostgresWrite
2260 })?;
2261 info!("Persisted {} objects backward history", len);
2262 Ok(())
2263 }
2264
2265 async fn persist_checkpointed_objects(
2266 &self,
2267 objects: Vec<CheckpointObjectChanges>,
2268 ) -> Result<(), IndexerError> {
2269 if objects.is_empty() {
2270 return Ok(());
2271 }
2272 let CheckpointObjectChanges {
2273 changed_objects: mutations,
2274 deleted_objects: deletions,
2275 } = retain_latest_objects_from_checkpoint_batch(objects);
2276 let mutation_len = mutations.len();
2277 let deletion_len = deletions.len();
2278
2279 let checkpointed_objects: Vec<StoredCheckpointedObject> = mutations
2280 .into_iter()
2281 .map(|live| {
2282 let (indexed, _tx_digest) = live.split();
2283 StoredCheckpointedObject::try_from(indexed)
2284 })
2285 .chain(
2286 deletions
2287 .into_iter()
2288 .map(|removed| Ok(StoredCheckpointedObject::from(removed.indexed_object))),
2289 )
2290 .collect::<Result<Vec<_>, IndexerError>>()?;
2291
2292 let len = checkpointed_objects.len();
2293 let chunks = chunk!(
2294 checkpointed_objects,
2295 self.config.parallel_objects_chunk_size
2296 );
2297 let futures = chunks.into_iter().map(|c| {
2298 self.spawn_blocking_task(move |this| this.persist_checkpointed_objects_chunk(c))
2299 });
2300 futures::future::try_join_all(futures)
2301 .await
2302 .map_err(|e| {
2303 tracing::error!("failed to join futures for persisting checkpointed objects: {e}");
2304 IndexerError::from(e)
2305 })?
2306 .into_iter()
2307 .collect::<Result<Vec<_>, _>>()
2308 .map_err(|e| {
2309 tracing::error!("failed to persist all checkpointed object chunks: {e:?}");
2310 IndexerError::PostgresWrite
2311 })?;
2312
2313 info!(
2314 "Persisted {len} checkpointed objects ({mutation_len} mutations, {deletion_len} deletions)",
2315 );
2316 Ok(())
2317 }
2318
2319 async fn persist_tx_global_order(
2320 &self,
2321 tx_order: Vec<TxGlobalOrder>,
2322 ) -> Result<(), IndexerError> {
2323 let guard = self
2324 .metrics
2325 .checkpoint_db_commit_latency_tx_insertion_order
2326 .start_timer();
2327 let len = tx_order.len();
2328
2329 let chunks = chunk!(tx_order, self.config.parallel_chunk_size);
2330 let futures = chunks
2331 .into_iter()
2332 .map(|c| self.spawn_blocking_task(move |this| this.persist_tx_global_order_chunk(c)));
2333
2334 futures::future::try_join_all(futures)
2335 .await
2336 .map_err(|e| {
2337 tracing::error!("failed to join persist_tx_global_order_chunk futures: {e}",);
2338 IndexerError::from(e)
2339 })?
2340 .into_iter()
2341 .collect::<Result<Vec<_>, _>>()
2342 .map_err(|e| {
2343 tracing::error!("failed to persist all txs insertion order chunks: {e:?}");
2344 IndexerError::PostgresWrite
2345 })?;
2346 let elapsed = guard.stop_and_record();
2347 info!(elapsed, "Persisted {len} txs insertion orders");
2348 Ok(())
2349 }
2350
2351 async fn update_watermarks_lower_bound<Table: AsRef<str> + Send + 'static>(
2352 &self,
2353 watermarks: Vec<(Table, u64)>,
2354 ) -> Result<(), IndexerError> {
2355 self.execute_in_blocking_worker(move |this| this.update_watermarks_lower_bound(watermarks))
2356 .await
2357 }
2358
2359 async fn get_watermarks(&self) -> Result<(Vec<StoredWatermark>, i64), IndexerError> {
2360 self.execute_in_blocking_worker(move |this| this.get_watermarks())
2361 .await
2362 }
2363
2364 async fn prune_table_by_checkpoint_range(
2365 &self,
2366 table: &crate::pruning::pruner::PrunableTable,
2367 min_checkpoint: u64,
2368 max_checkpoint: u64,
2369 ) -> Result<(), IndexerError> {
2370 let table_clone = *table;
2371 self.execute_in_blocking_worker(move |this| {
2372 this.prune_table_by_checkpoint_range(&table_clone, min_checkpoint, max_checkpoint)
2373 })
2374 .await
2375 }
2376
2377 async fn prune_table_by_tx_range(
2378 &self,
2379 table: &crate::pruning::pruner::PrunableTable,
2380 min_tx: u64,
2381 max_tx: u64,
2382 ) -> Result<(), IndexerError> {
2383 let table_clone = *table;
2384 self.execute_in_blocking_worker(move |this| {
2385 this.prune_single_tx_or_event_table(&table_clone, min_tx, max_tx)
2386 })
2387 .await
2388 }
2389
2390 async fn prune_table_by_optimistic_seq(
2391 &self,
2392 table: &crate::pruning::pruner::PrunableTable,
2393 start: u64,
2394 end: u64,
2395 ) -> Result<(), IndexerError> {
2396 use crate::pruning::pruner::PrunableTable;
2397
2398 if !matches!(table, PrunableTable::OptimisticTransactions) {
2399 return Err(IndexerError::InvalidArgument(format!(
2400 "table {} does not support pruning by optimistic sequence number",
2401 table.as_ref()
2402 )));
2403 }
2404
2405 self.execute_in_blocking_worker(move |this| {
2406 this.prune_optimistic_tx_by_optimistic_seq(start, end)
2407 })
2408 .await
2409 }
2410
2411 async fn prune_table_by_checkpoint_with_limit(
2412 &self,
2413 table: &crate::pruning::pruner::PrunableTable,
2414 start: u64,
2415 end: u64,
2416 limit: i64,
2417 ) -> Result<usize, IndexerError> {
2418 use crate::pruning::pruner::PrunableTable;
2419
2420 if !matches!(table, PrunableTable::ObjectsBackwardHistory) {
2421 return Err(IndexerError::InvalidArgument(format!(
2422 "table {} does not support pruning by checkpoint with limit",
2423 table.as_ref()
2424 )));
2425 }
2426
2427 self.execute_in_blocking_worker(move |this| {
2428 this.prune_backward_history_by_checkpoint_with_limit(start, end, limit)
2429 })
2430 .await
2431 }
2432
2433 async fn update_watermark_lowest_unpruned_key(
2434 &self,
2435 table: &PrunableTable,
2436 lowest_unpruned_key: u64,
2437 ) -> Result<(), IndexerError> {
2438 <Self as IndexerStore>::update_watermarks_lowest_unpruned_key(
2439 &self,
2440 vec![(*table, lowest_unpruned_key)],
2441 )
2442 .await
2443 }
2444
2445 async fn update_watermarks_lowest_unpruned_key<Table: AsRef<str> + Send + 'static>(
2446 &self,
2447 unpruned_keys: Vec<(Table, u64)>,
2448 ) -> Result<(), IndexerError> {
2449 self.execute_in_blocking_worker(move |this| {
2450 this.update_watermark_lowest_unpruned_keys(&unpruned_keys)
2451 })
2452 .await
2453 }
2454
2455 async fn get_watermark_by_entity(
2456 &self,
2457 entity: String,
2458 ) -> Result<Option<StoredWatermark>, IndexerError> {
2459 self.execute_in_blocking_worker(move |this| this.get_watermark_by_entity(&entity))
2460 .await
2461 }
2462}
2463
2464fn retain_latest_indexed_objects(
2470 tx_object_changes: Vec<TransactionObjectChangesToCommit>,
2471) -> (Vec<IndexedObject>, Vec<IndexedDeletedObject>) {
2472 use std::collections::HashMap;
2473
2474 let mut mutations = HashMap::<ObjectId, IndexedObject>::new();
2475 let mut deletions = HashMap::<ObjectId, IndexedDeletedObject>::new();
2476
2477 for change in tx_object_changes {
2478 for mutation in change.changed_objects {
2483 let id = mutation.object.id();
2484 let version = mutation.object.version();
2485
2486 if let Some(existing) = deletions.remove(&id) {
2487 assert!(
2488 existing.object_version < version,
2489 "mutation version ({version}) should be greater than existing deletion version ({}) for object {id}",
2490 existing.object_version
2491 );
2492 }
2493
2494 if let Some(existing) = mutations.insert(id, mutation) {
2495 assert!(
2496 existing.object.version() < version,
2497 "mutation version ({version}) should be greater than existing mutation version ({}) for object {id}",
2498 existing.object.version()
2499 );
2500 }
2501 }
2502 for deletion in change.deleted_objects {
2504 let id = deletion.object_id;
2505 let version = deletion.object_version;
2506
2507 if let Some(existing) = mutations.remove(&id) {
2508 assert!(
2509 existing.object.version() < version,
2510 "deletion version ({version}) should be greater than existing mutation version ({}) for object {id}",
2511 existing.object.version(),
2512 );
2513 }
2514
2515 if let Some(existing) = deletions.insert(id, deletion) {
2516 assert!(
2517 existing.object_version < version,
2518 "deletion version ({version}) should be greater than existing deletion version ({}) for object {id}",
2519 existing.object_version
2520 );
2521 }
2522 }
2523 }
2524
2525 (
2526 mutations.into_values().collect(),
2527 deletions.into_values().collect(),
2528 )
2529}