-
Notifications
You must be signed in to change notification settings - Fork 325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(nns): Add pagination to list_neurons API #3358
base: master
Are you sure you want to change the base?
Changes from 6 commits
fb60ba2
6f297f8
467b225
3fa5938
264a28f
2d22844
4fb4103
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,7 +117,7 @@ use rust_decimal_macros::dec; | |
use std::{ | ||
borrow::Cow, | ||
cmp::{max, Ordering}, | ||
collections::{BTreeMap, HashMap, HashSet}, | ||
collections::{BTreeMap, BTreeSet, HashMap, HashSet}, | ||
convert::{TryFrom, TryInto}, | ||
fmt, | ||
future::Future, | ||
|
@@ -143,8 +143,6 @@ pub mod tla; | |
|
||
use crate::storage::with_voting_state_machines_mut; | ||
#[cfg(feature = "tla")] | ||
use std::collections::BTreeSet; | ||
#[cfg(feature = "tla")] | ||
pub use tla::{ | ||
tla_update_method, InstrumentationState, ToTla, CLAIM_NEURON_DESC, MERGE_NEURONS_DESC, | ||
SPAWN_NEURONS_DESC, SPAWN_NEURON_DESC, SPLIT_NEURON_DESC, TLA_INSTRUMENTATION_STATE, | ||
|
@@ -2419,6 +2417,7 @@ impl Governance { | |
include_neurons_readable_by_caller, | ||
include_empty_neurons_readable_by_caller, | ||
include_public_neurons_in_full_neurons, | ||
start_from_neuron_id, | ||
} = list_neurons; | ||
|
||
let include_empty_neurons_readable_by_caller = include_empty_neurons_readable_by_caller | ||
|
@@ -2455,12 +2454,26 @@ impl Governance { | |
neuron_ids.iter().map(|id| NeuronId { id: *id }).collect(); | ||
requested_neuron_ids.append(&mut implicitly_requested_neuron_ids); | ||
|
||
if let Some(start_from_neuron_id) = start_from_neuron_id { | ||
requested_neuron_ids = requested_neuron_ids | ||
.into_iter() | ||
.skip_while(|id| id.id != *start_from_neuron_id) | ||
.collect(); | ||
} | ||
|
||
// These will be assembled into the final result. | ||
let mut neuron_infos = hashmap![]; | ||
let mut full_neurons = vec![]; | ||
|
||
const MAX_LIST_NEURONS_COUNT: usize = 500; | ||
|
||
let mut next_start_from_neuron_id = None; | ||
// Populate the above two neuron collections. | ||
for neuron_id in requested_neuron_ids { | ||
for (count, neuron_id) in &mut requested_neuron_ids.into_iter().enumerate() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This creates a somewhat surprising behavior if some of the (explicitly) requested neuron ids don't exist, since the I think it might be good to prevent >500 lookups on non-existent neuron ids, but this should probably be documented through comments. |
||
if count >= MAX_LIST_NEURONS_COUNT { | ||
next_start_from_neuron_id = Some(neuron_id.id); | ||
break; | ||
} | ||
// Ignore when a neuron is not found. It is not guaranteed that a | ||
// neuron will be found, because some of the elements in | ||
// requested_neuron_ids are supplied by the caller. | ||
|
@@ -2494,6 +2507,7 @@ impl Governance { | |
ListNeuronsResponse { | ||
neuron_infos, | ||
full_neurons, | ||
next_start_from_neuron_id, | ||
} | ||
} | ||
|
||
|
@@ -3869,7 +3883,7 @@ impl Governance { | |
match proposal_data { | ||
None => None, | ||
Some(pd) => { | ||
let caller_neurons: HashSet<NeuronId> = | ||
let caller_neurons: BTreeSet<NeuronId> = | ||
self.neuron_store.get_neuron_ids_readable_by_caller(*caller); | ||
let now = self.env.now(); | ||
Some(self.proposal_data_to_info(pd, &caller_neurons, now, false)) | ||
|
@@ -3955,7 +3969,7 @@ impl Governance { | |
/// retrieve dropped payloads by calling `get_proposal_info` for | ||
/// each proposal of interest. | ||
pub fn get_pending_proposals(&self, caller: &PrincipalId) -> Vec<ProposalInfo> { | ||
let caller_neurons: HashSet<NeuronId> = | ||
let caller_neurons: BTreeSet<NeuronId> = | ||
self.neuron_store.get_neuron_ids_readable_by_caller(*caller); | ||
let now = self.env.now(); | ||
self.get_pending_proposals_data() | ||
|
@@ -3993,7 +4007,7 @@ impl Governance { | |
fn proposal_data_to_info( | ||
&self, | ||
data: &ProposalData, | ||
caller_neurons: &HashSet<NeuronId>, | ||
caller_neurons: &BTreeSet<NeuronId>, | ||
now_seconds: u64, | ||
multi_query: bool, | ||
) -> ProposalInfo { | ||
|
@@ -4025,7 +4039,7 @@ impl Governance { | |
/// in `except_from`. | ||
fn remove_ballots_not_cast_by( | ||
all_ballots: &HashMap<u64, Ballot>, | ||
except_from: &HashSet<NeuronId>, | ||
except_from: &BTreeSet<NeuronId>, | ||
) -> HashMap<u64, Ballot> { | ||
let mut ballots = HashMap::new(); | ||
for neuron_id in except_from.iter() { | ||
|
@@ -4065,7 +4079,7 @@ impl Governance { | |
fn proposal_is_visible_to_neurons( | ||
&self, | ||
info: &ProposalData, | ||
caller_neurons: &HashSet<NeuronId>, | ||
caller_neurons: &BTreeSet<NeuronId>, | ||
) -> bool { | ||
// Is 'info' a manage neuron proposal? | ||
if let Some(ref managed_id) = info.proposal.as_ref().and_then(|x| x.managed_neuron()) { | ||
|
@@ -4135,7 +4149,7 @@ impl Governance { | |
caller: &PrincipalId, | ||
req: &ListProposalInfo, | ||
) -> ListProposalInfoResponse { | ||
let caller_neurons: HashSet<NeuronId> = | ||
let caller_neurons: BTreeSet<NeuronId> = | ||
self.neuron_store.get_neuron_ids_readable_by_caller(*caller); | ||
let exclude_topic: HashSet<i32> = req.exclude_topic.iter().cloned().collect(); | ||
let include_reward_status: HashSet<i32> = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
use crate::{ | ||
governance::Governance, | ||
pb::v1::{neuron::DissolveState, ListNeurons, NetworkEconomics, Neuron}, | ||
test_utils::{MockEnvironment, StubCMC, StubIcpLedger}, | ||
}; | ||
use ic_base_types::PrincipalId; | ||
use ic_nns_common::pb::v1::NeuronId; | ||
|
||
#[test] | ||
fn test_list_neurons_with_paging() { | ||
let user_id = PrincipalId::new_user_test_id(100); | ||
|
||
let neurons = (1..1000u64) | ||
.map(|id| { | ||
let dissolve_state = DissolveState::DissolveDelaySeconds(100); | ||
let account = crate::test_utils::test_subaccount_for_neuron_id(id); | ||
( | ||
id, | ||
Neuron { | ||
id: Some(NeuronId::from_u64(id)), | ||
controller: Some(user_id), | ||
account, | ||
dissolve_state: Some(dissolve_state), | ||
// Fill in the rest as needed (stake, maturity, etc.) | ||
..Default::default() | ||
}, | ||
) | ||
}) | ||
.collect(); | ||
|
||
let governance = Governance::new( | ||
crate::pb::v1::Governance { | ||
neurons, | ||
economics: Some(NetworkEconomics { | ||
voting_power_economics: Some(Default::default()), | ||
..Default::default() | ||
}), | ||
..crate::pb::v1::Governance::default() | ||
}, | ||
Box::new(MockEnvironment::new(Default::default(), 0)), | ||
Box::new(StubIcpLedger {}), | ||
Box::new(StubCMC {}), | ||
); | ||
|
||
let response = governance.list_neurons( | ||
&ListNeurons { | ||
neuron_ids: vec![], | ||
include_neurons_readable_by_caller: true, | ||
include_empty_neurons_readable_by_caller: None, | ||
include_public_neurons_in_full_neurons: None, | ||
start_from_neuron_id: None, | ||
}, | ||
user_id, | ||
); | ||
|
||
assert_eq!(response.full_neurons.len(), 500); | ||
assert_eq!(response.next_start_from_neuron_id, Some(501)); | ||
|
||
let response = governance.list_neurons( | ||
&ListNeurons { | ||
neuron_ids: vec![], | ||
include_neurons_readable_by_caller: true, | ||
include_empty_neurons_readable_by_caller: None, | ||
include_public_neurons_in_full_neurons: None, | ||
start_from_neuron_id: Some(501), | ||
}, | ||
user_id, | ||
); | ||
|
||
assert_eq!(response.full_neurons.len(), 499); | ||
assert_eq!(response.next_start_from_neuron_id, None); | ||
|
||
// Edge case, just barely fit all the neurons in second response | ||
let response = governance.list_neurons( | ||
&ListNeurons { | ||
neuron_ids: vec![], | ||
include_neurons_readable_by_caller: true, | ||
include_empty_neurons_readable_by_caller: None, | ||
include_public_neurons_in_full_neurons: None, | ||
start_from_neuron_id: Some(500), | ||
}, | ||
user_id, | ||
); | ||
|
||
assert_eq!(response.full_neurons.len(), 500); | ||
assert_eq!(response.next_start_from_neuron_id, None); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the neuron ID have to be an ID that exists?
Or is it allowed to pass
lastNeuronIdFromPreviousPage + 1
in order to continue after the previous page?Also it could be clearer whether the skipped neurons have higher IDs or lower IDs than the passed one. For example when fetching transactions, the newest transactions are in the first page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dskloetd it doesn't have to exist, but it would need to be what's returned from the previous request. If the neuron didn't exist, it would have been in the list that was being queried.
Typically the skipped neurons will have lower IDs b/c of the BTreeMap, but that's not a guarantee of the API by design - it's only that if you call it with the same request but with 'start_from_neuron_id' that you got from the last response, it will give you another set.