Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
Closes Step 10 (10a discovery + 10b webfinger + 10c fetch). New
next/kernel/discovery_fetch.erl produces a 1-arity FetchFn closure
suitable for peer_actors:lookup_or_fetch_srv/2, completing the
discovery half that Step 5c's peer_actors cache stubbed out.
discovery_fetch API:
make_fetch_fn(Cfg) -> fun((PeerId) -> {ok, AS} | {error, _})
fetch(Url, Cfg) -> {ok, AS} | {error, _}
actor_doc_url(BaseUrl, PeerAtom) -> <Base>/actors/<peer>
accept_header/0 -> <<"application/vnd.fed-sx.actor-doc">>
decode_body(Body) -> {ok, AS} | {error, bad_actor_doc}
Closure GETs <base>/actors/<peer> via the Step 8e BIF with
Accept = application/vnd.fed-sx.actor-doc, decodes the response
body via term_codec:decode/1, returns the peer-actor-state
proplist (currently [{public_keys, [...]}]) in the shape
envelope:verify_signature consumes.
Cfg reuses dispatch_http's :peer_url / :peer_url_fn resolution so
a single Cfg threads through both delivery (8f) and discovery (10c).
Server side: http_server.erl extended to serve the same MIME.
- accept_format/1 matches application/vnd.fed-sx.actor-doc first
via the new actor_doc_prefix/0 — content negotiation atom is
`actor_doc`.
- content_type_for(actor_doc) emits the MIME on outbound.
- actor_doc_response_for/3 kernel-aware arm: with kernel + actor
-> 200 + term_codec:encode of nx_kernel:state_for/1 result.
Unknown actor -> not_found_response/0. Other formats fall
through to the existing /2 stub variants.
- actor_get/3 route dispatch threads Cfg to the /3 arm.
Port quirks documented:
* This Erlang doesn't support Mod:Fun(X) dispatch on a variable
module — kernel_actor_state/2 hardcodes nx_kernel; the Cfg
:kernel field is just a "no kernel wired" -> nil flag.
* nx_kernel:actor_state/1 is the LEGACY single-bucket accessor
that takes State (not ActorId); the server-side variant we
want is state_for/1 (gen_server:call wrapper). Easy mismatch,
documented in the comment.
Outcome mapping:
2xx + decodable body -> {ok, AS}
2xx + bad body -> {error, bad_actor_doc}
non-2xx -> {error, {status, N}}
resolver miss -> {error, no_peer_url}
transport -> {error, Reason} (BIF re-raises)
Test: next/tests/discovery_fetch.sh 11/11
Server side (in-process via http_server:actor_doc_response_for):
- Accept negotiation
- kernel + actor -> 200 + decodable body w/ :public_keys
- unknown actor -> 404
Closure side (live HTTP against background python stub returning
hand-crafted term_codec bytes):
- URL construction <base>/actors/X
- fetch live -> {ok, AS}
- make_fetch_fn closure -> {ok, AS} via static :peer_url map
- missing peer -> {error, no_peer_url}
- 404 path -> {error, {status, 404}}
- peer_actors:lookup_or_fetch/3 caches the result
Test setup note: Python term_codec encoder uses ELEMENT COUNT
(not byte length) for l/t headers — see encode/1 in term_codec.erl
which does integer_to_list(length(T)). Easy bug, documented in the
test's python source.
No-regression gates green: Erlang conformance 761/761,
httpc_request 10/10, dispatch_http 10/10, http_listen_bif 5/5,
peer_actors 19/19, discovery 12/12, http_accept 13/13,
http_actors 13/13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.6 KiB
Erlang
90 lines
3.6 KiB
Erlang
-module(discovery_fetch).
|
|
-export([make_fetch_fn/1,
|
|
fetch/2,
|
|
actor_doc_url/2,
|
|
decode_body/1,
|
|
accept_header/0]).
|
|
|
|
%% Live peer-actor-doc fetch for peer_actors — Step 10c per design
|
|
%% §13.6. The peer_actors gen_server already exposes
|
|
%% lookup_or_fetch_srv/2(PeerId, FetchFn) where FetchFn is a
|
|
%% 1-arity closure that returns {ok, PeerAS} | {error, Reason} on
|
|
%% cache miss. For tests we wire a fake FetchFn that returns a
|
|
%% pre-baked AS; for live federation we wire the closure this
|
|
%% module produces — it GETs <base>/actors/<peer> with an Accept
|
|
%% header that asks for the actor_doc format
|
|
%% (http_server.erl Step 10c), decodes the response body via
|
|
%% term_codec, and returns the AS proplist.
|
|
%%
|
|
%% Cfg shape (reuses dispatch_http's peer URL resolution so a
|
|
%% single Cfg threads through both delivery and discovery):
|
|
%% {peer_url, [{PeerId, BaseUrl}, ...]}
|
|
%% {peer_url_fn, fun ((PeerId) -> {ok, BaseUrl} | not_found)}
|
|
%%
|
|
%% BaseUrl shape: <<"http://host:port">> (no trailing slash; this
|
|
%% module appends the path). PeerId is the actor atom.
|
|
%%
|
|
%% Outcomes:
|
|
%% 2xx + decodable body -> {ok, PeerAS}
|
|
%% 2xx + bad body -> {error, bad_actor_doc}
|
|
%% non-2xx -> {error, {status, N}}
|
|
%% resolver miss -> {error, no_peer_url}
|
|
%% transport -> {error, Reason}
|
|
%%
|
|
%% Cache write semantics live in peer_actors:lookup_or_fetch/3 —
|
|
%% successful fetches store; errors do NOT poison so callers can
|
|
%% retry on transients.
|
|
|
|
%% ── Accept header ────────────────────────────────────────────
|
|
%% "application/vnd.fed-sx.actor-doc" — same MIME the http_server
|
|
%% content_type_for(actor_doc) emits, so the Accept negotiation
|
|
%% in accept_format/1 routes the peer's response to the term_codec
|
|
%% serializer arm.
|
|
accept_header() ->
|
|
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
118,110,100,46,102,101,100,45,115,120,46,
|
|
97,99,116,111,114,45,100,111,99>>.
|
|
|
|
%% ── public API ───────────────────────────────────────────────
|
|
|
|
make_fetch_fn(Cfg) ->
|
|
fun (PeerId) ->
|
|
case dispatch_http:resolve_peer_url(PeerId, Cfg) of
|
|
{error, R} -> {error, R};
|
|
{ok, BaseUrl} -> fetch(actor_doc_url(BaseUrl, PeerId), Cfg)
|
|
end
|
|
end.
|
|
|
|
fetch(Url, _Cfg) ->
|
|
AcceptKey = <<97,99,99,101,112,116>>, % "accept"
|
|
Headers = [{AcceptKey, accept_header()}],
|
|
try httpc:request(Url, get, Headers, <<>>) of
|
|
{ok, Status, _H, Body} when Status >= 200, Status < 300 ->
|
|
decode_body(Body);
|
|
{ok, Status, _H, _B} ->
|
|
{error, {status, Status}};
|
|
Other ->
|
|
{error, {bad_response, Other}}
|
|
catch
|
|
error:Reason -> {error, Reason}
|
|
end.
|
|
|
|
%% actor_doc_url/2 — <BaseUrl>/actors/<peer>. PeerId is the actor
|
|
%% atom; rendered to a binary via its name (matches the same path
|
|
%% layout http_server.erl uses for the route registration at
|
|
%% prefix "/actors/").
|
|
actor_doc_url(BaseUrl, PeerId) when is_atom(PeerId) ->
|
|
PeerBin = list_to_binary(atom_to_list(PeerId)),
|
|
%% "/actors/" — 8 bytes
|
|
Prefix = <<47,97,99,116,111,114,115,47>>,
|
|
<<BaseUrl/binary, Prefix/binary, PeerBin/binary>>.
|
|
|
|
%% decode_body/1 — round the wire body back through term_codec.
|
|
%% Returns {ok, AS} on a proplist-shaped decode (matching the
|
|
%% peer-actor-state schema), {error, bad_actor_doc} otherwise.
|
|
decode_body(Body) ->
|
|
case term_codec:decode(Body) of
|
|
{ok, AS, _} when is_list(AS) -> {ok, AS};
|
|
_ -> {error, bad_actor_doc}
|
|
end.
|