yolo007wizard

yolo007wizard

Problem Statement:

I am trying to build an ‘action runner’ for judge json rules. In a nut shell, say a json defined rule matches with action string:

{
...rule
    "action": "collect_signature"
}

I’m trying to think of best architecture to run that ‘collect_signature’ code. It could be a module name or a script file. The intention is for rules to trigger actions in a dynamic way so actions are easy to maintain separately.

Desired Requirements:

  • the action code has its own dependencies/hex packages
  • adding new actions does not require app reload/recompile/redeploy of the ‘action runner’ i.e. the actions are dynamically loaded and run somehow.

Idea 1:

standalone exs scripts - Actions are defined in standalone exs files with their own Mix.installs. I tried this already by using Code.require_file/1 to dynamically run an exs file however ran into dependency errors. The ‘action runner’ runtime dependencies had conflicts with the loaded exs dependencies. Need a way to get around that. This seems the most promising however.

Idea 2:

Umbrella project where each action is a separate application. I don’t have experience with umbrella projects but this might be a feasible path. It seems a bit overkill however and I think the action runner would have to be reloaded every time a new action is created (which is a big con).

Idea 3:

Similar to umbrella, load actions as private local packages via a dependency path. The ‘action runner’ would then dynamically load dependencies in its mix.exs. Sudo code like:

defp deps do
    for each action_name in actions folder return:
        {:action_name, ">= 0.0.0", path: "/actions/action_name"}
end

Then maybe add a Mix task ‘new action’ that loads up the boiler plate for a new action in the correct folder. The main con with this is same as #2 - the action runner would have to be recompiled with every new action.

Showing Posts 1 to 10

derek-zhou

derek-zhou

Or you can do what Livebook does. Use a separate BEAM node.

yolo007wizard

yolo007wizard OP

Ok cool I tried that out and still getting dependency errors. Do nodes have their own isolated dependencies?

I have a simple test setup. A file called ‘action_test.exs’ contains:

Mix.install([
  {:req, "~> 0.4.1"}
])

Req.get!("https://hex.pm/api/packages/req").body["meta"]["description"]
|> IO.inspect()

Then from the ‘acion runner’ I have tried:

