Skip to content

Commit b746b9b

Browse files
Merge pull request #231 from permaweb/feat/node-processes
feat: `node-process@1.0`
2 parents d249065 + f23b9d3 commit b746b9b

File tree

11 files changed

+372
-57
lines changed

11 files changed

+372
-57
lines changed

docs/source-code-docs/dev_router.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ Calculate the minimum distance between two numbers
178178

179179
Load the current routes for the node. Allows either explicit routes from
180180
the node message's `routes` key, or dynamic routes generated by resolving the
181-
`routes_provider` message.
181+
`route_provider` message.
182182

183183
<a name="lowest_distance-1"></a>
184184

src/dev_local_name.erl

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22
%%% the node message to store a local cache of its known names, and the typical
33
%%% non-volatile storage of the node message to store the names long-term.
44
-module(dev_local_name).
5-
-export([lookup/3, register/3]).
5+
-export([info/1, lookup/3, register/3]).
6+
%%% HyperBEAM public (non-AO resolvable) functions.
7+
-export([direct_register/2]).
68
-include("include/hb.hrl").
79
-include_lib("eunit/include/eunit.hrl").
810

911
%%% The location that the device should use in the store for its links.
1012
-define(DEV_CACHE, <<"local-name@1.0">>).
1113

12-
%% @doc Takes a `key` argument and returns the value of the name, if it exists.
14+
%% @doc Export only the `lookup' and `register' functions.
15+
info(_Opts) ->
16+
#{
17+
excludes => [<<"direct_register">>, <<"keys">>, <<"set">>],
18+
default => fun default_lookup/4
19+
}.
20+
21+
%% @doc Takes a `key' argument and returns the value of the name, if it exists.
1322
lookup(_, Req, Opts) ->
1423
Key = hb_ao:get(<<"key">>, Req, no_key_specified, Opts),
1524
?event(local_name, {lookup, Key}),
@@ -19,6 +28,10 @@ lookup(_, Req, Opts) ->
1928
Opts
2029
).
2130

31+
%% @doc Handle all other requests by delegating to the lookup function.
32+
default_lookup(Key, _, Req, Opts) ->
33+
lookup(Key, Req#{ <<"key">> => Key }, Opts).
34+
2235
%% @doc Takes a `key' and `value' argument and registers the name. The caller
2336
%% must be the node operator in order to register a name.
2437
register(_, Req, Opts) ->
@@ -31,30 +44,35 @@ register(_, Req, Opts) ->
3144
}
3245
};
3346
true ->
34-
case hb_cache:write(hb_ao:get(<<"value">>, Req, Opts), Opts) of
35-
{ok, MsgPath} ->
36-
hb_cache:link(
37-
MsgPath,
38-
LinkPath =
39-
[
40-
?DEV_CACHE,
41-
Name = hb_ao:get(<<"key">>, Req, Opts)
42-
],
43-
Opts
44-
),
45-
load_names(Opts),
46-
?event(
47-
local_name,
48-
{registered,
49-
Name,
50-
{link, LinkPath},
51-
{msg, MsgPath}
52-
}
53-
),
54-
{ok, <<"Registered.">>};
55-
{error, _} ->
56-
not_found
57-
end
47+
direct_register(Req, Opts)
48+
end.
49+
50+
%% @doc Register a name without checking if the caller is an operator. Exported
51+
%% for use by other devices, but not publicly available.
52+
direct_register(Req, Opts) ->
53+
case hb_cache:write(hb_ao:get(<<"value">>, Req, Opts), Opts) of
54+
{ok, MsgPath} ->
55+
hb_cache:link(
56+
MsgPath,
57+
LinkPath =
58+
[
59+
?DEV_CACHE,
60+
Name = hb_ao:get(<<"key">>, Req, Opts)
61+
],
62+
Opts
63+
),
64+
load_names(Opts),
65+
?event(
66+
local_name,
67+
{registered,
68+
Name,
69+
{link, LinkPath},
70+
{msg, MsgPath}
71+
}
72+
),
73+
{ok, <<"Registered.">>};
74+
{error, _} ->
75+
not_found
5876
end.
5977

6078
%% @doc Returns a message containing all known names.
@@ -103,7 +121,7 @@ generate_test_opts() ->
103121
<<"prefix">> => "cache-TEST/"
104122
}
105123
],
106-
priv_wallet => hb:wallet()
124+
priv_wallet => ar_wallet:new()
107125
},
108126
Opts.
109127

