Skip to content

router-perf@1.0 — Erlang Execution Device for Performance-Based Routing#741

Open
Lucifer0x17 wants to merge 22 commits intoneo/edgefrom
feat/permissionless-id-fetch
Open

router-perf@1.0 — Erlang Execution Device for Performance-Based Routing#741
Lucifer0x17 wants to merge 22 commits intoneo/edgefrom
feat/permissionless-id-fetch

Conversation

@Lucifer0x17
Copy link

Continuation of #737

Summary

Introduces dev_router_perf.erl, an Erlang execution device for process@1.0 that provides performance-based routing with automatic weight adjustment. It replaces dynamic-router.lua for Arweave gateway routing, supporting match/with route format — Arweave gateways use match/with instead of prefix/price/topup

How It Works

The device runs as a process@1.0 node process. The feedback loop:

           ┌─────────────────────────────────────────────────┐
           │                                                 │
           ▼                                                 │
   HTTP Request arrives                                      │
           │                                                 │
           ▼                                                 │
   dev_router:route                                          │
     └─ load_routes via provider ──► GET /perf-router~       │
        node-process@1.0/now/routes                          │
     └─ choose(N, "By-Weight") uses weights                  │
     └─ apply_route injects http_reference                   │
           │                                                 │
           ▼                                                 │
   hb_http_client fans out to selected gateways              │
           │                                                 │
           ▼                                                 │
   Response completes                                        │
     └─ record_duration → maybe_invoke_monitor               │
     └─ POST to /perf-router~node-process@1.0/schedule       │
        with {duration, reference, ...}                      │
           │                                                 │
           ▼                                                 │
   dev_router_perf:compute                                   │
     └─ duration/3 updates node performance via EMA ─────────┘
        new_perf = old * (1 - 1/period) + duration * (1/period)

Actions

Action Description
register Add a node to a route. Supports match/with and prefix formats. Sets http_reference for the feedback loop.
duration Update a node's performance score using exponential moving average (EMA). Matches node by http_reference.
recalculate Recompute weights for all nodes using decay-based percentile scoring. Called automatically after each register.

Configuration

Set these on the node process definition:

These are example values can be configured differently.

Key Default Description
performance-period 1000 EMA smoothing period. Lower = more responsive.
initial-performance 30000 Starting perf score (ms) for new nodes.
sampling-rate 0.1 Fraction of random sampling to prevent starvation.
performance-weight 1 Weight factor for performance in scoring.
pricing-weight 1 Weight factor for price in scoring.
score-preference 1 Decay exponent — higher values penalize slower nodes more aggressively.

Production Setup

1. Node configuration:

AdditionalOpts = #{
    http_monitor => #{
        <<"method">> => <<"POST">>,
        <<"path">> => <<"/perf-router~node-process@1.0/schedule">>
    },
    router_opts => #{
        <<"provider">> => #{
            <<"path">> => <<"/perf-router~node-process@1.0/now/routes">>
        }
    },
    node_processes => #{
        <<"perf-router">> => #{
            <<"device">> => <<"process@1.0">>,
            <<"execution-device">> => <<"router-perf@1.0">>,
            <<"scheduler-device">> => <<"scheduler@1.0">>,
            <<"performance-period">> => 6,
            <<"initial-performance">> => 30000
        }
    },
    routes => [
        #{<<"template">> => <<"^/arweave">>,
          <<"nodes">> => BootstrapNodes,
          <<"parallel">> => true,
          <<"admissible-status">> => 200}
    ]
}).

2. Register gateways (once, at startup or via admin script):

lists:foreach( fun(GatewayNode) ->
    Body = 
           hb_message:commit(#{
                  <<"action">> => <<"register">>,
                  <<"route">> => 
                          maps:merge(
                                   GatewayNode, 
                                   #{
                                          <<"template">> => <<"^/arweave">>,
                                          <<"parallel">> => true,
                                          <<"strategy">> => <<"Random">>,
                                          <<"choose">> => 10,
                                          <<"admissible-status">> => 200
                                    }
                           )
                  }, 
                  Opts
           ),
    hb_http:post(
         Node, 
         #{
              <<"path">> => <<"/perf-router~node-process@1.0/schedule">>,
              <<"method">> => <<"POST">>,
              <<"body">> => Body
         }, 
         Opts
    )
end, GatewayNodes).

3. Verify:

# Check routes and weights
curl "http://localhost:PORT/perf-router~node-process@1.0/now/routes?require-codec=application/json&accept-bundle=true"

# Check a specific node's performance score
curl "http://localhost:PORT/perf-router~node-process@1.0/now/routes/1/nodes/1/performance"

samcamwilliams and others added 10 commits March 4, 2026 04:55
Arweave v2 TXIDs are SHA-256(signature), which differs from the unsigned
  content hash used by HyperBEAM messages. Add a fallback in is_tx_admissible
  that checks the commitment-based ID via hb_message:id(CommittedMsg, all, Opts)
  when the unsigned content hash doesn't match.
…ed routing

Introduces dev_router_perf.erl, an Erlang execution device for process@1.0 that replaces dynamic-router.lua for Arweave gateway routing. Fixes gaps in the Lua version: supports match/with route format.

Key functionality:
   - register: adds nodes to routes with http_reference for the feedback loop
   - duration: updates node performance via exponential weighted average (EMA)
   - recalculate: recomputes weights using decay-based percentile scoring
   - Supports both match/with and prefix

Registers router-perf@1.0 in hb_opts preloaded_devices.

