3434% % @doc Device function that returns all known routes.
3535routes (M1 , M2 , Opts ) ->
3636 ? event ({routes_msg , M1 , M2 }),
37- Routes = hb_opts : get ( routes , [], Opts ),
37+ Routes = load_routes ( Opts ),
3838 ? event ({routes , Routes }),
3939 case hb_ao :get (<<" method" >>, M2 , Opts ) of
4040 <<" POST" >> ->
@@ -75,9 +75,10 @@ routes(M1, M2, Opts) ->
7575% % If we have a route that has multiple resolving nodes, check
7676% % the load distribution strategy and choose a node. Supported strategies:
7777% % <pre>
78- % % All: Return all nodes (default).
79- % % Random: Distribute load evenly across all nodes, non-deterministically.
78+ % % All: Return all nodes (default).
79+ % % Random: Distribute load evenly across all nodes, non-deterministically.
8080% % By-Base: According to the base message's hashpath.
81+ % % By-Weight: According to the node's `weight' key.
8182% % Nearest: According to the distance of the node's wallet address to the
8283% % base message's hashpath.
8384% % </pre>
@@ -90,7 +91,7 @@ routes(M1, M2, Opts) ->
9091% % function, taking only the request message and the `Opts' map.
9192route (Msg , Opts ) -> route (undefined , Msg , Opts ).
9293route (_ , Msg , Opts ) ->
93- Routes = hb_opts : get ( routes , [], Opts ),
94+ Routes = load_routes ( Opts ),
9495 R = match_routes (Msg , Routes , Opts ),
9596 ? event ({find_route , {msg , Msg }, {routes , Routes }, {res , R }}),
9697 case (R =/= no_matches ) andalso hb_ao :get (<<" node" >>, R , Opts ) of
@@ -137,6 +138,21 @@ route(_, Msg, Opts) ->
137138 end
138139 end .
139140
141+ % % @doc Load the current routes for the node. Allows either explicit routes from
142+ % % the node message's `routes' key, or dynamic routes generated by resolving the
143+ % % `routes_provider' message.
144+ load_routes (Opts ) ->
145+ case hb_opts :get (routes_provider , not_found , Opts ) of
146+ not_found -> hb_opts :get (routes , [], Opts );
147+ RoutesProvider ->
148+ ProviderMsgs = hb_singleton :from (RoutesProvider ),
149+ ? event ({routes_provider , ProviderMsgs }),
150+ case hb_ao :resolve_many (ProviderMsgs , Opts ) of
151+ {ok , Routes } -> Routes ;
152+ {error , Error } -> throw ({routes , routes_provider_failed , Error })
153+ end
154+ end .
155+
140156% % @doc Extract the base message ID from a request message. Produces a single
141157% % binary ID that can be used for routing decisions.
142158extract_base (#{ <<" path" >> := Path }, Opts ) ->
@@ -231,6 +247,19 @@ choose(0, _, _, _, _) -> [];
231247choose (N , <<" Random" >>, _ , Nodes , _Opts ) ->
232248 Node = lists :nth (rand :uniform (length (Nodes )), Nodes ),
233249 [Node | choose (N - 1 , <<" Random" >>, nop , lists :delete (Node , Nodes ), _Opts )];
250+ choose (N , <<" By-Weight" >>, _ , Nodes , Opts ) ->
251+ NodesWithWeight =
252+ [
253+ { Node , hb_util :int (hb_ao :get (<<" weight" >>, Node , Opts )) }
254+ ||
255+ Node <- Nodes
256+ ],
257+ Node = hb_util :weighted_random (NodesWithWeight ),
258+ [
259+ Node
260+ |
261+ choose (N - 1 , <<" By-Weight" >>, nop , lists :delete (Node , Nodes ), Opts )
262+ ];
234263choose (N , <<" By-Base" >>, Hashpath , Nodes , Opts ) when is_binary (Hashpath ) ->
235264 choose (N , <<" By-Base" >>, binary_to_bignum (Hashpath ), Nodes , Opts );
236265choose (N , <<" By-Base" >>, HashInt , Nodes , Opts ) ->
@@ -303,6 +332,51 @@ binary_to_bignum(Bin) when ?IS_ID(Bin) ->
303332
304333% %% Tests
305334
335+ routes_provider_test () ->
336+ Node = hb_http_server :start_node (#{
337+ routes_provider => #{
338+ <<" path" >> => <<" /test-key/routes" >>,
339+ <<" test-key" >> => #{
340+ <<" routes" >> => [
341+ #{
342+ <<" template" >> => <<" *" >>,
343+ <<" node" >> => <<" testnode" >>
344+ }
345+ ]
346+ }
347+ }
348+ }),
349+ ? assertEqual (
350+ {ok , <<" testnode" >>},
351+ hb_http :get (Node , <<" /~r outer@1.0/routes/1/node" >>, #{})
352+ ).
353+
354+ dynamic_routes_provider_test () ->
355+ {ok , Script } = file :read_file (" test/test.lua" ),
356+ Node = hb_http_server :start_node (#{
357+ routes_provider => #{
358+ <<" device" >> => <<" lua@5.3a" >>,
359+ <<" path" >> => <<" routes" >>,
360+ <<" script" >> => Script ,
361+ <<" node" >> => <<" test-dynamic-node" >>
362+ }
363+ }),
364+ ? assertEqual (
365+ {ok , <<" test-dynamic-node" >>},
366+ hb_http :get (Node , <<" /~r outer@1.0/routes/1/node" >>, #{})
367+ ).
368+
369+ weighted_random_strategy_test () ->
370+ Nodes =
371+ [
372+ #{ <<" host" >> => <<" 1" >>, <<" weight" >> => 1 },
373+ #{ <<" host" >> => <<" 2" >>, <<" weight" >> => 99 }
374+ ],
375+ SimRes = simulate (1000 , 1 , Nodes , <<" By-Weight" >>),
376+ [One , _ ] = simulation_distribution (SimRes , Nodes ),
377+ ? assert (One < 20 ),
378+ ? assert (One > 5 ).
379+
306380strategy_suite_test_ () ->
307381 lists :map (
308382 fun (Strategy ) ->
0 commit comments