src/dev_lua.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ compute(Key, RawBase, Req, Opts) ->
119119
?event(debug_lua, ensure_initialized_done),
120120
% Get the state from the base message's private element.
121121
OldPriv = #{ <<"state">> := State } = hb_private:from_message(Base),
122-
% NOTE: looks like the script is injected in multiple places, does the script need to be passed?
122+
% TODO: looks like the script is injected in multiple places, does the
123+
% script need to be passed?
123124
% Get the Lua function to call from the base message.
124125
Function =
125126
hb_ao:get_first(
@@ -148,13 +149,12 @@ compute(Key, RawBase, Req, Opts) ->
148149
),
149150
?event(debug_lua, parameters_found),
150151
% Call the VM function with the given arguments.
151-
?event({calling_lua_function, {function, Function}, {args, Params}, {req, Req}}),
152-
?event(debug_lua, calling_lua_function),
152+
?event({calling_lua_func, {function, Function}, {args, Params}, {req, Req}}),
153+
?event(debug_lua, calling_lua_func),
153154
% ?event(debug_lua, {lua_params, Params}),
154155
case luerl:call_function_dec([Function], encode(Params), State) of
155156
{ok, [LuaResult], NewState} ->
156157
?event(debug_lua, got_lua_result),
157-
% ?event(debug_lua, {lua_result, {result_before_decoding, {explicit, LuaResult}}}),
158158
Result = decode(LuaResult),
159159
?event(debug_lua, decoded_result),
160160
{ok, Result#{

src/dev_name.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
info(_) ->
1515
#{
1616
default => fun resolve/4,
17-
exclude => [<<"keys">>, <<"set">>]
17+
excludes => [<<"keys">>, <<"set">>]
1818
}.
1919

2020
%% @doc Resolve a name to its corresponding value. The name is given by the key

