Our database has some index out of sync bugs.
What is the issue
Steps to reproduce the bug
- Insert two json files with the following format
{
"id": 1,
"age": 70,
"name": "Josh",
},
{
"id": 2,
"age": 80,
"name": "David"
}
Let's assume the id of these two Unit are 1 and 2.
- Create Index
We call execute_create_index to create index for current grouping.
Till so far, our index works as expected. We can query our db with select by age=70, or select by age=80, we will get the expected result.
- Now we will use the unique feature in ImmuxDB, we insert following json file:
{
"id": 1,
"age": 100,
"name": "Josh",
}
We are changing the age from 70 to 100.
- If we query our db with
select by age=70, or select by age=100, both of the query will get the result:
{
"id": 1,
"age": 100,
"name": "Josh",
}
Which is not the expected result of select by age=70.
This bug will also lead to the problem of versioning, similar to the above example, after we call execute_create_index, if we do a revert operation, the index will have the result like above, which is out of sync.
The reason behind this bug
- When we call
execute_create_index, we are actually only creating index for current db, let's have a look in our execute_create_index function.
let prefix: StoreKeyFragment = grouping.marshal().into();
let get_by_prefix = Instruction::DataAccess(DataInstruction::Read(
DataReadInstruction::GetMany(GetManyInstruction {
height: None,
targets: GetManyTargetSpec::KeyPrefix(prefix),
}),
));
We are using prefix here to extract all the unit in current grouping, which is the "newest" unit for each unit_id, our history data are stored in the InstructionRecord, which are not being "indexed".
- When we insert a new
unit which has the same unit_id as the previous unit (step 3 in the above example), filed age is already being recorded in indexed_names, the updates_for_index will be called:
let reverse_index: ReverseIndex = {
let mut index = ReverseIndex::new();
for target in &insert.targets {
match &target.content {
UnitContent::JsonString(json_string) => {
match serde_json::from_str::<JsonValue>(json_string) {
Err(_error) => continue,
Ok(json_value) => {
for name in indexed_names.clone() {
index.index_new_json(target.id, &json_value, &name)?;
}
}
}
}
_ => continue,
}
}
index
};
if the new inserted unit has the same unit_id as before, which will leads to both old id_list_key and the new id_list_key will point to the same unit_id.
The versioning problem also comes out from this reason, when we do revert operation, essentialy, we just insert a new unit with some existed unit_id.
but we didn't update our index properly in the following code:
match self.get_journal(key) {
Err(error) => Err(error),
Ok(mut journal) => {
match find_appropriate_height(&journal.update_heights, &target_height) {
None => Err(VkvError::CannotFindSuitableVersion.into()),
Some(height) => {
// didn't update index here.
let value = self.get_value_after_height(key, &height)?;
journal.update_heights.push(next_height);
journal.value = value;
self.set_journal(key, &journal)
}
}
}
}
How to fix it.
The essential problem of this bug is that the index is out of sync, only the "newest" unit are indexed when we call "execute_create_index", and then when "new" unit with the same unit_id are inserted, our index system is not updated properly, this particular unit_id are existed in both id_list(corresponding id_list_key are get_store_key_of_indexed_id_list(&insert.grouping, &property_name, &old_unit_content); and get_store_key_of_indexed_id_list(&insert.grouping, &property_name, &new_unit_content);).
I have two ideas how to solve this problem:
Solution 1
When we call "execute_create_index", we create index for everything including history data. This solution requires that we need to scan all history data as well. This can be achieved by following steps:
- We read all the
units in the specific grouping(We can achieve this by prefix).
- We have all the
unit_id from step 1, and for each unit_id, we do inspect.
- For each result of
inspect is the historical data regarding to that specific unit_id, at this point, we can know these informations: grouping, unit_id, unit_content, NameProperty, as well as its chain_height.
- Instead of saving the
unit_id only, we should also pass in chain_height as well.
pub fn add_to_index(&mut self, name: &PropertyName, property_bytes: &[u8], id: UnitId) -> () {
let key = (name.to_owned(), property_bytes.to_owned());
match self.inner.get(&key) {
Some(id_list) => {
let mut new_list = id_list.to_owned();
// new_list.push(id); Instead of only saving the id
// we should save the combination of (unit_id, chain_height)
self.inner.insert(key, new_list)
}
None => {
// let new_list = IdList::new(vec![id]);
// same as above
self.inner.insert(key, new_list)
}
};
}
So, we are not only saving unit_id but also saving its corresponding chain_height together. I don't
have a proper name for the combination of (unit_id, chain_height) for now, if you can help with naming, that would be great.
When we are query our DB with SelectCondition::NameProperty, what we get will be a list of the combination of (unit_id, chain_height), so, the following problem is how to get the unit when we know its unit_id and chain_height. We don't have a specific API for this kind of query yet, but we can use inspect to get it.
For this solution, we also need to remember to update our index when we do revert operation.
Solution 2
We only keep the "newest" index, for each corresponding unit_id, we don't support SelectCondition::NameProperty for its historical data.
In the above example, when we query our db with select by age=70, or select by age=100, both of the query will get the result:
{
"id": 1,
"age": 100,
"name": "Josh",
}
but with solution 2, we will only get the above result if we do select by age=100. We don't get the above result if we query our db with select by age=70. At least our db is correct in this way.
To achieve this, we need to keep our index update to date, which means we can keep our create_index as it is for now, but every time when we insert a new unit, or we revert unit or units, we need to update our db's index.
To update our index, we need to do a read operation regarding to this unit_id first, find out what is the unit_contents of this unit_id in current db, then, we go through all the NameProperty, we need to do a query SelectCondition::NameProperty, and delete its unit_id in the corresponding indexed_id_list.
After we call execute_create_index, We need to do this update_index operation in several places in our codebase.
- Everytime when we insert a new
unit or multi units.
- Everytime when we revert a
unit or multi units.
Comparison
Solution 1:
Pros:
- Give us really nice query feature, we can query historical data by
SelectCondition::NameProperty as well. the cost in insert and revert is lower then solution 2.
Cons:
- really heavy work load at the
execute_create_index period, it go through all the historical data in the whole grouping.
- a huge problem when we do
SelectCondition::NameProperty on some "hot" data, since everytime we query by NameProperty, for each existed unit_id and chian_height, we involve one inspect operation, which is really costly.
Solution 2:
Pros:
- not that much work load at the
execute_create_index period.
SelectCondition::NameProperty is not costly.
Cons:
- We lost the feature that we can index historical data.
- Since we need to update our
indexed_id_list, everytime when we insert a new unit or units, or revert a unit or units, before we can actually perform these operations, we need to do following steps:
(I) A read operation regarding to this unit_id.
(II) Go through all the NameProperty, for each NameProperty, do a query SelectCondition::NameProperty.
(III) Delete its unit_id in the corresponding indexed_id_list
insert and revert operation in solution 2 is costly then solution 1.
Our database has some index out of sync bugs.
What is the issue
Steps to reproduce the bug
{ "id": 1, "age": 70, "name": "Josh", }, { "id": 2, "age": 80, "name": "David" }Let's assume the
idof these twoUnitare1and2.We call
execute_create_indexto create index for currentgrouping.Till so far, our index works as expected. We can query our db with
selectbyage=70, orselectbyage=80, we will get the expected result.{ "id": 1, "age": 100, "name": "Josh", }We are changing the age from 70 to 100.
selectbyage=70, orselectbyage=100, both of the query will get the result:{ "id": 1, "age": 100, "name": "Josh", }Which is not the expected result of
selectbyage=70.This bug will also lead to the problem of versioning, similar to the above example, after we call
execute_create_index, if we do arevertoperation, the index will have the result like above, which is out of sync.The reason behind this bug
execute_create_index, we are actually only creating index for current db, let's have a look in ourexecute_create_indexfunction.We are using
prefixhere to extract all theunitin current grouping, which is the "newest"unitfor eachunit_id, our history data are stored in theInstructionRecord, which are not being "indexed".unitwhich has the sameunit_idas the previous unit (step 3 in the above example), filedageis already being recorded inindexed_names, theupdates_for_indexwill be called:if the new inserted
unithas the sameunit_idas before, which will leads to both oldid_list_keyand the newid_list_keywill point to the sameunit_id.The versioning problem also comes out from this reason, when we do
revertoperation, essentialy, we just insert a newunitwith some existedunit_id.but we didn't update our index properly in the following code:
How to fix it.
The essential problem of this bug is that the index is out of sync, only the "newest" unit are indexed when we call
"execute_create_index", and then when "new"unitwith the sameunit_idare inserted, our index system is not updated properly, this particularunit_idare existed in bothid_list(correspondingid_list_keyareget_store_key_of_indexed_id_list(&insert.grouping, &property_name, &old_unit_content);andget_store_key_of_indexed_id_list(&insert.grouping, &property_name, &new_unit_content);).I have two ideas how to solve this problem:
Solution 1
When we call
"execute_create_index", we create index for everything including history data. This solution requires that we need to scan all history data as well. This can be achieved by following steps:unitsin the specificgrouping(We can achieve this byprefix).unit_idfrom step 1, and for eachunit_id, we doinspect.inspectis the historical data regarding to that specificunit_id, at this point, we can know these informations:grouping,unit_id,unit_content,NameProperty, as well as itschain_height.unit_idonly, we should also pass inchain_heightas well.So, we are not only saving
unit_idbut also saving its correspondingchain_heighttogether. I don'thave a proper name for the combination of (
unit_id,chain_height) for now, if you can help with naming, that would be great.When we are query our DB with
SelectCondition::NameProperty, what we get will be a list of the combination of (unit_id,chain_height), so, the following problem is how to get theunitwhen we know itsunit_idandchain_height. We don't have a specific API for this kind of query yet, but we can useinspectto get it.For this solution, we also need to remember to update our index when we do
revertoperation.Solution 2
We only keep the "newest" index, for each corresponding
unit_id, we don't supportSelectCondition::NamePropertyfor its historical data.In the above example, when we query our db with
selectbyage=70, orselectbyage=100, both of the query will get the result:{ "id": 1, "age": 100, "name": "Josh", }but with solution 2, we will only get the above result if we do
selectbyage=100. We don't get the above result if we query our db withselectbyage=70. At least our db is correct in this way.To achieve this, we need to keep our index update to date, which means we can keep our
create_indexas it is for now, but every time when we insert a new unit, or we revert unit or units, we need to update our db's index.To update our index, we need to do a read operation regarding to this
unit_idfirst, find out what is theunit_contents of thisunit_idin current db, then, we go through all theNameProperty, we need to do a querySelectCondition::NameProperty, and delete itsunit_idin the correspondingindexed_id_list.After we call
execute_create_index, We need to do thisupdate_indexoperation in several places in our codebase.unitor multiunits.unitor multiunits.Comparison
Solution 1:
Pros:
SelectCondition::NamePropertyas well. the cost ininsertandrevertis lower then solution 2.Cons:
execute_create_indexperiod, it go through all the historical data in the whole grouping.SelectCondition::NamePropertyon some "hot" data, since everytime we query byNameProperty, for each existedunit_idandchian_height, we involve oneinspectoperation, which is really costly.Solution 2:
Pros:
execute_create_indexperiod.SelectCondition::NamePropertyis not costly.Cons:
indexed_id_list, everytime when we insert a newunitorunits, or revert aunitorunits, before we can actually perform these operations, we need to do following steps:(I) A read operation regarding to this
unit_id.(II) Go through all the
NameProperty, for eachNameProperty, do a querySelectCondition::NameProperty.(III) Delete its
unit_idin the correspondingindexed_id_listinsertandrevertoperation in solution 2 is costly then solution 1.