Skip to content

Commit

Permalink
[pick 2024.4][GraphQL] Object DataLoader (#17332) (#17424)
Browse files Browse the repository at this point in the history
## Description

Implement data loaders for fetching historical object versions, objects
bounded by their parent versions, and the latest versions of an object
at a given checkpoint.

By implementing an Object DataLoader, we also implicitly get support for
data-loading all derived types (`MoveObject`, `MovePackage`,
`MoveModule`, `DynamicField`, `Coin`, etc).

These implementations (particularly historical queries and queries where
the version can be bounded by a parent version) can be made even more
efficient with the existence of an index/side table that maps an
object's ID and version to the checkpoint it is part of. This change has
not been included in this PR, but we will follow up on this as part of
Object query benchmarking.

As part of this change, I enabled queries for historical objects outside
the available range. Later (with the use of an `obj_version` index) it
will also be possible to enable dynamic field look-ups on historical
objects as well.

## Test Plan

```
sui$ cargo nextest run -p sui-graphql-rpc
sui$ cargo nextest run -p sui-graphql-e2e-tests --features pg_integration
```

Run the following query -- after this change, it takes about 8s to
complete on the server, fetching about 80 objects, while previously it
would either timeout or squeak in *just* under the 40s timeout. I expect
this number to improve further once we have an efficient way to map
object ids and versions to a checkpoint sequence number.

```graphql
query {
  transactionBlocks(last: 5) {
    nodes {
      effects {
        objectChanges(first: 50) {
          pageInfo { hasNextPage }
          nodes {
            idCreated
            idDeleted
            inputState { asMoveObject { contents { json } } }
            outputState { asMoveObject { contents { json } } }
          }
        }
      }
    }
  }
}
```

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol:
- [ ] Nodes (Validators and Full nodes):
- [ ] Indexer:
- [ ] JSON-RPC:
- [x] GraphQL: Queries for historical versions of objects will now
return data even if that version of the object is outside the available
range.
- [ ] CLI:
- [ ] Rust SDK:
  • Loading branch information
amnn authored Apr 30, 2024
1 parent 06c34b4 commit 20a87ea
Show file tree
Hide file tree
Showing 21 changed files with 380 additions and 355 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,24 @@ Checkpoint created: 7
task 33 'run-graphql'. lines 445-495:
Response: {
"data": {
"parent_version_4_outside_consistent_range": null,
"parent_version_4_outside_consistent_range": {
"dynamicFields": {
"edges": [
{
"cursor": "IPzS8naECYD4KyjqOrwvPZSgKp0kO3zeSAqOIyZX0G0bBwAAAAAAAAA=",
"node": {
"name": {
"bcs": "pAEAAAAAAAA=",
"type": {
"repr": "u64"
}
},
"value": null
}
}
]
}
},
"parent_version_4_paginated_outside_consistent_range": null,
"parent_version_6_no_df_1_2_3": {
"dynamicFields": {
Expand Down Expand Up @@ -886,13 +903,33 @@ Response: {
"edges": []
}
}
}
},
"errors": [
{
"message": "Requested data is outside the available range",
"locations": [
{
"line": 34,
"column": 5
}
],
"path": [
"parent_version_4_paginated_outside_consistent_range",
"dynamicFields"
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
]
}

task 34 'run-graphql'. lines 497-528:
Response: {
"data": {
"parent_version_4": null,
"parent_version_4": {
"dfAtParentVersion4_outside_range": null
},
"parent_version_6": {
"dfAtParentVersion6": null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Objects snapshot updated to [0 to 5)
task 18 'create-checkpoint'. lines 219-219:
Checkpoint created: 6

task 19 'run-graphql'. lines 221-259:
task 19 'run-graphql'. lines 221-260:
Response: {
"data": {
"object_within_available_range": {
Expand All @@ -192,7 +192,22 @@ Response: {
}
}
},
"object_outside_available_range": null,
"object_not_in_snapshot": null
"object_outside_available_range": {
"status": "WRAPPED_OR_DELETED",
"version": 5,
"asMoveObject": null
},
"object_not_in_snapshot": {
"status": "INDEXED",
"version": 3,
"asMoveObject": {
"contents": {
"json": {
"id": "0x7a166d93d67f2815b463e90d909798a5c572b3e15cdbd6dc8fc10f9fc88618f9",
"value": "0"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ module Test::M1 {
//# create-checkpoint

//# run-graphql
# Querying objects by version doesn't require it to be in the snapshot table.
{
object_within_available_range: object(
address: "@{obj_2_0}"
Expand Down
7 changes: 3 additions & 4 deletions crates/sui-graphql-rpc/src/types/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub(crate) struct CheckpointId {
pub sequence_number: Option<u64>,
}

/// DataLoader key for fetching a `Checkpoint` by its sequence number, optionally constrained by a
/// consistency cursor.
/// DataLoader key for fetching a `Checkpoint` by its sequence number, constrained by a consistency
/// cursor.
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
struct SeqNumKey {
pub sequence_number: u64,
Expand All @@ -46,8 +46,7 @@ struct SeqNumKey {
pub checkpoint_viewed_at: u64,
}

/// DataLoader key for fetching a `Checkpoint` by its digest, optionally constrained by a
/// consistency cursor.
/// DataLoader key for fetching a `Checkpoint` by its digest, constrained by a consistency cursor.
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
struct DigestKey {
pub digest: Digest,
Expand Down
31 changes: 16 additions & 15 deletions crates/sui-graphql-rpc/src/types/dynamic_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use sui_types::dynamic_field::{derive_dynamic_field_id, DynamicFieldInfo, Dynami

use super::available_range::AvailableRange;
use super::cursor::{Page, Target};
use super::object::{self, deserialize_move_struct, Object, ObjectKind, ObjectLookupKey};
use super::object::{self, deserialize_move_struct, Object, ObjectKind, ObjectLookup};
use super::type_filter::ExactTypeFilter;
use super::{
base64::Base64, move_object::MoveObject, move_value::MoveValue, sui_address::SuiAddress,
Expand Down Expand Up @@ -105,12 +105,14 @@ impl DynamicField {
// object. Thus, we use the version of the field object to bound the value object at the
// correct version.
let obj = MoveObject::query(
ctx.data_unchecked(),
ctx,
self.df_object_id,
ObjectLookupKey::LatestAtParentVersion {
version: self.super_.super_.version_impl(),
checkpoint_viewed_at: self.super_.super_.checkpoint_viewed_at,
},
Object::under_parent(
// TODO (RPC-131): The dynamic object field value's version should be bounded by
// the field's parent version, not the version of the field object itself.
self.super_.super_.version_impl(),
self.super_.super_.checkpoint_viewed_at,
),
)
.await
.extend()?;
Expand Down Expand Up @@ -152,7 +154,7 @@ impl DynamicField {
/// before the provided version. If `parent_version` is not provided, the latest version of the
/// field is returned as bounded by the `checkpoint_viewed_at` parameter.
pub(crate) async fn query(
db: &Db,
ctx: &Context<'_>,
parent: SuiAddress,
parent_version: Option<u64>,
name: DynamicFieldName,
Expand All @@ -169,16 +171,15 @@ impl DynamicField {
let field_id = derive_dynamic_field_id(parent, &type_, &name.bcs.0)
.map_err(|e| Error::Internal(format!("Failed to derive dynamic field id: {e}")))?;

use ObjectLookupKey as K;
let key = match parent_version {
None => K::LatestAt(checkpoint_viewed_at),
Some(version) => K::LatestAtParentVersion {
version,
let super_ = MoveObject::query(
ctx,
SuiAddress::from(field_id),
ObjectLookup::LatestAt {
parent_version,
checkpoint_viewed_at,
},
};

let super_ = MoveObject::query(db, SuiAddress::from(field_id), key).await?;
)
.await?;

super_.map(Self::try_from).transpose()
}
Expand Down
2 changes: 1 addition & 1 deletion crates/sui-graphql-rpc/src/types/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl Event {
/// the sending module would be A::m1.
async fn sending_module(&self, ctx: &Context<'_>) -> Result<Option<MoveModule>> {
MoveModule::query(
ctx.data_unchecked(),
ctx,
self.native.package_id.into(),
&self.native.transaction_module.to_string(),
self.checkpoint_viewed_at,
Expand Down
10 changes: 3 additions & 7 deletions crates/sui-graphql-rpc/src/types/gas.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::types::object::Object;
use async_graphql::connection::Connection;
use async_graphql::*;
use sui_types::{
Expand All @@ -10,7 +9,7 @@ use sui_types::{
transaction::GasData,
};

use super::{address::Address, big_int::BigInt, object::ObjectLookupKey, sui_address::SuiAddress};
use super::{address::Address, big_int::BigInt, object::Object, sui_address::SuiAddress};
use super::{
cursor::Page,
object::{self, ObjectFilter, ObjectKey},
Expand Down Expand Up @@ -130,12 +129,9 @@ impl GasCostSummary {
impl GasEffects {
async fn gas_object(&self, ctx: &Context<'_>) -> Result<Option<Object>> {
Object::query(
ctx.data_unchecked(),
ctx,
self.object_id,
ObjectLookupKey::VersionAt {
version: self.object_version,
checkpoint_viewed_at: self.checkpoint_viewed_at,
},
Object::at_version(self.object_version, self.checkpoint_viewed_at),
)
.await
.extend()
Expand Down
18 changes: 7 additions & 11 deletions crates/sui-graphql-rpc/src/types/move_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use async_graphql::*;
use sui_package_resolver::FunctionDef;

use crate::{data::Db, error::Error};
use crate::error::Error;

use super::{
move_module::MoveModule,
Expand Down Expand Up @@ -34,14 +34,10 @@ pub(crate) struct MoveFunctionTypeParameter {
impl MoveFunction {
/// The module this function was defined in.
async fn module(&self, ctx: &Context<'_>) -> Result<MoveModule> {
let Some(module) = MoveModule::query(
ctx.data_unchecked(),
self.package,
&self.module,
self.checkpoint_viewed_at,
)
.await
.extend()?
let Some(module) =
MoveModule::query(ctx, self.package, &self.module, self.checkpoint_viewed_at)
.await
.extend()?
else {
return Err(Error::Internal(format!(
"Failed to load module for function: {}::{}::{}",
Expand Down Expand Up @@ -123,13 +119,13 @@ impl MoveFunction {
}

pub(crate) async fn query(
db: &Db,
ctx: &Context<'_>,
address: SuiAddress,
module: &str,
function: &str,
checkpoint_viewed_at: u64,
) -> Result<Option<Self>, Error> {
let Some(module) = MoveModule::query(db, address, module, checkpoint_viewed_at).await?
let Some(module) = MoveModule::query(ctx, address, module, checkpoint_viewed_at).await?
else {
return Ok(None);
};
Expand Down
16 changes: 7 additions & 9 deletions crates/sui-graphql-rpc/src/types/move_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ use move_disassembler::disassembler::Disassembler;
use move_ir_types::location::Loc;

use crate::consistency::{ConsistentIndexCursor, ConsistentNamedCursor};
use crate::data::Db;
use crate::error::Error;
use sui_package_resolver::Module as ParsedMoveModule;

use super::cursor::{JsonCursor, Page};
use super::move_function::MoveFunction;
use super::move_struct::MoveStruct;
use super::object::ObjectLookupKey;
use super::object::Object;
use super::{base64::Base64, move_package::MovePackage, sui_address::SuiAddress};

#[derive(Clone)]
Expand All @@ -37,9 +36,9 @@ impl MoveModule {
/// The package that this Move module was defined in
async fn package(&self, ctx: &Context<'_>) -> Result<MovePackage> {
MovePackage::query(
ctx.data_unchecked(),
ctx,
self.storage_id,
ObjectLookupKey::LatestAt(self.checkpoint_viewed_at),
Object::latest_at(self.checkpoint_viewed_at),
)
.await
.extend()?
Expand Down Expand Up @@ -88,9 +87,9 @@ impl MoveModule {

let runtime_id = *bytecode.self_id().address();
let Some(package) = MovePackage::query(
ctx.data_unchecked(),
ctx,
self.storage_id,
ObjectLookupKey::LatestAt(checkpoint_viewed_at),
Object::latest_at(checkpoint_viewed_at),
)
.await
.extend()?
Expand Down Expand Up @@ -315,14 +314,13 @@ impl MoveModule {
}

pub(crate) async fn query(
db: &Db,
ctx: &Context<'_>,
address: SuiAddress,
name: &str,
checkpoint_viewed_at: u64,
) -> Result<Option<Self>, Error> {
let Some(package) =
MovePackage::query(db, address, ObjectLookupKey::LatestAt(checkpoint_viewed_at))
.await?
MovePackage::query(ctx, address, Object::latest_at(checkpoint_viewed_at)).await?
else {
return Ok(None);
};
Expand Down
8 changes: 4 additions & 4 deletions crates/sui-graphql-rpc/src/types/move_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::display::DisplayEntry;
use super::dynamic_field::{DynamicField, DynamicFieldName};
use super::move_type::MoveType;
use super::move_value::MoveValue;
use super::object::{self, ObjectFilter, ObjectImpl, ObjectLookupKey, ObjectOwner, ObjectStatus};
use super::object::{self, ObjectFilter, ObjectImpl, ObjectLookup, ObjectOwner, ObjectStatus};
use super::owner::OwnerImpl;
use super::stake::StakedSuiDowncastError;
use super::sui_address::SuiAddress;
Expand Down Expand Up @@ -423,11 +423,11 @@ impl MoveObjectImpl<'_> {

impl MoveObject {
pub(crate) async fn query(
db: &Db,
ctx: &Context<'_>,
address: SuiAddress,
key: ObjectLookupKey,
key: ObjectLookup,
) -> Result<Option<Self>, Error> {
let Some(object) = Object::query(db, address, key).await? else {
let Some(object) = Object::query(ctx, address, key).await? else {
return Ok(None);
};

Expand Down
9 changes: 4 additions & 5 deletions crates/sui-graphql-rpc/src/types/move_package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::cursor::{JsonCursor, Page};
use super::move_module::MoveModule;
use super::move_object::MoveObject;
use super::object::{
self, Object, ObjectFilter, ObjectImpl, ObjectLookupKey, ObjectOwner, ObjectStatus,
self, Object, ObjectFilter, ObjectImpl, ObjectLookup, ObjectOwner, ObjectStatus,
};
use super::owner::OwnerImpl;
use super::stake::StakedSui;
Expand All @@ -18,7 +18,6 @@ use super::suins_registration::{DomainFormat, SuinsRegistration};
use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter};
use super::type_filter::ExactTypeFilter;
use crate::consistency::ConsistentNamedCursor;
use crate::data::Db;
use crate::error::Error;
use async_graphql::connection::{Connection, CursorType, Edge};
use async_graphql::*;
Expand Down Expand Up @@ -413,11 +412,11 @@ impl MovePackage {
}

pub(crate) async fn query(
db: &Db,
ctx: &Context<'_>,
address: SuiAddress,
key: ObjectLookupKey,
key: ObjectLookup,
) -> Result<Option<Self>, Error> {
let Some(object) = Object::query(db, address, key).await? else {
let Some(object) = Object::query(ctx, address, key).await? else {
return Ok(None);
};

Expand Down
2 changes: 1 addition & 1 deletion crates/sui-graphql-rpc/src/types/move_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl MoveStruct {
/// The module this struct was originally defined in.
async fn module(&self, ctx: &Context<'_>) -> Result<MoveModule> {
let Some(module) = MoveModule::query(
ctx.data_unchecked(),
ctx,
self.defining_id,
&self.module,
self.checkpoint_viewed_at,
Expand Down
Loading

0 comments on commit 20a87ea

Please sign in to comment.