Comments added in the file by: Claude Opus 4.6 <noreply@anthropic.com>
Add `is_admissible_hook_routed_test_` that validates the full perf-router feedback loop: gateway registration, TX fetch through routed stack, async monitor duration posts, and performance score updates via EMA.
@Lucifer0x17 Lucifer0x17 self-assigned this Mar 9, 2026
@Lucifer0x17 Lucifer0x17 added the enhancement New feature or request label Mar 9, 2026
@samcamwilliams
Copy link
Collaborator

Amazing! dynamic-router.lua takes a new, faster form 🫡

If it is like the original the registration process should just be either:

  1. Permissioned: Hard-coded in the node message on start. If it is like the Lua script you should just be able to literally put the starting routes in your local process. In this mode is-admissible would always return false.
  2. Permissionless: In this case, if the deployment is going to use the ~arweave@2.9/admissible-tx-response flow then there is no need to gate access at all. Anyone should be able to register on the /ID route.
    ...in either case, no need for the lists:foreach(...) custom code.

Congrats! Let's ship it and test a beta 🙂

Lightweight device that gates HTTP monitor invocations via 1-in-N probabilistic sampling. Reads `sample-rate` from the request message and rolls `rand:uniform(Rate) =:= 1`. If absent, all requests pass.
- Gate monitor invocations via monitor-sampler@1.0 when `sample-rate` is set in the http_monitor config
- When `is-wasm-process` is set, strip the reserved `path` key from the monitor body and move it to `monitor-type`.

opts example
---
``` json
  "http_monitor": {
    "device": "relay@1.0",
    "method": "POST",
    "peer": "http://localhost:9000",
    "path": "call",
    "relay-path": "/<process-id>/push",
    "commit-request": true,
    "sample-rate": 10,
    "is-wasm-process": true
  }
```
@Lucifer0x17
Copy link
Author

Lucifer0x17 commented Mar 15, 2026

@samcamwilliams
I have added the sampling based on die. the device which is just a function actually is called monitor-sampler@1.0 would have been better to create a lua but random is the issue commit for devicd is: 0240c01b9815f86240cc19339329997449530fc8

Okay so I tested it by spawning an aos process from an HB instace from edge running on port 9000 by running aos --scheduler tkmwEVXxO-3n4zSL9CfXCUeu9-k0Znw4vy6LMs4nmts --mainnet http://localhost:9000 test-rputer-15

And when i did 35 requests I did get around 4 msgs on the process:
image

Config i used was as following:

{
  "port": "8734",
  "relay_allow_commit_request": true,

  "on": {
    "request": [
      {
        "device": "router@1.0",
        "path": "preprocess",
        "template": "^/[a-zA-Z0-9_-]{43}$"
      }
    ]
  },
  "routes": [
    {
      "template": "^/[a-zA-Z0-9_-]{43}",
      "strategy": "Random",
      "choose": 2,
      "parallel": true,
      "nodes": [
        {
          "prefix": "https://blue.hyperbeam.zephyrdev.xyz",
          "opts": { "http_reference": "blue" }
        },
        {
          "prefix": "https://neo.hyperbeam.zephyrdev.xyz",
          "opts": { "http_reference": "neo" }
        },
        {
          "prefix": "https://neo2.hyperbeam.zephyrdev.xyz",
          "opts": { "http_reference": "neo2" }
        }
      ]
    }
  ],
  "http_monitor": {
    "device": "relay@1.0",
    "method": "POST",
    "peer": "http://localhost:9000",
    "path": "call",
    "relay-path": "/Slu1LZ-PNgaRq0_CsCyloSi9WQkmTwztetN1l3R3t1Y/push",
    "commit-request": true,
    "sample-rate": 10,
    "is-wasm-process": true
  }
}

}
),
to_message(Path, Method, best_response(Res), LogExtra, Opts).
case to_message(Path, Method, best_response(Res), LogExtra, Opts) of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raw path unfortunately doesn't emit the user's message as-is, so I don't think this change can work/is needed?

%% When the target is a WASM process (`is-wasm-process' in the monitor config),
%% the reserved `path' key is moved to `monitor-type' to avoid interfering
%% with AO-Core routing.
maybe_invoke_monitor(Details, Opts) ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on/http-client/response: UserPath

dev_hook:on(<<"http-client/response">>, MonitoringInfo, Opts)


%% @doc Check whether this request should be forwarded to the monitor.
%% If `sample-rate' is not set in the monitor config, all requests are forwarded.
should_forward(Monitor, Opts) ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be hook-implementation level:

on/http-client/response: ~chance/20/your/path/to/execute

%%% ~1-in-N requests will be forwarded. If absent, every request is forwarded.

info(_Base) ->
#{default => fun should_sample/3}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default should be...

handle(X, Base, _, Opts) ->
if roll(X) -> {ok, Base}; true -> {error, Base} end.

Change find/3 to use hb_util:deep_get instead of maps:get so that slash-separated hook names like <<"http-client/response">> resolve
to nested keys in the on config map
Add dev_chance.erl as a composable 1-in-N probabilistic gate using the 4-arity default handler pattern.
- Replace ~60 lines of custom monitor invocation (maybe_invoke_monitor, should_forward, do_invoke_monitor, sanitize_body) with a single `dev_hook:on(<<"http-client/response">>, ...)` call.

- Reorder relay-path priority in dev_relay:call so that the explicit relay-path key is checked before the generic path key, fixing a conflict where the handler's dispatch path shadowed the relay URL.
The multirequest-admissible check was in the generic request/5 function which also handles /raw paths that don't emit the user's message as-is. Moved the admissibility + tx-admissible hook logic into get_tx where it belongs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants