this is archived repo. please, use the new one ergo
Implementation of Erlang/OTP node in Go
- Publish listen port via EPMD
- Embedded EPMD server
- Handle incoming connection from other node using Erlang Distribution Protocol
- Spawn Erlang-like processes
- Register and unregister processes with simple atom
- Send sync and async messages like
erlang:gen_call
anderlang:gen_cast
- Create own process with
GenServer
behaviour (likegen_server
in Erlang/OTP) - Atomic 'state' of GenServer
- Initiate connection to other node
- RPC callbacks
- Monitor processes
- Monitor nodes
- Support Erlang 21.*
- Go 1.10 and above
Ergonode has embedded EPMD implementation. It allows to run your nodes without erlang's empd dependency. There are two reason to activate embedded epmd:
- EPMD port is not taken during an ergonode initialization
- lost connection to the EPMD server
Current implementation has a bit different behaviour (from the original ones) - ergonode tryes to restore connection to EPMD server in case of its has been lost. At the same time ergonode tryes to start its own EPMD (as embedded set of goroutines) to serve all epmd-requests from the nodes.
You may want to use epmd as standalone application. There is simple drop-in replacement of epmd:
go get -u github.com/halturin/ergonode/cmd/epmd
Here is the changes of latest release. For more details see the ChangeLog
0.2.0 - 2019-02-25
- Now we make versioning releases
- Improve node creation. Now you can specify the listening port range. See 'Usage' for the details
- Add embedded EPMD.
type goGenServ struct {
ergonode.GenServer
completeChan chan bool
}
// listen from ListenRangeBegin ... ListenRangeEnd and use custom EPMD port
// n := ergonode.Create(NodeName, Cookie, uint16(ListenRangeBegin), uint16(ListenRangeEnd), uint16(EPMDPort))
//
// listen from ListenRangeBegin ... ListenRangeEnd with default EPMD port 4369
// n := ergonode.Create(NodeName, Cookie, uint16(ListenRangeBegin), uint16(ListenRangeEnd))
//
// listen from ListenRangeBegin ... 65000 with default EPMD port 4369
// n := ergonode.Create(NodeName, Cookie, uint16(ListenRangeBegin))
// use default listen port range: 15000...65000 and use default EPMD port 4369
Node := ergonode.Create("[email protected]", "SecretCookie")
completeChan := make(chan bool)
gs := new(goGenServ)
n.Spawn(gs, completeChan)
message := etf.Term(etf.Atom("hello"))
// gen_server:call({pname, 'node@address'} , hello) with default timeout 5 seconds
to := etf.Tuple{etf.Atom("pname"), etf.Atom("node@address")}
answer, err := gs.Call(to, message)
fmt.Printf("Got response: %v\n", answer)
// specify the custom calling timeout
// gen_server:call({pname, 'node@address'} , hello, 8)
answer, err := gs.Call(Pid, message, 12)
// it's also possible to call using Pid (etf.Pid)
answer, err := gs.Call(Pid, message)
// gen_server:cast({pname, 'node@address'} , hello)
to := etf.Tuple{etf.Atom("pname"), etf.Atom("node@address")}
gs.Cast(to, message)
// the same way using Pid
gs.Cast(Pid, message)
// simple sending message 'Pid ! hello'
gs.Send(Pid, message)
// to get pid like it does erlang:self()
gs.Self()
// set monitor. this gen_server will recieve the message (via HandleInfo) like
// {'DOWN',#Ref<0.0.13893633.237772>,process,<26194.4.1>, Reason})
// in case of remote process went down by some reason
gs.Monitor(Pid)
// *** http://erlang.org/doc/man/erlang.html#monitor_node-2
// *** Making several calls to monitor_node(Node, true) for the same Node is not an error;
// *** it results in as many independent monitoring instances.
// seting up node monitor (will recieve {nodedown, Nodename})
gs.MonitorNode(etf.Atom("node@address"), true)
// removing monitor
gs.MonitorNode(etf.Atom("node@address"), false)
/*
* Simple example how are handling incoming messages.
* Interface implementation
*/
// Init initializes process state using arbitrary arguments
func (gs *goGenServ) Init(args ...interface{}) (state interface{}) {
// Self-registration with name SrvName
gs.Node.Register(etf.Atom(SrvName), gs.Self)
return nil
}
// HandleCast serves incoming messages sending via gen_server:cast
// HandleCast -> (0, state) - noreply
// (-1, state) - normal stop (-2, -3 .... custom reasons to stop)
func (gs *goGenServ) HandleCast(message *etf.Term, state interface{}) (code int, stateout interface{}) {
return 0, state
}
// HandleCall serves incoming messages sending via gen_server:call
// HandleCall -> (1, reply, state) - reply
// (0, _, state) - noreply
// (-1, _, state) - normal stop (-2, -3 .... custom reasons to stop)
func (gs *goGenServ) HandleCall(from *etf.Tuple, message *etf.Term) (code int, reply *etf.Term, stateout interface{}) {
reply = etf.Term(etf.Atom("ok"))
return 1, &reply, state
}
// HandleInfo serves all another incoming messages (Pid ! message)
// HandleInfo -> (0, state) - noreply
// (-1, state) - normal stop (-2, -3 .... custom reasons to stop)
func (gs *goGenServ) HandleInfo(message *etf.Term, state interface{}) (code int, stateout interface{}) {
fmt.Printf("HandleInfo: %#v\n", *message)
return 0, state
}
// Terminate called when process died
func (gs *goGenServ) Terminate(reason int, state interface{}) {
fmt.Printf("Terminate: %#v\n", reason)
}
See examples/
for simple implementation of node and GenServer
process
Users of the Elixir Phoenix framework might encounter timeouts when trying to connect a Phoenix node to an ergonode node. The reason is that, in addition to global_name_server and net_kernel, Phoenix attemts to broadcast messages to the pg2 PubSub handler: https://hexdocs.pm/phoenix/1.1.0/Phoenix.PubSub.PG2.html
To work with Phoenix nodes, you must create and register a dedicated pg2 GenServer, and spawn it inside your node. Take inspiration from the global_name_server.go for the rest of the GenServer methods, but the Init must specify the "pg2" atom:
func (pg2 *pg2Server) Init(args ...interface{}) (state interface{}) {
pg2.Node.Register(etf.Atom("pg2"), pg2.Self)
return nil
}