Code.require_file(Path.join(__DIR__, "action_test.exs")

and

Node.spawn(:node1@localhost, Code.require_file(Path.join(__DIR__, "action_test.exs")))

but getting error:

** (Mix.Error) Mix.install/2 can only be called with the same dependencies in the given VM
(mix 1.15.2) lib/mix.ex:577: Mix.raise/2

al2o3cr

al2o3cr

Not sure if it’s the problem, but note that Node.spawn expects a function as its second argument and you’re trying to pass the result of Code.require_file there instead. Maybe you need a wrapping fn -> ... end?

yolo007wizard

yolo007wizard OP

ah nice catch. I retried with:

Node.spawn(:node1@localhost, fn -> Code.require_file(Path.join(__DIR__, "action_test.exs")) end)

And its running now but with a warning:
** Can not start :erlang::apply,[function<43.125776118/0 in :erl_eval.expr/6>, ] on :node1@localhost **

In order to see output I tweaked action_test.exs to be:

Mix.install([
  {:req, "~> 0.4.1"}
])

msg = Req.get!("https://hex.pm/api/packages/req").body["meta"]["description"]

File.write(Path.join(__DIR__, "action_test.txt"), msg)

After re-running

Node.spawn(:node1@localhost, fn -> Code.require_file(Path.join(__DIR__, "action_test.exs")) end)

I see no output :smiling_face_with_tear: so not sure whats wrong

al2o3cr

al2o3cr

That message comes from an internal Erlang function crasher:

https://github.com/erlang/otp/blob/7b164938ec6d8c8889417d51fe0e6b1a90cb7d39/erts/preloaded/src/erts_internal.erl#L1008

When spawn_opt can’t connect to the destination node to spawn the requested process, it spawns crasher instead and returns that PID:

https://github.com/erlang/otp/blob/7b164938ec6d8c8889417d51fe0e6b1a90cb7d39/erts/preloaded/src/erlang.erl#L3492-L3496

How are you creating node1@localhost and connecting to it?

D4no0

D4no0

Node.spawn/2 is a low-level function, try instead :erpc.call/2, that will handle redirection of errors and output.

yolo007wizard

yolo007wizard OP

I’m running tests from a livebook:

Node.spawn(:node1@localhost, fn -> Code.require_file(Path.join(__DIR__, "action_test.exs")) end)

No luck

I also tried:

Node.start(:node1@localhost)
Node.set_cookie(:foo)

Node.connect(:node2@localhost)
Node.spawn(:node2@localhost, fn -> Code.require_file(Path.join(__DIR__, "action_test.exs")) end)

still no luck

yolo007wizard

yolo007wizard OP

Ok I gave up on nodes as I’m not very confident with them. Not sure how I would prodify that either.

Another idea I didn’t think of is dead simple:

System.cmd("elixir", [Path.join(__DIR__, "action_test.exs")])

Getting back to the desired requirements this could work. Put all the action scripts in a cloud directory. Then the action runner checks if script exists before running it.

axelson

axelson

Scenic Core Team

When I tried running multiple notes before I used the Erlang :peer module.

Here’s a few snippets from a semi-defunct unreleased project I have:

    {:ok, node} =
      :peer.start_link(%{
        host: :localhost,
        name: 'peer_node',
        args: peer_args()
      })

    IO.puts("DONE Starting peer node!")
    add_code_paths(node)
    ensure_applications_started(node)
  defp add_code_paths(node) do
    rpc(node, :code, :add_paths, [:code.get_path()])
  end

  @apps_to_start [
    # :iex,
    :logger,
    # :file_system,
    # :jason,
    :erlexec,
    :runtime_tools,
    # :inets,
    :stdlib,
    :crypto,
    # :hex,
    :elixir,
    # :public_key,
    # :gviz,
    # :mix,
    # :gettext,
    :kernel,
    :ssl,
    :compiler
    # :asn1,
  ]

  defp ensure_applications_started(node) do
    rpc(node, Application, :ensure_all_started, [:mix])
    rpc(node, Mix, :env, [Mix.env()])

    # for {app_name, _, _} <- Application.loaded_applications() do
    #   rpc(node, Application, :ensure_all_started, [app_name])
    # end
    for app_name <- @apps_to_start do
      IO.inspect(app_name, label: "starting app_name")

      rpc(node, Application, :ensure_all_started, [app_name])
      |> IO.inspect(label: "started #{inspect(app_name)}")
    end
  end

  defp rpc(node, module, function, args) do
    :rpc.block_call(node, module, function, args)
  end

  defp peer_args do
    Enum.join(
      [
        "-loader inet -hosts 127.0.0.1",
        "-setcookie #{:erlang.get_cookie()}",
        # "-env MIX_BUILD_ROOT /tmp/gviz_build"
      ],
      " "
    )
    |> to_charlist()
  end
yolo007wizard

yolo007wizard OP

Ok I gave it a spin with a module wrapper like so:

defmodule PeerNode do

  def start(name) do
    {:ok, node} =
      :peer.start_link(%{
        host: :localhost,
        name: name,
        args: peer_args()
      })

    IO.puts("DONE Starting peer node!")
    add_code_paths(node)
    ensure_applications_started(node)
  end

  def peer_args do
    Enum.join(
      [
        "-loader inet -hosts 127.0.0.1",
        "-setcookie #{:erlang.get_cookie()}",
        # "-env MIX_BUILD_ROOT /tmp/gviz_build"
      ],
      " "
    )
    |> to_charlist()
  end

  defp rpc(node, module, function, args) do
    :rpc.block_call(node, module, function, args)
  end

  defp add_code_paths(node) do
    rpc(node, :code, :add_paths, [:code.get_path()])
  end

  @apps_to_start [
    # :iex,
    :logger,
    # :file_system,
    # :jason,
    :erlexec,
    :runtime_tools,
    # :inets,
    :stdlib,
    :crypto,
    # :hex,
    :elixir,
    # :public_key,
    # :gviz,
    # :mix,
    # :gettext,
    :kernel,
    :ssl,
    :compiler
    # :asn1,
  ]

  defp ensure_applications_started(node) do
    rpc(node, Application, :ensure_all_started, [:mix])
    rpc(node, Mix, :env, [Mix.env()])

    # for {app_name, _, _} <- Application.loaded_applications() do
    #   rpc(node, Application, :ensure_all_started, [app_name])
    # end
    for app_name <- @apps_to_start do
      IO.inspect(app_name, label: "starting app_name")

      rpc(node, Application, :ensure_all_started, [app_name])
      |> IO.inspect(label: "started #{inspect(app_name)}")
    end
  end
end

Then I ran:

PeerNode.start("first_peer_node")
Node.list(:hidden)

But its complaining about :peer args:

** (ErlangError) Erlang error: {:invalid_arg, 45}
(stdlib 5.0.2) peer.erl:563: :peer.“-verify_args/1-lc$^0/1-0-”/1
(stdlib 5.0.2) peer.erl:563: :peer.verify_args/1
(stdlib 5.0.2) peer.erl:626: :peer.start_it/2

For debugging I tried both with no luck:

:peer.start_link(%{
        host: :localhost,
        name: name,
        args: ["-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"]
      })
:peer.start_link(%{
        host: :localhost,
        name: name,
        args: ~c"-loader inet -hosts 127.0.0.1 -setcookie hardcoded_cookie"
      })

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
mnkhod
So i have been using ash framework for a while and i love it. However currently the issue im having with ash framework is the error handl...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews