Refactored and enhanced the consolidation algorithm - #1101
Conversation
037a723 to
a337d7c
Compare
| ``[1,2], [1,4]``. Observe that, if those four fragments were to | ||
| be consolidated, the cells of the second and third fragment | ||
| would be completely overwritten from the cells of the | ||
| fourth fragment. Therefore, the existence of those two fragments |
There was a problem hiding this comment.
Two therefores in a row in copy
| be considered for consolidation. | ||
|
|
||
| - The third criterion is the fragment *amplification factor*, applicable to | ||
| the case where the fragment subset fo be consolidated contains at least |
| during consolidation. | ||
| ``"sm.consolidation.step_max_frags"`` ``"4294967295"`` The maximum number of fragments to consolidate in | ||
| a single step. | ||
| ``"sm.consolidation.step_min_frags"`` ``"4294967295"`` The minimum number of fragments to consolidate in |
There was a problem hiding this comment.
shouldn't the default be 2?
There was a problem hiding this comment.
This is up for discussion. There are no defaults that can simulate the current consolidation algorithm (i.e., force-consolidate all), as there may be scenarios that fragments are not "consolidatable". In that case, a default of 2 will attempt to consolidate at least 2 fragments. Is that desirable?
| a single step. | ||
| ``"sm.consolidation.step_min_frags"`` ``"4294967295"`` The minimum number of fragments to consolidate in | ||
| a single step. | ||
| ``"sm.consolidation.step_size_ratio"`` ``"0"`` The size ratio that two ("adjacent") fragments |
There was a problem hiding this comment.
Doesn't this mean the default config params cause consolidation to immediately terminate doing nothing?
There was a problem hiding this comment.
The algorithm permits two fragments to be consolidated if their ratio (always min / max) is larger than sm.consolidation.step_size_ratio. I'll clarify in the docs.
| ``"sm.consolidation.step_size_ratio"`` ``"0"`` The size ratio that two ("adjacent") fragments | ||
| must satisfy to be considered for consolidation in | ||
| a single step. | ||
| ``"sm.consolidation.steps"`` ``"4294967295"`` The number of consolidation steps to be performed |
There was a problem hiding this comment.
shouldn't this be 1 or a small number?
There was a problem hiding this comment.
Again, this is up for discussion. A huge number means that consolidation will proceed recursively until there is a single fragment, or if there is no next fragment subset that is "consolidatable".
| // Delete old fragment metadata. This makes the old fragments invisible | ||
| st = delete_old_fragment_metadata(old_fragment_uris); | ||
| st = delete_fragment_metadata(to_delete); | ||
| if (!st.ok()) { |
There was a problem hiding this comment.
If deleting the fragment metadata fails, shouldn't the behavior be to not delete the fragment data? It seems like the array could quickly get in an inconsistent state if this operation fails.
It might be cleaner to delete fragment metadata + fragments at a time, that way the errors are correlated with data loss.
There was a problem hiding this comment.
This one is tricky. Regardless of whether or not you delete the fragment data (after the fragment metadata deletion fails), the fragments whose metadata you did delete will not be visible to the read algorithm. The best idea would be to copy the fragment metadata in a temporary location, such that they can be recovered if this function fails. I would suggest we merge this PR and open a separate issue for this (i.e., on robustness/resilience), as testing will add complexity and more development time for this issue (which is already pretty large).
| return Status::Ok(); | ||
| } | ||
|
|
||
| Status StorageManager::get_fragment_info( |
There was a problem hiding this comment.
Should we check the on-disk format version of the array here?
There was a problem hiding this comment.
No, I don't think so, as this depends on format agnostic parameters. However, retrieving the timestamp from the fragment URI will have to be refactored when we swap the UUID and timestamp in a separate issue.
There was a problem hiding this comment.
the format defines the binary layout + on disk organization, the two are coupled
There was a problem hiding this comment.
Still I am not sure I understand why we would have to handle the format version here. We are not doing any parsing of the binary files in this function.
There was a problem hiding this comment.
Well the filename example is something that will cause a format number increment (so that we can keep reading old arrays). So @jakebolewski might be correct that some checks should be added here, even though the binary format is not touched.
There was a problem hiding this comment.
Regarding the fragment folder name format, this will be revisited when we swap the UUID with the timestamp. So we will put the necessary checks then. Right now, there is no violation of backwards compatibility.
| if (!open_array->fragment_metadata_exists(frag_uri) && | ||
| frag_timestamp <= timestamp) { | ||
| auto metadata = open_array->fragment_metadata(frag_uri); | ||
| if (metadata == nullptr) { // Fragment metadata does not exist - load it |
There was a problem hiding this comment.
check the on-disk format version here?
There was a problem hiding this comment.
I think that any format-specific stuff will be handled directly by FragmentMetadata. Now, if we change completely the format to drop fragment metadata as a whole, we will need to revisit this (but let's punt for now).
| return vfs_->move_dir(old_uri, new_uri); | ||
| } | ||
|
|
||
| Status StorageManager::get_fragment_info( |
There was a problem hiding this comment.
Should we check the on-disk format version of the array here?
There was a problem hiding this comment.
Same as above, I don't think so for now.
| // fragment sets in the middle of the timeline may get consolidated, | ||
| // which will hinder the next step of consolidation (which will | ||
| // select some small and some big fragments). | ||
| if (min_size == UINT64_MAX || m[i][j] < (min_size / 1.25)) { |
There was a problem hiding this comment.
should we document that the consolidation algorithm biases older fragments?
There was a problem hiding this comment.
Why are older fragments a good metric to bias? Given no domain overlap information each fragment would assume to contribute equally to degradation in read performance.
There was a problem hiding this comment.
Great catch - I was sure you were going to see that. :)
This is to prevent a subtle but important case, which I encountered when I was performing some perf analysis locally. Suppose that you have 10 fragments, each of roughly equal size. I am saying roughly, because even if you write the exact same number of cells in each fragment, if you use compression the actual fragment sizes may be slightly different.
Suppose also that we consolidate 5 at a time. if the algorithm chooses to consolidate 2-6 first, then the algorithm will next choose 1, 2-6, 7, 8, 9 into 1-9 (or 2-6, 7, 8, 9,10 into 2-10), and then 1-9, 10. Observe the imbalance. The ideal scenario would have been 1-5, 6-10 and then 1-5, 6-10. This condition biases towards "alignment", rather than "old" fragments.
| class T, | ||
| typename std::enable_if<!std::is_integral<T>::value, T>::type* = nullptr> | ||
| uint64_t cell_num(const T* rect, unsigned dim_num) { | ||
| assert(false); |
There was a problem hiding this comment.
What is the point of implementing this function? There is no definition generated for cell_num if T is non-integral, so I believe it would be already be a compile time error if you tried e.g. cell_num<float>(...)
There was a problem hiding this comment.
This is called in Consolidator::are_consolidatable, which applies to all T (integral and real). It takes proper care if all fragments are sparse (the only case where the domain might be real, as this is only applicable to sparse arrays). Therefore, the definition for non-integrals is needed. In general, a real domain can only be determined at run-time, so we need to provide function definitions for non-integral T in several places, but throw asserts to make sure the code never invokes them. I am open to alternatives if any, but let's address in a separate PR as this is not specific to consolidation (and may appear in other places as well).
|
|
||
| // Check the encryption key. Note we always pass true for cache hit by | ||
| // definition of reopening an array. | ||
| auto st = check_array_encryption_key( |
There was a problem hiding this comment.
This status is not checked! You can reopen an array with an arbitrary encryption key with this bug.
| RETURN_NOT_OK(vfs_->dir_size(uri.second, &size)); | ||
|
|
||
| // Get fragment non-empty domain | ||
| auto metadata = |
There was a problem hiding this comment.
Don't need new and delete for this local object -- can just do FragmentMetadata metadata(...); to stack allocate.
| return Status::Ok(); | ||
| } | ||
|
|
||
| Status StorageManager::get_fragment_info( |
There was a problem hiding this comment.
Well the filename example is something that will cause a format number increment (so that we can keep reading old arrays). So @jakebolewski might be correct that some checks should be added here, even though the binary format is not touched.
| RETURN_NOT_OK(vfs_->is_file(coords_uri, &sparse)); | ||
|
|
||
| // Get fragment non-empty domain | ||
| auto metadata = |
There was a problem hiding this comment.
You should stack allocate this object as in the previous case.
| return true; | ||
|
|
||
| // Check overlap of union with earlier fragments | ||
| for (int i = 0; i <= (int)start - 1; ++i) { |
There was a problem hiding this comment.
You can just do size_t i and i < start
| auto domain_size = 2 * array_schema->coords_size(); | ||
|
|
||
| // Allocate memory for union of non-empty domains of fragments to consolidate | ||
| auto union_non_empty_domains = (T*)std::malloc(domain_size); |
There was a problem hiding this comment.
Can we start using either stack allocations or std::unique_ptr<uint8_t> for these ad-hoc allocations? We shouldn't be introducing more malloc/frees than necessary.
| storage_manager_->vfs()->remove_dir(new_fragment_uri); | ||
| bool is_dir; | ||
| auto st2 = storage_manager_->vfs()->is_dir(*new_fragment_uri, &is_dir); | ||
| if (is_dir) |
There was a problem hiding this comment.
st2 also doesn't appear to be checked anywhere here.
| storage_manager_->vfs()->remove_dir(new_fragment_uri); | ||
| clean_up(buffer_num, buffers, buffer_sizes, query_r, query_w); | ||
| bool is_dir; | ||
| auto st2 = storage_manager_->vfs()->is_dir(*new_fragment_uri, &is_dir); |
There was a problem hiding this comment.
These two places handle a previous error status. Since we do not provide an error stack (yet), checking st2 here is redundant, since we will report only st. However, we need to initialize is_dir to false to properly handle the if command immediately below.
| } else if (i + j >= col_num) { // Non-valid entries | ||
| m[i][j] = UINT64_MAX; | ||
| } else { // Every other row is computed using the previous row | ||
| auto ratio = (float)fragments[i + (j - 1)].fragment_size_ / |
There was a problem hiding this comment.
Can this happen when j == 0? In that case we are doing i + (0 - 1).
There was a problem hiding this comment.
In this else block, it is always i>0, since the case of i==0 is handled in the first if block above.
There was a problem hiding this comment.
What about j? If it can be 0 then it will cause an OOB access into the fragments vector, since size_t is an unsigned type.
There was a problem hiding this comment.
Yeah, you are right, the parenthesis there may be problematic. I think that even with overflow this would work (as integers "wrap around"), but I will remove the parenthesis anyway.
bc231c6 to
9385576
Compare
9385576 to
c9e57a4
Compare
Closes #52