src/dev_node_process.erl

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
%%% @doc A device that implements the singleton pattern for processes specific
2+
%%% to an individual node. This device uses the `local-name@1.0' device to
3+
%%% register processes with names locally, persistenting them across reboots.
4+
%%%
5+
%%% Definitions of singleton processes are expected to be found with their
6+
%%% names in the `node_processes' section of the node message.
7+
-module(dev_node_process).
8+
-export([info/1]).
9+
-include("include/hb.hrl").
10+
-include_lib("eunit/include/eunit.hrl").
11+
12+
%% @doc Register a default handler for the device. Inherits `keys' and `set'
13+
%% from the default device.
14+
info(_Opts) ->
15+
#{
16+
default => fun lookup/4,
17+
excludes => [<<"set">>, <<"keys">>]
18+
}.
19+
20+
%% @doc Lookup a process by name.
21+
lookup(Name, _Base, Req, Opts) ->
22+
?event(node_process, {lookup, {name, Name}}),
23+
LookupRes =
24+
hb_ao:resolve(
25+
#{ <<"device">> => <<"local-name@1.0">> },
26+
#{ <<"path">> => <<"lookup">>, <<"key">> => Name, <<"load">> => true },
27+
Opts
28+
),
29+
case LookupRes of
30+
{ok, ProcessID} ->
31+
hb_cache:read(ProcessID, Opts);
32+
{error, not_found} ->
33+
case hb_ao:get(<<"spawn">>, Req, true, Opts) of
34+
true ->
35+
spawn_register(Name, Opts);
36+
false ->
37+
{error, not_found}
38+
end
39+
end.
40+
41+
%% @doc Spawn a new process according to the process definition found in the
42+
%% node message, and register it with the given name.
43+
spawn_register(Name, Opts) ->
44+
case hb_opts:get(node_processes, #{}, Opts) of
45+
#{ Name := BaseDef } ->
46+
% We have found the base process definition. Augment it with the
47+
% node's address as necessary, then commit to the result.
48+
?event(node_process, {registering, {name, Name}, {base_def, BaseDef}}),
49+
Signed = hb_message:commit(augment_definition(BaseDef, Opts), Opts),
50+
ID = hb_message:id(Signed, signed, Opts),
51+
?event(node_process, {spawned, {name, Name}, {process, Signed}}),
52+
% `POST` to the schedule device for the process to start its sequence.
53+
{ok, Assignment} =
54+
hb_ao:resolve(
55+
Signed,
56+
#{
57+
<<"path">> => <<"schedule">>,
58+
<<"method">> => <<"POST">>,
59+
<<"body">> => Signed
60+
},
61+
Opts
62+
),
63+
?event(node_process, {initialized, {name, Name}, {assignment, Assignment}}),
64+
RegResult =
65+
dev_local_name:direct_register(
66+
#{ <<"key">> => Name, <<"value">> => ID },
67+
Opts
68+
),
69+
?event(node_process, {registered, {name, Name}, {process_id, ID}}),
70+
case RegResult of
71+
{ok, _} ->
72+
{ok, Signed};
73+
{error, Err} ->
74+
{error, #{
75+
<<"status">> => 500,
76+
<<"body">> => <<"Failed to register process.">>,
77+
<<"details">> => Err
78+
}}
79+
end;
80+
_ ->
81+
% We could not find the base process definition for the given name
82+
% in the node message.
83+
{error, not_found}
84+
end.
85+
86+
%% @doc Augment the given process definition with the node's address.
87+
augment_definition(BaseDef, Opts) ->
88+
Address =
89+
hb_util:human_id(
90+
ar_wallet:to_address(
91+
hb_opts:get(priv_wallet, no_viable_wallet, Opts)
92+
)
93+
),
94+
hb_ao:set(
95+
BaseDef,
96+
#{
97+
<<"scheduler">> => Address
98+
}
99+
).
100+
101+
%%% Tests
102+
103+
%%% The name that should be used for the singleton process during tests.
104+
-define(TEST_NAME, <<"test-node-process">>).
105+
106+
%% @doc Helper function to generate a test environment and its options.
107+
generate_test_opts() ->
108+
{ok, Script} = file:read_file(<<"test/test.lua">>),
109+
generate_test_opts(#{
110+
?TEST_NAME => #{
111+
<<"device">> => <<"process@1.0">>,
112+
<<"execution-device">> => <<"lua@5.3a">>,
113+
<<"scheduler-device">> => <<"scheduler@1.0">>,
114+
<<"script">> => Script
115+
}
116+
}).
117+
generate_test_opts(Defs) ->
118+
#{
119+
store =>
120+
[
121+
#{
122+
<<"store-module">> => hb_store_fs,
123+
<<"prefix">> =>
124+
<<
125+
"cache-TEST-",
126+
(integer_to_binary(os:system_time(millisecond)))/binary
127+
>>
128+
}
129+
],
130+
node_processes => Defs,
131+
priv_wallet => ar_wallet:new()
132+
}.
133+
134+
lookup_no_spawn_test() ->
135+
Opts = generate_test_opts(),
136+
?assertEqual(
137+
{error, not_found},
138+
lookup(<<"name1">>, #{}, #{}, Opts)
139+
).
140+
141+
lookup_spawn_test() ->
142+
Opts = generate_test_opts(),
143+
Res1 = {_, Process1} =
144+
hb_ao:resolve(
145+
#{ <<"device">> => <<"node-process@1.0">> },
146+
?TEST_NAME,
147+
Opts
148+
),
149+
?assertMatch(
150+
{ok, #{ <<"device">> := <<"process@1.0">> }},
151+
Res1
152+
),
153+
{ok, Process2} = hb_ao:resolve(
154+
#{ <<"device">> => <<"node-process@1.0">> },
155+
?TEST_NAME,
156+
Opts
157+
),
158+
?assertEqual(Process1, Process2).
159+
160+
%% @doc Test that a process can be spawned, executed upon, and its result retrieved.
161+
lookup_execute_test() ->
162+
Opts = generate_test_opts(),
163+
Res1 =
164+
hb_ao:resolve_many(
165+
[
166+
#{ <<"device">> => <<"node-process@1.0">> },
167+
?TEST_NAME,
168+
#{
169+
<<"path">> => <<"schedule">>,
170+
<<"method">> => <<"POST">>,
171+
<<"body">> =>
172+
hb_message:commit(
173+
#{
174+
<<"path">> => <<"compute">>,
175+
<<"test-key">> => <<"test-value">>
176+
},
177+
Opts
178+
)
179+
}
180+
],
181+
Opts
182+
),
183+
?assertMatch(
184+
{ok, #{ <<"slot">> := 1 }},
185+
Res1
186+
),
187+
?assertMatch(
188+
42,
189+
hb_ao:get(
190+
<< ?TEST_NAME/binary, "/now/results/output/body" >>,
191+
#{ <<"device">> => <<"node-process@1.0">> },
192+
Opts
193+
)
194+
).

src/dev_process.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ info(_Msg1) ->
6969
worker => fun dev_process_worker:server/3,
7070
grouper => fun dev_process_worker:group/3,
7171
await => fun dev_process_worker:await/5,
72-
exclude => [
72+
excludes => [
7373
<<"test">>,
7474
<<"init">>,
7575
<<"ping_ping_script">>,

0 commit comments

Comments
 (0)