NebulaAPI.AST (NebulaAPI v0.5.0)

Copy Markdown View Source

AST macros for NebulaAPI.

Provides:

  • defapi - Define an API method with node targeting
  • on_nebula_nodes - Conditional compilation based on node
  • call_on_node - Unicast call with node selector
  • call_on_nodes - Multicast call with node selector
  • call_on_all_nodes - Multicast call on all nodes

Summary

Functions

call_on_all_nodes(opts_or_block)

(macro)

Multicast call on all available nodes.

Named alias of the selector-less call_on_nodes form: it targets every node serving the method (i.e. with a registered worker for it).

Examples

call_on_all_nodes do
  MyModule.api_method()
end

call_on_all_nodes timeout: 5000, strategy: :first do
  MyModule.api_method()
end

call_on_all_nodes(opts, list)

(macro)

call_on_node(opts)

(macro)

Unicast call - execute API calls on a specific node.

The selector must be written literally at the call site — one of:

  • A Nebula AST expression (like @api, &db)
  • A literal function that receives the nodes_info map and returns a single node atom
  • Omitted (the options-only form, or a literal nil) — "no restriction": the call routes to the first available worker, with the block's options still applying.

A variable or computed selector is a compile error (branch in Elixir and write a separate call_on_* per case). Distinct from omitting it, a selector function that returns nil means "nothing matched" — the call fails with {:nebula_error, {:no_worker_on_node, nil}}, it never widens the target.

Inside the block, the innermost explicit routing wins: a call carrying its own truthy node_selector:/multicast: trailing opts routes itself (the block's routing and options are ignored for that call), and a routing key explicitly set to nil opts the call out of the block, back to default routing.

Examples

# With Nebula expression
call_on_node @api do
  MyModule.api_method()
end

# With selector function
call_on_node fn nodes_info ->
  nodes_info
  |> Enum.filter(fn {_node, info} -> :worker in info.tags end)
  |> Enum.map(fn {node, _} -> node end)
  |> Enum.random()
end, timeout: 5000 do
  MyModule.api_method()
end

# Options only — no selector: any available worker, with these options.
# The semantic with_options, free of the trailing-opts positional gotcha.
call_on_node timeout: 30_000 do
  MyModule.api_method()
end

call_on_node(selector_or_nebula_ast, opts_or_block)

(macro)

call_on_node(selector_or_nebula_ast, opts, list)

(macro)

call_on_nodes(opts)

(macro)

Multicast call - execute API calls on multiple nodes.

The selector must be written literally at the call site — one of:

  • A Nebula AST expression (like @api, &db) - all matching nodes
  • A literal function that receives the nodes_info map and returns a list of node atoms
  • Omitted (the options-only form, or a literal nil) — "no restriction": the call fans out to every node serving the method (like call_on_all_nodes), with the block's options still applying.

A variable or computed selector is a compile error (branch in Elixir and write a separate call_on_* per case). Distinct from omitting it, a selector function that returns nil or [] means "nothing matched" — zero calls are made (:all returns [], :first returns {:nebula_error, :no_success, []}, :quorum fails :quorum_unreachable); it never widens the target.

Inside the block, the innermost explicit routing wins: a call carrying its own truthy node_selector:/multicast: trailing opts routes itself (the block's routing and options are ignored for that call), and a routing key explicitly set to nil or false opts the call out of the block, back to default routing — MyMod.f(x, multicast: false) inside a multicast block is a plain default call.

Examples

# With Nebula expression (calls all &db nodes)
call_on_nodes &db do
  MyModule.api_method()
end

# With selector function
call_on_nodes fn nodes_info ->
  nodes_info
  |> Enum.filter(fn {_node, info} -> :storage in info.tags end)
  |> Enum.map(fn {node, _} -> node end)
end, timeout: 5000, strategy: :all do
  MyModule.api_method()
end

# Options only — no selector: every node serving the method.
# `call_on_all_nodes` is the named alias of this form.
call_on_nodes strategy: :quorum, at_least: 2 do
  MyModule.api_method()
end

call_on_nodes(selector_or_nebula_ast, opts_or_block)

(macro)

call_on_nodes(selector_or_nebula_ast, opts, list)

(macro)

defapi(selectors_signature_and_do)

(macro)

Defines an API method with node targeting.

The method is compiled either as a local implementation or a remote stub depending on whether the current node matches the target nodes.

Node Selectors

Selectors are juxtaposed by a space (never commas, never a [list]):

  • @node - specific node (short or full name)
  • &tag - nodes with the tag
  • !@node / !&tag - negation
  • @a &b - combine by juxtaposition (e.g. @worker &gpu)
  • (no selector) - the body runs on every node (local everywhere)

Examples

defapi @api, get_user(id) do
  Repo.get(User, id)
end

defapi &db !@worker, find_podcast(slug) do
  MyApp.Repo.find_one(:db, "records", %{identifier: slug})
end

# No selector → available on every node, each returns its own data.
defapi get_node_health() do
  collect_runtime_info()
end

defapi(selector_or_signature, list)

(macro)

defapi(nebula_ast, fn_ast, list)

(macro)

on_nebula_nodes(selectors_and_opts)

(macro)

on_nebula_nodes(nebula_ast, opts)

(macro)

Conditional compilation based on node.

Just like if but for NebulaAPI. Only compiles the do block on matching nodes, otherwise compiles the else block (if provided).

Examples

on_nebula_nodes &db do
  # This code only exists on &db nodes
  use MyApp.Repo, otp_app: :my_app
end

on_nebula_nodes @api do
  # Code for @api nodes
else
  # Code for other nodes
end