-
Notifications
You must be signed in to change notification settings - Fork 73
fix: Multiple store read #552
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
base: edge
Are you sure you want to change the base?
Changes from all commits
346498f
7b50d9a
71d5b76
101961c
2ae1845
b57ebab
af78376
8a7eedc
f5a9700
9e8da39
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 |
|---|---|---|
|
|
@@ -188,8 +188,7 @@ list(Path, Opts) when is_map(Opts) and not is_map_key(<<"store-module">>, Opts) | |
| list(Path, Store) | ||
| end; | ||
| list(Path, Store) -> | ||
| ResolvedPath = hb_store:resolve(Store, Path), | ||
| case hb_store:list(Store, ResolvedPath) of | ||
| case hb_store_common:resolved_list(Store, Path) of | ||
| {ok, Names} -> Names; | ||
| {error, _} -> []; | ||
| not_found -> [] | ||
|
|
@@ -407,67 +406,79 @@ do_read_commitment(Path, Opts) -> | |
|
|
||
| %% @doc Load all of the commitments for a message into memory. | ||
| read_all_commitments(Msg, Opts) -> | ||
| Store = hb_opts:get(store, no_viable_store, Opts), | ||
| UncommittedID = hb_message:id(Msg, none, Opts#{ linkify_mode => discard }), | ||
| Store = hb_store:scope(hb_opts:get(store, no_viable_store, Opts), local), | ||
|
Author
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. On This can impact performance, since now it will request to the configured stores. By limiting the scope to |
||
| CurrentCommitments = hb_maps:get(<<"commitments">>, Msg, #{}, Opts), | ||
| FoundCommitments = read_all_commitments_by_store(Msg, Store, Opts), | ||
| NewCommitments = | ||
| hb_maps:merge( | ||
| CurrentCommitments, | ||
| maps:from_list(FoundCommitments) | ||
| ), | ||
| Msg#{ <<"commitments">> => NewCommitments }. | ||
|
|
||
| read_all_commitments_by_store(Msg, Store, Opts) when not is_list(Store) -> | ||
|
Author
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. With |
||
| read_all_commitments_by_store(Msg, [Store], Opts); | ||
| read_all_commitments_by_store(_Msg, [], _Opts) -> | ||
| []; | ||
| read_all_commitments_by_store(Msg, [Store | ReaminingStores], Opts) -> | ||
| CurrentCommitments = hb_maps:get(<<"commitments">>, Msg, #{}, Opts), | ||
| AlreadyLoaded = hb_maps:keys(CurrentCommitments, Opts), | ||
| UncommittedID = hb_message:id(Msg, none, Opts#{ linkify_mode => discard }), | ||
| CommitmentsPath = | ||
| hb_store:resolve( | ||
| Store, | ||
| hb_store:path(Store, [UncommittedID, <<"commitments">>]) | ||
| ), | ||
| FoundCommitments = | ||
| case hb_store:list(Store, CommitmentsPath) of | ||
| {ok, CommitmentIDs} -> | ||
| lists:filtermap( | ||
| fun(CommitmentID) -> | ||
| ShouldLoad = not lists:member(CommitmentID, AlreadyLoaded), | ||
| ResolvedCommPath = | ||
| hb_store:path( | ||
| Store, | ||
| [CommitmentsPath, CommitmentID] | ||
| ), | ||
| case ShouldLoad andalso do_read_commitment(ResolvedCommPath, Opts) of | ||
| {ok, Commitment} -> | ||
| case hb_store:list(Store, CommitmentsPath) of | ||
| {ok, CommitmentIDs} -> | ||
| lists:filtermap( | ||
| fun(CommitmentID) -> | ||
| ShouldLoad = not lists:member(CommitmentID, AlreadyLoaded), | ||
| ResolvedCommPath = | ||
| hb_store:path( | ||
| Store, | ||
| [CommitmentsPath, CommitmentID] | ||
| ), | ||
| case ShouldLoad andalso do_read_commitment(ResolvedCommPath, Opts#{store => Store}) of | ||
| {ok, Commitment} -> | ||
| { | ||
| true, | ||
| { | ||
| true, | ||
| { | ||
| CommitmentID, | ||
| ensure_all_loaded( | ||
| Commitment, | ||
| Opts#{ commitment => true } | ||
| ) | ||
| } | ||
| }; | ||
| _ -> | ||
| false | ||
| end | ||
| end, | ||
| CommitmentIDs | ||
| ); | ||
| not_found -> | ||
| [] | ||
| end, | ||
| NewCommitments = | ||
| hb_maps:merge( | ||
| CurrentCommitments, | ||
| maps:from_list(FoundCommitments) | ||
| ), | ||
| Msg#{ <<"commitments">> => NewCommitments }. | ||
| CommitmentID, | ||
| ensure_all_loaded( | ||
| Commitment, | ||
| Opts#{ commitment => true } | ||
| ) | ||
| } | ||
| }; | ||
| _ -> | ||
| false | ||
| end | ||
| end, | ||
| CommitmentIDs | ||
| ); | ||
| not_found -> | ||
| read_all_commitments_by_store(Msg, ReaminingStores, Opts) | ||
| end. | ||
|
|
||
| %% @doc List all of the subpaths of a given path and return a map of keys and | ||
| %% links to the subpaths, including their types. | ||
| store_read(Path, Store, Opts) -> | ||
| store_read(Path, Path, Store, Opts). | ||
| store_read(_Target, _Path, no_viable_store, _) -> | ||
| not_found; | ||
| store_read(Target, Path, Store, Opts) -> | ||
| store_read(_Target, _Path, [], _) -> | ||
| not_found; | ||
| store_read(Target, Path, Store, Opts) when is_map(Store) -> | ||
| store_read(Target, Path, [Store], Opts); | ||
| store_read(Target, Path, [Store | RemainingStores], Opts) -> | ||
| ResolvedFullPath = hb_store:resolve(Store, PathBin = hb_path:to_binary(Path)), | ||
| ?event({reading, | ||
| {original_path, {string, PathBin}}, | ||
| {fully_resolved_path, ResolvedFullPath}, | ||
| {store, Store} | ||
| }), | ||
| case hb_store:type(Store, ResolvedFullPath) of | ||
| ResolvedFullPathContent = case hb_store:type(Store, ResolvedFullPath) of | ||
| not_found -> not_found; | ||
| simple -> | ||
| ?event({reading_data, ResolvedFullPath}), | ||
|
|
@@ -505,10 +516,14 @@ store_read(Target, Path, Store, Opts) -> | |
| } | ||
| ), | ||
| {ok, Msg}; | ||
| _ -> | ||
| not_found -> | ||
| ?event({empty_composite_message, ResolvedFullPath}), | ||
| {ok, #{}} | ||
| end | ||
| end, | ||
| case ResolvedFullPathContent of | ||
| {ok, _} = Response -> Response; | ||
| not_found -> store_read(Target, Path, RemainingStores, Opts) | ||
| end. | ||
|
|
||
| %% @doc Prepare a set of links from a listing of subpaths. | ||
|
|
@@ -1050,6 +1065,7 @@ test_match_typed_message(Store) -> | |
|
|
||
| cache_suite_test_() -> | ||
| hb_store:generate_test_suite([ | ||
| {"store ans104 message", fun test_store_ans104_message/1}, | ||
|
Author
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 test wasn't being used (maybe deleted by mistake?). Re-add it. |
||
| {"store unsigned empty message", | ||
| fun test_store_unsigned_empty_message/1}, | ||
| {"store binary", fun test_store_binary/1}, | ||
|
|
@@ -1082,3 +1098,22 @@ test_device_map_cannot_be_written_test() -> | |
| run_test() -> | ||
| Store = hb_test_utils:test_store(hb_store_lmdb), | ||
| test_match_typed_message(Store). | ||
|
|
||
| %% @doc Read value from Store1 and Store2 when is only available in Store2 | ||
| multiple_stores_store_read_test() -> | ||
| [_Store1, Store2] = Stores = hb_store_common:get_multiple_stores(), | ||
| %% Write test data | ||
| hb_store:make_group(Store2, <<"group1">>), | ||
| hb_store:write(Store2, <<"data/final_id">>, <<"data">>), | ||
| hb_store:make_link(Store2, <<"data/final_id">>, <<"group1/data">>), | ||
| hb_store:make_link(Store2, <<"group1">>, <<"random_id">>), | ||
| %% Check result | ||
| Opts = #{}, | ||
| Path = <<"random_id">>, | ||
| Content = store_read(Path, Stores, Opts), | ||
| try | ||
| ?assertMatch({ok, #{<<"data">> := _}}, Content) | ||
| after | ||
| hb_store_common:shutdown_stores(Stores) | ||
| end. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| %% @doc Store standard library | ||
| %% | ||
| %% Common patterns to access Stores (File System, LMDB, etc). | ||
|
|
||
| -module(hb_store_common). | ||
| -export([resolved_list/2, resolved_type/2]). | ||
| -export([get_multiple_stores/0, shutdown_stores/1]). | ||
| -include("include/hb.hrl"). | ||
| -include_lib("eunit/include/eunit.hrl"). | ||
|
|
||
| %% @doc Unify resolve and list functions into one call. | ||
| resolved_list(Stores, Path) when is_list(Stores) -> | ||
| do_resolved_list(Stores, Path); | ||
| resolved_list(Store, Path) -> | ||
| do_resolved_list([Store], Path). | ||
|
|
||
| do_resolved_list([], _Path) -> | ||
| not_found; | ||
| do_resolved_list([Store|RemainingStores], Path) -> | ||
| ResolvedPath = hb_store:resolve(Store, Path), | ||
| ?event({resolved_list, {path, Path}, {resolved_path, ResolvedPath}}), | ||
| case hb_store:list(Store, ResolvedPath) of | ||
| {ok, _} = Result -> Result; | ||
| not_found -> do_resolved_list(RemainingStores, Path) | ||
| end. | ||
|
|
||
| %% @doc Unify resolve and type functions into one call. | ||
| resolved_type(Stores, Path) when is_list(Stores) -> | ||
| do_resolved_type(Stores, Path); | ||
| resolved_type(Store, Path) -> | ||
| do_resolved_type([Store], Path). | ||
|
|
||
| do_resolved_type([], _Path) -> not_found; | ||
| do_resolved_type([Store|RemainingStores], Path) -> | ||
| ResolvedPath = hb_store:resolve(Store, Path), | ||
| ?event({resolved_type, {path, Path}, {resolved_path, ResolvedPath}}), | ||
| case hb_store:type(Store, ResolvedPath) of | ||
| Result when Result =/= not_found -> Result; | ||
| _ -> do_resolved_type(RemainingStores, Path) | ||
| end. | ||
|
|
||
| %% Tests | ||
|
|
||
| %% @doc Test that resolve and type must be made in the same store, | ||
| %% when multiple stores are provided. | ||
| resolved_type_test() -> | ||
| [_Store1, Store2] = Stores = get_multiple_stores(), | ||
| %% Write test data | ||
| hb_store:make_group(Store2, <<"group1">>), | ||
| hb_store:write(Store2, <<"data/final_id">>, <<"data">>), | ||
| hb_store:make_link(Store2, <<"data/final_id">>, <<"group1/data">>), | ||
| hb_store:make_link(Store2, <<"group1">>, <<"random_id">>), | ||
| %% Check result | ||
| RawPath = <<"random_id/data">>, | ||
| Result = resolved_type(Stores, RawPath), | ||
| try | ||
| ?assertEqual(simple, Result) | ||
| after | ||
| shutdown_stores(Stores) | ||
| end. | ||
|
|
||
| %% @doc Test that resolve and list must be made in the same store, | ||
| %% when multiple stores are provided. | ||
| resolved_list_test() -> | ||
| [_Store1, Store2] = Stores = get_multiple_stores(), | ||
| %% Write test data | ||
| hb_store:make_group(Store2, <<"group1">>), | ||
| hb_store:make_group(Store2, <<"group1/group12">>), | ||
| hb_store:write(Store2, <<"data/final_id2">>, <<"7890">>), | ||
| %% Link | ||
| hb_store:make_link(Store2, <<"data/final_id2">>, <<"group1/group12/data">>), | ||
|
Author
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. I'm not sure if this structure is possible in HB, but in this scenario, the old |
||
| hb_store:make_link(Store2, <<"group1">>, <<"random_id">>), | ||
| %% Check result | ||
| RawPath = <<"random_id/group12">>, | ||
| Result = resolved_list(Stores, RawPath), | ||
| try | ||
| ?assertEqual({ok, [<<"data">>]}, Result) | ||
| after | ||
| shutdown_stores(Stores) | ||
| end. | ||
|
|
||
| %% Test utilities | ||
|
|
||
| %% @doc Initialize multiple stores | ||
| get_multiple_stores() -> | ||
| get_multiple_stores(hb_store_lmdb). | ||
| get_multiple_stores(StoreModule) -> | ||
| Store1 = hb_test_utils:test_store(StoreModule, <<"store1">>), | ||
| Store2 = hb_test_utils:test_store(StoreModule, <<"store2">>), | ||
| hb_store:reset(Store1), | ||
| hb_store:reset(Store2), | ||
| [Store1, Store2]. | ||
|
|
||
| %% @doc Shutdown multiple stores | ||
| shutdown_stores([]) -> ok; | ||
| shutdown_stores([Store | RemainingStores]) -> | ||
| hb_store:reset(Store), | ||
| hb_store:stop(Store), | ||
| shutdown_stores(RemainingStores). | ||
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.
I didn't see a use case to return an empty array. The LMDB store didn't return
not_found, but thehb_store_fsdid, so swapping these two stores in thedev_query_test_vectors:simple_blocks_query_testwill make the test fail (which shouldn't happen).