2121
2222-ifndef (ENABLE_S3 ).
2323-export ([start /1 ]).
24-
2524start (_ ) -> error (s3_profile_not_enabled ).
26-
2725-else .
2826-behaviour (hb_store ).
2927% % Store behavior callbacks
@@ -33,9 +31,6 @@ start(_) -> error(s3_profile_not_enabled).
3331-export ([path /2 , add_path /3 ]).
3432-export ([default_test_opts /0 , get_config /1 ]).
3533
36- % % Helper functions
37- -export ([match /2 ]).
38-
3934-include (" include/hb.hrl" ).
4035-include_lib (" erlcloud/include/erlcloud_aws.hrl" ).
4136
@@ -200,15 +195,15 @@ get_config(Opts) ->
200195 end .
201196
202197% % @doc Write a value to a key in S3.
203- -spec write (opts (), key (), value ()) -> ok .
198+ -spec write (opts (), key (), value ()) -> ok | no_return .
204199write (Opts , Key , Value ) when is_list (Key ) ->
205200 write (Opts , hb_store :join (Key ), Value );
206201write (Opts , Key , Value ) when is_binary (Key ) ->
207202 RetryAttempts = maps :get (<<" retry-attempts" >>, Opts , ? DEFAULT_RETRIES ),
208203 write (Opts , Key , Value , RetryAttempts ).
209204write (_Opts , Key , _Value , 0 ) ->
210205 ? event (warning , {max_retries_reached_s3_write , {key , Key }}),
211- ok ;
206+ erlang : error ({ max_retries_reached_s3_write , { key , Key }}) ;
212207write (Opts , Key , Value , AttemptsRemaining ) ->
213208 #{bucket := Bucket , config := Config } = get_config (Opts ),
214209 BucketStr = hb_util :list (Bucket ),
@@ -372,6 +367,7 @@ shard_key(Key) when is_binary(Key) andalso byte_size(Key) > ?SHARD_CUT ->
372367 _ ->
373368 shard_key_inner (Key )
374369 end ;
370+ % % Data and computer are more specific prefixes, that will not be sharded
375371shard_key (<<" data" >>) -> <<" data" >>;
376372shard_key (Key )->
377373 ShardCut = integer_to_binary (? SHARD_CUT ),
@@ -420,7 +416,6 @@ create_parent_groups(Opts, Current, [Next | Rest]) ->
420416 GroupKey = create_make_group_key (GroupPath ),
421417 GroupPathValue = read_direct (Opts , GroupPath ),
422418 GroupKeyValue = read_direct (Opts , GroupKey ),
423-
424419 % Only create group if it doesn't already exist.
425420 case {GroupPathValue , GroupKeyValue } of
426421 {not_found , not_found } ->
@@ -649,7 +644,6 @@ resolve_path_accumulate(Opts, [Head|Tail], AccPath, Depth) ->
649644 % Replace the accumulated path with the link target and
650645 % continue with remaining segments
651646 NewPath = LinkSegments ++ Tail ,
652-
653647 resolve_path_segments (Opts , NewPath , Depth + 1 );
654648 false ->
655649 resolve_path_accumulate (Opts , Tail , [Head | AccPath ], Depth )
@@ -696,6 +690,7 @@ reset(Opts) ->
696690 {error , reset_not_confirmed }
697691 end .
698692
693+ % % @doc Delete all objects from a bucket.
699694delete_all_objects (Opts ) ->
700695 #{bucket := Bucket , config := Config } = get_config (Opts ),
701696 BucketStr = hb_util :list (Bucket ),
@@ -730,12 +725,6 @@ scope() -> local.
730725scope (#{ <<" scope" >> := Scope }) -> Scope ;
731726scope (_Opts ) -> scope ().
732727
733- % % @doc Match keys based on a template.
734- % % Simple implementation - just returns not_found for now.
735- -spec match (opts (), map ()) -> {ok , [binary ()]} | not_found .
736- match (_Opts , _Template ) ->
737- not_found .
738-
739728% % @doc Integration test suite demonstrating basic store operations.
740729% %
741730% % The following functions implement integration tests using EUnit to verify that
@@ -744,7 +733,6 @@ match(_Opts, _Template) ->
744733% % resolution, and type detection.
745734% %
746735% % Be sure that minio io server is running before executing the integration tests.
747-
748736default_test_opts () ->
749737 #{
750738 <<" store-module" >> => ? MODULE ,
@@ -764,32 +752,28 @@ default_test_opts() ->
764752% % This test creates a temporary database, writes a key-value pair, reads it
765753% % back to verify correctness, and cleans up by stopping the database. It
766754% % serves as a sanity check that the basic storage mechanism is working.
767-
768755init () ->
769756 % % Needed to use the gun http client used by HB.
770757 application :ensure_all_started (hb ).
771758
772- sharding_test () ->
759+ % % @doc Testing default operations for basic store functions.
760+ simple_test () ->
773761 init (),
774762 StoreOpts = default_test_opts (),
775763 start (StoreOpts ),
776764 reset (StoreOpts ),
777-
778765 Value = <<" value" >>,
779766 Key = <<" TWaabU6nSWpn7zPaiWSd9gQkfNwa1t3onrDRNB-bFiI" >>,
780767 % % Write
781768 ok = write (StoreOpts , Key , Value ),
782-
783769 % % Confirm sharding
784-
785770 #{bucket := Bucket , config := Config } = get_config (StoreOpts ),
786771 Result = erlcloud_s3 :head_object (
787772 hb_util :list (Bucket ),
788773 " TWaa/bU6nSWpn7zPaiWSd9gQkfNwa1t3onrDRNB-bFiI" ,
789774 Config
790775 ),
791776 ? assert (is_list (Result )),
792-
793777 % % Read
794778 {ok , Response } = read (StoreOpts , Key ),
795779 ? assertEqual (Value , Response ),
@@ -801,12 +785,14 @@ sharding_test() ->
801785 % Group
802786 GroupKey = <<" UDgFxz7qUcB_TijjDfhUpXD3UGXpw8Xq6OrpoDiv3Y0" >>,
803787 make_group (StoreOpts , GroupKey ),
804- write (StoreOpts , <<GroupKey /binary , " /" , " content-type" >>, <<" application/json" >>),
788+ Key2 = add_path (#{}, GroupKey , <<" content-type" >>),
789+ write (StoreOpts , Key2 , <<" application/json" >>),
805790 % % List
806791 R = list (StoreOpts , GroupKey ),
807792 ? assertEqual ({ok ,[<<" content-type" >>]}, R ),
808793 ok = stop (StoreOpts ).
809794
795+ % % @doc Testing reading a value where key doesn't exists.
810796not_found_test () ->
811797 init (),
812798 StoreOpts = default_test_opts (),
@@ -817,49 +803,60 @@ not_found_test() ->
817803 ? assertEqual (not_found , Result ),
818804 ok = stop (StoreOpts ).
819805
806+ % % @doc Testing starting store with a bucket that doesn't exists.
820807bucket_not_found_test () ->
821808 init (),
822809 StoreOpts = (default_test_opts ())#{<<" bucket" >> => <<" invalid_bucket" >>},
823810 ? assertError ({bucket_access_failed , {aws_error , {http_error , 400 , _ , _ }}}, start (StoreOpts )),
824811 ok = stop (StoreOpts ).
825812
813+ % % @doc Test a failure in writing a key/value to the store. It should
814+ % % retry writing it.
826815failed_write_test () ->
827816 init (),
828817 StoreOpts = (default_test_opts ())#{<<" retry-attempts" >> => 2 },
829818 start (StoreOpts ),
830819 reset (StoreOpts ),
831820 Key = hb_message :id (#{}),
832821 Value = <<" value" >>,
833-
822+ % Mock S3 dependency to return 400 error
834823 ok = meck :new (erlcloud_s3 , [unstick , passthrough ]),
835824 XMLBody = <<" " >>,
836825 ok = meck :expect (erlcloud_s3 , put_object , fun (_ , _ , _ , _ , _ ) ->
837826 error ({aws_error ,{http_error , 400 , " Bad Request" , XMLBody }}) end ),
838-
839- {Time , Result } = timer :tc (fun () -> write (StoreOpts , Key , Value ) end , second ),
827+ % Make sure it spends time retry write to the store
828+ {Time , Result } = timer :tc (fun () ->
829+ ? assertError (
830+ {max_retries_reached_s3_write , {key , Key }},
831+ write (StoreOpts , Key , Value )
832+ )
833+ end ,
834+ millisecond
835+ ),
840836 ? assertMatch (ok , Result ),
841- ? assert (Time >= 3 ),
842-
837+ ? assert (Time >= 3 * ? DEFAULT_RETRY_DELAY ),
843838 ? assert (meck :called (erlcloud_s3 , put_object , ['_' , '_' , '_' , '_' , '_' ])),
839+ % Unload and stop store
844840 ok = meck :unload (erlcloud_s3 ),
845841 ok = stop (StoreOpts ).
846842
843+ % % @doc Test a failure in reading a key from S3 datasource.
847844failed_read_test () ->
848845 init (),
849846 StoreOpts = default_test_opts (),
850847 start (StoreOpts ),
851848 reset (StoreOpts ),
852849 Key = hb_message :id (#{}),
853-
850+ % Mock S3 dependency to return 400 error
854851 ok = meck :new (erlcloud_s3 , [passthrough ]),
855852 XMLBody = <<" " >>,
856853 ok = meck :expect (erlcloud_s3 , get_object , fun (_ , _ , _ , _ ) ->
857854 error ({aws_error ,{http_error , 400 , " Bad Request" , XMLBody }}) end ),
858-
855+ % Read key and test result
859856 Result = read (StoreOpts , Key ),
860857 ? assertMatch (not_found , Result ),
861-
862858 ? assert (meck :called (erlcloud_s3 , get_object , ['_' , '_' , '_' , '_' ])),
859+ % Unload and stop store
863860 ok = meck :unload (erlcloud_s3 ),
864861 ok = stop (StoreOpts ).
865862
@@ -1058,7 +1055,7 @@ cache_style_test() ->
10581055 StoreOpts = default_test_opts (),
10591056 % Start the store
10601057 hb_store :start (StoreOpts ),
1061- reset (StoreOpts ),
1058+ hb_store : reset (StoreOpts ),
10621059 % Test writing through hb_store interface
10631060 ok = hb_store :write (StoreOpts , <<" test-key" >>, <<" test-value" >>),
10641061 % Test reading through hb_store interface
@@ -1275,17 +1272,18 @@ list_with_link_test() ->
12751272 ? assertEqual (ExpectedChildren , lists :sort (LinkChildren )),
12761273 stop (StoreOpts ).
12771274
1278- resolve_bellow_max_test () ->
1275+ % % Test if resolves link bellow the maxmimum number of link redirection.
1276+ resolve_bellow_max_redirect_test () ->
12791277 init (),
12801278 StoreOpts = default_test_opts (),
1281-
12821279 start (StoreOpts ),
12831280 reset (StoreOpts ),
1284-
1281+ % Create a group with a children
12851282 GroupName = hb_message :id (#{}),
12861283 make_group (StoreOpts , GroupName ),
12871284 Key1 = <<GroupName /binary , " /key" >>,
12881285 ok = write (StoreOpts , Key1 , <<" Value" >>),
1286+ % Create a link chain (a link that refers the previous one)
12891287 LastLink = lists :foldl (
12901288 fun (ID , Link ) ->
12911289 BinID = integer_to_binary (ID ),
@@ -1294,24 +1292,27 @@ resolve_bellow_max_test() ->
12941292 NewLink
12951293 end ,
12961294 GroupName ,
1297- lists :seq (1 , 88 )
1295+ lists :seq (1 , ? MAX_REDIRECTS )
12981296 ),
1297+ % Resolve link path
12991298 PathToResolve = <<LastLink /binary , " /key" >>,
13001299 Result = resolve (StoreOpts , PathToResolve ),
13011300 % Return the resolved link path
13021301 ? assertEqual (Key1 , Result ).
13031302
1304- resolve_above_max_test () ->
1303+ % % Test if returns the same path when number of redirection is above the
1304+ % % maximum defined.
1305+ resolve_above_max_redirect_test () ->
13051306 init (),
13061307 StoreOpts = default_test_opts (),
1307-
13081308 start (StoreOpts ),
13091309 reset (StoreOpts ),
1310-
1310+ % Create a group with a children
13111311 GroupName = hb_message :id (#{}),
13121312 make_group (StoreOpts , GroupName ),
13131313 Key1 = <<GroupName /binary , " /key" >>,
13141314 ok = write (StoreOpts , Key1 , <<" Value" >>),
1315+ % Create a link chain (a link that refers the previous one)
13151316 LastLink = lists :foldl (
13161317 fun (ID , Link ) ->
13171318 BinID = integer_to_binary (ID ),
@@ -1322,26 +1323,32 @@ resolve_above_max_test() ->
13221323 GroupName ,
13231324 lists :seq (1 , ? MAX_REDIRECTS + 1 )
13241325 ),
1326+ % Resolve link path
13251327 PathToResolve = <<LastLink /binary , " /key" >>,
13261328 Result = resolve (StoreOpts , PathToResolve ),
13271329 % Return the same path as given
13281330 ? assertEqual (PathToResolve , Result ).
13291331
1330- shard_key_test () ->
1332+ % % @doc Test invalid keys, defined by keys with length less or equal
1333+ % % to ?SHARD_CUT
1334+ invalid_sharded_key_test () ->
13311335 InvalidKey1 = <<" data/xpto" >>,
13321336 ? assertError ({invalid_key_for_sharding , <<" xpto" >>}, shard_key (InvalidKey1 )),
13331337 InvalidKey2 = <<" xpto" >>,
1334- ? assertException (error , {invalid_key_for_sharding , InvalidKey2 }, shard_key (InvalidKey2 )),
1338+ ? assertException (error , {invalid_key_for_sharding , InvalidKey2 }, shard_key (InvalidKey2 )).
1339+
1340+ % % @doc Shard valid keys.
1341+ shard_key_test () ->
13351342 Key1 = <<" data/xpto1" >>,
13361343 ? assertEqual (<<" data/xpto/1" >>, shard_key (Key1 )),
13371344 Key2 = <<" xpto1" >>,
1338- ? assertEqual (<<" xpto/1" >>, shard_key (Key2 )),
1339- % % Only first key is sharded.
1340- Key3 = << " xpto1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " >>,
1341- ? assertEqual (<< " xpto/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " >>, shard_key ( Key3 )),
1342- Key4 = <<" data/ xpto1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >>,
1343- ? assertEqual (<<" data/ xpto/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >>, shard_key (Key4 )),
1344- ok .
1345-
1345+ ? assertEqual (<<" xpto/1" >>, shard_key (Key2 )).
1346+
1347+ % % @doc Test that only shards the first Key (second when it starts with `data/`)
1348+ only_first_key_is_sharded_test () ->
1349+ Key1 = <<" xpto1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >>,
1350+ ? assertEqual (<<" xpto/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >>, shard_key (Key1 )),
1351+ Key2 = << " data/xpto1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " >>,
1352+ ? assertEqual (<< " data/xpto/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " >>, shard_key ( Key2 )).
13461353-endif .
13471354-endif .
0 commit comments