Skip to main content

iota_indexer/store/
pg_indexer_store.rs

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