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