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